diff options
Diffstat (limited to 'src/content/components/common/follow.js')
-rw-r--r-- | src/content/components/common/follow.js | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.js new file mode 100644 index 0000000..92d8822 --- /dev/null +++ b/src/content/components/common/follow.js @@ -0,0 +1,169 @@ +import messages from 'shared/messages'; +import Hint from './hint'; + +const TARGET_SELECTOR = [ + 'a', 'button', 'input', 'textarea', + '[contenteditable=true]', '[contenteditable=""]' +].join(','); + +const inViewport = (win, element, viewSize, framePosition) => { + let { + top, left, bottom, right + } = element.getBoundingClientRect(); + let doc = win.doc; + let frameWidth = win.innerWidth || doc.documentElement.clientWidth; + let frameHeight = win.innerHeight || doc.documentElement.clientHeight; + + if (right < 0 || bottom < 0 || top > frameHeight || left > frameWidth) { + // out of frame + return false; + } + if (right + framePosition.x < 0 || bottom + framePosition.y < 0 || + left + framePosition.x > viewSize.width || + top + framePosition.y > viewSize.height) { + // out of viewport + return false; + } + return true; +}; + +export default class Follow { + constructor(win, store) { + this.win = win; + this.store = store; + this.newTab = false; + this.hints = {}; + this.targets = []; + } + + update() { + } + + key(key) { + if (Object.keys(this.hints).length === 0) { + return false; + } + this.win.parent.postMessage(JSON.stringify({ + type: messages.FOLLOW_KEY_PRESS, + key, + }), '*'); + return true; + } + + openLink(element) { + if (!this.newTab) { + 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: this.newTab, + }); + } + + countHints(sender, viewSize, framePosition) { + this.targets = Follow.getTargetElements(this.win, viewSize, framePosition); + sender.postMessage(JSON.stringify({ + type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, + count: this.targets.length, + }), '*'); + } + + createHints(keysArray, newTab) { + if (keysArray.length !== this.targets.length) { + throw new Error('illegal hint count'); + } + + this.newTab = newTab; + 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; + } + } + + showHints(keys) { + Object.keys(this.hints).filter(key => key.startsWith(keys)) + .forEach(key => this.hints[key].show()); + Object.keys(this.hints).filter(key => !key.startsWith(keys)) + .forEach(key => this.hints[key].hide()); + } + + removeHints() { + Object.keys(this.hints).forEach((key) => { + this.hints[key].remove(); + }); + this.hints = {}; + this.targets = []; + } + + activateHints(keys) { + let hint = this.hints[keys]; + if (!hint) { + return; + } + let element = hint.target; + switch (element.tagName.toLowerCase()) { + case 'a': + return this.openLink(element, this.newTab); + 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(); + default: + // it may contenteditable + return element.focus(); + } + } + + onMessage(message, sender) { + switch (message.type) { + case messages.FOLLOW_REQUEST_COUNT_TARGETS: + return this.countHints(sender, message.viewSize, message.framePosition); + case messages.FOLLOW_CREATE_HINTS: + return this.createHints(message.keysArray, message.newTab); + case messages.FOLLOW_SHOW_HINTS: + return this.showHints(message.keys); + case messages.FOLLOW_ACTIVATE: + return this.activateHints(message.keys); + case messages.FOLLOW_REMOVE_HINTS: + return this.removeHints(message.keys); + } + } + + static getTargetElements(win, viewSize, framePosition) { + let all = win.document.querySelectorAll(TARGET_SELECTOR); + let filtered = Array.prototype.filter.call(all, (element) => { + let style = win.getComputedStyle(element); + return style.display !== 'none' && + style.visibility !== 'hidden' && + element.type !== 'hidden' && + element.offsetHeight > 0 && + inViewport(win, element, viewSize, framePosition); + }); + return filtered; + } +} |