From 63731f9dd7b477d8784999feaa29d5d00e845e8c Mon Sep 17 00:00:00 2001 From: Depickere Sven Date: Tue, 25 Apr 2023 15:35:03 +0100 Subject: [PATCH] feat(): Fix navigation with # in markdown --- src/services/AppStore.ts | 2 +- src/services/HistoryService.ts | 62 ++++++++++++++++++++++++++-------- src/services/MenuStore.ts | 29 ++++++++++++++-- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 6886f657..c5882460 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -73,7 +73,7 @@ export class AppStore { MenuStore.updateOnHistory(this.history.currentId, this.scroll); this.spec = new SpecStore(spec, specUrl, this.options); - this.menu = new MenuStore(this.spec, this.scroll, this.history); + this.menu = new MenuStore(this.spec, this.scroll, this.history, this.options); if (!this.options.disableSearch) { this.search = new SearchStore(); diff --git a/src/services/HistoryService.ts b/src/services/HistoryService.ts index cd89dc07..7ad0d1a4 100644 --- a/src/services/HistoryService.ts +++ b/src/services/HistoryService.ts @@ -18,7 +18,13 @@ export class HistoryService { get currentId(): string { if (IS_BROWSER) { if (this.shouldQueryParamNavigationBeUsed()) { - return this.getQueryParams(window.location.search); + // When the window.location.hash is not empty this means that we have clicked on + // router that's for example stored in the description via markdown + if (window.location.hash == '') { + return this.getQueryParams(window.location.search); + } else { + return decodeURIComponent(window.location.hash.substring(1)); + } } else { return decodeURIComponent(window.location.hash.substring(1)); } @@ -30,7 +36,11 @@ export class HistoryService { if (!id) { return ''; } - return this.getHrefSplitCharacter() + id; + if (this.shouldQueryParamNavigationBeUsed()) { + return this.getFullUrl(id); + } else { + return '#' + id; + } } subscribe(cb): () => void { @@ -61,23 +71,33 @@ export class HistoryService { return; } - if (id == null || id === this.currentId) { + // If there currentId and the ID are equal but there is still + // a hash left when using query param navigation + // that means that the URL hasn't been overridden + if ( + id == null || + (id === this.currentId && this.checkIfThereIsHashLeftWhenQueryParamNavigationShouldBeUsed()) + ) { return; } if (rewriteHistory) { - window.history.replaceState( - null, - '', - window.location.href.split(this.getHrefSplitCharacter())[0] + this.linkForId(id), - ); + if (this.shouldQueryParamNavigationBeUsed()) { + window.history.replaceState(null, '', this.getFullUrl(id)); + } else { + window.history.replaceState( + null, + '', + window.location.href.split('#')[0] + this.linkForId(id), + ); + } return; } - window.history.pushState( - null, - '', - window.location.href.split(this.getHrefSplitCharacter())[0] + this.linkForId(id), - ); + if (this.shouldQueryParamNavigationBeUsed()) { + window.history.pushState(null, '', this.getFullUrl(id)); + } else { + window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id)); + } this.emit(); } @@ -93,7 +113,19 @@ export class HistoryService { return ''; } - private getHrefSplitCharacter(): string { - return this.shouldQueryParamNavigationBeUsed() ? '?redoc=' : '#'; + private getFullUrl(id: string): string { + const url = this.getUrl(); + // Override the hash, so it's removed when using query param navigation + url.hash = ''; + url.searchParams.set('redoc', id); + return url.toString(); + } + + private getUrl(): URL { + return new URL(window.location.href); + } + + private checkIfThereIsHashLeftWhenQueryParamNavigationShouldBeUsed(): boolean { + return !(this.shouldQueryParamNavigationBeUsed() && window.location.hash != ''); } } diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index 44b1d0cb..41660e6f 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -8,6 +8,7 @@ import { GROUP_DEPTH } from './MenuBuilder'; import type { SpecStore } from './models'; import type { ScrollService } from './ScrollService'; import type { IMenuItem } from './types'; +import { RedocNormalizedOptions } from './RedocNormalizedOptions'; /** Generic interface for MenuItems */ @@ -42,6 +43,7 @@ export class MenuStore { items: IMenuItem[]; flatItems: IMenuItem[]; + private options: RedocNormalizedOptions; /** * cached flattened menu items to support absolute indexing @@ -53,11 +55,19 @@ export class MenuStore { * * @param spec [SpecStore](#SpecStore) which contains page content structure * @param scroll scroll service instance used by this menu + * @param history the history service + * @param options the RedocNormalizedOptions that can be used to retrieve the config options */ - constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) { + constructor( + spec: SpecStore, + public scroll: ScrollService, + public history: HistoryService, + options: RedocNormalizedOptions, + ) { makeObservable(this); this.items = spec.contentItems; + this.options = options; this.flatItems = flattenByProp(this.items || [], 'items'); this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx)); @@ -126,16 +136,28 @@ export class MenuStore { item = this.flatItems.find(i => i.id === id); if (item) { - this.activateAndScroll(item, false); + this.activateAndScrollWithNavigationStrategy(item); } else { if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) { item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); - this.activateAndScroll(item, false); + this.activateAndScrollWithNavigationStrategy(item); } this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${escapeHTMLAttrChars(id)}"]`); } }; + private activateAndScrollWithNavigationStrategy(item: IMenuItem | undefined): void { + if (this.shouldQueryParamNavigationBeUsed()) { + this.activateAndScroll(item, true, true); + } else { + this.activateAndScroll(item, false); + } + } + + private shouldQueryParamNavigationBeUsed(): boolean { + return this.options?.userQueryParamToNavigate; + } + /** * get section/operation DOM Node related to the item or null if it doesn't exist * @param idx item absolute index @@ -237,6 +259,7 @@ export class MenuStore { ) { // item here can be a copy from search results so find corresponding item from menu const menuItem = (item && this.getItemById(item.id)) || item; + console.log('activateAndScroll', menuItem?.id, updateLocation, rewriteHistory); this.activate(menuItem, updateLocation, rewriteHistory); this.scrollToActive(); if (!menuItem || !menuItem.items.length) {