chore: refactor HistoryService

This commit is contained in:
Roman Hotsiy 2018-08-17 14:50:58 +03:00
parent d3d35189f5
commit cfddb3afe1
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
5 changed files with 69 additions and 52 deletions

View File

@ -2,7 +2,7 @@ import { observe } from 'mobx';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { HistoryService } from './HistoryService'; import { history } from './HistoryService';
import { MarkerService } from './MarkerService'; import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore'; import { MenuStore } from './MenuStore';
import { SpecStore } from './models'; import { SpecStore } from './models';
@ -71,10 +71,10 @@ export class AppStore {
this.scroll = new ScrollService(this.options); this.scroll = new ScrollService(this.options);
// update position statically based on hash (in case of SSR) // update position statically based on hash (in case of SSR)
MenuStore.updateOnHash(HistoryService.hash, this.scroll); MenuStore.updateOnHistory(history.currentId, this.scroll);
this.spec = new SpecStore(spec, specUrl, this.options); this.spec = new SpecStore(spec, specUrl, this.options);
this.menu = new MenuStore(this.spec, this.scroll); this.menu = new MenuStore(this.spec, this.scroll, history);
if (!this.options.disableSearch) { if (!this.options.disableSearch) {
this.search = new SearchStore(); this.search = new SearchStore();
@ -89,7 +89,7 @@ export class AppStore {
} }
onDidMount() { onDidMount() {
this.menu.updateOnHash(); this.menu.updateOnHistory();
this.updateMarkOnMenu(this.menu.activeItemIdx); this.updateMarkOnMenu(this.menu.activeItemIdx);
} }

View File

@ -8,7 +8,7 @@ function isSameHash(a: string, b: string): boolean {
return a === b || '#' + a === b || a === '#' + b; return a === b || '#' + a === b || a === '#' + b;
} }
export class IntHistoryService { export class HistoryService {
private _emiter; private _emiter;
constructor() { constructor() {
@ -16,8 +16,12 @@ export class IntHistoryService {
this.bind(); this.bind();
} }
get hash(): string { get currentId(): string {
return IS_BROWSER ? window.location.hash : ''; return IS_BROWSER ? window.location.hash.substring(1) : '';
}
linkForId(id: string) {
return '#' + id;
} }
subscribe(cb): () => void { subscribe(cb): () => void {
@ -26,7 +30,7 @@ export class IntHistoryService {
} }
emit = () => { emit = () => {
this._emiter.emit(EVENT, this.hash); this._emiter.emit(EVENT, this.currentId);
}; };
bind() { bind() {
@ -43,26 +47,31 @@ export class IntHistoryService {
@bind @bind
@debounce @debounce
update(hash: string | null, rewriteHistory: boolean = false) { replace(id: string | null, rewriteHistory: boolean = false) {
if (hash == null || isSameHash(hash, this.hash)) { if (!IS_BROWSER) {
return;
}
if (id == null || isSameHash(id, this.currentId)) {
return; return;
} }
if (rewriteHistory) { if (rewriteHistory) {
if (IS_BROWSER) { window.history.replaceState(
window.history.replaceState(null, '', window.location.href.split('#')[0] + '#' + hash); null,
} '',
window.location.href.split('#')[0] + this.linkForId(id),
);
return; return;
} }
if (IS_BROWSER) { window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id));
window.history.pushState(null, '', window.location.href.split('#')[0] + '#' + hash);
}
} }
} }
export const HistoryService = new IntHistoryService(); export const history = new HistoryService();
if (module.hot) { if (module.hot) {
module.hot.dispose(() => { module.hot.dispose(() => {
HistoryService.dispose(); history.dispose();
}); });
} }

View File

@ -2,10 +2,10 @@ import { action, observable } from 'mobx';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { SpecStore } from './models'; import { SpecStore } from './models';
import { HistoryService } from './HistoryService'; import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { flattenByProp, normalizeHash } from '../utils'; import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder'; import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section'; export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -42,11 +42,11 @@ export class MenuStore {
* Statically try update scroll position * Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster * Used before hydrating from server-side rendered html to scroll page faster
*/ */
static updateOnHash(hash: string = HistoryService.hash, scroll: ScrollService) { static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!hash) { if (!id) {
return; return;
} }
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${normalizeHash(hash)}"]`); scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
/** /**
@ -73,7 +73,7 @@ export class MenuStore {
* @param spec [SpecStore](#SpecStore) which contains page content structure * @param spec [SpecStore](#SpecStore) which contains page content structure
* @param scroll scroll service instance used by this menu * @param scroll scroll service instance used by this menu
*/ */
constructor(spec: SpecStore, public scroll: ScrollService) { constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) {
this.items = spec.contentItems; this.items = spec.contentItems;
this.flatItems = flattenByProp(this.items || [], 'items'); this.flatItems = flattenByProp(this.items || [], 'items');
@ -84,7 +84,7 @@ export class MenuStore {
subscribe() { subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll); this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash); this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
} }
@action @action
@ -132,24 +132,23 @@ export class MenuStore {
/** /**
* update active items on hash change * update active items on hash change
* @param hash current hash * @param id current hash
*/ */
updateOnHash = (hash: string = HistoryService.hash): boolean => { updateOnHistory = (id: string = this.history.currentId) => {
if (!hash) { if (!id) {
return false; return;
} }
let item: IMenuItem | undefined; let item: IMenuItem | undefined;
hash = normalizeHash(hash);
item = this.flatItems.find(i => i.id === hash); item = this.flatItems.find(i => i.id === id);
if (item) { if (item) {
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} else { } else {
if (hash.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) { if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) {
item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activate(item); this.activate(item);
} }
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${hash}"]`); this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
}; };
@ -176,13 +175,13 @@ export class MenuStore {
/** /**
* activate menu item * activate menu item
* @param item item to activate * @param item item to activate
* @param updateHash [true] whether to update location hash * @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty) * @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty)
*/ */
@action @action
activate( activate(
item: IMenuItem | undefined, item: IMenuItem | undefined,
updateHash: boolean = true, updateLocation: boolean = true,
rewriteHistory: boolean = false, rewriteHistory: boolean = false,
) { ) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) { if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
@ -190,7 +189,7 @@ export class MenuStore {
} }
this.deactivate(this.activeItem); this.deactivate(this.activeItem);
if (!item) { if (!item) {
HistoryService.update('', rewriteHistory); this.history.replace('', rewriteHistory);
return; return;
} }
@ -201,8 +200,8 @@ export class MenuStore {
} }
this.activeItemIdx = item.absoluteIdx!; this.activeItemIdx = item.absoluteIdx!;
if (updateHash) { if (updateLocation) {
HistoryService.update(item.id, rewriteHistory); this.history.replace(item.id, rewriteHistory);
} }
item.activate(); item.activate();
@ -229,10 +228,14 @@ export class MenuStore {
* @see MenuStore.activate * @see MenuStore.activate
*/ */
@action.bound @action.bound
activateAndScroll(item: IMenuItem | undefined, updateHash?: boolean, rewriteHistory?: boolean) { activateAndScroll(
item: IMenuItem | undefined,
updateLocation?: boolean,
rewriteHistory?: boolean,
) {
// item here can be a copy from search results so find corresponding item from menu // item here can be a copy from search results so find corresponding item from menu
const menuItem = (item && this.getItemById(item.id)) || item; const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateHash, rewriteHistory); this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive(); this.scrollToActive();
if (!menuItem || !menuItem.items.length) { if (!menuItem || !menuItem.items.length) {
this.closeSidebar(); this.closeSidebar();

View File

@ -1,25 +1,34 @@
import { HistoryService } from '../HistoryService'; import { history } from '../HistoryService';
describe('History service', () => { describe('History service', () => {
test('should be an instance', () => { test('should be an instance', () => {
expect(typeof HistoryService).not.toBe('function'); expect(typeof history).not.toBe('function');
expect(HistoryService.subscribe).toBeDefined(); expect(history.subscribe).toBeDefined();
}); });
test('History subscribe', () => { test('History subscribe', () => {
const fn = jest.fn(); const fn = jest.fn();
HistoryService.subscribe(fn); history.subscribe(fn);
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
}); });
test('History subscribe should return unsubsribe function', () => { test('History subscribe should return unsubsribe function', () => {
const fn = jest.fn(); const fn = jest.fn();
const unsubscribe = HistoryService.subscribe(fn); const unsubscribe = history.subscribe(fn);
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
unsubscribe(); unsubscribe();
HistoryService.emit(); history.emit();
expect(fn).toHaveBeenCalledTimes(1); expect(fn).toHaveBeenCalledTimes(1);
}); });
test('currentId should return correct id', () => {
window.location.hash = '#testid';
expect(history.currentId).toEqual('testid');
});
test('should return correct link for id', () => {
expect(history.linkForId('testid')).toEqual('#testid');
});
}); });

View File

@ -24,10 +24,6 @@ export function html2Str(html: string): string {
.join(' '); .join(' ');
} }
export function normalizeHash(hash: string): string {
return hash.startsWith('#') ? hash.substr(1) : hash;
}
// scrollIntoViewIfNeeded polyfill // scrollIntoViewIfNeeded polyfill
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) { if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {