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 { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { HistoryService } from './HistoryService';
import { history } from './HistoryService';
import { MarkerService } from './MarkerService';
import { MenuStore } from './MenuStore';
import { SpecStore } from './models';
@ -71,10 +71,10 @@ export class AppStore {
this.scroll = new ScrollService(this.options);
// 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.menu = new MenuStore(this.spec, this.scroll);
this.menu = new MenuStore(this.spec, this.scroll, history);
if (!this.options.disableSearch) {
this.search = new SearchStore();
@ -89,7 +89,7 @@ export class AppStore {
}
onDidMount() {
this.menu.updateOnHash();
this.menu.updateOnHistory();
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;
}
export class IntHistoryService {
export class HistoryService {
private _emiter;
constructor() {
@ -16,8 +16,12 @@ export class IntHistoryService {
this.bind();
}
get hash(): string {
return IS_BROWSER ? window.location.hash : '';
get currentId(): string {
return IS_BROWSER ? window.location.hash.substring(1) : '';
}
linkForId(id: string) {
return '#' + id;
}
subscribe(cb): () => void {
@ -26,7 +30,7 @@ export class IntHistoryService {
}
emit = () => {
this._emiter.emit(EVENT, this.hash);
this._emiter.emit(EVENT, this.currentId);
};
bind() {
@ -43,26 +47,31 @@ export class IntHistoryService {
@bind
@debounce
update(hash: string | null, rewriteHistory: boolean = false) {
if (hash == null || isSameHash(hash, this.hash)) {
replace(id: string | null, rewriteHistory: boolean = false) {
if (!IS_BROWSER) {
return;
}
if (id == null || isSameHash(id, this.currentId)) {
return;
}
if (rewriteHistory) {
if (IS_BROWSER) {
window.history.replaceState(null, '', window.location.href.split('#')[0] + '#' + hash);
}
window.history.replaceState(
null,
'',
window.location.href.split('#')[0] + this.linkForId(id),
);
return;
}
if (IS_BROWSER) {
window.history.pushState(null, '', window.location.href.split('#')[0] + '#' + hash);
}
window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id));
}
}
export const HistoryService = new IntHistoryService();
export const history = new HistoryService();
if (module.hot) {
module.hot.dispose(() => {
HistoryService.dispose();
history.dispose();
});
}

View File

@ -2,10 +2,10 @@ import { action, observable } from 'mobx';
import { querySelector } from '../utils/dom';
import { SpecStore } from './models';
import { HistoryService } from './HistoryService';
import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService';
import { flattenByProp, normalizeHash } from '../utils';
import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { GROUP_DEPTH } from './MenuBuilder';
export type MenuItemGroupType = 'group' | 'tag' | 'section';
@ -42,11 +42,11 @@ export class MenuStore {
* Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster
*/
static updateOnHash(hash: string = HistoryService.hash, scroll: ScrollService) {
if (!hash) {
static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!id) {
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 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.flatItems = flattenByProp(this.items || [], 'items');
@ -84,7 +84,7 @@ export class MenuStore {
subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash);
this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
}
@action
@ -132,24 +132,23 @@ export class MenuStore {
/**
* update active items on hash change
* @param hash current hash
* @param id current hash
*/
updateOnHash = (hash: string = HistoryService.hash): boolean => {
if (!hash) {
return false;
updateOnHistory = (id: string = this.history.currentId) => {
if (!id) {
return;
}
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) {
this.activateAndScroll(item, false);
} 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));
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
* @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)
*/
@action
activate(
item: IMenuItem | undefined,
updateHash: boolean = true,
updateLocation: boolean = true,
rewriteHistory: boolean = false,
) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
@ -190,7 +189,7 @@ export class MenuStore {
}
this.deactivate(this.activeItem);
if (!item) {
HistoryService.update('', rewriteHistory);
this.history.replace('', rewriteHistory);
return;
}
@ -201,8 +200,8 @@ export class MenuStore {
}
this.activeItemIdx = item.absoluteIdx!;
if (updateHash) {
HistoryService.update(item.id, rewriteHistory);
if (updateLocation) {
this.history.replace(item.id, rewriteHistory);
}
item.activate();
@ -229,10 +228,14 @@ export class MenuStore {
* @see MenuStore.activate
*/
@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
const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateHash, rewriteHistory);
this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive();
if (!menuItem || !menuItem.items.length) {
this.closeSidebar();

View File

@ -1,25 +1,34 @@
import { HistoryService } from '../HistoryService';
import { history } from '../HistoryService';
describe('History service', () => {
test('should be an instance', () => {
expect(typeof HistoryService).not.toBe('function');
expect(HistoryService.subscribe).toBeDefined();
expect(typeof history).not.toBe('function');
expect(history.subscribe).toBeDefined();
});
test('History subscribe', () => {
const fn = jest.fn();
HistoryService.subscribe(fn);
HistoryService.emit();
history.subscribe(fn);
history.emit();
expect(fn).toHaveBeenCalled();
});
test('History subscribe should return unsubsribe function', () => {
const fn = jest.fn();
const unsubscribe = HistoryService.subscribe(fn);
HistoryService.emit();
const unsubscribe = history.subscribe(fn);
history.emit();
expect(fn).toHaveBeenCalled();
unsubscribe();
HistoryService.emit();
history.emit();
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(' ');
}
export function normalizeHash(hash: string): string {
return hash.startsWith('#') ? hash.substr(1) : hash;
}
// scrollIntoViewIfNeeded polyfill
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {