├── dist
└── .gitkeep
├── .gitignore
├── demo
├── webpack.config.js
├── index.js
└── index.html
├── LICENSE
├── package.json
├── README.md
└── src
└── index.js
/dist/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/*.js
3 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 |
3 | module.exports = {
4 | entry: './index.js',
5 | devtool: 'inline-source-map',
6 | module: {
7 | loaders: [
8 | { test: /\.js$/, loader: 'buble-loader', exclude: /node_modules/ }
9 | ]
10 | },
11 | output: {
12 | filename: './demo-bundle.js'
13 | },
14 | resolve: {
15 | alias: {
16 | vue: 'vue/dist/vue.js'
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Paul Collett
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueMasonry from '../src/index';
3 | import VueDummy from 'vue-dummy';
4 | import DummyJS from 'dummyjs';
5 |
6 | Vue.use(VueMasonry);
7 | Vue.use(VueDummy);
8 |
9 | const randomColsOptions = [
10 | { default: 4, 1000: 3, 700: 2, 400: 1 },
11 | { default: 3, 900: 2, 600: 1 },
12 | { default: 5, 1000: 4, 800: 3, 500: 2, 400: 1 }
13 | ];
14 |
15 | // Define a new component called button-counter
16 | Vue.component('dynamically-generated-masonry', {
17 | methods: {
18 | onResetClick() {
19 | this.$forceUpdate();
20 | }
21 | },
22 | render: function (createElement) {
23 | const childElements = Array(10).fill().map((v, i) => createElement(
24 | 'div',
25 | {class: 'item'},
26 | `Item: #${i}: ` + DummyJS.text()
27 | ));
28 |
29 | const resetButton = createElement('button', {
30 | style: {
31 | marginBottom: '10px'
32 | },
33 | on: {
34 | click: this.onResetClick
35 | }
36 | }, 'Rebuild Dynamic Layout');
37 |
38 | const colOptions = randomColsOptions[Math.floor(Math.random()*randomColsOptions.length)];
39 |
40 | const masonry = createElement(
41 | 'masonry',
42 | {
43 | props: {
44 | cols: colOptions,
45 | gutter: { default: '30px', 700: '20px' }
46 | }
47 | },
48 | childElements
49 | );
50 |
51 | return createElement('div', {}, [resetButton, masonry]);
52 | },
53 | })
54 |
55 | new Vue({
56 | el: '#app'
57 | });
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-masonry-css",
3 | "version": "1.0.3",
4 | "description": "Vue Masonry component powered by CSS, dependancy free",
5 | "main": "dist/vue-masonry.common.js",
6 | "module": "dist/vue-masonry.es2015.js",
7 | "unpkg": "dist/vue-masonry.min.js",
8 | "jsdelivr": "dist/vue-masonry.min.js",
9 | "scripts": {
10 | "build": "node ./build.js && npm run build:demo",
11 | "build:demo": "cd ./demo && webpack",
12 | "dev": "cd ./demo && webpack-dev-server --progress --colors",
13 | "preversion": "npm run build",
14 | "prepublishOnly": "npm run build",
15 | "publish-package": "git push && git push --tags && npm publish --registry=https://registry.npmjs.org/"
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "keywords": [
21 | "vue",
22 | "vue.js",
23 | "masonry",
24 | "masonary",
25 | "component",
26 | "css"
27 | ],
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/paulcollett/vue-masonry-css.git"
31 | },
32 | "author": "Paul Collett",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/paulcollett/vue-masonry-css/issues"
36 | },
37 | "homepage": "https://github.com/paulcollett/vue-masonry-css#readme",
38 | "devDependencies": {
39 | "buble-loader": "^0.4.1",
40 | "rollup": "^0.50.0",
41 | "rollup-plugin-buble": "^0.16.0",
42 | "uglify-js": "^3.1.3",
43 | "vue": "^2.5.16",
44 | "vue-dummy": "^1.1.1",
45 | "webpack": "^3.6.0",
46 | "webpack-dev-server": "^2.9.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vue Masonry Css Example
6 |
7 |
8 |
25 |
26 |
27 |
28 | ← GitHub / Docs
29 | vue-masonry-css example
30 | A Vue masonry component powered by CSS to rendered fast, responsive and to be free of jQuery or other dependencies. Build specifically for Vue projects.
31 |
32 |
33 |
37 |
38 |
Item: {{index + 1}}
39 |
![]()
45 |
46 |
47 |
48 |
49 |
Dynamic Masonry Layout showing re-rendering:
50 |
51 |
52 |
Example with static HTML (see source):
53 |
57 | My Element 1
58 | My Element 2
59 | My Element 3
With another line
60 | My Element 4
61 | My Element 5
62 | My Element 6
63 | My Element 7
64 | My Element 8
65 |
66 |
67 |
Example with a mix of static HTML and dynamically rendered (see source):
68 |
Also, a smaller gutter size
69 |
73 | My Element 1
74 | My Element 2
75 | Item: {{index + 1 + 2}}
76 | My Element 6
77 | My Element 7
78 | My Element 8
79 |
80 |
81 |
Placeholder text and images rendered with Vue-Dummy
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A new masonry component powered by CSS to be fast loading and free of jQuery or other dependencies. Build specifically for Vue.js projects.
2 |
3 | 
4 |
5 | ### 😎 Why?
6 |
7 | Existing solutions like Vue wrapped DeSandro Masonry, while popular, don't actually leverage Vue's highly optimized Virtual DOM renderer and in DeSandro Masonry's case, actually renders elements twice before showing the layout. All of this is ok but we found it to lead to a slow, "laggy" user experience that would occasionally miss-render our layout.
8 |
9 | Our need for a simple Masonry layout that was fast, used Vue's Virtual DOM without needing jQuery or other dependencies led us to explore what we could do with the latest techniques using just CSS within a React Component.
10 |
11 | Between flexbox, css columns, css grid we settled on plain ol' div's and a dab of flexbox that allows for "fluid" responsive layouts by default but most importantly is true to Vue's rendering lifecycle.
12 |
13 | *`vue-masonry-css`* Is a Vue Component with a simple interface to order items into the desired columns at specified breakpoints. With minimal CSS this leads to a quick, reliable solution that also has great browser support along with fast rendering performance ..just as Vue.js intended.
14 |
15 | 😄 What does this do
16 | - Responsive! ..always
17 | - IE 10+ CSS Support (and, IE9)
18 | - No Dependencies - Which means no need for jQuery!
19 | - Works with existing CSS animations on your elements, like fading in on first load
20 | - CSS powered (Faster to render)
21 | - Allows for Gaps (Gutters) between elements
22 |
23 | 🏳️ What doesn't this do
24 | - Works with elements with different widths
25 | - Sorting based on height - This kills performance, so if you don't need it we're here for you
26 |
27 | ### 😲 Simple Usage
28 |
29 | Add `vue-masonry-css` to your project:
30 |
31 | By script..
32 |
33 | ```HTML
34 |
35 |
36 | ```
37 |
38 | Or as a module... `npm install vue-masonry-css --save-dev`
39 |
40 | ```JS
41 | import Vue from 'vue'
42 | import VueMasonry from 'vue-masonry-css'
43 |
44 | Vue.use(VueMasonry);
45 | ```
46 |
47 | In your HTML template...
48 | ```HTML
49 |
53 | Item: {{index + 1}}
54 |
55 | ```
56 |
57 | ### Resposive Breakpoints
58 |
59 | Different columns and gutter sizes can be specified by passing an object containing key's of the window widths and their values representing the number of columns or gutter size. To have a fallback value, use the `default` key.
60 |
61 | _note:_ The `cols=` attribute needs to use Vues bind method to evaluate objects. Instead of `cols=""` use either `v-bind:cols="{ 700: 3 }"` or the shorthand `:cols="{ 700: 3 }"`
62 |
63 | ```HTML
64 |
68 | Item: {{index + 1}}
69 |
70 | ```
71 |
72 | In the above example, the number of columns will default to 4. When the window's is between 1000px and 700px, the number of columns will be 3. The key represents the `max-width` of the window, and `vue-masonry-css` will use the largest key that satisfies this.
73 |
74 | ### Example
75 |
76 | https://paulcollett.github.io/vue-masonry-css/demo/
77 |
78 | ### Suggestions & Issues
79 | https://github.com/paulcollett/vue-masonry-css
80 |
81 | **Contact me direct:**
82 | * https://github.com/paulcollett
83 | * http://paulcollett.com
84 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // the component name ``
2 | // can be overridden with `Vue.use(Masonry, { name: 'the-masonry' });`
3 | const componentName = 'masonry';
4 |
5 | const props = {
6 | tag: {
7 | type: [String],
8 | default: 'div'
9 | },
10 | cols: {
11 | type: [Object, Number, String],
12 | default: 2
13 | },
14 | gutter: {
15 | type: [Object, Number, String],
16 | default: 0
17 | },
18 | css: {
19 | type: [Boolean],
20 | default: true
21 | },
22 | columnTag: {
23 | type: [String],
24 | default: 'div'
25 | },
26 | columnClass: {
27 | type: [String, Array, Object],
28 | default: () => []
29 | },
30 | columnAttr: {
31 | type: [Object],
32 | default: () => ({})
33 | }
34 | };
35 |
36 | // Get the resulting value from `:col=` prop
37 | // based on the window width
38 | const breakpointValue = (mixed, windowWidth) => {
39 | const valueAsNum = parseInt(mixed);
40 |
41 | if(valueAsNum > -1) {
42 | return mixed;
43 | }else if(typeof mixed !== 'object') {
44 | return 0;
45 | }
46 |
47 | let matchedBreakpoint = Infinity;
48 | let matchedValue = mixed.default || 0;
49 |
50 | for(let k in mixed) {
51 | const breakpoint = parseInt(k);
52 | const breakpointValRaw = mixed[breakpoint];
53 | const breakpointVal = parseInt(breakpointValRaw);
54 |
55 | if(isNaN(breakpoint) || isNaN(breakpointVal)) {
56 | continue;
57 | }
58 |
59 | const isNewBreakpoint = windowWidth <= breakpoint && breakpoint < matchedBreakpoint;
60 |
61 | if(isNewBreakpoint) {
62 | matchedBreakpoint = breakpoint;
63 | matchedValue = breakpointValRaw;
64 | }
65 | }
66 |
67 | return matchedValue;
68 | }
69 |
70 | const component = {
71 | props,
72 |
73 | data() {
74 | return {
75 | displayColumns: 2,
76 | displayGutter: 0
77 | }
78 | },
79 |
80 | mounted() {
81 | this.$nextTick(() => {
82 | this.reCalculate();
83 | });
84 |
85 | // Bind resize handler to page
86 | if(window) {
87 | window.addEventListener('resize', this.reCalculate);
88 | }
89 | },
90 |
91 | updated() {
92 | this.$nextTick(() => {
93 | this.reCalculate();
94 | });
95 | },
96 |
97 | beforeDestroy() {
98 | if(window) {
99 | window.removeEventListener('resize', this.reCalculate);
100 | }
101 | },
102 |
103 | methods: {
104 | // Recalculate how many columns to display based on window width
105 | // and the value of the passed `:cols=` prop
106 | reCalculate() {
107 | const previousWindowWidth = this.windowWidth;
108 |
109 | this.windowWidth = (window ? window.innerWidth : null) || Infinity;
110 |
111 | // Window resize events get triggered on page height
112 | // change which when loading the page can result in multiple
113 | // needless calculations. We prevent this here.
114 | if(previousWindowWidth === this.windowWidth) {
115 | return;
116 | }
117 |
118 | this._reCalculateColumnCount(this.windowWidth);
119 |
120 | this._reCalculateGutterSize(this.windowWidth);
121 | },
122 |
123 | _reCalculateGutterSize(windowWidth) {
124 | this.displayGutter = breakpointValue(this.gutter, windowWidth);
125 | },
126 |
127 | _reCalculateColumnCount(windowWidth) {
128 | let newColumns = breakpointValue(this.cols, windowWidth);
129 |
130 | // Make sure we can return a valid value
131 | newColumns = Math.max(1, Number(newColumns) || 0);
132 |
133 | this.displayColumns = newColumns;
134 | },
135 |
136 | _getChildItemsInColumnsArray() {
137 | const columns = [];
138 | let childItems = this.$slots.default || [];
139 |
140 | // This component does not work with a child ..yet,
141 | // so for now we think it may be helpful to ignore until we can find a way for support
142 | if(childItems.length === 1 && childItems[0].componentOptions && childItems[0].componentOptions.tag == 'transition-group') {
143 | childItems = childItems[0].componentOptions.children;
144 | }
145 |
146 | // Loop through child elements
147 | for (let i = 0, visibleItemI = 0; i < childItems.length; i++, visibleItemI++) {
148 | // skip Vue elements without tags, which includes
149 | // whitespace elements and also plain text
150 | if(!childItems[i].tag) {
151 | visibleItemI--;
152 |
153 | continue;
154 | }
155 |
156 | // Get the column index the child item will end up in
157 | const columnIndex = visibleItemI % this.displayColumns;
158 |
159 | if(!columns[columnIndex]) {
160 | columns[columnIndex] = [];
161 | }
162 |
163 | columns[columnIndex].push(childItems[i]);
164 | }
165 |
166 | return columns;
167 | }
168 | },
169 |
170 | render(createElement) {
171 | const columnsContainingChildren = this._getChildItemsInColumnsArray();
172 | const isGutterSizeUnitless = parseInt(this.displayGutter) === this.displayGutter * 1;
173 | const gutterSizeWithUnit = isGutterSizeUnitless ? `${this.displayGutter}px` : this.displayGutter;
174 |
175 | const columnStyle = {
176 | boxSizing: 'border-box',
177 | backgroundClip: 'padding-box',
178 | width: `${100 / this.displayColumns}%`,
179 | border: '0 solid transparent',
180 | borderLeftWidth: gutterSizeWithUnit
181 | };
182 |
183 | const columns = columnsContainingChildren.map((children, index) => {
184 | /// Create column element and inject the children
185 | return createElement(this.columnTag, {
186 | key: index + '-' + columnsContainingChildren.length,
187 | style: this.css ? columnStyle : null,
188 | class: this.columnClass,
189 | attrs: this.columnAttr
190 | }, children); // specify child items here
191 | });
192 |
193 | const containerStyle = {
194 | display: ['-webkit-box', '-ms-flexbox', 'flex'],
195 | marginLeft: `-${gutterSizeWithUnit}`
196 | };
197 |
198 | // Return wrapper with columns
199 | return createElement(
200 | this.tag, // tag name
201 | this.css ? { style: containerStyle } : null, // element options
202 | columns // column vue elements
203 | );
204 | }
205 | };
206 |
207 | const Plugin = function () {}
208 |
209 | Plugin.install = function (Vue, options) {
210 | if (Plugin.installed) {
211 | return;
212 | }
213 |
214 | if(options && options.name) {
215 | Vue.component(options.name, component);
216 | } else {
217 | Vue.component(componentName, component);
218 | }
219 | }
220 |
221 | if (typeof window !== 'undefined' && window.Vue) {
222 | window.Vue.use(Plugin);
223 | }
224 |
225 | export default Plugin;
226 |
--------------------------------------------------------------------------------