diff options
Diffstat (limited to 'src/content/components')
-rw-r--r-- | src/content/components/common/follow.ts | 79 | ||||
-rw-r--r-- | src/content/components/common/hint.ts | 33 | ||||
-rw-r--r-- | src/content/components/common/index.ts | 43 | ||||
-rw-r--r-- | src/content/components/common/input.ts | 46 | ||||
-rw-r--r-- | src/content/components/common/keymapper.ts | 8 | ||||
-rw-r--r-- | src/content/components/common/mark.ts | 2 | ||||
-rw-r--r-- | src/content/components/top-content/find.ts | 25 | ||||
-rw-r--r-- | src/content/components/top-content/follow-controller.ts | 65 | ||||
-rw-r--r-- | src/content/components/top-content/index.ts | 28 |
9 files changed, 210 insertions, 119 deletions
diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index 63ce603..67f2dd9 100644 --- a/src/content/components/common/follow.ts +++ 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.ts b/src/content/components/common/hint.ts index 1472587..2fcbb0f 100644 --- a/src/content/components/common/hint.ts +++ 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.ts b/src/content/components/common/index.ts index bcab4fa..9b5164e 100644 --- a/src/content/components/common/index.ts +++ b/src/content/components/common/index.ts @@ -2,33 +2,37 @@ 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 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 blacklists from '../../../shared/blacklists'; +import * as keys from '../../../shared/utils/keys'; export default class Common { - constructor(win, store) { - const input = new InputComponent(win.document.body, store); - const follow = new FollowComponent(win, store); + 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(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)); + 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.prevEnabled = undefined; - this.prevBlacklist = undefined; this.reloadSettings(); - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onBackgroundMessage(this.onMessage.bind(this)); } - onMessage(message) { + onMessage(message: messages.Message) { let { enabled } = this.store.getState().addon; switch (message.type) { case messages.SETTINGS_CHANGED: @@ -40,12 +44,13 @@ export default class Common { 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)); - }); + this.store.dispatch(settingActions.load()) + .then(({ value: settings }: any) => { + 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); diff --git a/src/content/components/common/input.ts b/src/content/components/common/input.ts index eefaf10..64eb5f3 100644 --- a/src/content/components/common/input.ts +++ 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; } @@ -63,13 +72,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.ts b/src/content/components/common/keymapper.ts index ec0d093..d9c9834 100644 --- a/src/content/components/common/keymapper.ts +++ b/src/content/components/common/keymapper.ts @@ -1,7 +1,7 @@ -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) => { if (mapping.length < keys.length) { diff --git a/src/content/components/common/mark.ts b/src/content/components/common/mark.ts index 0f838a9..500d03b 100644 --- a/src/content/components/common/mark.ts +++ b/src/content/components/common/mark.ts @@ -3,7 +3,7 @@ import * as scrolls from 'content/scrolls'; import * as consoleFrames from 'content/console-frames'; import * as properties from 'shared/settings/properties'; -const cancelKey = (key) => { +const cancelKey = (key): boolean => { return key.key === 'Esc' || key.key === '[' && key.ctrlKey; }; diff --git a/src/content/components/top-content/find.ts b/src/content/components/top-content/find.ts index 4d46d79..74b95bc 100644 --- a/src/content/components/top-content/find.ts +++ b/src/content/components/top-content/find.ts @@ -1,15 +1,17 @@ -import * as findActions from 'content/actions/find'; -import messages from 'shared/messages'; +import * as findActions from '../../actions/find'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; export default class FindComponent { - constructor(win, store) { - this.win = win; + private store: any; + + constructor(store: any) { this.store = store; - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onMessage.bind(this)); } - onMessage(message) { + onMessage(message: messages.Message) { switch (message.type) { case messages.CONSOLE_ENTER_FIND: return this.start(message.text); @@ -20,22 +22,25 @@ export default class FindComponent { } } - start(text) { + start(text: string) { let state = this.store.getState().find; if (text.length === 0) { - return this.store.dispatch(findActions.next(state.keyword, true)); + return this.store.dispatch( + findActions.next(state.keyword as string, true)); } return this.store.dispatch(findActions.next(text, true)); } next() { let state = this.store.getState().find; - return this.store.dispatch(findActions.next(state.keyword, false)); + return this.store.dispatch( + findActions.next(state.keyword as string, false)); } prev() { let state = this.store.getState().find; - return this.store.dispatch(findActions.prev(state.keyword, false)); + return this.store.dispatch( + findActions.prev(state.keyword as string, false)); } } diff --git a/src/content/components/top-content/follow-controller.ts b/src/content/components/top-content/follow-controller.ts index 7f36604..be71f6e 100644 --- a/src/content/components/top-content/follow-controller.ts +++ b/src/content/components/top-content/follow-controller.ts @@ -1,30 +1,46 @@ -import * as followControllerActions from 'content/actions/follow-controller'; -import messages from 'shared/messages'; -import HintKeyProducer from 'content/hint-key-producer'; -import * as properties from 'shared/settings/properties'; +import * as followControllerActions from '../../actions/follow-controller'; +import * as messages from '../../../shared/messages'; +import MessageListener, { WebMessageSender } from '../../MessageListener'; +import HintKeyProducer from '../../hint-key-producer'; +import * as properties from '../../../shared/settings/properties'; -const broadcastMessage = (win, message) => { +const broadcastMessage = (win: Window, message: messages.Message): void => { let json = JSON.stringify(message); - let frames = [window.self].concat(Array.from(window.frames)); + let frames = [win.self].concat(Array.from(win.frames as any)); frames.forEach(frame => frame.postMessage(json, '*')); }; export default class FollowController { - constructor(win, store) { + private win: Window; + + private store: any; + + private state: { + enabled?: boolean; + newTab?: boolean; + background?: boolean; + keys?: string, + }; + + private keys: string[]; + + private producer: HintKeyProducer | null; + + constructor(win: Window, store: any) { this.win = win; this.store = store; this.state = {}; this.keys = []; this.producer = null; - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onMessage.bind(this)); store.subscribe(() => { this.update(); }); } - onMessage(message, sender) { + onMessage(message: messages.Message, sender: WebMessageSender) { switch (message.type) { case messages.FOLLOW_START: return this.store.dispatch( @@ -36,7 +52,7 @@ export default class FollowController { } } - update() { + update(): void { let prevState = this.state; this.state = this.store.getState().followController; @@ -49,8 +65,10 @@ export default class FollowController { } } - updateHints() { - let shown = this.keys.filter(key => key.startsWith(this.state.keys)); + updateHints(): void { + let shown = this.keys.filter((key) => { + return key.startsWith(this.state.keys as string); + }); if (shown.length === 1) { this.activate(); this.store.dispatch(followControllerActions.disable()); @@ -58,18 +76,18 @@ export default class FollowController { broadcastMessage(this.win, { type: messages.FOLLOW_SHOW_HINTS, - keys: this.state.keys, + keys: this.state.keys as string, }); } - activate() { + activate(): void { broadcastMessage(this.win, { type: messages.FOLLOW_ACTIVATE, - keys: this.state.keys, + keys: this.state.keys as string, }); } - keyPress(key, ctrlKey) { + keyPress(key: string, ctrlKey: boolean): boolean { if (key === '[' && ctrlKey) { this.store.dispatch(followControllerActions.disable()); return true; @@ -107,25 +125,28 @@ export default class FollowController { viewSize: { width: viewWidth, height: viewHeight }, framePosition: { x: 0, y: 0 }, }), '*'); - frameElements.forEach((element) => { - let { left: frameX, top: frameY } = element.getBoundingClientRect(); + frameElements.forEach((ele) => { + 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 }, }); - element.contentWindow.postMessage(message, '*'); + if (ele instanceof HTMLFrameElement && ele.contentWindow || + ele instanceof HTMLIFrameElement && ele.contentWindow) { + ele.contentWindow.postMessage(message, '*'); + } }); } - create(count, sender) { + create(count: number, sender: WebMessageSender) { let produced = []; for (let i = 0; i < count; ++i) { - produced.push(this.producer.produce()); + produced.push((this.producer as HintKeyProducer).produce()); } this.keys = this.keys.concat(produced); - sender.postMessage(JSON.stringify({ + (sender as Window).postMessage(JSON.stringify({ type: messages.FOLLOW_CREATE_HINTS, keysArray: produced, newTab: this.state.newTab, diff --git a/src/content/components/top-content/index.ts b/src/content/components/top-content/index.ts index 1aaef1b..ac95ea9 100644 --- a/src/content/components/top-content/index.ts +++ b/src/content/components/top-content/index.ts @@ -2,33 +2,43 @@ import CommonComponent from '../common'; import FollowController from './follow-controller'; import FindComponent from './find'; import * as consoleFrames from '../../console-frames'; -import messages from 'shared/messages'; -import * as scrolls from 'content/scrolls'; +import * as messages from '../../../shared/messages'; +import MessageListener from '../../MessageListener'; +import * as scrolls from '../../scrolls'; export default class TopContent { + private win: Window; - constructor(win, store) { + private store: any; + + constructor(win: Window, store: any) { this.win = win; this.store = store; new CommonComponent(win, store); // eslint-disable-line no-new new FollowController(win, store); // eslint-disable-line no-new - new FindComponent(win, store); // eslint-disable-line no-new + new FindComponent(store); // eslint-disable-line no-new // TODO make component consoleFrames.initialize(this.win.document); - messages.onMessage(this.onMessage.bind(this)); + new MessageListener().onWebMessage(this.onWebMessage.bind(this)); + new MessageListener().onBackgroundMessage( + this.onBackgroundMessage.bind(this)); } - onMessage(message) { - let addonState = this.store.getState().addon; - + onWebMessage(message: messages.Message) { switch (message.type) { case messages.CONSOLE_UNFOCUS: this.win.focus(); consoleFrames.blur(window.document); - return Promise.resolve(); + } + } + + onBackgroundMessage(message: messages.Message) { + let addonState = this.store.getState().addon; + + switch (message.type) { case messages.ADDON_ENABLED_QUERY: return Promise.resolve({ type: messages.ADDON_ENABLED_RESPONSE, |