UNPKG

@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
'use strict'; 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