mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import global from 'global';
|
|
import { addons, makeDecorator } from '@storybook/preview-api';
|
|
import { STORY_CHANGED, SELECT_STORY } from '@storybook/core-events';
|
|
import type { StoryId, StoryName, ComponentTitle } from '@storybook/types';
|
|
import { toId } from '@storybook/csf';
|
|
import { PARAM_KEY } from './constants';
|
|
|
|
const { document, HTMLElement } = global;
|
|
|
|
interface ParamsId {
|
|
storyId: StoryId;
|
|
}
|
|
interface ParamsCombo {
|
|
kind?: ComponentTitle;
|
|
story?: StoryName;
|
|
}
|
|
|
|
function parseQuery(queryString: string) {
|
|
const query: Record<string, string> = {};
|
|
const pairs = (queryString[0] === '?' ? queryString.substring(1) : queryString)
|
|
.split('&')
|
|
.filter(Boolean);
|
|
|
|
// eslint-disable-next-line no-plusplus
|
|
for (let i = 0; i < pairs.length; i++) {
|
|
const pair = pairs[i].split('=');
|
|
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
|
|
}
|
|
return query;
|
|
}
|
|
|
|
export const navigate = (params: ParamsId | ParamsCombo) =>
|
|
addons.getChannel().emit(SELECT_STORY, params);
|
|
|
|
export const hrefTo = (title: ComponentTitle, name: StoryName): Promise<string> => {
|
|
return new Promise((resolve) => {
|
|
const { location } = document;
|
|
const query = parseQuery(location.search);
|
|
// @ts-expect-error (Converted from ts-ignore)
|
|
const existingId = [].concat(query.id)[0];
|
|
// @ts-expect-error (Converted from ts-ignore)
|
|
const titleToLink = title || existingId.split('--', 2)[0];
|
|
const id = toId(titleToLink, name);
|
|
const url = `${location.origin + location.pathname}?${Object.entries({ ...query, id })
|
|
.map((item) => `${item[0]}=${item[1]}`)
|
|
.join('&')}`;
|
|
|
|
resolve(url);
|
|
});
|
|
};
|
|
|
|
const valueOrCall = (args: string[]) => (value: string | ((...args: string[]) => string)) =>
|
|
typeof value === 'function' ? value(...args) : value;
|
|
|
|
export const linkTo =
|
|
(
|
|
idOrTitle: string | ((...args: any[]) => string),
|
|
nameInput?: string | ((...args: any[]) => string)
|
|
) =>
|
|
(...args: any[]) => {
|
|
const resolver = valueOrCall(args);
|
|
const title = resolver(idOrTitle);
|
|
const name = nameInput ? resolver(nameInput) : false;
|
|
|
|
if (title?.match(/--/) && !name) {
|
|
navigate({ storyId: title });
|
|
} else if (name && title) {
|
|
navigate({ kind: title, story: name });
|
|
} else if (title) {
|
|
navigate({ kind: title });
|
|
} else if (name) {
|
|
navigate({ story: name });
|
|
}
|
|
};
|
|
|
|
const linksListener = (e: Event) => {
|
|
const { target } = e;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const element = target as HTMLElement;
|
|
const { sbKind: kind, sbStory: story } = element.dataset;
|
|
if (kind || story) {
|
|
e.preventDefault();
|
|
navigate({ kind, story });
|
|
}
|
|
};
|
|
|
|
let hasListener = false;
|
|
|
|
const on = () => {
|
|
if (!hasListener) {
|
|
hasListener = true;
|
|
document.addEventListener('click', linksListener);
|
|
}
|
|
};
|
|
const off = () => {
|
|
if (hasListener) {
|
|
hasListener = false;
|
|
document.removeEventListener('click', linksListener);
|
|
}
|
|
};
|
|
|
|
export const withLinks = makeDecorator({
|
|
name: 'withLinks',
|
|
parameterName: PARAM_KEY,
|
|
wrapper: (getStory, context) => {
|
|
on();
|
|
addons.getChannel().once(STORY_CHANGED, off);
|
|
return getStory(context);
|
|
},
|
|
});
|