feat(): Fix navigation with # in markdown

This commit is contained in:
Depickere Sven 2023-04-25 15:35:03 +01:00
parent 99aec2fffd
commit 63731f9dd7
3 changed files with 74 additions and 19 deletions

View File

@ -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();

View File

@ -18,10 +18,16 @@ export class HistoryService {
get currentId(): string {
if (IS_BROWSER) {
if (this.shouldQueryParamNavigationBeUsed()) {
// 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));
}
}
return '';
}
@ -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) {
if (this.shouldQueryParamNavigationBeUsed()) {
window.history.replaceState(null, '', this.getFullUrl(id));
} else {
window.history.replaceState(
null,
'',
window.location.href.split(this.getHrefSplitCharacter())[0] + this.linkForId(id),
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 != '');
}
}

View File

@ -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) {