VImg.mjs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import { withDirectives as _withDirectives, mergeProps as _mergeProps, resolveDirective as _resolveDirective, Fragment as _Fragment, createVNode as _createVNode } from "vue";
  2. // Styles
  3. import "./VImg.css";
  4. // Components
  5. import { makeVResponsiveProps, VResponsive } from "../VResponsive/VResponsive.mjs"; // Composables
  6. import { useBackgroundColor } from "../../composables/color.mjs";
  7. import { makeComponentProps } from "../../composables/component.mjs";
  8. import { makeRoundedProps, useRounded } from "../../composables/rounded.mjs";
  9. import { makeTransitionProps, MaybeTransition } from "../../composables/transition.mjs"; // Directives
  10. import intersect from "../../directives/intersect/index.mjs"; // Utilities
  11. import { computed, nextTick, onBeforeMount, onBeforeUnmount, ref, shallowRef, toRef, vShow, watch, withDirectives } from 'vue';
  12. import { convertToUnit, genericComponent, getCurrentInstance, propsFactory, SUPPORTS_INTERSECTION, useRender } from "../../util/index.mjs"; // Types
  13. // not intended for public use, this is passed in by vuetify-loader
  14. export const makeVImgProps = propsFactory({
  15. absolute: Boolean,
  16. alt: String,
  17. cover: Boolean,
  18. color: String,
  19. draggable: {
  20. type: [Boolean, String],
  21. default: undefined
  22. },
  23. eager: Boolean,
  24. gradient: String,
  25. lazySrc: String,
  26. options: {
  27. type: Object,
  28. // For more information on types, navigate to:
  29. // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
  30. default: () => ({
  31. root: undefined,
  32. rootMargin: undefined,
  33. threshold: undefined
  34. })
  35. },
  36. sizes: String,
  37. src: {
  38. type: [String, Object],
  39. default: ''
  40. },
  41. crossorigin: String,
  42. referrerpolicy: String,
  43. srcset: String,
  44. position: String,
  45. ...makeVResponsiveProps(),
  46. ...makeComponentProps(),
  47. ...makeRoundedProps(),
  48. ...makeTransitionProps()
  49. }, 'VImg');
  50. export const VImg = genericComponent()({
  51. name: 'VImg',
  52. directives: {
  53. intersect
  54. },
  55. props: makeVImgProps(),
  56. emits: {
  57. loadstart: value => true,
  58. load: value => true,
  59. error: value => true
  60. },
  61. setup(props, _ref) {
  62. let {
  63. emit,
  64. slots
  65. } = _ref;
  66. const {
  67. backgroundColorClasses,
  68. backgroundColorStyles
  69. } = useBackgroundColor(toRef(props, 'color'));
  70. const {
  71. roundedClasses
  72. } = useRounded(props);
  73. const vm = getCurrentInstance('VImg');
  74. const currentSrc = shallowRef(''); // Set from srcset
  75. const image = ref();
  76. const state = shallowRef(props.eager ? 'loading' : 'idle');
  77. const naturalWidth = shallowRef();
  78. const naturalHeight = shallowRef();
  79. const normalisedSrc = computed(() => {
  80. return props.src && typeof props.src === 'object' ? {
  81. src: props.src.src,
  82. srcset: props.srcset || props.src.srcset,
  83. lazySrc: props.lazySrc || props.src.lazySrc,
  84. aspect: Number(props.aspectRatio || props.src.aspect || 0)
  85. } : {
  86. src: props.src,
  87. srcset: props.srcset,
  88. lazySrc: props.lazySrc,
  89. aspect: Number(props.aspectRatio || 0)
  90. };
  91. });
  92. const aspectRatio = computed(() => {
  93. return normalisedSrc.value.aspect || naturalWidth.value / naturalHeight.value || 0;
  94. });
  95. watch(() => props.src, () => {
  96. init(state.value !== 'idle');
  97. });
  98. watch(aspectRatio, (val, oldVal) => {
  99. if (!val && oldVal && image.value) {
  100. pollForSize(image.value);
  101. }
  102. });
  103. // TODO: getSrc when window width changes
  104. onBeforeMount(() => init());
  105. function init(isIntersecting) {
  106. if (props.eager && isIntersecting) return;
  107. if (SUPPORTS_INTERSECTION && !isIntersecting && !props.eager) return;
  108. state.value = 'loading';
  109. if (normalisedSrc.value.lazySrc) {
  110. const lazyImg = new Image();
  111. lazyImg.src = normalisedSrc.value.lazySrc;
  112. pollForSize(lazyImg, null);
  113. }
  114. if (!normalisedSrc.value.src) return;
  115. nextTick(() => {
  116. emit('loadstart', image.value?.currentSrc || normalisedSrc.value.src);
  117. setTimeout(() => {
  118. if (vm.isUnmounted) return;
  119. if (image.value?.complete) {
  120. if (!image.value.naturalWidth) {
  121. onError();
  122. }
  123. if (state.value === 'error') return;
  124. if (!aspectRatio.value) pollForSize(image.value, null);
  125. if (state.value === 'loading') onLoad();
  126. } else {
  127. if (!aspectRatio.value) pollForSize(image.value);
  128. getSrc();
  129. }
  130. });
  131. });
  132. }
  133. function onLoad() {
  134. if (vm.isUnmounted) return;
  135. getSrc();
  136. pollForSize(image.value);
  137. state.value = 'loaded';
  138. emit('load', image.value?.currentSrc || normalisedSrc.value.src);
  139. }
  140. function onError() {
  141. if (vm.isUnmounted) return;
  142. state.value = 'error';
  143. emit('error', image.value?.currentSrc || normalisedSrc.value.src);
  144. }
  145. function getSrc() {
  146. const img = image.value;
  147. if (img) currentSrc.value = img.currentSrc || img.src;
  148. }
  149. let timer = -1;
  150. onBeforeUnmount(() => {
  151. clearTimeout(timer);
  152. });
  153. function pollForSize(img) {
  154. let timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
  155. const poll = () => {
  156. clearTimeout(timer);
  157. if (vm.isUnmounted) return;
  158. const {
  159. naturalHeight: imgHeight,
  160. naturalWidth: imgWidth
  161. } = img;
  162. if (imgHeight || imgWidth) {
  163. naturalWidth.value = imgWidth;
  164. naturalHeight.value = imgHeight;
  165. } else if (!img.complete && state.value === 'loading' && timeout != null) {
  166. timer = window.setTimeout(poll, timeout);
  167. } else if (img.currentSrc.endsWith('.svg') || img.currentSrc.startsWith('data:image/svg+xml')) {
  168. naturalWidth.value = 1;
  169. naturalHeight.value = 1;
  170. }
  171. };
  172. poll();
  173. }
  174. const containClasses = computed(() => ({
  175. 'v-img__img--cover': props.cover,
  176. 'v-img__img--contain': !props.cover
  177. }));
  178. const __image = () => {
  179. if (!normalisedSrc.value.src || state.value === 'idle') return null;
  180. const img = _createVNode("img", {
  181. "class": ['v-img__img', containClasses.value],
  182. "style": {
  183. objectPosition: props.position
  184. },
  185. "src": normalisedSrc.value.src,
  186. "srcset": normalisedSrc.value.srcset,
  187. "alt": props.alt,
  188. "crossorigin": props.crossorigin,
  189. "referrerpolicy": props.referrerpolicy,
  190. "draggable": props.draggable,
  191. "sizes": props.sizes,
  192. "ref": image,
  193. "onLoad": onLoad,
  194. "onError": onError
  195. }, null);
  196. const sources = slots.sources?.();
  197. return _createVNode(MaybeTransition, {
  198. "transition": props.transition,
  199. "appear": true
  200. }, {
  201. default: () => [withDirectives(sources ? _createVNode("picture", {
  202. "class": "v-img__picture"
  203. }, [sources, img]) : img, [[vShow, state.value === 'loaded']])]
  204. });
  205. };
  206. const __preloadImage = () => _createVNode(MaybeTransition, {
  207. "transition": props.transition
  208. }, {
  209. default: () => [normalisedSrc.value.lazySrc && state.value !== 'loaded' && _createVNode("img", {
  210. "class": ['v-img__img', 'v-img__img--preload', containClasses.value],
  211. "style": {
  212. objectPosition: props.position
  213. },
  214. "src": normalisedSrc.value.lazySrc,
  215. "alt": props.alt,
  216. "crossorigin": props.crossorigin,
  217. "referrerpolicy": props.referrerpolicy,
  218. "draggable": props.draggable
  219. }, null)]
  220. });
  221. const __placeholder = () => {
  222. if (!slots.placeholder) return null;
  223. return _createVNode(MaybeTransition, {
  224. "transition": props.transition,
  225. "appear": true
  226. }, {
  227. default: () => [(state.value === 'loading' || state.value === 'error' && !slots.error) && _createVNode("div", {
  228. "class": "v-img__placeholder"
  229. }, [slots.placeholder()])]
  230. });
  231. };
  232. const __error = () => {
  233. if (!slots.error) return null;
  234. return _createVNode(MaybeTransition, {
  235. "transition": props.transition,
  236. "appear": true
  237. }, {
  238. default: () => [state.value === 'error' && _createVNode("div", {
  239. "class": "v-img__error"
  240. }, [slots.error()])]
  241. });
  242. };
  243. const __gradient = () => {
  244. if (!props.gradient) return null;
  245. return _createVNode("div", {
  246. "class": "v-img__gradient",
  247. "style": {
  248. backgroundImage: `linear-gradient(${props.gradient})`
  249. }
  250. }, null);
  251. };
  252. const isBooted = shallowRef(false);
  253. {
  254. const stop = watch(aspectRatio, val => {
  255. if (val) {
  256. // Doesn't work with nextTick, idk why
  257. requestAnimationFrame(() => {
  258. requestAnimationFrame(() => {
  259. isBooted.value = true;
  260. });
  261. });
  262. stop();
  263. }
  264. });
  265. }
  266. useRender(() => {
  267. const responsiveProps = VResponsive.filterProps(props);
  268. return _withDirectives(_createVNode(VResponsive, _mergeProps({
  269. "class": ['v-img', {
  270. 'v-img--absolute': props.absolute,
  271. 'v-img--booting': !isBooted.value
  272. }, backgroundColorClasses.value, roundedClasses.value, props.class],
  273. "style": [{
  274. width: convertToUnit(props.width === 'auto' ? naturalWidth.value : props.width)
  275. }, backgroundColorStyles.value, props.style]
  276. }, responsiveProps, {
  277. "aspectRatio": aspectRatio.value,
  278. "aria-label": props.alt,
  279. "role": props.alt ? 'img' : undefined
  280. }), {
  281. additional: () => _createVNode(_Fragment, null, [_createVNode(__image, null, null), _createVNode(__preloadImage, null, null), _createVNode(__gradient, null, null), _createVNode(__placeholder, null, null), _createVNode(__error, null, null)]),
  282. default: slots.default
  283. }), [[_resolveDirective("intersect"), {
  284. handler: init,
  285. options: props.options
  286. }, null, {
  287. once: true
  288. }]]);
  289. });
  290. return {
  291. currentSrc,
  292. image,
  293. state,
  294. naturalWidth,
  295. naturalHeight
  296. };
  297. }
  298. });
  299. //# sourceMappingURL=VImg.mjs.map