aboutsummaryrefslogtreecommitdiff
path: root/src/content/components/common
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/components/common')
-rw-r--r--src/content/components/common/follow.ts (renamed from src/content/components/common/follow.js)79
-rw-r--r--src/content/components/common/hint.ts (renamed from src/content/components/common/hint.js)33
-rw-r--r--src/content/components/common/index.js55
-rw-r--r--src/content/components/common/index.ts61
-rw-r--r--src/content/components/common/input.ts (renamed from src/content/components/common/input.js)47
-rw-r--r--src/content/components/common/keymapper.ts (renamed from src/content/components/common/keymapper.js)36
-rw-r--r--src/content/components/common/mark.js74
-rw-r--r--src/content/components/common/mark.ts79
8 files changed, 267 insertions, 197 deletions
diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.ts
index 63ce603..67f2dd9 100644
--- a/src/content/components/common/follow.js
+++ b/src/content/components/common/follow.ts
@@ -1,6 +1,8 @@
-import messages from 'shared/messages';
+import MessageListener from '../../MessageListener';
import Hint from './hint';
-import * as dom from 'shared/utils/dom';
+import * as dom from '../../../shared/utils/dom';
+import * as messages from '../../../shared/messages';
+import * as keyUtils from '../../../shared/utils/keys';
const TARGET_SELECTOR = [
'a', 'button', 'input', 'textarea', 'area',
@@ -8,8 +10,22 @@ const TARGET_SELECTOR = [
'[role="button"]', 'summary'
].join(',');
+interface Size {
+ width: number;
+ height: number;
+}
+
+interface Point {
+ x: number;
+ y: number;
+}
-const inViewport = (win, element, viewSize, framePosition) => {
+const inViewport = (
+ win: Window,
+ element: Element,
+ viewSize: Size,
+ framePosition: Point,
+): boolean => {
let {
top, left, bottom, right
} = dom.viewportRect(element);
@@ -30,34 +46,44 @@ const inViewport = (win, element, viewSize, framePosition) => {
return true;
};
-const isAriaHiddenOrAriaDisabled = (win, element) => {
+const isAriaHiddenOrAriaDisabled = (win: Window, element: Element): boolean => {
if (!element || win.document.documentElement === element) {
return false;
}
for (let attr of ['aria-hidden', 'aria-disabled']) {
- if (element.hasAttribute(attr)) {
- let hidden = element.getAttribute(attr).toLowerCase();
+ let value = element.getAttribute(attr);
+ if (value !== null) {
+ let hidden = value.toLowerCase();
if (hidden === '' || hidden === 'true') {
return true;
}
}
}
- return isAriaHiddenOrAriaDisabled(win, element.parentNode);
+ return isAriaHiddenOrAriaDisabled(win, element.parentElement as Element);
};
export default class Follow {
- constructor(win, store) {
+ private win: Window;
+
+ private newTab: boolean;
+
+ private background: boolean;
+
+ private hints: {[key: string]: Hint };
+
+ private targets: HTMLElement[] = [];
+
+ constructor(win: Window) {
this.win = win;
- this.store = store;
this.newTab = false;
this.background = false;
this.hints = {};
this.targets = [];
- messages.onMessage(this.onMessage.bind(this));
+ new MessageListener().onWebMessage(this.onMessage.bind(this));
}
- key(key) {
+ key(key: keyUtils.Key): boolean {
if (Object.keys(this.hints).length === 0) {
return false;
}
@@ -69,7 +95,7 @@ export default class Follow {
return true;
}
- openLink(element) {
+ openLink(element: HTMLAreaElement|HTMLAnchorElement) {
// Browser prevent new tab by link with target='_blank'
if (!this.newTab && element.getAttribute('target') !== '_blank') {
element.click();
@@ -90,7 +116,7 @@ export default class Follow {
});
}
- countHints(sender, viewSize, framePosition) {
+ 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,
@@ -98,7 +124,7 @@ export default class Follow {
}), '*');
}
- createHints(keysArray, newTab, background) {
+ createHints(keysArray: string[], newTab: boolean, background: boolean) {
if (keysArray.length !== this.targets.length) {
throw new Error('illegal hint count');
}
@@ -113,7 +139,7 @@ export default class Follow {
}
}
- showHints(keys) {
+ 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))
@@ -128,18 +154,19 @@ export default class Follow {
this.targets = [];
}
- activateHints(keys) {
+ activateHints(keys: string) {
let hint = this.hints[keys];
if (!hint) {
return;
}
- let element = hint.target;
+ let element = hint.getTarget();
switch (element.tagName.toLowerCase()) {
case 'a':
+ return this.openLink(element as HTMLAnchorElement);
case 'area':
- return this.openLink(element);
+ return this.openLink(element as HTMLAreaElement);
case 'input':
- switch (element.type) {
+ switch ((element as HTMLInputElement).type) {
case 'file':
case 'checkbox':
case 'radio':
@@ -166,7 +193,7 @@ export default class Follow {
}
}
- onMessage(message, sender) {
+ onMessage(message: messages.Message, sender: any) {
switch (message.type) {
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
return this.countHints(sender, message.viewSize, message.framePosition);
@@ -178,19 +205,23 @@ export default class Follow {
case messages.FOLLOW_ACTIVATE:
return this.activateHints(message.keys);
case messages.FOLLOW_REMOVE_HINTS:
- return this.removeHints(message.keys);
+ return this.removeHints();
}
}
- static getTargetElements(win, viewSize, framePosition) {
+ static getTargetElements(
+ win: Window,
+ viewSize:
+ Size, framePosition: Point,
+ ): HTMLElement[] {
let all = win.document.querySelectorAll(TARGET_SELECTOR);
- let filtered = Array.prototype.filter.call(all, (element) => {
+ 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.type !== 'hidden' &&
+ (element as HTMLInputElement).type !== 'hidden' &&
element.offsetHeight > 0 &&
!isAriaHiddenOrAriaDisabled(win, element) &&
inViewport(win, element, viewSize, framePosition);
diff --git a/src/content/components/common/hint.js b/src/content/components/common/hint.ts
index 1472587..2fcbb0f 100644
--- a/src/content/components/common/hint.js
+++ b/src/content/components/common/hint.ts
@@ -1,6 +1,11 @@
-import * as dom from 'shared/utils/dom';
+import * as dom from '../../../shared/utils/dom';
-const hintPosition = (element) => {
+interface Point {
+ x: number;
+ y: number;
+}
+
+const hintPosition = (element: Element): Point => {
let { left, top, right, bottom } = dom.viewportRect(element);
if (element.tagName !== 'AREA') {
@@ -14,17 +19,21 @@ const hintPosition = (element) => {
};
export default class Hint {
- constructor(target, tag) {
- if (!(document.body instanceof HTMLElement)) {
- throw new TypeError('target is not an HTMLElement');
- }
+ private target: HTMLElement;
- this.target = target;
+ private element: HTMLElement;
+ constructor(target: HTMLElement, tag: string) {
let doc = target.ownerDocument;
+ if (doc === null) {
+ throw new TypeError('ownerDocument is null');
+ }
+
let { x, y } = hintPosition(target);
let { scrollX, scrollY } = window;
+ this.target = target;
+
this.element = doc.createElement('span');
this.element.className = 'vimvixen-hint';
this.element.textContent = tag;
@@ -35,15 +44,19 @@ export default class Hint {
doc.body.append(this.element);
}
- show() {
+ show(): void {
this.element.style.display = 'inline';
}
- hide() {
+ hide(): void {
this.element.style.display = 'none';
}
- remove() {
+ remove(): void {
this.element.remove();
}
+
+ getTarget(): HTMLElement {
+ return this.target;
+ }
}
diff --git a/src/content/components/common/index.js b/src/content/components/common/index.js
deleted file mode 100644
index bcab4fa..0000000
--- a/src/content/components/common/index.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import InputComponent from './input';
-import FollowComponent from './follow';
-import MarkComponent from './mark';
-import KeymapperComponent from './keymapper';
-import * as settingActions from 'content/actions/setting';
-import messages from 'shared/messages';
-import * as addonActions from '../../actions/addon';
-import * as blacklists from 'shared/blacklists';
-
-export default class Common {
- constructor(win, store) {
- const input = new InputComponent(win.document.body, store);
- const follow = new FollowComponent(win, store);
- const mark = new MarkComponent(win.document.body, store);
- const keymapper = new KeymapperComponent(store);
-
- input.onKey(key => follow.key(key));
- input.onKey(key => mark.key(key));
- input.onKey(key => keymapper.key(key));
-
- this.win = win;
- this.store = store;
- this.prevEnabled = undefined;
- this.prevBlacklist = undefined;
-
- this.reloadSettings();
-
- messages.onMessage(this.onMessage.bind(this));
- }
-
- onMessage(message) {
- let { enabled } = this.store.getState().addon;
- switch (message.type) {
- case messages.SETTINGS_CHANGED:
- return this.reloadSettings();
- case messages.ADDON_TOGGLE_ENABLED:
- this.store.dispatch(addonActions.setEnabled(!enabled));
- }
- }
-
- reloadSettings() {
- try {
- this.store.dispatch(settingActions.load()).then(({ value: settings }) => {
- let enabled = !blacklists.includes(
- settings.blacklist, this.win.location.href
- );
- this.store.dispatch(addonActions.setEnabled(enabled));
- });
- } catch (e) {
- // Sometime sendMessage fails when background script is not ready.
- console.warn(e);
- setTimeout(() => this.reloadSettings(), 500);
- }
- }
-}
diff --git a/src/content/components/common/index.ts b/src/content/components/common/index.ts
new file mode 100644
index 0000000..5b097b6
--- /dev/null
+++ b/src/content/components/common/index.ts
@@ -0,0 +1,61 @@
+import InputComponent from './input';
+import FollowComponent from './follow';
+import MarkComponent from './mark';
+import KeymapperComponent from './keymapper';
+import * as settingActions from '../../actions/setting';
+import * as messages from '../../../shared/messages';
+import MessageListener from '../../MessageListener';
+import * as addonActions from '../../actions/addon';
+import * as blacklists from '../../../shared/blacklists';
+import * as keys from '../../../shared/utils/keys';
+import * as actions from '../../actions';
+
+export default class Common {
+ private win: Window;
+
+ private store: any;
+
+ constructor(win: Window, store: any) {
+ const input = new InputComponent(win.document.body);
+ const follow = new FollowComponent(win);
+ const mark = new MarkComponent(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));
+
+ this.win = win;
+ this.store = store;
+
+ this.reloadSettings();
+
+ new MessageListener().onBackgroundMessage(this.onMessage.bind(this));
+ }
+
+ onMessage(message: messages.Message) {
+ let { enabled } = this.store.getState().addon;
+ switch (message.type) {
+ case messages.SETTINGS_CHANGED:
+ return this.reloadSettings();
+ case messages.ADDON_TOGGLE_ENABLED:
+ this.store.dispatch(addonActions.setEnabled(!enabled));
+ }
+ }
+
+ reloadSettings() {
+ try {
+ this.store.dispatch(settingActions.load())
+ .then((action: actions.SettingAction) => {
+ let enabled = !blacklists.includes(
+ action.settings.blacklist, this.win.location.href
+ );
+ this.store.dispatch(addonActions.setEnabled(enabled));
+ });
+ } catch (e) {
+ // Sometime sendMessage fails when background script is not ready.
+ console.warn(e);
+ setTimeout(() => this.reloadSettings(), 500);
+ }
+ }
+}
diff --git a/src/content/components/common/input.js b/src/content/components/common/input.ts
index eefaf10..1fe34c9 100644
--- a/src/content/components/common/input.js
+++ b/src/content/components/common/input.ts
@@ -1,12 +1,16 @@
-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) => {
+const cancelKey = (e: KeyboardEvent): boolean => {
return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
};
export default class InputComponent {
- constructor(target) {
+ private pressed: {[key: string]: string} = {};
+
+ private onKeyListeners: ((key: keys.Key) => boolean)[] = [];
+
+ constructor(target: HTMLElement) {
this.pressed = {};
this.onKeyListeners = [];
@@ -15,11 +19,11 @@ export default class InputComponent {
target.addEventListener('keyup', this.onKeyUp.bind(this));
}
- onKey(cb) {
+ onKey(cb: (key: keys.Key) => boolean) {
this.onKeyListeners.push(cb);
}
- onKeyPress(e) {
+ onKeyPress(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') {
return;
}
@@ -27,7 +31,7 @@ export default class InputComponent {
this.capture(e);
}
- onKeyDown(e) {
+ onKeyDown(e: KeyboardEvent) {
if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') {
return;
}
@@ -35,14 +39,19 @@ export default class InputComponent {
this.capture(e);
}
- onKeyUp(e) {
+ onKeyUp(e: KeyboardEvent) {
delete this.pressed[e.key];
}
- capture(e) {
- if (this.fromInput(e)) {
- if (cancelKey(e) && e.target.blur) {
- e.target.blur();
+ // eslint-disable-next-line max-statements
+ capture(e: KeyboardEvent) {
+ let target = e.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+ if (this.fromInput(target)) {
+ if (cancelKey(e) && target.blur) {
+ target.blur();
}
return;
}
@@ -52,7 +61,6 @@ export default class InputComponent {
}
let key = keys.fromKeyboardEvent(e);
-
for (let listener of this.onKeyListeners) {
let stop = listener(key);
if (stop) {
@@ -63,13 +71,10 @@ export default class InputComponent {
}
}
- fromInput(e) {
- if (!e.target) {
- return false;
- }
- return e.target instanceof HTMLInputElement ||
- e.target instanceof HTMLTextAreaElement ||
- e.target instanceof HTMLSelectElement ||
- dom.isContentEditable(e.target);
+ fromInput(e: Element) {
+ return e instanceof HTMLInputElement ||
+ e instanceof HTMLTextAreaElement ||
+ e instanceof HTMLSelectElement ||
+ dom.isContentEditable(e);
}
}
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.ts
index ec0d093..c94bae0 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.ts
@@ -1,9 +1,12 @@
-import * as inputActions from 'content/actions/input';
-import * as operationActions from 'content/actions/operation';
-import operations from 'shared/operations';
-import * as keyUtils from 'shared/utils/keys';
+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';
-const mapStartsWith = (mapping, keys) => {
+const mapStartsWith = (
+ mapping: keyUtils.Key[],
+ keys: keyUtils.Key[],
+): boolean => {
if (mapping.length < keys.length) {
return false;
}
@@ -16,26 +19,33 @@ const mapStartsWith = (mapping, keys) => {
};
export default class KeymapperComponent {
- constructor(store) {
+ private store: any;
+
+ constructor(store: any) {
this.store = store;
}
// eslint-disable-next-line max-statements
- key(key) {
+ key(key: keyUtils.Key): boolean {
this.store.dispatch(inputActions.keyPress(key));
let state = this.store.getState();
let input = state.input;
- let keymaps = new Map(state.setting.keymaps);
+ let keymaps = new Map<keyUtils.Key[], operations.Operation>(
+ state.setting.keymaps.map(
+ (e: {key: keyUtils.Key[], op: operations.Operation}) => [e.key, e.op],
+ )
+ );
- let matched = Array.from(keymaps.keys()).filter((mapping) => {
- return mapStartsWith(mapping, input.keys);
- });
+ let matched = Array.from(keymaps.keys()).filter(
+ (mapping: keyUtils.Key[]) => {
+ return mapStartsWith(mapping, input.keys);
+ });
if (!state.addon.enabled) {
// available keymaps are only ADDON_ENABLE and ADDON_TOGGLE_ENABLED if
// the addon disabled
matched = matched.filter((keys) => {
- let type = keymaps.get(keys).type;
+ let type = (keymaps.get(keys) as operations.Operation).type;
return type === operations.ADDON_ENABLE ||
type === operations.ADDON_TOGGLE_ENABLED;
});
@@ -47,7 +57,7 @@ export default class KeymapperComponent {
matched.length === 1 && input.keys.length < matched[0].length) {
return true;
}
- let operation = keymaps.get(matched[0]);
+ let operation = keymaps.get(matched[0]) as operations.Operation;
let act = operationActions.exec(
operation, state.setting, state.addon.enabled
);
diff --git a/src/content/components/common/mark.js b/src/content/components/common/mark.js
deleted file mode 100644
index 0f838a9..0000000
--- a/src/content/components/common/mark.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as markActions from 'content/actions/mark';
-import * as scrolls from 'content/scrolls';
-import * as consoleFrames from 'content/console-frames';
-import * as properties from 'shared/settings/properties';
-
-const cancelKey = (key) => {
- return key.key === 'Esc' || key.key === '[' && key.ctrlKey;
-};
-
-const globalKey = (key) => {
- return (/^[A-Z0-9]$/).test(key);
-};
-
-export default class MarkComponent {
- constructor(body, store) {
- this.body = body;
- this.store = store;
- }
-
- // eslint-disable-next-line max-statements
- key(key) {
- let { mark: markStage, setting } = this.store.getState();
- let smoothscroll = setting.properties.smoothscroll ||
- properties.defaults.smoothscroll;
-
- if (!markStage.setMode && !markStage.jumpMode) {
- return false;
- }
-
- if (cancelKey(key)) {
- this.store.dispatch(markActions.cancel());
- return true;
- }
-
- if (key.ctrlKey || key.metaKey || key.altKey) {
- consoleFrames.postError('Unknown mark');
- } else if (globalKey(key.key) && markStage.setMode) {
- this.doSetGlobal(key);
- } else if (globalKey(key.key) && markStage.jumpMode) {
- this.doJumpGlobal(key);
- } else if (markStage.setMode) {
- this.doSet(key);
- } else if (markStage.jumpMode) {
- this.doJump(markStage.marks, key, smoothscroll);
- }
-
- this.store.dispatch(markActions.cancel());
- return true;
- }
-
- doSet(key) {
- let { x, y } = scrolls.getScroll();
- this.store.dispatch(markActions.setLocal(key.key, x, y));
- }
-
- doJump(marks, key, smoothscroll) {
- if (!marks[key.key]) {
- consoleFrames.postError('Mark is not set');
- return;
- }
-
- let { x, y } = marks[key.key];
- scrolls.scrollTo(x, y, smoothscroll);
- }
-
- doSetGlobal(key) {
- let { x, y } = scrolls.getScroll();
- this.store.dispatch(markActions.setGlobal(key.key, x, y));
- }
-
- doJumpGlobal(key) {
- this.store.dispatch(markActions.jumpGlobal(key.key));
- }
-}
diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts
new file mode 100644
index 0000000..1237385
--- /dev/null
+++ b/src/content/components/common/mark.ts
@@ -0,0 +1,79 @@
+import * as markActions from '../../actions/mark';
+import * as scrolls from '../..//scrolls';
+import * as consoleFrames from '../..//console-frames';
+import * as keyUtils from '../../../shared/utils/keys';
+import Mark from '../../Mark';
+
+const cancelKey = (key: keyUtils.Key): boolean => {
+ return key.key === 'Esc' || key.key === '[' && Boolean(key.ctrlKey);
+};
+
+const globalKey = (key: string): boolean => {
+ return (/^[A-Z0-9]$/).test(key);
+};
+
+export default class MarkComponent {
+ private store: any;
+
+ constructor(store: any) {
+ this.store = store;
+ }
+
+ // eslint-disable-next-line max-statements
+ key(key: keyUtils.Key) {
+ let { mark: markState, setting } = this.store.getState();
+ let smoothscroll = setting.properties.smoothscroll;
+
+ if (!markState.setMode && !markState.jumpMode) {
+ return false;
+ }
+
+ if (cancelKey(key)) {
+ this.store.dispatch(markActions.cancel());
+ return true;
+ }
+
+ if (key.ctrlKey || key.metaKey || key.altKey) {
+ consoleFrames.postError('Unknown mark');
+ } else if (globalKey(key.key) && markState.setMode) {
+ this.doSetGlobal(key);
+ } else if (globalKey(key.key) && markState.jumpMode) {
+ this.doJumpGlobal(key);
+ } else if (markState.setMode) {
+ this.doSet(key);
+ } else if (markState.jumpMode) {
+ this.doJump(markState.marks, key, smoothscroll);
+ }
+
+ this.store.dispatch(markActions.cancel());
+ return true;
+ }
+
+ doSet(key: keyUtils.Key) {
+ let { x, y } = scrolls.getScroll();
+ this.store.dispatch(markActions.setLocal(key.key, x, y));
+ }
+
+ doJump(
+ marks: { [key: string]: Mark },
+ key: keyUtils.Key,
+ smoothscroll: boolean,
+ ) {
+ if (!marks[key.key]) {
+ consoleFrames.postError('Mark is not set');
+ return;
+ }
+
+ let { x, y } = marks[key.key];
+ scrolls.scrollTo(x, y, smoothscroll);
+ }
+
+ doSetGlobal(key: keyUtils.Key) {
+ let { x, y } = scrolls.getScroll();
+ this.store.dispatch(markActions.setGlobal(key.key, x, y));
+ }
+
+ doJumpGlobal(key: keyUtils.Key) {
+ this.store.dispatch(markActions.jumpGlobal(key.key));
+ }
+}