separate content
This commit is contained in:
		
							parent
							
								
									d886d7de29
								
							
						
					
					
						commit
						39fb540037
					
				
					 17 changed files with 24 additions and 24 deletions
				
			
		
							
								
								
									
										29
									
								
								src/content/actions/follow.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/content/actions/follow.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import actions from 'content/actions'; | ||||
| 
 | ||||
| const enable = (newTab) => { | ||||
|   return { | ||||
|     type: actions.FOLLOW_ENABLE, | ||||
|     newTab, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const disable = () => { | ||||
|   return { | ||||
|     type: actions.FOLLOW_DISABLE, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const keyPress = (key) => { | ||||
|   return { | ||||
|     type: actions.FOLLOW_KEY_PRESS, | ||||
|     key: key | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const backspace = () => { | ||||
|   return { | ||||
|     type: actions.FOLLOW_BACKSPACE, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export { enable, disable, keyPress, backspace }; | ||||
							
								
								
									
										20
									
								
								src/content/actions/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/content/actions/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| export default { | ||||
|   // User input
 | ||||
|   INPUT_KEY_PRESS: 'input.key,press', | ||||
|   INPUT_CLEAR_KEYS: 'input.clear.keys', | ||||
|   INPUT_SET_KEYMAPS: 'input.set,keymaps', | ||||
| 
 | ||||
|   // Completion
 | ||||
|   COMPLETION_SET_ITEMS: 'completion.set.items', | ||||
|   COMPLETION_SELECT_NEXT: 'completions.select.next', | ||||
|   COMPLETION_SELECT_PREV: 'completions.select.prev', | ||||
| 
 | ||||
|   // Settings
 | ||||
|   SETTING_SET_SETTINGS: 'setting.set.settings', | ||||
| 
 | ||||
|   // Follow
 | ||||
|   FOLLOW_ENABLE: 'follow.enable', | ||||
|   FOLLOW_DISABLE: 'follow.disable', | ||||
|   FOLLOW_KEY_PRESS: 'follow.key.press', | ||||
|   FOLLOW_BACKSPACE: 'follow.backspace', | ||||
| }; | ||||
							
								
								
									
										23
									
								
								src/content/actions/input.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/content/actions/input.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import actions from 'content/actions'; | ||||
| 
 | ||||
| const asKeymapChars = (key, ctrl) => { | ||||
|   if (ctrl) { | ||||
|     return '<C-' + key.toUpperCase() + '>'; | ||||
|   } | ||||
|   return key; | ||||
| }; | ||||
| 
 | ||||
| const keyPress = (key, ctrl) => { | ||||
|   return { | ||||
|     type: actions.INPUT_KEY_PRESS, | ||||
|     key: asKeymapChars(key, ctrl), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const clearKeys = () => { | ||||
|   return { | ||||
|     type: actions.INPUT_CLEAR_KEYS | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export { keyPress, clearKeys }; | ||||
							
								
								
									
										43
									
								
								src/content/actions/operation.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/content/actions/operation.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import operations from 'shared/operations'; | ||||
| import messages from 'shared/messages'; | ||||
| import * as scrolls from 'content/scrolls'; | ||||
| import * as navigates from 'content/navigates'; | ||||
| import * as followActions from 'content/actions/follow'; | ||||
| 
 | ||||
| const exec = (operation) => { | ||||
|   switch (operation.type) { | ||||
|   case operations.SCROLL_LINES: | ||||
|     return scrolls.scrollLines(window, operation.count); | ||||
|   case operations.SCROLL_PAGES: | ||||
|     return scrolls.scrollPages(window, operation.count); | ||||
|   case operations.SCROLL_TOP: | ||||
|     return scrolls.scrollTop(window); | ||||
|   case operations.SCROLL_BOTTOM: | ||||
|     return scrolls.scrollBottom(window); | ||||
|   case operations.SCROLL_HOME: | ||||
|     return scrolls.scrollLeft(window); | ||||
|   case operations.SCROLL_END: | ||||
|     return scrolls.scrollRight(window); | ||||
|   case operations.FOLLOW_START: | ||||
|     return followActions.enable(false); | ||||
|   case operations.NAVIGATE_HISTORY_PREV: | ||||
|     return navigates.historyPrev(window); | ||||
|   case operations.NAVIGATE_HISTORY_NEXT: | ||||
|     return navigates.historyNext(window); | ||||
|   case operations.NAVIGATE_LINK_PREV: | ||||
|     return navigates.linkPrev(window); | ||||
|   case operations.NAVIGATE_LINK_NEXT: | ||||
|     return navigates.linkNext(window); | ||||
|   case operations.NAVIGATE_PARENT: | ||||
|     return navigates.parent(window); | ||||
|   case operations.NAVIGATE_ROOT: | ||||
|     return navigates.root(window); | ||||
|   default: | ||||
|     browser.runtime.sendMessage({ | ||||
|       type: messages.BACKGROUND_OPERATION, | ||||
|       operation, | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export { exec }; | ||||
							
								
								
									
										67
									
								
								src/content/components/content-input.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/content/components/content-input.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | |||
| export default class ContentInputComponent { | ||||
|   constructor(target) { | ||||
|     this.pressed = {}; | ||||
|     this.onKeyListeners = []; | ||||
| 
 | ||||
|     target.addEventListener('keypress', this.onKeyPress.bind(this)); | ||||
|     target.addEventListener('keydown', this.onKeyDown.bind(this)); | ||||
|     target.addEventListener('keyup', this.onKeyUp.bind(this)); | ||||
|   } | ||||
| 
 | ||||
|   update() { | ||||
|   } | ||||
| 
 | ||||
|   onKey(cb) { | ||||
|     this.onKeyListeners.push(cb); | ||||
|   } | ||||
| 
 | ||||
|   onKeyPress(e) { | ||||
|     if (this.pressed[e.key] && this.pressed[e.key] !== 'keypress') { | ||||
|       return; | ||||
|     } | ||||
|     this.pressed[e.key] = 'keypress'; | ||||
|     this.capture(e); | ||||
|   } | ||||
| 
 | ||||
|   onKeyDown(e) { | ||||
|     if (this.pressed[e.key] && this.pressed[e.key] !== 'keydown') { | ||||
|       return; | ||||
|     } | ||||
|     this.pressed[e.key] = 'keydown'; | ||||
|     this.capture(e); | ||||
|   } | ||||
| 
 | ||||
|   onKeyUp(e) { | ||||
|     delete this.pressed[e.key]; | ||||
|   } | ||||
| 
 | ||||
|   capture(e) { | ||||
|     if (this.fromInput(e)) { | ||||
|       if (e.key === 'Escape' && e.target.blur) { | ||||
|         e.target.blur(); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|     if (e.key === 'OS') { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let stop = false; | ||||
|     for (let listener of this.onKeyListeners) { | ||||
|       stop = stop || listener(e.key, e.ctrlKey); | ||||
|       if (stop) { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     if (stop) { | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   fromInput(e) { | ||||
|     return e.target instanceof HTMLInputElement || | ||||
|       e.target instanceof HTMLTextAreaElement || | ||||
|       e.target instanceof HTMLSelectElement; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										168
									
								
								src/content/components/follow.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/content/components/follow.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										43
									
								
								src/content/components/keymapper.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/content/components/keymapper.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| import * as inputActions from 'content/actions/input'; | ||||
| import * as operationActions from 'content/actions/operation'; | ||||
| 
 | ||||
| export default class KeymapperComponent { | ||||
|   constructor(store) { | ||||
|     this.store = store; | ||||
|   } | ||||
| 
 | ||||
|   update() { | ||||
|   } | ||||
| 
 | ||||
|   key(key, ctrl) { | ||||
|     let keymaps = this.keymaps(); | ||||
|     if (!keymaps) { | ||||
|       return; | ||||
|     } | ||||
|     this.store.dispatch(inputActions.keyPress(key, ctrl)); | ||||
| 
 | ||||
|     let input = this.store.getState().input; | ||||
|     let matched = Object.keys(keymaps).filter((keyStr) => { | ||||
|       return keyStr.startsWith(input.keys); | ||||
|     }); | ||||
|     if (matched.length === 0) { | ||||
|       this.store.dispatch(inputActions.clearKeys()); | ||||
|       return false; | ||||
|     } else if (matched.length > 1 || | ||||
|       matched.length === 1 && input.keys !== matched[0]) { | ||||
|       return true; | ||||
|     } | ||||
|     let operation = keymaps[matched]; | ||||
|     this.store.dispatch(operationActions.exec(operation)); | ||||
|     this.store.dispatch(inputActions.clearKeys()); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   keymaps() { | ||||
|     let settings = this.store.getState().setting.settings; | ||||
|     if (!settings || !settings.json) { | ||||
|       return null; | ||||
|     } | ||||
|     return JSON.parse(settings.json).keymaps; | ||||
|   } | ||||
| } | ||||
|  | @ -2,10 +2,10 @@ import './console-frame.scss'; | |||
| import * as consoleFrames from './console-frames'; | ||||
| import * as settingActions from 'settings/actions/setting'; | ||||
| import { createStore } from 'store'; | ||||
| import ContentInputComponent from 'components/content-input'; | ||||
| import KeymapperComponent from 'components/keymapper'; | ||||
| import FollowComponent from 'components/follow'; | ||||
| import reducers from 'reducers'; | ||||
| import ContentInputComponent from 'content/components/content-input'; | ||||
| import KeymapperComponent from 'content/components/keymapper'; | ||||
| import FollowComponent from 'content/components/follow'; | ||||
| import reducers from 'content/reducers'; | ||||
| import messages from 'shared/messages'; | ||||
| 
 | ||||
| const store = createStore(reducers); | ||||
|  |  | |||
							
								
								
									
										32
									
								
								src/content/reducers/follow.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/content/reducers/follow.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import actions from 'content/actions'; | ||||
| 
 | ||||
| const defaultState = { | ||||
|   enabled: false, | ||||
|   newTab: false, | ||||
|   keys: '', | ||||
| }; | ||||
| 
 | ||||
| export default function reducer(state = defaultState, action = {}) { | ||||
|   switch (action.type) { | ||||
|   case actions.FOLLOW_ENABLE: | ||||
|     return Object.assign({}, state, { | ||||
|       enabled: true, | ||||
|       newTab: action.newTab, | ||||
|       keys: '', | ||||
|     }); | ||||
|   case actions.FOLLOW_DISABLE: | ||||
|     return Object.assign({}, state, { | ||||
|       enabled: false, | ||||
|     }); | ||||
|   case actions.FOLLOW_KEY_PRESS: | ||||
|     return Object.assign({}, state, { | ||||
|       keys: state.keys + action.key, | ||||
|     }); | ||||
|   case actions.FOLLOW_BACKSPACE: | ||||
|     return Object.assign({}, state, { | ||||
|       keys: state.keys.slice(0, -1), | ||||
|     }); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/content/reducers/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/content/reducers/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import settingReducer from 'settings/reducers/setting'; | ||||
| import inputReducer from './input'; | ||||
| import followReducer from './follow'; | ||||
| 
 | ||||
| // Make setting reducer instead of re-use
 | ||||
| const defaultState = { | ||||
|   input: inputReducer(undefined, {}), | ||||
|   setting: settingReducer(undefined, {}), | ||||
|   follow: followReducer(undefined, {}), | ||||
| }; | ||||
| 
 | ||||
| export default function reducer(state = defaultState, action = {}) { | ||||
|   return Object.assign({}, state, { | ||||
|     input: inputReducer(state.input, action), | ||||
|     setting: settingReducer(state.setting, action), | ||||
|     follow: followReducer(state.follow, action), | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/content/reducers/input.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/content/reducers/input.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import actions from 'content/actions'; | ||||
| 
 | ||||
| const defaultState = { | ||||
|   keys: '', | ||||
| }; | ||||
| 
 | ||||
| export default function reducer(state = defaultState, action = {}) { | ||||
|   switch (action.type) { | ||||
|   case actions.INPUT_KEY_PRESS: | ||||
|     return Object.assign({}, state, { | ||||
|       keys: state.keys + action.key | ||||
|     }); | ||||
|   case actions.INPUT_CLEAR_KEYS: | ||||
|     return Object.assign({}, state, { | ||||
|       keys: '', | ||||
|     }); | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
| } | ||||
		Reference in a new issue