From ac5354020e5efdf6e284d4b36696b9f94d46bef9 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 15 Oct 2017 12:34:26 +0900 Subject: support multi-frame following --- .../components/top-content/follow-controller.js | 115 +++++++++++++++++++++ src/content/components/top-content/index.js | 32 ++++++ 2 files changed, 147 insertions(+) create mode 100644 src/content/components/top-content/follow-controller.js create mode 100644 src/content/components/top-content/index.js (limited to 'src/content/components/top-content') diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js new file mode 100644 index 0000000..0474690 --- /dev/null +++ b/src/content/components/top-content/follow-controller.js @@ -0,0 +1,115 @@ +import * as followActions from 'content/actions/follow'; +import messages from 'shared/messages'; +import HintKeyProducer from 'content/hint-key-producer'; + +const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz'; + +const broadcastMessage = (win, message) => { + let json = JSON.stringify(message); + let frames = [window.self].concat(Array.from(window.frames)); + frames.forEach(frame => frame.postMessage(json, '*')); +}; + +export default class FollowController { + constructor(win, store) { + this.win = win; + this.store = store; + this.state = {}; + this.keys = []; + this.producer = null; + } + + onMessage(message, sender) { + switch (message.type) { + case messages.FOLLOW_START: + return this.store.dispatch(followActions.enable(message.newTab)); + case messages.FOLLOW_RESPONSE_COUNT_TARGETS: + return this.create(message.count, sender); + case messages.FOLLOW_KEY_PRESS: + return this.keyPress(message.key); + } + } + + update() { + let prevState = this.state; + this.state = this.store.getState().follow; + + if (!prevState.enabled && this.state.enabled) { + this.count(); + } else if (prevState.enabled && !this.state.enabled) { + this.remove(); + } else if (prevState.keys !== this.state.keys) { + this.updateHints(); + } + } + + updateHints() { + let shown = this.keys.filter(key => key.startsWith(this.state.keys)); + if (shown.length === 1) { + this.activate(); + this.store.dispatch(followActions.disable()); + } + + broadcastMessage(this.win, { + type: messages.FOLLOW_SHOW_HINTS, + keys: this.state.keys, + }); + } + + activate() { + broadcastMessage(this.win, { + type: messages.FOLLOW_ACTIVATE, + keys: this.state.keys, + }); + } + + keyPress(key) { + switch (key) { + case 'Enter': + this.activate(); + this.store.dispatch(followActions.disable()); + break; + case 'Escape': + this.store.dispatch(followActions.disable()); + break; + 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; + } + + count() { + this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); + broadcastMessage(this.win, { + type: messages.FOLLOW_REQUEST_COUNT_TARGETS, + }); + } + + create(count, sender) { + let produced = []; + for (let i = 0; i < count; ++i) { + produced.push(this.producer.produce()); + } + this.keys = this.keys.concat(produced); + + sender.postMessage(JSON.stringify({ + type: messages.FOLLOW_CREATE_HINTS, + keysArray: produced, + newTab: this.state.newTab, + }), '*'); + } + + remove() { + this.keys = []; + broadcastMessage(this.win, { + type: messages.FOLLOW_REMOVE_HINTS, + }); + } +} diff --git a/src/content/components/top-content/index.js b/src/content/components/top-content/index.js new file mode 100644 index 0000000..a2179da --- /dev/null +++ b/src/content/components/top-content/index.js @@ -0,0 +1,32 @@ +import CommonComponent from '../common'; +import FollowController from './follow-controller'; +import * as consoleFrames from '../../console-frames'; +import messages from 'shared/messages'; + +export default class TopContent { + + constructor(win, store) { + this.win = win; + this.children = [ + new CommonComponent(win, store), + new FollowController(win, store), + ]; + + // TODO make component + consoleFrames.initialize(window.document); + } + + update() { + this.children.forEach(c => c.update()); + } + + onMessage(message, sender) { + switch (message.type) { + case messages.CONSOLE_HIDE_COMMAND: + this.win.focus(); + consoleFrames.blur(window.document); + return Promise.resolve(); + } + this.children.forEach(c => c.onMessage(message, sender)); + } +} -- cgit v1.2.3 From 45b3b0510fd0babe392ee46319fa345433af7736 Mon Sep 17 00:00:00 2001 From: Shin'ya Ueoka Date: Sun, 15 Oct 2017 21:59:37 +0900 Subject: follow links for multi-frames in viewport --- .eslintrc | 1 + src/content/components/common/follow.js | 31 ++++++++++++++-------- .../components/top-content/follow-controller.js | 18 ++++++++++++- test/content/components/common/follow.test.js | 5 +++- 4 files changed, 42 insertions(+), 13 deletions(-) (limited to 'src/content/components/top-content') diff --git a/.eslintrc b/.eslintrc index d244c8f..5636171 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,6 +29,7 @@ "id-length": "off", "indent": ["error", 2], "jsx-quotes": ["error", "prefer-single"], + "max-params": ["error", 5], "max-statements": ["error", 15], "multiline-ternary": "off", "newline-after-var": "off", diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.js index a5fbab4..92d8822 100644 --- a/src/content/components/common/follow.js +++ b/src/content/components/common/follow.js @@ -6,16 +6,25 @@ const TARGET_SELECTOR = [ '[contenteditable=true]', '[contenteditable=""]' ].join(','); -const inWindow = (win, element) => { +const inViewport = (win, element, viewSize, framePosition) => { let { top, left, bottom, right } = element.getBoundingClientRect(); let doc = win.doc; - return ( - top >= 0 && left >= 0 && - bottom <= (win.innerHeight || doc.documentElement.clientHeight) && - right <= (win.innerWidth || doc.documentElement.clientWidth) - ); + 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 { @@ -60,8 +69,8 @@ export default class Follow { }); } - countHints(sender) { - this.targets = Follow.getTargetElements(this.win); + 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, @@ -133,7 +142,7 @@ export default class Follow { onMessage(message, sender) { switch (message.type) { case messages.FOLLOW_REQUEST_COUNT_TARGETS: - return this.countHints(sender); + 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: @@ -145,7 +154,7 @@ export default class Follow { } } - static getTargetElements(win) { + 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); @@ -153,7 +162,7 @@ export default class Follow { style.visibility !== 'hidden' && element.type !== 'hidden' && element.offsetHeight > 0 && - inWindow(win, element); + inViewport(win, element, viewSize, framePosition); }); return filtered; } diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js index 0474690..29f40b3 100644 --- a/src/content/components/top-content/follow-controller.js +++ b/src/content/components/top-content/follow-controller.js @@ -87,8 +87,24 @@ export default class FollowController { count() { this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET); - broadcastMessage(this.win, { + let doc = this.win.document; + let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth; + let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight; + let frameElements = this.win.document.querySelectorAll('frame,iframe'); + + this.win.postMessage(JSON.stringify({ type: messages.FOLLOW_REQUEST_COUNT_TARGETS, + viewSize: { width: viewWidth, height: viewHeight }, + framePosition: { x: 0, y: 0 }, + }), '*'); + frameElements.forEach((element) => { + let { left: frameX, top: frameY } = element.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, '*'); }); } diff --git a/test/content/components/common/follow.test.js b/test/content/components/common/follow.test.js index 97bd1d2..1fc935e 100644 --- a/test/content/components/common/follow.test.js +++ b/test/content/components/common/follow.test.js @@ -8,7 +8,10 @@ describe('FollowComponent', () => { }); it('returns visible links', () => { - let targets = FollowComponent.getTargetElements(window); + let targets = FollowComponent.getTargetElements( + window, + { width: window.innerWidth, height: window.innerHeight }, + { x: 0, y: 0 }); expect(targets).to.have.lengthOf(3); let ids = Array.prototype.map.call(targets, (e) => e.id); -- cgit v1.2.3