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 (
);
},
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;
}
}