import { defineComponent } from "vue"; import type { PropType, VNode } from "vue"; import type { DAspectRatioProps } from "@darwin-studio/vue-ui/src/components/containers/d-aspect-ratio/types"; import { DAspectRatioAsync as DAspectRatio } from "@darwin-studio/vue-ui/src/components/containers/d-aspect-ratio/async"; import { EVENT_NAME } from "@darwin-studio/vue-ui/src/constants/event-name"; import generateProp from "@darwin-studio/vue-ui/src/utils/generate-prop"; import generateClass from "@darwin-studio/vue-ui/src/utils/generate-class"; import type { PictureSource, Source, SourceType, PreparedSource, Loading, ObjectFit, } from "./types"; import { prepareSource } from "./utils"; import { LOADING, OBJECT_FIT, SOURCE_TYPE } from "./constants"; import config from "./config"; // TODO: separate figure component with caption, loader and no-image placeholder ??? // TODO: is it a molecule, not an atom ??? /** * The component renders picture element according to the Responsive Image Principle.
* Supports plain image source string, object with srcset and array of image assets for different screen width and pixel density. */ export default defineComponent({ name: config.name, props: { /** * Expected formats:
* * '/image_src_string'
* * { srcset: [
* { density: '1x', src: 'img_src_string_sm_1x', type: 'image/jpeg' },
* { density: '2x', src: 'img_src_string_sm_2x', type: 'image/jpeg' }
* ]}
* * [
* { min_width: 320, src: 'img_src_string_xs', type: 'image/jpeg' },
* { max_width: 1280, srcset: [
* { density: '1x', src: 'img_src_string_sm_1x', type: 'image/jpeg' },
* { density: '2x', src: 'img_src_string_sm_2x', type: 'image/jpeg' }
* ]}
* ] */ source: { // TODO: what if { srcset: [{ src: '', min/max_width: 999, src_width: 999 }]} type: [Array, Object, String] as PropType, // TODO: add prop generator? }, /** * Aspect ratio of the picture. * Expected format: 2 || '0.5' || 'width/height' || 'width:height' */ aspectRatio: generateProp.text(), /** * Pass any DAspectRatio.props to customize aspect ratio container, f.e. { class: "someClass" } */ aspectRatioOptions: generateProp.options(), /** * Renders to the object-fit attr of the img element */ // TODO: imgOptions objectFit: generateProp.string(OBJECT_FIT.COVER), /** * You can pass own class name to the img element. */ // TODO: imgOptions imageClass: String, /** * The picture caption. Also used as alt and title attrs if they don't present */ // TODO: imgOptions, descr alt: String, // TODO: use DCaption caption: generateProp.content(), /** * Defines font size of the caption element. By default depends on props.size */ // TODO: captionOptions captionFont: generateProp.font(), /** * You can pass own class name to the caption element. */ // TODO: captionOptions captionClass: String, /** * Renders to the loading attr of the img element */ loading: generateProp.string(LOADING.LAZY), // TODO: use DLoader??? // TODO: preventDefault ??? // TODO: caption gaps/spacing /** * Alternative way to catch load event */ whenLoad: { type: Function as PropType<(event?: Event) => void | Promise>, }, /** * Alternative way to catch error event */ whenError: { type: Function as PropType<(event?: Event) => void | Promise>, }, }, emits: [EVENT_NAME.LOAD, EVENT_NAME.ERROR], computed: { sourceType(): SourceType | null { if (Array.isArray(this.source)) { return SOURCE_TYPE.ARRAY; } if (typeof this.source === "object" && this.source !== null) { return SOURCE_TYPE.OBJECT; // TODO: test case coverage } if (typeof this.source === "string") { return SOURCE_TYPE.STRING; } return null; }, preparedSourceList(): PreparedSource[] { switch (this.sourceType) { case SOURCE_TYPE.ARRAY: // TODO: min/max/medium sort order, is it make sense ??? return (this.source as PictureSource[]).map((pictureSource) => { return prepareSource(pictureSource); }); case SOURCE_TYPE.OBJECT: return [prepareSource(this.source as PictureSource)]; case SOURCE_TYPE.STRING: default: return [{ src: this.source as string }]; } }, imgVNode(): VNode { const src = this.preparedSourceList[0].src; // TODO: use first/last/special element from the array ??? const srcset = this.preparedSourceList[0].srcset; const classes = this.hasContainer || this.sourceType === SOURCE_TYPE.ARRAY ? [config.innerImageClass, this.imageClass] : [config.class, this.imageClass]; return ( {String(this.alt)} ); }, renderCaption(): VNode { return (
{this.$slots.caption?.() || this.caption}
); }, hasContainer(): boolean { return Boolean( this.aspectRatio || this.$slots.caption?.() || this.caption ); }, // TODO: simplify renderImage(): VNode | null { /* hasn't container */ if (!this.hasContainer) { return this.imgVNode; } /* has aspectRation container */ if (this.aspectRatio && !this.caption) { return ( {this.imgVNode} ); } /* has figure container with figcaption */ if (!this.aspectRatio && (this.caption || this.$slots.caption)) { return (
{this.imgVNode} {this.renderCaption}
); } /* has aspect ratio container with figcaption */ return ( {this.imgVNode} {this.renderCaption} ); }, // TODO: simplify renderPicture(): VNode | null { const sourceVNodeList = this.preparedSourceList?.map((item) => ( )); const pictureVNode = ( {sourceVNodeList} {this.imgVNode} ); /* container is the picture itself */ if (!this.hasContainer) { return pictureVNode; } /* has aspectRation container */ if (this.aspectRatio && !this.caption) { return ( {sourceVNodeList} {this.imgVNode} ); } /* has figure container with figcaption */ if (!this.aspectRatio && (this.caption || this.$slots.caption)) { return (
{pictureVNode} {this.renderCaption}
); } /* has aspect ratio container with figcaption */ return ( {pictureVNode} {this.renderCaption} ); }, }, methods: { loadedHandler(event: Event): void { /** * Emits on image load with Event payload * @event load * @type {event: Event} */ this.$emit(EVENT_NAME.LOAD, event); this.whenLoad?.(event); }, errorHandler(event: Event): void { /** * Emits on image error with Event payload * @event click * @type {event: Event} */ this.$emit(EVENT_NAME.ERROR, event); this.whenError?.(event); }, }, /** * @slot $slots.caption * Use instead of props.caption to fully customize caption content * */ render(): VNode | null { switch (this.sourceType) { case SOURCE_TYPE.ARRAY: return this.renderPicture; case SOURCE_TYPE.OBJECT: case SOURCE_TYPE.STRING: return this.renderImage; default: return null; } }, }); // TODO: move to the common ??? declare module "@vue/runtime-dom" { interface ImgHTMLAttributes extends HTMLAttributes { loading?: Loading; } }