awesome-data-view
Version:
388 lines (339 loc) • 15.6 kB
text/typescript
import Pipelined from "../interface/Pipelined";
import PopupButton from "../helpers/PopupButton";
import hexToRGBA from "../helpers/hexToRGB";
import SetStyle from "../helpers/SetStyle";
import paleColor from "../helpers/PaleColor";
import PaleColor from "../helpers/PaleColor";
type WhereClause = "AND" | "OR"
type Operator =
"equal to"
| "in"
| "not equal to"
| "not in"
| "between"
| "greater than"
| "less than"
| "greater than or equal"
| "less than or equal"
| "contains"
interface Where {
type: WhereClause,
column?: string,
value?: string | number | Array<string | number>,
operator?: Operator,
filter?: FilterWhere,
}
export default class FilterWhere implements Pipelined {
protected _conditions: Array<Where> = [];
render(buttonStyle = {
background: 'transparent',
color: '#000',
border: '1px solid #323232'
}, popupStyle = {
background: 'white',
color: '#000',
border: '1px solid #323232',
transformOrigin: 'center'
}, onChange: Function) {
let { button, div, placeholder } = PopupButton(buttonStyle, popupStyle);
let icon = `<svg xmlns="http://www.w3.org/2000/svg" style="width:16px;" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>`;
placeholder.innerHTML = `${icon} Filter`
div.appendChild(this.renderFilterWhere(this, 0, popupStyle));
button.addEventListener('click-outside', () => {
onChange();
})
return button;
}
protected renderCondition(condition: Where, popupStyle = {
background: 'white',
color: '#000',
border: '1px solid #323232'
}, level: number, onRemove: Function, isFirst: boolean) {
let style = {
background: PaleColor(hexToRGBA(popupStyle.background, 1), level / 10),
color: popupStyle.color,
padding: '3px 6px',
outline: 'none',
borderBottom: `0.5px solid ${popupStyle.color}`
};
if (condition.filter) {
let conditionFilter = this.renderFilterWhere(condition.filter, level + 1, popupStyle);
conditionFilter.style.margin = '6px'
let whereType = document.createElement('select');
whereType.style.width = 'fit-content';
["AND", "OR"].forEach((value) => {
let option = document.createElement('option');
option.value = value;
option.text = value;
if (value === condition.type) {
option.selected = true;
}
whereType.appendChild(option);
})
whereType.addEventListener('change', () => {
condition.type = (whereType.value as WhereClause);
})
SetStyle(whereType, style)
conditionFilter.insertBefore(whereType, conditionFilter.children[0]);
return conditionFilter
}
let conditionBox = document.createElement('div');
conditionBox.style.display = 'flex';
conditionBox.style.alignItems = 'bottom';
conditionBox.style.padding = '4px';
conditionBox.style.columnGap = '20px';
let attributeName = document.createElement('input');
attributeName.placeholder = 'Column name';
attributeName.value = condition.column!;
attributeName.addEventListener('change', () => {
condition.column = attributeName.value;
})
let operator = document.createElement('select');
["equal to", "in", "not equal to", "not in", "between", "greater than", "less than", "greater than or equal", "less than or equal", "contains"]
.forEach((value) => {
let option = document.createElement('option');
option.value = value;
option.text = value;
if (value === condition.operator) {
option.selected = true;
}
operator.appendChild(option);
})
operator.addEventListener('change', () => {
condition.operator = (operator.value as Operator);
})
let value = document.createElement('input');
value.placeholder = 'comma separated value'
value.value = condition.value instanceof Array ? condition.value.join(',') : condition.value!.toString();
value.addEventListener('change', () => {
// split the values by comma and assign
condition.value = ['in', 'not in', 'between'].includes(condition.operator!) ? value.value.split(',') : value.value;
})
SetStyle(attributeName, style)
SetStyle(operator, style)
SetStyle(value, style)
let removeButton = document.createElement('button');
removeButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
onRemove();
// since there are "add condition" and "add condition group",
if (level > 0 && conditionBox.parentElement?.children.length === 4) {
conditionBox.parentElement.remove();
} else {
conditionBox.remove();
}
})
removeButton.innerHTML = `<svg style="width: 20px; color:#ff5050;" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-minus"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/></svg>`
if (!isFirst) {
let whereType = document.createElement('select');
["AND", "OR"].forEach((value) => {
let option = document.createElement('option');
option.value = value;
option.text = value;
if (value === condition.type) {
option.selected = true;
}
whereType.appendChild(option);
})
whereType.addEventListener('change', () => {
condition.type = (whereType.value as WhereClause);
})
SetStyle(whereType, style)
conditionBox.appendChild(whereType);
}
conditionBox.appendChild(attributeName);
conditionBox.appendChild(operator);
conditionBox.appendChild(value);
conditionBox.appendChild(removeButton);
return conditionBox
}
protected renderFilterWhere(filterWhere: FilterWhere, level: number, popupStyle = {
background: 'white',
color: '#000',
border: '1px solid #323232'
}) {
let conditions = filterWhere._conditions;
popupStyle.background = hexToRGBA(popupStyle.background, 1 - (level / 10));
let container = document.createElement('div');
SetStyle(container, popupStyle)
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.rowGap = '12px';
// container.style.width = '100%';
container.style.padding = '8px'
if (level === 0) {
container.style.border = 'none'
}
container.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
})
conditions.forEach((condition, i) => {
container.appendChild(this.renderCondition(condition, popupStyle, level, () => filterWhere._conditions = filterWhere._conditions.filter((_condition) => _condition !== condition), i === 0))
})
const addButton = () => {
let button = document.createElement('button');
button.style.background = paleColor(popupStyle.background, 0.1 + (0.1 * level))
button.style.padding = "3px 6px";
button.style.borderRadius = "4px";
button.style.display = "flex";
button.style.alignItems = "center";
button.style.columnGap = "2px";
button.style.fontSize = "12px";
button.style.width = 'fit-content'
return button;
}
const addConditionButton = addButton();
addConditionButton.innerHTML = `<svg style="width: 10px;" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg> Add Condition`;
addConditionButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
let condition: Where = {
type: 'AND',
operator: "equal to",
column: '',
value: ''
}
filterWhere._conditions.push(condition);
container.insertBefore(
this.renderCondition(condition, popupStyle, level, () => filterWhere._conditions = filterWhere._conditions.filter((_condition) => _condition !== condition), filterWhere._conditions.length === 1),
addConditionButton)
})
const addFilterWhereButton = addButton();
addFilterWhereButton.innerHTML = `<svg style="width: 10px;" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg> Add Condition Group`;
addFilterWhereButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
let _condition: Where = {
type: 'AND',
operator: "equal to",
column: '',
value: ''
}
let fw = (new FilterWhere)
fw._conditions.push(_condition)
let condition: Where = {
type: 'AND',
filter: fw
}
filterWhere._conditions.push(condition);
container.insertBefore(
this.renderCondition(condition, popupStyle, level + 1, () => filterWhere._conditions = filterWhere._conditions.filter((_condition) => _condition !== condition), filterWhere._conditions.length === 1),
addConditionButton)
})
container.appendChild(addConditionButton);
container.appendChild(addFilterWhereButton);
return container;
// container
}
and(columnOrFilter: string | FilterWhere, value?: string | number | Array<string | number>, operator: Operator = "equal to") {
let where: Where = {
'type': 'AND'
}
if (columnOrFilter instanceof FilterWhere) {
where.filter = columnOrFilter;
} else {
where.column = columnOrFilter;
where.value = value;
where.operator = operator;
}
this._conditions.push(where)
return this;
}
or(columnOrFilter: string | FilterWhere, value?: string | number | Array<string | number>, operator: Operator = "equal to") {
let where: Where = {
'type': 'OR'
}
if (columnOrFilter instanceof FilterWhere) {
where.filter = columnOrFilter;
} else {
where.column = columnOrFilter;
where.value = value;
where.operator = operator;
}
this._conditions.push(where)
return this;
}
build() {
return (item: object) => {
if (this._conditions.length === 0) return true;
return this._conditions.reduce((result, condition) => {
if (condition.filter) {
const nestedResult = condition.filter.build()(item);
return condition.type === 'AND' ? result && nestedResult : result || nestedResult;
} else if (condition.column && condition.value) {
let comparison = false;
let itemValue = item[condition.column];
if (typeof itemValue === 'object' && itemValue.raw) {
itemValue = itemValue.raw;
}
switch (condition.operator) {
case "between":
if (!(condition.value instanceof Array)) {
throw new Error('Please provide array for "between" operator');
}
comparison = itemValue >= condition.value[0] && itemValue <= condition.value[1];
break;
case "equal to":
comparison = itemValue === condition.value;
break;
case "greater than":
comparison = itemValue > condition.value;
break;
case "less than":
comparison = itemValue < condition.value;
break;
case "greater than or equal":
comparison = itemValue >= condition.value;
break;
case "less than or equal":
comparison = itemValue <= condition.value;
break;
case "not equal to":
comparison = itemValue !== condition.value;
break;
case "not in":
if (!(condition.value instanceof Array)) {
throw new Error('Please provide array for "between" operator');
}
comparison = !condition.value.includes(itemValue);
break;
case "in":
if (!(condition.value instanceof Array)) {
throw new Error('Please provide array for "between" operator');
}
comparison = condition.value.includes(itemValue);
break;
case "contains":
comparison = (new RegExp(condition.value.toString().toLowerCase(), 'i')).test(itemValue.toString());
break;
}
// const comparison = item[condition.column] === condition.value;
return condition.type === 'AND' ? result && comparison : result || comparison;
} else {
return true;
}
}, this._conditions[0].type === 'AND');
};
}
handle(data: Array<object> | object): Array<object> | object {
if (this._conditions.length === 0) {
return data;
}
if (data instanceof Array) {
return data.filter(this.build());
}
Object.keys(data).forEach((key) => {
let values = data[key];
if (!values.forEach) {
return;
}
data[key] = values.filter(this.build())
})
return data;
}
toQuery() {
return '';
}
}