aboutsummaryrefslogtreecommitdiff
path: root/src/content/components/follow.js
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2017-10-08 15:19:25 +0900
committerGitHub <noreply@github.com>2017-10-08 15:19:25 +0900
commit0183161145d36cbafb7dbd86ca3a1aac6faca43f (patch)
treeaed757bfb5f8789156439d1e1fdff4e221376aaa /src/content/components/follow.js
parent0f54a203dba38acdd080a928cee95f875fe84706 (diff)
parent57f798044d32ba7f9dc10a34ac31ad5dbdbf56ae (diff)
Merge pull request #22 from ueokande/separate-domains
Refactor: Separate domains
Diffstat (limited to 'src/content/components/follow.js')
-rw-r--r--src/content/components/follow.js168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/content/components/follow.js b/src/content/components/follow.js
new file mode 100644
index 0000000..c87424d
--- /dev/null
+++ b/src/content/components/follow.js
@@ -0,0 +1,168 @@
+import * as followActions from 'content/actions/follow';
+import messages from 'shared/messages';
+import Hint from 'content/hint';
+import HintKeyProducer from 'content/hint-key-producer';
+
+const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
+
+const inWindow = (window, element) => {
+ let {
+ top, left, bottom, right
+ } = element.getBoundingClientRect();
+ return (
+ top >= 0 && left >= 0 &&
+ bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+ right <= (window.innerWidth || document.documentElement.clientWidth)
+ );
+};
+
+export default class FollowComponent {
+ constructor(wrapper, store) {
+ this.wrapper = wrapper;
+ this.store = store;
+ this.hintElements = {};
+ this.state = {};
+ }
+
+ update() {
+ let prevState = this.state;
+ this.state = this.store.getState().follow;
+ if (!prevState.enabled && this.state.enabled) {
+ this.create();
+ } else if (prevState.enabled && !this.state.enabled) {
+ this.remove();
+ } else if (prevState.keys !== this.state.keys) {
+ this.updateHints();
+ }
+ }
+
+ key(key) {
+ if (!this.state.enabled) {
+ return false;
+ }
+
+ switch (key) {
+ case 'Enter':
+ this.activate(this.hintElements[this.state.keys].target);
+ return;
+ case 'Escape':
+ this.store.dispatch(followActions.disable());
+ return;
+ case 'Backspace':
+ case 'Delete':
+ this.store.dispatch(followActions.backspace());
+ break;
+ default:
+ if (DEFAULT_HINT_CHARSET.includes(key)) {
+ this.store.dispatch(followActions.keyPress(key));
+ }
+ break;
+ }
+ return true;
+ }
+
+ updateHints() {
+ let keys = this.state.keys;
+ let shown = Object.keys(this.hintElements).filter((key) => {
+ return key.startsWith(keys);
+ });
+ let hidden = Object.keys(this.hintElements).filter((key) => {
+ return !key.startsWith(keys);
+ });
+ if (shown.length === 0) {
+ this.remove();
+ return;
+ } else if (shown.length === 1) {
+ this.activate(this.hintElements[keys].target);
+ this.store.dispatch(followActions.disable());
+ }
+
+ shown.forEach((key) => {
+ this.hintElements[key].show();
+ });
+ hidden.forEach((key) => {
+ this.hintElements[key].hide();
+ });
+ }
+
+ activate(element) {
+ switch (element.tagName.toLowerCase()) {
+ case 'a':
+ if (this.state.newTab) {
+ // getAttribute() to avoid to resolve absolute path
+ let href = element.getAttribute('href');
+
+ // eslint-disable-next-line no-script-url
+ if (!href || href === '#' || href.startsWith('javascript:')) {
+ return;
+ }
+ return browser.runtime.sendMessage({
+ type: messages.OPEN_URL,
+ url: element.href,
+ newTab: this.state.newTab,
+ });
+ }
+ if (element.href.startsWith('http://') ||
+ element.href.startsWith('https://') ||
+ element.href.startsWith('ftp://')) {
+ return browser.runtime.sendMessage({
+ type: messages.OPEN_URL,
+ url: element.href,
+ newTab: this.state.newTab,
+ });
+ }
+ return element.click();
+ case 'input':
+ switch (element.type) {
+ case 'file':
+ case 'checkbox':
+ case 'radio':
+ case 'submit':
+ case 'reset':
+ case 'button':
+ case 'image':
+ case 'color':
+ return element.click();
+ default:
+ return element.focus();
+ }
+ case 'textarea':
+ return element.focus();
+ case 'button':
+ return element.click();
+ }
+ }
+
+ create() {
+ let doc = this.wrapper.ownerDocument;
+ let elements = FollowComponent.getTargetElements(doc);
+ let producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
+ let hintElements = {};
+ Array.prototype.forEach.call(elements, (ele) => {
+ let keys = producer.produce();
+ let hint = new Hint(ele, keys);
+ hintElements[keys] = hint;
+ });
+ this.hintElements = hintElements;
+ }
+
+ remove() {
+ let hintElements = this.hintElements;
+ Object.keys(this.hintElements).forEach((key) => {
+ hintElements[key].remove();
+ });
+ }
+
+ static getTargetElements(doc) {
+ let all = doc.querySelectorAll('a,button,input,textarea');
+ let filtered = Array.prototype.filter.call(all, (element) => {
+ let style = window.getComputedStyle(element);
+ return style.display !== 'none' &&
+ style.visibility !== 'hidden' &&
+ element.type !== 'hidden' &&
+ element.offsetHeight > 0 &&
+ inWindow(window, element);
+ });
+ return filtered;
+ }
+}