├── .eslintrc.json ├── .gitignore ├── demo-preview.gif ├── example ├── test.jpg ├── main.js └── App.vue ├── src ├── index.js └── ExpandableImage.vue ├── LICENSE ├── package.json ├── README.md └── dist ├── vue-expandable-image.min.js └── vue-expandable-image.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": {} 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log -------------------------------------------------------------------------------- /demo-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TahaSh/vue-expandable-image/HEAD/demo-preview.gif -------------------------------------------------------------------------------- /example/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TahaSh/vue-expandable-image/HEAD/example/test.jpg -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | 4 | import VueExpandableImage from '../src' 5 | Vue.use(VueExpandableImage) 6 | 7 | new Vue({ 8 | render: h => h(App) 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ExpandableImage from './ExpandableImage.vue' 2 | let vueExpandableImage = {} 3 | 4 | vueExpandableImage.install = function (Vue) { 5 | Vue.component('expandable-image', ExpandableImage) 6 | } 7 | 8 | if (typeof window !== 'undefined' && window.Vue) { 9 | window.Vue.use(vueExpandableImage) 10 | } 11 | 12 | export default vueExpandableImage 13 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Taha Shashtari 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-expandable-image", 3 | "version": "0.1.0", 4 | "main": "dist/vue-expandable-image.js", 5 | "description": "A wrapper component for images to make them open in fullscreen.", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/TahaSh/vue-expandable-image" 10 | }, 11 | "bugs": "https://github.com/TahaSh/vue-expandable-image/issues", 12 | "devDependencies": { 13 | "@vue/cli": "^3.3.0", 14 | "@vue/cli-service": "^3.3.0", 15 | "rollup": "^1.1.2", 16 | "rollup-plugin-buble": "^0.19.6", 17 | "rollup-plugin-commonjs": "^9.2.0", 18 | "rollup-plugin-node-resolve": "^4.0.0", 19 | "rollup-plugin-replace": "^2.1.0", 20 | "rollup-plugin-vue": "^4.7.2", 21 | "uglify-js": "^3.4.9", 22 | "vue": "^2.5.22", 23 | "vue-template-compiler": "^2.5.22", 24 | "webpack": "^4.29.0" 25 | }, 26 | "scripts": { 27 | "build": "rollup --environment NODE_ENV:production -c build/rollup.config.js && uglifyjs dist/vue-expandable-image.js -cm --comments -o dist/vue-expandable-image.min.js", 28 | "dev": "vue-cli-service serve example/main.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-expandable-image 2 | 3 | > Allows your images to open in full size. 4 | 5 | # Demo Preview 6 | 7 | ![](./demo-preview.gif) 8 | 9 | ## Setup 10 | 11 | ``` 12 | npm install vue-expandable-image 13 | ``` 14 | 15 | You have two ways to setup `vue-expandable-image`: 16 | 17 | #### CommonJS (Webpack/Browserify) 18 | 19 | - ES6 20 | 21 | ```js 22 | import VueExpandableImage from 'vue-expandable-image' 23 | Vue.use(VueExpandableImage) 24 | ``` 25 | 26 | - ES5 27 | 28 | ```js 29 | var VueExpandableImage = require('vue-expandable-image') 30 | Vue.use(VueExpandableImage) 31 | ``` 32 | 33 | #### Include 34 | 35 | Include it directly with a ` 122 | 123 | 200 | -------------------------------------------------------------------------------- /dist/vue-expandable-image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * vue-expandable-image v0.1.0 3 | * (c) 2019 Taha Shashtari 4 | * @license MIT 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global = global || self, global.VueExpandableImage = factory()); 10 | }(this, function () { 'use strict'; 11 | 12 | // 13 | // 14 | // 15 | // 16 | // 17 | // 18 | // 19 | // 20 | // 21 | // 22 | // 23 | // 24 | // 25 | // 26 | // 27 | // 28 | // 29 | // 30 | // 31 | // 32 | // 33 | // 34 | // 35 | // 36 | // 37 | // 38 | // 39 | // 40 | 41 | var script = { 42 | props: { 43 | closeOnBackgroundClick: { 44 | type: Boolean, 45 | default: false 46 | } 47 | }, 48 | 49 | data: function data () { 50 | return { 51 | expanded: false, 52 | closeButtonRef: null 53 | } 54 | }, 55 | 56 | methods: { 57 | closeImage: function closeImage (event) { 58 | this.expanded = false; 59 | event.stopPropagation(); 60 | }, 61 | 62 | freezeVp: function freezeVp (e) { 63 | e.preventDefault(); 64 | }, 65 | 66 | onExpandedImageClick: function onExpandedImageClick (e) { 67 | e.stopPropagation(); 68 | var image = this.cloned.querySelector('img'); 69 | var imagePosition = this.getRenderedSize(image.width, image.height, image.naturalWidth, image.naturalHeight); 70 | if ( 71 | (e.clientX < imagePosition.left) || 72 | (e.clientX > imagePosition.right) || 73 | (e.clientY < imagePosition.top) || 74 | (e.clientY > imagePosition.bottom) 75 | ) { 76 | this.expanded = false; 77 | } 78 | }, 79 | 80 | getRenderedSize: function getRenderedSize (cWidth, cHeight, oWidth, oHeight) { 81 | var oRatio = oWidth > oHeight 82 | ? oWidth / oHeight 83 | : oHeight / oWidth; 84 | var width = oWidth >= oHeight 85 | ? oRatio * cHeight 86 | : cWidth; 87 | var height = oHeight > oWidth 88 | ? oRatio * cWidth 89 | : cHeight; 90 | var left = (this.cloned.clientWidth - width) / 2; 91 | var right = left + width; 92 | var top = (this.cloned.clientHeight - height) / 2; 93 | var bottom = top + height; 94 | return { left: left, top: top, right: right, bottom: bottom } 95 | } 96 | }, 97 | 98 | watch: { 99 | expanded: function expanded (status) { 100 | var this$1 = this; 101 | 102 | this.$nextTick(function () { 103 | if (status) { 104 | this$1.cloned = this$1.$el.cloneNode(true); 105 | this$1.closeButtonRef = this$1.cloned.querySelector('.close-button'); 106 | this$1.closeButtonRef.addEventListener('click', this$1.closeImage); 107 | document.body.appendChild(this$1.cloned); 108 | document.body.style.overflow = 'hidden'; 109 | this$1.cloned.addEventListener('touchmove', this$1.freezeVp, false); 110 | if (this$1.closeOnBackgroundClick) { 111 | this$1.cloned.addEventListener('click', this$1.onExpandedImageClick); 112 | } 113 | setTimeout(function () { 114 | this$1.cloned.style.opacity = 1; 115 | }, 0); 116 | } else { 117 | this$1.cloned.style.opacity = 0; 118 | this$1.cloned.removeEventListener('touchmove', this$1.freezeVp, false); 119 | if (this$1.closeOnBackgroundClick) { 120 | this$1.cloned.removeEventListener('click', this$1.onExpandedImageClick); 121 | } 122 | setTimeout(function () { 123 | this$1.closeButtonRef.removeEventListener('click', this$1.closeImage); 124 | this$1.cloned.remove(); 125 | this$1.cloned = null; 126 | this$1.closeButtonRef = null; 127 | document.body.style.overflow = 'auto'; 128 | }, 250); 129 | } 130 | }); 131 | } 132 | } 133 | }; 134 | 135 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier 136 | /* server only */ 137 | , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { 138 | if (typeof shadowMode !== 'boolean') { 139 | createInjectorSSR = createInjector; 140 | createInjector = shadowMode; 141 | shadowMode = false; 142 | } // Vue.extend constructor export interop. 143 | 144 | 145 | var options = typeof script === 'function' ? script.options : script; // render functions 146 | 147 | if (template && template.render) { 148 | options.render = template.render; 149 | options.staticRenderFns = template.staticRenderFns; 150 | options._compiled = true; // functional template 151 | 152 | if (isFunctionalTemplate) { 153 | options.functional = true; 154 | } 155 | } // scopedId 156 | 157 | 158 | if (scopeId) { 159 | options._scopeId = scopeId; 160 | } 161 | 162 | var hook; 163 | 164 | if (moduleIdentifier) { 165 | // server build 166 | hook = function hook(context) { 167 | // 2.3 injection 168 | context = context || // cached call 169 | this.$vnode && this.$vnode.ssrContext || // stateful 170 | this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional 171 | // 2.2 with runInNewContext: true 172 | 173 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 174 | context = __VUE_SSR_CONTEXT__; 175 | } // inject component styles 176 | 177 | 178 | if (style) { 179 | style.call(this, createInjectorSSR(context)); 180 | } // register component module identifier for async chunk inference 181 | 182 | 183 | if (context && context._registeredComponents) { 184 | context._registeredComponents.add(moduleIdentifier); 185 | } 186 | }; // used by ssr in case component is cached and beforeCreate 187 | // never gets called 188 | 189 | 190 | options._ssrRegister = hook; 191 | } else if (style) { 192 | hook = shadowMode ? function () { 193 | style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); 194 | } : function (context) { 195 | style.call(this, createInjector(context)); 196 | }; 197 | } 198 | 199 | if (hook) { 200 | if (options.functional) { 201 | // register for functional component in vue file 202 | var originalRender = options.render; 203 | 204 | options.render = function renderWithStyleInjection(h, context) { 205 | hook.call(context); 206 | return originalRender(h, context); 207 | }; 208 | } else { 209 | // inject component registration as beforeCreate hook 210 | var existing = options.beforeCreate; 211 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; 212 | } 213 | } 214 | 215 | return script; 216 | } 217 | 218 | var normalizeComponent_1 = normalizeComponent; 219 | 220 | var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\\b/.test(navigator.userAgent.toLowerCase()); 221 | function createInjector(context) { 222 | return function (id, style) { 223 | return addStyle(id, style); 224 | }; 225 | } 226 | var HEAD = document.head || document.getElementsByTagName('head')[0]; 227 | var styles = {}; 228 | 229 | function addStyle(id, css) { 230 | var group = isOldIE ? css.media || 'default' : id; 231 | var style = styles[group] || (styles[group] = { 232 | ids: new Set(), 233 | styles: [] 234 | }); 235 | 236 | if (!style.ids.has(id)) { 237 | style.ids.add(id); 238 | var code = css.source; 239 | 240 | if (css.map) { 241 | // https://developer.chrome.com/devtools/docs/javascript-debugging 242 | // this makes source maps inside style tags work properly in Chrome 243 | code += '\n/*# sourceURL=' + css.map.sources[0] + ' */'; // http://stackoverflow.com/a/26603875 244 | 245 | code += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) + ' */'; 246 | } 247 | 248 | if (!style.element) { 249 | style.element = document.createElement('style'); 250 | style.element.type = 'text/css'; 251 | if (css.media) { style.element.setAttribute('media', css.media); } 252 | HEAD.appendChild(style.element); 253 | } 254 | 255 | if ('styleSheet' in style.element) { 256 | style.styles.push(code); 257 | style.element.styleSheet.cssText = style.styles.filter(Boolean).join('\n'); 258 | } else { 259 | var index = style.ids.size - 1; 260 | var textNode = document.createTextNode(code); 261 | var nodes = style.element.childNodes; 262 | if (nodes[index]) { style.element.removeChild(nodes[index]); } 263 | if (nodes.length) { style.element.insertBefore(textNode, nodes[index]); }else { style.element.appendChild(textNode); } 264 | } 265 | } 266 | } 267 | 268 | var browser = createInjector; 269 | 270 | /* script */ 271 | var __vue_script__ = script; 272 | 273 | /* template */ 274 | var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"expandable-image",class:{ 275 | expanded: _vm.expanded 276 | },on:{"click":function($event){_vm.expanded = true;}}},[(_vm.expanded)?_c('i',{staticClass:"close-button"},[_c('svg',{staticStyle:{"width":"24px","height":"24px"},attrs:{"viewBox":"0 0 24 24"}},[_c('path',{attrs:{"fill":"#666666","d":"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"}})])]):_vm._e(),_vm._v(" "),(!_vm.expanded)?_c('i',{staticClass:"expand-button"},[_c('svg',{staticStyle:{"width":"24px","height":"24px"},attrs:{"viewBox":"0 0 24 24"}},[_c('path',{attrs:{"fill":"#000000","d":"M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z"}})])]):_vm._e(),_vm._v(" "),_c('img',_vm._b({},'img',_vm.$attrs,false))])}; 277 | var __vue_staticRenderFns__ = []; 278 | 279 | /* style */ 280 | var __vue_inject_styles__ = function (inject) { 281 | if (!inject) { return } 282 | inject("data-v-3c2a268c_0", { source: ".expandable-image{position:relative;transition:.25s opacity;cursor:zoom-in}body>.expandable-image.expanded{position:fixed;z-index:999999;top:0;left:0;width:100%;height:100%;background:#000;display:flex;align-items:center;opacity:0;padding-bottom:0!important;cursor:default}body>.expandable-image.expanded>img{width:100%;max-width:1200px;max-height:100%;object-fit:contain;margin:0 auto}body>.expandable-image.expanded>.close-button{display:block}.close-button{position:fixed;top:10px;right:10px;display:none;cursor:pointer}.close-button svg,.expand-button svg{filter:drop-shadow(1px 1px 1px rgba(0, 0, 0, .5))}.close-button svg path,.expand-button svg path{fill:#fff}.expand-button{position:absolute;z-index:999;right:10px;top:10px;padding:0;align-items:center;justify-content:center;padding:3px;opacity:0;transition:.2s opacity}.expandable-image:hover .expand-button{opacity:1}.expand-button svg{width:20px;height:20px}.expand-button path{fill:#fff}.expandable-image img{width:100%}", map: undefined, media: undefined }); 283 | 284 | }; 285 | /* scoped */ 286 | var __vue_scope_id__ = undefined; 287 | /* module identifier */ 288 | var __vue_module_identifier__ = undefined; 289 | /* functional template */ 290 | var __vue_is_functional_template__ = false; 291 | /* style inject SSR */ 292 | 293 | 294 | 295 | var ExpandableImage = normalizeComponent_1( 296 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, 297 | __vue_inject_styles__, 298 | __vue_script__, 299 | __vue_scope_id__, 300 | __vue_is_functional_template__, 301 | __vue_module_identifier__, 302 | browser, 303 | undefined 304 | ); 305 | 306 | var vueExpandableImage = {}; 307 | 308 | vueExpandableImage.install = function (Vue) { 309 | Vue.component('expandable-image', ExpandableImage); 310 | }; 311 | 312 | if (typeof window !== 'undefined' && window.Vue) { 313 | window.Vue.use(vueExpandableImage); 314 | } 315 | 316 | return vueExpandableImage; 317 | 318 | })); 319 | --------------------------------------------------------------------------------