├── .github └── FUNDING.yml ├── docs ├── images │ ├── element.png │ └── preview.png ├── README.md ├── 01-installation.md └── 02-basics.md ├── assets ├── .htaccess ├── handler │ ├── handler.min.js │ └── handler.js └── isotope │ ├── isotope.pkgd.min.js │ └── isotope.pkgd.js ├── package.json ├── config ├── config.php ├── autoload.php └── autoload.ini ├── languages ├── en │ ├── tl_content.php │ └── tl_article.php └── de │ ├── tl_content.php │ └── tl_article.php ├── dca ├── tl_content.php └── tl_article.php ├── gulpfile.js ├── composer.json ├── README.md ├── src ├── EventListener │ ├── ContentDataContainer.php │ └── TemplateListener.php └── FilterHelper.php └── templates └── modules └── mod_article_elements_filter.html5 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: codefog 4 | -------------------------------------------------------------------------------- /docs/images/element.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefog/contao-elements-filter/HEAD/docs/images/element.png -------------------------------------------------------------------------------- /docs/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefog/contao-elements-filter/HEAD/docs/images/preview.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Elements filter – Documentation 2 | 3 | 1. [Installation](01-installation.md) 4 | 2. [Basic configuration](02-basics.md) 5 | -------------------------------------------------------------------------------- /assets/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Order allow,deny 3 | Allow from all 4 | 5 | 6 | Require all granted 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "gulp": "^3.9.1", 4 | "gulp-rename": "^1.2.2", 5 | "gulp-uglify": "^2.1.2", 6 | "isotope-layout": "^3.0.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'system/modules/elements-filter/templates/modules', 9 | ] 10 | ); 11 | -------------------------------------------------------------------------------- /config/autoload.ini: -------------------------------------------------------------------------------- 1 | ;; 2 | ; List modules which are required to be loaded beforehand 3 | ;; 4 | requires[] = "core" 5 | requires[] = "multicolumnwizard" 6 | 7 | ;; 8 | ; Configure what you want the autoload creator to register 9 | ;; 10 | register_namespaces = false 11 | register_classes = false 12 | register_templates = false 13 | -------------------------------------------------------------------------------- /languages/en/tl_content.php: -------------------------------------------------------------------------------- 1 | &$GLOBALS['TL_LANG']['tl_content']['elementsFilter_filters'], 16 | 'exclude' => true, 17 | 'inputType' => 'checkbox', 18 | 'options_callback' => [\Codefog\ElementsFilter\EventListener\ContentDataContainer::class, 'getFilters'], 19 | 'eval' => ['multiple' => true, 'tl_class' => 'clr'], 20 | 'sql' => "blob NULL", 21 | ]; 22 | -------------------------------------------------------------------------------- /assets/handler/handler.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t,n){t.fadeOut(function(){t.children().each(function(){var t=e(this);!n||t.hasClass(n)?t.show():t.hide()}),t.fadeIn()})}function n(t,n){t.isotope({filter:function(){return!n||e(this).hasClass(n)}})}function i(){var i=e(this),a=i.data("handler"),s=e(i.data("elements"));if(s.length<1)return void console.error("The element containing elements does not exist: "+i.data("elements"));var l=s.find(".elements-filter").wrapAll('
').eq(0).parent();"isotope"===a&&l.isotope({itemSelector:".elements-filter"});var r=i.find("a");r.on("click",function(i){i.preventDefault();var s=e(this),o=s.data("filter");o&&(o="elements-filter-"+o),r.removeClass("active"),s.addClass("active"),"isotope"===a?n(l,o):t(l,o)})}e(document).ready(function(){e("[data-elements-filter]").each(i)})}(jQuery); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const rename = require('gulp-rename'); 5 | const uglify = require('gulp-uglify'); 6 | 7 | // Configuration 8 | const scripts = [ 9 | 'assets/handler/handler.js' 10 | ]; 11 | 12 | // Compress the scripts 13 | gulp.task('compress', function () { 14 | return gulp.src(scripts, {base: './'}) 15 | .pipe(uglify()) 16 | .pipe(rename(function (path) { 17 | path.extname = '.min' + path.extname; 18 | })) 19 | .pipe(gulp.dest('./')); 20 | }); 21 | 22 | // Copy the Isotope 23 | gulp.task('isotope', function () { 24 | return gulp.src(['node_modules/isotope-layout/dist/*'], {base: 'node_modules/isotope-layout/dist'}) 25 | .pipe(gulp.dest('assets/isotope')); 26 | }); 27 | 28 | // Build by default 29 | gulp.task('default', ['compress', 'isotope']); 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codefog/contao-elements-filter", 3 | "description": "elements-filter extension for Contao Open Source CMS", 4 | "keywords": ["contao", "content", "elements", "filter"], 5 | "type": "contao-module", 6 | "license": "LGPL-3.0+", 7 | "authors": [ 8 | { 9 | "name": "Codefog", 10 | "homepage": "https://codefog.pl" 11 | }, 12 | { 13 | "name": "heartcodiert", 14 | "homepage": "https://www.heartcodiert.de/" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.4.0", 19 | "contao/core-bundle": "~3.5 || ~4.1", 20 | "contao-community-alliance/composer-plugin": "~2.4 || ~3.0", 21 | "menatwork/contao-multicolumnwizard": "~3.3" 22 | }, 23 | "autoload":{ 24 | "psr-4": { 25 | "Codefog\\ElementsFilter\\": "src/" 26 | } 27 | }, 28 | "extra": { 29 | "contao": { 30 | "sources": { 31 | "": "system/modules/elements-filter" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /languages/en/tl_article.php: -------------------------------------------------------------------------------- 1 | 'Default', 27 | 'isotope' => 'Isotope (by Metafizzy.co)', 28 | ]; 29 | -------------------------------------------------------------------------------- /languages/de/tl_article.php: -------------------------------------------------------------------------------- 1 | 'Standard', 27 | 'isotope' => 'Isotope (by Metafizzy.co)', 28 | ]; 29 | -------------------------------------------------------------------------------- /docs/02-basics.md: -------------------------------------------------------------------------------- 1 | # Basic configuration – Elements Fitler 2 | 3 | 1. [Installation](01-installation.md) 4 | 2. [**Basic configuration**](02-basics.md) 5 | 6 | 7 | ## Enabling the filters 8 | 9 | To enable the filters simply go to the article settings and check the appropriate box. Once the fields 10 | appear you can choose the Javascript handler you would like to use for filtering and define the 11 | available filters. 12 | 13 | ![](images/preview.png) 14 | 15 | ### Filters in details 16 | 17 | The filter value is an internal ID for the filter. The allowed characters are only alphanumeric characters 18 | and the dashes (this value will be used as CSS class). **Hint:** to create the "Show all" button do not 19 | enter any value. 20 | 21 | The label is a text value displayed on the filter button. 22 | 23 | As an extra you can also enter the CSS class per each filter button and give it a special styling. 24 | 25 | 26 | ## Content element settings 27 | 28 | Once you got the filters ready, you can start assign the content elements to appropriate filters. 29 | A content element can belong to one or multiple filters. If the filters are not assigned at all 30 | then the content element will not be filtered. 31 | 32 | ![](images/element.png) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elements Filter extension for Contao Open Source CMS 2 | 3 | ![](https://img.shields.io/packagist/v/codefog/contao-elements-filter.svg) 4 | ![](https://img.shields.io/packagist/l/codefog/contao-elements-filter.svg) 5 | ![](https://img.shields.io/packagist/dt/codefog/contao-elements-filter.svg) 6 | 7 | Elements Filter is an extension for the [Contao Open Source CMS](https://contao.org). 8 | 9 | Contao extension that allows to setup content element filtering using Javascript. The predefined 10 | filters can be set up in the article settings and then the content elements inside that article 11 | can be marked to be filtered by specific values. 12 | 13 | The filtering is done using Javascript and there are two handles provided out of the box: 14 | 15 | 1. Default handler (simple fade in/out effect) 16 | 2. [Isotope](http://isotope.metafizzy.co/) by Metafizzy.co (may require a license) 17 | 18 | ![](docs/images/preview.png) 19 | 20 | ## Documentation 21 | 22 | 1. [Installation](docs/01-installation.md) 23 | 2. [Basic configuration](docs/02-basics.md) 24 | 25 | ## Copyright 26 | 27 | This project has been created and is maintained by [Codefog](https://codefog.pl). 28 | 29 | Thanks to Kim Wormer from [heartcodiert](https://www.heartcodiert.de/) for sponsoring this extension! 30 | -------------------------------------------------------------------------------- /src/EventListener/ContentDataContainer.php: -------------------------------------------------------------------------------- 1 | $v) { 19 | if (is_array($v)) { 20 | continue; 21 | } 22 | 23 | $GLOBALS['TL_DCA']['tl_content']['palettes'][$k] = str_replace( 24 | 'protected;', 25 | 'protected;{elementsFilter_legend},elementsFilter_filters;', 26 | $v 27 | ); 28 | } 29 | } 30 | 31 | /** 32 | * Get the filters 33 | * 34 | * @return array 35 | */ 36 | public function getFilters() 37 | { 38 | $filters = []; 39 | 40 | foreach (FilterHelper::getArticleFilters(CURRENT_ID) as $filter) { 41 | if ($filter['value']) { 42 | $filters[$filter['value']] = $filter['label']; 43 | } 44 | } 45 | 46 | return $filters; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/handler/handler.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | function filterDefault(container, value) { 3 | container.fadeOut(function () { 4 | container.children().each(function () { 5 | var el = $(this); 6 | 7 | if (!value || el.hasClass(value)) { 8 | el.show(); 9 | } else { 10 | el.hide(); 11 | } 12 | }); 13 | 14 | container.fadeIn(); 15 | }); 16 | } 17 | 18 | function filterIsotope(wrapper, value) { 19 | wrapper.isotope({ 20 | filter: function () { 21 | return value ? $(this).hasClass(value) : true; 22 | } 23 | }); 24 | } 25 | 26 | function init() { 27 | var filterContainer = $(this); 28 | var handler = filterContainer.data('handler'); 29 | var elementsWrapper = $(filterContainer.data('elements')); 30 | 31 | if (elementsWrapper.length < 1) { 32 | console.error('The element containing elements does not exist: ' + filterContainer.data('elements')); 33 | return; 34 | } 35 | 36 | var elementsParent = elementsWrapper.find('.elements-filter').wrapAll('
').eq(0).parent(); 37 | 38 | if (handler === 'isotope') { 39 | elementsParent.isotope({ itemSelector: '.elements-filter' }); 40 | } 41 | 42 | var filters = filterContainer.find('a'); 43 | 44 | filters.on('click', function (e) { 45 | e.preventDefault(); 46 | 47 | var filter = $(this); 48 | var value = filter.data('filter'); 49 | 50 | if (value) { 51 | value = 'elements-filter-' + value; 52 | } 53 | 54 | filters.removeClass('active'); 55 | filter.addClass('active'); 56 | 57 | if (handler === 'isotope') { 58 | filterIsotope(elementsParent, value); 59 | } else { 60 | filterDefault(elementsParent, value); 61 | } 62 | }); 63 | } 64 | 65 | $(document).ready(function () { 66 | $('[data-elements-filter]').each(init); 67 | }); 68 | })(jQuery); 69 | -------------------------------------------------------------------------------- /src/EventListener/TemplateListener.php: -------------------------------------------------------------------------------- 1 | handleArticleTemplate($template); 18 | $this->handleElementTemplate($template); 19 | } 20 | 21 | /** 22 | * Handle the article template 23 | * 24 | * @param Template $template 25 | */ 26 | private function handleArticleTemplate(Template $template) 27 | { 28 | if (TL_MODE !== 'FE' 29 | || stripos($template->getName(), 'mod_article') !== 0 30 | || $template->customTpl 31 | || !FilterHelper::isArticleEnabled($template->id) 32 | ) { 33 | return; 34 | } 35 | 36 | $template->setName('mod_article_elements_filter'); 37 | $template->elementsFilters = FilterHelper::getArticleFilters($template->id); 38 | $template->elementsFiltersHandler = $template->elementsFilter_handler; 39 | 40 | // Add the handler assets 41 | if (($assets = FilterHelper::getHandlerAssets($template->id)) !== null) { 42 | foreach ($assets as $asset) { 43 | $GLOBALS['TL_JAVASCRIPT'][] = $asset; 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Handle the content element template 50 | * 51 | * @param Template $template 52 | */ 53 | private function handleElementTemplate(Template $template) 54 | { 55 | if (TL_MODE !== 'FE' 56 | || (stripos($template->getName(), 'ce_') !== 0 && stripos($template->getName(), 'rsce_') !== 0) 57 | || !FilterHelper::isArticleEnabled($template->pid) 58 | || count(($filters = deserialize($template->elementsFilter_filters, true))) < 1 59 | ) { 60 | return; 61 | } 62 | 63 | $classes = []; 64 | 65 | foreach ($filters as $filter) { 66 | $classes[] = 'elements-filter-'.$filter; 67 | } 68 | 69 | $template->class = trim($template->class.' elements-filter '.implode(' ', $classes)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/FilterHelper.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT elementsFilter_enable FROM tl_article WHERE id=?") 19 | ->execute($articleId); 20 | 21 | return $article->elementsFilter_enable ? true : false; 22 | } 23 | 24 | /** 25 | * Get the javascript handler assets 26 | * 27 | * @param int $articleId 28 | * 29 | * @return array|null 30 | */ 31 | public static function getHandlerAssets($articleId) 32 | { 33 | if (!static::isArticleEnabled($articleId)) { 34 | return null; 35 | } 36 | 37 | $assets = ['system/modules/elements-filter/assets/handler/handler.min.js']; 38 | 39 | $article = Database::getInstance()->prepare("SELECT elementsFilter_handler FROM tl_article WHERE id=?") 40 | ->execute($articleId); 41 | 42 | if ($article->elementsFilter_handler === 'isotope') { 43 | $assets[] = 'system/modules/elements-filter/assets/isotope/isotope.pkgd.min.js'; 44 | } 45 | 46 | return $assets; 47 | } 48 | 49 | /** 50 | * Get the article filters 51 | * 52 | * @param int $articleId 53 | * 54 | * @return array 55 | */ 56 | public static function getArticleFilters($articleId) 57 | { 58 | if (!static::isArticleEnabled($articleId)) { 59 | return []; 60 | } 61 | 62 | $filters = []; 63 | $article = Database::getInstance()->prepare("SELECT elementsFilter_filters FROM tl_article WHERE id=?") 64 | ->execute($articleId); 65 | 66 | foreach (deserialize($article->elementsFilter_filters, true) as $filter) { 67 | $filters[] = [ 68 | 'value' => $filter['elementsFilter_filters_value'], 69 | 'label' => $filter['elementsFilter_filters_label'], 70 | 'cssClass' => $filter['elementsFilter_filters_cssClass'], 71 | ]; 72 | } 73 | 74 | return $filters; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dca/tl_article.php: -------------------------------------------------------------------------------- 1 | &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_enable'], 20 | 'exclude' => true, 21 | 'inputType' => 'checkbox', 22 | 'eval' => ['submitOnChange' => true, 'tl_class' => 'clr'], 23 | 'sql' => "char(1) NOT NULL default ''", 24 | ]; 25 | 26 | $GLOBALS['TL_DCA']['tl_article']['fields']['elementsFilter_handler'] = [ 27 | 'label' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_handler'], 28 | 'exclude' => true, 29 | 'inputType' => 'select', 30 | 'options' => ['default', 'isotope'], 31 | 'reference' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_handlerRef'], 32 | 'eval' => ['tl_class' => 'clr'], 33 | 'sql' => "varchar(8) NOT NULL default ''", 34 | ]; 35 | 36 | $GLOBALS['TL_DCA']['tl_article']['fields']['elementsFilter_filters'] = [ 37 | 'label' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_filters'], 38 | 'exclude' => true, 39 | 'inputType' => 'multiColumnWizard', 40 | 'eval' => [ 41 | 'mandatory' => true, 42 | 'tl_class' => 'clr', 43 | 'columnFields' => [ 44 | 'elementsFilter_filters_value' => [ 45 | 'label' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_filters_value'], 46 | 'exclude' => true, 47 | 'inputType' => 'text', 48 | 'eval' => ['rgxp' => 'alias', 'style' => 'width:200px;'], 49 | ], 50 | 'elementsFilter_filters_label' => [ 51 | 'label' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_filters_label'], 52 | 'exclude' => true, 53 | 'inputType' => 'text', 54 | 'eval' => ['style' => 'width:200px;'], 55 | ], 56 | 'elementsFilter_filters_cssClass' => [ 57 | 'label' => &$GLOBALS['TL_LANG']['tl_article']['elementsFilter_filters_cssClass'], 58 | 'exclude' => true, 59 | 'inputType' => 'text', 60 | 'eval' => ['style' => 'width:150px;'], 61 | ], 62 | ], 63 | ], 64 | 'sql' => "blob NULL", 65 | ]; 66 | -------------------------------------------------------------------------------- /templates/modules/mod_article_elements_filter.html5: -------------------------------------------------------------------------------- 1 | 2 |
cssID ?>style): ?> style="style ?>"> 3 | 4 | printable): ?> 5 | 6 | 29 | 30 | 31 | 32 | elementsFilters): ?> 33 |
34 |
    35 | elementsFilters as $filter): ?> 36 | class=""> 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 | elements) ?> 46 |
47 | 48 | backlink): ?> 49 | 50 |

back ?>

51 | 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /assets/isotope/isotope.pkgd.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Isotope PACKAGED v3.0.3 3 | * 4 | * Licensed GPLv3 for open source use 5 | * or Isotope Commercial License for commercial use 6 | * 7 | * http://isotope.metafizzy.co 8 | * Copyright 2017 Metafizzy 9 | */ 10 | 11 | !function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,s,a){function u(t,e,n){var o,s="$()."+i+'("'+e+'")';return t.each(function(t,u){var h=a.data(u,i);if(!h)return void r(i+" not initialized. Cannot call methods, i.e. "+s);var d=h[e];if(!d||"_"==e.charAt(0))return void r(s+" is not a valid method");var l=d.apply(h,n);o=void 0===o?l:o}),void 0!==o?o:t}function h(t,e){t.each(function(t,n){var o=a.data(n,i);o?(o.option(e),o._init()):(o=new s(n,e),a.data(n,i,o))})}a=a||e||t.jQuery,a&&(s.prototype.option||(s.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=o.call(arguments,1);return u(this,t,e)}return h(this,t),this},n(a))}function n(t){!t||t&&t.bridget||(t.bridget=i)}var o=Array.prototype.slice,s=t.console,r="undefined"==typeof s?function(){}:function(t){s.error(t)};return n(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return n.indexOf(e)==-1&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return n!=-1&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=0,o=i[n];e=e||[];for(var s=this._onceEvents&&this._onceEvents[t];o;){var r=s&&s[o];r&&(this.off(t,o),delete s[o]),o.apply(this,e),n+=r?0:1,o=i[n]}return this}},t}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("get-size/get-size",[],function(){return e()}):"object"==typeof module&&module.exports?module.exports=e():t.getSize=e()}(window,function(){"use strict";function t(t){var e=parseFloat(t),i=t.indexOf("%")==-1&&!isNaN(e);return i&&e}function e(){}function i(){for(var t={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},e=0;e