UNPKG

15.5 kBJavaScriptView Raw
1const _excluded = ["id"],
2 _excluded2 = ["kind", "story", "storyId"];
3
4function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
5
6import "core-js/modules/es.array.reduce.js";
7import global from 'global';
8import { toId, sanitize } from '@storybook/csf';
9import { PRELOAD_STORIES, STORY_PREPARED, UPDATE_STORY_ARGS, RESET_STORY_ARGS, STORY_ARGS_UPDATED, STORY_CHANGED, SELECT_STORY, SET_STORIES, STORY_SPECIFIED, STORY_INDEX_INVALIDATED, CONFIG_ERROR } from '@storybook/core-events';
10import deprecate from 'util-deprecate';
11import { logger } from '@storybook/client-logger';
12import { getEventMetadata } from '../lib/events';
13import { denormalizeStoryParameters, transformStoriesRawToStoriesHash, isStory, isRoot, transformStoryIndexToStoriesHash, getComponentLookupList, getStoriesLookupList } from '../lib/stories';
14const {
15 DOCS_MODE,
16 FEATURES,
17 fetch
18} = global;
19const STORY_INDEX_PATH = './stories.json';
20const deprecatedOptionsParameterWarnings = ['enableShortcuts', 'theme', 'showRoots'].reduce((acc, option) => {
21 acc[option] = deprecate(() => {}, `parameters.options.${option} is deprecated and will be removed in Storybook 7.0.
22To change this setting, use \`addons.setConfig\`. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-immutable-options-parameters
23 `);
24 return acc;
25}, {});
26
27function checkDeprecatedOptionParameters(options) {
28 if (!options) {
29 return;
30 }
31
32 Object.keys(options).forEach(option => {
33 if (deprecatedOptionsParameterWarnings[option]) {
34 deprecatedOptionsParameterWarnings[option]();
35 }
36 });
37}
38
39export const init = ({
40 fullAPI,
41 store,
42 navigate,
43 provider,
44 storyId: initialStoryId,
45 viewMode: initialViewMode
46}) => {
47 const api = {
48 storyId: toId,
49 getData: (storyId, refId) => {
50 const result = api.resolveStory(storyId, refId);
51 return isRoot(result) ? undefined : result;
52 },
53 isPrepared: (storyId, refId) => {
54 const data = api.getData(storyId, refId);
55
56 if (data.isLeaf) {
57 return data.prepared;
58 } // Groups are always prepared :shrug:
59
60
61 return true;
62 },
63 resolveStory: (storyId, refId) => {
64 const {
65 refs,
66 storiesHash
67 } = store.getState();
68
69 if (refId) {
70 return refs[refId].stories ? refs[refId].stories[storyId] : undefined;
71 }
72
73 return storiesHash ? storiesHash[storyId] : undefined;
74 },
75 getCurrentStoryData: () => {
76 const {
77 storyId,
78 refId
79 } = store.getState();
80 return api.getData(storyId, refId);
81 },
82 getParameters: (storyIdOrCombo, parameterName) => {
83 const {
84 storyId,
85 refId
86 } = typeof storyIdOrCombo === 'string' ? {
87 storyId: storyIdOrCombo,
88 refId: undefined
89 } : storyIdOrCombo;
90 const data = api.getData(storyId, refId);
91
92 if (isStory(data)) {
93 const {
94 parameters
95 } = data;
96
97 if (parameters) {
98 return parameterName ? parameters[parameterName] : parameters;
99 }
100
101 return {};
102 }
103
104 return null;
105 },
106 getCurrentParameter: parameterName => {
107 const {
108 storyId,
109 refId
110 } = store.getState();
111 const parameters = api.getParameters({
112 storyId,
113 refId
114 }, parameterName); // FIXME Returning falsey parameters breaks a bunch of toolbars code,
115 // so this strange logic needs to be here until various client code is updated.
116
117 return parameters || undefined;
118 },
119 jumpToComponent: direction => {
120 const {
121 storiesHash,
122 storyId,
123 refs,
124 refId
125 } = store.getState();
126 const story = api.getData(storyId, refId); // cannot navigate when there's no current selection
127
128 if (!story) {
129 return;
130 }
131
132 const hash = refId ? refs[refId].stories || {} : storiesHash;
133 const result = api.findSiblingStoryId(storyId, hash, direction, true);
134
135 if (result) {
136 api.selectStory(result, undefined, {
137 ref: refId
138 });
139 }
140 },
141 jumpToStory: direction => {
142 const {
143 storiesHash,
144 storyId,
145 refs,
146 refId
147 } = store.getState();
148 const story = api.getData(storyId, refId);
149
150 if (DOCS_MODE) {
151 api.jumpToComponent(direction);
152 return;
153 } // cannot navigate when there's no current selection
154
155
156 if (!story) {
157 return;
158 }
159
160 const hash = story.refId ? refs[story.refId].stories : storiesHash;
161 const result = api.findSiblingStoryId(storyId, hash, direction, false);
162
163 if (result) {
164 api.selectStory(result, undefined, {
165 ref: refId
166 });
167 }
168 },
169 setStories: async (input, error) => {
170 // Now create storiesHash by reordering the above by group
171 const hash = transformStoriesRawToStoriesHash(input, {
172 provider
173 });
174 await store.setState({
175 storiesHash: hash,
176 storiesConfigured: true,
177 storiesFailed: error
178 });
179 },
180 selectFirstStory: () => {
181 const {
182 storiesHash
183 } = store.getState();
184 const firstStory = Object.keys(storiesHash).find(k => !(storiesHash[k].children || Array.isArray(storiesHash[k])));
185
186 if (firstStory) {
187 api.selectStory(firstStory);
188 return;
189 }
190
191 navigate('/');
192 },
193 selectStory: (kindOrId = undefined, story = undefined, options = {}) => {
194 const {
195 ref,
196 viewMode: viewModeFromArgs
197 } = options;
198 const {
199 viewMode: viewModeFromState = 'story',
200 storyId,
201 storiesHash,
202 refs
203 } = store.getState();
204 const hash = ref ? refs[ref].stories : storiesHash;
205 const kindSlug = storyId === null || storyId === void 0 ? void 0 : storyId.split('--', 2)[0];
206
207 if (!story) {
208 const s = kindOrId ? hash[kindOrId] || hash[sanitize(kindOrId)] : hash[kindSlug]; // eslint-disable-next-line no-nested-ternary
209
210 const id = s ? s.children ? s.children[0] : s.id : kindOrId;
211 let viewMode = s && !isRoot(s) && (viewModeFromArgs || s.parameters.viewMode) ? s.parameters.viewMode : viewModeFromState; // Some viewModes are not story-specific, and we should reset viewMode
212 // to 'story' if one of those is active when navigating to another story
213
214 if (['settings', 'about', 'release'].includes(viewMode)) {
215 viewMode = 'story';
216 }
217
218 const p = s && s.refId ? `/${viewMode}/${s.refId}_${id}` : `/${viewMode}/${id}`;
219 navigate(p);
220 } else if (!kindOrId) {
221 // This is a slugified version of the kind, but that's OK, our toId function is idempotent
222 const id = toId(kindSlug, story);
223 api.selectStory(id, undefined, options);
224 } else {
225 const id = ref ? `${ref}_${toId(kindOrId, story)}` : toId(kindOrId, story);
226
227 if (hash[id]) {
228 api.selectStory(id, undefined, options);
229 } else {
230 // Support legacy API with component permalinks, where kind is `x/y` but permalink is 'z'
231 const k = hash[sanitize(kindOrId)];
232
233 if (k && k.children) {
234 const foundId = k.children.find(childId => hash[childId].name === story);
235
236 if (foundId) {
237 api.selectStory(foundId, undefined, options);
238 }
239 }
240 }
241 }
242 },
243
244 findLeafStoryId(storiesHash, storyId) {
245 if (storiesHash[storyId].isLeaf) {
246 return storyId;
247 }
248
249 const childStoryId = storiesHash[storyId].children[0];
250 return api.findLeafStoryId(storiesHash, childStoryId);
251 },
252
253 findSiblingStoryId(storyId, hash, direction, toSiblingGroup) {
254 if (toSiblingGroup) {
255 const lookupList = getComponentLookupList(hash);
256 const index = lookupList.findIndex(i => i.includes(storyId)); // cannot navigate beyond fist or last
257
258 if (index === lookupList.length - 1 && direction > 0) {
259 return;
260 }
261
262 if (index === 0 && direction < 0) {
263 return;
264 }
265
266 if (lookupList[index + direction]) {
267 // eslint-disable-next-line consistent-return
268 return lookupList[index + direction][0];
269 }
270
271 return;
272 }
273
274 const lookupList = getStoriesLookupList(hash);
275 const index = lookupList.indexOf(storyId); // cannot navigate beyond fist or last
276
277 if (index === lookupList.length - 1 && direction > 0) {
278 return;
279 }
280
281 if (index === 0 && direction < 0) {
282 return;
283 } // eslint-disable-next-line consistent-return
284
285
286 return lookupList[index + direction];
287 },
288
289 updateStoryArgs: (story, updatedArgs) => {
290 const {
291 id: storyId,
292 refId
293 } = story;
294 fullAPI.emit(UPDATE_STORY_ARGS, {
295 storyId,
296 updatedArgs,
297 options: {
298 target: refId ? `storybook-ref-${refId}` : 'storybook-preview-iframe'
299 }
300 });
301 },
302 resetStoryArgs: (story, argNames) => {
303 const {
304 id: storyId,
305 refId
306 } = story;
307 fullAPI.emit(RESET_STORY_ARGS, {
308 storyId,
309 argNames,
310 options: {
311 target: refId ? `storybook-ref-${refId}` : 'storybook-preview-iframe'
312 }
313 });
314 },
315 fetchStoryList: async () => {
316 try {
317 const result = await fetch(STORY_INDEX_PATH);
318 if (result.status !== 200) throw new Error(await result.text());
319 const storyIndex = await result.json(); // We can only do this if the stories.json is a proper storyIndex
320
321 if (storyIndex.v !== 3) {
322 logger.warn(`Skipping story index with version v${storyIndex.v}, awaiting SET_STORIES.`);
323 return;
324 }
325
326 await fullAPI.setStoryList(storyIndex);
327 } catch (err) {
328 store.setState({
329 storiesConfigured: true,
330 storiesFailed: err
331 });
332 }
333 },
334 setStoryList: async storyIndex => {
335 const hash = transformStoryIndexToStoriesHash(storyIndex, {
336 provider
337 });
338 await store.setState({
339 storiesHash: hash,
340 storiesConfigured: true,
341 storiesFailed: null
342 });
343 },
344 updateStory: async (storyId, update, ref) => {
345 if (!ref) {
346 const {
347 storiesHash
348 } = store.getState();
349 storiesHash[storyId] = Object.assign({}, storiesHash[storyId], update);
350 await store.setState({
351 storiesHash
352 });
353 } else {
354 const {
355 id: refId,
356 stories
357 } = ref;
358 stories[storyId] = Object.assign({}, stories[storyId], update);
359 await fullAPI.updateRef(refId, {
360 stories
361 });
362 }
363 }
364 };
365
366 const initModule = async () => {
367 // On initial load, the local iframe will select the first story (or other "selection specifier")
368 // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change.
369 fullAPI.on(STORY_SPECIFIED, function handler({
370 storyId,
371 viewMode
372 }) {
373 const {
374 sourceType
375 } = getEventMetadata(this, fullAPI);
376 if (fullAPI.isSettingsScreenActive()) return;
377
378 if (sourceType === 'local') {
379 // Special case -- if we are already at the story being specified (i.e. the user started at a given story),
380 // we don't need to change URL. See https://github.com/storybookjs/storybook/issues/11677
381 const state = store.getState();
382
383 if (state.storyId !== storyId || state.viewMode !== viewMode) {
384 navigate(`/${viewMode}/${storyId}`);
385 }
386 }
387 });
388 fullAPI.on(STORY_CHANGED, function handler() {
389 const {
390 sourceType
391 } = getEventMetadata(this, fullAPI);
392
393 if (sourceType === 'local') {
394 const options = fullAPI.getCurrentParameter('options');
395
396 if (options) {
397 checkDeprecatedOptionParameters(options);
398 fullAPI.setOptions(options);
399 }
400 }
401 });
402 fullAPI.on(STORY_PREPARED, function handler(_ref) {
403 let {
404 id
405 } = _ref,
406 update = _objectWithoutPropertiesLoose(_ref, _excluded);
407
408 const {
409 ref,
410 sourceType
411 } = getEventMetadata(this, fullAPI);
412 fullAPI.updateStory(id, Object.assign({}, update, {
413 prepared: true
414 }), ref);
415
416 if (!ref) {
417 if (!store.getState().hasCalledSetOptions) {
418 const {
419 options
420 } = update.parameters;
421 checkDeprecatedOptionParameters(options);
422 fullAPI.setOptions(options);
423 store.setState({
424 hasCalledSetOptions: true
425 });
426 }
427 } else {
428 fullAPI.updateRef(ref.id, {
429 ready: true
430 });
431 }
432
433 if (sourceType === 'local') {
434 const {
435 storyId,
436 storiesHash
437 } = store.getState(); // create a list of related stories to be preloaded
438
439 const toBePreloaded = Array.from(new Set([api.findSiblingStoryId(storyId, storiesHash, 1, true), api.findSiblingStoryId(storyId, storiesHash, -1, true)])).filter(Boolean);
440 fullAPI.emit(PRELOAD_STORIES, toBePreloaded);
441 }
442 });
443 fullAPI.on(SET_STORIES, function handler(data) {
444 const {
445 ref
446 } = getEventMetadata(this, fullAPI);
447 const stories = data.v ? denormalizeStoryParameters(data) : data.stories;
448
449 if (!ref) {
450 if (!data.v) {
451 throw new Error('Unexpected legacy SET_STORIES event from local source');
452 }
453
454 fullAPI.setStories(stories);
455 const options = fullAPI.getCurrentParameter('options');
456 checkDeprecatedOptionParameters(options);
457 fullAPI.setOptions(options);
458 } else {
459 fullAPI.setRef(ref.id, Object.assign({}, ref, data, {
460 stories
461 }), true);
462 }
463 });
464 fullAPI.on(SELECT_STORY, function handler(_ref2) {
465 let {
466 kind,
467 story,
468 storyId
469 } = _ref2,
470 rest = _objectWithoutPropertiesLoose(_ref2, _excluded2);
471
472 const {
473 ref
474 } = getEventMetadata(this, fullAPI);
475
476 if (!ref) {
477 fullAPI.selectStory(storyId || kind, story, rest);
478 } else {
479 fullAPI.selectStory(storyId || kind, story, Object.assign({}, rest, {
480 ref: ref.id
481 }));
482 }
483 });
484 fullAPI.on(STORY_ARGS_UPDATED, function handleStoryArgsUpdated({
485 storyId,
486 args
487 }) {
488 const {
489 ref
490 } = getEventMetadata(this, fullAPI);
491 fullAPI.updateStory(storyId, {
492 args
493 }, ref);
494 });
495 fullAPI.on(CONFIG_ERROR, function handleConfigError(err) {
496 store.setState({
497 storiesConfigured: true,
498 storiesFailed: err
499 });
500 });
501
502 if (FEATURES !== null && FEATURES !== void 0 && FEATURES.storyStoreV7) {
503 var _provider$serverChann;
504
505 (_provider$serverChann = provider.serverChannel) === null || _provider$serverChann === void 0 ? void 0 : _provider$serverChann.on(STORY_INDEX_INVALIDATED, () => fullAPI.fetchStoryList());
506 await fullAPI.fetchStoryList();
507 }
508 };
509
510 return {
511 api,
512 state: {
513 storiesHash: {},
514 storyId: initialStoryId,
515 viewMode: initialViewMode,
516 storiesConfigured: false,
517 hasCalledSetOptions: false
518 },
519 init: initModule
520 };
521};
\No newline at end of file