mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-31 07:47:29 +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 { 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); | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
|   }); | ||||
| } | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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'); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user