mirror of
				https://github.com/Redocly/redoc.git
				synced 2025-10-30 23:37:28 +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