mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-25 08:34:29 +00:00
add custom tag filter
This commit is contained in:
@@ -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 "<span class='m-0 p-0'></span>";
|
||||
|
||||
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 = "<strong>" + value + "</strong>";
|
||||
table.querySelector("tr").appendChild(firstHeading);
|
||||
|
||||
let secondHeading = document.createElement("th");
|
||||
secondHeading.innerHTML = "<strong>AND</strong>";
|
||||
table.querySelector("tr").appendChild(secondHeading);
|
||||
|
||||
let thirdHeading = document.createElement("th");
|
||||
thirdHeading.innerHTML = "<strong>OR</strong>";
|
||||
table.querySelector("tr").appendChild(thirdHeading);
|
||||
|
||||
let fourthHeading = document.createElement("th");
|
||||
fourthHeading.innerHTML = "<strong>NOT</strong>";
|
||||
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",
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
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 = `<strong>${h}</strong>`;
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user