Make find as a clean architecture

This commit is contained in:
Shin'ya Ueoka 2019-05-11 11:37:18 +09:00
parent bacf83a320
commit 1ba1660269
15 changed files with 424 additions and 182 deletions

View file

@ -1,100 +0,0 @@
//
// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
// aWholeWord, aSearchInFrames);
//
// NOTE: window.find is not standard API
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
import * as messages from '../../shared/messages';
import * as actions from './index';
import * as consoleFrames from '../console-frames';
interface MyWindow extends Window {
find(
aString: string,
aCaseSensitive?: boolean,
aBackwards?: boolean,
aWrapAround?: boolean,
aWholeWord?: boolean,
aSearchInFrames?: boolean,
aShowDialog?: boolean): boolean;
}
// eslint-disable-next-line no-var, vars-on-top, init-declarations
declare var window: MyWindow;
const find = (str: string, backwards: boolean): boolean => {
let caseSensitive = false;
let wrapScan = true;
// NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
// because of same origin policy
// eslint-disable-next-line no-extra-parens
let found = window.find(str, caseSensitive, backwards, wrapScan);
if (found) {
return found;
}
let sel = window.getSelection();
if (sel) {
sel.removeAllRanges();
}
// eslint-disable-next-line no-extra-parens
return window.find(str, caseSensitive, backwards, wrapScan);
};
// eslint-disable-next-line max-statements
const findNext = async(
currentKeyword: string, reset: boolean, backwards: boolean,
): Promise<actions.FindAction> => {
if (reset) {
let sel = window.getSelection();
if (sel) {
sel.removeAllRanges();
}
}
let keyword = currentKeyword;
if (currentKeyword) {
browser.runtime.sendMessage({
type: messages.FIND_SET_KEYWORD,
keyword: currentKeyword,
});
} else {
keyword = await browser.runtime.sendMessage({
type: messages.FIND_GET_KEYWORD,
});
}
if (!keyword) {
await consoleFrames.postError('No previous search keywords');
return { type: actions.NOOP };
}
let found = find(keyword, backwards);
if (found) {
consoleFrames.postInfo('Pattern found: ' + keyword);
} else {
consoleFrames.postError('Pattern not found: ' + keyword);
}
return {
type: actions.FIND_SET_KEYWORD,
keyword,
found,
};
};
const next = (
currentKeyword: string, reset: boolean,
): Promise<actions.FindAction> => {
return findNext(currentKeyword, reset, false);
};
const prev = (
currentKeyword: string, reset: boolean,
): Promise<actions.FindAction> => {
return findNext(currentKeyword, reset, true);
};
export { next, prev };

View file

@ -1,9 +1,6 @@
import Redux from 'redux';
import * as keyUtils from '../../shared/utils/keys';
// Find
export const FIND_SET_KEYWORD = 'find.set.keyword';
// User input
export const INPUT_KEY_PRESS = 'input.key.press';
export const INPUT_CLEAR_KEYS = 'input.clear.keys';
@ -27,12 +24,6 @@ export const MARK_SET_LOCAL = 'mark.set.local';
export const NOOP = 'noop';
export interface FindSetKeywordAction extends Redux.Action {
type: typeof FIND_SET_KEYWORD;
keyword: string;
found: boolean;
}
export interface InputKeyPressAction extends Redux.Action {
type: typeof INPUT_KEY_PRESS;
key: keyUtils.Key;
@ -84,7 +75,6 @@ export interface NoopAction extends Redux.Action {
type: typeof NOOP;
}
export type FindAction = FindSetKeywordAction | NoopAction;
export type InputAction = InputKeyPressAction | InputClearKeysAction;
export type FollowAction =
FollowControllerEnableAction | FollowControllerDisableAction |
@ -94,7 +84,6 @@ export type MarkAction =
MarkCancelAction | MarkSetLocalAction | NoopAction;
export type Action =
FindAction |
InputAction |
FollowAction |
MarkAction |

View file

@ -0,0 +1,30 @@
import * as messages from '../../shared/messages';
export default interface ConsoleClient {
info(text: string): Promise<void>;
error(text: string): Promise<void>;
// eslint-disable-next-line semi
}
export class ConsoleClientImpl implements ConsoleClient {
async info(text: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.CONSOLE_FRAME_MESSAGE,
message: {
type: messages.CONSOLE_SHOW_INFO,
text,
},
});
}
async error(text: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.CONSOLE_FRAME_MESSAGE,
message: {
type: messages.CONSOLE_SHOW_ERROR,
text,
},
});
}
}

View file

@ -0,0 +1,25 @@
import * as messages from '../../shared/messages';
export default interface FindClient {
getGlobalLastKeyword(): Promise<string | null>;
setGlobalLastKeyword(keyword: string): Promise<void>;
// eslint-disable-next-line semi
}
export class FindClientImpl implements FindClient {
async getGlobalLastKeyword(): Promise<string | null> {
let keyword = await browser.runtime.sendMessage({
type: messages.FIND_GET_KEYWORD,
});
return keyword as string;
}
async setGlobalLastKeyword(keyword: string): Promise<void> {
await browser.runtime.sendMessage({
type: messages.FIND_SET_KEYWORD,
keyword: keyword,
});
}
}

View file

@ -1,13 +1,12 @@
import * as findActions from '../../actions/find';
import * as messages from '../../../shared/messages';
import MessageListener from '../../MessageListener';
import FindUseCase from '../../usecases/FindUseCase';
let findUseCase = new FindUseCase();
export default class FindComponent {
private store: any;
constructor(store: any) {
this.store = store;
constructor() {
new MessageListener().onWebMessage(this.onMessage.bind(this));
}
@ -20,27 +19,18 @@ export default class FindComponent {
case messages.FIND_PREV:
return this.prev();
}
return Promise.resolve();
}
start(text: string) {
let state = this.store.getState().find;
if (text.length === 0) {
return this.store.dispatch(
findActions.next(state.keyword as string, true));
}
return this.store.dispatch(findActions.next(text, true));
return findUseCase.startFind(text.length === 0 ? null : text);
}
next() {
let state = this.store.getState().find;
return this.store.dispatch(
findActions.next(state.keyword as string, false));
return findUseCase.findNext();
}
prev() {
let state = this.store.getState().find;
return this.store.dispatch(
findActions.prev(state.keyword as string, false));
return findUseCase.findPrev();
}
}

View file

@ -17,7 +17,7 @@ export default class TopContent {
new CommonComponent(win, store); // eslint-disable-line no-new
new FollowController(win, store); // eslint-disable-line no-new
new FindComponent(store); // eslint-disable-line no-new
new FindComponent(); // eslint-disable-line no-new
// TODO make component
consoleFrames.initialize(this.win.document);

View file

@ -0,0 +1,59 @@
import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient';
export default interface FindPresenter {
find(keyword: string, backwards: boolean): boolean;
clearSelection(): void;
// eslint-disable-next-line semi
}
// window.find(aString, aCaseSensitive, aBackwards, aWrapAround,
// aWholeWord, aSearchInFrames);
//
// NOTE: window.find is not standard API
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
interface MyWindow extends Window {
find(
aString: string,
aCaseSensitive?: boolean,
aBackwards?: boolean,
aWrapAround?: boolean,
aWholeWord?: boolean,
aSearchInFrames?: boolean,
aShowDialog?: boolean): boolean;
}
// eslint-disable-next-line no-var, vars-on-top, init-declarations
declare var window: MyWindow;
export class FindPresenterImpl implements FindPresenter {
private consoleClient: ConsoleClient;
constructor({ consoleClient = new ConsoleClientImpl() } = {}) {
this.consoleClient = consoleClient;
}
find(keyword: string, backwards: boolean): boolean {
let caseSensitive = false;
let wrapScan = true;
// NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work
// because of same origin policy
let found = window.find(keyword, caseSensitive, backwards, wrapScan);
if (found) {
return found;
}
this.clearSelection();
return window.find(keyword, caseSensitive, backwards, wrapScan);
}
clearSelection(): void {
let sel = window.getSelection();
if (sel) {
sel.removeAllRanges();
}
}
}

View file

@ -1,25 +0,0 @@
import * as actions from '../actions';
export interface State {
keyword: string | null;
found: boolean;
}
const defaultState: State = {
keyword: null,
found: false,
};
export default function reducer(
state: State = defaultState,
action: actions.FindAction,
): State {
switch (action.type) {
case actions.FIND_SET_KEYWORD:
return { ...state,
keyword: action.keyword,
found: action.found, };
default:
return state;
}
}

View file

@ -1,17 +1,15 @@
import { combineReducers } from 'redux';
import find, { State as FindState } from './find';
import input, { State as InputState } from './input';
import followController, { State as FollowControllerState }
from './follow-controller';
import mark, { State as MarkState } from './mark';
export interface State {
find: FindState;
input: InputState;
followController: FollowControllerState;
mark: MarkState;
}
export default combineReducers({
find, input, followController, mark,
input, followController, mark,
});

View file

@ -0,0 +1,19 @@
export default interface FindRepository {
getLastKeyword(): string | null;
setLastKeyword(keyword: string): void;
// eslint-disable-next-line semi
}
let current: string | null = null;
export class FindRepositoryImpl implements FindRepository {
getLastKeyword(): string | null {
return current;
}
setLastKeyword(keyword: string): void {
current = keyword;
}
}

View file

@ -18,5 +18,4 @@ export class SettingRepositoryImpl implements SettingRepository {
get(): Settings {
return current;
}
}

View file

@ -0,0 +1,81 @@
import FindPresenter, { FindPresenterImpl } from '../presenters/FindPresenter';
import FindRepository, { FindRepositoryImpl }
from '../repositories/FindRepository';
import FindClient, { FindClientImpl } from '../client/FindClient';
import ConsoleClient, { ConsoleClientImpl } from '../client/ConsoleClient';
export default class FindUseCase {
private presenter: FindPresenter;
private repository: FindRepository;
private client: FindClient;
private consoleClient: ConsoleClient;
constructor({
presenter = new FindPresenterImpl() as FindPresenter,
repository = new FindRepositoryImpl(),
client = new FindClientImpl(),
consoleClient = new ConsoleClientImpl(),
} = {}) {
this.presenter = presenter;
this.repository = repository;
this.client = client;
this.consoleClient = consoleClient;
}
async startFind(keyword: string | null): Promise<void> {
this.presenter.clearSelection();
if (keyword) {
this.saveKeyword(keyword);
} else {
let lastKeyword = await this.getKeyword();
if (!lastKeyword) {
return this.showNoLastKeywordError();
}
this.saveKeyword(lastKeyword);
}
return this.findNext();
}
findNext(): Promise<void> {
return this.findNextPrev(false);
}
findPrev(): Promise<void> {
return this.findNextPrev(true);
}
private async findNextPrev(
backwards: boolean,
): Promise<void> {
let keyword = await this.getKeyword();
if (!keyword) {
return this.showNoLastKeywordError();
}
let found = this.presenter.find(keyword, backwards);
if (found) {
this.consoleClient.info('Pattern found: ' + keyword);
} else {
this.consoleClient.error('Pattern not found: ' + keyword);
}
}
private async getKeyword(): Promise<string | null> {
let keyword = this.repository.getLastKeyword();
if (!keyword) {
keyword = await this.client.getGlobalLastKeyword();
}
return keyword;
}
private async saveKeyword(keyword: string): Promise<void> {
this.repository.setLastKeyword(keyword);
await this.client.setGlobalLastKeyword(keyword);
}
private async showNoLastKeywordError(): Promise<void> {
await this.consoleClient.error('No previous search keywords');
}
}