├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── index.js ├── magic-grid.js └── magic-grid.vue └── test ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets ├── foo.jpg └── logo.png ├── components ├── card.vue ├── grid.vue └── magic-grid │ ├── magic-grid.js │ └── magic-grid.vue └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | test/node_modules 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-Magic-Grid 2 | 3 | [![GitHub forks](https://img.shields.io/github/forks/imlinus/Vue-Magic-Grid.svg)](https://github.com/imlinus/Vue-Magic-Grid/network) 4 | [![GitHub stars](https://img.shields.io/github/stars/imlinus/Vue-Magic-Grid.svg)](https://github.com/imlinus/Vue-Magic-Grid/stargazers) 5 | [![GitHub issues](https://img.shields.io/github/issues/imlinus/Vue-Magic-Grid.svg)](https://github.com/imlinus/Vue-Magic-Grid/issues) 6 | [![GitHub license](https://img.shields.io/github/license/imlinus/Vue-Magic-Grid.svg)](https://github.com/imlinus/Vue-Magic-Grid/blob/master/LICENSE) 7 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 8 | 9 | This is a Vue.js port of @[e-oj's](https://github.com/e-oj) [Magic Grid](https://github.com/e-oj/Magic-Grid). 10 | Please check the `/test` folder for a example. 11 | 12 | If you use images, make sure they have a set height, otherwise the grid will calculate weirdly. 13 | 14 | 15 | 16 | ### Setup 17 | Install & Register the component 18 | ```js 19 | $ npm i -S vue-magic-grid 20 | ``` 21 | 22 | ```js 23 | import MagicGrid from 'vue-magic-grid' 24 | 25 | Vue.use(MagicGrid) 26 | ``` 27 | 28 | ### Setup with Nuxt 29 | Create a magicgrid.js in your plugin folder 30 | ```js 31 | import Vue from 'vue' 32 | import MagicGrid from 'vue-magic-grid' 33 | 34 | Vue.use(MagicGrid) 35 | ``` 36 | 37 | Add the plugin in your nuxt.config.js file 38 | ```js 39 | plugins: [ 40 | {src: '~/plugins/magicgrid.js'} 41 | ] 42 | ``` 43 | 44 | ### Use 45 | ```html 46 | 47 | 52 | 53 | ``` 54 | 55 | ### Props 56 | | Prop | Default | Comment | 57 | |:------------|:----------|:---------------------------| 58 | | wrapper | `wrapper` | _Wrapper class_ | 59 | | gap | `32` | _Gap between elements_ | 60 | | maxCols | `5` | _Max number of colums_ | 61 | | maxColWidth | `280` | _Max width of columns_ | 62 | | animate | `false` | _Animate item positioning_ | 63 | 64 | [![js-standard-style](https://cdn.rawgit.com/standard/standard/master/badge.svg)](http://standardjs.com) 65 | 66 | Cheers, 67 | ImLinus 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-magic-grid", 3 | "version": "0.0.3", 4 | "description": "This is a Vue.js port of @e-oj 's Magic Grid", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "cd test && npm run serve" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/imlinus/Vue-Magic-Grid.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/imlinus/Vue-Magic-Grid/issues" 18 | }, 19 | "homepage": "https://github.com/imlinus/Vue-Magic-Grid#readme" 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import magicGrid from './magic-grid.vue' 2 | 3 | export default { 4 | install (Vue) { 5 | Vue.component('magic-grid', magicGrid) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/magic-grid.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | name: 'magic-grid', 4 | 5 | props: { 6 | wrapper: { 7 | type: String, // Required. Class or id of the container. 8 | default: 'wrapper' 9 | }, 10 | gap: { 11 | type: Number, // Optional. Space between items. Default: 32px 12 | default: 32 13 | }, 14 | maxCols: { 15 | type: Number, // Maximum number of colums. Default: Infinite 16 | default: 5 17 | }, 18 | maxColWidth: { 19 | type: Number, 20 | default: 280 21 | }, 22 | animate: { 23 | type: Boolean, // Animate item positioning. Default: false. 24 | default: true 25 | }, 26 | useMin: { 27 | type: Boolean, // Place items in lower column 28 | default: false 29 | } 30 | }, 31 | 32 | data () { 33 | return { 34 | started: false, 35 | items: [] 36 | } 37 | }, 38 | 39 | mounted () { 40 | this.waitUntilReady() 41 | }, 42 | 43 | methods: { 44 | waitUntilReady () { 45 | if (this.isReady()) { 46 | this.positionItems() 47 | 48 | window.addEventListener('resize', () => { 49 | setTimeout(this.positionItems(), 200) 50 | }) 51 | } else this.getReady() 52 | }, 53 | 54 | isReady () { 55 | return this.$el && this.items.length > 0 56 | }, 57 | 58 | getReady () { 59 | let interval = setInterval(() => { 60 | this.items = this.$el.children 61 | 62 | if (this.isReady()) { 63 | clearInterval(interval) 64 | this.init() 65 | } 66 | }, 100) 67 | }, 68 | 69 | init () { 70 | if (!this.isReady() || this.started) return 71 | 72 | this.$el.style.position = 'relative' 73 | 74 | Array.prototype.forEach.call(this.items, item => { 75 | item.style.position = 'absolute' 76 | item.style.maxWidth = this.maxColWidth + 'px' 77 | if (this.animate) item.style.transition = 'top, left 0.2s ease' 78 | }) 79 | 80 | this.started = true 81 | this.waitUntilReady() 82 | }, 83 | 84 | colWidth () { 85 | return this.items[0].getBoundingClientRect().width + this.gap 86 | }, 87 | 88 | setup () { 89 | let width = this.$el.getBoundingClientRect().width 90 | let numCols = Math.floor(width / this.colWidth()) || 1 91 | let cols = [] 92 | 93 | if (this.maxCols && numCols > this.maxCols) { 94 | numCols = this.maxCols 95 | } 96 | 97 | for (let i = 0; i < numCols; i++) { 98 | cols[i] = { 99 | height: 0, 100 | top: 0, 101 | index: i 102 | } 103 | } 104 | 105 | let wSpace = width - numCols * this.colWidth() + this.gap 106 | 107 | return { 108 | cols, 109 | wSpace 110 | } 111 | }, 112 | 113 | nextCol (cols, i) { 114 | if (this.useMin) return this.getMin(cols) 115 | 116 | return cols[i % cols.length] 117 | }, 118 | 119 | positionItems () { 120 | let { cols, wSpace } = this.setup() 121 | 122 | wSpace = Math.floor(wSpace / 2) 123 | 124 | Array.prototype.forEach.call(this.items, (item, i) => { 125 | let min = this.nextCol(cols, i) 126 | let left = min.index * this.colWidth() + wSpace 127 | 128 | item.style.left = left + 'px' 129 | item.style.top = min.height + min.top + 'px' 130 | 131 | min.height += min.top + item.getBoundingClientRect().height 132 | min.top = this.gap 133 | }) 134 | 135 | this.$el.style.height = this.getMax(cols).height + 'px' 136 | }, 137 | 138 | getMax (cols) { 139 | let max = cols[0] 140 | 141 | for (let col of cols) { 142 | if (col.height > max.height) max = col 143 | } 144 | 145 | return max 146 | }, 147 | 148 | getMin (cols) { 149 | let min = cols[0] 150 | 151 | for (let col of cols) { 152 | if (col.height < min.height) min = col 153 | } 154 | 155 | return min 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/magic-grid.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /test/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # test 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /test/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "vue": "^2.5.17" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-plugin-babel": "^3.1.1", 15 | "@vue/cli-plugin-eslint": "^3.1.5", 16 | "@vue/cli-service": "^3.1.4", 17 | "@vue/eslint-config-standard": "^4.0.0", 18 | "babel-eslint": "^10.0.1", 19 | "eslint": "^5.8.0", 20 | "eslint-plugin-vue": "^5.0.0-0", 21 | "vue-template-compiler": "^2.5.17" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-oj/vue-magic-grid/1a14272dc5eac3af9486db54974cec6280aaaa0b/test/public/favicon.ico -------------------------------------------------------------------------------- /test/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | test 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /test/src/assets/foo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-oj/vue-magic-grid/1a14272dc5eac3af9486db54974cec6280aaaa0b/test/src/assets/foo.jpg -------------------------------------------------------------------------------- /test/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-oj/vue-magic-grid/1a14272dc5eac3af9486db54974cec6280aaaa0b/test/src/assets/logo.png -------------------------------------------------------------------------------- /test/src/components/card.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /test/src/components/grid.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | -------------------------------------------------------------------------------- /test/src/components/magic-grid/magic-grid.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | name: 'magic-grid', 4 | 5 | props: { 6 | wrapper: { 7 | type: String, // Required. Class or id of the container. 8 | default: 'wrapper' 9 | }, 10 | gap: { 11 | type: Number, // Optional. Space between items. Default: 32px 12 | default: 32 13 | }, 14 | maxCols: { 15 | type: Number, // Maximum number of colums. Default: Infinite 16 | default: 5 17 | }, 18 | maxColWidth: { 19 | type: Number, 20 | default: 280 21 | }, 22 | animate: { 23 | type: Boolean, // Animate item positioning. Default: false. 24 | default: true 25 | } 26 | }, 27 | 28 | data () { 29 | return { 30 | started: false, 31 | items: [] 32 | } 33 | }, 34 | 35 | mounted () { 36 | this.waitUntilReady() 37 | }, 38 | 39 | methods: { 40 | waitUntilReady () { 41 | if (this.isReady()) { 42 | this.positionItems() 43 | 44 | window.addEventListener('resize', () => { 45 | setTimeout(this.positionItems(), 200) 46 | }) 47 | } else this.getReady() 48 | }, 49 | 50 | isReady () { 51 | return this.$el && this.items.length > 0 52 | }, 53 | 54 | getReady () { 55 | let interval = setInterval(() => { 56 | this.items = this.$el.children 57 | 58 | if (this.isReady()) { 59 | clearInterval(interval) 60 | this.init() 61 | } 62 | }, 100) 63 | }, 64 | 65 | init () { 66 | if (!this.isReady() || this.started) return 67 | 68 | this.$el.style.position = 'relative' 69 | 70 | Array.prototype.forEach.call(this.items, item => { 71 | item.style.position = 'absolute' 72 | item.style.maxWidth = this.maxColWidth + 'px' 73 | if (this.animate) item.style.transition = 'top, left 0.2s ease' 74 | }) 75 | 76 | this.started = true 77 | this.waitUntilReady() 78 | }, 79 | 80 | colWidth () { 81 | return this.items[0].getBoundingClientRect().width + this.gap 82 | }, 83 | 84 | setup () { 85 | let width = this.$el.getBoundingClientRect().width 86 | let numCols = Math.floor(width / this.colWidth()) || 1 87 | let cols = [] 88 | 89 | if (this.maxCols && numCols > this.maxCols) { 90 | numCols = this.maxCols 91 | } 92 | 93 | for (let i = 0; i < numCols; i++) { 94 | cols[i] = { 95 | height: 0, 96 | top: 0, 97 | index: i 98 | } 99 | } 100 | 101 | let wSpace = width - numCols * this.colWidth() + this.gap 102 | 103 | return { 104 | cols, 105 | wSpace 106 | } 107 | }, 108 | 109 | nextCol (cols, i) { 110 | if (this.useMin) return this.getMin(cols) 111 | 112 | return cols[i % cols.length] 113 | }, 114 | 115 | positionItems () { 116 | let { cols, wSpace } = this.setup() 117 | 118 | wSpace = Math.floor(wSpace / 2) 119 | 120 | Array.prototype.forEach.call(this.items, (item, i) => { 121 | let min = this.nextCol(cols, i) 122 | let left = min.index * this.colWidth() + wSpace 123 | 124 | item.style.left = left + 'px' 125 | item.style.top = min.height + min.top + 'px' 126 | 127 | min.height += min.top + item.getBoundingClientRect().height 128 | min.top = this.gap 129 | }) 130 | 131 | this.$el.style.height = this.getMax(cols).height + 'px' 132 | }, 133 | 134 | getMax (cols) { 135 | let max = cols[0] 136 | 137 | for (let col of cols) { 138 | if (col.height > max.height) max = col 139 | } 140 | 141 | return max 142 | }, 143 | 144 | getMin (cols) { 145 | let min = cols[0] 146 | 147 | for (let col of cols) { 148 | if (col.height < min.height) min = col 149 | } 150 | 151 | return min 152 | } 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /test/src/components/magic-grid/magic-grid.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount('#app') 9 | --------------------------------------------------------------------------------