UNPKG

awesome-data-view

Version:
388 lines (339 loc) 15.6 kB
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 ''; } }