commit
						8b72aac09a
					
				
					 24 changed files with 451 additions and 18 deletions
				
			
		
							
								
								
									
										15
									
								
								src/background/controllers/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/background/controllers/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import MarkInteractor from '../usecases/mark';
 | 
			
		||||
 | 
			
		||||
export default class MarkController {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.markInteractor = new MarkInteractor();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setGlobal(key, x, y) {
 | 
			
		||||
    this.markInteractor.setGlobal(key, x, y);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  jumpGlobal(key) {
 | 
			
		||||
    this.markInteractor.jumpGlobal(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/background/domains/global-mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/background/domains/global-mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
export default class GlobalMark {
 | 
			
		||||
  constructor(tabId, url, x, y) {
 | 
			
		||||
    this.tabId0 = tabId;
 | 
			
		||||
    this.url0 = url;
 | 
			
		||||
    this.x0 = x;
 | 
			
		||||
    this.y0 = y;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get tabId() {
 | 
			
		||||
    return this.tabId0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get url() {
 | 
			
		||||
    return this.url0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get x() {
 | 
			
		||||
    return this.x0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get y() {
 | 
			
		||||
    return this.y0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,4 +22,12 @@ export default class ContentMessageClient {
 | 
			
		|||
      type: messages.ADDON_TOGGLE_ENABLED,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollTo(tabId, x, y) {
 | 
			
		||||
    return browser.tabs.sendMessage(tabId, {
 | 
			
		||||
      type: messages.TAB_SCROLL_TO,
 | 
			
		||||
      x,
 | 
			
		||||
      y,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import FindController from '../controllers/find';
 | 
			
		|||
import AddonEnabledController from '../controllers/addon-enabled';
 | 
			
		||||
import LinkController from '../controllers/link';
 | 
			
		||||
import OperationController from '../controllers/operation';
 | 
			
		||||
import MarkController from '../controllers/mark';
 | 
			
		||||
 | 
			
		||||
export default class ContentMessageListener {
 | 
			
		||||
  constructor() {
 | 
			
		||||
| 
						 | 
				
			
			@ -14,6 +15,7 @@ export default class ContentMessageListener {
 | 
			
		|||
    this.addonEnabledController = new AddonEnabledController();
 | 
			
		||||
    this.linkController = new LinkController();
 | 
			
		||||
    this.backgroundOperationController = new OperationController();
 | 
			
		||||
    this.markController = new MarkController();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  run() {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +61,10 @@ export default class ContentMessageListener {
 | 
			
		|||
        message.newTab, message.url, sender.tab.id, message.background);
 | 
			
		||||
    case messages.BACKGROUND_OPERATION:
 | 
			
		||||
      return this.onBackgroundOperation(message.operation);
 | 
			
		||||
    case messages.MARK_SET_GLOBAL:
 | 
			
		||||
      return this.onMarkSetGlobal(message.key, message.x, message.y);
 | 
			
		||||
    case messages.MARK_JUMP_GLOBAL:
 | 
			
		||||
      return this.onMarkJumpGlobal(message.key);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,4 +108,12 @@ export default class ContentMessageListener {
 | 
			
		|||
  onBackgroundOperation(operation) {
 | 
			
		||||
    return this.backgroundOperationController.exec(operation);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMarkSetGlobal(key, x, y) {
 | 
			
		||||
    return this.markController.setGlobal(key, x, y);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMarkJumpGlobal(key) {
 | 
			
		||||
    return this.markController.jumpGlobal(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										33
									
								
								src/background/repositories/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/background/repositories/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import MemoryStorage from '../infrastructures/memory-storage';
 | 
			
		||||
import GlobalMark from 'background/domains/global-mark';
 | 
			
		||||
 | 
			
		||||
const MARK_KEY = 'mark';
 | 
			
		||||
 | 
			
		||||
export default class MarkRepository {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.cache = new MemoryStorage();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMark(key) {
 | 
			
		||||
    let marks = this.getOrEmptyMarks();
 | 
			
		||||
    let data = marks[key];
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      return Promise.resolve(undefined);
 | 
			
		||||
    }
 | 
			
		||||
    let mark = new GlobalMark(data.tabId, data.url, data.x, data.y);
 | 
			
		||||
    return Promise.resolve(mark);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setMark(key, mark) {
 | 
			
		||||
    let marks = this.getOrEmptyMarks();
 | 
			
		||||
    marks[key] = { tabId: mark.tabId, url: mark.url, x: mark.x, y: mark.y };
 | 
			
		||||
    this.cache.set(MARK_KEY, marks);
 | 
			
		||||
 | 
			
		||||
    return Promise.resolve();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOrEmptyMarks() {
 | 
			
		||||
    return this.cache.get(MARK_KEY) || {};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								src/background/usecases/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/background/usecases/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
import GlobalMark from '../domains/global-mark';
 | 
			
		||||
import TabPresenter from '../presenters/tab';
 | 
			
		||||
import MarkRepository from '../repositories/mark';
 | 
			
		||||
import ConsolePresenter from '../presenters/console';
 | 
			
		||||
import ContentMessageClient from '../infrastructures/content-message-client';
 | 
			
		||||
 | 
			
		||||
export default class MarkInteractor {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.tabPresenter = new TabPresenter();
 | 
			
		||||
    this.markRepository = new MarkRepository();
 | 
			
		||||
    this.consolePresenter = new ConsolePresenter();
 | 
			
		||||
    this.contentMessageClient = new ContentMessageClient();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setGlobal(key, x, y) {
 | 
			
		||||
    let tab = await this.tabPresenter.getCurrent();
 | 
			
		||||
    let mark = new GlobalMark(tab.id, tab.url, x, y);
 | 
			
		||||
    return this.markRepository.setMark(key, mark);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async jumpGlobal(key) {
 | 
			
		||||
    let current = await this.tabPresenter.getCurrent();
 | 
			
		||||
 | 
			
		||||
    let mark = await this.markRepository.getMark(key);
 | 
			
		||||
    if (!mark) {
 | 
			
		||||
      return this.consolePresenter.showError(current.id, 'Mark is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.contentMessageClient.scrollTo(
 | 
			
		||||
      mark.tabId, mark.x, mark.y
 | 
			
		||||
    ).then(() => {
 | 
			
		||||
      return this.tabPresenter.select(mark.tabId);
 | 
			
		||||
    }).catch(async() => {
 | 
			
		||||
      let tab = await this.tabPresenter.create(mark.url);
 | 
			
		||||
      let mark2 = new GlobalMark(tab.id, mark.url, mark.x, mark.y);
 | 
			
		||||
      return this.markRepository.setMark(key, mark2);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,4 +22,10 @@ export default {
 | 
			
		|||
 | 
			
		||||
  // Find
 | 
			
		||||
  FIND_SET_KEYWORD: 'find.set.keyword',
 | 
			
		||||
 | 
			
		||||
  // Mark
 | 
			
		||||
  MARK_START_SET: 'mark.start.set',
 | 
			
		||||
  MARK_START_JUMP: 'mark.start.jump',
 | 
			
		||||
  MARK_CANCEL: 'mark.cancel',
 | 
			
		||||
  MARK_SET_LOCAL: 'mark.set.local',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										46
									
								
								src/content/actions/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/content/actions/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
import actions from 'content/actions';
 | 
			
		||||
import messages from 'shared/messages';
 | 
			
		||||
 | 
			
		||||
const startSet = () => {
 | 
			
		||||
  return { type: actions.MARK_START_SET };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const startJump = () => {
 | 
			
		||||
  return { type: actions.MARK_START_JUMP };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const cancel = () => {
 | 
			
		||||
  return { type: actions.MARK_CANCEL };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setLocal = (key, x, y) => {
 | 
			
		||||
  return {
 | 
			
		||||
    type: actions.MARK_SET_LOCAL,
 | 
			
		||||
    key,
 | 
			
		||||
    x,
 | 
			
		||||
    y,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setGlobal = (key, x, y) => {
 | 
			
		||||
  browser.runtime.sendMessage({
 | 
			
		||||
    type: messages.MARK_SET_GLOBAL,
 | 
			
		||||
    key,
 | 
			
		||||
    x,
 | 
			
		||||
    y,
 | 
			
		||||
  });
 | 
			
		||||
  return { type: '' };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const jumpGlobal = (key) => {
 | 
			
		||||
  browser.runtime.sendMessage({
 | 
			
		||||
    type: messages.MARK_JUMP_GLOBAL,
 | 
			
		||||
    key,
 | 
			
		||||
  });
 | 
			
		||||
  return { type: '' };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  startSet, startJump, cancel, setLocal,
 | 
			
		||||
  setGlobal, jumpGlobal,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import * as focuses from 'content/focuses';
 | 
			
		|||
import * as urls from 'content/urls';
 | 
			
		||||
import * as consoleFrames from 'content/console-frames';
 | 
			
		||||
import * as addonActions from './addon';
 | 
			
		||||
import * as markActions from './mark';
 | 
			
		||||
import * as properties from 'shared/settings/properties';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line complexity, max-lines-per-function
 | 
			
		||||
| 
						 | 
				
			
			@ -39,16 +40,16 @@ const exec = (operation, repeat, settings, addonEnabled) => {
 | 
			
		|||
    scrolls.scrollPages(operation.count, smoothscroll, repeat);
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.SCROLL_TOP:
 | 
			
		||||
    scrolls.scrollTop(smoothscroll, repeat);
 | 
			
		||||
    scrolls.scrollToTop(smoothscroll);
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.SCROLL_BOTTOM:
 | 
			
		||||
    scrolls.scrollBottom(smoothscroll, repeat);
 | 
			
		||||
    scrolls.scrollToBottom(smoothscroll);
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.SCROLL_HOME:
 | 
			
		||||
    scrolls.scrollHome(smoothscroll, repeat);
 | 
			
		||||
    scrolls.scrollToHome(smoothscroll);
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.SCROLL_END:
 | 
			
		||||
    scrolls.scrollEnd(smoothscroll, repeat);
 | 
			
		||||
    scrolls.scrollToEnd(smoothscroll);
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.FOLLOW_START:
 | 
			
		||||
    window.top.postMessage(JSON.stringify({
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +58,10 @@ const exec = (operation, repeat, settings, addonEnabled) => {
 | 
			
		|||
      background: operation.background,
 | 
			
		||||
    }), '*');
 | 
			
		||||
    break;
 | 
			
		||||
  case operations.MARK_SET_PREFIX:
 | 
			
		||||
    return markActions.startSet();
 | 
			
		||||
  case operations.MARK_JUMP_PREFIX:
 | 
			
		||||
    return markActions.startJump();
 | 
			
		||||
  case operations.NAVIGATE_HISTORY_PREV:
 | 
			
		||||
    navigates.historyPrev(window);
 | 
			
		||||
    break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import InputComponent from './input';
 | 
			
		||||
import KeymapperComponent from './keymapper';
 | 
			
		||||
import FollowComponent from './follow';
 | 
			
		||||
import MarkComponent from './mark';
 | 
			
		||||
import KeymapperComponent from './keymapper';
 | 
			
		||||
import * as settingActions from 'content/actions/setting';
 | 
			
		||||
import messages from 'shared/messages';
 | 
			
		||||
import * as addonActions from '../../actions/addon';
 | 
			
		||||
| 
						 | 
				
			
			@ -8,11 +9,13 @@ import * as blacklists from 'shared/blacklists';
 | 
			
		|||
 | 
			
		||||
export default class Common {
 | 
			
		||||
  constructor(win, store) {
 | 
			
		||||
    const follow = new FollowComponent(win, store);
 | 
			
		||||
    const input = new InputComponent(win.document.body, store);
 | 
			
		||||
    const follow = new FollowComponent(win, store);
 | 
			
		||||
    const mark = new MarkComponent(win.document.body, store);
 | 
			
		||||
    const keymapper = new KeymapperComponent(store);
 | 
			
		||||
 | 
			
		||||
    input.onKey(key => follow.key(key));
 | 
			
		||||
    input.onKey(key => mark.key(key));
 | 
			
		||||
    input.onKey(key => keymapper.key(key));
 | 
			
		||||
 | 
			
		||||
    this.win = win;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										74
									
								
								src/content/components/common/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/content/components/common/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
import * as markActions from 'content/actions/mark';
 | 
			
		||||
import * as scrolls from 'content/scrolls';
 | 
			
		||||
import * as consoleFrames from 'content/console-frames';
 | 
			
		||||
import * as properties from 'shared/settings/properties';
 | 
			
		||||
 | 
			
		||||
const cancelKey = (key) => {
 | 
			
		||||
  return key.key === 'Esc' || key.key === '[' && key.ctrlKey;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const globalKey = (key) => {
 | 
			
		||||
  return (/^[A-Z0-9]$/).test(key);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default class MarkComponent {
 | 
			
		||||
  constructor(body, store) {
 | 
			
		||||
    this.body = body;
 | 
			
		||||
    this.store = store;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line max-statements
 | 
			
		||||
  key(key) {
 | 
			
		||||
    let { mark: markStage, setting } = this.store.getState();
 | 
			
		||||
    let smoothscroll = setting.properties.smoothscroll ||
 | 
			
		||||
      properties.defaults.smoothscroll;
 | 
			
		||||
 | 
			
		||||
    if (!markStage.setMode && !markStage.jumpMode) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cancelKey(key)) {
 | 
			
		||||
      this.store.dispatch(markActions.cancel());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (key.ctrlKey || key.metaKey || key.altKey) {
 | 
			
		||||
      consoleFrames.postError(window.document, 'Unknown mark');
 | 
			
		||||
    } else if (globalKey(key.key) && markStage.setMode) {
 | 
			
		||||
      this.doSetGlobal(key);
 | 
			
		||||
    } else if (globalKey(key.key) && markStage.jumpMode) {
 | 
			
		||||
      this.doJumpGlobal(key);
 | 
			
		||||
    } else if (markStage.setMode) {
 | 
			
		||||
      this.doSet(key);
 | 
			
		||||
    } else if (markStage.jumpMode) {
 | 
			
		||||
      this.doJump(markStage.marks, key, smoothscroll);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.store.dispatch(markActions.cancel());
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doSet(key) {
 | 
			
		||||
    let { x, y } = scrolls.getScroll();
 | 
			
		||||
    this.store.dispatch(markActions.setLocal(key.key, x, y));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doJump(marks, key, smoothscroll) {
 | 
			
		||||
    if (!marks[key.key]) {
 | 
			
		||||
      consoleFrames.postError(window.document, 'Mark is not set');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let { x, y } = marks[key.key];
 | 
			
		||||
    scrolls.scrollTo(x, y, smoothscroll);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doSetGlobal(key) {
 | 
			
		||||
    let { x, y } = scrolls.getScroll();
 | 
			
		||||
    this.store.dispatch(markActions.setGlobal(key.key, x, y));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doJumpGlobal(key) {
 | 
			
		||||
    this.store.dispatch(markActions.jumpGlobal(key.key));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import FollowController from './follow-controller';
 | 
			
		|||
import FindComponent from './find';
 | 
			
		||||
import * as consoleFrames from '../../console-frames';
 | 
			
		||||
import messages from 'shared/messages';
 | 
			
		||||
import * as scrolls from 'content/scrolls';
 | 
			
		||||
 | 
			
		||||
export default class TopContent {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +34,8 @@ export default class TopContent {
 | 
			
		|||
        type: messages.ADDON_ENABLED_RESPONSE,
 | 
			
		||||
        enabled: addonState.enabled,
 | 
			
		||||
      });
 | 
			
		||||
    case messages.TAB_SCROLL_TO:
 | 
			
		||||
      return scrolls.scrollTo(message.x, message.y, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,8 @@ import find from './find';
 | 
			
		|||
import setting from './setting';
 | 
			
		||||
import input from './input';
 | 
			
		||||
import followController from './follow-controller';
 | 
			
		||||
import mark from './mark';
 | 
			
		||||
 | 
			
		||||
export default combineReducers({
 | 
			
		||||
  addon, find, setting, input, followController,
 | 
			
		||||
  addon, find, setting, input, followController, mark,
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								src/content/reducers/mark.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/content/reducers/mark.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import actions from 'content/actions';
 | 
			
		||||
 | 
			
		||||
const defaultState = {
 | 
			
		||||
  setMode: false,
 | 
			
		||||
  jumpMode: false,
 | 
			
		||||
  marks: {},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function reducer(state = defaultState, action = {}) {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
  case actions.MARK_START_SET:
 | 
			
		||||
    return { ...state, setMode: true };
 | 
			
		||||
  case actions.MARK_START_JUMP:
 | 
			
		||||
    return { ...state, jumpMode: true };
 | 
			
		||||
  case actions.MARK_CANCEL:
 | 
			
		||||
    return { ...state, setMode: false, jumpMode: false };
 | 
			
		||||
  case actions.MARK_SET_LOCAL: {
 | 
			
		||||
    let marks = { ...state.marks };
 | 
			
		||||
    marks[action.key] = { x: action.x, y: action.y };
 | 
			
		||||
    return { ...state, setMode: false, marks };
 | 
			
		||||
  }
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +130,11 @@ const scroller = (element, smooth, repeat) => {
 | 
			
		|||
  return new RoughtScroller(element);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getScroll = () => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  return { x: target.scrollLeft, y: target.scrollTop };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollVertically = (count, smooth, repeat) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  let x = target.scrollLeft;
 | 
			
		||||
| 
						 | 
				
			
			@ -158,35 +163,42 @@ const scrollPages = (count, smooth, repeat) => {
 | 
			
		|||
  scroller(target, smooth, repeat).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollTop = (smooth, repeat) => {
 | 
			
		||||
const scrollTo = (x, y, smooth) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  scroller(target, smooth, false).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollToTop = (smooth) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  let x = target.scrollLeft;
 | 
			
		||||
  let y = 0;
 | 
			
		||||
  scroller(target, smooth, repeat).scroll(x, y);
 | 
			
		||||
  scroller(target, smooth, false).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollBottom = (smooth, repeat) => {
 | 
			
		||||
const scrollToBottom = (smooth) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  let x = target.scrollLeft;
 | 
			
		||||
  let y = target.scrollHeight;
 | 
			
		||||
  scroller(target, smooth, repeat).scroll(x, y);
 | 
			
		||||
  scroller(target, smooth, false).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollHome = (smooth, repeat) => {
 | 
			
		||||
const scrollToHome = (smooth) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  let x = 0;
 | 
			
		||||
  let y = target.scrollTop;
 | 
			
		||||
  scroller(target, smooth, repeat).scroll(x, y);
 | 
			
		||||
  scroller(target, smooth, false).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const scrollEnd = (smooth, repeat) => {
 | 
			
		||||
const scrollToEnd = (smooth) => {
 | 
			
		||||
  let target = scrollTarget();
 | 
			
		||||
  let x = target.scrollWidth;
 | 
			
		||||
  let y = target.scrollTop;
 | 
			
		||||
  scroller(target, smooth, repeat).scroll(x, y);
 | 
			
		||||
  scroller(target, smooth, false).scroll(x, y);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  getScroll,
 | 
			
		||||
  scrollVertically, scrollHorizonally, scrollPages,
 | 
			
		||||
  scrollTop, scrollBottom, scrollHome, scrollEnd
 | 
			
		||||
  scrollTo,
 | 
			
		||||
  scrollToTop, scrollToBottom, scrollToHome, scrollToEnd
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,9 @@ const KeyMapFields = [
 | 
			
		|||
    ['scroll.pages?{"count":0.5}', 'Scroll down by half of screen'],
 | 
			
		||||
    ['scroll.pages?{"count":-1}', 'Scroll up by a screen'],
 | 
			
		||||
    ['scroll.pages?{"count":1}', 'Scroll down by a screen'],
 | 
			
		||||
  ], [
 | 
			
		||||
    ['mark.set.prefix', 'Set mark at current position'],
 | 
			
		||||
    ['mark.jump.prefix', 'Jump to the mark'],
 | 
			
		||||
  ], [
 | 
			
		||||
    ['tabs.close', 'Close a tab'],
 | 
			
		||||
    ['tabs.reopen', 'Reopen closed tab'],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,11 @@ export default {
 | 
			
		|||
  FOLLOW_ACTIVATE: 'follow.activate',
 | 
			
		||||
  FOLLOW_KEY_PRESS: 'follow.key.press',
 | 
			
		||||
 | 
			
		||||
  MARK_SET_GLOBAL: 'mark.set.global',
 | 
			
		||||
  MARK_JUMP_GLOBAL: 'mark.jump.global',
 | 
			
		||||
 | 
			
		||||
  TAB_SCROLL_TO: 'tab.scroll.to',
 | 
			
		||||
 | 
			
		||||
  FIND_NEXT: 'find.next',
 | 
			
		||||
  FIND_PREV: 'find.prev',
 | 
			
		||||
  FIND_GET_KEYWORD: 'find.get.keyword',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -69,4 +69,8 @@ export default {
 | 
			
		|||
  FIND_START: 'find.start',
 | 
			
		||||
  FIND_NEXT: 'find.next',
 | 
			
		||||
  FIND_PREV: 'find.prev',
 | 
			
		||||
 | 
			
		||||
  // Mark
 | 
			
		||||
  MARK_SET_PREFIX: 'mark.set.prefix',
 | 
			
		||||
  MARK_JUMP_PREFIX: 'mark.jump.prefix',
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,6 +42,8 @@ export default {
 | 
			
		|||
    "zz": { "type": "zoom.neutral" },
 | 
			
		||||
    "f": { "type": "follow.start", "newTab": false },
 | 
			
		||||
    "F": { "type": "follow.start", "newTab": true, "background": false },
 | 
			
		||||
    "m": { "type": "mark.set.prefix" },
 | 
			
		||||
    "'": { "type": "mark.jump.prefix" },
 | 
			
		||||
    "H": { "type": "navigate.history.prev" },
 | 
			
		||||
    "L": { "type": "navigate.history.next" },
 | 
			
		||||
    "[[": { "type": "navigate.link.prev" },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								test/background/domains/global-mark.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								test/background/domains/global-mark.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import GlobalMark from 'background/domains/global-mark';
 | 
			
		||||
 | 
			
		||||
describe('background/domains/global-mark', () => {
 | 
			
		||||
  describe('constructor and getter', () => {
 | 
			
		||||
    let mark = new GlobalMark(1, 'http://example.com', 10, 30);
 | 
			
		||||
    expect(mark.tabId).to.equal(1);
 | 
			
		||||
    expect(mark.url).to.equal('http://example.com');
 | 
			
		||||
    expect(mark.x).to.equal(10);
 | 
			
		||||
    expect(mark.y).to.equal(30);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
import MemoryStorage from 'background/infrastructures/memory-storage';
 | 
			
		||||
 | 
			
		||||
describe("background/infrastructures/memory-storage", () => {
 | 
			
		||||
  let versionRepository;
 | 
			
		||||
 | 
			
		||||
  it('stores values', () => {
 | 
			
		||||
    let cache = new MemoryStorage();
 | 
			
		||||
    cache.set('number', 123);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										26
									
								
								test/background/repositories/mark.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								test/background/repositories/mark.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import MarkRepository from 'background/repositories/mark';
 | 
			
		||||
import GlobalMark from 'background/domains/global-mark';
 | 
			
		||||
 | 
			
		||||
describe('background/repositories/mark', () => {
 | 
			
		||||
  let repository;
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    repository = new MarkRepository;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('get and set', async() => {
 | 
			
		||||
    let mark = new GlobalMark(1, 'http://example.com', 10, 30);
 | 
			
		||||
 | 
			
		||||
    repository.setMark('A', mark);
 | 
			
		||||
 | 
			
		||||
    let got = await repository.getMark('A');
 | 
			
		||||
    expect(got).to.be.a('object');
 | 
			
		||||
    expect(got.tabId).to.equal(1);
 | 
			
		||||
    expect(got.url).to.equal('http://example.com');
 | 
			
		||||
    expect(got.x).to.equal(10);
 | 
			
		||||
    expect(got.y).to.equal(30);
 | 
			
		||||
 | 
			
		||||
    got = await repository.getMark('B');
 | 
			
		||||
    expect(got).to.be.undefined;
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										35
									
								
								test/content/actions/mark.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								test/content/actions/mark.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import actions from 'content/actions';
 | 
			
		||||
import * as markActions from 'content/actions/mark';
 | 
			
		||||
 | 
			
		||||
describe('mark actions', () => {
 | 
			
		||||
  describe('startSet', () => {
 | 
			
		||||
    it('create MARK_START_SET action', () => {
 | 
			
		||||
      let action = markActions.startSet();
 | 
			
		||||
      expect(action.type).to.equal(actions.MARK_START_SET);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('startJump', () => {
 | 
			
		||||
    it('create MARK_START_JUMP action', () => {
 | 
			
		||||
      let action = markActions.startJump();
 | 
			
		||||
      expect(action.type).to.equal(actions.MARK_START_JUMP);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('cancel', () => {
 | 
			
		||||
    it('create MARK_CANCEL action', () => {
 | 
			
		||||
      let action = markActions.cancel();
 | 
			
		||||
      expect(action.type).to.equal(actions.MARK_CANCEL);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('setLocal', () => {
 | 
			
		||||
    it('create setLocal action', () => {
 | 
			
		||||
      let action = markActions.setLocal('a', 20, 30);
 | 
			
		||||
      expect(action.type).to.equal(actions.MARK_SET_LOCAL);
 | 
			
		||||
      expect(action.key).to.equal('a');
 | 
			
		||||
      expect(action.x).to.equal(20);
 | 
			
		||||
      expect(action.y).to.equal(30);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										41
									
								
								test/content/reducers/mark.test.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								test/content/reducers/mark.test.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
import actions from 'content/actions';
 | 
			
		||||
import reducer from 'content/reducers/mark';
 | 
			
		||||
 | 
			
		||||
describe("mark reducer", () => {
 | 
			
		||||
  it('return the initial state', () => {
 | 
			
		||||
    let state = reducer(undefined, {});
 | 
			
		||||
    expect(state.setMode).to.be.false;
 | 
			
		||||
    expect(state.jumpMode).to.be.false;
 | 
			
		||||
    expect(state.marks).to.be.empty;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('starts set mode', () => {
 | 
			
		||||
    let action = { type: actions.MARK_START_SET };
 | 
			
		||||
    let state = reducer(undefined, action);
 | 
			
		||||
    expect(state.setMode).to.be.true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('starts jump mode', () => {
 | 
			
		||||
    let action = { type: actions.MARK_START_JUMP };
 | 
			
		||||
    let state = reducer(undefined, action);
 | 
			
		||||
    expect(state.jumpMode).to.be.true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('cancels set and jump mode', () => {
 | 
			
		||||
    let action = { type: actions.MARK_CANCEL };
 | 
			
		||||
    let state = reducer({ setMode: true }, action);
 | 
			
		||||
    expect(state.setMode).to.be.false;
 | 
			
		||||
 | 
			
		||||
    state = reducer({ jumpMode: true }, action);
 | 
			
		||||
    expect(state.jumpMode).to.be.false;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('stores local mark', () => {
 | 
			
		||||
    let action = { type: actions.MARK_SET_LOCAL, key: 'a', x: 20, y: 30};
 | 
			
		||||
    let state = reducer({ setMode: true }, action);
 | 
			
		||||
    expect(state.setMode).to.be.false;
 | 
			
		||||
    expect(state.marks['a']).to.be.an('object')
 | 
			
		||||
    expect(state.marks['a'].x).to.equal(20)
 | 
			
		||||
    expect(state.marks['a'].y).to.equal(30)
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in a new issue