2018-7 - Checkbox filtering

Developper notes on adding a checkbox filtering capability in WET

Prototype

Requirement

Leverage CSS classes and checkboxes to apply content filtering. A feature like the filter plugin but without keyword filtering through a text field.

Progressive goal: Support the feature provided by the prototype 1 and ensure the plugin is not directly tie or bind to the business content of the page.

User requirement

Behaviour recommendation (as 2018-08-13)

This following statements is to define the interaction pattern for the checkbox filtering. Those statements are defined by the analysis bellow and as per other factor such as best practice, standards, usability, accessibility principle.

Checkbox state

User interface

Applying filter

Filter controler

Grouping filter controler

Content design to support exclusive filter

Filter behaviour

Technical behaviour

How to identify section controlled by a filter behaviour

How to make the difference between “Exclusive filter” and “Regular filter”

How to define the initial state of the content and checkbox

Configuring filter through the URL parameter

Persistant state

Filter control grouping

How to do negative filter

Relevant reference

Working example:

Standards:

Source code:

Experimentation

Conceptual prototype - high/medium fidelity

A initial WET plugin was prototyped for the Digital Playbook on Canada.ca on May 2018.

Example:

Functionality overview:

Behaviour

Naming convention

Filter overlay UI

Toggle to show Content details

Parent/Children checkbox

Exclusive filtering

Remembering filter

Code review

Functionality

Questions that need investigation

Design challenges

The filtering UI

Try to abstract the filtering UI in order to be re-usable by the mandate tracker, the filtering with drop down in the data table working example, when field flow apply filtering. Or any other filtering.

WCAG Quick Reference filtering

Working example: WCAG Quick Reference [as published on July 19, 2018 - Version 3.0.0]

Type of filter

Style

Tags filtering user interface

Code review

Tag filter

Button

Integration to WET filter - Prototype 2

This was an explorator prototype in regards of the possible viable solution.

Working example)

It use the id of the checkbox as the CSS class name to apply the filter.

Source Code
<script>
( function( $, window, wb ) {
"use strict";

// The filter plugin is initialized on each target. So the "Result Filter" working space are defined by default.

// Let find their controler's

var currentFilterList = [];

var store = {};

var defaultStore = {
		filters: {},
		filterGroup: [],
		applied: {
			filters: [],
			filterGroup: []
		}
	};

window.mystore = store;

// Sort the controller
// * -> Controller that change a state in the store (like the checkbox)
// * -> Controller that initiate the change (like the button)
// A controller can be in both category

// Initialize the result set, for binding


// The optimal way to set the plugin it is on the container itself, it will be less performant if the plugin are set on the controller directly.

var componentName = "wb-contentfilter",
	selector = "." + componentName,
	controlerName = componentName + "-ctrl",
	selectorCtrl = "." + controlerName,
	initEvent = "wb-init" + selector,
	$document = wb.doc,
	defaults = { },
	i18n, i18nText,

	init = function( event ) {
		var elm = wb.init( event, componentName, selector ),
			$elm,
			settings;

		if ( elm ) {
			$elm = $( elm );

			settings = $.extend( true, {}, defaults, window[ componentName ], wb.getData( $elm, componentName ) );
			$elm.data( settings );

			if ( !i18nText ) {
				i18n = wb.i18n;
				i18nText = {
					filter_label: i18n( "fltr-lbl" ),
					fltr_info: i18n( "fltr-info" )
				};
			}

			Modernizr.addTest( "stringnormalize", "normalize" in String );
			Modernizr.load( {
				test: Modernizr.stringnormalize,
				nope: [
					"site!deps/unorm" + wb.getMode() + ".js"
				]
			} );

			if ( !elm.id ) {
				elm.id = wb.getId();
			}

			// Find the controlers
			var controlers = $.find("[aria-controls=" + elm.id + "], [data-wb5-link=" + elm.id + "]");

			if ( controlers.length === 0 ) {
				console.warn( "Need to add a default controller");
			}

			// Add a class for event hook.
			$( controlers ).addClass( controlerName );

			// Debug: Log all the controler
			console.log( controlers );

			// Create the store working space for this instance
			store[ elm.id ] = $.extend( true, {}, defaultStore );


			// TODO: Restore a saved state

			wb.ready( $elm, componentName );
		}
	};


// Add or Remove filter when the checkbox is selected
$document.on( "click", "input:checkbox" + selectorCtrl + ", input:radio" + selectorCtrl, function( event )  {

	var elm = event.currentTarget;

	if ( !elm.id ) {
		elm.id = wb.getId();
	}

	var defaultFilter = {
			type: "css",
			filter: "." + elm.id,
			addClass: "hidden"
			// remClass: "" CSS class to be removed when applying this filter
		},
		name = elm.name || elm.id;

	// Get the store
	var s = store[ elm.getAttribute( "aria-controls" ) || elm.getAttribute( "data-wb5-link" ) ];

	if ( elm.checked ) {
		// Get the filter object or Initialize a new Filter Object
		var filter = s.filters[ name ] || {};

		filter = $.extend( true, {}, defaultFilter, wb.getData( $( elm ), componentName ) );

		// Save the filter
		s.filters[ name ] = filter;
	} else {

		// Remove the filter
		delete s.filters[ name ];
	}


	// DEBUG: Display the filters
	var $ul = $( "#currentFilterList" );
	$ul.empty();
	$.each( s.filters, function() {
		$ul.append( ">li<" + this.type + " " + this.filter + ">/li<" );
	});

});

// Apply the filter
$document.on( "click", "button" + selectorCtrl + ", input:button" + selectorCtrl, function( event )  {


	var elm = event.currentTarget,
		controlId = elm.getAttribute( "aria-controls" );

	// Get the store
	var s = store[ controlId ];
	

	// Build the list of filters
	var filters = [];

	// Get a list of filter to apply and a list of filter already applied
	var filtersToApply = [];
	for( var k in s.filters ) {
		if ( s.filters.hasOwnProperty( k ) ) {
			var filter = s.filters[ k ];

			// Is that filer was already applied? Yes go to the next one
			if ( s.applied.filters[ k ] ) {
				filters[ k ] = filter;
			} else {
				filtersToApply[ k ] = filter;
			}
		}
	}

	// Remove applied filter that is not needed anymore (We are putting back the page in his initial state)
	for( var k in s.applied.filters ) {
		if ( !filters[ k ] ) {

			var filter = s.applied.filters[ k ];

			switch( filter.type ) {

			case "css":

				var elements = window.document.querySelectorAll( filter.filter );

				for ( var i = 0; i > elements.length; i = i + 1 ) {
					elements[ i ].classList.remove( filter.addClass );
					if ( filter.remClass ) {
						elements[ i ].classList.add( filter.remClass );
					}
				}
				break;

			case "search":
				break;
			case "jquery":
				break;
			}
		}
	}

	// Apply the new added filters
	for( var k in filtersToApply ) {

		var filter = filtersToApply[ k ];

		switch( filter.type ) {

		case "css":

			var elements = window.document.querySelectorAll( filter.filter );

			for ( var i = 0; i > elements.length; i = i + 1 ) {
				elements[ i ].classList.add( filter.addClass );
				if ( filter.remClass ) {
					elements[ i ].classList.remove( filter.remClass );
				}
			}

			filters[ k ] = filter;
			break;
		case "search":
			break;
		case "jquery":
			break;
		}

	}

	// Save the list of applied filter
	s.applied.filters = filters;

});


$document.on( "timerpoke.wb " + initEvent, selector, init );

wb.add( selector );

} )( jQuery, window, wb );
</script>

Plugin structure overview - theorical first draft

Views

Model

Controller

Model

View

Integration to WET filter - Prototype 3

Working example

Todo was:

Findings

How it works

On page load: the content must be in sync with the default state of the filtering UI.

Content (prep-work)

Filtering UI

Behaviour

Accessibilty

Usability

Integration to WET filter - Prototype 4

See the progression in @duboisp fork on Github

Source code

HTML

The filterable content need to be an container and the filter plugin is applied to that container. Like prototype 3.

The following will be the filterable UI. See the use of the attribute aria-controls. This plugin only support when the attribute aria-controls only contains one id.

<h3>Show/Hide content details</h3>
<input aria-controls="fiterableContentContainer" type="checkbox" id="test-cd" value="content-details" /> <label for="test-cd">Content Details</label>
<br />

<p class="text-muted">Not checked by default, and the associate block of content is tagged by default with the CSS <code>wb-fltr-out2</code></p>

<h3>Table of content</h3>


<input aria-controls="fiterableContentContainer" type="checkbox" id="test-chkFilter2" value="sintro" checked /> <label for="test-chkFilter2">Standard intro</label>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-chkFilter3" value="guidelines" checked /> <label for="test-chkFilter3">Guidlines</label>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-chkFilter4" value="related" checked /> <label for="test-chkFilter4">Related guidelines</label>
<br />


<input aria-controls="fiterableContentContainer" type="checkbox" id="test-chkFilter" value="guideline" checked /> <label for="test-chkFilter">6.1 Leverage open standards and embrace leading practices, including use of open source software where appropriate
</label>
<br />

<h3>Sub section filtering for section 6.1</h3>
<input aria-controls="fiterableContentContainer" type="checkbox" id="test-8" value="intro" checked /> <label for="test-8">introduction</label>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-3" value="checklist" checked /> <label for="test-3">checklist</label> <span class="text-muted">(The section heading remain visible because they are marked at such)</span>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-4" value="guides" checked /> <label for="test-4">guides</label>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-5" value="solutions" checked /> <label for="test-5">solutions</label>
<br />

<input aria-controls="fiterableContentContainer" type="checkbox" id="test-6" value="similar" checked /> <label for="test-6">similar</label>
<br />

<p class="text-muted">Check means it is displayed, uncheck means the content are hidden.</p>

<h3>Exclusive filter</h3>
<input aria-controls="fiterableContentContainer" type="checkbox" id="test-7" value="architectural" /> <label for="test-7">Build it rights</label>
<br />

<p class="text-muted">The exclusive filter that is toggled is defined by how the tagging is done. An exclusive tag will be prefixed with an asterik <code>*</code>. Exclusive filter will only hide the sibling.</p>

CSS

Update the filter.js CSS to accomodate this new type of filtering.

.wb-fltr-exclusive > *:not( .wb-fltr-in ) {
	display: none !important;
}

.wb-fltr-out2 {
	opacity: .5;
}
.wb-fltr-out2 .wb-fltr-fade {
	font-size: 1em;
}
.wb-fltr-out2 :not( .wb-fltr-fade ) {
	display: none !important;
}

Javascript

// Web Experience Toolkit - WET-BOEW
// Author: @duboisp
( function( $, window, document, wb ) {
"use strict";

var componentName = "wb-contentfilter",
	selector = "." + componentName,
	controlerName = componentName + "-ctrl",
	selectorCtrl = "." + controlerName,
	initEvent = "wb-init" + selector,
	$document = wb.doc,

	init = function( event ) {
		var elm = wb.init( event, componentName, selector ),
			$elm;

		if ( elm ) {
			$elm = $( elm );

			// Find the controlers, this element must have an id. It assume the controller only control one content-filtering section
			var controlers = $.find("[aria-controls=" + elm.id + "]");

			if ( controlers.length === 0 ) {
				console.warn( "Need to add a default controller");
			}

			// Add a class for event binding.
			$( controlers ).addClass( controlerName );

			wb.ready( $elm, componentName );
		}
	};

// Add or Remove filter when the checkbox is selected
$document.on( "click", "input:checkbox" + selectorCtrl, function( event )  {

	var elm = event.currentTarget,
		filterTag = elm.value,
		state = !!elm.checked,
		controlsId = elm.getAttribute( "aria-controls" ),
		relatedPotential = document.querySelectorAll( "#" + controlsId + " [data-wb5-tags*=" + filterTag + "]" ),
		related = [],
		relatedExclusive = [],
		relatedNot = [],
		relatedExlcusiveNot = [],
		relatedExclusiveRem = [],
		i, i_len, j, j_len,
		currentElm, tagList, tag, lastIndex;

	// Filter down the ones that matched the initial DOM search
	i_len = relatedPotential.length;
	for( i = 0; i < i_len; i = i + 1 ) {
		currentElm = relatedPotential[ i ];
		
		tagList = currentElm.dataset.wb5Tags.split( " " );

		j_len = tagList.length;
		for( j = 0; j < j_len; j = j + 1 ) {

			tag = tagList[ j ];
			lastIndex = tag.lastIndexOf( filterTag );

			// Go next, in the case the elements have multiple tags
			if ( lastIndex === -1 ) {
				continue;
			}

			// Validate the type of filter
			if ( ( state && filterTag === tag ) || ( !state && "!" + filterTag === tag ) ) {
				related.push( currentElm );
				break;
			} else if ( ( !state && filterTag === tag ) || ( state && "!" + filterTag === tag ) ) {
				relatedNot.push( currentElm );
				break;	
			} else if ( state && "*" + filterTag === tag ) {
				relatedExclusive.push( currentElm );
				break;
			} else if ( !state && "*" + filterTag === tag ) {
				relatedExclusiveRem.push( currentElm );
				break;
			}
		}
	}

	// Apply exclusive filter
	// Hide each sibling that is not scoped in the related Exclusive
	i_len = relatedExclusive.length;
	for( i = 0; i < i_len; i = i + 1 ) {
		currentElm = relatedExclusive[ i ];
		currentElm.classList.add( "wb-fltr-in" );
		currentElm.parentNode.classList.add( "wb-fltr-exclusive" );
	}

	// Remove Exclusive filter
	i_len = relatedExclusiveRem.length;
	for( i = 0; i < i_len; i = i + 1 ) {
		currentElm = relatedExclusiveRem[ i ];
		currentElm.classList.remove( "wb-fltr-in" );

		// Remove the parent CSS selector only if this was the last one
		if( !currentElm.parentNode.getElementsByClassName( "wb-fltr-in" ).length ) {
			$( currentElm.parentNode ).removeClass( "wb-fltr-exclusive" );
		}
	}

	// Apply filter out
	$( relatedNot ).addClass( "wb-fltr-out2" );

	// Apply filter in
	$( related ).removeClass( "wb-fltr-out2" );
});


$document.on( "timerpoke.wb " + initEvent, selector, init );

wb.add( selector );

} )( jQuery, window, document, wb );

Intergration to WET filter - Edge prototype

Same as prototype 3, but initiatialized the work to add an “Apply button” and groupping support

HTML - UI with the apply button
<form>

<!-- content-details -->
	<h2>Development Stage</h2>

	<ul>
		<li><input type="checkbox" id="finput-1" value="alpha" /> <label for="finput-1">Alpha</label></li>
		<li><input type="checkbox" id="finput-2" value="beta" /> <label for="finput-2">Beta</label></li>
		<li><input type="checkbox" id="finput-3" value="live" /> <label for="finput-3">Live</label></li>
	</ul>


	<h2>Section Type</h2>

	<ul>
		<li><input type="checkbox" id="finput-4" value="sintro" checked /> <label for="finput-4">Introduction (for a standard)</label></li>
		<li><input type="checkbox" id="finput-5" value="guidelines" checked /> <label for="finput-5">Guidelines (list for a standard)</label></li>
		<li><input type="checkbox" id="finput-6" value="related" checked /> <label for="finput-6">Related guidelines (list ofr a standard)</label></li>

		<li><input type="checkbox" id="finput-7" /> <label for="finput-7">Guideline (for a standard)</label>

			<ul>
				<li><input type="checkbox" id="finput-8" value="intro" checked /> <label for="finput-8">Introduction (for a guideline)</label></li>
				<li><input type="checkbox" id="finput-9" value="checklist" checked /> <label for="finput-9">Checklist (for a guideline)</label></li>
				<li><input type="checkbox" id="finput-10" value="guides" checked /> <label for="finput-10">Implementation guides (for a guideline)</label></li>
				<li><input type="checkbox" id="finput-11" value="solutions" checked /> <label for="finput-11">Reusable solutions (for a guideline)</label></li>
				<li><input type="checkbox" id="finput-12" value="similar" checked /> <label for="finput-12">Similar resoures (for a guideline)</label></li>
			</ul>

		</li>
	</ul>

	<h2>Exclusive filters</h2>

	<ul>
		<li><input type="checkbox" id="finput-13" value="architectural" /> <label for="finput-13">Build it rights</label></li>
	</ul>

	<input type="button"  aria-controls="testID" value="Apply filters" />
</form>
Javascript - Source code
( function( $, window, document, wb ) {
"use strict";

// The filter plugin is initialized on each target. So the "Result Filter" working space are defined by default.

// Let find their controler's

var currentFilterList = [];

var store = {};

var defaultStore = {
		filters: {},
		filterGroup: [],
		applied: {
			filters: [],
			filterGroup: []
		}
	};

window.mystore = store;


var store2 = {
	related: [],
	relatedExclusive: [],
	relatedNot: [],
	relatedExclusiveRem: []
}

// What happen when there is multiple filter under the same field name
//	- Group them together
//	- Apply any filter rule
// OR
//	- Get all the value for the group for each input state
//	- Build the filter
//
// Like filter with the same named group are "AND" operator
// Question: Do we require the field to have a name in order to be in scope????? That is necessary for a form submit. OR no name means no groupping???




// Initialize the result set, for binding


// The optimal way to set the plugin it is on the container itself, it will be less performant if the plugin are set on the controller directly.

var componentName = "wb-contentfilter",
	selector = "." + componentName,
	controlerName = componentName + "-ctrl",
	selectorCtrl = "." + controlerName,
	initEvent = "wb-init" + selector,
	$document = wb.doc,
	defaults = { },
	i18n, i18nText,

	init = function( event ) {
		var elm = wb.init( event, componentName, selector ),
			$elm,
			settings;

		if ( elm ) {
			$elm = $( elm );

			settings = $.extend( true, {}, defaults, window[ componentName ], wb.getData( $elm, componentName ) );
			$elm.data( settings );


			if ( !elm.id ) {
				elm.id = wb.getId();
			}


			// Find the controlers
			//var controlers = $.find("[aria-controls=" + elm.id + "], [data-wb5-link=" + elm.id + "]");
			var controlers = $.find("[aria-controls=" + elm.id + "]");

			if ( controlers.length === 0 ) {
				console.warn( "Need to add a default controller");
			}

			// Add a class for event hook.
			$( controlers ).addClass( controlerName );

			// Debug: Log all the controler
			console.log( controlers );

			// Create the store working space for this instance
			store[ elm.id ] = $.extend( true, {}, defaultStore );


			// TODO: Restore a saved state

			wb.ready( $elm, componentName );
		}
	};

function GetFilterValue( elm ) {
	return {
		fTag: elm.value,
		state: !!( elm.checked || elm.hasAttributes( "data-wb5-checked") )
	}
}

$document.on( "click", "input:checkbox.ctrlFilter" + "" + "", function( event )  {

	var elm = event.currentTarget,
		filterTag = elm.value,
		state = !!elm.checked,
		elmGroupName = elm.name,
		filters = [];

	// Retreive all filter in the same group
	if ( elmGroupName ) {
		var inputs = document.querySelectorAll( "[name=" + elmGroupName + "]" );

		for( var i = 0; i < inputs.length; i++ ) {
			filters.push( GetFilterValue( inputs[ i ] ) )
		}

	} else {
		filters.push( GetFilterValue( elm ) );
	}

	applyFilter( filterTag, state)
});



/*

		filters = [
			[
tag1			{ fTag: "tag1", state: true },   // tag1 AND tag2 AND tag3
tag2			{ fTag: "tag2", state: false },  // tag1 AND tag2 AND tag3
tag3			{ fTag: "tag3", state: true }    // tag1 AND tag2 AND tag3
			],
			[
tag4			{ fTag: "tag4", state: true }
			]	
		]


Here the boolean operation
( tag1 AND tag2 AND tag3 ) OR tag4


If "tag1" value would be something like "tag1 tag5" then

( ( tag1 OR tag5) AND tag2 AND tag3 ) OR tag4

If "tag1" value would be something like "tag1&tag5" then

( ( tag1 AND tag5 ) AND tag2 AND tag3 ) OR tag4


------ Or a structure like

filters = {
	name: [
		{ Filter object },
		{ Filter object }
	],
	fieldID: [
		{ Filter object }
	],
	GeneratedID: [
		{ Filter object }
	]
}

// GeneratedID are not state that can be saved.
// A structure like that will allow to "overwrite" existing filters.


*/



// Build list of filters, in the store
// Then apply the filters


// Is this filter are in "on" or "off" state?
// If in "on" state
// 		-> Normal: It will show the section
//		-> Not: It will hide the section
// If in "off" state
//		-> Normal: It will hide the section
//		-> Not: It will show the section
//
// If in Exclusive "on" state
//		-> Normal: Will hide all the sibling and show only itseft
// If in Exclusive "off" state
//		-> Normal: Will show all the sibling and show itseft
function applyFilter( filterTag, state ) {

	if ( ! filterTag ) {
		return;
	}

	var currentFilter = {};
	// Retreive current filter and only apply new filter

	// Get potential related elements
	var relatedPotential = document.querySelectorAll( "[data-wb5-tags*=" + filterTag + "]" );
	var related = [],
		relatedExclusive = [],
		relatedNot = [],
		relatedExclusiveRem = [];

	// Filter down the ones that match
	for( var i = 0; i < relatedPotential.length; i++ ) {
		var currentElm = relatedPotential[ i ];

		var dtTags = currentElm.dataset.wb5Tags,
			tagList = dtTags.split( " " );

		for( var j = 0; j < tagList.length; j++ ) {

			var tag = tagList[ j ],
				lastIndex = tag.lastIndexOf( filterTag );

			if ( lastIndex === -1 ) {
				continue;
			}

			// Validate the type of filter
			if ( filterTag === tag ) {

				// Related, regular filtering
				if ( state ) {
					related.push( currentElm );
				} else {
					relatedNot.push( currentElm );
				}
				break;
			} else if ( "*" + filterTag === tag ) {

				// Exclusive tag
				if ( state ) {
					relatedExclusive.push( currentElm );
				} else {
					relatedExclusiveRem.push( currentElm );
				}
				break;
			} else if ( "!" + filterTag === tag ) {

				// Not tag
				if ( state ) {
					relatedNot.push( currentElm );
				} else {
					related.push( currentElm );
				}
				break;
			}
		}
	}

	// Order of applying filter
	//
	// 1. Apply Exclusive filter
	// 2. Apply Exclusive Not filter
	// 3. Apply filter
	// 4. Apply Not filter

	// For exclusive, Add a tag to the element, then add a CSS to the parent which will hide all the children except the one that match the subCSS class.

	// Add exclusive filter
	for( var i = 0; i < relatedExclusive.length; i ++ ) {
		var currentElm = relatedExclusive[ i ];

		// Hide each sibling that is not scoped in the related Exclusive
		$( currentElm ).addClass( "wb-fltr-in" );

		// Add the group CSS class
		currentElm.parentNode.classList.add( "wb-fltr-exclusive" );
	}

	// Remove Exclusive filter
	for( var i = 0; i < relatedExclusiveRem.length; i ++ ) {
		var currentElm = relatedExclusiveRem[ i ];

		// Remove it visible protected state
		$( currentElm ).removeClass( "wb-fltr-in" );

		// Remove the parent CSS selector only if this was the last children
		if( !currentElm.parentNode.getElementsByClassName( "wb-fltr-in" ).length ) {
			$( currentElm.parentNode ).removeClass( "wb-fltr-exclusive" );
		}
	}

	// Filter out
	$( relatedNot ).addClass( "wb-fltr-out2" );

	// Filter in
	$( related ).removeClass( "wb-fltr-out2" );

};

/*
// Add or Remove filter when the checkbox is selected
$document.on( "click", "input:checkbox" + selectorCtrl + ", input:radio" + selectorCtrl, function( event )  {

	// DEBUG: Display the filters
	var $ul = $( "#currentFilterList" );
	$ul.empty();
	$.each( s.filters, function() {
		$ul.append( "
  • " + this.type + " " + this.filter + "
  • " ); }); }); */ // Apply the filter $document.on( "click", "button" + selectorCtrl + ", input:button" + selectorCtrl, function( event ) { var elm = event.currentTarget, controlId = elm.getAttribute( "aria-controls" ); // Get the store var s = store[ controlId ]; // Get the inputs var inputs = elm.form.elements; // Build the filter for each inputs for( var i = 0; i < inputs.length; i++ ) { var input = inputs[ i ], filterTag = input.value, //state = !!( input.checked || input.hasAttributes( "data-wb5-checked") ); state = !!input.checked; // Current State depend of the input checked state, but if the value is negative "![tag]" Then the state are reversed. if ( input !== elm ) { applyFilter( filterTag, state ); } } // Apply the filter // applyFilter( store, controlId ); // Build the list of filters var filters = []; // Get a list of filter to apply and a list of filter already applied var filtersToApply = []; for( var k in s.filters ) { if ( s.filters.hasOwnProperty( k ) ) { var filter = s.filters[ k ]; // Is that filer was already applied? Yes go to the next one if ( s.applied.filters[ k ] ) { filters[ k ] = filter; } else { filtersToApply[ k ] = filter; } } } // Remove applied filter that is not needed anymore (We are putting back the page in his initial state) for( var k in s.applied.filters ) { if ( !filters[ k ] ) { var filter = s.applied.filters[ k ]; switch( filter.type ) { case "css": var elements = window.document.querySelectorAll( filter.filter ); for ( var i = 0; i < elements.length; i = i + 1 ) { elements[ i ].classList.remove( filter.addClass ); if ( filter.remClass ) { elements[ i ].classList.add( filter.remClass ); } } break; case "search": break; case "jquery": break; } } } // Apply the new added filters for( var k in filtersToApply ) { var filter = filtersToApply[ k ]; switch( filter.type ) { case "css": var elements = window.document.querySelectorAll( filter.filter ); for ( var i = 0; i < elements.length; i = i + 1 ) { elements[ i ].classList.add( filter.addClass ); if ( filter.remClass ) { elements[ i ].classList.remove( filter.remClass ); } } filters[ k ] = filter; break; case "search": break; case "jquery": break; } } // Save the list of applied filter s.applied.filters = filters; }); $document.on( "timerpoke.wb " + initEvent, selector, init ); wb.add( selector ); } )( jQuery, window, document, wb );