@stories-js/vue
Version:
Vue 3 Stories renderer and wrapper for custom elements of Stories to be used as first-class Vue 3 components
510 lines (502 loc) • 21.4 kB
JavaScript
import { defineComponent, h, ref, getCurrentInstance, inject } from 'vue';
import { defineCustomElement } from '@stories-js/core/components/stories-addon-actions.js';
import { defineCustomElement as defineCustomElement$1 } from '@stories-js/core/components/stories-addon-controls.js';
import { defineCustomElement as defineCustomElement$2 } from '@stories-js/core/components/stories-addons.js';
import { defineCustomElement as defineCustomElement$3 } from '@stories-js/core/components/stories-app.js';
import { defineCustomElement as defineCustomElement$4 } from '@stories-js/core/components/stories-badge.js';
import { defineCustomElement as defineCustomElement$5 } from '@stories-js/core/components/stories-button.js';
import { defineCustomElement as defineCustomElement$6 } from '@stories-js/core/components/stories-buttons.js';
import { defineCustomElement as defineCustomElement$7 } from '@stories-js/core/components/stories-checkbox.js';
import { defineCustomElement as defineCustomElement$8 } from '@stories-js/core/components/stories-col.js';
import { defineCustomElement as defineCustomElement$9 } from '@stories-js/core/components/stories-footer.js';
import { defineCustomElement as defineCustomElement$a } from '@stories-js/core/components/stories-grid.js';
import { defineCustomElement as defineCustomElement$b } from '@stories-js/core/components/stories-icon.js';
import { defineCustomElement as defineCustomElement$c } from '@stories-js/core/components/stories-input.js';
import { defineCustomElement as defineCustomElement$d } from '@stories-js/core/components/stories-label.js';
import { defineCustomElement as defineCustomElement$e } from '@stories-js/core/components/stories-preview.js';
import { defineCustomElement as defineCustomElement$f } from '@stories-js/core/components/stories-router.js';
import { defineCustomElement as defineCustomElement$g } from '@stories-js/core/components/stories-row.js';
import { defineCustomElement as defineCustomElement$h } from '@stories-js/core/components/stories-searchbar.js';
import { defineCustomElement as defineCustomElement$i } from '@stories-js/core/components/stories-sidebar.js';
import { defineCustomElement as defineCustomElement$j } from '@stories-js/core/components/stories-split-pane.js';
import { defineCustomElement as defineCustomElement$k } from '@stories-js/core/components/stories-tab.js';
import { defineCustomElement as defineCustomElement$l } from '@stories-js/core/components/stories-tab-bar.js';
import { defineCustomElement as defineCustomElement$m } from '@stories-js/core/components/stories-tab-button.js';
import { defineCustomElement as defineCustomElement$n } from '@stories-js/core/components/stories-tabs.js';
import { defineCustomElement as defineCustomElement$o } from '@stories-js/core/components/stories-tool-bar.js';
import { defineCustomElement as defineCustomElement$p } from '@stories-js/core/components/stories-tool-button.js';
import { defineCustomElement as defineCustomElement$q } from '@stories-js/core/components/stories-tool-zoom.js';
import { defineCustomElement as defineCustomElement$r } from '@stories-js/core/components/stories-zoom.js';
import { initialize } from '@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 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 = 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 h(r1);
}
return 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 = defineComponent((props, { attrs, slots, emit }) => {
var _a;
let modelPropValue = props[modelProp];
const containerRef = 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 = 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 ? 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 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', defineCustomElement);
const StoriesAddonControls = /*@__PURE__*/ defineContainer('stories-addon-controls', defineCustomElement$1);
const StoriesAddons = /*@__PURE__*/ defineContainer('stories-addons', defineCustomElement$2);
const StoriesApp = /*@__PURE__*/ defineContainer('stories-app', defineCustomElement$3, [
'modules',
'store',
'storyChange',
'storyContextChange'
]);
const StoriesBadge = /*@__PURE__*/ defineContainer('stories-badge', defineCustomElement$4, [
'color'
]);
const StoriesButton = /*@__PURE__*/ defineContainer('stories-button', defineCustomElement$5, [
'color',
'buttonType',
'disabled',
'expand',
'fill',
'routerDirection',
'href',
'shape',
'type',
'size',
'strong',
'target',
'storiesFocus',
'storiesBlur',
'storiesClick'
]);
const StoriesButtons = /*@__PURE__*/ defineContainer('stories-buttons', defineCustomElement$6, [
'collapse'
]);
const StoriesCheckbox = /*@__PURE__*/ defineContainer('stories-checkbox', defineCustomElement$7, [
'color',
'name',
'checked',
'indeterminate',
'disabled',
'value',
'storiesChange',
'storiesFocus',
'storiesBlur',
'storiesStyle'
], 'checked', 'v-stories-change', 'storiesChange');
const StoriesCol = /*@__PURE__*/ defineContainer('stories-col', defineCustomElement$8, [
'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', defineCustomElement$9);
const StoriesGrid = /*@__PURE__*/ defineContainer('stories-grid', defineCustomElement$a, [
'fixed'
]);
const StoriesIcon = /*@__PURE__*/ defineContainer('stories-icon', defineCustomElement$b, [
'name'
]);
const StoriesInput = /*@__PURE__*/ defineContainer('stories-input', defineCustomElement$c, [
'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', defineCustomElement$d, [
'color',
'position'
]);
const StoriesPreview = /*@__PURE__*/ defineContainer('stories-preview', defineCustomElement$e);
const StoriesRouter = /*@__PURE__*/ defineContainer('stories-router', defineCustomElement$f);
const StoriesRow = /*@__PURE__*/ defineContainer('stories-row', defineCustomElement$g);
const StoriesSearchbar = /*@__PURE__*/ defineContainer('stories-searchbar', defineCustomElement$h, [
'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', defineCustomElement$i);
const StoriesSplitPane = /*@__PURE__*/ defineContainer('stories-split-pane', defineCustomElement$j, [
'split',
'minSize',
'defaultSize',
'isResizing',
'storiesSizeChange'
]);
const StoriesTab = /*@__PURE__*/ defineContainer('stories-tab', defineCustomElement$k, [
'active',
'tab'
]);
const StoriesTabBar = /*@__PURE__*/ defineContainer('stories-tab-bar', defineCustomElement$l, [
'color',
'selectedTab',
'storiesTabBarChange'
]);
const StoriesTabButton = /*@__PURE__*/ defineContainer('stories-tab-button', defineCustomElement$m, [
'disabled',
'layout',
'selected',
'tab',
'storiesTabButtonClick'
]);
const StoriesTabs = /*@__PURE__*/ defineContainer('stories-tabs', defineCustomElement$n);
const StoriesToolBar = /*@__PURE__*/ defineContainer('stories-tool-bar', defineCustomElement$o);
const StoriesToolButton = /*@__PURE__*/ defineContainer('stories-tool-button', defineCustomElement$p, [
'disabled',
'icon',
'command',
'storiesAction'
]);
const StoriesToolZoom = /*@__PURE__*/ defineContainer('stories-tool-zoom', defineCustomElement$q);
const StoriesZoom = /*@__PURE__*/ defineContainer('stories-zoom', defineCustomElement$r, [
'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();
initialize({
_ael: ael,
_rel: rel,
_ce: ce,
});
}
},
};
export { StoriesAddonActions, StoriesAddonControls, StoriesAddons, StoriesApp, StoriesBadge, StoriesButton, StoriesButtons, StoriesCheckbox, StoriesCol, StoriesFooter, StoriesGrid, StoriesIcon, StoriesInput, StoriesLabel, StoriesPreview, StoriesRouter, StoriesRow, StoriesSearchbar, StoriesSidebar, StoriesSplitPane, StoriesTab, StoriesTabBar, StoriesTabButton, StoriesTabs, StoriesToolBar, StoriesToolButton, StoriesToolZoom, StoriesVue, StoriesZoom, StoryVueRenderer };
//# sourceMappingURL=index.esm.js.map