Skip to content

Instantly share code, notes, and snippets.

@KajSzy
Created June 1, 2023 10:32
Show Gist options
  • Select an option

  • Save KajSzy/4ee15d2c52282a8014d7bee3a29e0356 to your computer and use it in GitHub Desktop.

Select an option

Save KajSzy/4ee15d2c52282a8014d7bee3a29e0356 to your computer and use it in GitHub Desktop.
// 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