Merge branch 'improve-follow-command'
This commit is contained in:
		
						commit
						bf07e89126
					
				
					 7 changed files with 87 additions and 26 deletions
				
			
		|  | @ -43,8 +43,8 @@ Firefox by WebExtensions API. | ||||||
|   - [x] open root page |   - [x] open root page | ||||||
| - [ ] hints | - [ ] hints | ||||||
|   - [x] open a link |   - [x] open a link | ||||||
|   - [ ] open a link in new tab |   - [x] open a link in new tab | ||||||
|   - [ ] activate input form |   - [x] activate input form | ||||||
| - [ ] misc | - [ ] misc | ||||||
|   - [ ] configurable keymaps |   - [ ] configurable keymaps | ||||||
|   - [ ] .rc file |   - [ ] .rc file | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								src/actions/tab.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/actions/tab.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | const openNewTab = (url) => { | ||||||
|  |   return browser.tabs.create({ url: url }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const openToTab = (url, tab) => { | ||||||
|  |   return browser.tabs.update(tab.id, { url: url }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export { openToTab, openNewTab }; | ||||||
|  | @ -3,6 +3,7 @@ import * as inputActions from '../actions/input'; | ||||||
| import * as operationActions from '../actions/operation'; | import * as operationActions from '../actions/operation'; | ||||||
| import * as commandActions from '../actions/command'; | import * as commandActions from '../actions/command'; | ||||||
| import * as consoleActions from '../actions/console'; | import * as consoleActions from '../actions/console'; | ||||||
|  | import * as tabActions from '../actions/tab'; | ||||||
| import reducers from '../reducers'; | import reducers from '../reducers'; | ||||||
| import messages from '../messages'; | import messages from '../messages'; | ||||||
| import * as store from '../store'; | import * as store from '../store'; | ||||||
|  | @ -60,6 +61,13 @@ const handleMessage = (message, sender) => { | ||||||
|   case messages.KEYDOWN: |   case messages.KEYDOWN: | ||||||
|     return backgroundStore.dispatch( |     return backgroundStore.dispatch( | ||||||
|       inputActions.keyPress(message.code, message.ctrl), sender); |       inputActions.keyPress(message.code, message.ctrl), sender); | ||||||
|  |   case messages.OPEN_URL: | ||||||
|  |     if (message.newTab) { | ||||||
|  |       return backgroundStore.dispatch( | ||||||
|  |         tabActions.openNewTab(message.url), sender); | ||||||
|  |     } | ||||||
|  |     return backgroundStore.dispatch( | ||||||
|  |       tabActions.openToTab(message.url, sender.tab), sender); | ||||||
|   case messages.CONSOLE_BLURRED: |   case messages.CONSOLE_BLURRED: | ||||||
|     return backgroundStore.dispatch( |     return backgroundStore.dispatch( | ||||||
|       consoleActions.hide(), sender); |       consoleActions.hide(), sender); | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ export default class Follow { | ||||||
|     this.doc = doc; |     this.doc = doc; | ||||||
|     this.hintElements = {}; |     this.hintElements = {}; | ||||||
|     this.keys = []; |     this.keys = []; | ||||||
|  |     this.onActivatedCallbacks = []; | ||||||
| 
 | 
 | ||||||
|     // TODO activate input elements and push button elements
 |     // TODO activate input elements and push button elements
 | ||||||
|     let links = Follow.getTargetElements(doc); |     let links = Follow.getTargetElements(doc); | ||||||
|  | @ -36,7 +37,7 @@ export default class Follow { | ||||||
|     } else if (keyCode === KeyboardEvent.DOM_VK_ENTER || |     } else if (keyCode === KeyboardEvent.DOM_VK_ENTER || | ||||||
|                keyCode === KeyboardEvent.DOM_VK_RETURN) { |                keyCode === KeyboardEvent.DOM_VK_RETURN) { | ||||||
|       let chars = Follow.codeChars(this.keys); |       let chars = Follow.codeChars(this.keys); | ||||||
|       this.hintElements[chars].activate(); |       this.activate(this.hintElements[chars].target); | ||||||
|       return; |       return; | ||||||
|     } else if (Follow.availableKey(keyCode)) { |     } else if (Follow.availableKey(keyCode)) { | ||||||
|       this.keys.push(keyCode); |       this.keys.push(keyCode); | ||||||
|  | @ -45,6 +46,9 @@ export default class Follow { | ||||||
|       this.keys.pop(); |       this.keys.pop(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     e.stopPropagation(); | ||||||
|  |     e.preventDefault(); | ||||||
|  | 
 | ||||||
|     this.refreshKeys(); |     this.refreshKeys(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +65,7 @@ export default class Follow { | ||||||
|       return; |       return; | ||||||
|     } else if (shown.length === 1) { |     } else if (shown.length === 1) { | ||||||
|       this.remove(); |       this.remove(); | ||||||
|       this.hintElements[chars].activate(); |       this.activate(this.hintElements[chars].target); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     shown.forEach((key) => { |     shown.forEach((key) => { | ||||||
|  | @ -72,7 +76,6 @@ export default class Follow { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   remove() { |   remove() { | ||||||
|     this.doc.removeEventListener('keydown', this.boundKeydown); |     this.doc.removeEventListener('keydown', this.boundKeydown); | ||||||
|     Object.keys(this.hintElements).forEach((key) => { |     Object.keys(this.hintElements).forEach((key) => { | ||||||
|  | @ -80,6 +83,14 @@ export default class Follow { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   activate(element) { | ||||||
|  |     this.onActivatedCallbacks.forEach(f => f(element)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   onActivated(f) { | ||||||
|  |     this.onActivatedCallbacks.push(f); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static availableKey(keyCode) { |   static availableKey(keyCode) { | ||||||
|     return ( |     return ( | ||||||
|       KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 || |       KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 || | ||||||
|  | @ -113,21 +124,26 @@ export default class Follow { | ||||||
|     return chars; |     return chars; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   static 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) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static getTargetElements(doc) { |   static getTargetElements(doc) { | ||||||
|     let all = doc.querySelectorAll('a'); |     let all = doc.querySelectorAll('a,button,input,textarea'); | ||||||
|     let filtered = Array.prototype.filter.call(all, (e) => { |     let filtered = Array.prototype.filter.call(all, (element) => { | ||||||
|       return Follow.isVisibleElement(e); |       let style = window.getComputedStyle(element); | ||||||
|  |       return style.display !== 'none' && | ||||||
|  |         style.visibility !== 'hidden' && | ||||||
|  |         element.type !== 'hidden' && | ||||||
|  |         Follow.inWindow(window, element); | ||||||
|     }); |     }); | ||||||
|     return filtered; |     return filtered; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   static isVisibleElement(element) { |  | ||||||
|     let style = window.getComputedStyle(element); |  | ||||||
|     if (style.display === 'none') { |  | ||||||
|       return false; |  | ||||||
|     } else if (style.visibility === 'hidden') { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -33,10 +33,4 @@ export default class Hint { | ||||||
|   remove() { |   remove() { | ||||||
|     this.element.remove(); |     this.element.remove(); | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   activate() { |  | ||||||
|     if (this.target.tagName.toLowerCase() === 'a') { |  | ||||||
|       this.target.click(); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,38 @@ import messages from '../messages'; | ||||||
| 
 | 
 | ||||||
| consoleFrames.initialize(window.document); | consoleFrames.initialize(window.document); | ||||||
| 
 | 
 | ||||||
|  | const startFollows = (newTab) => { | ||||||
|  |   let follow = new Follow(window.document, newTab); | ||||||
|  |   follow.onActivated((element) => { | ||||||
|  |     switch (element.tagName.toLowerCase()) { | ||||||
|  |     case 'a': | ||||||
|  |       return browser.runtime.sendMessage({ | ||||||
|  |         type: messages.OPEN_URL, | ||||||
|  |         url: element.href, | ||||||
|  |         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(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| window.addEventListener('keypress', (e) => { | window.addEventListener('keypress', (e) => { | ||||||
|   if (e.target instanceof HTMLInputElement) { |   if (e.target instanceof HTMLInputElement) { | ||||||
|     return; |     return; | ||||||
|  | @ -34,7 +66,7 @@ const execOperation = (operation) => { | ||||||
|   case operations.SCROLL_RIGHT: |   case operations.SCROLL_RIGHT: | ||||||
|     return scrolls.scrollRight(window); |     return scrolls.scrollRight(window); | ||||||
|   case operations.FOLLOW_START: |   case operations.FOLLOW_START: | ||||||
|     return new Follow(window.document, operation.newTab); |     return startFollows(operation.newTab); | ||||||
|   case operations.NAVIGATE_HISTORY_PREV: |   case operations.NAVIGATE_HISTORY_PREV: | ||||||
|     return navigates.historyPrev(window); |     return navigates.historyPrev(window); | ||||||
|   case operations.NAVIGATE_HISTORY_NEXT: |   case operations.NAVIGATE_HISTORY_NEXT: | ||||||
|  |  | ||||||
|  | @ -6,5 +6,7 @@ export default { | ||||||
|   CONSOLE_ENTERED: 'console.entered', |   CONSOLE_ENTERED: 'console.entered', | ||||||
|   CONSOLE_CHANGEED: 'console.changed', |   CONSOLE_CHANGEED: 'console.changed', | ||||||
| 
 | 
 | ||||||
|   KEYDOWN: 'keydown' |   KEYDOWN: 'keydown', | ||||||
|  | 
 | ||||||
|  |   OPEN_URL: 'open.url' | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Reference in a new issue