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
 | 
			
		||||
- [ ] hints
 | 
			
		||||
  - [x] open a link
 | 
			
		||||
  - [ ] open a link in new tab
 | 
			
		||||
  - [ ] activate input form
 | 
			
		||||
  - [x] open a link in new tab
 | 
			
		||||
  - [x] activate input form
 | 
			
		||||
- [ ] misc
 | 
			
		||||
  - [ ] configurable keymaps
 | 
			
		||||
  - [ ] .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 commandActions from '../actions/command';
 | 
			
		||||
import * as consoleActions from '../actions/console';
 | 
			
		||||
import * as tabActions from '../actions/tab';
 | 
			
		||||
import reducers from '../reducers';
 | 
			
		||||
import messages from '../messages';
 | 
			
		||||
import * as store from '../store';
 | 
			
		||||
| 
						 | 
				
			
			@ -60,6 +61,13 @@ const handleMessage = (message, sender) => {
 | 
			
		|||
  case messages.KEYDOWN:
 | 
			
		||||
    return backgroundStore.dispatch(
 | 
			
		||||
      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:
 | 
			
		||||
    return backgroundStore.dispatch(
 | 
			
		||||
      consoleActions.hide(), sender);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ export default class Follow {
 | 
			
		|||
    this.doc = doc;
 | 
			
		||||
    this.hintElements = {};
 | 
			
		||||
    this.keys = [];
 | 
			
		||||
    this.onActivatedCallbacks = [];
 | 
			
		||||
 | 
			
		||||
    // TODO activate input elements and push button elements
 | 
			
		||||
    let links = Follow.getTargetElements(doc);
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +37,7 @@ export default class Follow {
 | 
			
		|||
    } else if (keyCode === KeyboardEvent.DOM_VK_ENTER ||
 | 
			
		||||
               keyCode === KeyboardEvent.DOM_VK_RETURN) {
 | 
			
		||||
      let chars = Follow.codeChars(this.keys);
 | 
			
		||||
      this.hintElements[chars].activate();
 | 
			
		||||
      this.activate(this.hintElements[chars].target);
 | 
			
		||||
      return;
 | 
			
		||||
    } else if (Follow.availableKey(keyCode)) {
 | 
			
		||||
      this.keys.push(keyCode);
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +46,9 @@ export default class Follow {
 | 
			
		|||
      this.keys.pop();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    this.refreshKeys();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +65,7 @@ export default class Follow {
 | 
			
		|||
      return;
 | 
			
		||||
    } else if (shown.length === 1) {
 | 
			
		||||
      this.remove();
 | 
			
		||||
      this.hintElements[chars].activate();
 | 
			
		||||
      this.activate(this.hintElements[chars].target);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    shown.forEach((key) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +76,6 @@ export default class Follow {
 | 
			
		|||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  remove() {
 | 
			
		||||
    this.doc.removeEventListener('keydown', this.boundKeydown);
 | 
			
		||||
    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) {
 | 
			
		||||
    return (
 | 
			
		||||
      KeyboardEvent.DOM_VK_0 <= keyCode && keyCode <= KeyboardEvent.DOM_VK_9 ||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,21 +124,26 @@ export default class Follow {
 | 
			
		|||
    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) {
 | 
			
		||||
    let all = doc.querySelectorAll('a');
 | 
			
		||||
    let filtered = Array.prototype.filter.call(all, (e) => {
 | 
			
		||||
      return Follow.isVisibleElement(e);
 | 
			
		||||
    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' &&
 | 
			
		||||
        Follow.inWindow(window, element);
 | 
			
		||||
    });
 | 
			
		||||
    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() {
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
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) => {
 | 
			
		||||
  if (e.target instanceof HTMLInputElement) {
 | 
			
		||||
    return;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +66,7 @@ const execOperation = (operation) => {
 | 
			
		|||
  case operations.SCROLL_RIGHT:
 | 
			
		||||
    return scrolls.scrollRight(window);
 | 
			
		||||
  case operations.FOLLOW_START:
 | 
			
		||||
    return new Follow(window.document, operation.newTab);
 | 
			
		||||
    return startFollows(operation.newTab);
 | 
			
		||||
  case operations.NAVIGATE_HISTORY_PREV:
 | 
			
		||||
    return navigates.historyPrev(window);
 | 
			
		||||
  case operations.NAVIGATE_HISTORY_NEXT:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,5 +6,7 @@ export default {
 | 
			
		|||
  CONSOLE_ENTERED: 'console.entered',
 | 
			
		||||
  CONSOLE_CHANGEED: 'console.changed',
 | 
			
		||||
 | 
			
		||||
  KEYDOWN: 'keydown'
 | 
			
		||||
  KEYDOWN: 'keydown',
 | 
			
		||||
 | 
			
		||||
  OPEN_URL: 'open.url'
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue