commit
8b72aac09a
24 changed files with 451 additions and 18 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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) || {}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -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); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -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, |
||||||
|
}; |
@ -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)); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
}); |
||||||
|
}); |
@ -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; |
||||||
|
}); |
||||||
|
}); |
@ -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); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -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 new issue