@stories-js/vue
Version:
Vue 3 Stories renderer and wrapper for custom elements of Stories to be used as first-class Vue 3 components
543 lines (533 loc) • 21.9 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
const vue = require('vue');
const storiesAddonActions_js = require('@stories-js/core/components/stories-addon-actions.js');
const storiesAddonControls_js = require('@stories-js/core/components/stories-addon-controls.js');
const storiesAddons_js = require('@stories-js/core/components/stories-addons.js');
const storiesApp_js = require('@stories-js/core/components/stories-app.js');
const storiesBadge_js = require('@stories-js/core/components/stories-badge.js');
const storiesButton_js = require('@stories-js/core/components/stories-button.js');
const storiesButtons_js = require('@stories-js/core/components/stories-buttons.js');
const storiesCheckbox_js = require('@stories-js/core/components/stories-checkbox.js');
const storiesCol_js = require('@stories-js/core/components/stories-col.js');
const storiesFooter_js = require('@stories-js/core/components/stories-footer.js');
const storiesGrid_js = require('@stories-js/core/components/stories-grid.js');
const storiesIcon_js = require('@stories-js/core/components/stories-icon.js');
const storiesInput_js = require('@stories-js/core/components/stories-input.js');
const storiesLabel_js = require('@stories-js/core/components/stories-label.js');
const storiesPreview_js = require('@stories-js/core/components/stories-preview.js');
const storiesRouter_js = require('@stories-js/core/components/stories-router.js');
const storiesRow_js = require('@stories-js/core/components/stories-row.js');
const storiesSearchbar_js = require('@stories-js/core/components/stories-searchbar.js');
const storiesSidebar_js = require('@stories-js/core/components/stories-sidebar.js');
const storiesSplitPane_js = require('@stories-js/core/components/stories-split-pane.js');
const storiesTab_js = require('@stories-js/core/components/stories-tab.js');
const storiesTabBar_js = require('@stories-js/core/components/stories-tab-bar.js');
const storiesTabButton_js = require('@stories-js/core/components/stories-tab-button.js');
const storiesTabs_js = require('@stories-js/core/components/stories-tabs.js');
const storiesToolBar_js = require('@stories-js/core/components/stories-tool-bar.js');
const storiesToolButton_js = require('@stories-js/core/components/stories-tool-button.js');
const storiesToolZoom_js = require('@stories-js/core/components/stories-tool-zoom.js');
const storiesZoom_js = require('@stories-js/core/components/stories-zoom.js');
const components = require('@stories-js/core/components');
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
// import { Component } from 'vue';
// function getType(fn: any) {
// const match = fn && fn.toString().match(/^\s*function (\w+)/);
// return match ? match[1] : '';
// }
// // https://github.com/vuejs/vue/blob/dev/src/core/util/props.js#L92
// function resolveDefault({ type, default: def }: any) {
// if (typeof def === 'function' && getType(type) !== 'Function') {
// // known limitation: we don't have the component instance to pass
// return def.call();
// }
// return def;
// }
// export function extractProps(component: Component): any {
// // this options business seems not good according to the types
// return Object.entries((component as any).options.props || {})
// .map(([name, prop]) => ({ [name]: resolveDefault(prop) }))
// .reduce((wrap, prop) => ({ ...wrap, ...prop }), {});
// }
/**
* Performs a rest spread on an object.
*
* @param source The source value.
* @param propertyNames The property names excluded from the rest spread.
*/
function rest(source, propertyNames) {
const result = {};
for (const p in source)
if (Object.prototype.hasOwnProperty.call(source, p) && propertyNames.indexOf(p) < 0)
result[p] = source[p];
if (source != null && typeof Object.getOwnPropertySymbols === "function")
for (let i = 0, p = Object.getOwnPropertySymbols(source); i < p.length; i++) {
if (propertyNames.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(source, p[i]))
result[p[i]] = source[p[i]];
}
return result;
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
This normalizes a functional component into a render method in ComponentOptions.
The concept is taken from Vue 3's `defineComponent` but changed from creating a `setup`
method on the ComponentOptions so end-users don't need to specify a "thunk" as a decorator.
*/
function normalizeFunctionalComponent(options) {
return typeof options === 'function' ? { render: options, name: options.name } : options;
}
/**
* Currently StoryContextUpdates are allowed to have any key in the type.
* However, you cannot overwrite any of the build-it "static" keys.
*
* @param inputContextUpdate StoryContextUpdate
* @returns StoryContextUpdate
*/
function sanitizeStoryContextUpdate(inputContextUpdate) {
return rest(inputContextUpdate, ["componentId", "title", "kind", "id", "name", "story", "parameters", "initialArgs", "argTypes"]);
}
function prepare(rawStory, innerStory) {
const story = rawStory;
if (story == null) {
return null;
}
if (innerStory) {
return Object.assign(Object.assign({}, normalizeFunctionalComponent(story)), { components: Object.assign(Object.assign({}, (story.components || {})), { story: innerStory }) });
}
return {
render() {
return vue.h(story);
},
};
}
function decorateStory(storyFn, decorators) {
return decorators.reduce((decorated, decorator) => (context) => {
let story;
const decoratedStory = decorator((update) => {
story = decorated(Object.assign(Object.assign({}, context), sanitizeStoryContextUpdate(update)));
return story;
}, context);
if (!story) {
story = decorated(context);
}
if (decoratedStory === story) {
return story;
}
return prepare(decoratedStory, story);
}, (context) => prepare(storyFn(context)));
}
const StoryVueRenderer = vue.defineComponent({
name: "StoryVueRenderer",
props: {
story: Object,
},
render() {
const story = this.story;
console.log("StoryVueRenderer.story", story);
if (story) {
const storyFn = story.storyFn;
const decorators = story.decorators || [];
const context = {
args: story.args || {},
argTypes: {},
parameters: {},
initialArgs: {}
};
const r = decorateStory(storyFn, decorators);
const r1 = r(context);
console.log("rendering", r1);
return vue.h(r1);
}
return vue.h("div", "No story selected");
},
});
const UPDATE_VALUE_EVENT = 'update:modelValue';
const MODEL_VALUE = 'modelValue';
const ROUTER_LINK_VALUE = 'routerLink';
const NAV_MANAGER = 'navManager';
const ROUTER_PROP_PREFIX = 'router';
/**
* Starting in Vue 3.1.0, all properties are
* added as keys to the props object, even if
* they are not being used. In order to correctly
* account for both value props and v-model props,
* we need to check if the key exists for Vue <3.1.0
* and then check if it is not undefined for Vue >= 3.1.0.
* See https://github.com/vuejs/vue-next/issues/3889
*/
const EMPTY_PROP = Symbol();
const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP };
const getComponentClasses = (classes) => {
var _a;
return ((_a = classes) === null || _a === void 0 ? void 0 : _a.split(' ')) || [];
};
const getElementClasses = (ref, componentClasses, defaultClasses = []) => {
var _a;
return [...Array.from(((_a = ref.value) === null || _a === void 0 ? void 0 : _a.classList) || []), ...defaultClasses]
.filter((c, i, self) => !componentClasses.has(c) && self.indexOf(c) === i);
};
/**
* Create a callback to define a Vue component wrapper around a Web Component.
*
* @prop name - The component tag name (i.e. `ion-button`)
* @prop componentProps - An array of properties on the
* component. These usually match up with the @Prop definitions
* in each component's TSX file.
* @prop customElement - An option custom element instance to pass
* to customElements.define. Only set if `includeImportCustomElements: true` in your config.
* @prop modelProp - The prop that v-model binds to (i.e. value)
* @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange)
* @prop externalModelUpdateEvent - The external event to fire from your Vue component when modelUpdateEvent fires. This is used for ensuring that v-model references have been
* correctly updated when a user's event callback fires.
*/
const defineContainer = (name, defineCustomElement, componentProps = [], modelProp, modelUpdateEvent, externalModelUpdateEvent) => {
/**
* Create a Vue component wrapper around a Web Component.
* Note: The `props` here are not all properties on a component.
* They refer to whatever properties are set on an instance of a component.
*/
if (defineCustomElement !== undefined) {
defineCustomElement();
}
const Container = vue.defineComponent((props, { attrs, slots, emit }) => {
var _a;
let modelPropValue = props[modelProp];
const containerRef = vue.ref();
const classes = new Set(getComponentClasses(attrs.class));
const onVnodeBeforeMount = (vnode) => {
// Add a listener to tell Vue to update the v-model
if (vnode.el) {
const eventsNames = Array.isArray(modelUpdateEvent) ? modelUpdateEvent : [modelUpdateEvent];
eventsNames.forEach((eventName) => {
vnode.el.addEventListener(eventName.toLowerCase(), (e) => {
modelPropValue = (e === null || e === void 0 ? void 0 : e.target)[modelProp];
emit(UPDATE_VALUE_EVENT, modelPropValue);
/**
* We need to emit the change event here
* rather than on the web component to ensure
* that any v-model bindings have been updated.
* Otherwise, the developer will listen on the
* native web component, but the v-model will
* not have been updated yet.
*/
if (externalModelUpdateEvent) {
emit(externalModelUpdateEvent, e);
}
});
});
}
};
const currentInstance = vue.getCurrentInstance();
const hasRouter = (_a = currentInstance === null || currentInstance === void 0 ? void 0 : currentInstance.appContext) === null || _a === void 0 ? void 0 : _a.provides[NAV_MANAGER];
const navManager = hasRouter ? vue.inject(NAV_MANAGER) : undefined;
const handleRouterLink = (ev) => {
const { routerLink } = props;
if (routerLink === EMPTY_PROP)
return;
if (navManager !== undefined) {
let navigationPayload = { event: ev };
for (const key in props) {
const value = props[key];
if (props.hasOwnProperty(key) && key.startsWith(ROUTER_PROP_PREFIX) && value !== EMPTY_PROP) {
navigationPayload[key] = value;
}
}
navManager.navigate(navigationPayload);
}
else {
console.warn('Tried to navigate, but no router was found. Make sure you have mounted Vue Router.');
}
};
return () => {
modelPropValue = props[modelProp];
getComponentClasses(attrs.class).forEach(value => {
classes.add(value);
});
const oldClick = props.onClick;
const handleClick = (ev) => {
if (oldClick !== undefined) {
oldClick(ev);
}
if (!ev.defaultPrevented) {
handleRouterLink(ev);
}
};
let propsToAdd = {
ref: containerRef,
class: getElementClasses(containerRef, classes),
onClick: handleClick,
onVnodeBeforeMount: (modelUpdateEvent) ? onVnodeBeforeMount : undefined
};
/**
* We can use Object.entries here
* to avoid the hasOwnProperty check,
* but that would require 2 iterations
* where as this only requires 1.
*/
for (const key in props) {
const value = props[key];
if (props.hasOwnProperty(key) && value !== EMPTY_PROP) {
propsToAdd[key] = value;
}
}
if (modelProp) {
/**
* If form value property was set using v-model
* then we should use that value.
* Otherwise, check to see if form value property
* was set as a static value (i.e. no v-model).
*/
if (props[MODEL_VALUE] !== EMPTY_PROP) {
propsToAdd = Object.assign(Object.assign({}, propsToAdd), { [modelProp]: props[MODEL_VALUE] });
}
else if (modelPropValue !== EMPTY_PROP) {
propsToAdd = Object.assign(Object.assign({}, propsToAdd), { [modelProp]: modelPropValue });
}
}
return vue.h(name, propsToAdd, slots.default && slots.default());
};
});
Container.displayName = name;
Container.props = {
[ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP
};
componentProps.forEach(componentProp => {
Container.props[componentProp] = DEFAULT_EMPTY_PROP;
});
if (modelProp) {
Container.props[MODEL_VALUE] = DEFAULT_EMPTY_PROP;
Container.emits = [UPDATE_VALUE_EVENT, externalModelUpdateEvent];
}
return Container;
};
/* eslint-disable */
const StoriesAddonActions = /*@__PURE__*/ defineContainer('stories-addon-actions', storiesAddonActions_js.defineCustomElement);
const StoriesAddonControls = /*@__PURE__*/ defineContainer('stories-addon-controls', storiesAddonControls_js.defineCustomElement);
const StoriesAddons = /*@__PURE__*/ defineContainer('stories-addons', storiesAddons_js.defineCustomElement);
const StoriesApp = /*@__PURE__*/ defineContainer('stories-app', storiesApp_js.defineCustomElement, [
'modules',
'store',
'storyChange',
'storyContextChange'
]);
const StoriesBadge = /*@__PURE__*/ defineContainer('stories-badge', storiesBadge_js.defineCustomElement, [
'color'
]);
const StoriesButton = /*@__PURE__*/ defineContainer('stories-button', storiesButton_js.defineCustomElement, [
'color',
'buttonType',
'disabled',
'expand',
'fill',
'routerDirection',
'href',
'shape',
'type',
'size',
'strong',
'target',
'storiesFocus',
'storiesBlur',
'storiesClick'
]);
const StoriesButtons = /*@__PURE__*/ defineContainer('stories-buttons', storiesButtons_js.defineCustomElement, [
'collapse'
]);
const StoriesCheckbox = /*@__PURE__*/ defineContainer('stories-checkbox', storiesCheckbox_js.defineCustomElement, [
'color',
'name',
'checked',
'indeterminate',
'disabled',
'value',
'storiesChange',
'storiesFocus',
'storiesBlur',
'storiesStyle'
], 'checked', 'v-stories-change', 'storiesChange');
const StoriesCol = /*@__PURE__*/ defineContainer('stories-col', storiesCol_js.defineCustomElement, [
'offset',
'offsetXs',
'offsetSm',
'offsetMd',
'offsetLg',
'offsetXl',
'pull',
'pullXs',
'pullSm',
'pullMd',
'pullLg',
'pullXl',
'push',
'pushXs',
'pushSm',
'pushMd',
'pushLg',
'pushXl',
'size',
'sizeXs',
'sizeSm',
'sizeMd',
'sizeLg',
'sizeXl'
]);
const StoriesFooter = /*@__PURE__*/ defineContainer('stories-footer', storiesFooter_js.defineCustomElement);
const StoriesGrid = /*@__PURE__*/ defineContainer('stories-grid', storiesGrid_js.defineCustomElement, [
'fixed'
]);
const StoriesIcon = /*@__PURE__*/ defineContainer('stories-icon', storiesIcon_js.defineCustomElement, [
'name'
]);
const StoriesInput = /*@__PURE__*/ defineContainer('stories-input', storiesInput_js.defineCustomElement, [
'fireFocusEvents',
'color',
'autofocus',
'clearInput',
'debounce',
'disabled',
'inputmode',
'max',
'maxlength',
'min',
'minlength',
'name',
'pattern',
'placeholder',
'readonly',
'required',
'step',
'size',
'type',
'value',
'storiesInput',
'storiesChange',
'storiesBlur',
'storiesFocus',
'storiesStyle'
], 'value', 'v-stories-change', 'storiesChange');
const StoriesLabel = /*@__PURE__*/ defineContainer('stories-label', storiesLabel_js.defineCustomElement, [
'color',
'position'
]);
const StoriesPreview = /*@__PURE__*/ defineContainer('stories-preview', storiesPreview_js.defineCustomElement);
const StoriesRouter = /*@__PURE__*/ defineContainer('stories-router', storiesRouter_js.defineCustomElement);
const StoriesRow = /*@__PURE__*/ defineContainer('stories-row', storiesRow_js.defineCustomElement);
const StoriesSearchbar = /*@__PURE__*/ defineContainer('stories-searchbar', storiesSearchbar_js.defineCustomElement, [
'color',
'cancelButtonIcon',
'clearIcon',
'debounce',
'disabled',
'inputmode',
'placeholder',
'searchIcon',
'showCancelButton',
'showClearButton',
'type',
'value',
'storiesInput',
'storiesChange',
'storiesCancel',
'storiesClear',
'storiesBlur',
'storiesFocus',
'storiesStyle'
], 'value', 'v-stories-change', 'storiesChange');
const StoriesSidebar = /*@__PURE__*/ defineContainer('stories-sidebar', storiesSidebar_js.defineCustomElement);
const StoriesSplitPane = /*@__PURE__*/ defineContainer('stories-split-pane', storiesSplitPane_js.defineCustomElement, [
'split',
'minSize',
'defaultSize',
'isResizing',
'storiesSizeChange'
]);
const StoriesTab = /*@__PURE__*/ defineContainer('stories-tab', storiesTab_js.defineCustomElement, [
'active',
'tab'
]);
const StoriesTabBar = /*@__PURE__*/ defineContainer('stories-tab-bar', storiesTabBar_js.defineCustomElement, [
'color',
'selectedTab',
'storiesTabBarChange'
]);
const StoriesTabButton = /*@__PURE__*/ defineContainer('stories-tab-button', storiesTabButton_js.defineCustomElement, [
'disabled',
'layout',
'selected',
'tab',
'storiesTabButtonClick'
]);
const StoriesTabs = /*@__PURE__*/ defineContainer('stories-tabs', storiesTabs_js.defineCustomElement);
const StoriesToolBar = /*@__PURE__*/ defineContainer('stories-tool-bar', storiesToolBar_js.defineCustomElement);
const StoriesToolButton = /*@__PURE__*/ defineContainer('stories-tool-button', storiesToolButton_js.defineCustomElement, [
'disabled',
'icon',
'command',
'storiesAction'
]);
const StoriesToolZoom = /*@__PURE__*/ defineContainer('stories-tool-zoom', storiesToolZoom_js.defineCustomElement);
const StoriesZoom = /*@__PURE__*/ defineContainer('stories-zoom', storiesZoom_js.defineCustomElement, [
'zoom'
]);
/**
* We need to make sure that the web component fires an event
* that will not conflict with the user's @stories-change binding,
* otherwise the binding's callback will fire before any
* v-model values have been updated.
*/
const toKebabCase = (eventName) => eventName === "stories-change"
? "v-stories-change"
: eventName.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase();
const getHelperFunctions = () => {
return {
ael: (el, eventName, cb, opts) => el.addEventListener(toKebabCase(eventName), cb, opts),
rel: (el, eventName, cb, opts) => el.removeEventListener(toKebabCase(eventName), cb, opts),
ce: (eventName, opts) => new CustomEvent(toKebabCase(eventName), opts),
};
};
const StoriesVue = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async install(_) {
if (typeof window !== "undefined") {
const { ael, rel, ce } = getHelperFunctions();
components.initialize({
_ael: ael,
_rel: rel,
_ce: ce,
});
}
},
};
exports.StoriesAddonActions = StoriesAddonActions;
exports.StoriesAddonControls = StoriesAddonControls;
exports.StoriesAddons = StoriesAddons;
exports.StoriesApp = StoriesApp;
exports.StoriesBadge = StoriesBadge;
exports.StoriesButton = StoriesButton;
exports.StoriesButtons = StoriesButtons;
exports.StoriesCheckbox = StoriesCheckbox;
exports.StoriesCol = StoriesCol;
exports.StoriesFooter = StoriesFooter;
exports.StoriesGrid = StoriesGrid;
exports.StoriesIcon = StoriesIcon;
exports.StoriesInput = StoriesInput;
exports.StoriesLabel = StoriesLabel;
exports.StoriesPreview = StoriesPreview;
exports.StoriesRouter = StoriesRouter;
exports.StoriesRow = StoriesRow;
exports.StoriesSearchbar = StoriesSearchbar;
exports.StoriesSidebar = StoriesSidebar;
exports.StoriesSplitPane = StoriesSplitPane;
exports.StoriesTab = StoriesTab;
exports.StoriesTabBar = StoriesTabBar;
exports.StoriesTabButton = StoriesTabButton;
exports.StoriesTabs = StoriesTabs;
exports.StoriesToolBar = StoriesToolBar;
exports.StoriesToolButton = StoriesToolButton;
exports.StoriesToolZoom = StoriesToolZoom;
exports.StoriesVue = StoriesVue;
exports.StoriesZoom = StoriesZoom;
exports.StoryVueRenderer = StoryVueRenderer;
//# sourceMappingURL=index.js.map