Commit b4f69d49 authored by Marc Mautz's avatar Marc Mautz

initial commit

parents
node_modules
\ No newline at end of file
The MIT License (MIT)
Copyright (c) 2016 diemeisterei GmbH, Stuttgart
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
# vue-shufflejs-plugin
A [Vue.js](https://vuejs.org) plugin for [Shuffle.js](https://vestride.github.io/Shuffle/) library.
**Note**: This project is still under development!
The plugin use and require the following packages:
* [Bootstrap 3](http://getbootstrap.com) - Grid system
* [vue-multiselect](https://github.com/shentao/vue-multiselect) - Probably the most complete selecting solution for Vue.js 2.0, without jQuery.
* [desandro/imagesloaded](http://imagesloaded.desandro.com) - Detect when images have been loaded.
## Documentation
- [Shuffle.js Docs](https://vestride.github.io/Shuffle/)
## Installation
The preferred way to install this extension is through [yarn](https://yarnpkg.com) or [npm](https://www.npmjs.com).
Either run
`yarn add vue-shufflejs-plugin`
or via npm
`npm install vue-shufflejs-plugin --save`
## Usage
The following types are supported:
- [Basic masonry layout](https://vestride.github.io/Shuffle/basic) without any extra options
- Grid with [filter, search and / or sort options](https://vestride.github.io/Shuffle/)
- ...
The following example renders the [Shuffle.js](https://vestride.github.io/Shuffle/) plugin with Filter, Search and Sort `options`. Please check the [Shuffle.js](https://vestride.github.io/Shuffle/) documentation for available `options` supported by the plugin.
Import plugin
import Vue from 'vue'
import VueShuffleJsPlugin from vue-shufflejs-plugin'
Use Component
<vue-shuffle :product-data="data" :product-option="option" :product-filter="filter"></vue-shuffle>
## Tests
WIP!
## Devlopment
WIP!
``` bash
# serve with hot reload at localhost:8080
yarn run dev
# distribution build with minification
yarn run bundle
# build the documentation into docs
yarn run docs
# run unit tests
yarn run test
# run unit tests watch
yarn run unit-watch
```
For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
### Further Information
[Shuffle.js](https://vestride.github.io/Shuffle/) has lots of configuration options, please check the [Shuffle.js](https://vestride.github.io/Shuffle/) library website.
## Credits
* [Glen Cheney](https://github.com/Vestride) - [Shuffle.js](https://vestride.github.io/Shuffle/)
* [Damian Dulisz](https://github.com/shentao/vue-multiselect) - [vue-multiselect](https://github.com/shentao/vue-multiselect)
* [David DeSandro](https://github.com/desandro) - [desandro/imagesloaded](http://imagesloaded.desandro.com)
## License
The MIT License (MIT). Please see [License File](./LICENSE.md) for more information.
<template>
<div class="loading-indicator"></div>
</template>
<script>
export default {
name: 'LoadingIndicator'
}
</script>
<style>
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@-moz-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-indicator {
position: absolute;
left: 50%;
top: 50%;
margin: -15px 0 0 -15px;
border: 2px solid #999;
font-size: 40px;
width: 30px;
height: 30px;
border-radius: .5em;
border-top-color: #c80148;
display: inline-block;
box-sizing: border-box;
animation: spin 1s linear infinite;
}
</style>
\ No newline at end of file
<template>
<div class="shuffle-view">
<section class="container">
<vue-shuffle-navbar :filter="filterOptions" :search="false" :sort="false" @filterChange="filterChange"></vue-shuffle-navbar>
<vue-shuffle-grid :product-data="productData" :product-option="productOption" :product-filter="productFilter"></vue-shuffle-grid>
</section>
</div>
</template>
<script>
import Shuffle from 'shufflejs'
import imagesLoaded from 'imagesloaded'
export default {
name: 'VueShuffle',
props: [
'productData',
'productOption',
'productFilter'
],
data () {
return {
filters: {}
}
},
created () {},
mounted () {},
updated () {
if (!this.shuffle && document.querySelectorAll('.shuffle-item').length) {
imagesLoaded(document.querySelectorAll('.img-responsive'))
.on('always', (event) => {
// console.log('all images loaded', event)
this.initShuffle()
})
.on('done', (event) => {
// console.log('all images successfully loaded', event)
})
.on('fail', (event) => {
// console.log('all images loaded, at least one is broken', event)
})
.on('progress', (instance, image) => {
// const result = image.isLoaded ? 'loaded' : 'broken'
// console.log('image is ' + result + ' for ' + image.img.src)
image.img.previousElementSibling.remove()
image.img.classList.add('fade-in')
})
}
},
computed: {
/**
* Compound filter options
* @return {Object|boolean}
*/
filterOptions () {
let opts = {}
if (this.productFilter && this.productOption) {
this.productFilter.forEach((value) => {
const key = this.$options.filters.sanitize(value.name)
opts[key] = {
label: value.name,
options: this.productOption.filter((option) => {
return (option.product_filter_id === value.id) ? option : false
})
}
})
return opts
}
return false
}
},
methods: {
/**
* Initialize shuffle instance
*/
initShuffle () {
const element = document.querySelector('.shuffle-grid')
this.shuffle = new Shuffle(element, {
easing: 'cubic-bezier(0.165, 0.840, 0.440, 1.000)', // easeOutQuart
itemSelector: '.shuffle-item',
sizer: '.sizer-element'
// filterMode: Shuffle.FilterMode.ANY // When using an array with filter(), the element passes the test if any of its groups are in the array. With "all", the element only passes if all groups are in the array.
// group: Shuffle.ALL_ITEMS // Initial filter group.
})
},
/**
* Get shuffle item filter attributes
* @param item
* @returns {Object}
*/
getItemFilterAttributes (item) {
let filters = {}
this.productFilter.forEach((value) => {
const key = this.$options.filters.sanitize(value.name)
filters[key] = []
filters[key] = this.productOption.filter((option) => {
return (option.product_filter_id === value.id && item.groups.indexOf(option.name) !== -1) ? option : false
})
})
return filters
},
/**
* Filter change handler
* @param {Object} filters Change event object
*/
filterChange (filters) {
// console.log('filterChange', filters)
this.filters = filters
this.filter()
},
/**
* Filter shuffle based on the current state of filters
*/
filter () {
if (this.hasActiveFilters()) {
this.shuffle.filter(this.itemPassesFilters)
} else {
this.shuffle.filter(Shuffle.ALL_ITEMS)
}
},
/**
* If any of the arrays in the `filters` property have a length of more than zero, that means there is an active filter
* @return {boolean}
*/
hasActiveFilters () {
return Object.keys(this.filters).some((key) => {
return this.filters[key].length > 0
})
},
/**
* Determine whether an element passes the current filters
* @param {Element} element Element to test
* @return {boolean} Whether it satisfies all current filters
*/
itemPassesFilters (element) {
for (let key in this.filters) {
if (!this.filters.hasOwnProperty(key)) continue
const filters = element.getAttribute('data-' + key)
let obj = this.filters[key]
for (let prop in obj) {
if (!obj.hasOwnProperty(prop)) continue
// If there are active filters and this label is not in that array
if (!filters.includes(obj[prop].label)) {
return false
}
}
}
return true
}
}
}
</script>
<style lang="scss" type="text/scss">
</style>
\ No newline at end of file
<template>
<div class="row">
<div class="shuffle-grid">
<div class="col-xs-6 col-sm-6 col-md-4 shuffle-item"
v-for="item in productData" :key="item.id"
v-shuffle-item-attributes="{ attributes: getItemFilterAttributes(item) }">
<figure class="shuffle-item-inner">
<figcaption>
<p>{{ item.heading }} - {{ item.groups }}</p>
</figcaption>
<div class="figure-img-wrap">
<loading-indicator v-if="item.image_url"></loading-indicator>
<img class="img-responsive" v-if="item.image_url" :src="'http://manufacturer-blueprint.workbench.oneba.se/filefly/api?action=stream&path=' + item.image_url" :title="item.heading +' - '+ item.groups">
</div>
</figure>
</div>
<div class="col-xs-1 sizer-element"></div>
</div>
</div>
</template>
<script>
export default {
name: 'VueShuffleGrid',
props: [
'productData',
'productOption',
'productFilter'
],
data () {
return {}
},
created () {},
mounted () {},
methods: {
/**
* Get shuffle item filter attributes
* @param item
* @returns {Object}
*/
getItemFilterAttributes (item) {
let filters = {}
this.productFilter.forEach((value) => {
const key = this.$options.filters.sanitize(value.name)
filters[key] = []
filters[key] = this.productOption.filter((option) => {
return (option.product_filter_id === value.id && item.groups.indexOf(option.name) !== -1) ? option : false
})
})
return filters
}
}
}
</script>
<style lang="scss" type="text/scss">
.shuffle-item {
padding-right: 10px;
padding-left: 10px;
.shuffle-item-inner {
height: 250px;
margin: 0 0 15px 0;
padding: 0;
figcaption {
position: absolute;
z-index: 1;
}
.figure-img-wrap {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
img {
height: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.5, 1.5);
opacity: 0;
}
}
}
}
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@-moz-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn ease-in 1;
animation-fill-mode: forwards;
animation-duration: 0.4s;
}
</style>
\ No newline at end of file
<template>
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#filter-navbar-collapse" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div class="collapse navbar-collapse" id="filter-navbar-collapse">
<ul class="nav navbar-nav" v-if="filter">
<li v-for="(item, index) in filter" :key="item.id" class="select-wrap col-sm-4">
<multiselect
:id="index"
:value="selected[index]"
:options="getOptions(item.options)"
:multiple="true"
:hideSelected="true"
:close-on-select="true"
@input="filterChange"
:track-by="item.id"
:placeholder="'Filter by ' + item.label"
label="label"
selectLabel=""
:searchable="true"
:block-keys="['Tab', 'Enter']">
</multiselect>
</li>
</ul>
<!--<form class="navbar-form navbar-left">
<div class=" form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right"></ul>-->
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'VueShuffleNavbar',
props: [
'filter',
'search',
'sort'
],
data () {
return {
selected: [],
filtered: []
}
},
methods: {
/**
* Get filter options
* @param options
*/
getOptions (options) {
let opts = []
options.forEach((option) => {
opts.push({value: option.id, label: option.name})
})
return opts
},
/**
* Filter change handler
* @param value
* @param id
*/
filterChange (value, id) {
// console.log(id, value)
// console.log(value.map(a => a.label))
// this.filtered[id] = value.map(a => a.label)
this.filtered[id] = value
this.$emit('filterChange', this.filtered)
}
}
}
</script>
<style lang="scss" type="text/scss">
.navbar {
&.navbar-default {
border: none;
box-shadow: none;
background-image: none;
background-color: transparent;
.container-fluid {
padding-right: 0;
padding-left: 0;
.navbar-collapse {
padding-right: 0;
padding-left: 0;
.nav {
&.navbar-nav {
width: 100%;
li {
padding-right: 5px;
padding-left: 5px;
}
}
}
}
}
}
}
</style>
import Vue from 'vue'
/**
* Shuffle item attributes directive
* Set shuffle specific data attributes for filtering, sorting or searching
*
* Usage:
* const attr = {
* color: [
* {id: 1, value: 'red'},
* {id: 2, value: 'green'}
* ]
* size: {
* {id: 1, value: 'large'},
* {id: 2, value: 'small'}
* }
* }
*
* <div v-shuffle-item-attributes="{ attributes: attr }"></div>
*
* Result:
* <div data-color="["red","green"]" data-size="["large","small"]"></div>
*/
const ShuffleItemAttributes = {
bind (el, binding, vnode, oldVnode) {
const attributes = binding.value.attributes
// console.log(attributes)
// el.setAttribute('data-groups', JSON.stringify(attributes))
for (let prop in attributes) {
let attr = []
attributes[prop].forEach((attribute) => {
attr.push(attribute.name)
})
el.setAttribute('data-' + prop, JSON.stringify(attr))
}
}
}
export default ShuffleItemAttributes
Vue.directive('shuffle-item-attributes', ShuffleItemAttributes)
/**
* Sanitize filter
* @param value
* @returns {string}
*/
export const sanitize = (value) => {
let slug = ''
// Change to lower case
let toLowerCase = value.toLowerCase()
// Letter "e"
slug = toLowerCase.replace(/e|é|è|ẽ|ẻ|ẹ|ê|ế|ề|ễ|ể|ệ/gi, 'e')
// Letter "a"
slug = slug.replace(/a|á|à|ã|ả|ạ|ă|ắ|ằ|ẵ|ẳ|ặ|â|ấ|ầ|ẫ|ẩ|ậ/gi, 'a')
// Letter "o"
slug = slug.replace(/o|ó|ò|õ|ỏ|ọ|ô|ố|ồ|ỗ|ổ|ộ|ơ|ớ|ờ|ỡ|ở|ợ/gi, 'o')
// Letter "đ"
slug = slug.replace(/u|ú|ù|ũ|ủ|ụ|ư|ứ|ừ|ữ|ử|ự/gi, 'u')
// Letter "d"
slug = slug.replace(/đ/gi, 'd')
// Letter "ö"
slug = slug.replace(/ä/gi, 'ae')
// Letter "ö"
slug = slug.replace(/ö/gi, 'oe')
// Letter "ö"
slug = slug.replace(/ü/gi, 'ue')
// Letter "ß"
slug = slug.replace(/ß/gi, 'ss')
// Trim the last whitespace
slug = slug.replace(/\s*$/g, '')
// Change whitespace to "-"
slug = slug.replace(/\s+/g, '-')
return slug
}
import VueShuffle from './components/VueShuffle'
import VueShuffleNavbar from './components/VueShuffleNavbar'
import VueShuffleGrid from './components/VueShuffleGrid'
import LoadingIndicator from './components/LoadingIndicator'
import ShuffleItemAttributes from './directives/shuffle-item-attributes'
import { sanitize } from './filters'
import 'vue-multiselect/dist/vue-multiselect.min.css'
import Multiselect from 'vue-multiselect'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/css/bootstrap-theme.min.css'
const VueShuffleJsPlugin = {
install (Vue, options) {
/**
* Add filters
*/
Vue.filter('sanitize', sanitize)
/**
* Install plugin components
*/
Vue.component('multiselect', Multiselect)
Vue.component(VueShuffle.name, VueShuffle)
Vue.component(VueShuffleNavbar.name, VueShuffleNavbar)
Vue.component(VueShuffleGrid.name, VueShuffleGrid)
Vue.component(LoadingIndicator.name, LoadingIndicator)
/**
* Install plugin directives
*/
Vue.directive(ShuffleItemAttributes.name, ShuffleItemAttributes)
/**
* Add component lifecycle hooks or properties
* Call Vue.mixin() here to inject functionality into all components
* Anything added to a mixin will be injected into all components
* created() method runs when the component is created
* mounted() method runs when the component is added to the DOM
*/
/* Vue.mixin({
created () {
console.log('component created...', this)
},
mounted () {
console.log('component mounted...', this)
}
}) */
}
}
/**
* Automatic installation if Vue has been added to the global scope
*/
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(VueShuffleJsPlugin)
}
export default VueShuffleJsPlugin
{
"name": "vue-shufflejs-plugin",
"version": "0.0.1",
"description": "A \"ShuffleJs\" plugin for Vue.js",
"main": "index.js",
"repository": "https://git.hrzg.de/m.mautz/vue-shufflejs-plugin.git",
"author": "Marc Mautz <m.mautz@herzogkommunikation.de>",
"private": true,
"license": "MIT",
"dependencies": {
"bootstrap": "3.3.7",
"imagesloaded": "^4.1.4",
"shufflejs": "^5.0.3",
"vue-multiselect": "^2.0.8"
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
array-parallel@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/array-parallel/-/array-parallel-0.1.3.tgz#8f785308926ed5aa478c47e64d1b334b6c0c947d"
bootstrap@3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.3.7.tgz#5a389394549f23330875a3b150656574f8a9eb71"
ev-emitter@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
imagesloaded@^4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/imagesloaded/-/imagesloaded-4.1.4.tgz#1376efcd162bb768c34c3727ac89cc04051f3cc7"
dependencies:
ev-emitter "^1.0.0"
matches-selector@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/matches-selector/-/matches-selector-1.2.0.tgz#d1814e7e8f43e69d22ac33c9af727dc884ecf12a"
shufflejs@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/shufflejs/-/shufflejs-5.0.3.tgz#04fc417cb79d833044683881e57c9779fc98c8ca"
dependencies:
array-parallel "^0.1.3"
matches-selector "^1.0.0"
throttleit "^1.0.0"
tiny-emitter "^2.0.1"
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
tiny-emitter@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
vue-multiselect@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-2.0.8.tgz#8933c667723b7310d3516b381b834a3da8ca26be"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment