<!-- Filters column -->
<div ref="filters"
- class="products-filters-wrapper sticky sm:static top-60 mr-1v sm:mr-0 pt-4 whitespace-no-wrap float-left sm:float-none">
+ class="products-filters-wrapper sticky sm:static top-60 mr-1v sm:mr-0 pt-4 whitespace-no-wrap float-left sm:float-none">
<!-- Filters panel -->
<div class="bg-white p-4">
@click="changeViewStyle">
<svg viewBox="0 0 38 36">
<g>
- <polygon fill="currentColor" points="28,11 10,11 10,13 28,13 28,11 "/>
+ <polygon fill="currentColor" points="28,11 10,11 10,13 28,13 28,11 "/>
</g>
<g>
- <polygon fill="currentColor" points="28,17 10,17 10,19 28,19 28,17 "/>
+ <polygon fill="currentColor" points="28,17 10,17 10,19 28,19 28,17 "/>
</g>
<g>
- <polygon fill="currentColor" points="28,23 10,23 10,25 28,25 28,23 "/>
+ <polygon fill="currentColor" points="28,23 10,23 10,25 28,25 28,23 "/>
</g>
</svg>
</a>
</template>
<script>
- import 'polyfill-array-includes' // For IE 11 and friends
- import 'ie-array-find-polyfill'
- import 'es7-object-polyfill'
- import VueSlider from 'vue-slider-component' // See vue-slider.styl for CSS
+import 'polyfill-array-includes' // For IE 11 and friends
+import 'ie-array-find-polyfill'
+import 'es7-object-polyfill'
+import VueSlider from 'vue-slider-component' // See vue-slider.styl for CSS
- export default {
+export default {
- components: {
- VueSlider
- },
+ components: {
+ VueSlider
+ },
- props: {
- productType: {
- required: true,
- },
- baseFilters: {
- type: String,
- default: '',
- },
- filterData: {
- required: true,
- },
- resultData: {
- required: true,
- },
- translations: {
- type: Object,
- required: true,
- }
+ props: {
+ productType: {
+ required: true,
+ },
+ baseFilters: {
+ type: String,
+ default: '',
+ },
+ filterData: {
+ required: true,
},
- data: () => ({
- viewStyle: 'grid',
- filters: {},
- filterWrappers: [], // All the sets of filters in the accordion
- currentFilterIndex: 0, // Index of the filter group that is open
- matches: {},
- products: [],
- log: function (v) {
- var res = Math.pow(10, v);
- var nbZeros = Math.max(-1, Math.round(v - 2));
- var roundFactor = Math.pow(10, nbZeros);
- var introunded = Math.round(res / roundFactor);
- var rounded = introunded * roundFactor;
- rounded = rounded.toFixed(Math.max(0, -nbZeros));
-
- return rounded;
- },
- linear: function (v) {
- return v;
+ resultData: {
+ required: true,
+ },
+ translations: {
+ type: Object,
+ required: true,
+ }
+ },
+ data: () => ({
+ viewStyle: 'grid',
+ filters: {},
+ filterWrappers: [], // All the sets of filters in the accordion
+ currentFilterIndex: 0, // Index of the filter group that is open
+ matches: {},
+ products: [],
+ log: function (v) {
+ var res = Math.pow(10, v);
+ var nbZeros = Math.max(-1, Math.round(v - 2));
+ var roundFactor = Math.pow(10, nbZeros);
+ var introunded = Math.round(res / roundFactor);
+ var rounded = introunded * roundFactor;
+ rounded = rounded.toFixed(Math.max(0, -nbZeros));
+
+ return rounded;
+ },
+ linear: function (v) {
+ return v;
+ }
+ }),
+
+ watch: {
+ // Whenever a filter is changed, update the querystring
+ filters: {
+ deep: true,
+
+ handler(oldFilters, newFilters) {
+ // Using square brackets in the URL can cause problems due to URL encoding and decoding
+ // so it's better to remove them while also shortening the URL to simply use Fx=...
+ const safeQuerystring = this.filterQuerystring.replace(/filter\[(\d+)]/g, 'F$1');
+ history.replaceState(newFilters, '', '?' + safeQuerystring);
}
- }),
-
- watch: {
- // Whenever a filter is changed, update the querystring
- filters: {
- deep: true,
-
- handler(oldFilters, newFilters) {
- // Using square brackets in the URL can cause problems due to URL encoding and decoding
- // so it's better to remove them while also shortening the URL to simply use Fx=...
- const safeQuerystring = this.filterQuerystring.replace(/filter\[(\d+)]/g, 'F$1');
- history.replaceState(newFilters, '', '?' + safeQuerystring);
- }
- },
+ },
- viewStyle(newStyle) {
- localStorage.viewStyle = newStyle;
+ viewStyle(newStyle) {
+ localStorage.viewStyle = newStyle;
+ }
+ },
+
+ computed: {
+ resultsCount() {
+ let count = Object.keys(this.matches.hits).length;
+ let results = '';
+
+ // Handle pluralisation and localisation
+ if (count === 1) {
+ results = this.translations.result;
+ } else {
+ results = this.translations.results;
}
+
+ return `${count} ${results}`;
},
- computed: {
- resultsCount() {
- let count = Object.keys(this.matches.hits).length;
- let results = '';
+ noResults() {
+ return Object.keys(this.matches.hits).length === 0
+ },
- // Handle pluralisation and localisation
- if (count === 1) {
- results = this.translations.result;
- } else {
- results = this.translations.results;
- }
+ filterQuerystring() {
+ let filter_list = [];
- return `${count} ${results}`;
- },
+ Object.keys(this.filters).forEach(filterID => {
- noResults() {
- return Object.keys(this.matches.hits).length === 0
- },
+ let filter = this.getFilter(filterID);
- filterQuerystring() {
- let filter_list = [];
+ // Don't include range values if they're unchanged (at min / max values)
+ if (filter.type === 'range'
+ && filter.minscale === this.filters[filterID][0]
+ && filter.maxscale === this.filters[filterID][1]) {
+ return;
+ }
- Object.keys(this.filters).forEach(filterID => {
+ if (this.filters[filterID].length > 0) {
+ filter_list.push(`filter[${filterID}]=` + this.filters[filterID].join(';'));
+ }
+ });
- let filter = this.getFilter(filterID);
+ return filter_list.join('&');
+ },
+ },
- // Don't include range values if they're unchanged (at min / max values)
- if (filter.type === 'range'
- && filter.minscale === this.filters[filterID][0]
- && filter.maxscale === this.filters[filterID][1]) {
- return;
- }
+ created() {
- if (this.filters[filterID].length > 0) {
- filter_list.push(`filter[${filterID}]=` + this.filters[filterID].join(';'));
- }
- });
+ // Initially, it will be all the IDs from the server
+ this.matches = this.resultData;
- return filter_list.join('&');
- },
- },
+ // Create dynamic data so we can bind the filter fields via v-model for each option
+ let filters = {};
+ Object.values(this.filterData).forEach(filter => {
+ if (filter.type === 'range') {
+ filters[filter.id] = [filter.minscale, filter.maxscale];
+ } else {
+ filters[filter.id] = [];
+ }
+ });
- created() {
+ this.filters = filters;
+ this.parseQuerystring();
+ },
- // Initially, it will be all the IDs from the server
- this.matches = this.resultData;
+ mounted() {
- // Create dynamic data so we can bind the filter fields via v-model for each option
- let filters = {};
- Object.values(this.filterData).forEach(filter => {
- if (filter.type === 'range') {
- filters[filter.id] = [filter.minscale, filter.maxscale];
- } else {
- filters[filter.id] = [];
- }
- });
+ // Collect product elements from the default slot so we can handle them better as Vue objects
+ this.$slots.default[0].children.forEach(child => {
- this.filters = filters;
- this.parseQuerystring();
- },
+ if (child.tag) { // Ensure it's a tag and not an empty text element
+ let product = child.elm;
+ let ID = parseInt(product.dataset.productId);
+ this.products.push({id: ID, html: product.outerHTML});
+ }
+ });
- mounted() {
+ if (localStorage.viewStyle) {
+ this.viewStyle = localStorage.viewStyle;
+ }
- // Collect product elements from the default slot so we can handle them better as Vue objects
- this.$slots.default[0].children.forEach(child => {
+ // Gather all the filter sets for the accordion
+ this.filterWrappers = this.$refs.filters.querySelectorAll('.filters-wrapper');
- if (child.tag) { // Ensure it's a tag and not an empty text element
- let product = child.elm;
- let ID = parseInt(product.dataset.productId);
- this.products.push({id: ID, html: product.outerHTML});
- }
- });
+ // Calculate the max-height of each filter set (required so accordion height animation can work)
+ this.filterWrappers.forEach(function(el) {
- if (localStorage.viewStyle) {
- this.viewStyle = localStorage.viewStyle;
+ let totalHeight = 0;
+ for (let i = 0; i < el.children.length; i++) {
+ totalHeight += el.children[i].clientHeight;
}
- // Gather all the filter sets for the accordion
- this.filterWrappers = this.$refs.filters.querySelectorAll('.filters-wrapper');
+ el.style.maxHeight = totalHeight + 'px';
+ });
- // Calculate the max-height of each filter set (required so accordion height animation can work)
- this.filterWrappers.forEach(function(el) {
+ // Find the first filter set that has pre-selected filters and open it
+ let initialFilterIndex = 0;
+ for (let fi = 0; fi < this.filterData.length; fi++) {
+ let id = this.filterData[fi].id;
+ if (this.filters[id].length > 0 && this.filterData[fi].type !== 'range') {
+ initialFilterIndex = fi;
+ break;
+ }
+ }
+ this.expandFilters(initialFilterIndex);
- let totalHeight = 0;
- for (let i = 0; i < el.children.length; i++) {
- totalHeight += el.children[i].clientHeight;
- }
+ },
- el.style.maxHeight = totalHeight + 'px';
- });
+ methods: {
- // Find the first filter set that has pre-selected filters and open it
- let initialFilterIndex = 0;
- for (let fi = 0; fi < this.filterData.length; fi++) {
- let id = this.filterData[fi].id;
- if (this.filters[id].length > 0 && this.filterData[fi].type !== 'range') {
- initialFilterIndex = fi;
- break;
- }
- }
- this.expandFilters(initialFilterIndex);
+ parseQuerystring() {
+ const $this = this;
+ const querystring = this.baseFilters + '&' + location.search.substring(1); // Get querystring minus first character (?)
+
+ if (querystring.length > 0) {
+ querystring.split('&').forEach(function (pair) {
+ if (pair === '') {
+ return;
+ }
+
+ let [key, value] = pair.split('=');
+
+ // Key will be in the format of 'filter[x]' where 'x' is a number
+ // We only need the numeric ID of the filter so strip everything else
+ key = key.replace(/\D/g, '');
+
+ // Values are a string delimited by ';' so split them into array
+ // Values may be a number or a string ('-') so don't try to convert them to integers
+ value = value.split(';');
+
+ // Update the actual filter data if the key exists
+ if ($this.filters.hasOwnProperty(key)) {
+ $this.filters[key] = value;
+ }
+ });
+ $this.updateFilters();
+ }
},
- methods: {
+ getFilter(id) {
+ // Find the filter by its ID in the array of filters
+ return this.filterData.find(filter => filter.id === parseInt(id)) || {};
+ },
- parseQuerystring() {
- const $this = this;
- const querystring = this.baseFilters + '&' + location.search.substring(1); // Get querystring minus first character (?)
+ updateFilters() {
+ let $this = this;
+ let endpoint = `/ajax/filtercatalog/?productType=${this.productType}&${this.filterQuerystring}`;
- if (querystring.length > 0) {
- querystring.split('&').forEach(function (pair) {
- if (pair === '') {
- return;
- }
+ axios.get(endpoint)
+ .then(function (response) {
+ $this.matches = response.data.results;
+ })
+ .catch(function (error) {
+ console.error('Error filtering products', error);
+ });
+ },
- let [key, value] = pair.split('=');
+ productVisible(id) {
+ return Object.values(this.matches.hits).includes(id)
+ },
- // Key will be in the format of 'filter[x]' where 'x' is a number
- // We only need the numeric ID of the filter so strip everything else
- key = key.replace(/\D/g, '');
+ productLoaded(id) {
+ console.log(id + ' loaded');
+ },
- // Values are a string delimited by ';' so split them into array
- // Values may be a number or a string ('-') so don't try to convert them to integers
- value = value.split(';');
+ removeFilter(filterID, optionID) {
+ let index = this.filters[filterID].indexOf(optionID);
- // Update the actual filter data if the key exists
- if ($this.filters.hasOwnProperty(key)) {
- $this.filters[key] = value;
- }
- });
+ if (index !== -1) {
+ this.filters[filterID].splice(index, 1);
+ this.updateFilters();
+ }
+ },
- $this.updateFilters();
- }
- },
-
- getFilter(id) {
- // Find the filter by its ID in the array of filters
- return this.filterData.find(filter => filter.id === parseInt(id)) || {};
- },
-
- updateFilters() {
- let $this = this;
- let endpoint = `/ajax/filtercatalog/?productType=${this.productType}&${this.filterQuerystring}`;
-
- axios.get(endpoint)
- .then(function (response) {
- $this.matches = response.data.results;
- })
- .catch(function (error) {
- console.error('Error filtering products', error);
- });
- },
-
- productVisible(id) {
- return Object.values(this.matches.hits).includes(id)
- },
-
- productLoaded(id) {
- console.log(id + ' loaded');
- },
-
- removeFilter(filterID, optionID) {
- let index = this.filters[filterID].indexOf(optionID);
-
- if (index !== -1) {
- this.filters[filterID].splice(index, 1);
- this.updateFilters();
- }
- },
+ changeViewStyle(event) {
+ if (event) {
+ event.preventDefault()
+ }
+ this.viewStyle = this.viewStyle === 'list' ? 'grid' : 'list';
+ },
- changeViewStyle(event) {
- if (event) {
- event.preventDefault()
- }
- this.viewStyle = this.viewStyle === 'list' ? 'grid' : 'list';
- },
-
- expandFilters(index) {
- // For the accordion animation to work, overflow:hidden must be set on the wrapping element.
- // However, some filter elements like sliders don't display properly if the overflow is hidden
- // (due to tooltips etc), so we need to set the overflow to visible on the open set of filters
- // immediately *after* the CSS transition (300ms) ends. When a new filter set is opened, the
- // overflow:visible override needs to be removed immediately, before the transition accordion
- // animation begins...
- let $this = this;
- let previousIndex = this.currentFilterIndex;
- this.filterWrappers[previousIndex].style.removeProperty('overflow');
- this.currentFilterIndex = index;
- setTimeout(function() {
- $this.filterWrappers[index].style.overflow = 'visible';
- }, 300); // Keep this value in sync with the CSS transition duration defined below!
- },
+ expandFilters(index) {
+ // For the accordion animation to work, overflow:hidden must be set on the wrapping element.
+ // However, some filter elements like sliders don't display properly if the overflow is hidden
+ // (due to tooltips etc), so we need to set the overflow to visible on the open set of filters
+ // immediately *after* the CSS transition (300ms) ends. When a new filter set is opened, the
+ // overflow:visible override needs to be removed immediately, before the transition accordion
+ // animation begins...
+ let $this = this;
+ let previousIndex = this.currentFilterIndex;
+ this.filterWrappers[previousIndex].style.removeProperty('overflow');
+ this.currentFilterIndex = index;
+ setTimeout(function() {
+ $this.filterWrappers[index].style.overflow = 'visible';
+ }, 300); // Keep this value in sync with the CSS transition duration defined below!
},
+ },
- }
+}
</script>
<style lang="stylus" scoped>