Keymaps as a clean architecture [WIP]
This commit is contained in:
parent
a88324acd9
commit
efc48dc742
15 changed files with 620 additions and 88 deletions
|
@ -1,11 +1,11 @@
|
|||
import * as dom from '../../../shared/utils/dom';
|
||||
import * as keys from '../../../shared/utils/keys';
|
||||
import * as dom from '../shared/utils/dom';
|
||||
import * as keys from '../shared/utils/keys';
|
||||
|
||||
const cancelKey = (e: KeyboardEvent): boolean => {
|
||||
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
|
||||
};
|
||||
|
||||
export default class InputComponent {
|
||||
export default class InputDriver {
|
||||
private pressed: {[key: string]: string} = {};
|
||||
|
||||
private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
|
||||
|
@ -23,7 +23,7 @@ export default class InputComponent {
|
|||
this.onKeyListeners.push(cb);
|
||||
}
|
||||
|
||||
onKeyPress(e: KeyboardEvent) {
|
||||
private onKeyPress(e: KeyboardEvent) {
|
||||
if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
|
||||
return;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export default class InputComponent {
|
|||
this.capture(e);
|
||||
}
|
||||
|
||||
onKeyDown(e: KeyboardEvent) {
|
||||
private onKeyDown(e: KeyboardEvent) {
|
||||
if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
|
||||
return;
|
||||
}
|
||||
|
@ -39,12 +39,12 @@ export default class InputComponent {
|
|||
this.capture(e);
|
||||
}
|
||||
|
||||
onKeyUp(e: KeyboardEvent) {
|
||||
private onKeyUp(e: KeyboardEvent) {
|
||||
delete this.pressed[e.key];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-statements
|
||||
capture(e: KeyboardEvent) {
|
||||
private capture(e: KeyboardEvent) {
|
||||
let target = e.target;
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
|
@ -71,7 +71,7 @@ export default class InputComponent {
|
|||
}
|
||||
}
|
||||
|
||||
fromInput(e: Element) {
|
||||
private fromInput(e: Element) {
|
||||
return e instanceof HTMLInputElement ||
|
||||
e instanceof HTMLTextAreaElement ||
|
||||
e instanceof HTMLSelectElement ||
|
11
src/content/client/BackgroundClient.ts
Normal file
11
src/content/client/BackgroundClient.ts
Normal file
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
23
src/content/client/FindMasterClient.ts
Normal file
23
src/content/client/FindMasterClient.ts
Normal file
|
@ -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,
|
||||
}), '*');
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import InputComponent from './input';
|
||||
import InputDriver from './../../InputDriver';
|
||||
import FollowComponent from './follow';
|
||||
import MarkComponent from './mark';
|
||||
import KeymapperComponent from './keymapper';
|
||||
|
@ -15,7 +15,7 @@ let settingUseCase = new SettingUseCase();
|
|||
|
||||
export default class Common {
|
||||
constructor(win: Window, store: any) {
|
||||
const input = new InputComponent(win.document.body);
|
||||
const input = new InputDriver(win.document.body);
|
||||
const follow = new FollowComponent();
|
||||
const mark = new MarkComponent(store);
|
||||
const keymapper = new KeymapperComponent(store);
|
||||
|
|
139
src/content/controllers/KeymapController.ts
Normal file
139
src/content/controllers/KeymapController.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,20 @@
|
|||
import TopContentComponent from './components/top-content';
|
||||
import FrameContentComponent from './components/frame-content';
|
||||
// import TopContentComponent from './components/top-content';
|
||||
// import FrameContentComponent from './components/frame-content';
|
||||
import consoleFrameStyle from './site-style';
|
||||
import { newStore } from './store';
|
||||
// import { newStore } from './store';
|
||||
import MessageListener from './MessageListener';
|
||||
import FindController from './controllers/FindController';
|
||||
import * as messages from '../shared/messages';
|
||||
import InputDriver from './InputDriver';
|
||||
import KeymapController from './controllers/KeymapController';
|
||||
import AddonEnabledUseCase from './usecases/AddonEnabledUseCase';
|
||||
import SettingUseCase from './usecases/SettingUseCase';
|
||||
import * as blacklists from '../shared/blacklists';
|
||||
|
||||
const store = newStore();
|
||||
// const store = newStore();
|
||||
|
||||
if (window.self === window.top) {
|
||||
new TopContentComponent(window, store); // eslint-disable-line no-new
|
||||
// new TopContentComponent(window, store); // eslint-disable-line no-new
|
||||
|
||||
let findController = new FindController();
|
||||
new MessageListener().onWebMessage((message: messages.Message) => {
|
||||
|
@ -24,9 +29,38 @@ if (window.self === window.top) {
|
|||
return undefined;
|
||||
});
|
||||
} else {
|
||||
new FrameContentComponent(window, store); // eslint-disable-line no-new
|
||||
// new FrameContentComponent(window, store); // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
let keymapController = new KeymapController();
|
||||
let inputDriver = new InputDriver(document.body);
|
||||
// inputDriver.onKey(key => followSlaveController.pressKey(key));
|
||||
// inputDriver.onKey(key => markController.pressKey(key));
|
||||
inputDriver.onKey(key => keymapController.press(key));
|
||||
|
||||
let style = window.document.createElement('style');
|
||||
style.textContent = consoleFrameStyle;
|
||||
window.document.head.appendChild(style);
|
||||
|
||||
// TODO move the following to a class
|
||||
const reloadSettings = async() => {
|
||||
let addonEnabledUseCase = new AddonEnabledUseCase();
|
||||
let settingUseCase = new SettingUseCase();
|
||||
|
||||
try {
|
||||
let current = await settingUseCase.reload();
|
||||
let disabled = blacklists.includes(
|
||||
current.blacklist, window.location.href,
|
||||
);
|
||||
if (disabled) {
|
||||
addonEnabledUseCase.disable();
|
||||
} else {
|
||||
addonEnabledUseCase.enable();
|
||||
}
|
||||
} catch (e) {
|
||||
// Sometime sendMessage fails when background script is not ready.
|
||||
console.warn(e);
|
||||
setTimeout(() => reloadSettings(), 500);
|
||||
}
|
||||
};
|
||||
reloadSettings();
|
||||
|
|
25
src/content/presenters/FocusPresenter.ts
Normal file
25
src/content/presenters/FocusPresenter.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
23
src/content/repositories/KeymapRepository.ts
Normal file
23
src/content/repositories/KeymapRepository.ts
Normal file
|
@ -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 = [];
|
||||
}
|
||||
}
|
20
src/content/usecases/FindSlaveUseCase.ts
Normal file
20
src/content/usecases/FindSlaveUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
15
src/content/usecases/FocusUseCase.ts
Normal file
15
src/content/usecases/FocusUseCase.ts
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
100
src/content/usecases/KeymapUseCase.ts
Normal file
100
src/content/usecases/KeymapUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
27
src/content/usecases/NavigateUseCase.ts
Normal file
27
src/content/usecases/NavigateUseCase.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
58
src/content/usecases/ScrollUseCase.ts
Normal file
58
src/content/usecases/ScrollUseCase.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
129
test/content/InputDriver.test.ts
Normal file
129
test/content/InputDriver.test.ts
Normal file
|
@ -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 a new issue