From fb8b4d28ce47171a83a7bf5148293fd2318cc02f Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Thu, 16 May 2019 21:35:58 +0900 Subject: [PATCH] Hints as a classes --- src/content/client/TabsClient.ts | 12 +- src/content/components/common/follow.ts | 79 +++------ src/content/components/common/hint.ts | 62 ------- src/content/presenters/Hint.ts | 127 ++++++++++++++ test/content/components/common/hint.test.ts | 57 ------- .../hint.html => presenters/Hint.test.html} | 0 test/content/presenters/Hint.test.ts | 158 ++++++++++++++++++ 7 files changed, 318 insertions(+), 177 deletions(-) delete mode 100644 src/content/components/common/hint.ts create mode 100644 src/content/presenters/Hint.ts delete mode 100644 test/content/components/common/hint.test.ts rename test/content/{components/common/hint.html => presenters/Hint.test.html} (100%) create mode 100644 test/content/presenters/Hint.test.ts diff --git a/src/content/client/TabsClient.ts b/src/content/client/TabsClient.ts index fe72e11..e1af078 100644 --- a/src/content/client/TabsClient.ts +++ b/src/content/client/TabsClient.ts @@ -1,18 +1,22 @@ import * as messages from '../../shared/messages'; export default interface TabsClient { - openUrl(url: string, newTab: boolean): Promise; + openUrl(url: string, newTab: boolean, background?: boolean): Promise; // eslint-disable-next-line semi } -export class TabsClientImpl { - async openUrl(url: string, newTab: boolean): Promise { +export class TabsClientImpl implements TabsClient { + async openUrl( + url: string, + newTab: boolean, + background?: boolean, + ): Promise { await browser.runtime.sendMessage({ type: messages.OPEN_URL, url, newTab, + background, }); } } - diff --git a/src/content/components/common/follow.ts b/src/content/components/common/follow.ts index 67f2dd9..a30a3d5 100644 --- a/src/content/components/common/follow.ts +++ b/src/content/components/common/follow.ts @@ -1,8 +1,11 @@ import MessageListener from '../../MessageListener'; -import Hint from './hint'; +import Hint, { LinkHint, InputHint } from '../../presenters/Hint'; import * as dom from '../../../shared/utils/dom'; import * as messages from '../../../shared/messages'; import * as keyUtils from '../../../shared/utils/keys'; +import TabsClient, { TabsClientImpl } from '../../client/TabsClient'; + +let tabsClient: TabsClient = new TabsClientImpl(); const TARGET_SELECTOR = [ 'a', 'button', 'input', 'textarea', 'area', @@ -95,27 +98,6 @@ export default class Follow { return true; } - openLink(element: HTMLAreaElement|HTMLAnchorElement) { - // Browser prevent new tab by link with target='_blank' - if (!this.newTab && element.getAttribute('target') !== '_blank') { - element.click(); - return; - } - - let href = element.getAttribute('href'); - - // eslint-disable-next-line no-script-url - if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) { - return; - } - return browser.runtime.sendMessage({ - type: messages.OPEN_URL, - url: element.href, - newTab: true, - background: this.background, - }); - } - countHints(sender: any, viewSize: Size, framePosition: Point) { this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); sender.postMessage(JSON.stringify({ @@ -134,8 +116,13 @@ export default class Follow { this.hints = {}; for (let i = 0; i < keysArray.length; ++i) { let keys = keysArray[i]; - let hint = new Hint(this.targets[i], keys); - this.hints[keys] = hint; + let target = this.targets[i]; + if (target instanceof HTMLAnchorElement || + target instanceof HTMLAreaElement) { + this.hints[keys] = new LinkHint(target, keys); + } else { + this.hints[keys] = new InputHint(target, keys); + } } } @@ -154,42 +141,26 @@ export default class Follow { this.targets = []; } - activateHints(keys: string) { + async activateHints(keys: string): Promise { let hint = this.hints[keys]; if (!hint) { return; } - let element = hint.getTarget(); - switch (element.tagName.toLowerCase()) { - case 'a': - return this.openLink(element as HTMLAnchorElement); - case 'area': - return this.openLink(element as HTMLAreaElement); - case 'input': - switch ((element as HTMLInputElement).type) { - case 'file': - case 'checkbox': - case 'radio': - case 'submit': - case 'reset': - case 'button': - case 'image': - case 'color': - return element.click(); - default: - return element.focus(); + + if (hint instanceof LinkHint) { + let url = hint.getLink(); + // ignore taget='_blank' + if (!this.newTab && hint.getLinkTarget() !== '_blank') { + hint.click(); + return; } - case 'textarea': - return element.focus(); - case 'button': - case 'summary': - return element.click(); - default: - if (dom.isContentEditable(element)) { - return element.focus(); - } else if (element.hasAttribute('tabindex')) { - return element.click(); + // eslint-disable-next-line no-script-url + if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) { + return; } + await tabsClient.openUrl(url, this.newTab, this.background); + } else if (hint instanceof InputHint) { + hint.activate(); } } diff --git a/src/content/components/common/hint.ts b/src/content/components/common/hint.ts deleted file mode 100644 index 2fcbb0f..0000000 --- a/src/content/components/common/hint.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as dom from '../../../shared/utils/dom'; - -interface Point { - x: number; - y: number; -} - -const hintPosition = (element: Element): Point => { - let { left, top, right, bottom } = dom.viewportRect(element); - - if (element.tagName !== 'AREA') { - return { x: left, y: top }; - } - - return { - x: (left + right) / 2, - y: (top + bottom) / 2, - }; -}; - -export default class Hint { - private target: HTMLElement; - - 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; - this.element.style.left = x + scrollX + 'px'; - this.element.style.top = y + scrollY + 'px'; - - this.show(); - doc.body.append(this.element); - } - - show(): void { - this.element.style.display = 'inline'; - } - - hide(): void { - this.element.style.display = 'none'; - } - - remove(): void { - this.element.remove(); - } - - getTarget(): HTMLElement { - return this.target; - } -} diff --git a/src/content/presenters/Hint.ts b/src/content/presenters/Hint.ts new file mode 100644 index 0000000..60c0f4c --- /dev/null +++ b/src/content/presenters/Hint.ts @@ -0,0 +1,127 @@ +import * as doms from '../../shared/utils/dom'; + +interface Point { + x: number; + y: number; +} + +const hintPosition = (element: Element): Point => { + let { left, top, right, bottom } = doms.viewportRect(element); + + if (element.tagName !== 'AREA') { + return { x: left, y: top }; + } + + return { + x: (left + right) / 2, + y: (top + bottom) / 2, + }; +}; + +export default abstract class Hint { + private hint: HTMLElement; + + private tag: string; + + constructor(target: HTMLElement, tag: string) { + this.tag = tag; + + let doc = target.ownerDocument; + if (doc === null) { + throw new TypeError('ownerDocument is null'); + } + + let { x, y } = hintPosition(target); + let { scrollX, scrollY } = window; + + let hint = doc.createElement('span'); + hint.className = 'vimvixen-hint'; + hint.textContent = tag; + hint.style.left = x + scrollX + 'px'; + hint.style.top = y + scrollY + 'px'; + + doc.body.append(hint); + + this.hint = hint; + this.show(); + } + + show(): void { + this.hint.style.display = 'inline'; + } + + hide(): void { + this.hint.style.display = 'none'; + } + + remove(): void { + this.hint.remove(); + } + + getTag(): string { + return this.tag; + } +} + +export class LinkHint extends Hint { + private target: HTMLAnchorElement | HTMLAreaElement; + + constructor(target: HTMLAnchorElement | HTMLAreaElement, tag: string) { + super(target, tag); + + this.target = target; + } + + getLink(): string { + return this.target.href; + } + + getLinkTarget(): string | null { + return this.target.getAttribute('target'); + } + + click(): void { + this.target.click(); + } +} + +export class InputHint extends Hint { + private target: HTMLElement; + + constructor(target: HTMLElement, tag: string) { + super(target, tag); + + this.target = target; + } + + activate(): void { + let target = this.target; + switch (target.tagName.toLowerCase()) { + case 'input': + switch ((target as HTMLInputElement).type) { + case 'file': + case 'checkbox': + case 'radio': + case 'submit': + case 'reset': + case 'button': + case 'image': + case 'color': + return target.click(); + default: + return target.focus(); + } + case 'textarea': + return target.focus(); + case 'button': + case 'summary': + return target.click(); + default: + if (doms.isContentEditable(target)) { + return target.focus(); + } else if (target.hasAttribute('tabindex')) { + return target.click(); + } + } + } +} diff --git a/test/content/components/common/hint.test.ts b/test/content/components/common/hint.test.ts deleted file mode 100644 index 42d571f..0000000 --- a/test/content/components/common/hint.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import Hint from 'content/components/common/hint'; - -describe('Hint class', () => { - beforeEach(() => { - document.body.innerHTML = __html__['test/content/components/common/hint.html']; - }); - - describe('#constructor', () => { - it('creates a hint element with tag name', () => { - let link = document.getElementById('test-link'); - let hint = new Hint(link, 'abc'); - expect(hint.element.textContent.trim()).to.be.equal('abc'); - }); - - it('throws an exception when non-element given', () => { - expect(() => new Hint(window, 'abc')).to.throw(TypeError); - }); - }); - - describe('#show', () => { - it('shows an element', () => { - let link = document.getElementById('test-link'); - let hint = new Hint(link, 'abc'); - hint.hide(); - hint.show(); - - expect(hint.element.style.display).to.not.equal('none'); - }); - }); - - describe('#hide', () => { - it('hides an element', () => { - let link = document.getElementById('test-link'); - let hint = new Hint(link, 'abc'); - hint.hide(); - - expect(hint.element.style.display).to.equal('none'); - }); - }); - - describe('#remove', () => { - it('removes an element', () => { - let link = document.getElementById('test-link'); - let hint = new Hint(link, 'abc'); - - expect(hint.element.parentElement).to.not.be.null; - hint.remove(); - expect(hint.element.parentElement).to.be.null; - }); - }); - - describe('#activate', () => { - // TODO test activations - }); -}); - - diff --git a/test/content/components/common/hint.html b/test/content/presenters/Hint.test.html similarity index 100% rename from test/content/components/common/hint.html rename to test/content/presenters/Hint.test.html diff --git a/test/content/presenters/Hint.test.ts b/test/content/presenters/Hint.test.ts new file mode 100644 index 0000000..7994788 --- /dev/null +++ b/test/content/presenters/Hint.test.ts @@ -0,0 +1,158 @@ +import AbstractHint, { LinkHint, InputHint } from '../../../src/content/presenters/Hint'; +import { expect } from 'chai'; + +class Hint extends AbstractHint { +} + +describe('Hint', () => { + beforeEach(() => { + document.body.innerHTML = `link`; + }); + + describe('#constructor', () => { + it('creates a hint element with tag name', () => { + let link = document.getElementById('test-link'); + let hint = new Hint(link, 'abc'); + + let elem = document.querySelector('.vimvixen-hint'); + expect(elem.textContent.trim()).to.be.equal('abc'); + }); + }); + + describe('#show', () => { + it('shows an element', () => { + let link = document.getElementById('test-link'); + let hint = new Hint(link, 'abc'); + hint.hide(); + hint.show(); + + let elem = document.querySelector('.vimvixen-hint') as HTMLElement; + expect(elem.style.display).to.not.equal('none'); + }); + }); + + describe('#hide', () => { + it('hides an element', () => { + let link = document.getElementById('test-link') as HTMLElement; + let hint = new Hint(link, 'abc'); + hint.hide(); + + let elem = document.querySelector('.vimvixen-hint') as HTMLElement; + expect(elem.style.display).to.equal('none'); + }); + }); + + describe('#remove', () => { + it('removes an element', () => { + let link = document.getElementById('test-link'); + let hint = new Hint(link, 'abc'); + + let elem = document.querySelector('.vimvixen-hint'); + expect(elem.parentElement).to.not.be.null; + hint.remove(); + expect(elem.parentElement).to.be.null; + }); + }); +}); + +describe('LinkHint', () => { + beforeEach(() => { + document.body.innerHTML = ` +link +link +link +`; + }); + + describe('#getLink()', () => { + it('returns value of "href" attribute', () => { + let link = document.getElementById('test-link1') as HTMLAnchorElement; + let hint = new LinkHint(link, 'abc'); + + expect(hint.getLink()).to.equal('https://google.com/'); + }); + }); + + describe('#getLinkTarget()', () => { + it('returns value of "target" attribute', () => { + let link = document.getElementById('test-link1') as HTMLAnchorElement; + let hint = new LinkHint(link, 'abc'); + + expect(hint.getLinkTarget()).to.be.null; + + link = document.getElementById('test-link2') as HTMLAnchorElement; + hint = new LinkHint(link, 'abc'); + + expect(hint.getLinkTarget()).to.equal('_blank'); + }); + }); + + describe('#click()', () => { + it('clicks a element', (done) => { + let link = document.getElementById('test-link3') as HTMLAnchorElement; + let hint = new LinkHint(link, 'abc'); + link.onclick = () => { done() }; + + hint.click(); + }); + }); +}); + +describe('InputHint', () => { + describe('#activate()', () => { + context('', () => { + beforeEach(() => { + document.body.innerHTML = ``; + }); + + it('focuses to the input', () => { + let input = document.getElementById('test-input') as HTMLInputElement; + let hint = new InputHint(input, 'abc'); + hint.activate(); + + expect(document.activeElement).to.equal(input); + }); + }); + + context('', () => { + beforeEach(() => { + document.body.innerHTML = ``; + }); + + it('checks and focuses to the input', () => { + let input = document.getElementById('test-input') as HTMLInputElement; + let hint = new InputHint(input, 'abc'); + hint.activate(); + + expect(input.checked).to.be.true; + }); + }); + context('`; + }); + + it('focuses to the textarea', () => { + let textarea = document.getElementById('test-textarea') as HTMLTextAreaElement; + let hint = new InputHint(textarea, 'abc'); + hint.activate(); + + expect(document.activeElement).to.equal(textarea); + }); + }); + + context('`; + }); + + it('clicks the button', (done) => { + let button = document.getElementById('test-button') as HTMLButtonElement; + button.onclick = () => { done() }; + + let hint = new InputHint(button, 'abc'); + hint.activate(); + }); + }); + }); +});