isomorphic filters + update fields + better style

* create isomorphic tabulator filters but getting the right data based on the component object available keys
* create an extendTabulator function for formatters and filters overrides
* add fields to filter table on component + udpate fields to emit all field input
* add css style for stripes table and some hover fix
* enhance filter logic to work with tabulator flaws (impossible to add one filter at a time and impossible to filter multiple fields with one filter by default, added) + avoid filters on some cases (no value or select with value "all", ...)
This commit is contained in:
Jordan Blasenhauer 2024-08-11 17:48:57 +02:00
parent 98fc174a22
commit d7b512c7a1
7 changed files with 306 additions and 24 deletions

View file

@ -38,10 +38,14 @@ const props = defineProps({
default: {},
},
});
// emits
const emit = defineEmits(["inp"]);
</script>
<template>
<Checkbox
@inp="(value) => $emit('inp', value)"
v-if="props.setting.inpType === 'checkbox'"
:id="props.setting.id || ''"
:columns="props.setting.columns || false"
@ -60,6 +64,7 @@ const props = defineProps({
:attrs="props.setting.attrs || {}"
/>
<Select
@inp="(value) => $emit('inp', value)"
v-if="props.setting.inpType === 'select'"
:id="props.setting.id || ''"
:columns="props.setting.columns || false"
@ -83,6 +88,7 @@ const props = defineProps({
:attrs="props.setting.attrs || {}"
/>
<Datepicker
@inp="(value) => $emit('inp', value)"
v-if="props.setting.inpType === 'datepicker'"
:id="props.setting.id || ''"
:columns="props.setting.columns || false"
@ -104,6 +110,7 @@ const props = defineProps({
:attrs="props.setting.attrs || {}"
/>
<Input
@inp="(value) => $emit('inp', value)"
v-if="props.setting.inpType === 'input'"
:id="props.setting.id || ''"
:columns="props.setting.columns || false"
@ -127,6 +134,7 @@ const props = defineProps({
:attrs="props.setting.attrs || {}"
/>
<Editor
@inp="(value) => $emit('inp', value)"
v-if="props.setting.inpType === 'editor'"
:id="props.setting.id || ''"
:columns="props.setting.columns || false"

View file

@ -40,7 +40,7 @@ import { useUUID } from "@utils/global.js";
* ],
* }
* @param {string} [id=uuidv4()] - Unique id
* @param {string} type - text, email, password, number, tel, url
* @param {string} [type="text"] - text, email, password, number, tel, url
* @param {string} label - The label of the field. Can be a translation key or by default raw text.
* @param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.* @param {string} label
* @param {string} value
@ -79,7 +79,8 @@ const props = defineProps({
},
type: {
type: String,
required: true,
required: false,
default: "text",
},
attrs: {
type: Object,
@ -224,7 +225,7 @@ onMounted(() => {
props.placeholder
? $t(
props.placeholder,
$t('dashboard_placeholder', props.placeholder),
$t('dashboard_placeholder', props.placeholder)
)
: ''
"

View file

@ -5,12 +5,15 @@ import Text from "@components/Widget/Text.vue";
import Fields from "@components/Form/Fields.vue";
import Button from "@components/Widget/Button.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import Container from "@components/Widget/Container.vue";
import { TabulatorFull as Tabulator } from "tabulator-tables"; //import Tabulator library
import { useEqualStr } from "@utils/global.js";
import {
addColumnsSorter,
addColumnsWidth,
a18yTable,
applyTableFilter,
overrideDefaultFilters,
} from "@utils/tabulator.js";
import { useTableStore } from "@store/global.js";
@ -26,6 +29,16 @@ const props = defineProps({
required: true,
default: "table-component",
},
isStriped: {
type: Boolean,
required: false,
default: true,
},
filters: {
type: Array,
required: false,
default: [],
},
columns: {
type: Array,
required: true,
@ -78,6 +91,7 @@ const tableEl = ref(null); //reference to your table element
const table = reactive({
test: true,
instance: null,
filters: {},
columns: props.columns,
items: props.items,
customComponents: [],
@ -144,13 +158,15 @@ function addCustomComponent(type, values, elDOM) {
}
/**
* @name addComponentsFormats
* @description Add all custom components on a list to later add them to each tabulator cell.
* We are using the Tabulart.extendModule() that allow use to execute a custom function when we are matching a custom formatter.
* We need to define on rows the formatter that we want to use to render the custom component.
* @name extendTabulator
* @description Wrapper that will do some extend or override on the Tabulator instance:
* 1 - Add custom components to a list in order to render them and teleport them when formatting the cell.
* 2 - Add custom formatters for each custom components in order to force Tabulator to render empty string.
* 3 - Override default filters to add custom filters for each custom components (because we need to access a specific key in the props object).
* We are using the Tabular.extendModule() that allow use to do this.
* @returns {void}
*/
function addComponentsFormats() {
function extendTabulator() {
const formatOpts = {};
for (let i = 0; i < customComponents.length; i++) {
const module = customComponents[i];
@ -160,10 +176,27 @@ function addComponentsFormats() {
};
}
Tabulator.extendModule("format", "formatters", formatOpts);
Tabulator.extendModule("filter", "filters", overrideDefaultFilters());
}
/**
* @name filterTable
* @description We can't directly send the current filter input to filter the table because the Tabulator will filter everything at once.
* So we need to get the value and store on the table.filters dict to merge all filters and apply them at once.
* We will use the applyTableFilter() to apply the filters. Additionnal checks (like empty value) are done on the applyTableFilter() function.
* @param {object} tableInstance - The tableInstance is the current table instance.
* @param {object} filter - the filter dict is here the setting filter data
* @param {string} value - the value is the current value return by the filter input.
* @returns {void}
*/
function filterTable(filter, value = "") {
// Merge all filters
table.filters[filter.setting.id] = { ...filter, value: value };
applyTableFilter(table.instance, table.filters);
}
onMounted(() => {
addComponentsFormats();
extendTabulator();
table.instance = new Tabulator(tableEl.value, table.options);
table.instance.on("tableBuilt", () => {
table.instance.redraw();
@ -180,7 +213,19 @@ onUnmounted(() => {
</script>
<template>
<div data-is="table" ref="tableEl"></div>
<Container :containerClass="'layout-settings'">
<template v-for="filter in props.filters">
<Fields
:setting="filter.setting"
@inp="(value) => filterTable(filter, value)"
/>
</template>
</Container>
<div
:class="[props.isStriped ? 'striped' : '']"
data-is="table"
ref="tableEl"
></div>
<template
:key="table.customComponents"
v-for="comp in table.customComponents"

View file

@ -29,7 +29,45 @@ const tableStore = useTableStore();
const columns = [
{ title: "Name", field: "text", formatter: "text" },
{ title: "Icon", field: "icon", formatter: "icons" },
{
title: "Icon",
field: "icon",
formatter: "icons",
},
];
// Because we are going to use built-in filters, we can't use the Filter component
// So we need this format in order to create under the hood fields that will be linked to the tabulator filter
// We need to pass on the setting key the same props as the Fields component. For example a "=" tabulator filter will be used with a select field, this one need "values" array to work.
// type : Choose between available tabulator built-in filters ("keywords", "like", "!=", ">", "<", ">=", "<=", "in", "regex", "!=")
const filters = [
{
type: "like",
fields: ["text"],
setting: {
id: "test-input",
name: "test-input",
label: "Test input",
value: "",
inpType: "input",
columns: { pc: 3, tablet: 4, mobile: 12 },
},
},
{
type: "=",
fields: ["icon"],
setting: {
id: "test-select",
name: "test-select",
label: "Test select",
value: "all",
values: ["all", "box", "document"],
setting: { inpType: "input" },
inpType: "select",
columns: { pc: 3, tablet: 4, mobile: 12 },
onlyDown: true,
},
},
];
const items = [
@ -111,19 +149,20 @@ const builder = [
id: "table-test",
columns: columns,
items: items,
filters: filters,
},
},
],
},
];
onMounted(() => {
setTimeout(() => {
const table = tableStore.getTableById("table-test");
console.log(table);
table.setFilter("text", "keywords", "fesfs");
}, 1000);
});
// Interact with table instance from another component
// onMounted(() => {
// setTimeout(() => {
// const table = tableStore.getTableById("table-test");
// console.log(table);
// table.setFilter("text", "keywords", "fesfs");
// }, 1000);
// });
</script>
<template>

View file

@ -141,4 +141,192 @@ function _a18yFooter() {
}
}
export { addColumnsSorter, addColumnsWidth, a18yTable };
/**
* @name applyTableFilter
* @description Apply setting filter to the tabulator instance.
* We can't easily add filter after another, so we need to remove the previous one and add all new ones at once.
* @example { "filter-1" : { type: "keywords", fields: ["text", "icon"], setting: {}, value : "test" }}
* @param {object} tableInstance - The table instance to apply the filter.
* @param {object} filters - All filters to apply to the table.
* @param {string|number|regex} value - The value to apply to the filter.
* @returns {void}
*/
function applyTableFilter(tableInstance, filters) {
// loop on dict filters
const filtersSend = [];
for (const [key, filter] of Object.entries(filters)) {
const inpType = filter.setting.inpType;
const filterType = filter.type;
const value = filter.value;
const fields = filter.fields;
// Cases we don't want to apply filter
if (value === "") continue;
if (value === "all" && inpType === "select") continue;
// format value if needed
if (filterType === "number") value = +value;
if (filterType === "regex") value = new RegExp(value, "i");
for (let i = 0; i < fields.length; i++) {
filtersSend.push({ field: fields[i], type: filterType, value: value });
}
}
tableInstance.setFilter(filtersSend);
}
/**
* @name overrideDefaultFilters
* @description Create isomorphic filters for the tabulator library.
* Override default filters retrieving the right value for each custom components.
* @returns {object} - The custom filter function.
*/
function overrideDefaultFilters() {
//
const getRightKey = (rowValue) => {
const buttons = rowValue?.buttons
? rowValue?.buttons?.map((btn) => btn.text).join(" ")
: null;
return (
rowValue?.iconName?.toLowerCase() ||
rowValue?.value?.toLowerCase() ||
rowValue?.text.toLowerCase() ||
buttons ||
rowValue
);
};
return {
//equal to
"=": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value == filterVal ? true : false;
},
//less than
"<": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value < filterVal ? true : false;
},
//less than or equal to
"<=": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value <= filterVal ? true : false;
},
//greater than
">": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value > filterVal ? true : false;
},
//greater than or equal to
">=": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value >= filterVal ? true : false;
},
//not equal to
"!=": function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
return value != filterVal ? true : false;
},
regex: function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
if (typeof filterVal == "string") filterVal = new RegExp(filterVal);
return filterVal.test(value);
},
//contains the string
like: function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
if (filterVal === null || typeof filterVal === "undefined")
return value === filterVal ? true : false;
if (typeof value !== "undefined" && value !== null)
return (
String(value).toLowerCase().indexOf(filterVal.toLowerCase()) > -1
);
return false;
},
//contains the keywords
keywords: function (filterVal, rowVal, rowData, filterParams) {
let value = getRightKey(rowVal);
const keywords = filterVal
.toLowerCase()
.split(
typeof filterParams.separator === "undefined"
? " "
: filterParams.separator
);
value = String(
value === null || typeof value === "undefined" ? "" : value
).toLowerCase();
const matches = [];
keywords.forEach((keyword) => {
if (value.includes(keyword)) {
matches.push(true);
}
});
return filterParams.matchAll
? matches.length === keywords.length
: !!matches.length;
},
//starts with the string
starts: function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
if (filterVal === null || typeof filterVal === "undefined")
return value === filterVal ? true : false;
if (typeof value !== "undefined" && value !== null)
return String(value).toLowerCase().startsWith(filterVal.toLowerCase());
return false;
},
//ends with the string
ends: function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
if (filterVal === null || typeof filterVal === "undefined")
return value === filterVal ? true : false;
if (typeof value !== "undefined" && value !== null)
return String(value).toLowerCase().endsWith(filterVal.toLowerCase());
return false;
},
//in array
in: function (filterVal, rowVal, rowData, filterParams) {
const value = getRightKey(rowVal);
if (Array.isArray(filterVal))
return filterVal.length ? filterVal.indexOf(value) > -1 : true;
console.warn("Filter Error - filter value is not an array:", filterVal);
return false;
},
};
}
export {
addColumnsSorter,
addColumnsWidth,
a18yTable,
applyTableFilter,
overrideDefaultFilters,
};

View file

@ -4636,7 +4636,7 @@ body.tabulator-print-fullscreen-hide > *:not(.tabulator-print-fullscreen) {
}
.tabulator.striped .tabulator-row:nth-child(even) {
background-color: #f2f2f2;
background-color: #f6f6f6;
}
.tabulator.celled {
@ -4894,7 +4894,7 @@ body.tabulator-print-fullscreen-hide > *:not(.tabulator-print-fullscreen) {
border-color: #e5e7eb !important;
}
.dark .tabulator-row:nth-child(even) {
.dark .tabulator.striped .tabulator-row:nth-child(even) {
background-color: #4b5563 !important;
}
@ -5108,7 +5108,8 @@ body.tabulator-print-fullscreen-hide > *:not(.tabulator-print-fullscreen) {
.tabulator
.tabulator-header
.tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover,
.dark .tabulator-row.tabulator-selectable:hover {
.dark .tabulator-row.tabulator-selectable:hover,
.dark .tabulator.striped .tabulator-row:nth-child(even):hover {
background: #1f2937 !important;
}
}

File diff suppressed because one or more lines are too long