diff --git a/public/js/components/Stv/Studentenverwaltung/List.js b/public/js/components/Stv/Studentenverwaltung/List.js
index e70c48d83..3079eb080 100644
--- a/public/js/components/Stv/Studentenverwaltung/List.js
+++ b/public/js/components/Stv/Studentenverwaltung/List.js
@@ -1,7 +1,7 @@
import { CoreFilterCmpt } from "../../filter/Filter.js";
import ListNew from "./List/New.js";
import CoreTag from "../../Tag/Tag.js";
-import { tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js";
+import { buildTagHeaderFilterExpression, buildTagOptionsFromRows, customTagFilter, setTagHeaderFilterValue, tagHeaderFilter } from "../../../tabulator/filters/extendedHeaderFilter.js";
import {
addTagInTable,
deleteTagInTable,
@@ -64,8 +64,11 @@ export default {
}
return {
- initialTagData: [],
- selectedTagOptions: [],
+ tagFilterState: {
+ initialOptions: [],
+ selectedOptions: [],
+ },
+ selectedColumnValues: [],
tabulatorOptions: {
columns: [
{ title: "UID", field: "uid", headerFilter: true },
@@ -398,7 +401,7 @@ export default {
{
event: "dataProcessed",
handler: (data) => {
- console.log("dataProcessed", data);
+ //console.log("dataProcessed", data);
this.getAllRows();
this.autoSelectRows(data);
},
@@ -406,7 +409,7 @@ export default {
{
event: "dataLoaded",
handler: (data) => {
- console.log("dataLoaded", data);
+ //console.log("dataLoaded", data);
if (Array.isArray(data)) {
this.count = data.length;
this.allPrestudents = data.map((item) => item.prestudent_id);
@@ -414,12 +417,13 @@ export default {
this.count = 0;
this.allPrestudents = [];
}
+ this.refreshTagHeaderFilterInitialOptions(Array.isArray(data) ? data : []);
},
},
{
event: "dataFiltered",
handler: (filters, rows) => {
- console.log("dataFiltered", filters, rows);
+ //console.log("dataFiltered", filters, rows);
this.filteredcount = rows.length;
},
},
@@ -430,14 +434,14 @@ export default {
{
event: "dataTreeRowExpanded",
handler: (data) => {
- console.log("dataTreeRowExpanded", data);
+ //console.log("dataTreeRowExpanded", data);
this.getExpandedRows();
},
},
{
event: "dataTreeRowCollapsed",
handler: (data) => {
- console.log("dataTreeRowCollapsed", data);
+ //console.log("dataTreeRowCollapsed", data);
this.getExpandedRows();
},
},
@@ -448,7 +452,7 @@ export default {
{
event: "columnWidth",
handler: (column) => {
- console.log("columnWidth", column);
+ //console.log("columnWidth", column);
if (column.getField() !== "tags") return;
column.getCells().forEach((cell) => {
@@ -465,7 +469,6 @@ export default {
selectedcount: 0,
//tags
expanded: [],
- selectedColumnValues: [],
tagEndpoint: ApiTag,
currentEndpoint: null,
headerFilterActive: false,
@@ -561,175 +564,13 @@ export default {
title: "Tags",
field: "tags",
tooltip: false,
- headerFilter: "list",
+ headerFilter: customTagFilter,
headerFilterParams: {
- valuesLookup: () => {
- console.log("headerFilterParams.valuesLookup called");
- const options = new Map();
-
- this.$refs.table.tabulator.getData("active").forEach((row) => {
- let tags = row.tags;
-
- if (typeof tags === "string") {
- try {
- tags = JSON.parse(tags);
- } catch {
- return;
- }
- }
-
- if (!Array.isArray(tags)) return;
-
- tags
- .filter((tag) => tag && tag.done !== true)
- .forEach((tag) => {
- options.set(tag.beschreibung, {
- label: tag.beschreibung,
- value: tag.beschreibung,
- style: tag.style,
- });
- });
- });
-
- return [
- {
- isHeader: true,
- label: "test",
- subOptions: [...options.values()],
- },
- ];
- },
- itemFormatter: (value, label, item) => {
- console.log("value, label, item", value, label, item);
- if (!item.isHeader) return "";
-
- let table = document.createElement("table");
- table.className = "table table-sm mb-0";
- table.appendChild(document.createElement("thead"));
- table
- .querySelector("thead")
- .appendChild(document.createElement("tr"));
-
- let firstHeading = document.createElement("th");
- firstHeading.innerHTML = "" + value + "";
- table.querySelector("tr").appendChild(firstHeading);
-
- let secondHeading = document.createElement("th");
- secondHeading.innerHTML = "AND";
- table.querySelector("tr").appendChild(secondHeading);
-
- let thirdHeading = document.createElement("th");
- thirdHeading.innerHTML = "OR";
- table.querySelector("tr").appendChild(thirdHeading);
-
- let fourthHeading = document.createElement("th");
- fourthHeading.innerHTML = "NOT";
- table.querySelector("tr").appendChild(fourthHeading);
-
- table.appendChild(document.createElement("tbody"));
-
- item.subOptions.forEach((option) => {
- let row = document.createElement("tr");
-
- let firstColumn = document.createElement("td");
-
- let optionLabel = document.createElement("span");
- optionLabel.className = "tag " + option.style;
- optionLabel.innerText = option.label;
- firstColumn.appendChild(optionLabel);
- row.appendChild(firstColumn);
-
- let secondColumn = document.createElement("td");
- let firstCheckbox = document.createElement("input");
- firstCheckbox.type = "checkbox";
- firstCheckbox.label = "test";
- firstCheckbox.style.cursor = "pointer";
- firstCheckbox.checked = this.selectedTagOptions.some(
- (tag) => tag.value === option.value && tag.connector === "AND",
- );
- firstCheckbox.addEventListener("click", (event) => {
- event.stopPropagation();
- if (event.target.checked) {
- this.selectedTagOptions.push({
- label: option.label,
- value: option.value,
- connector: "AND",
- });
- } else {
- this.selectedTagOptions = this.selectedTagOptions.filter(
- (tag) =>
- tag.value !== option.value || tag.connector !== "AND",
- );
- }
- console.log("selectedTagOptions", this.selectedTagOptions);
- });
- secondColumn.appendChild(firstCheckbox);
- row.appendChild(secondColumn);
-
- let thirdColumn = document.createElement("td");
- let secondCheckbox = document.createElement("input");
- secondCheckbox.type = "checkbox";
- secondCheckbox.label = "test";
- secondCheckbox.style.cursor = "pointer";
- secondCheckbox.checked = this.selectedTagOptions.some(
- (tag) => tag.value === option.value && tag.connector === "OR",
- );
- secondCheckbox.addEventListener("click", (event) => {
- event.stopPropagation();
- if (event.target.checked) {
- this.selectedTagOptions.push({
- label: option.label,
- value: option.value,
- connector: "OR",
- });
- } else {
- this.selectedTagOptions = this.selectedTagOptions.filter(
- (tag) =>
- tag.value !== option.value || tag.connector !== "OR",
- );
- }
- });
- thirdColumn.appendChild(secondCheckbox);
- row.appendChild(thirdColumn);
-
- let fourthColumn = document.createElement("td");
- let thirdCheckbox = document.createElement("input");
- thirdCheckbox.type = "checkbox";
- thirdCheckbox.label = "test";
- thirdCheckbox.style.cursor = "pointer";
- thirdCheckbox.checked = this.selectedTagOptions.some(
- (tag) => tag.value === option.value && tag.connector === "NOT",
- );
- thirdCheckbox.addEventListener("click", (event) => {
- event.stopPropagation();
- console.log(event.target.checked);
- if (event.target.checked) {
- this.selectedTagOptions.push({
- label: option.label,
- value: option.value,
- connector: "NOT",
- });
- } else {
- this.selectedTagOptions = this.selectedTagOptions.filter(
- (tag) =>
- tag.value !== option.value || tag.connector !== "NOT",
- );
- }
- });
- fourthColumn.appendChild(thirdCheckbox);
- row.appendChild(fourthColumn);
-
- row.addEventListener("click", (event) => {
- event.stopPropagation();
- });
-
- table.querySelector("tbody").appendChild(row);
- });
- return table;
- },
listOnEmpty: true,
autocomplete: true,
sort: "asc",
+ initialOptions: this.tagFilterState.initialOptions,
+ selectedOptions: this.tagFilterState.selectedOptions,
},
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: { field: "tags" },
@@ -746,33 +587,18 @@ export default {
this.translateTabulator();
}
},
- selectedTagOptions: {
+ "tagFilterState.selectedOptions": {
handler() {
- let combinedFilterStatement = null;
+ const selectedOptions = this.tagFilterState.selectedOptions;
+ const combinedFilterStatement = buildTagHeaderFilterExpression(
+ selectedOptions,
+ );
- this.selectedTagOptions.forEach((tagOption) => {
- const { value, connector } = tagOption;
-
- let mappedConnector = connector === "AND" ? "&&" : connector === "OR" ? "||" : "!";
- if (connector === "NOT") {
- combinedFilterStatement = combinedFilterStatement
- ? `${combinedFilterStatement} && ${mappedConnector + value}`
- : value;
- }
- else {
- combinedFilterStatement = combinedFilterStatement
- ? `${combinedFilterStatement} ${mappedConnector} ${value}`
- : value;
- }
- });
-
- this.$refs.table.tabulator.setHeaderFilterValue(
- "tags",
+ setTagHeaderFilterValue(
+ this.$refs.table.tabulator,
combinedFilterStatement,
);
- this.$emit("filterActive", this.selectedTagOptions.length > 0);
- console.log("selectedTagOptions changed:");
- console.log(combinedFilterStatement);
+ this.$emit("filterActive", selectedOptions.length > 0);
},
deep: true,
},
@@ -868,7 +694,7 @@ export default {
this.$refs.new.open();
},
rowSelectionChanged(data, rows, selected, deselected) {
- console.log("rowSelectionChanged", data, rows, selected, deselected);
+ //console.log("rowSelectionChanged", data, rows, selected, deselected);
this.selectedcount = data.length;
if (selected.length > 0 || deselected.length > 0) {
@@ -915,8 +741,8 @@ export default {
updateUrl(endpoint, first) {
this.lastSelected = first ? undefined : this.selected;
- /* console.log('function param endpoint: ' + JSON.stringify(endpoint));
- console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/
+ /* //console.log('function param endpoint: ' + JSON.stringify(endpoint));
+ //console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));*/
if (endpoint === undefined && this.currentEndpoint === null) {
endpoint = { url: "" };
@@ -1021,7 +847,7 @@ export default {
}
},
handleRowClick(e, row) {
- console.log("handleRowClick", e, row);
+ //console.log("handleRowClick", e, row);
// TODO(chris): this should be in the filter component
if (this.focusObj) {
let el = row.getElement();
@@ -1035,12 +861,34 @@ export default {
//methods tags
addedTag(addedTag) {
addTagInTable(addedTag, this.allRows, "prestudent_id");
+ this.refreshTagHeaderFilterInitialOptions();
},
deletedTag(deletedTag) {
+ console.log("deletedTag", deletedTag);
deleteTagInTable(deletedTag, this.allRows);
+ this.refreshTagHeaderFilterInitialOptions();
},
updatedTag(updatedTag) {
updateTagInTable(updatedTag, this.allRows);
+ this.refreshTagHeaderFilterInitialOptions();
+ },
+ refreshTagHeaderFilterInitialOptions(rows) {
+ const data =
+ rows || this.$refs.table?.tabulator?.getData() || [];
+
+ const options = buildTagOptionsFromRows(data);
+ this.tagFilterState.initialOptions.splice(
+ 0,
+ this.tagFilterState.initialOptions.length,
+ ...options,
+ );
+
+ const availableValues = new Set(options.map((option) => option.value));
+ for (let i = this.tagFilterState.selectedOptions.length - 1; i >= 0; i--) {
+ if (!availableValues.has(this.tagFilterState.selectedOptions[i].value)) {
+ this.tagFilterState.selectedOptions.splice(i, 1);
+ }
+ }
},
getAllRows() {
this.allRows = this.$refs.table.tabulator.getRows();
@@ -1053,7 +901,7 @@ export default {
this.headerFilterActive = filterActive;
},
handleMouseDown(e, row) {
- console.log("handleMouseDown", e, row);
+ //console.log("handleMouseDown", e, row);
let data = row.getData();
let id = data.uid ?? data.prestudent_id ?? data.person_id;
@@ -1065,12 +913,12 @@ export default {
isAlreadySelected && this.selected?.length ? this.selected : [data];
},
handleDataLoading() {
- console.log("handleDataLoading");
+ //console.log("handleDataLoading");
this.oldScrollLeft = this.$refs.table.tabulator.rowManager.scrollLeft;
this.oldScrollTop = this.$refs.table.tabulator.rowManager.scrollTop;
},
handleRenderComplete() {
- console.log("handleRenderComplete");
+ //console.log("handleRenderComplete");
const table = this.$refs.table.tabulator.element.querySelector(
".tabulator-tableholder",
);
diff --git a/public/js/tabulator/filters/extendedHeaderFilter.js b/public/js/tabulator/filters/extendedHeaderFilter.js
index 148ddb7fa..d9f6faf7a 100644
--- a/public/js/tabulator/filters/extendedHeaderFilter.js
+++ b/public/js/tabulator/filters/extendedHeaderFilter.js
@@ -1,163 +1,456 @@
-function parseFilterExpression(expression)
-{
- const collections = [];
+let activeTagDropdownCleanup = null;
+const TAG_FILTER_CONNECTORS = {
+ AND: {
+ key: "AND",
+ label: "AND",
+ operator: "&&",
+ },
+ OR: {
+ key: "OR",
+ label: "OR",
+ operator: "||",
+ },
+ NOT: {
+ key: "NOT",
+ label: "NOT",
+ operator: "!",
+ },
+};
+const TAG_FILTER_CONNECTOR_ORDER = [
+ TAG_FILTER_CONNECTORS.AND,
+ TAG_FILTER_CONNECTORS.OR,
+ TAG_FILTER_CONNECTORS.NOT,
+];
- try {
- const orParts = expression.split('||').map(part => part.trim());
+export function setTagHeaderFilterValue(tabulator, value) {
+ tabulator?.setHeaderFilterValue("tags", value);
- orParts.forEach(part => {
+ const input = tabulator
+ ?.getColumn("tags")
+ ?.getElement()
+ ?.querySelector(".tabulator-header-filter input");
- const andParts = part.split('&&').map(p => p.trim());
-
- const collection = { positives: [], negatives: [] };
-
- andParts.forEach(term => {
-
- const comparisonMatch = term.match(/^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/);
-
- if (comparisonMatch)
- {
- const operator = comparisonMatch[1];
- const numberStr = comparisonMatch[2].replace(',', '.');
- const number = parseFloat(numberStr);
- collection.positives.push({ type: 'comparison', operator, number });
- }
- else if (term.startsWith('!'))
- {
- const excludeTerm = term.substring(1).trim().replace(/\*/g, '.*');
- collection.negatives.push({ type: 'regex', regex: new RegExp(excludeTerm, 'i') });
- }
- else
- {
- const includeTerm = term.replace(/\*/g, '.*');
- collection.positives.push({ type: 'regex', regex: new RegExp(includeTerm, 'i') });
- }
- });
- collections.push(collection);
- });
- } catch (e) {}
- return collections;
+ if (input) {
+ input.value = value ?? "";
+ }
}
-export function extendedHeaderFilter(headerValue, rowValue, rowData, filterParams)
-{
- const fields = Array.isArray(filterParams?.field)
- ? filterParams.field
- : [filterParams?.field];
+export function buildTagHeaderFilterExpression(selectedTagOptions) {
+ let expression = null;
- if (fields.length > 1 && rowData)
- {
- rowValue = fields
- .map(f => rowData[f] ?? '')
- .filter(Boolean)
- .join(' ');
- }
- if (typeof headerValue === 'boolean')
- {
- return rowValue === headerValue;
- }
+ selectedTagOptions.forEach(({ value, connector }) => {
+ const connectorDef = TAG_FILTER_CONNECTORS[connector];
+ if (!connectorDef) return;
- const collections = parseFilterExpression(headerValue);
+ if (connectorDef.key === TAG_FILTER_CONNECTORS.NOT.key) {
+ expression = expression
+ ? `${expression} ${TAG_FILTER_CONNECTORS.AND.operator} ${connectorDef.operator}${value}`
+ : `${connectorDef.operator}${value}`;
+ } else {
+ expression = expression
+ ? `${expression} ${connectorDef.operator} ${value}`
+ : value;
+ }
+ });
- function matchValue(value)
- {
- try {
- return collections.some(collection => {
-
- let positives = collection.positives.length === 0 || collection.positives.every(condition => {
-
- if (condition.type === 'comparison')
- {
- let value = parseFloat(rowValue);
- if (isNaN(value)) return false;
-
- switch (condition.operator) {
- case '<':
- return value < condition.number;
- case '>':
- return value > condition.number;
- case '<=':
- return value <= condition.number;
- case '>=':
- return value >= condition.number;
- case '=':
- return value === condition.number;
- case '!=':
- return value !== condition.number;
- default:
- return false;
- }
- }
- else if (condition.type === 'regex')
- {
- return condition.regex.test(rowValue);
- }
- return false;
- });
-
- let negatives = collection.negatives.every(condition => {
- return !condition.regex.test(rowValue);
- });
-
- return positives && negatives;
- });
- } catch (e) {
-
- }
- }
-
- if (matchValue(rowValue))
- return true;
-
- if (rowData && filterParams)
- {
- const childrenField = filterParams?.children || '_children';
- const field = filterParams?.field;
-
- const children = rowData[childrenField];
- if (Array.isArray(children))
- {
- for (let child of children)
- {
- let childValue = child[field];
- if (extendedHeaderFilter(headerValue, childValue, child, filterParams))
- {
- return true;
- }
- }
- }
- }
-
- return false;
+ return expression;
}
-export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams)
-{
- console.log('tagHeaderFilter', headerValue, rowValue, rowData, filterParams);
- let data;
- console.log('rowValue', rowValue);
- try {
- data = typeof rowValue === 'string' ? JSON.parse(rowValue) : rowValue;
- } catch (error) {
- return false;
- }
- let combinedText;
+export function buildTagOptionsFromRows(rows = []) {
+ const options = new Map();
- if (Array.isArray(data))
- {
- combinedText = data
- .filter(item => item?.done !== true)
- .map(item => `${item?.beschreibung} ${item?.notiz}`)
- .join(' ');
- }
- else if (typeof data === 'object' && data !== null)
- {
- combinedText = data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : '';
- }
- else
- {
- combinedText = String(data);
- }
+ rows.forEach((row) => {
+ let tags = row.tags;
- return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams)
-}
\ No newline at end of file
+ if (typeof tags === "string") {
+ try {
+ tags = JSON.parse(tags);
+ } catch {
+ return;
+ }
+ }
+
+ if (!Array.isArray(tags)) return;
+
+ tags
+ .filter((tag) => tag && tag.done !== true)
+ .forEach((tag) => {
+ options.set(tag.beschreibung, {
+ label: tag.beschreibung,
+ value: tag.beschreibung,
+ style: tag.style,
+ });
+ });
+ });
+
+ return [...options.values()];
+}
+
+export function customTagFilter(cell, onRendered, success, cancel, params) {
+ const container = document.createElement("div");
+ container.style.width = "100%";
+
+ const input = document.createElement("input");
+ input.style.width = "100%";
+ input.type = "text";
+ let dropdownMenu = null;
+
+ Object.defineProperty(container, "value", {
+ get: () => input.value,
+ set: (value) => {
+ input.value = value ?? "";
+ },
+ });
+
+ const positionDropdown = () => {
+ if (!dropdownMenu) return;
+
+ const rect = input.getBoundingClientRect();
+ const width = Math.min(Math.max(rect.width, 360), window.innerWidth - 16);
+ const left = Math.min(
+ Math.max(rect.left, 8),
+ window.innerWidth - width - 8,
+ );
+
+ dropdownMenu.style.position = "fixed";
+ dropdownMenu.style.left = `${left}px`;
+ dropdownMenu.style.top = `${rect.bottom + 2}px`;
+ dropdownMenu.style.width = `${width}px`;
+ dropdownMenu.style.maxHeight = "50vh";
+ dropdownMenu.style.overflowY = "auto";
+ dropdownMenu.style.zIndex = "2000";
+ };
+
+ const closeDropdown = () => {
+ if (dropdownMenu) {
+ dropdownMenu.remove();
+ dropdownMenu = null;
+ }
+
+ if (activeTagDropdownCleanup === closeDropdown) {
+ activeTagDropdownCleanup = null;
+ }
+
+ window.removeEventListener("scroll", positionDropdown, true);
+ window.removeEventListener("resize", positionDropdown);
+ document.removeEventListener("mousedown", handleOutsideClick, true);
+ };
+
+ const handleOutsideClick = (event) => {
+ if (
+ dropdownMenu?.contains(event.target) ||
+ container.contains(event.target)
+ ) {
+ return;
+ }
+
+ closeDropdown();
+ };
+
+ const clearFilter = () => {
+ input.value = "";
+
+ if (Array.isArray(params.selectedOptions)) {
+ params.selectedOptions.splice(
+ 0,
+ params.selectedOptions.length,
+ );
+ }
+
+ success("");
+ closeDropdown();
+ };
+
+ input.addEventListener("input", () => {
+ success(input.value);
+ });
+ input.addEventListener("change", () => {
+ success(input.value);
+ });
+ input.addEventListener("keydown", (event) => {
+ if (event.key !== "Escape") return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ clearFilter();
+ });
+ input.addEventListener("focus", (e) => {
+ e.stopPropagation();
+
+ if (activeTagDropdownCleanup) {
+ activeTagDropdownCleanup();
+ }
+
+ dropdownMenu = getRebuiltDropdown(
+ params.initialOptions,
+ params.selectedOptions,
+ );
+ dropdownMenu.addEventListener("mousedown", (event) => {
+ event.stopPropagation();
+ });
+
+ document.body.appendChild(dropdownMenu);
+ activeTagDropdownCleanup = closeDropdown;
+
+ positionDropdown();
+
+ window.addEventListener("scroll", positionDropdown, true);
+ window.addEventListener("resize", positionDropdown);
+ document.addEventListener("mousedown", handleOutsideClick, true);
+ });
+
+ input.addEventListener("blur", (e) => {
+ });
+
+ container.appendChild(input);
+
+ return container;
+}
+
+function getRebuiltDropdown(initialOptions, selectedOptions) {
+ let dropdownTable = generateDropdownTable(
+ initialOptions || [],
+ selectedOptions || [],
+ );
+
+ const menu = document.createElement("div");
+ menu.className = "dropdown-menu show";
+
+ menu.appendChild(dropdownTable);
+
+ return menu;
+}
+
+function generateDropdownTable(options, selectedTagOptions) {
+ const table = document.createElement("table");
+ table.className = "table table-sm mb-0";
+
+ // HEADER
+ const thead = document.createElement("thead");
+ const headRow = document.createElement("tr");
+
+ const headers = [
+ "Tag",
+ ...TAG_FILTER_CONNECTOR_ORDER.map((connector) => connector.label),
+ ];
+
+ headers.forEach((h) => {
+ const th = document.createElement("th");
+ th.innerHTML = `${h}`;
+ headRow.appendChild(th);
+ });
+
+ thead.appendChild(headRow);
+ table.appendChild(thead);
+
+ // BODY
+ const tbody = document.createElement("tbody");
+ options.forEach((option) => {
+ const row = document.createElement("tr");
+
+ // LABEL
+ const labelTd = document.createElement("td");
+ const label = document.createElement("span");
+ label.className = "tag " + option.style;
+ label.innerText = option.label;
+ labelTd.appendChild(label);
+ row.appendChild(labelTd);
+
+ function makeCheckbox(connector) {
+ const td = document.createElement("td");
+ const cb = document.createElement("input");
+
+ cb.type = "checkbox";
+ cb.style.cursor = "pointer";
+
+ cb.checked = selectedTagOptions.some(
+ (t) => t.value === option.value && t.connector === connector.key,
+ );
+
+ cb.addEventListener("click", (event) => {
+ event.stopPropagation();
+
+ if (event.target.checked) {
+ selectedTagOptions.push({
+ label: option.label,
+ value: option.value,
+ connector: connector.key,
+ });
+ } else {
+ const idx = selectedTagOptions.findIndex(
+ (t) => t.value === option.value && t.connector === connector.key,
+ );
+
+ if (idx !== -1) {
+ selectedTagOptions.splice(idx, 1);
+ }
+ }
+
+ });
+
+ td.appendChild(cb);
+ return td;
+ }
+
+ TAG_FILTER_CONNECTOR_ORDER.forEach((connector) => {
+ row.appendChild(makeCheckbox(connector));
+ });
+
+ tbody.appendChild(row);
+ });
+
+ table.appendChild(tbody);
+
+ return table;
+}
+
+function parseFilterExpression(expression) {
+ const collections = [];
+
+ try {
+ const orParts = expression.split("||").map((part) => part.trim());
+
+ orParts.forEach((part) => {
+ const andParts = part.split("&&").map((p) => p.trim());
+
+ const collection = { positives: [], negatives: [] };
+
+ andParts.forEach((term) => {
+ const comparisonMatch = term.match(
+ /^(<=|>=|<|>|=|!=)\s*(-?\d+(?:[.,]\d+)?)$/,
+ );
+
+ if (comparisonMatch) {
+ const operator = comparisonMatch[1];
+ const numberStr = comparisonMatch[2].replace(",", ".");
+ const number = parseFloat(numberStr);
+ collection.positives.push({ type: "comparison", operator, number });
+ } else if (term.startsWith("!")) {
+ const excludeTerm = term.substring(1).trim().replace(/\*/g, ".*");
+ collection.negatives.push({
+ type: "regex",
+ regex: new RegExp(excludeTerm, "i"),
+ });
+ } else {
+ const includeTerm = term.replace(/\*/g, ".*");
+ collection.positives.push({
+ type: "regex",
+ regex: new RegExp(includeTerm, "i"),
+ });
+ }
+ });
+ collections.push(collection);
+ });
+ } catch (e) {}
+ return collections;
+}
+
+export function extendedHeaderFilter(
+ headerValue,
+ rowValue,
+ rowData,
+ filterParams,
+) {
+ const fields = Array.isArray(filterParams?.field)
+ ? filterParams.field
+ : [filterParams?.field];
+
+ if (fields.length > 1 && rowData) {
+ rowValue = fields
+ .map((f) => rowData[f] ?? "")
+ .filter(Boolean)
+ .join(" ");
+ }
+ if (typeof headerValue === "boolean") {
+ return rowValue === headerValue;
+ }
+
+ const collections = parseFilterExpression(headerValue);
+
+ function matchValue(value) {
+ try {
+ return collections.some((collection) => {
+ let positives =
+ collection.positives.length === 0 ||
+ collection.positives.every((condition) => {
+ if (condition.type === "comparison") {
+ let value = parseFloat(rowValue);
+ if (isNaN(value)) return false;
+
+ switch (condition.operator) {
+ case "<":
+ return value < condition.number;
+ case ">":
+ return value > condition.number;
+ case "<=":
+ return value <= condition.number;
+ case ">=":
+ return value >= condition.number;
+ case "=":
+ return value === condition.number;
+ case "!=":
+ return value !== condition.number;
+ default:
+ return false;
+ }
+ } else if (condition.type === "regex") {
+ return condition.regex.test(rowValue);
+ }
+ return false;
+ });
+
+ let negatives = collection.negatives.every((condition) => {
+ return !condition.regex.test(rowValue);
+ });
+
+ return positives && negatives;
+ });
+ } catch (e) {}
+ }
+
+ if (matchValue(rowValue)) return true;
+
+ if (rowData && filterParams) {
+ const childrenField = filterParams?.children || "_children";
+ const field = filterParams?.field;
+
+ const children = rowData[childrenField];
+ if (Array.isArray(children)) {
+ for (let child of children) {
+ let childValue = child[field];
+ if (
+ extendedHeaderFilter(headerValue, childValue, child, filterParams)
+ ) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams) {
+ let data;
+
+ try {
+ data = typeof rowValue === "string" ? JSON.parse(rowValue) : rowValue;
+ } catch (error) {
+ return false;
+ }
+
+ let combinedText;
+
+ if (Array.isArray(data)) {
+ combinedText = data
+ .filter((item) => item?.done !== true)
+ .map((item) => `${item?.beschreibung} ${item?.notiz}`)
+ .join(" ");
+ } else if (typeof data === "object" && data !== null) {
+ combinedText =
+ data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : "";
+ } else {
+ combinedText = String(data);
+ }
+
+ return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams);
+}