aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/content/client/TabsClient.ts12
-rw-r--r--src/content/components/common/follow.ts79
-rw-r--r--src/content/components/common/hint.ts62
-rw-r--r--src/content/presenters/Hint.ts127
-rw-r--r--test/content/components/common/hint.test.ts57
-rw-r--r--test/content/presenters/Hint.test.html (renamed from test/content/components/common/hint.html)0
-rw-r--r--test/content/presenters/Hint.test.ts158
7 files changed, 318 insertions, 177 deletions
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<void>;
+ openUrl(url: string, newTab: boolean, background?: boolean): Promise<void>;
// eslint-disable-next-line semi
}
-export class TabsClientImpl {
- async openUrl(url: string, newTab: boolean): Promise<void> {
+export class TabsClientImpl implements TabsClient {
+ async openUrl(
+ url: string,
+ newTab: boolean,
+ background?: boolean,
+ ): Promise<void> {
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<void> {
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
index b50c5fe..b50c5fe 100644
--- a/test/content/components/common/hint.html
+++ b/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 = `<a id='test-link' href='#'>link</a>`;
+ });
+
+ 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 = `
+<a id='test-link1' href='https://google.com/'>link</a>
+<a id='test-link2' href='https://yahoo.com/' target='_blank'>link</a>
+<a id='test-link3' href='#' target='_blank'>link</a>
+`;
+ });
+
+ 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('<input>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<input id='test-input'></input>`;
+ });
+
+ 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('<input type="checkbox">', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<input type="checkbox" id='test-input'></input>`;
+ });
+
+ 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('<textarea>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<textarea id='test-textarea'></textarea>`;
+ });
+
+ 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('<button>', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `<button id='test-button'></button>`;
+ });
+
+ it('clicks the button', (done) => {
+ let button = document.getElementById('test-button') as HTMLButtonElement;
+ button.onclick = () => { done() };
+
+ let hint = new InputHint(button, 'abc');
+ hint.activate();
+ });
+ });
+ });
+});