mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-22 16:46:34 +03:00
chore: refactor HistoryService
This commit is contained in:
parent
d3d35189f5
commit
cfddb3afe1
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user