@nextcloud/vue
Version:
Nextcloud vue components
1 lines • 7.77 kB
Source Map (JSON)
{"version":3,"file":"NcBlurHash-BGaeAt4-.mjs","sources":["../../src/components/NcBlurHash/NcBlurHash.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors\n - SPDX-License-Identifier: AGPL-3.0-or-later\n-->\n\n<docs>\n## BlurHash\n\nA [blur hash](https://blurha.sh/) is a very compact representation of an image,\nthat can be used as a placeholder until the image was fully loaded.\n\n### Image placeholder\n\nThe default use case is as a placeholder that is transferred in initial state,\nwhile the real image will be fetched from the network.\nIn this case the image source can be passed to the component.\nThe component will immediately start to preload it,\nas soon as it is loaded the blur hash will be swapped with the real image and this component will behave like an `<a>`-element.\n\n```vue\n\t<template>\n\t\t<div class=\"wrapper\">\n\t\t\t<NcBlurHash class=\"shown-image\"\n\t\t\t\t:hash=\"blurHash\"\n\t\t\t\t:src=\"imageSource\"\n\t\t\t\t@load=\"onLoaded\" />\n\n\t\t\t<NcButton @click=\"toggleImage\">\n\t\t\t\t{{\n\t\t\t\t\tloading\n\t\t\t\t\t\t? 'Loading...'\n\t\t\t\t\t\t: (loaded ? 'Unload image' : 'Load image')\n\t\t\t\t}}\n\t\t\t</NcButton>\n\t\t</div>\n\t</template>\n\t<script>\n\texport default {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\tloaded: false,\n\t\t\t\tloading: false,\n\t\t\t\tblurHash: 'M8CR]OkDD%kD9ZtRayofaykC00ay$_ay~T',\n\t\t\t}\n\t\t},\n\t\tcomputed: {\n\t\t\t// This is cheating but we can not emulate slow network connection\n\t\t\t// so imagine that this means the source becomes loaded\n\t\t\timageSource() {\n\t\t\t\treturn this.loaded\n\t\t\t\t\t? 'favicon-touch.png'\n\t\t\t\t\t: 'invalid-file-that-will-never-load.png'\n\t\t\t},\n\t\t},\n\t\tmethods: {\n\t\t\ttoggleImage() {\n\t\t\t\tif (this.loaded) {\n\t\t\t\t\tthis.loaded = false\n\t\t\t\t\tthis.loading = false\n\t\t\t\t} else {\n\t\t\t\t\t// emulate slow network\n\t\t\t\t\tthis.loading = true\n\t\t\t\t\twindow.setTimeout(() => {\n\t\t\t\t\t\tthis.loaded = !this.loaded\n\t\t\t\t\t\tthis.loading = false\n\t\t\t\t\t}, 3000)\n\t\t\t\t}\n\t\t\t},\n\n\t\t\t// you could use `success` here (boolean) to decide if the image is loaded or failed\n\t\t\tonLoaded(success) {\n\t\t\t\t// ...\n\t\t\t},\n\t\t},\n\t}\n\t</script>\n\t<style scoped>\n\t\t.wrapper {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\talign-items: center;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\t.shown-image {\n\t\t\twidth: 150px;\n\t\t\theight: 150px;\n\t\t\tborder-radius: 24px;\n\t\t}\n\t</style>\n```\n\n### Manual usage as a placeholder\n\nUsing `v-if` is also possible, this can e.g. used if the image is not loaded from an URL.\n\n```vue\n\t<template>\n\t\t<div class=\"wrapper\">\n\t\t\t<img :class=\"loaded ? 'shown-image' : 'hidden-visually'\"\n\t\t\t\talt=\"\"\n\t\t\t\tsrc=\"favicon-touch.png\">\n\t\t\t<NcBlurHash v-if=\"!loaded\"\n\t\t\t\tclass=\"shown-image\"\n\t\t\t\t:hash=\"blurHash\" />\n\t\t\t<NcButton @click=\"loaded = !loaded\">Toggle blur hash</NcButton>\n\t\t</div>\n\t</template>\n\t<script>\n\texport default {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\tloaded: false,\n\t\t\t\tblurHash: 'M8CR]OkDD%kD9ZtRayofaykC00ay$_ay~T',\n\t\t\t}\n\t\t},\n\t}\n\t</script>\n\t<style scoped>\n\t\t.wrapper {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\talign-items: center;\n\t\t\tgap: 12px;\n\t\t}\n\n\t\t.shown-image {\n\t\t\twidth: 150px;\n\t\t\theight: 150px;\n\t\t\tborder-radius: 24px;\n\t\t}\n\t</style>\n```\n</docs>\n\n<script setup lang=\"ts\">\nimport { decode } from 'blurhash'\nimport { nextTick, ref, watch } from 'vue'\nimport { preloadImage } from '../../functions/preloadImage/index.ts'\nimport { logger } from '../../utils/logger.ts'\n\nconst props = defineProps<{\n\t/**\n\t * The blur hash value to use.\n\t */\n\thash: string\n\n\t/**\n\t * This is normally not needed, but if this blur hash is not only intended\n\t * for decorative purpose, descriptive text should be passed for accessibility.\n\t */\n\talt?: string\n\n\t/**\n\t * Optional an image source to load, during the load the blur hash is shown.\n\t * As soon as it is loaded the image will be shown instead.\n\t */\n\tsrc?: string\n}>()\nconst emit = defineEmits<{\n\t/**\n\t * Emitted when the image (`src`) has been loaded.\n\t */\n\tload: [boolean]\n}>()\n\nconst canvas = ref()\nconst imageLoaded = ref(false)\n\n// Redraw when hash has changed\nwatch(() => props.hash, drawBlurHash)\n// Redraw if image loaded again - also draw immediate on mount\nwatch(imageLoaded, () => {\n\tif (imageLoaded.value === false) {\n\t\t// We need to wait one tick to make sure the canvas is in the DOM\n\t\tnextTick(() => drawBlurHash())\n\t}\n}, { immediate: true })\n\n// Preload image on source change\nwatch(() => props.src, () => {\n\timageLoaded.value = false\n\tif (props.src) {\n\t\tpreloadImage(props.src)\n\t\t\t.then((success) => {\n\t\t\t\timageLoaded.value = success\n\t\t\t\temit('load', success)\n\t\t\t})\n\t}\n}, { immediate: true })\n\n/**\n * Render the BlurHash within the canvas\n */\nfunction drawBlurHash() {\n\tif (imageLoaded.value) {\n\t\t// skip\n\t\treturn\n\t}\n\n\tif (!props.hash) {\n\t\tlogger.error('Invalid BlurHash value')\n\t\treturn\n\t}\n\n\tif (canvas.value === undefined) {\n\t\t// Should never happen but better safe than sorry\n\t\tlogger.error('BlurHash canvas not available')\n\t\treturn\n\t}\n\n\tconst { height, width } = canvas.value\n\tconst pixels = decode(props.hash, width, height)\n\n\tconst ctx = canvas.value.getContext('2d')\n\tif (ctx === null) {\n\t\tlogger.error('Cannot create context for BlurHash canvas')\n\t\treturn\n\t}\n\n\tconst imageData = ctx.createImageData(width, height)\n\timageData.data.set(pixels)\n\tctx.putImageData(imageData, 0, 0)\n}\n</script>\n\n<template>\n\t<Transition\n\t\t:css=\"src ? undefined : false\"\n\t\t:enterActiveClass=\"$style.fadeTransition\"\n\t\t:leaveActiveClass=\"$style.fadeTransition\"\n\t\t:enterClass=\"$style.fadeTransitionActive\"\n\t\t:leaveToClass=\"$style.fadeTransitionActive\">\n\t\t<canvas\n\t\t\tv-if=\"!imageLoaded\"\n\t\t\tref=\"canvas\"\n\t\t\t:aria-hidden=\"alt ? undefined : 'true'\"\n\t\t\t:aria-label=\"alt\" />\n\t\t<img v-else :alt=\"alt\" :src=\"src\">\n\t</Transition>\n</template>\n\n<style module>\n.fadeTransition {\n\ttransition: all var(--animation-quick) ease;\n}\n\n.fadeTransitionActive {\n\topacity: 0;\n\tposition: absolute;\n}\n</style>\n"],"names":["_createBlock","_Transition","$style","_createElementBlock"],"mappings":";;;;;;;;;;;;;;;;AA6IA,UAAM,QAAQ;AAkBd,UAAM,OAAO;AAOb,UAAM,SAAS,IAAA;AACf,UAAM,cAAc,IAAI,KAAK;AAG7B,UAAM,MAAM,MAAM,MAAM,YAAY;AAEpC,UAAM,aAAa,MAAM;AACxB,UAAI,YAAY,UAAU,OAAO;AAEhC,iBAAS,MAAM,cAAc;AAAA,MAC9B;AAAA,IACD,GAAG,EAAE,WAAW,MAAM;AAGtB,UAAM,MAAM,MAAM,KAAK,MAAM;AAC5B,kBAAY,QAAQ;AACpB,UAAI,MAAM,KAAK;AACd,qBAAa,MAAM,GAAG,EACpB,KAAK,CAAC,YAAY;AAClB,sBAAY,QAAQ;AACpB,eAAK,QAAQ,OAAO;AAAA,QACrB,CAAC;AAAA,MACH;AAAA,IACD,GAAG,EAAE,WAAW,MAAM;AAKtB,aAAS,eAAe;AACvB,UAAI,YAAY,OAAO;AAEtB;AAAA,MACD;AAEA,UAAI,CAAC,MAAM,MAAM;AAChB,eAAO,MAAM,wBAAwB;AACrC;AAAA,MACD;AAEA,UAAI,OAAO,UAAU,QAAW;AAE/B,eAAO,MAAM,+BAA+B;AAC5C;AAAA,MACD;AAEA,YAAM,EAAE,QAAQ,MAAA,IAAU,OAAO;AACjC,YAAM,SAAS,OAAO,MAAM,MAAM,OAAO,MAAM;AAE/C,YAAM,MAAM,OAAO,MAAM,WAAW,IAAI;AACxC,UAAI,QAAQ,MAAM;AACjB,eAAO,MAAM,2CAA2C;AACxD;AAAA,MACD;AAEA,YAAM,YAAY,IAAI,gBAAgB,OAAO,MAAM;AACnD,gBAAU,KAAK,IAAI,MAAM;AACzB,UAAI,aAAa,WAAW,GAAG,CAAC;AAAA,IACjC;;0BAICA,YAYaC,YAAA;AAAA,QAXX,KAAK,QAAA,MAAM,SAAS;AAAA,QACpB,kBAAkBC,KAAAA,OAAO;AAAA,QACzB,kBAAkBA,KAAAA,OAAO;AAAA,QACzB,YAAYA,KAAAA,OAAO;AAAA,QACnB,cAAcA,KAAAA,OAAO;AAAA,MAAA;yBACtB,MAIqB;AAAA,WAHb,YAAA,sBADRC,mBAIqB,UAAA;AAAA;qBAFhB;AAAA,YAAJ,KAAI;AAAA,YACH,eAAa,QAAA,MAAM,SAAS;AAAA,YAC5B,cAAY,QAAA;AAAA,UAAA,yCACdA,mBAAkC,OAAA;AAAA;YAArB,KAAK,QAAA;AAAA,YAAM,KAAK,QAAA;AAAA,UAAA;;;;;;;;;;;;;;;;;"}