Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Column filter like handsontable #4225

Closed
andreaordonselli opened this issue Jun 28, 2023 · 3 comments
Closed

Column filter like handsontable #4225

andreaordonselli opened this issue Jun 28, 2023 · 3 comments
Labels
Suggested Feature A suggested feature that is waiting review

Comments

@andreaordonselli
Copy link

andreaordonselli commented Jun 28, 2023

This is the column filter of handsontable.
Is there something similar for tabulator?
Documentation here: https://handsontable.com/docs/javascript-data-grid/column-filter/
Many thanks for your work.

image

@andreaordonselli andreaordonselli added the Suggested Feature A suggested feature that is waiting review label Jun 28, 2023
@andreaordonselli andreaordonselli changed the title Column filter like handontable Column filter like handsontable Jul 3, 2023
@andreaordonselli
Copy link
Author

Hi,
I've started making a distinct advanced filter per column, I'm still far from making an excel or handsontable type filter, but it's a start.
I leave the code and video.
It can be seen in action here https://www.opengis.it/demo/ clicking the icon image
In my case the data is taken from geojson which represent spatial data. The integration is with openlayers, but the code I report also works in any table.

I hope it is useful and that in the future it can be supported natively.
Greetings.

Animazione

<div class="menu-overlay">
	<div class="filter-menu">
	  <button class="close-menu-button">x</button>
	  <input type="text" id="filter-input" placeholder="Cerca..."></input>
	  <button id="clear-selection-button"></button>  
	  <button id="select-all-button"></button>
	  <div id="value-select">
	  </div>
	  <button id="remove-filter-button"></button>
	  <button id="filter-button">OK</button>
	</div>
</div>
.tabulator-header-popup-button {
padding: 2px 5px 2px 6px;		
}

.tabulator-header-popup-button.active{
color: rgba(251, 214, 105);
text-shadow: 1px 1px 3px #000;	
}

.menu-overlay {
position: absolute;
z-index: 1;
display: none;
}

.close-menu-button {
position: relative;
float: right;
border-radius: 5px;
}
.close-menu-button:hover{
background-color: #ddd;
}

.filter-menu {
position: absolute;
bottom: 0px;
background-color: #f8f8f8;
padding: 5px;
max-height: 355px;
min-width: 150px;
border: 1px solid #999;
}

.filter-menu > button {
border-radius: 4px;
border-width: 1px;
}

#clear-selection-button {
position: relative;
background: none;
border: none;
color: red;
cursor: pointer;
font-size: 15px;
top: 3px;
}

#select-all-button {
position: relative;
background: none;
border: none;
color: blue;
cursor: pointer;
font-size: 15px;
top: 3px;
}


#value-select {
position: relative;
height: 250px;
width: 200px;
margin: 5px;
overflow: auto;
background-color: #ffffff;
}

#value-select > label {
display: flex;
}

#remove-filter-button {
background: none;
border: none;
font-size: 15px;
color: red;
cursor: pointer;
}

#filter-button {
float: right;
position: relative;
background-color: #f8f8f8;
border-radius: 5px;
}
#filter-button:hover{
background-color: #ddd;
}
headerMenu:true, //show advanced filter menu (Set in column properties)
	// Add filter icon
	$(".tabulator-header-popup-button").html('<i class="fas fa-filter"></i>'); 

	  var menuOverlay = $('.menu-overlay');
	  
	  // Clear data on change
	  var clickedColumn = null;
	  var filters = [];// Array per memorizzare i filtri per ogni colonna
	  
	  // Declaration of the global variable for the separate data of the columns for filter button activation
	  var distinctColumnData = {};

	  // Adding the click event to the "Advanced Filter" button
	  $(".tabulator-header-popup-button").on('click', function(e) {

		// Show the menu overlay
		menuOverlay.show();

		var posX = e.pageX;
		var posY = e.pageY;

		menuOverlay.css({
		  top: posY + 'px',
		  left: posX + 'px',
		  display: 'block'
		});

		// Get the clicked column
		var columnElement = $(e.target).closest('.tabulator-col').get(0);
		clickedColumn = table.getColumn(columnElement).getField();

		// Removing the previous options from the list of values
		$('#value-select').empty();

		// Get the visible data in the selected column
		var columnData = [];
		var visibleRows = table.getRows("active"); // Get only the current rows

		// Iteration over the visible lines
		visibleRows.forEach(function(row) {
		  var cellValue = row.getCell(clickedColumn).getValue(); // Get the cell value for the visible row

		  // Adding the value to the list of distinct values
		  if (cellValue && columnData.indexOf(cellValue) === -1) {
			columnData.push(cellValue);

			// Create a checkbox for each value
			var checkbox = $('<label><input type="checkbox" value="' + cellValue + '">' + cellValue + '<br>' + '</label>');
			$('#value-select').append(checkbox);
		  }
		});
		
	  // Updating the distinctColumnData variable with the distinct column data
	  distinctColumnData[clickedColumn] = columnData;  
		
	  // Select all checkboxes in the #value-select list
	  $('#value-select input[type="checkbox"]').prop('checked', true);
		
	  });
	  
	  // Adding the click event to the "Close menu" button
	  $(".close-menu-button").on('click', function() {
		hideMenuOverlay();
	  });
	  
	  // Hide or show the checkboxes based on the value entered in the input-box
	  function filterListValues() {
		$(function() {
		  $('#filter-input').on('keyup', function() {
			  var query = this.value.toLowerCase();
			  $('#value-select input[type="checkbox"]').each(function(i, elem) {
					if (elem.value.toLowerCase().indexOf(query) != -1) {
						$(this).closest('label').show();
					}else{
						$(this).closest('label').hide();
					}
			  });
		  });
		});
	  }
	  filterListValues();
	  
	  // Added icon to "Select All" button
	  $("#select-all-button").html('<i class="far fa-square" style="position: relative; top: -2px; left: -1px;"></i><i class="fas fa-square fa-inverse" style="position: absolute; top: 3px; left: 9px;"></i><i class="far fa-square-check" style="position: absolute; top: 3px; left: 8px;"></i>');
	  // Adding the click event to the "Select All" button
	  $("#select-all-button").on('click', function() {
		// Select only value-select checkboxes that don't have a label with display: none
		$('#value-select label:not([style*="display: none"]) input[type="checkbox"]').prop('checked', true);
	  });

	  // Added icon to "Clear Selection" button
	  $("#clear-selection-button").html('<i class="far fa-square" style="position: relative; top: -2px; left: -1px;"></i><i class="fas fa-square fa-inverse" style="position: absolute; top: 3px; left: 9px;"></i><i class="far fa-square-xmark" style="position: absolute; top: 3px; left: 8px;"></i>');
	  // Adding the click event to the "Clear Selection" button
	  $("#clear-selection-button").on('click', function() {
		// Remove the filter for the current column
		var columnFilter = filters.find(function(filter) {
		  return filter.field === clickedColumn;
		});

		if (columnFilter) {
		  var filterIndex = filters.indexOf(columnFilter);
		  if (filterIndex !== -1) {
			filters.splice(filterIndex, 1);
		  }
		}
		// Remove the selection from all value-select checkboxes
		$('#value-select input[type="checkbox"]').prop('checked', false);
	  });
	  
	// Update values ​​in list, keep filter in columns already filtered
	$(document).on('change', '#value-select input[type="checkbox"]', function() {
	  var value = $(this).val();
	  var columnFilter = filters.find(function(filter) {
		return filter.field === clickedColumn;
	  });

	  if (columnFilter) {
		if (!columnFilter.values) {
		  columnFilter.values = [];
		}

		if ($(this).is(':checked')) {
		  columnFilter.values.push(value);
		} else {
		  var index = columnFilter.values.indexOf(value);
		  if (index !== -1) {
			columnFilter.values.splice(index, 1);
		  }
		}
	  } else {
		columnFilter = {
		  field: clickedColumn,
		  values: []
		};

		if ($(this).is(':checked')) {
		  columnFilter.values.push(value);
		  filters.push(columnFilter);
		}
	  }
	});

	// Added icon to "Remove Filter" button
	  $('#remove-filter-button').html('<i class="fas fa-filter-circle-xmark"></i>')
	// Adding the click event to the "Remove Filter" button
	  $(document).on('click', '#remove-filter-button', function() {
	   // Remove the filter for the current column
		$('#clear-selection-button').click();

		// Click the "Filter" button to apply the remaining filters
		$('#filter-button').click();  
	  });  
	  
	  
	 // Adding the click event to the "Filter" button
	$(document).on('click', '#filter-button', function() {
	  // Close the menu
	  hideMenuOverlay();
	  
	  // Get the current distinct data of the column
	  var currentColumnData = distinctColumnData[clickedColumn];

	  // Get the values ​​of the selected checkboxes that don't have a label with display: none
	  var selectedValues = $('#value-select label').filter(function() {
		return $(this).css('display') !== 'none';
	  }).find('input[type="checkbox"]:checked').map(function() {
		return $(this).val();
	  }).get();
	  
	  // Comparison between the current distinct data and the selected values
	  var isDistinctDataDifferent = JSON.stringify(currentColumnData) !== JSON.stringify(selectedValues);
	  
	// Adding/Removing the "active" class to the filtered column button
	setTimeout(function() {
	  var targetButton = $('.tabulator-col[tabulator-field="' + clickedColumn + '"] .tabulator-header-popup-button');

	  if (isDistinctDataDifferent && selectedValues.length > 0) {
		targetButton.addClass('active');
	  } else {
		targetButton.removeClass('active');
	  }
	}, 0);
	  
	  // Create the filter array for Tabulator
	  var tabulatorFilters = filters.map(function(filter) {
		return {
		  field: filter.field,
		  type: 'in',
		  value: filter.values
		};
	  });

	  // Add the values ​​of the selected checkboxes to the filters
	  if (selectedValues.length > 0) {
		tabulatorFilters.push({
		  field: clickedColumn,
		  type: 'in',
		  value: selectedValues
		});
	  }

	  // Apply filters
	  table.setFilter(tabulatorFilters);
	});

	  // Close the menu when clicked out of it
	  $(document).on('click', function(e) {
		if (!$(e.target).closest('.filter-menu').length && !$(e.target).is('#advanced-filter')) {
		  hideMenuOverlay();
		}
	  });

	  // Function to hide the filter menu and empty the inputbox
	  function hideMenuOverlay() {
		menuOverlay.hide();
		$('#filter-input').val('');
	  }
	}

@andreaordonselli
Copy link
Author

Here is the code that adds the operator filter.
I leave a short video.

filter operator

html

    <div class="menu-overlay">
      <div class="filter-operator-window">
        <button class="filter-operator-close-button">x</button>
        <h3>Filtra per operatore </h3>
        <select id="filter-operator-select">
          <option value="=" selected>filtra numero: = uguale</option>
          <option value="!=">filtra numero: != diverso</option>
          <option value="<">filtra numero: &lt; minore</option>
          <option value="<=">filtra numero: &lt;= minore-uguale</option>
          <option value=">">filtra numero: &gt; maggiore</option>
          <option value=">=">filtra numero: &gt;= maggiore-uguale</option>
          <option value="between">filtra numero: da/a</option>
          <option value="contains">filtra testo: contiene</option>
          <option value="starts_with">filtra testo: inizia con</option>
          <option value="ends_with">filtra testo: finisce con</option>
        </select>
        <input type="text" id="filter-value-1" placeholder="Inserisci il valore da filtrare"></input>
        <input type="text" id="filter-value-2" placeholder="Inserisci il valore da filtrare"></input>
        <button id="apply-filter-button">Salva</button>
      </div>

      <div class="filter-menu">
        <button class="close-menu-button">x</button>
        <button id="filter-operator">Filtra per operatore...</button>
        <div id="filter-status">
          <span class="filter-text"></span>
          <button id="delete-filter-operator">x</button>
        </div>

        <input type="text" id="filter-input" placeholder="Cerca..."></input>
        <button id="clear-selection-button"></button>
        <button id="select-all-button"></button>
        <div id="value-select">
        </div>
        <button id="remove-filter-button"></button>
        <button id="filter-button">OK</button>
        <div class="filter-menu-overlay">
        </div>
      </div>
    </div>

css

#filter-operator {
  background: unset;
}
#filter-operator:hover {
  background-color: #ddd;
}
.filter-operator-window {
  position: absolute;
  bottom: 180px;
  left: -25px;
  width: 250px;
  height: auto;
  background-color: #fff;
  border: 1px solid #ccc;
  padding: 10px;
  z-index: 1;
  display: none;
}

.filter-operator-close-button {
  position: relative;
  float: right;
  margin-top: unset !important;
  border: 1px solid black;
  border-radius: 4px;
  cursor: pointer;
}
.filter-operator-close-button:hover {
  background-color: #ddd;
}

.filter-operator-window h3 {
  margin: 0 0 10px 0;
}

.filter-operator-window select,
.filter-operator-window input {
  width: 100%;
  margin-top: 4px;
}

.filter-operator-window button {
  margin-top: 10px;
}

#filter-value-2 {
  display: none;
}

#apply-filter-button {
  border: 1px solid black;
  border-radius: 4px;
  cursor: pointer;
  float: right;
  background-color: #4caf50 !important;
  color: white;
}

#apply-filter-button:hover {
  background-color: #ddd;
}

#filter-status {
  position: relative;
  display: none;
}

#filter-status .filter-text {
  display: inline-block;
}

#delete-filter-operator {
  background-color: unset;
  color: red;
  border: none;
  cursor: pointer;
  display: none;
  bottom: 1px;
  position: relative;
}

javascript

// When you click the "Filter operator" button, the window pops up
        $('#filter-operator').off('click').on('click', function() {
          $('.filter-operator-window').show().addClass('active');
          $('#filter-operator-select').val('=');
          $('#filter-value-1').val('');
          $('#filter-value-2').val('');
        });
      
        // When you click the "Filter operator close button" the window is closed
        $('.filter-operator-close-button').off('click').on('click', function() {
          $('.filter-operator-window').hide().removeClass('active');
        });

        function filterOperator() {
          // Get the operator and values ​​from the drop-down menu and input boxes
          var operator = $('#filter-operator-select').val();
          var value1 = $('#filter-value-1').val();
          var value2 = $('#filter-value-2').val();

          // Update dynamic lettering with active filter
          var filterText = 'Filtro:  ' + operator + '  ' + value1 + '  ' + value2;
          $('#filter-status .filter-text').text(filterText);
          $('#filter-status').show();
          $('#delete-filter-operator').show();

          // Filter the options contained in the #value-select input
          $('#value-select input').each(function() {
              var $label = $(this).closest('label');
              // Check if the label has display: none
              if ($label.css('display') === 'none') {
                return; // Ignore value with display: none
              }

              var elementValue = $(this).val().toLowerCase();

              // If the option matches the filter, show it
              switch (operator) {
                case '=':
                  if (elementValue === value1) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case '!=':
                  if (elementValue !== value1) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case '<':
                  if (parseInt($(this).val()) < parseInt(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case '<=':
                  if (parseInt($(this).val()) <= parseInt(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case '>':
                  if (parseInt($(this).val()) > parseInt(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case '>=':
                  if (parseInt($(this).val()) >= parseInt(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case 'between':
                  var val = parseInt($(this).val());
                  if (val >= parseInt(value1) && val <= parseInt(value2)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case 'contains':
                  if (elementValue.includes(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case 'starts_with':
                  if (elementValue.startsWith(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                case 'ends_with':
                  if (elementValue.endsWith(value1)) {
                    $label.show();
                  } else {
                    $label.hide();
                  }
                  break;
                default:
                  // Nothing needs to be done if the operator is invalid
                  break;
              }
            });
        }

        // When you click the "OK" button, the filter is applied to the options contained in the value-select
        $('#apply-filter-button').off('click').on('click', function() {

          filterOperator(); // Adding the call to the filterOperator() function

          // Hide the window
          $('.filter-operator-window').hide().removeClass('active');
        });

        // When the option in the dropdown menu is changed
        $('#filter-operator-select').off('click').on('change', function() {
          var selectedOperator = $(this).val();

          // Show input #filter-value-2 only if the selected operator is "between"
          if (selectedOperator === 'between') {
            $('#filter-value-2').show();
          } else {
            $('#filter-value-2').hide();
          }
        });   

        // When you click the "x" button to delete the filter
        $('#delete-filter-operator').off('click').on('click', function() {
          // Get the current value into the input of the #filter-input
          var filterInputValue = $('#filter-input').val().toLowerCase();

          // Show all hidden values
          $('#value-select input').closest('label').show();

          // Hide the filter text and the "x" button
          $('#filter-status').hide();
          $('#delete-filter-operator').hide();

          // Reset the operator and values ​​to their default values
          $('#filter-operator-select').val('=');
          $('#filter-value-1').val('');
          $('#filter-value-2').val('');

          // Reapply the input filter if there is an inserted value
          if (filterInputValue !== '') {
            filterListValues(filterInputValue);
          }
        });

        // Function to filter the input values ​​based on the value entered in the input-box
        function filterListValues(query) {
          $('#value-select input[type="checkbox"]').each(function(i, elem) {
            var $label = $(this).closest('label');
            if (elem.value.toLowerCase().indexOf(query) !== -1) {
              $label.show();
            } else {
              $label.hide();
            }
          });
        }

        // When the value in the input-box is changed
        $('#filter-input').off('keyup').on('keyup', function() {
          var query = this.value.toLowerCase();
          filterListValues(query);

          // Get the values ​​of #filter-value-1 and #filter-value-2
          var value1 = $('#filter-value-1').val();
          var value2 = $('#filter-value-2').val();

          // Apply filterOperator() only if at least one of the two values ​​has been set
          if (value1 !== '' || value2 !== '') {
            filterOperator();
          }
        });

@olifolkerd
Copy link
Owner

Hey @andreaordonselli

Tabulator already provides an example of how to achieve this, checkout the header menu here:
https://tabulator.info/examples/5.5#menu

Cheers

Oli

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Suggested Feature A suggested feature that is waiting review
Projects
None yet
Development

No branches or pull requests

2 participants