mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-04 17:41:06 +08:00
Merge pull request #2628 from storybooks/tmeasday/shilman-refactor-core-rebase
Merge `master` into `shilman/refactor-core`
This commit is contained in:
commit
b4dec5fa48
@ -27,6 +27,7 @@
|
||||
"@storybook/addon-links": "^3.3.3",
|
||||
"@storybook/addons": "^3.3.3",
|
||||
"@storybook/channel-postmessage": "^3.3.3",
|
||||
"@storybook/core": "^3.3.3",
|
||||
"@storybook/ui": "^3.3.3",
|
||||
"airbnb-js-shims": "^1.1.1",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
@ -59,7 +60,7 @@
|
||||
"postcss-flexbugs-fixes": "^3.0.0",
|
||||
"postcss-loader": "^2.0.5",
|
||||
"prop-types": "^15.5.10",
|
||||
"qs": "^6.4.0",
|
||||
"qs": "^6.5.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
|
94
app/angular/src/client/preview/client_api.js
vendored
94
app/angular/src/client/preview/client_api.js
vendored
@ -1,94 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
export default class ClientApi {
|
||||
constructor({ channel, storyStore }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._addons = {};
|
||||
this._globalDecorators = [];
|
||||
}
|
||||
|
||||
setAddon(addon) {
|
||||
this._addons = {
|
||||
...this._addons,
|
||||
...addon,
|
||||
};
|
||||
}
|
||||
|
||||
addDecorator(decorator) {
|
||||
this._globalDecorators.push(decorator);
|
||||
}
|
||||
|
||||
clearDecorators() {
|
||||
this._globalDecorators = [];
|
||||
}
|
||||
|
||||
storiesOf(kind, m) {
|
||||
if (!kind && typeof kind !== 'string') {
|
||||
throw new Error('Invalid or missing kind provided for stories, should be a string');
|
||||
}
|
||||
|
||||
if (m && m.hot) {
|
||||
m.hot.dispose(() => {
|
||||
this._storyStore.removeStoryKind(kind);
|
||||
});
|
||||
}
|
||||
|
||||
const localDecorators = [];
|
||||
const api = {
|
||||
kind,
|
||||
};
|
||||
|
||||
// apply addons
|
||||
Object.keys(this._addons).forEach(name => {
|
||||
const addon = this._addons[name];
|
||||
api[name] = (...args) => {
|
||||
addon.apply(api, args);
|
||||
return api;
|
||||
};
|
||||
});
|
||||
|
||||
api.add = (storyName, getStory) => {
|
||||
if (typeof storyName !== 'string') {
|
||||
throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`);
|
||||
}
|
||||
|
||||
if (this._storyStore.hasStory(kind, storyName)) {
|
||||
throw new Error(`Story of "${kind}" named "${storyName}" already exists`);
|
||||
}
|
||||
|
||||
// Wrap the getStory function with each decorator. The first
|
||||
// decorator will wrap the story function. The second will
|
||||
// wrap the first decorator and so on.
|
||||
const decorators = [...localDecorators, ...this._globalDecorators];
|
||||
|
||||
const getDecoratedStory = decorators.reduce(
|
||||
(decorated, decorator) => context => decorator(() => decorated(context), context),
|
||||
getStory
|
||||
);
|
||||
|
||||
// Add the fully decorated getStory function.
|
||||
this._storyStore.addStory(kind, storyName, getDecoratedStory);
|
||||
return api;
|
||||
};
|
||||
|
||||
api.addDecorator = decorator => {
|
||||
localDecorators.push(decorator);
|
||||
return api;
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
getStorybook() {
|
||||
return this._storyStore.getStoryKinds().map(kind => {
|
||||
const stories = this._storyStore.getStories(kind).map(name => {
|
||||
const render = this._storyStore.getStory(kind, name);
|
||||
return { name, render };
|
||||
});
|
||||
return { kind, stories };
|
||||
});
|
||||
}
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import ClientAPI from './client_api';
|
||||
|
||||
class StoryStore {
|
||||
constructor() {
|
||||
this.stories = [];
|
||||
}
|
||||
|
||||
addStory(kind, story, fn) {
|
||||
this.stories.push({ kind, story, fn });
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
return this.stories.reduce((kinds, info) => {
|
||||
if (kinds.indexOf(info.kind) === -1) {
|
||||
kinds.push(info.kind);
|
||||
}
|
||||
return kinds;
|
||||
}, []);
|
||||
}
|
||||
|
||||
getStories(kind) {
|
||||
return this.stories.reduce((stories, info) => {
|
||||
if (info.kind === kind) {
|
||||
stories.push(info.story);
|
||||
}
|
||||
return stories;
|
||||
}, []);
|
||||
}
|
||||
|
||||
getStory(kind, name) {
|
||||
return this.stories.reduce((fn, info) => {
|
||||
if (!fn && info.kind === kind && info.story === name) {
|
||||
return info.fn;
|
||||
}
|
||||
return fn;
|
||||
}, null);
|
||||
}
|
||||
|
||||
hasStory(kind, name) {
|
||||
return Boolean(this.getStory(kind, name));
|
||||
}
|
||||
}
|
||||
|
||||
describe('preview.client_api', () => {
|
||||
describe('setAddon', () => {
|
||||
it('should register addons', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = 'foo';
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none').aa();
|
||||
expect(data).toBe('foo');
|
||||
});
|
||||
|
||||
it('should not remove previous addons', () => {
|
||||
const api = new ClientAPI({});
|
||||
const data = [];
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data.push('foo');
|
||||
},
|
||||
});
|
||||
|
||||
api.setAddon({
|
||||
bb() {
|
||||
data.push('bar');
|
||||
},
|
||||
});
|
||||
|
||||
api
|
||||
.storiesOf('none')
|
||||
.aa()
|
||||
.bb();
|
||||
expect(data).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should call with the api context', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = typeof this.add;
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none').aa();
|
||||
expect(data).toBe('function');
|
||||
});
|
||||
|
||||
it('should be able to access addons added previously', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = 'foo';
|
||||
},
|
||||
});
|
||||
|
||||
api.setAddon({
|
||||
bb() {
|
||||
this.aa();
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none').bb();
|
||||
expect(data).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to access the current kind', () => {
|
||||
const api = new ClientAPI({});
|
||||
const kind = 'dfdwf3e3';
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = this.kind;
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf(kind).aa();
|
||||
expect(data).toBe(kind);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDecorator', () => {
|
||||
it('should add local decorators', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none');
|
||||
localApi.addDecorator(fn => `aa-${fn()}`);
|
||||
|
||||
localApi.add('storyName', () => 'Hello');
|
||||
expect(storyStore.stories[0].fn()).toBe('aa-Hello');
|
||||
});
|
||||
|
||||
it('should add global decorators', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
api.addDecorator(fn => `bb-${fn()}`);
|
||||
const localApi = api.storiesOf('none');
|
||||
|
||||
localApi.add('storyName', () => 'Hello');
|
||||
expect(storyStore.stories[0].fn()).toBe('bb-Hello');
|
||||
});
|
||||
|
||||
it('should utilize both decorators at once', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none');
|
||||
|
||||
api.addDecorator(fn => `aa-${fn()}`);
|
||||
localApi.addDecorator(fn => `bb-${fn()}`);
|
||||
|
||||
localApi.add('storyName', () => 'Hello');
|
||||
expect(storyStore.stories[0].fn()).toBe('aa-bb-Hello');
|
||||
});
|
||||
|
||||
it('should pass the context', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none');
|
||||
localApi.addDecorator(fn => `aa-${fn()}`);
|
||||
|
||||
localApi.add('storyName', ({ kind, story }) => `${kind}-${story}`);
|
||||
|
||||
const kind = 'dfdfd';
|
||||
const story = 'ef349ff';
|
||||
|
||||
const result = storyStore.stories[0].fn({ kind, story });
|
||||
expect(result).toBe(`aa-${kind}-${story}`);
|
||||
});
|
||||
|
||||
it('should have access to the context', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none');
|
||||
localApi.addDecorator((fn, { kind, story }) => `${kind}-${story}-${fn()}`);
|
||||
|
||||
localApi.add('storyName', () => 'Hello');
|
||||
|
||||
const kind = 'dfdfd';
|
||||
const story = 'ef349ff';
|
||||
|
||||
const result = storyStore.stories[0].fn({ kind, story });
|
||||
expect(result).toBe(`${kind}-${story}-Hello`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearDecorators', () => {
|
||||
it('should remove all global decorators', () => {
|
||||
const api = new ClientAPI({});
|
||||
api._globalDecorators = 1234;
|
||||
api.clearDecorators();
|
||||
expect(api._globalDecorators).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStorybook', () => {
|
||||
it('should return storybook when empty', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const book = api.getStorybook();
|
||||
expect(book).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return storybook with stories', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const functions = {
|
||||
'story-1.1': () => 'story-1.1',
|
||||
'story-1.2': () => 'story-1.2',
|
||||
'story-2.1': () => 'story-2.1',
|
||||
'story-2.2': () => 'story-2.2',
|
||||
};
|
||||
const kind1 = api.storiesOf('kind-1');
|
||||
kind1.add('story-1.1', functions['story-1.1']);
|
||||
kind1.add('story-1.2', functions['story-1.2']);
|
||||
const kind2 = api.storiesOf('kind-2');
|
||||
kind2.add('story-2.1', functions['story-2.1']);
|
||||
kind2.add('story-2.2', functions['story-2.2']);
|
||||
const book = api.getStorybook();
|
||||
expect(book).toEqual([
|
||||
{
|
||||
kind: 'kind-1',
|
||||
stories: [
|
||||
{ name: 'story-1.1', render: functions['story-1.1'] },
|
||||
{ name: 'story-1.2', render: functions['story-1.2'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'kind-2',
|
||||
stories: [
|
||||
{ name: 'story-2.1', render: functions['story-2.1'] },
|
||||
{ name: 'story-2.2', render: functions['story-2.2'] },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
67
app/angular/src/client/preview/config_api.js
vendored
67
app/angular/src/client/preview/config_api.js
vendored
@ -1,67 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import { location } from 'global';
|
||||
import { setInitialStory, setError, clearError } from './actions';
|
||||
import { clearDecorators } from './';
|
||||
|
||||
export default class ConfigApi {
|
||||
constructor({ channel, storyStore, reduxStore }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._reduxStore = reduxStore;
|
||||
}
|
||||
|
||||
_renderMain(loaders) {
|
||||
if (loaders) loaders();
|
||||
|
||||
const stories = this._storyStore.dumpStoryBook();
|
||||
// send to the parent frame.
|
||||
this._channel.emit('setStories', { stories });
|
||||
|
||||
// clear the error if exists.
|
||||
this._reduxStore.dispatch(clearError());
|
||||
this._reduxStore.dispatch(setInitialStory(stories));
|
||||
}
|
||||
|
||||
_renderError(e) {
|
||||
const { stack, message } = e;
|
||||
const error = { stack, message };
|
||||
this._reduxStore.dispatch(setError(error));
|
||||
}
|
||||
|
||||
configure(loaders, module) {
|
||||
const render = () => {
|
||||
try {
|
||||
this._renderMain(loaders);
|
||||
} catch (error) {
|
||||
if (module.hot && module.hot.status() === 'apply') {
|
||||
// We got this issue, after webpack fixed it and applying it.
|
||||
// Therefore error message is displayed forever even it's being fixed.
|
||||
// So, we'll detect it reload the page.
|
||||
location.reload();
|
||||
} else {
|
||||
// If we are accessing the site, but the error is not fixed yet.
|
||||
// There we can render the error.
|
||||
this._renderError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept(() => {
|
||||
setTimeout(render);
|
||||
});
|
||||
module.hot.dispose(() => {
|
||||
clearDecorators();
|
||||
});
|
||||
}
|
||||
|
||||
if (this._channel) {
|
||||
render();
|
||||
} else {
|
||||
loaders();
|
||||
}
|
||||
}
|
||||
}
|
39
app/angular/src/client/preview/index.js
vendored
39
app/angular/src/client/preview/index.js
vendored
@ -2,14 +2,16 @@ import { window, navigator } from 'global';
|
||||
import { createStore } from 'redux';
|
||||
import addons from '@storybook/addons';
|
||||
import createChannel from '@storybook/channel-postmessage';
|
||||
import qs from 'qs';
|
||||
import StoryStore from './story_store';
|
||||
import ClientApi from './client_api';
|
||||
import ConfigApi from './config_api';
|
||||
import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events';
|
||||
import {
|
||||
StoryStore,
|
||||
ClientApi,
|
||||
ConfigApi,
|
||||
Actions,
|
||||
reducer,
|
||||
syncUrlWithStore,
|
||||
} from '@storybook/core/client';
|
||||
import render from './render';
|
||||
import init from './init';
|
||||
import { selectStory } from './actions';
|
||||
import reducer from './reducer';
|
||||
|
||||
// check whether we're running on node/browser
|
||||
const isBrowser =
|
||||
@ -24,26 +26,25 @@ const reduxStore = createStore(reducer);
|
||||
const context = { storyStore, reduxStore };
|
||||
|
||||
if (isBrowser) {
|
||||
const queryParams = qs.parse(window.location.search.substring(1));
|
||||
// create preview channel
|
||||
const channel = createChannel({ page: 'preview' });
|
||||
channel.on('setCurrentStory', data => {
|
||||
reduxStore.dispatch(selectStory(data.kind, data.story));
|
||||
reduxStore.dispatch(Actions.selectStory(data.kind, data.story));
|
||||
});
|
||||
Object.assign(context, { channel, window, queryParams });
|
||||
addons.setChannel(channel);
|
||||
init(context);
|
||||
Object.assign(context, { channel });
|
||||
|
||||
syncUrlWithStore(reduxStore);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
window.onkeydown = handleKeyboardShortcuts(channel);
|
||||
}
|
||||
|
||||
const clientApi = new ClientApi(context);
|
||||
const configApi = new ConfigApi(context);
|
||||
export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi;
|
||||
|
||||
// do exports
|
||||
export const storiesOf = clientApi.storiesOf.bind(clientApi);
|
||||
export const setAddon = clientApi.setAddon.bind(clientApi);
|
||||
export const addDecorator = clientApi.addDecorator.bind(clientApi);
|
||||
export const clearDecorators = clientApi.clearDecorators.bind(clientApi);
|
||||
export const getStorybook = clientApi.getStorybook.bind(clientApi);
|
||||
export const configure = configApi.configure.bind(configApi);
|
||||
const configApi = new ConfigApi({ ...context, clearDecorators });
|
||||
export const { configure } = configApi;
|
||||
|
||||
// initialize the UI
|
||||
const renderUI = () => {
|
||||
|
33
app/angular/src/client/preview/init.js
vendored
33
app/angular/src/client/preview/init.js
vendored
@ -1,33 +0,0 @@
|
||||
import keyEvents from '@storybook/ui/dist/libs/key_events';
|
||||
import qs from 'qs';
|
||||
|
||||
import { selectStory } from './actions';
|
||||
|
||||
export default function(context) {
|
||||
const { queryParams, reduxStore, window, channel } = context;
|
||||
// set the story if correct params are loaded via the URL.
|
||||
if (queryParams.selectedKind) {
|
||||
reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory));
|
||||
}
|
||||
|
||||
// Keep whichever of these are set that we don't override when stories change
|
||||
const originalQueryParams = queryParams;
|
||||
reduxStore.subscribe(() => {
|
||||
const { selectedKind, selectedStory } = reduxStore.getState();
|
||||
|
||||
const queryString = qs.stringify({
|
||||
...originalQueryParams,
|
||||
selectedKind,
|
||||
selectedStory,
|
||||
});
|
||||
window.history.pushState({}, '', `?${queryString}`);
|
||||
});
|
||||
|
||||
// Handle keyEvents and pass them to the parent.
|
||||
window.onkeydown = e => {
|
||||
const parsedEvent = keyEvents(e);
|
||||
if (parsedEvent) {
|
||||
channel.emit('applyShortcut', { event: parsedEvent });
|
||||
}
|
||||
};
|
||||
}
|
89
app/angular/src/client/preview/story_store.js
vendored
89
app/angular/src/client/preview/story_store.js
vendored
@ -1,89 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
let count = 0;
|
||||
|
||||
function getId() {
|
||||
count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
export default class StoryStore {
|
||||
constructor() {
|
||||
this._data = {};
|
||||
}
|
||||
|
||||
addStory(kind, name, fn) {
|
||||
if (!this._data[kind]) {
|
||||
this._data[kind] = {
|
||||
kind,
|
||||
index: getId(),
|
||||
stories: {},
|
||||
};
|
||||
}
|
||||
|
||||
this._data[kind].stories[name] = {
|
||||
name,
|
||||
index: getId(),
|
||||
fn,
|
||||
};
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
return Object.keys(this._data)
|
||||
.map(key => this._data[key])
|
||||
.filter(kind => Object.keys(kind.stories).length > 0)
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.kind);
|
||||
}
|
||||
|
||||
getStories(kind) {
|
||||
if (!this._data[kind]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this._data[kind].stories)
|
||||
.map(name => this._data[kind].stories[name])
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.name);
|
||||
}
|
||||
|
||||
getStory(kind, name) {
|
||||
const storiesKind = this._data[kind];
|
||||
if (!storiesKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storyInfo = storiesKind.stories[name];
|
||||
if (!storyInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storyInfo.fn;
|
||||
}
|
||||
|
||||
removeStoryKind(kind) {
|
||||
this._data[kind].stories = {};
|
||||
}
|
||||
|
||||
hasStoryKind(kind) {
|
||||
return Boolean(this._data[kind]);
|
||||
}
|
||||
|
||||
hasStory(kind, name) {
|
||||
return Boolean(this.getStory(kind, name));
|
||||
}
|
||||
|
||||
dumpStoryBook() {
|
||||
const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) }));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
size() {
|
||||
return Object.keys(this._data).length;
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.getStoryKinds().forEach(kind => delete this._data[kind]);
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@
|
||||
"@storybook/addon-links": "^3.3.3",
|
||||
"@storybook/addons": "^3.3.3",
|
||||
"@storybook/channel-websocket": "^3.3.3",
|
||||
"@storybook/core": "^3.3.3",
|
||||
"@storybook/ui": "^3.3.3",
|
||||
"autoprefixer": "^7.2.4",
|
||||
"babel-loader": "^7.1.2",
|
||||
@ -48,7 +49,6 @@
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.1",
|
||||
"commander": "^2.12.2",
|
||||
"css-loader": "^0.28.7",
|
||||
"events": "^1.1.1",
|
||||
"express": "^4.16.2",
|
||||
"file-loader": "^1.1.6",
|
||||
"find-cache-dir": "^1.0.0",
|
||||
|
2
app/react-native/src/preview/index.js
vendored
2
app/react-native/src/preview/index.js
vendored
@ -6,7 +6,7 @@ import parse from 'url-parse';
|
||||
import addons from '@storybook/addons';
|
||||
import createChannel from '@storybook/channel-websocket';
|
||||
import { EventEmitter } from 'events';
|
||||
import StoryStore from './story_store';
|
||||
import { StoryStore } from '@storybook/core/client';
|
||||
import StoryKindApi from './story_kind';
|
||||
import OnDeviceUI from './components/OnDeviceUI';
|
||||
import StoryView from './components/StoryView';
|
||||
|
98
app/react-native/src/preview/story_store.js
vendored
98
app/react-native/src/preview/story_store.js
vendored
@ -1,98 +0,0 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
let count = 0;
|
||||
|
||||
export default class StoryStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._data = {};
|
||||
}
|
||||
|
||||
addStory(kind, name, fn, fileName) {
|
||||
count += 1;
|
||||
if (!this._data[kind]) {
|
||||
this._data[kind] = {
|
||||
kind,
|
||||
fileName,
|
||||
index: count,
|
||||
stories: {},
|
||||
};
|
||||
}
|
||||
|
||||
this._data[kind].stories[name] = {
|
||||
name,
|
||||
index: count,
|
||||
fn,
|
||||
};
|
||||
|
||||
this.emit('storyAdded', kind, name, fn);
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
return Object.keys(this._data)
|
||||
.map(key => this._data[key])
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.kind);
|
||||
}
|
||||
|
||||
getStories(kind) {
|
||||
if (!this._data[kind]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this._data[kind].stories)
|
||||
.map(name => this._data[kind].stories[name])
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.name);
|
||||
}
|
||||
|
||||
getStoryFileName(kind) {
|
||||
const storiesKind = this._data[kind];
|
||||
if (!storiesKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storiesKind.fileName;
|
||||
}
|
||||
|
||||
getStory(kind, name) {
|
||||
const storiesKind = this._data[kind];
|
||||
if (!storiesKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storyInfo = storiesKind.stories[name];
|
||||
if (!storyInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storyInfo.fn;
|
||||
}
|
||||
|
||||
removeStoryKind(kind) {
|
||||
delete this._data[kind];
|
||||
}
|
||||
|
||||
hasStoryKind(kind) {
|
||||
return Boolean(this._data[kind]);
|
||||
}
|
||||
|
||||
hasStory(kind, name) {
|
||||
return Boolean(this.getStory(kind, name));
|
||||
}
|
||||
|
||||
dumpStoryBook() {
|
||||
const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) }));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
size() {
|
||||
return Object.keys(this._data).length;
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.getStoryKinds().forEach(kind => delete this._data[kind]);
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@
|
||||
"@storybook/addons": "^3.3.3",
|
||||
"@storybook/channel-postmessage": "^3.3.3",
|
||||
"@storybook/client-logger": "^3.3.3",
|
||||
"@storybook/core": "^3.3.3",
|
||||
"@storybook/node-logger": "^3.3.3",
|
||||
"@storybook/ui": "^3.3.3",
|
||||
"airbnb-js-shims": "^1.4.0",
|
||||
|
@ -1,34 +0,0 @@
|
||||
export const types = {
|
||||
SET_ERROR: 'PREVIEW_SET_ERROR',
|
||||
CLEAR_ERROR: 'PREVIEW_CLEAR_ERROR',
|
||||
SELECT_STORY: 'PREVIEW_SELECT_STORY',
|
||||
SET_INITIAL_STORY: 'PREVIEW_SET_INITIAL_STORY',
|
||||
};
|
||||
|
||||
export function setInitialStory(storyKindList) {
|
||||
return {
|
||||
type: types.SET_INITIAL_STORY,
|
||||
storyKindList,
|
||||
};
|
||||
}
|
||||
|
||||
export function setError(error) {
|
||||
return {
|
||||
type: types.SET_ERROR,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearError() {
|
||||
return {
|
||||
type: types.CLEAR_ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectStory(kind, story) {
|
||||
return {
|
||||
type: types.SELECT_STORY,
|
||||
kind,
|
||||
story,
|
||||
};
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
|
||||
import { location } from 'global';
|
||||
import { setInitialStory, setError, clearError } from './actions';
|
||||
import { clearDecorators } from './';
|
||||
|
||||
export default class ConfigApi {
|
||||
constructor({ channel, storyStore, reduxStore }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._reduxStore = reduxStore;
|
||||
}
|
||||
|
||||
_renderMain(loaders) {
|
||||
if (loaders) loaders();
|
||||
|
||||
const stories = this._storyStore.dumpStoryBook();
|
||||
// send to the parent frame.
|
||||
this._channel.emit('setStories', { stories });
|
||||
|
||||
// clear the error if exists.
|
||||
this._reduxStore.dispatch(clearError());
|
||||
this._reduxStore.dispatch(setInitialStory(stories));
|
||||
}
|
||||
|
||||
_renderError(e) {
|
||||
const { stack, message } = e;
|
||||
const error = { stack, message };
|
||||
this._reduxStore.dispatch(setError(error));
|
||||
}
|
||||
|
||||
configure(loaders, module) {
|
||||
const render = () => {
|
||||
try {
|
||||
this._renderMain(loaders);
|
||||
} catch (error) {
|
||||
if (module.hot && module.hot.status() === 'apply') {
|
||||
// We got this issue, after webpack fixed it and applying it.
|
||||
// Therefore error message is displayed forever even it's being fixed.
|
||||
// So, we'll detect it reload the page.
|
||||
location.reload();
|
||||
} else {
|
||||
// If we are accessing the site, but the error is not fixed yet.
|
||||
// There we can render the error.
|
||||
this._renderError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept(() => {
|
||||
setTimeout(render);
|
||||
});
|
||||
module.hot.dispose(() => {
|
||||
clearDecorators();
|
||||
});
|
||||
}
|
||||
|
||||
if (this._channel) {
|
||||
render();
|
||||
} else {
|
||||
loaders();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
import { createStore } from 'redux';
|
||||
import addons from '@storybook/addons';
|
||||
import createChannel from '@storybook/channel-postmessage';
|
||||
import qs from 'qs';
|
||||
import { navigator, window } from 'global';
|
||||
import createChannel from '@storybook/channel-postmessage';
|
||||
import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events';
|
||||
import {
|
||||
StoryStore,
|
||||
ClientApi,
|
||||
ConfigApi,
|
||||
Actions,
|
||||
reducer,
|
||||
syncUrlWithStore,
|
||||
} from '@storybook/core/client';
|
||||
|
||||
import StoryStore from './story_store';
|
||||
import ClientApi from './client_api';
|
||||
import ConfigApi from './config_api';
|
||||
import render from './render';
|
||||
import init from './init';
|
||||
import { selectStory } from './actions';
|
||||
import reducer from './reducer';
|
||||
|
||||
// check whether we're running on node/browser
|
||||
const isBrowser =
|
||||
@ -27,26 +29,25 @@ const reduxStore = createStore(reducer);
|
||||
const context = { storyStore, reduxStore };
|
||||
|
||||
if (isBrowser) {
|
||||
const queryParams = qs.parse(window.location.search.substring(1));
|
||||
// setup preview channel
|
||||
const channel = createChannel({ page: 'preview' });
|
||||
channel.on('setCurrentStory', data => {
|
||||
reduxStore.dispatch(selectStory(data.kind, data.story));
|
||||
reduxStore.dispatch(Actions.selectStory(data.kind, data.story));
|
||||
});
|
||||
Object.assign(context, { channel, window, queryParams });
|
||||
addons.setChannel(channel);
|
||||
init(context);
|
||||
Object.assign(context, { channel });
|
||||
|
||||
syncUrlWithStore(reduxStore);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
window.onkeydown = handleKeyboardShortcuts(channel);
|
||||
}
|
||||
|
||||
const clientApi = new ClientApi(context);
|
||||
const configApi = new ConfigApi(context);
|
||||
export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi;
|
||||
|
||||
// do exports
|
||||
export const storiesOf = clientApi.storiesOf.bind(clientApi);
|
||||
export const setAddon = clientApi.setAddon.bind(clientApi);
|
||||
export const addDecorator = clientApi.addDecorator.bind(clientApi);
|
||||
export const clearDecorators = clientApi.clearDecorators.bind(clientApi);
|
||||
export const getStorybook = clientApi.getStorybook.bind(clientApi);
|
||||
export const configure = configApi.configure.bind(configApi);
|
||||
const configApi = new ConfigApi({ clearDecorators, ...context });
|
||||
export const { configure } = configApi;
|
||||
|
||||
// initialize the UI
|
||||
const renderUI = () => {
|
||||
|
@ -1,33 +0,0 @@
|
||||
import keyEvents from '@storybook/ui/dist/libs/key_events';
|
||||
import qs from 'qs';
|
||||
|
||||
import { selectStory } from './actions';
|
||||
|
||||
export default function(context) {
|
||||
const { queryParams, reduxStore, window, channel } = context;
|
||||
// set the story if correct params are loaded via the URL.
|
||||
if (queryParams.selectedKind) {
|
||||
reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory));
|
||||
}
|
||||
|
||||
// Keep whichever of these are set that we don't override when stories change
|
||||
const originalQueryParams = queryParams;
|
||||
reduxStore.subscribe(() => {
|
||||
const { selectedKind, selectedStory } = reduxStore.getState();
|
||||
|
||||
const queryString = qs.stringify({
|
||||
...originalQueryParams,
|
||||
selectedKind,
|
||||
selectedStory,
|
||||
});
|
||||
window.history.pushState({}, '', `?${queryString}`);
|
||||
});
|
||||
|
||||
// Handle keyEvents and pass them to the parent.
|
||||
window.onkeydown = e => {
|
||||
const parsedEvent = keyEvents(e);
|
||||
if (parsedEvent) {
|
||||
channel.emit('applyShortcut', { event: parsedEvent });
|
||||
}
|
||||
};
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { types } from './actions';
|
||||
|
||||
export default function reducer(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case types.CLEAR_ERROR: {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_ERROR: {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SELECT_STORY: {
|
||||
return {
|
||||
...state,
|
||||
selectedKind: action.kind,
|
||||
selectedStory: action.story,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_INITIAL_STORY: {
|
||||
const newState = { ...state };
|
||||
const { storyKindList } = action;
|
||||
if (!newState.selectedKind && storyKindList.length > 0) {
|
||||
newState.selectedKind = storyKindList[0].kind;
|
||||
[newState.selectedStory] = storyKindList[0].stories;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@
|
||||
"@storybook/addon-links": "^3.3.3",
|
||||
"@storybook/addons": "^3.3.3",
|
||||
"@storybook/channel-postmessage": "^3.3.3",
|
||||
"@storybook/core": "^3.3.3",
|
||||
"@storybook/ui": "^3.3.3",
|
||||
"airbnb-js-shims": "^1.4.0",
|
||||
"autoprefixer": "^7.2.4",
|
||||
|
@ -1,34 +0,0 @@
|
||||
export const types = {
|
||||
SET_ERROR: 'PREVIEW_SET_ERROR',
|
||||
CLEAR_ERROR: 'PREVIEW_CLEAR_ERROR',
|
||||
SELECT_STORY: 'PREVIEW_SELECT_STORY',
|
||||
SET_INITIAL_STORY: 'PREVIEW_SET_INITIAL_STORY',
|
||||
};
|
||||
|
||||
export function setInitialStory(storyKindList) {
|
||||
return {
|
||||
type: types.SET_INITIAL_STORY,
|
||||
storyKindList,
|
||||
};
|
||||
}
|
||||
|
||||
export function setError(error) {
|
||||
return {
|
||||
type: types.SET_ERROR,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearError() {
|
||||
return {
|
||||
type: types.CLEAR_ERROR,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectStory(kind, story) {
|
||||
return {
|
||||
type: types.SELECT_STORY,
|
||||
kind,
|
||||
story,
|
||||
};
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
|
||||
export default class ClientApi {
|
||||
constructor({ channel, storyStore }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._addons = {};
|
||||
this._globalDecorators = [];
|
||||
}
|
||||
|
||||
setAddon(addon) {
|
||||
this._addons = {
|
||||
...this._addons,
|
||||
...addon,
|
||||
};
|
||||
}
|
||||
|
||||
addDecorator(decorator) {
|
||||
this._globalDecorators.push(decorator);
|
||||
}
|
||||
|
||||
clearDecorators() {
|
||||
this._globalDecorators = [];
|
||||
}
|
||||
|
||||
storiesOf(kind, m) {
|
||||
if (!kind && typeof kind !== 'string') {
|
||||
throw new Error('Invalid or missing kind provided for stories, should be a string');
|
||||
}
|
||||
|
||||
if (!m) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`Missing 'module' parameter for story with a kind of '${kind}'. It will break your HMR`
|
||||
);
|
||||
}
|
||||
|
||||
if (m && m.hot) {
|
||||
m.hot.dispose(() => {
|
||||
this._storyStore.removeStoryKind(kind);
|
||||
});
|
||||
}
|
||||
|
||||
const localDecorators = [];
|
||||
const api = {
|
||||
kind,
|
||||
};
|
||||
|
||||
// apply addons
|
||||
Object.keys(this._addons).forEach(name => {
|
||||
const addon = this._addons[name];
|
||||
api[name] = (...args) => {
|
||||
addon.apply(api, args);
|
||||
return api;
|
||||
};
|
||||
});
|
||||
|
||||
const createWrapperComponent = Target => ({
|
||||
functional: true,
|
||||
render(h, c) {
|
||||
return h(Target, c.data, c.children);
|
||||
},
|
||||
});
|
||||
|
||||
api.add = (storyName, getStory) => {
|
||||
if (typeof storyName !== 'string') {
|
||||
throw new Error(`Invalid or missing storyName provided for a "${kind}" story.`);
|
||||
}
|
||||
|
||||
if (this._storyStore.hasStory(kind, storyName)) {
|
||||
throw new Error(`Story of "${kind}" named "${storyName}" already exists`);
|
||||
}
|
||||
|
||||
// Wrap the getStory function with each decorator. The first
|
||||
// decorator will wrap the story function. The second will
|
||||
// wrap the first decorator and so on.
|
||||
const decorators = [...localDecorators, ...this._globalDecorators];
|
||||
|
||||
const getDecoratedStory = decorators.reduce(
|
||||
(decorated, decorator) => context => {
|
||||
const story = () => decorated(context);
|
||||
const decoratedStory = decorator(story, context);
|
||||
decoratedStory.components = decoratedStory.components || {};
|
||||
decoratedStory.components.story = createWrapperComponent(story());
|
||||
return decoratedStory;
|
||||
},
|
||||
getStory
|
||||
);
|
||||
|
||||
const fileName = m ? m.filename : null;
|
||||
|
||||
// Add the fully decorated getStory function.
|
||||
this._storyStore.addStory(kind, storyName, getDecoratedStory, fileName);
|
||||
return api;
|
||||
};
|
||||
|
||||
api.addDecorator = decorator => {
|
||||
localDecorators.push(decorator);
|
||||
return api;
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
getStorybook() {
|
||||
return this._storyStore.getStoryKinds().map(kind => {
|
||||
const fileName = this._storyStore.getStoryFileName(kind);
|
||||
|
||||
const stories = this._storyStore.getStories(kind).map(name => {
|
||||
const render = this._storyStore.getStory(kind, name);
|
||||
return { name, render };
|
||||
});
|
||||
|
||||
return { kind, fileName, stories };
|
||||
});
|
||||
}
|
||||
}
|
@ -1,295 +0,0 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
|
||||
import ClientAPI from './client_api';
|
||||
|
||||
class StoryStore {
|
||||
constructor() {
|
||||
this.stories = [];
|
||||
}
|
||||
|
||||
addStory(kind, story, fn, fileName) {
|
||||
this.stories.push({ kind, story, fn, fileName });
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
return this.stories.reduce((kinds, info) => {
|
||||
if (kinds.indexOf(info.kind) === -1) {
|
||||
kinds.push(info.kind);
|
||||
}
|
||||
return kinds;
|
||||
}, []);
|
||||
}
|
||||
|
||||
getStories(kind) {
|
||||
return this.stories.reduce((stories, info) => {
|
||||
if (info.kind === kind) {
|
||||
stories.push(info.story);
|
||||
}
|
||||
return stories;
|
||||
}, []);
|
||||
}
|
||||
|
||||
getStoryFileName(kind) {
|
||||
const story = this.stories.find(info => info.kind === kind);
|
||||
return story ? story.fileName : null;
|
||||
}
|
||||
|
||||
getStory(kind, name) {
|
||||
return this.stories.reduce((fn, info) => {
|
||||
if (!fn && info.kind === kind && info.story === name) {
|
||||
return info.fn;
|
||||
}
|
||||
return fn;
|
||||
}, null);
|
||||
}
|
||||
|
||||
hasStory(kind, name) {
|
||||
return Boolean(this.getStory(kind, name));
|
||||
}
|
||||
}
|
||||
|
||||
describe('preview.client_api', () => {
|
||||
describe('setAddon', () => {
|
||||
it('should register addons', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = 'foo';
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none', module).aa();
|
||||
expect(data).toBe('foo');
|
||||
});
|
||||
|
||||
it('should not remove previous addons', () => {
|
||||
const api = new ClientAPI({});
|
||||
const data = [];
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data.push('foo');
|
||||
},
|
||||
});
|
||||
|
||||
api.setAddon({
|
||||
bb() {
|
||||
data.push('bar');
|
||||
},
|
||||
});
|
||||
|
||||
api
|
||||
.storiesOf('none', module)
|
||||
.aa()
|
||||
.bb();
|
||||
expect(data).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
it('should call with the api context', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = typeof this.add;
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none', module).aa();
|
||||
expect(data).toBe('function');
|
||||
});
|
||||
|
||||
it('should be able to access addons added previously', () => {
|
||||
const api = new ClientAPI({});
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = 'foo';
|
||||
},
|
||||
});
|
||||
|
||||
api.setAddon({
|
||||
bb() {
|
||||
this.aa();
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf('none', module).bb();
|
||||
expect(data).toBe('foo');
|
||||
});
|
||||
|
||||
it('should be able to access the current kind', () => {
|
||||
const api = new ClientAPI({});
|
||||
const kind = 'dfdwf3e3';
|
||||
let data;
|
||||
|
||||
api.setAddon({
|
||||
aa() {
|
||||
data = this.kind;
|
||||
},
|
||||
});
|
||||
|
||||
api.storiesOf(kind, module).aa();
|
||||
expect(data).toBe(kind);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addDecorator', () => {
|
||||
it('should add local decorators', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none', module);
|
||||
localApi.addDecorator(fn => ({ template: `<div>aa${fn().template}</div>` }));
|
||||
|
||||
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
|
||||
expect(storyStore.stories[0].fn().template).toBe('<div>aa<p>hello</p></div>');
|
||||
});
|
||||
|
||||
it('should add global decorators', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
api.addDecorator(fn => ({ template: `<div>bb${fn().template}</div>` }));
|
||||
const localApi = api.storiesOf('none', module);
|
||||
|
||||
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
|
||||
expect(storyStore.stories[0].fn().template).toBe('<div>bb<p>hello</p></div>');
|
||||
});
|
||||
|
||||
it('should utilize both decorators at once', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none', module);
|
||||
|
||||
api.addDecorator(fn => ({ template: `<div>aa${fn().template}</div>` }));
|
||||
localApi.addDecorator(fn => ({ template: `<div>bb${fn().template}</div>` }));
|
||||
|
||||
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
|
||||
expect(storyStore.stories[0].fn().template).toBe('<div>aa<div>bb<p>hello</p></div></div>');
|
||||
});
|
||||
|
||||
it('should pass the context', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none', module);
|
||||
localApi.addDecorator(fn => ({ template: `<div>aa${fn().template}</div>` }));
|
||||
|
||||
localApi.add('storyName', ({ kind, story }) => ({ template: `<p>${kind}-${story}</p>` }));
|
||||
|
||||
const kind = 'dfdfd';
|
||||
const story = 'ef349ff';
|
||||
|
||||
const result = storyStore.stories[0].fn({ kind, story });
|
||||
expect(result.template).toBe(`<div>aa<p>${kind}-${story}</p></div>`);
|
||||
});
|
||||
|
||||
it('should have access to the context', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const localApi = api.storiesOf('none', module);
|
||||
localApi.addDecorator((fn, { kind, story }) => ({
|
||||
template: `<div>${kind}-${story}-${fn().template}</div>`,
|
||||
}));
|
||||
|
||||
localApi.add('storyName', () => ({ template: '<p>hello</p>' }));
|
||||
|
||||
const kind = 'dfdfd';
|
||||
const story = 'ef349ff';
|
||||
|
||||
const result = storyStore.stories[0].fn({ kind, story });
|
||||
expect(result.template).toBe(`<div>${kind}-${story}-<p>hello</p></div>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearDecorators', () => {
|
||||
it('should remove all global decorators', () => {
|
||||
const api = new ClientAPI({});
|
||||
api._globalDecorators = 1234;
|
||||
api.clearDecorators();
|
||||
expect(api._globalDecorators).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStorybook', () => {
|
||||
it('should return storybook when empty', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const book = api.getStorybook();
|
||||
expect(book).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return storybook with stories', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const functions = {
|
||||
'story-1.1': () => 'story-1.1',
|
||||
'story-1.2': () => 'story-1.2',
|
||||
'story-2.1': () => 'story-2.1',
|
||||
'story-2.2': () => 'story-2.2',
|
||||
};
|
||||
const kind1 = api.storiesOf('kind-1', { filename: 'kind1.js' });
|
||||
kind1.add('story-1.1', functions['story-1.1']);
|
||||
kind1.add('story-1.2', functions['story-1.2']);
|
||||
const kind2 = api.storiesOf('kind-2', { filename: 'kind2.js' });
|
||||
kind2.add('story-2.1', functions['story-2.1']);
|
||||
kind2.add('story-2.2', functions['story-2.2']);
|
||||
const book = api.getStorybook();
|
||||
expect(book).toEqual([
|
||||
{
|
||||
kind: 'kind-1',
|
||||
fileName: 'kind1.js',
|
||||
stories: [
|
||||
{ name: 'story-1.1', render: functions['story-1.1'] },
|
||||
{ name: 'story-1.2', render: functions['story-1.2'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'kind-2',
|
||||
fileName: 'kind2.js',
|
||||
stories: [
|
||||
{ name: 'story-2.1', render: functions['story-2.1'] },
|
||||
{ name: 'story-2.2', render: functions['story-2.2'] },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return storybook with file names when module with file name provided', () => {
|
||||
const storyStore = new StoryStore();
|
||||
const api = new ClientAPI({ storyStore });
|
||||
const functions = {
|
||||
'story-1.1': () => 'story-1.1',
|
||||
'story-1.2': () => 'story-1.2',
|
||||
'story-2.1': () => 'story-2.1',
|
||||
'story-2.2': () => 'story-2.2',
|
||||
};
|
||||
const kind1 = api.storiesOf('kind-1', { filename: 'foo' });
|
||||
kind1.add('story-1.1', functions['story-1.1']);
|
||||
kind1.add('story-1.2', functions['story-1.2']);
|
||||
const kind2 = api.storiesOf('kind-2', { filename: 'bar' });
|
||||
kind2.add('story-2.1', functions['story-2.1']);
|
||||
kind2.add('story-2.2', functions['story-2.2']);
|
||||
const book = api.getStorybook();
|
||||
expect(book).toEqual([
|
||||
{
|
||||
kind: 'kind-1',
|
||||
fileName: 'foo',
|
||||
stories: [
|
||||
{ name: 'story-1.1', render: functions['story-1.1'] },
|
||||
{ name: 'story-1.2', render: functions['story-1.2'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'kind-2',
|
||||
fileName: 'bar',
|
||||
stories: [
|
||||
{ name: 'story-2.1', render: functions['story-2.1'] },
|
||||
{ name: 'story-2.2', render: functions['story-2.2'] },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,16 +1,18 @@
|
||||
import { createStore } from 'redux';
|
||||
import addons from '@storybook/addons';
|
||||
import createChannel from '@storybook/channel-postmessage';
|
||||
import qs from 'qs';
|
||||
import { navigator, window } from 'global';
|
||||
import { handleKeyboardShortcuts } from '@storybook/ui/dist/libs/key_events';
|
||||
import {
|
||||
StoryStore,
|
||||
ClientApi,
|
||||
ConfigApi,
|
||||
Actions,
|
||||
reducer,
|
||||
syncUrlWithStore,
|
||||
} from '@storybook/core/client';
|
||||
|
||||
import StoryStore from './story_store';
|
||||
import ClientApi from './client_api';
|
||||
import ConfigApi from './config_api';
|
||||
import render from './render';
|
||||
import init from './init';
|
||||
import { selectStory } from './actions';
|
||||
import reducer from './reducer';
|
||||
|
||||
// check whether we're running on node/browser
|
||||
const isBrowser =
|
||||
@ -22,29 +24,46 @@ const isBrowser =
|
||||
|
||||
const storyStore = new StoryStore();
|
||||
const reduxStore = createStore(reducer);
|
||||
const context = { storyStore, reduxStore };
|
||||
|
||||
const createWrapperComponent = Target => ({
|
||||
functional: true,
|
||||
render(h, c) {
|
||||
return h(Target, c.data, c.children);
|
||||
},
|
||||
});
|
||||
const decorateStory = (getStory, decorators) =>
|
||||
decorators.reduce(
|
||||
(decorated, decorator) => context => {
|
||||
const story = () => decorated(context);
|
||||
const decoratedStory = decorator(story, context);
|
||||
decoratedStory.components = decoratedStory.components || {};
|
||||
decoratedStory.components.story = createWrapperComponent(story());
|
||||
return decoratedStory;
|
||||
},
|
||||
getStory
|
||||
);
|
||||
const context = { storyStore, reduxStore, decorateStory };
|
||||
|
||||
if (isBrowser) {
|
||||
const queryParams = qs.parse(window.location.search.substring(1));
|
||||
// create preview channel
|
||||
const channel = createChannel({ page: 'preview' });
|
||||
channel.on('setCurrentStory', data => {
|
||||
reduxStore.dispatch(selectStory(data.kind, data.story));
|
||||
reduxStore.dispatch(Actions.selectStory(data.kind, data.story));
|
||||
});
|
||||
Object.assign(context, { channel, window, queryParams });
|
||||
addons.setChannel(channel);
|
||||
init(context);
|
||||
Object.assign(context, { channel });
|
||||
|
||||
syncUrlWithStore(reduxStore);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
window.onkeydown = handleKeyboardShortcuts(channel);
|
||||
}
|
||||
|
||||
const clientApi = new ClientApi(context);
|
||||
const configApi = new ConfigApi(context);
|
||||
export const { storiesOf, setAddon, addDecorator, clearDecorators, getStorybook } = clientApi;
|
||||
|
||||
// do exports
|
||||
export const storiesOf = clientApi.storiesOf.bind(clientApi);
|
||||
export const setAddon = clientApi.setAddon.bind(clientApi);
|
||||
export const addDecorator = clientApi.addDecorator.bind(clientApi);
|
||||
export const clearDecorators = clientApi.clearDecorators.bind(clientApi);
|
||||
export const getStorybook = clientApi.getStorybook.bind(clientApi);
|
||||
export const configure = configApi.configure.bind(configApi);
|
||||
const configApi = new ConfigApi({ ...context, clearDecorators });
|
||||
export const { configure } = configApi;
|
||||
|
||||
// initialize the UI
|
||||
const renderUI = () => {
|
||||
|
@ -1,33 +0,0 @@
|
||||
import keyEvents from '@storybook/ui/dist/libs/key_events';
|
||||
import qs from 'qs';
|
||||
|
||||
import { selectStory } from './actions';
|
||||
|
||||
export default function(context) {
|
||||
const { queryParams, reduxStore, window, channel } = context;
|
||||
// set the story if correct params are loaded via the URL.
|
||||
if (queryParams.selectedKind) {
|
||||
reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory));
|
||||
}
|
||||
|
||||
// Keep whichever of these are set that we don't override when stories change
|
||||
const originalQueryParams = queryParams;
|
||||
reduxStore.subscribe(() => {
|
||||
const { selectedKind, selectedStory } = reduxStore.getState();
|
||||
|
||||
const queryString = qs.stringify({
|
||||
...originalQueryParams,
|
||||
selectedKind,
|
||||
selectedStory,
|
||||
});
|
||||
window.history.pushState({}, '', `?${queryString}`);
|
||||
});
|
||||
|
||||
// Handle keyEvents and pass them to the parent.
|
||||
window.onkeydown = e => {
|
||||
const parsedEvent = keyEvents(e);
|
||||
if (parsedEvent) {
|
||||
channel.emit('applyShortcut', { event: parsedEvent });
|
||||
}
|
||||
};
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { types } from './actions';
|
||||
|
||||
export default function reducer(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case types.CLEAR_ERROR: {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_ERROR: {
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SELECT_STORY: {
|
||||
return {
|
||||
...state,
|
||||
selectedKind: action.kind,
|
||||
selectedStory: action.story,
|
||||
};
|
||||
}
|
||||
|
||||
case types.SET_INITIAL_STORY: {
|
||||
const newState = { ...state };
|
||||
const { storyKindList } = action;
|
||||
if (!newState.selectedKind && storyKindList.length > 0) {
|
||||
newState.selectedKind = storyKindList[0].kind;
|
||||
[newState.selectedStory] = storyKindList[0].stories;
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
|
||||
let count = 0;
|
||||
|
||||
function getId() {
|
||||
count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
export default class StoryStore {
|
||||
constructor() {
|
||||
this._data = {};
|
||||
}
|
||||
|
||||
addStory(kind, name, fn, fileName) {
|
||||
if (!this._data[kind]) {
|
||||
this._data[kind] = {
|
||||
kind,
|
||||
fileName,
|
||||
index: getId(),
|
||||
stories: {},
|
||||
};
|
||||
}
|
||||
|
||||
this._data[kind].stories[name] = {
|
||||
name,
|
||||
index: getId(),
|
||||
fn,
|
||||
};
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
return Object.keys(this._data)
|
||||
.map(key => this._data[key])
|
||||
.filter(kind => Object.keys(kind.stories).length > 0)
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.kind);
|
||||
}
|
||||
|
||||
getStories(kind) {
|
||||
if (!this._data[kind]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this._data[kind].stories)
|
||||
.map(name => this._data[kind].stories[name])
|
||||
.sort((info1, info2) => info1.index - info2.index)
|
||||
.map(info => info.name);
|
||||
}
|
||||
|
||||
getStoryFileName(kind) {
|
||||
const storiesKind = this._data[kind];
|
||||
if (!storiesKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storiesKind.fileName;
|
||||
}
|
||||
|
||||
getStory(kind, name) {
|
||||
const storiesKind = this._data[kind];
|
||||
if (!storiesKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storyInfo = storiesKind.stories[name];
|
||||
if (!storyInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storyInfo.fn;
|
||||
}
|
||||
|
||||
removeStoryKind(kind) {
|
||||
this._data[kind].stories = {};
|
||||
}
|
||||
|
||||
hasStoryKind(kind) {
|
||||
return Boolean(this._data[kind]);
|
||||
}
|
||||
|
||||
hasStory(kind, name) {
|
||||
return Boolean(this.getStory(kind, name));
|
||||
}
|
||||
|
||||
dumpStoryBook() {
|
||||
const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) }));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
size() {
|
||||
return Object.keys(this._data).length;
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.getStoryKinds().forEach(kind => delete this._data[kind]);
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
"@storybook/channel-postmessage": "file:../../packs/storybook-channel-postmessage.tgz",
|
||||
"@storybook/channels": "file:../../packs/storybook-channels.tgz",
|
||||
"@storybook/components": "file:../../packs/storybook-components.tgz",
|
||||
"@storybook/core": "file:../../packs/storybook-core.tgz",
|
||||
"@storybook/react-native": "file:../../packs/storybook-react-native.tgz",
|
||||
"@storybook/ui": "file:../../packs/storybook-ui.tgz",
|
||||
"jest-expo": "^24.0.0",
|
||||
|
@ -26,6 +26,7 @@
|
||||
"@storybook/channels": "file:../../packs/storybook-channels.tgz",
|
||||
"@storybook/channel-postmessage": "file:../../packs/storybook-channel-postmessage.tgz",
|
||||
"@storybook/components": "file:../../packs/storybook-components.tgz",
|
||||
"@storybook/core": "file:../../packs/storybook-core.tgz",
|
||||
"@storybook/react-native": "file:../../packs/storybook-react-native.tgz",
|
||||
"@storybook/ui": "file:../../packs/storybook-ui.tgz",
|
||||
"react-dom": "^16.2.0"
|
||||
|
13
lib/core/README.md
Normal file
13
lib/core/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Storybook Core
|
||||
|
||||
[](https://greenkeeper.io/)
|
||||
[](https://travis-ci.org/storybooks/storybook)
|
||||
[](https://www.codefactor.io/repository/github/storybooks/storybook)
|
||||
[](https://snyk.io/test/github/storybooks/storybook/8f36abfd6697e58cd76df3526b52e4b9dc894847)
|
||||
[](https://bettercodehub.com/results/storybooks/storybook) [](https://codecov.io/gh/storybooks/storybook)
|
||||
[](https://storybooks-slackin.herokuapp.com/)
|
||||
|
||||
This package contains common data structures used among the different frameworks
|
||||
(React, RN, Vue, Angular, etc).
|
||||
|
||||
FIXME
|
2
lib/core/client.js
Normal file
2
lib/core/client.js
Normal file
@ -0,0 +1,2 @@
|
||||
/* eslint-disable global-require */
|
||||
module.exports = require('./dist/client').default;
|
28
lib/core/package.json
Normal file
28
lib/core/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@storybook/core",
|
||||
"version": "3.3.3",
|
||||
"description": "Storybook framework-agnostic API",
|
||||
"homepage": "https://github.com/storybooks/storybook/tree/master/lib/core",
|
||||
"bugs": {
|
||||
"url": "https://github.com/storybooks/storybook/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/client/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/storybooks/storybook.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "DEV_BUILD=1 nodemon --watch ./src --exec 'yarn prepare'",
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/client-logger": "^3.3.3",
|
||||
"events": "^1.1.1",
|
||||
"global": "^4.3.2",
|
||||
"qs": "^6.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.26.0"
|
||||
}
|
||||
}
|
3
lib/core/src/client/index.js
Normal file
3
lib/core/src/client/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import preview from './preview';
|
||||
|
||||
export default preview;
|
@ -2,32 +2,39 @@
|
||||
|
||||
import { logger } from '@storybook/client-logger';
|
||||
|
||||
const defaultDecorateStory = (getStory, decorators) =>
|
||||
decorators.reduce(
|
||||
(decorated, decorator) => context => decorator(() => decorated(context), context),
|
||||
getStory
|
||||
);
|
||||
|
||||
export default class ClientApi {
|
||||
constructor({ channel, storyStore }) {
|
||||
constructor({ channel, storyStore, decorateStory = defaultDecorateStory }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._addons = {};
|
||||
this._globalDecorators = [];
|
||||
this._decorateStory = decorateStory;
|
||||
}
|
||||
|
||||
setAddon(addon) {
|
||||
setAddon = addon => {
|
||||
this._addons = {
|
||||
...this._addons,
|
||||
...addon,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
addDecorator(decorator) {
|
||||
addDecorator = decorator => {
|
||||
this._globalDecorators.push(decorator);
|
||||
}
|
||||
};
|
||||
|
||||
clearDecorators() {
|
||||
clearDecorators = () => {
|
||||
this._globalDecorators = [];
|
||||
}
|
||||
};
|
||||
|
||||
storiesOf(kind, m) {
|
||||
storiesOf = (kind, m) => {
|
||||
if (!kind && typeof kind !== 'string') {
|
||||
throw new Error('Invalid or missing kind provided for stories, should be a string');
|
||||
}
|
||||
@ -73,15 +80,15 @@ export default class ClientApi {
|
||||
// wrap the first decorator and so on.
|
||||
const decorators = [...localDecorators, ...this._globalDecorators];
|
||||
|
||||
const fn = decorators.reduce(
|
||||
(decorated, decorator) => context => decorator(() => decorated(context), context),
|
||||
getStory
|
||||
);
|
||||
|
||||
const fileName = m ? m.filename : null;
|
||||
|
||||
// Add the fully decorated getStory function.
|
||||
this._storyStore.addStory(kind, storyName, fn, fileName);
|
||||
this._storyStore.addStory(
|
||||
kind,
|
||||
storyName,
|
||||
this._decorateStory(getStory, decorators),
|
||||
fileName
|
||||
);
|
||||
return api;
|
||||
};
|
||||
|
||||
@ -91,10 +98,10 @@ export default class ClientApi {
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
};
|
||||
|
||||
getStorybook() {
|
||||
return this._storyStore.getStoryKinds().map(kind => {
|
||||
getStorybook = () =>
|
||||
this._storyStore.getStoryKinds().map(kind => {
|
||||
const fileName = this._storyStore.getStoryFileName(kind);
|
||||
|
||||
const stories = this._storyStore.getStories(kind).map(name => {
|
||||
@ -104,5 +111,4 @@ export default class ClientApi {
|
||||
|
||||
return { kind, fileName, stories };
|
||||
});
|
||||
}
|
||||
}
|
@ -2,15 +2,15 @@
|
||||
|
||||
import { location } from 'global';
|
||||
import { setInitialStory, setError, clearError } from './actions';
|
||||
import { clearDecorators } from './';
|
||||
|
||||
export default class ConfigApi {
|
||||
constructor({ channel, storyStore, reduxStore }) {
|
||||
constructor({ channel, storyStore, reduxStore, clearDecorators }) {
|
||||
// channel can be null when running in node
|
||||
// always check whether channel is available
|
||||
this._channel = channel;
|
||||
this._storyStore = storyStore;
|
||||
this._reduxStore = reduxStore;
|
||||
this._clearDecorators = clearDecorators;
|
||||
}
|
||||
|
||||
_renderMain(loaders) {
|
||||
@ -31,7 +31,7 @@ export default class ConfigApi {
|
||||
this._reduxStore.dispatch(setError(error));
|
||||
}
|
||||
|
||||
configure(loaders, module) {
|
||||
configure = (loaders, module) => {
|
||||
const render = () => {
|
||||
try {
|
||||
this._renderMain(loaders);
|
||||
@ -54,7 +54,7 @@ export default class ConfigApi {
|
||||
setTimeout(render);
|
||||
});
|
||||
module.hot.dispose(() => {
|
||||
clearDecorators();
|
||||
this._clearDecorators();
|
||||
});
|
||||
}
|
||||
|
||||
@ -63,5 +63,5 @@ export default class ConfigApi {
|
||||
} else {
|
||||
loaders();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
8
lib/core/src/client/preview/index.js
Normal file
8
lib/core/src/client/preview/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import * as Actions from './actions';
|
||||
import ClientApi from './client_api';
|
||||
import ConfigApi from './config_api';
|
||||
import StoryStore from './story_store';
|
||||
import reducer from './reducer';
|
||||
import syncUrlWithStore from './syncUrlWithStore';
|
||||
|
||||
export default { Actions, ClientApi, ConfigApi, StoryStore, reducer, syncUrlWithStore };
|
@ -1,4 +1,5 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
let count = 0;
|
||||
|
||||
@ -7,12 +8,10 @@ function getId() {
|
||||
return count;
|
||||
}
|
||||
|
||||
export default class StoryStore {
|
||||
export default class StoryStore extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this._data = {};
|
||||
// This number is incremented on every HMR.
|
||||
// In theory it could also be incremented if stories were dynamically
|
||||
// changed in the store
|
||||
this._revision = 0;
|
||||
}
|
||||
|
||||
@ -39,6 +38,8 @@ export default class StoryStore {
|
||||
index: getId(),
|
||||
fn,
|
||||
};
|
||||
|
||||
this.emit('storyAdded', kind, name, fn);
|
||||
}
|
||||
|
||||
getStoryKinds() {
|
||||
@ -96,7 +97,10 @@ export default class StoryStore {
|
||||
}
|
||||
|
||||
dumpStoryBook() {
|
||||
const data = this.getStoryKinds().map(kind => ({ kind, stories: this.getStories(kind) }));
|
||||
const data = this.getStoryKinds().map(kind => ({
|
||||
kind,
|
||||
stories: this.getStories(kind),
|
||||
}));
|
||||
|
||||
return data;
|
||||
}
|
26
lib/core/src/client/preview/syncUrlWithStore.js
Normal file
26
lib/core/src/client/preview/syncUrlWithStore.js
Normal file
@ -0,0 +1,26 @@
|
||||
import qs from 'qs';
|
||||
import { window } from 'global';
|
||||
import { selectStory } from './actions';
|
||||
|
||||
// Ensure the story in the redux store and on the preview URL are in sync.
|
||||
// In theory we should listen to pushState events but given it's an iframe
|
||||
// the user can't actually change the URL.
|
||||
// We should change this if we support a "preview only" mode in the future.
|
||||
export default function syncUrlToStore(reduxStore) {
|
||||
// handle query params
|
||||
const queryParams = qs.parse(window.location.search.substring(1));
|
||||
if (queryParams.selectedKind) {
|
||||
reduxStore.dispatch(selectStory(queryParams.selectedKind, queryParams.selectedStory));
|
||||
}
|
||||
|
||||
reduxStore.subscribe(() => {
|
||||
const { selectedKind, selectedStory } = reduxStore.getState();
|
||||
|
||||
const queryString = qs.stringify({
|
||||
...queryParams,
|
||||
selectedKind,
|
||||
selectedStory,
|
||||
});
|
||||
window.history.pushState({}, '', `?${queryString}`);
|
||||
});
|
||||
}
|
@ -71,3 +71,13 @@ export default function handle(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// window.keydown handler to dispatch a key event to the preview channel
|
||||
export function handleKeyboardShortcuts(channel) {
|
||||
return event => {
|
||||
const parsedEvent = handle(event);
|
||||
if (parsedEvent) {
|
||||
channel.emit('applyShortcut', { event: parsedEvent });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -10939,7 +10939,7 @@ qs@4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
|
||||
|
||||
qs@6.5.1, qs@^6.4.0, qs@^6.5.1, qs@~6.5.1:
|
||||
qs@6.5.1, qs@^6.5.1, qs@~6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user