Instantly share code, notes, and snippets.
Created
June 1, 2023 10:32
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save KajSzy/4ee15d2c52282a8014d7bee3a29e0356 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | |
| // @ts-ignore | |
| const connection = window.__REDUX_DEVTOOLS_EXTENSION__?.connect({ | |
| // https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md#options | |
| name: 'Breadcrumb', | |
| trace: true, | |
| }); | |
| const breadcrumbSubject = new BehaviorSubject<BreadcrumbState>([]); | |
| class BreadcrumbController { | |
| static instance: BreadcrumbController = new BreadcrumbController(); | |
| // this should overwrite HashRouter with custom History provided as props | |
| history = createHashHistory(); | |
| referer?: BreadcrumbEntry['pathname']; | |
| constructor() { | |
| if (process.env.NODE_ENV === 'development') { | |
| connection?.init(); | |
| } | |
| } | |
| private get pathname() { | |
| return window.location.hash.slice(1); | |
| } | |
| get recentEntry(): BreadcrumbEntry | undefined { | |
| return getLastElementFromArray(this.state); | |
| } | |
| get recentFocus(): string | undefined { | |
| if (this.recentEntry?.focusKeys) { | |
| return getLastElementFromArray(this.recentEntry.focusKeys); | |
| } | |
| } | |
| get currentEntry(): BreadcrumbEntry | undefined { | |
| return this.state.find((entry) => entry.pathname === this.pathname); | |
| } | |
| get state() { | |
| return breadcrumbSubject.getValue(); | |
| } | |
| private set state(entries: ReturnType<typeof breadcrumbSubject.getValue>) { | |
| breadcrumbSubject.next(entries); | |
| } | |
| get allFocusKeys() { | |
| return this.state.map((entry) => entry.focusKeys).flat(); | |
| } | |
| get allEntries() { | |
| return this.state.map((entry) => entry.pathname); | |
| } | |
| public goBack(setFocus: FocusableProps['setFocus']) { | |
| const recentEntry = this.recentEntry; | |
| const recentFocusKey = this.recentFocus; | |
| if (recentFocusKey) { | |
| this.state = this.state.map((entry) => { | |
| if (entry === recentEntry) { | |
| return { | |
| ...entry, | |
| focusKeys: removeLastElementFromArray(entry.focusKeys), | |
| }; | |
| } | |
| return entry; | |
| }); | |
| this.sendToDevTools('GO_BACK [FOCUS_KEY]'); | |
| setFocus(recentFocusKey); | |
| return; | |
| } | |
| this.referer = recentEntry?.pathname; | |
| this.state = removeLastElementFromArray(this.state); | |
| if (this.recentEntry) { | |
| this.history.replace(this.recentEntry.pathname); | |
| if (this.recentFocus) { | |
| setFocus(this.recentFocus); | |
| this.state = this.state.map((entry) => { | |
| if (entry === this.recentEntry) { | |
| return { | |
| ...entry, | |
| focusKeys: removeLastElementFromArray(entry.focusKeys), | |
| }; | |
| } | |
| return entry; | |
| }); | |
| } | |
| this.sendToDevTools('GO_BACK [PATHNAME]'); | |
| return; | |
| } | |
| // if there is no entry left, should prompt exit screen | |
| // on going back from exit screen will move user to home | |
| this.state = [ | |
| { | |
| pathname: generatePath(routeKeys['/my-mix/:slug?']), | |
| focusKeys: [focusKeys.navigation.home], | |
| }, | |
| ]; | |
| this.exit(); | |
| return; | |
| } | |
| /** | |
| * should be called only on first enter of app | |
| */ | |
| public setInitialState(entries: BreadcrumbEntry[]) { | |
| const currentLocation = this.pathname; | |
| if (this.state.length !== 0) { | |
| return; | |
| } | |
| this.state = entries; | |
| this.sendToDevTools('SET_INITIAL_STATE'); | |
| // fake populate history | |
| entries.forEach((entry) => { | |
| this.history.push(entry.pathname); | |
| }); | |
| this.history.push(currentLocation); | |
| return this.state; | |
| } | |
| public getEntryByPath(path: keyof typeof routeKeys) { | |
| return this.state.find((entry) => matchPath(entry.pathname, path)); | |
| } | |
| public pushFocus(focusKey: string) { | |
| if (!this.state.some((entry) => entry === this.currentEntry)) { | |
| this.push(this.pathname); | |
| } | |
| this.state = this.state.map((entry, index) => { | |
| const lastIndex = findLastIndex(this.state, (el) => el.pathname === this.currentEntry?.pathname); | |
| if (entry.pathname === this.currentEntry?.pathname && lastIndex === index) { | |
| return { | |
| ...entry, | |
| focusKeys: [...entry.focusKeys, focusKey], | |
| }; | |
| } | |
| return entry; | |
| }); | |
| this.sendToDevTools('PUSH_FOCUS'); | |
| return this.currentEntry; | |
| } | |
| public push(pathname: string, shouldMutateState = true) { | |
| if (shouldMutateState) { | |
| this.state = [ | |
| ...this.state, | |
| { | |
| pathname, | |
| focusKeys: [], | |
| }, | |
| ]; | |
| } | |
| this.referer = undefined; | |
| this.sendToDevTools(shouldMutateState ? 'PUSH' : 'PUSH WITHOUT MUTATION'); | |
| // can not be replaced with "replace" due to default browser behavior | |
| this.history.push(pathname); | |
| } | |
| public replace(pathname: string, shouldMutateState = true) { | |
| if (this.recentEntry?.pathname === pathname) { | |
| return; | |
| } | |
| if (shouldMutateState) { | |
| this.state = [ | |
| ...removeLastElementFromArray(this.state), | |
| { | |
| pathname, | |
| focusKeys: [], | |
| }, | |
| ]; | |
| } | |
| this.sendToDevTools(shouldMutateState ? 'REPLACE' : 'REPLACE WITHOUT MUTATION'); | |
| this.history.replace(pathname); | |
| } | |
| public replaceFocus(focusKey: string) { | |
| if (!this.state.find((entry) => entry === this.currentEntry)) { | |
| this.push(this.pathname); | |
| } | |
| this.state = this.state.map((entry) => { | |
| if (entry === this.currentEntry) { | |
| return { | |
| ...entry, | |
| focusKeys: [...removeLastElementFromArray(entry.focusKeys), focusKey], | |
| }; | |
| } | |
| return entry; | |
| }); | |
| this.sendToDevTools('REPLACE_FOCUS'); | |
| } | |
| public replaceFocusByPath(path: keyof typeof routeKeys, focusKeyToReplace: string, newfocusKey: string) { | |
| const entry = this.getEntryByPath(path); | |
| if (!entry || !entry?.focusKeys.some((key) => key === focusKeyToReplace)) { | |
| return; | |
| } | |
| this.state = this.state.map((entry) => { | |
| if (matchPath(entry.pathname, path)) { | |
| const newFocusKeys = entry.focusKeys; | |
| const index = newFocusKeys.indexOf(focusKeyToReplace); | |
| newFocusKeys[index] = newfocusKey; | |
| return { | |
| ...entry, | |
| focusKeys: newFocusKeys, | |
| }; | |
| } | |
| return entry; | |
| }); | |
| this.sendToDevTools('REPLACE_FOCUS_BY_PATH'); | |
| } | |
| public removeFocus(focusKey: string) { | |
| if (!this.allFocusKeys.includes(focusKey)) { | |
| return; | |
| } | |
| this.state = this.removeFocusByKey(focusKey); | |
| this.sendToDevTools('REMOVE_FOCUS'); | |
| return this.currentEntry; | |
| } | |
| public removeFocusForManyItems(focusKeyPart: string) { | |
| if (!this.allFocusKeys.some((focusKey) => focusKey.includes(focusKeyPart))) { | |
| return; | |
| } | |
| this.state = this.removeFocusByKeyPart(focusKeyPart); | |
| this.sendToDevTools('REMOVE_FOCUS'); | |
| return this.currentEntry; | |
| } | |
| public removeRecentFocus() { | |
| if (!this.recentFocus) { | |
| return; | |
| } | |
| this.state = this.removeFocusByKey(this.recentFocus); | |
| this.sendToDevTools('REMOVE_RECENT_FOCUS'); | |
| return this.currentEntry; | |
| } | |
| public removeFocusByPath(path: keyof typeof routeKeys, focusKey: string) { | |
| const entry = this.getEntryByPath(path); | |
| if (!entry || !entry?.focusKeys.some((key) => key === focusKey)) { | |
| return; | |
| } | |
| this.state = this.state.map((entry) => { | |
| if (matchPath(entry.pathname, path)) { | |
| return { | |
| ...entry, | |
| focusKeys: entry.focusKeys.filter((key) => key !== focusKey), | |
| }; | |
| } | |
| return entry; | |
| }); | |
| this.sendToDevTools('REMOVE_FOCUS_BY_PATH'); | |
| } | |
| public remove(pathname: Parameters<typeof matchPath>[1]) { | |
| if (this.allEntries.some((entry) => matchPath(entry, pathname))) { | |
| this.state = this.state.filter((entry) => !matchPath(entry.pathname, pathname)); | |
| this.sendToDevTools('REMOVE'); | |
| } | |
| } | |
| public removeRecent() { | |
| this.state = removeLastElementFromArray(this.state); | |
| this.sendToDevTools('REMOVE RECENT'); | |
| } | |
| public matchRecentEntry(pathname: Parameters<typeof matchPath>[1]): boolean { | |
| if (!this.recentEntry) { | |
| return false; | |
| } | |
| return Boolean(matchPath(this.recentEntry.pathname, pathname)); | |
| } | |
| public exit() { | |
| if (this.currentEntry?.pathname === '/exit') { | |
| return; | |
| } | |
| this.sendToDevTools('EXIT'); | |
| this.push('/exit'); | |
| } | |
| public resetState() { | |
| this.state = []; | |
| this.sendToDevTools('RESET'); | |
| } | |
| private removeFocusByKey(focusKey: string) { | |
| return this.state.map((entry) => { | |
| if (entry === this.currentEntry) { | |
| return { | |
| ...entry, | |
| focusKeys: entry.focusKeys.filter((key) => key !== focusKey), | |
| }; | |
| } | |
| return entry; | |
| }); | |
| } | |
| private removeFocusByKeyPart(focusKeyPart: string) { | |
| return this.state.map((entry) => { | |
| if (entry === this.currentEntry) { | |
| return { | |
| ...entry, | |
| focusKeys: entry.focusKeys.filter((key) => !key.includes(focusKeyPart)), | |
| }; | |
| } | |
| return entry; | |
| }); | |
| } | |
| private sendToDevTools(action: string) { | |
| if (process.env.NODE_ENV === 'development') { | |
| connection?.send(action, this.state); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment