@memberjunction/ng-filter-builder 0.0.1 → 2.123.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/filter-builder/filter-builder.component.d.ts +155 -0
- package/dist/lib/filter-builder/filter-builder.component.d.ts.map +1 -0
- package/dist/lib/filter-builder/filter-builder.component.js +482 -0
- package/dist/lib/filter-builder/filter-builder.component.js.map +1 -0
- package/dist/lib/filter-builder.module.d.ts +39 -0
- package/dist/lib/filter-builder.module.d.ts.map +1 -0
- package/dist/lib/filter-builder.module.js +66 -0
- package/dist/lib/filter-builder.module.js.map +1 -0
- package/dist/lib/filter-group/filter-group.component.d.ts +102 -0
- package/dist/lib/filter-group/filter-group.component.d.ts.map +1 -0
- package/dist/lib/filter-group/filter-group.component.js +328 -0
- package/dist/lib/filter-group/filter-group.component.js.map +1 -0
- package/dist/lib/filter-rule/filter-rule.component.d.ts +165 -0
- package/dist/lib/filter-rule/filter-rule.component.d.ts.map +1 -0
- package/dist/lib/filter-rule/filter-rule.component.js +682 -0
- package/dist/lib/filter-rule/filter-rule.component.js.map +1 -0
- package/dist/lib/types/filter.types.d.ts +142 -0
- package/dist/lib/types/filter.types.d.ts.map +1 -0
- package/dist/lib/types/filter.types.js +82 -0
- package/dist/lib/types/filter.types.js.map +1 -0
- package/dist/lib/types/operators.d.ts +49 -0
- package/dist/lib/types/operators.d.ts.map +1 -0
- package/dist/lib/types/operators.js +99 -0
- package/dist/lib/types/operators.js.map +1 -0
- package/dist/public-api.d.ts +15 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +19 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +38 -6
- package/README.md +0 -45
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core';
|
|
2
|
+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
3
|
+
import { CompositeFilterDescriptor, FilterFieldInfo, FilterBuilderConfig } from '../types/filter.types';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
/**
|
|
6
|
+
* FilterBuilderComponent - Main filter builder component
|
|
7
|
+
*
|
|
8
|
+
* Provides a complete UI for building complex filter expressions
|
|
9
|
+
* with AND/OR logic and nested groups. Outputs Kendo-compatible
|
|
10
|
+
* CompositeFilterDescriptor JSON format.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```html
|
|
14
|
+
* <mj-filter-builder
|
|
15
|
+
* [fields]="filterFields"
|
|
16
|
+
* [filter]="currentFilter"
|
|
17
|
+
* (filterChange)="onFilterChange($event)"
|
|
18
|
+
* (apply)="onApply($event)">
|
|
19
|
+
* </mj-filter-builder>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class FilterBuilderComponent implements OnInit, OnChanges {
|
|
23
|
+
private sanitizer;
|
|
24
|
+
/**
|
|
25
|
+
* Available fields to filter on
|
|
26
|
+
*/
|
|
27
|
+
fields: FilterFieldInfo[];
|
|
28
|
+
/**
|
|
29
|
+
* Current filter state (Kendo-compatible CompositeFilterDescriptor)
|
|
30
|
+
*/
|
|
31
|
+
filter: CompositeFilterDescriptor | null;
|
|
32
|
+
/**
|
|
33
|
+
* Configuration options
|
|
34
|
+
*/
|
|
35
|
+
config: Partial<FilterBuilderConfig>;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the component is disabled
|
|
38
|
+
*/
|
|
39
|
+
disabled: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to show the natural language filter summary at the bottom
|
|
42
|
+
*/
|
|
43
|
+
showSummary: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the filter summary is expanded (visible)
|
|
46
|
+
*/
|
|
47
|
+
isSummaryExpanded: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Emitted when the filter changes
|
|
50
|
+
*/
|
|
51
|
+
filterChange: EventEmitter<CompositeFilterDescriptor>;
|
|
52
|
+
/**
|
|
53
|
+
* Emitted when the Apply button is clicked (if showApplyButton is true)
|
|
54
|
+
*/
|
|
55
|
+
apply: EventEmitter<CompositeFilterDescriptor>;
|
|
56
|
+
/**
|
|
57
|
+
* Emitted when the Clear button is clicked
|
|
58
|
+
*/
|
|
59
|
+
clear: EventEmitter<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Internal filter state
|
|
62
|
+
*/
|
|
63
|
+
internalFilter: CompositeFilterDescriptor;
|
|
64
|
+
/**
|
|
65
|
+
* Merged configuration
|
|
66
|
+
*/
|
|
67
|
+
mergedConfig: FilterBuilderConfig;
|
|
68
|
+
/**
|
|
69
|
+
* Whether there are any active filters
|
|
70
|
+
*/
|
|
71
|
+
hasActiveFilters: boolean;
|
|
72
|
+
constructor(sanitizer: DomSanitizer);
|
|
73
|
+
ngOnInit(): void;
|
|
74
|
+
ngOnChanges(changes: SimpleChanges): void;
|
|
75
|
+
/**
|
|
76
|
+
* Initialize the internal filter state
|
|
77
|
+
*/
|
|
78
|
+
private initializeFilter;
|
|
79
|
+
/**
|
|
80
|
+
* Merge provided config with defaults
|
|
81
|
+
*/
|
|
82
|
+
private mergeConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Handle filter change from the filter group
|
|
85
|
+
*/
|
|
86
|
+
onFilterChange(filter: CompositeFilterDescriptor): void;
|
|
87
|
+
/**
|
|
88
|
+
* Handle Apply button click
|
|
89
|
+
*/
|
|
90
|
+
onApply(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Handle Clear button click
|
|
93
|
+
*/
|
|
94
|
+
onClear(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Get the count of active filter rules
|
|
97
|
+
*/
|
|
98
|
+
getFilterCount(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Count filters recursively
|
|
101
|
+
*/
|
|
102
|
+
private countFilters;
|
|
103
|
+
/**
|
|
104
|
+
* Update hasActiveFilters flag
|
|
105
|
+
*/
|
|
106
|
+
private updateHasActiveFilters;
|
|
107
|
+
/**
|
|
108
|
+
* Deep clone a filter to prevent mutation
|
|
109
|
+
*/
|
|
110
|
+
private deepCloneFilter;
|
|
111
|
+
/**
|
|
112
|
+
* Toggle the filter summary visibility
|
|
113
|
+
*/
|
|
114
|
+
toggleSummary(): void;
|
|
115
|
+
/**
|
|
116
|
+
* Generate HTML-formatted summary of the filter expression with syntax highlighting
|
|
117
|
+
*/
|
|
118
|
+
getFilterSummaryHtml(): SafeHtml;
|
|
119
|
+
/**
|
|
120
|
+
* Inline styles for syntax highlighting (needed because Angular view encapsulation
|
|
121
|
+
* doesn't apply component CSS to dynamically injected innerHTML)
|
|
122
|
+
*/
|
|
123
|
+
private readonly styles;
|
|
124
|
+
/**
|
|
125
|
+
* Build HTML summary recursively with indentation and syntax highlighting
|
|
126
|
+
*/
|
|
127
|
+
private buildFilterSummaryHtml;
|
|
128
|
+
/**
|
|
129
|
+
* Build HTML summary for a single rule with syntax highlighting
|
|
130
|
+
*/
|
|
131
|
+
private buildRuleSummaryHtml;
|
|
132
|
+
/**
|
|
133
|
+
* Check if operator is a null-check operator (doesn't need a value)
|
|
134
|
+
*/
|
|
135
|
+
private isNullCheckOperator;
|
|
136
|
+
/**
|
|
137
|
+
* Get human-readable label for an operator
|
|
138
|
+
*/
|
|
139
|
+
private getOperatorLabel;
|
|
140
|
+
/**
|
|
141
|
+
* Format a value for HTML display with syntax highlighting
|
|
142
|
+
*/
|
|
143
|
+
private formatValueHtml;
|
|
144
|
+
/**
|
|
145
|
+
* Check if a string looks like an ISO date
|
|
146
|
+
*/
|
|
147
|
+
private isIsoDateString;
|
|
148
|
+
/**
|
|
149
|
+
* Escape HTML characters to prevent XSS
|
|
150
|
+
*/
|
|
151
|
+
private escapeHtml;
|
|
152
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FilterBuilderComponent, never>;
|
|
153
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<FilterBuilderComponent, "mj-filter-builder", never, { "fields": { "alias": "fields"; "required": false; }; "filter": { "alias": "filter"; "required": false; }; "config": { "alias": "config"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "showSummary": { "alias": "showSummary"; "required": false; }; }, { "filterChange": "filterChange"; "apply": "apply"; "clear": "clear"; }, never, never, false, never>;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=filter-builder.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-builder.component.d.ts","sourceRoot":"","sources":["../../../src/lib/filter-builder/filter-builder.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EACL,yBAAyB,EAEzB,eAAe,EACf,mBAAmB,EAGpB,MAAM,uBAAuB,CAAC;;AAa/B;;;;;;;;;;;;;;;;GAgBG;AACH,qBAKa,sBAAuB,YAAW,MAAM,EAAE,SAAS;IA6DlD,OAAO,CAAC,SAAS;IA5D7B;;OAEG;IACM,MAAM,EAAE,eAAe,EAAE,CAAM;IAExC;;OAEG;IACM,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAQ;IAEzD;;OAEG;IACM,MAAM,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAM;IAEnD;;OAEG;IACM,QAAQ,EAAE,OAAO,CAAS;IAEnC;;OAEG;IACM,WAAW,EAAE,OAAO,CAAS;IAEtC;;OAEG;IACI,iBAAiB,EAAE,OAAO,CAAS;IAE1C;;OAEG;IACO,YAAY,0CAAiD;IAEvE;;OAEG;IACO,KAAK,0CAAiD;IAEhE;;OAEG;IACO,KAAK,qBAA4B;IAE3C;;OAEG;IACI,cAAc,EAAE,yBAAyB,CAAuB;IAEvE;;OAEG;IACI,YAAY,EAAE,mBAAmB,CAAyB;IAEjE;;OAEG;IACI,gBAAgB,EAAE,OAAO,CAAS;gBAErB,SAAS,EAAE,YAAY;IAE3C,QAAQ,IAAI,IAAI;IAKhB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IASzC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI;IASvD;;OAEG;IACH,OAAO,IAAI,IAAI;IAKf;;OAEG;IACH,OAAO,IAAI,IAAI;IAOf;;OAEG;IACH,cAAc,IAAI,MAAM;IAIxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAgBpB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAI9B;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,aAAa,IAAI,IAAI;IAIrB;;OAEG;IACH,oBAAoB,IAAI,QAAQ;IAQhC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAWrB;IAEF;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA0B5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAmDvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACH,OAAO,CAAC,UAAU;yCAtWP,sBAAsB;2CAAtB,sBAAsB;CA2WlC"}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|
2
|
+
import { createEmptyFilter, isCompositeFilter } from '../types/filter.types';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
import * as i1 from "@angular/platform-browser";
|
|
5
|
+
import * as i2 from "../filter-group/filter-group.component";
|
|
6
|
+
function FilterBuilderComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
7
|
+
i0.ɵɵelementStart(0, "span", 5);
|
|
8
|
+
i0.ɵɵtext(1);
|
|
9
|
+
i0.ɵɵelementEnd();
|
|
10
|
+
} if (rf & 2) {
|
|
11
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
12
|
+
i0.ɵɵadvance();
|
|
13
|
+
i0.ɵɵtextInterpolate2("", ctx_r0.getFilterCount(), " condition", ctx_r0.getFilterCount() !== 1 ? "s" : "", "");
|
|
14
|
+
} }
|
|
15
|
+
function FilterBuilderComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
16
|
+
i0.ɵɵelementStart(0, "span", 6);
|
|
17
|
+
i0.ɵɵtext(1, "No filters applied");
|
|
18
|
+
i0.ɵɵelementEnd();
|
|
19
|
+
} }
|
|
20
|
+
function FilterBuilderComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
21
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
22
|
+
i0.ɵɵelementStart(0, "button", 14);
|
|
23
|
+
i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onClear()); });
|
|
24
|
+
i0.ɵɵelement(1, "i", 15);
|
|
25
|
+
i0.ɵɵelementStart(2, "span");
|
|
26
|
+
i0.ɵɵtext(3, "Clear All");
|
|
27
|
+
i0.ɵɵelementEnd()();
|
|
28
|
+
} if (rf & 2) {
|
|
29
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
30
|
+
i0.ɵɵproperty("disabled", ctx_r0.disabled);
|
|
31
|
+
} }
|
|
32
|
+
function FilterBuilderComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
|
|
33
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
34
|
+
i0.ɵɵelementStart(0, "button", 16);
|
|
35
|
+
i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_9_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onApply()); });
|
|
36
|
+
i0.ɵɵelement(1, "i", 17);
|
|
37
|
+
i0.ɵɵelementStart(2, "span");
|
|
38
|
+
i0.ɵɵtext(3, "Apply");
|
|
39
|
+
i0.ɵɵelementEnd()();
|
|
40
|
+
} if (rf & 2) {
|
|
41
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
42
|
+
i0.ɵɵproperty("disabled", ctx_r0.disabled);
|
|
43
|
+
} }
|
|
44
|
+
function FilterBuilderComponent_Conditional_12_Template(rf, ctx) { if (rf & 1) {
|
|
45
|
+
i0.ɵɵelementStart(0, "div", 12)(1, "p");
|
|
46
|
+
i0.ɵɵelement(2, "i", 18);
|
|
47
|
+
i0.ɵɵelementStart(3, "strong");
|
|
48
|
+
i0.ɵɵtext(4, "Tip:");
|
|
49
|
+
i0.ɵɵelementEnd();
|
|
50
|
+
i0.ɵɵtext(5, " Click \"Add Condition\" to create filter rules. Use \"Add Group\" for complex AND/OR combinations. ");
|
|
51
|
+
i0.ɵɵelementEnd()();
|
|
52
|
+
} }
|
|
53
|
+
function FilterBuilderComponent_Conditional_13_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
54
|
+
i0.ɵɵelementStart(0, "div", 25);
|
|
55
|
+
i0.ɵɵelement(1, "pre", 26);
|
|
56
|
+
i0.ɵɵelementEnd();
|
|
57
|
+
} if (rf & 2) {
|
|
58
|
+
const ctx_r0 = i0.ɵɵnextContext(2);
|
|
59
|
+
i0.ɵɵadvance();
|
|
60
|
+
i0.ɵɵproperty("innerHTML", ctx_r0.getFilterSummaryHtml(), i0.ɵɵsanitizeHtml);
|
|
61
|
+
} }
|
|
62
|
+
function FilterBuilderComponent_Conditional_13_Template(rf, ctx) { if (rf & 1) {
|
|
63
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
64
|
+
i0.ɵɵelementStart(0, "div", 19)(1, "button", 20);
|
|
65
|
+
i0.ɵɵlistener("click", function FilterBuilderComponent_Conditional_13_Template_button_click_1_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.toggleSummary()); });
|
|
66
|
+
i0.ɵɵelement(2, "i", 21)(3, "i", 22);
|
|
67
|
+
i0.ɵɵelementStart(4, "span", 23);
|
|
68
|
+
i0.ɵɵtext(5, "View Filter Expression");
|
|
69
|
+
i0.ɵɵelementEnd();
|
|
70
|
+
i0.ɵɵelementStart(6, "span", 24);
|
|
71
|
+
i0.ɵɵtext(7);
|
|
72
|
+
i0.ɵɵelementEnd()();
|
|
73
|
+
i0.ɵɵtemplate(8, FilterBuilderComponent_Conditional_13_Conditional_8_Template, 2, 1, "div", 25);
|
|
74
|
+
i0.ɵɵelementEnd();
|
|
75
|
+
} if (rf & 2) {
|
|
76
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
77
|
+
i0.ɵɵclassProp("expanded", ctx_r0.isSummaryExpanded);
|
|
78
|
+
i0.ɵɵadvance(2);
|
|
79
|
+
i0.ɵɵclassProp("fa-chevron-right", !ctx_r0.isSummaryExpanded)("fa-chevron-down", ctx_r0.isSummaryExpanded);
|
|
80
|
+
i0.ɵɵadvance(5);
|
|
81
|
+
i0.ɵɵtextInterpolate(ctx_r0.getFilterCount());
|
|
82
|
+
i0.ɵɵadvance();
|
|
83
|
+
i0.ɵɵconditional(ctx_r0.isSummaryExpanded ? 8 : -1);
|
|
84
|
+
} }
|
|
85
|
+
/**
|
|
86
|
+
* Default configuration for the filter builder
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_CONFIG = {
|
|
89
|
+
maxDepth: 3,
|
|
90
|
+
allowGroups: true,
|
|
91
|
+
showClearButton: true,
|
|
92
|
+
showApplyButton: false,
|
|
93
|
+
applyOnChange: true
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* FilterBuilderComponent - Main filter builder component
|
|
97
|
+
*
|
|
98
|
+
* Provides a complete UI for building complex filter expressions
|
|
99
|
+
* with AND/OR logic and nested groups. Outputs Kendo-compatible
|
|
100
|
+
* CompositeFilterDescriptor JSON format.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```html
|
|
104
|
+
* <mj-filter-builder
|
|
105
|
+
* [fields]="filterFields"
|
|
106
|
+
* [filter]="currentFilter"
|
|
107
|
+
* (filterChange)="onFilterChange($event)"
|
|
108
|
+
* (apply)="onApply($event)">
|
|
109
|
+
* </mj-filter-builder>
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export class FilterBuilderComponent {
|
|
113
|
+
sanitizer;
|
|
114
|
+
/**
|
|
115
|
+
* Available fields to filter on
|
|
116
|
+
*/
|
|
117
|
+
fields = [];
|
|
118
|
+
/**
|
|
119
|
+
* Current filter state (Kendo-compatible CompositeFilterDescriptor)
|
|
120
|
+
*/
|
|
121
|
+
filter = null;
|
|
122
|
+
/**
|
|
123
|
+
* Configuration options
|
|
124
|
+
*/
|
|
125
|
+
config = {};
|
|
126
|
+
/**
|
|
127
|
+
* Whether the component is disabled
|
|
128
|
+
*/
|
|
129
|
+
disabled = false;
|
|
130
|
+
/**
|
|
131
|
+
* Whether to show the natural language filter summary at the bottom
|
|
132
|
+
*/
|
|
133
|
+
showSummary = false;
|
|
134
|
+
/**
|
|
135
|
+
* Whether the filter summary is expanded (visible)
|
|
136
|
+
*/
|
|
137
|
+
isSummaryExpanded = false;
|
|
138
|
+
/**
|
|
139
|
+
* Emitted when the filter changes
|
|
140
|
+
*/
|
|
141
|
+
filterChange = new EventEmitter();
|
|
142
|
+
/**
|
|
143
|
+
* Emitted when the Apply button is clicked (if showApplyButton is true)
|
|
144
|
+
*/
|
|
145
|
+
apply = new EventEmitter();
|
|
146
|
+
/**
|
|
147
|
+
* Emitted when the Clear button is clicked
|
|
148
|
+
*/
|
|
149
|
+
clear = new EventEmitter();
|
|
150
|
+
/**
|
|
151
|
+
* Internal filter state
|
|
152
|
+
*/
|
|
153
|
+
internalFilter = createEmptyFilter();
|
|
154
|
+
/**
|
|
155
|
+
* Merged configuration
|
|
156
|
+
*/
|
|
157
|
+
mergedConfig = { ...DEFAULT_CONFIG };
|
|
158
|
+
/**
|
|
159
|
+
* Whether there are any active filters
|
|
160
|
+
*/
|
|
161
|
+
hasActiveFilters = false;
|
|
162
|
+
constructor(sanitizer) {
|
|
163
|
+
this.sanitizer = sanitizer;
|
|
164
|
+
}
|
|
165
|
+
ngOnInit() {
|
|
166
|
+
this.initializeFilter();
|
|
167
|
+
this.mergeConfig();
|
|
168
|
+
}
|
|
169
|
+
ngOnChanges(changes) {
|
|
170
|
+
if (changes['filter']) {
|
|
171
|
+
this.initializeFilter();
|
|
172
|
+
}
|
|
173
|
+
if (changes['config']) {
|
|
174
|
+
this.mergeConfig();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Initialize the internal filter state
|
|
179
|
+
*/
|
|
180
|
+
initializeFilter() {
|
|
181
|
+
if (this.filter && isCompositeFilter(this.filter)) {
|
|
182
|
+
this.internalFilter = this.deepCloneFilter(this.filter);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.internalFilter = createEmptyFilter();
|
|
186
|
+
}
|
|
187
|
+
this.updateHasActiveFilters();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Merge provided config with defaults
|
|
191
|
+
*/
|
|
192
|
+
mergeConfig() {
|
|
193
|
+
this.mergedConfig = { ...DEFAULT_CONFIG, ...this.config };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Handle filter change from the filter group
|
|
197
|
+
*/
|
|
198
|
+
onFilterChange(filter) {
|
|
199
|
+
this.internalFilter = filter;
|
|
200
|
+
this.updateHasActiveFilters();
|
|
201
|
+
if (this.mergedConfig.applyOnChange) {
|
|
202
|
+
this.filterChange.emit(filter);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Handle Apply button click
|
|
207
|
+
*/
|
|
208
|
+
onApply() {
|
|
209
|
+
this.filterChange.emit(this.internalFilter);
|
|
210
|
+
this.apply.emit(this.internalFilter);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Handle Clear button click
|
|
214
|
+
*/
|
|
215
|
+
onClear() {
|
|
216
|
+
this.internalFilter = createEmptyFilter();
|
|
217
|
+
this.updateHasActiveFilters();
|
|
218
|
+
this.filterChange.emit(this.internalFilter);
|
|
219
|
+
this.clear.emit();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get the count of active filter rules
|
|
223
|
+
*/
|
|
224
|
+
getFilterCount() {
|
|
225
|
+
return this.countFilters(this.internalFilter);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Count filters recursively
|
|
229
|
+
*/
|
|
230
|
+
countFilters(filter) {
|
|
231
|
+
let count = 0;
|
|
232
|
+
for (const item of filter.filters || []) {
|
|
233
|
+
if (isCompositeFilter(item)) {
|
|
234
|
+
count += this.countFilters(item);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Only count if the filter has a valid field and value (or null-check operators)
|
|
238
|
+
const rule = item;
|
|
239
|
+
if (rule.field) {
|
|
240
|
+
count++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return count;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Update hasActiveFilters flag
|
|
248
|
+
*/
|
|
249
|
+
updateHasActiveFilters() {
|
|
250
|
+
this.hasActiveFilters = this.getFilterCount() > 0;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Deep clone a filter to prevent mutation
|
|
254
|
+
*/
|
|
255
|
+
deepCloneFilter(filter) {
|
|
256
|
+
return JSON.parse(JSON.stringify(filter));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Toggle the filter summary visibility
|
|
260
|
+
*/
|
|
261
|
+
toggleSummary() {
|
|
262
|
+
this.isSummaryExpanded = !this.isSummaryExpanded;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Generate HTML-formatted summary of the filter expression with syntax highlighting
|
|
266
|
+
*/
|
|
267
|
+
getFilterSummaryHtml() {
|
|
268
|
+
if (!this.hasActiveFilters) {
|
|
269
|
+
return this.sanitizer.bypassSecurityTrustHtml('<span style="color: #9ca3af; font-style: italic;">No filters applied</span>');
|
|
270
|
+
}
|
|
271
|
+
const html = this.buildFilterSummaryHtml(this.internalFilter, 0);
|
|
272
|
+
return this.sanitizer.bypassSecurityTrustHtml(html);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Inline styles for syntax highlighting (needed because Angular view encapsulation
|
|
276
|
+
* doesn't apply component CSS to dynamically injected innerHTML)
|
|
277
|
+
*/
|
|
278
|
+
styles = {
|
|
279
|
+
fieldName: 'color: #0369a1; font-weight: 600;',
|
|
280
|
+
operator: 'color: #6b7280; font-style: italic;',
|
|
281
|
+
valueString: 'color: #059669; font-weight: 500;',
|
|
282
|
+
valueNumber: 'color: #7c3aed; font-weight: 500;',
|
|
283
|
+
valueDate: 'color: #c2410c; font-weight: 500;',
|
|
284
|
+
valueTrue: 'color: #16a34a; font-weight: 600;',
|
|
285
|
+
valueFalse: 'color: #dc2626; font-weight: 600;',
|
|
286
|
+
logicAnd: 'display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; background: #dbeafe; color: #1d4ed8;',
|
|
287
|
+
logicOr: 'display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.5px; background: #fef3c7; color: #b45309;',
|
|
288
|
+
groupBracket: 'color: #9333ea; font-weight: 700; font-size: 15px;'
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Build HTML summary recursively with indentation and syntax highlighting
|
|
292
|
+
*/
|
|
293
|
+
buildFilterSummaryHtml(filter, depth) {
|
|
294
|
+
const parts = [];
|
|
295
|
+
const indent = ' '.repeat(depth);
|
|
296
|
+
for (const item of filter.filters || []) {
|
|
297
|
+
if (isCompositeFilter(item)) {
|
|
298
|
+
const groupSummary = this.buildFilterSummaryHtml(item, depth + 1);
|
|
299
|
+
if (groupSummary) {
|
|
300
|
+
parts.push(`<span style="${this.styles.groupBracket}">(</span>\n${groupSummary}\n${indent}<span style="${this.styles.groupBracket}">)</span>`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const rule = item;
|
|
305
|
+
const ruleSummary = this.buildRuleSummaryHtml(rule);
|
|
306
|
+
if (ruleSummary) {
|
|
307
|
+
parts.push(ruleSummary);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (parts.length === 0) {
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
const logicStyle = filter.logic === 'and' ? this.styles.logicAnd : this.styles.logicOr;
|
|
315
|
+
const logicLabel = filter.logic === 'and' ? 'AND' : 'OR';
|
|
316
|
+
const connector = `\n${indent}<span style="${logicStyle}">${logicLabel}</span>\n${indent}`;
|
|
317
|
+
return `${indent}${parts.join(connector)}`;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build HTML summary for a single rule with syntax highlighting
|
|
321
|
+
*/
|
|
322
|
+
buildRuleSummaryHtml(rule) {
|
|
323
|
+
if (!rule.field) {
|
|
324
|
+
return '';
|
|
325
|
+
}
|
|
326
|
+
// Get the field display name
|
|
327
|
+
const field = this.fields.find(f => f.name === rule.field);
|
|
328
|
+
const fieldName = field?.displayName || rule.field;
|
|
329
|
+
// Get the operator label
|
|
330
|
+
const operatorLabel = this.getOperatorLabel(rule.operator);
|
|
331
|
+
// Format the value
|
|
332
|
+
const formattedValue = this.formatValueHtml(rule.value, rule.operator);
|
|
333
|
+
// Build the summary based on operator type
|
|
334
|
+
const fieldHtml = `<span style="${this.styles.fieldName}">${this.escapeHtml(fieldName)}</span>`;
|
|
335
|
+
const operatorHtml = `<span style="${this.styles.operator}">${operatorLabel}</span>`;
|
|
336
|
+
if (this.isNullCheckOperator(rule.operator)) {
|
|
337
|
+
return `${fieldHtml} ${operatorHtml}`;
|
|
338
|
+
}
|
|
339
|
+
return `${fieldHtml} ${operatorHtml} ${formattedValue}`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Check if operator is a null-check operator (doesn't need a value)
|
|
343
|
+
*/
|
|
344
|
+
isNullCheckOperator(operator) {
|
|
345
|
+
return ['isnull', 'isnotnull', 'isempty', 'isnotempty'].includes(operator);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get human-readable label for an operator
|
|
349
|
+
*/
|
|
350
|
+
getOperatorLabel(operator) {
|
|
351
|
+
const labels = {
|
|
352
|
+
'eq': 'equals',
|
|
353
|
+
'neq': 'does not equal',
|
|
354
|
+
'contains': 'contains',
|
|
355
|
+
'doesnotcontain': 'does not contain',
|
|
356
|
+
'startswith': 'starts with',
|
|
357
|
+
'endswith': 'ends with',
|
|
358
|
+
'isnull': 'is empty',
|
|
359
|
+
'isnotnull': 'is not empty',
|
|
360
|
+
'isempty': 'is empty',
|
|
361
|
+
'isnotempty': 'is not empty',
|
|
362
|
+
'gt': 'is greater than',
|
|
363
|
+
'gte': 'is greater than or equal to',
|
|
364
|
+
'lt': 'is less than',
|
|
365
|
+
'lte': 'is less than or equal to'
|
|
366
|
+
};
|
|
367
|
+
return labels[operator] || operator;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Format a value for HTML display with syntax highlighting
|
|
371
|
+
*/
|
|
372
|
+
formatValueHtml(value, operator) {
|
|
373
|
+
if (value === null || value === undefined) {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
// Don't show value for null-check operators
|
|
377
|
+
if (this.isNullCheckOperator(operator)) {
|
|
378
|
+
return '';
|
|
379
|
+
}
|
|
380
|
+
// Handle ISO date strings
|
|
381
|
+
if (typeof value === 'string' && this.isIsoDateString(value)) {
|
|
382
|
+
const date = new Date(value);
|
|
383
|
+
const formatted = date.toLocaleDateString('en-US', {
|
|
384
|
+
year: 'numeric',
|
|
385
|
+
month: 'short',
|
|
386
|
+
day: 'numeric'
|
|
387
|
+
});
|
|
388
|
+
return `<span style="${this.styles.valueDate}">${formatted}</span>`;
|
|
389
|
+
}
|
|
390
|
+
// Handle strings
|
|
391
|
+
if (typeof value === 'string') {
|
|
392
|
+
return `<span style="${this.styles.valueString}">"${this.escapeHtml(value)}"</span>`;
|
|
393
|
+
}
|
|
394
|
+
// Handle booleans
|
|
395
|
+
if (typeof value === 'boolean') {
|
|
396
|
+
const boolStyle = value ? this.styles.valueTrue : this.styles.valueFalse;
|
|
397
|
+
return `<span style="${boolStyle}">${value ? 'Yes' : 'No'}</span>`;
|
|
398
|
+
}
|
|
399
|
+
// Handle numbers
|
|
400
|
+
if (typeof value === 'number') {
|
|
401
|
+
return `<span style="${this.styles.valueNumber}">${value}</span>`;
|
|
402
|
+
}
|
|
403
|
+
// Handle Date objects
|
|
404
|
+
if (value instanceof Date) {
|
|
405
|
+
const formatted = value.toLocaleDateString('en-US', {
|
|
406
|
+
year: 'numeric',
|
|
407
|
+
month: 'short',
|
|
408
|
+
day: 'numeric'
|
|
409
|
+
});
|
|
410
|
+
return `<span style="${this.styles.valueDate}">${formatted}</span>`;
|
|
411
|
+
}
|
|
412
|
+
// Default
|
|
413
|
+
return `<span style="font-weight: 500;">${this.escapeHtml(String(value))}</span>`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Check if a string looks like an ISO date
|
|
417
|
+
*/
|
|
418
|
+
isIsoDateString(value) {
|
|
419
|
+
// Match ISO 8601 date format
|
|
420
|
+
return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/.test(value);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Escape HTML characters to prevent XSS
|
|
424
|
+
*/
|
|
425
|
+
escapeHtml(text) {
|
|
426
|
+
const div = document.createElement('div');
|
|
427
|
+
div.textContent = text;
|
|
428
|
+
return div.innerHTML;
|
|
429
|
+
}
|
|
430
|
+
static ɵfac = function FilterBuilderComponent_Factory(t) { return new (t || FilterBuilderComponent)(i0.ɵɵdirectiveInject(i1.DomSanitizer)); };
|
|
431
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: FilterBuilderComponent, selectors: [["mj-filter-builder"]], inputs: { fields: "fields", filter: "filter", config: "config", disabled: "disabled", showSummary: "showSummary" }, outputs: { filterChange: "filterChange", apply: "apply", clear: "clear" }, features: [i0.ɵɵNgOnChangesFeature], decls: 14, vars: 13, consts: [[1, "filter-builder"], [1, "filter-header"], [1, "filter-summary"], [1, "filter-icon"], [1, "fa-solid", "fa-filter"], [1, "filter-count"], [1, "filter-count", "no-filters"], [1, "filter-actions"], ["type", "button", "title", "Clear all filters", 1, "action-btn", "clear-btn", 3, "disabled"], ["type", "button", "title", "Apply filters", 1, "action-btn", "apply-btn", 3, "disabled"], [1, "filter-content"], [3, "filterChange", "filter", "fields", "isRoot", "depth", "maxDepth", "disabled"], [1, "filter-tips"], [1, "filter-expression-summary", 3, "expanded"], ["type", "button", "title", "Clear all filters", 1, "action-btn", "clear-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-times"], ["type", "button", "title", "Apply filters", 1, "action-btn", "apply-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-check"], [1, "fa-solid", "fa-lightbulb"], [1, "filter-expression-summary"], ["type", "button", 1, "expression-toggle", 3, "click"], [1, "fa-solid"], [1, "fa-solid", "fa-code", "expression-icon"], [1, "expression-label"], [1, "expression-badge"], [1, "expression-content"], [1, "expression-text", 3, "innerHTML"]], template: function FilterBuilderComponent_Template(rf, ctx) { if (rf & 1) {
|
|
432
|
+
i0.ɵɵelementStart(0, "div", 0)(1, "div", 1)(2, "div", 2)(3, "span", 3);
|
|
433
|
+
i0.ɵɵelement(4, "i", 4);
|
|
434
|
+
i0.ɵɵelementEnd();
|
|
435
|
+
i0.ɵɵtemplate(5, FilterBuilderComponent_Conditional_5_Template, 2, 2, "span", 5)(6, FilterBuilderComponent_Conditional_6_Template, 2, 0, "span", 6);
|
|
436
|
+
i0.ɵɵelementEnd();
|
|
437
|
+
i0.ɵɵelementStart(7, "div", 7);
|
|
438
|
+
i0.ɵɵtemplate(8, FilterBuilderComponent_Conditional_8_Template, 4, 1, "button", 8)(9, FilterBuilderComponent_Conditional_9_Template, 4, 1, "button", 9);
|
|
439
|
+
i0.ɵɵelementEnd()();
|
|
440
|
+
i0.ɵɵelementStart(10, "div", 10)(11, "mj-filter-group", 11);
|
|
441
|
+
i0.ɵɵlistener("filterChange", function FilterBuilderComponent_Template_mj_filter_group_filterChange_11_listener($event) { return ctx.onFilterChange($event); });
|
|
442
|
+
i0.ɵɵelementEnd()();
|
|
443
|
+
i0.ɵɵtemplate(12, FilterBuilderComponent_Conditional_12_Template, 6, 0, "div", 12)(13, FilterBuilderComponent_Conditional_13_Template, 9, 8, "div", 13);
|
|
444
|
+
i0.ɵɵelementEnd();
|
|
445
|
+
} if (rf & 2) {
|
|
446
|
+
i0.ɵɵclassProp("disabled", ctx.disabled);
|
|
447
|
+
i0.ɵɵadvance(5);
|
|
448
|
+
i0.ɵɵconditional(ctx.hasActiveFilters ? 5 : 6);
|
|
449
|
+
i0.ɵɵadvance(3);
|
|
450
|
+
i0.ɵɵconditional(ctx.mergedConfig.showClearButton && ctx.hasActiveFilters ? 8 : -1);
|
|
451
|
+
i0.ɵɵadvance();
|
|
452
|
+
i0.ɵɵconditional(ctx.mergedConfig.showApplyButton ? 9 : -1);
|
|
453
|
+
i0.ɵɵadvance(2);
|
|
454
|
+
i0.ɵɵproperty("filter", ctx.internalFilter)("fields", ctx.fields)("isRoot", true)("depth", 0)("maxDepth", ctx.mergedConfig.maxDepth)("disabled", ctx.disabled);
|
|
455
|
+
i0.ɵɵadvance();
|
|
456
|
+
i0.ɵɵconditional(!ctx.hasActiveFilters ? 12 : -1);
|
|
457
|
+
i0.ɵɵadvance();
|
|
458
|
+
i0.ɵɵconditional(ctx.showSummary && ctx.hasActiveFilters ? 13 : -1);
|
|
459
|
+
} }, dependencies: [i2.FilterGroupComponent], styles: [".filter-builder[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.filter-builder.disabled[_ngcontent-%COMP%] {\n opacity: 0.6;\n pointer-events: none;\n}\n\n\n\n.filter-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-bottom: 12px;\n border-bottom: 1px solid #eee;\n}\n\n.filter-summary[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.filter-icon[_ngcontent-%COMP%] {\n color: #1976d2;\n font-size: 14px;\n}\n\n.filter-count[_ngcontent-%COMP%] {\n font-size: 13px;\n color: #333;\n font-weight: 500;\n}\n\n.filter-count.no-filters[_ngcontent-%COMP%] {\n color: #999;\n font-weight: 400;\n}\n\n\n\n.filter-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n\n.action-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: none;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.action-btn[_ngcontent-%COMP%]:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.action-btn[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 11px;\n}\n\n.clear-btn[_ngcontent-%COMP%] {\n background: #f5f5f5;\n color: #666;\n}\n\n.clear-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #ffebee;\n color: #c62828;\n}\n\n.apply-btn[_ngcontent-%COMP%] {\n background: #1976d2;\n color: white;\n}\n\n.apply-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: #1565c0;\n}\n\n\n\n.filter-content[_ngcontent-%COMP%] {\n min-height: 60px;\n}\n\n\n\n.filter-tips[_ngcontent-%COMP%] {\n padding: 12px;\n background: #f5f9ff;\n border-radius: 6px;\n border: 1px solid #e3edfa;\n}\n\n.filter-tips[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 13px;\n color: #666;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n.filter-tips[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: #ffc107;\n margin-top: 2px;\n}\n\n.filter-tips[_ngcontent-%COMP%] strong[_ngcontent-%COMP%] {\n color: #333;\n}\n\n\n\n.filter-expression-summary[_ngcontent-%COMP%] {\n background: #f8f9fa;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n margin-top: 8px;\n overflow: hidden;\n transition: all 0.2s ease;\n}\n\n.filter-expression-summary.expanded[_ngcontent-%COMP%] {\n background: white;\n border-color: #d1d5db;\n}\n\n\n\n.expression-toggle[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 12px 16px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6b7280;\n text-align: left;\n transition: all 0.15s ease;\n}\n\n.expression-toggle[_ngcontent-%COMP%]:hover {\n background: rgba(0, 0, 0, 0.03);\n color: #374151;\n}\n\n.expression-toggle[_ngcontent-%COMP%] i[_ngcontent-%COMP%]:first-child {\n font-size: 10px;\n color: #9ca3af;\n width: 12px;\n transition: transform 0.2s ease;\n}\n\n.expression-icon[_ngcontent-%COMP%] {\n color: #6366f1;\n font-size: 13px;\n}\n\n.expression-label[_ngcontent-%COMP%] {\n flex: 1;\n}\n\n.expression-badge[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n background: #e0e7ff;\n color: #4f46e5;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n\n\n.expression-content[_ngcontent-%COMP%] {\n padding: 0 16px 16px 16px;\n animation: _ngcontent-%COMP%_slideDown 0.2s ease;\n}\n\n@keyframes _ngcontent-%COMP%_slideDown {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.expression-text[_ngcontent-%COMP%] {\n font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.8;\n color: #374151;\n padding: 16px;\n margin: 0;\n background: linear-gradient(135deg, #fafbfc 0%, #f3f4f6 100%);\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n\n\n.expression-text[_ngcontent-%COMP%] .field-name[_ngcontent-%COMP%] {\n color: #0369a1;\n font-weight: 600;\n}\n\n.expression-text[_ngcontent-%COMP%] .operator[_ngcontent-%COMP%] {\n color: #6b7280;\n font-style: italic;\n}\n\n.expression-text[_ngcontent-%COMP%] .value[_ngcontent-%COMP%] {\n font-weight: 500;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-string[_ngcontent-%COMP%] {\n color: #059669;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-number[_ngcontent-%COMP%] {\n color: #7c3aed;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-boolean[_ngcontent-%COMP%] {\n font-weight: 600;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-true[_ngcontent-%COMP%] {\n color: #16a34a;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-false[_ngcontent-%COMP%] {\n color: #dc2626;\n}\n\n.expression-text[_ngcontent-%COMP%] .value-date[_ngcontent-%COMP%] {\n color: #c2410c;\n font-weight: 500;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-keyword[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-and[_ngcontent-%COMP%] {\n background: #dbeafe;\n color: #1d4ed8;\n}\n\n.expression-text[_ngcontent-%COMP%] .logic-or[_ngcontent-%COMP%] {\n background: #fef3c7;\n color: #b45309;\n}\n\n.expression-text[_ngcontent-%COMP%] .group-bracket[_ngcontent-%COMP%] {\n color: #9333ea;\n font-weight: 700;\n font-size: 15px;\n}\n\n.expression-text[_ngcontent-%COMP%] .no-filters[_ngcontent-%COMP%] {\n color: #9ca3af;\n font-style: italic;\n}\n\n\n\n@media (max-width: 600px) {\n .filter-header[_ngcontent-%COMP%] {\n flex-direction: column;\n align-items: stretch;\n gap: 12px;\n }\n\n .filter-summary[_ngcontent-%COMP%] {\n justify-content: center;\n }\n\n .filter-actions[_ngcontent-%COMP%] {\n justify-content: center;\n }\n\n .expression-toggle[_ngcontent-%COMP%] {\n padding: 10px 12px;\n }\n\n .expression-content[_ngcontent-%COMP%] {\n padding: 0 12px 12px 12px;\n }\n\n .expression-text[_ngcontent-%COMP%] {\n font-size: 12px;\n padding: 12px;\n }\n}"] });
|
|
460
|
+
}
|
|
461
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FilterBuilderComponent, [{
|
|
462
|
+
type: Component,
|
|
463
|
+
args: [{ selector: 'mj-filter-builder', template: "<div class=\"filter-builder\" [class.disabled]=\"disabled\">\n <!-- Header with summary and actions -->\n <div class=\"filter-header\">\n <div class=\"filter-summary\">\n <span class=\"filter-icon\">\n <i class=\"fa-solid fa-filter\"></i>\n </span>\n @if (hasActiveFilters) {\n <span class=\"filter-count\">{{ getFilterCount() }} condition{{ getFilterCount() !== 1 ? 's' : '' }}</span>\n } @else {\n <span class=\"filter-count no-filters\">No filters applied</span>\n }\n </div>\n\n <div class=\"filter-actions\">\n @if (mergedConfig.showClearButton && hasActiveFilters) {\n <button\n type=\"button\"\n class=\"action-btn clear-btn\"\n (click)=\"onClear()\"\n [disabled]=\"disabled\"\n title=\"Clear all filters\">\n <i class=\"fa-solid fa-times\"></i>\n <span>Clear All</span>\n </button>\n }\n @if (mergedConfig.showApplyButton) {\n <button\n type=\"button\"\n class=\"action-btn apply-btn\"\n (click)=\"onApply()\"\n [disabled]=\"disabled\"\n title=\"Apply filters\">\n <i class=\"fa-solid fa-check\"></i>\n <span>Apply</span>\n </button>\n }\n </div>\n </div>\n\n <!-- Filter Builder Content -->\n <div class=\"filter-content\">\n <mj-filter-group\n [filter]=\"internalFilter\"\n [fields]=\"fields\"\n [isRoot]=\"true\"\n [depth]=\"0\"\n [maxDepth]=\"mergedConfig.maxDepth\"\n [disabled]=\"disabled\"\n (filterChange)=\"onFilterChange($event)\">\n </mj-filter-group>\n </div>\n\n <!-- Quick tips (shown when no filters) -->\n @if (!hasActiveFilters) {\n <div class=\"filter-tips\">\n <p>\n <i class=\"fa-solid fa-lightbulb\"></i>\n <strong>Tip:</strong> Click \"Add Condition\" to create filter rules.\n Use \"Add Group\" for complex AND/OR combinations.\n </p>\n </div>\n }\n\n <!-- Collapsible filter expression summary -->\n @if (showSummary && hasActiveFilters) {\n <div class=\"filter-expression-summary\" [class.expanded]=\"isSummaryExpanded\">\n <button type=\"button\" class=\"expression-toggle\" (click)=\"toggleSummary()\">\n <i class=\"fa-solid\" [class.fa-chevron-right]=\"!isSummaryExpanded\" [class.fa-chevron-down]=\"isSummaryExpanded\"></i>\n <i class=\"fa-solid fa-code expression-icon\"></i>\n <span class=\"expression-label\">View Filter Expression</span>\n <span class=\"expression-badge\">{{ getFilterCount() }}</span>\n </button>\n @if (isSummaryExpanded) {\n <div class=\"expression-content\">\n <pre class=\"expression-text\" [innerHTML]=\"getFilterSummaryHtml()\"></pre>\n </div>\n }\n </div>\n }\n</div>\n", styles: [".filter-builder {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.filter-builder.disabled {\n opacity: 0.6;\n pointer-events: none;\n}\n\n/* Header */\n.filter-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-bottom: 12px;\n border-bottom: 1px solid #eee;\n}\n\n.filter-summary {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.filter-icon {\n color: #1976d2;\n font-size: 14px;\n}\n\n.filter-count {\n font-size: 13px;\n color: #333;\n font-weight: 500;\n}\n\n.filter-count.no-filters {\n color: #999;\n font-weight: 400;\n}\n\n/* Actions */\n.filter-actions {\n display: flex;\n gap: 8px;\n}\n\n.action-btn {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n border: none;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.action-btn:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.action-btn i {\n font-size: 11px;\n}\n\n.clear-btn {\n background: #f5f5f5;\n color: #666;\n}\n\n.clear-btn:hover:not(:disabled) {\n background: #ffebee;\n color: #c62828;\n}\n\n.apply-btn {\n background: #1976d2;\n color: white;\n}\n\n.apply-btn:hover:not(:disabled) {\n background: #1565c0;\n}\n\n/* Content */\n.filter-content {\n min-height: 60px;\n}\n\n/* Tips */\n.filter-tips {\n padding: 12px;\n background: #f5f9ff;\n border-radius: 6px;\n border: 1px solid #e3edfa;\n}\n\n.filter-tips p {\n margin: 0;\n font-size: 13px;\n color: #666;\n display: flex;\n align-items: flex-start;\n gap: 8px;\n}\n\n.filter-tips i {\n color: #ffc107;\n margin-top: 2px;\n}\n\n.filter-tips strong {\n color: #333;\n}\n\n/* Collapsible Filter Expression Summary */\n.filter-expression-summary {\n background: #f8f9fa;\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n margin-top: 8px;\n overflow: hidden;\n transition: all 0.2s ease;\n}\n\n.filter-expression-summary.expanded {\n background: white;\n border-color: #d1d5db;\n}\n\n/* Toggle Button */\n.expression-toggle {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 12px 16px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 13px;\n font-weight: 500;\n color: #6b7280;\n text-align: left;\n transition: all 0.15s ease;\n}\n\n.expression-toggle:hover {\n background: rgba(0, 0, 0, 0.03);\n color: #374151;\n}\n\n.expression-toggle i:first-child {\n font-size: 10px;\n color: #9ca3af;\n width: 12px;\n transition: transform 0.2s ease;\n}\n\n.expression-icon {\n color: #6366f1;\n font-size: 13px;\n}\n\n.expression-label {\n flex: 1;\n}\n\n.expression-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n padding: 0 6px;\n background: #e0e7ff;\n color: #4f46e5;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n/* Expression Content */\n.expression-content {\n padding: 0 16px 16px 16px;\n animation: slideDown 0.2s ease;\n}\n\n@keyframes slideDown {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.expression-text {\n font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.8;\n color: #374151;\n padding: 16px;\n margin: 0;\n background: linear-gradient(135deg, #fafbfc 0%, #f3f4f6 100%);\n border: 1px solid #e5e7eb;\n border-radius: 8px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* Syntax Highlighting */\n.expression-text .field-name {\n color: #0369a1;\n font-weight: 600;\n}\n\n.expression-text .operator {\n color: #6b7280;\n font-style: italic;\n}\n\n.expression-text .value {\n font-weight: 500;\n}\n\n.expression-text .value-string {\n color: #059669;\n}\n\n.expression-text .value-number {\n color: #7c3aed;\n}\n\n.expression-text .value-boolean {\n font-weight: 600;\n}\n\n.expression-text .value-true {\n color: #16a34a;\n}\n\n.expression-text .value-false {\n color: #dc2626;\n}\n\n.expression-text .value-date {\n color: #c2410c;\n font-weight: 500;\n}\n\n.expression-text .logic-keyword {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 4px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n}\n\n.expression-text .logic-and {\n background: #dbeafe;\n color: #1d4ed8;\n}\n\n.expression-text .logic-or {\n background: #fef3c7;\n color: #b45309;\n}\n\n.expression-text .group-bracket {\n color: #9333ea;\n font-weight: 700;\n font-size: 15px;\n}\n\n.expression-text .no-filters {\n color: #9ca3af;\n font-style: italic;\n}\n\n/* Responsive */\n@media (max-width: 600px) {\n .filter-header {\n flex-direction: column;\n align-items: stretch;\n gap: 12px;\n }\n\n .filter-summary {\n justify-content: center;\n }\n\n .filter-actions {\n justify-content: center;\n }\n\n .expression-toggle {\n padding: 10px 12px;\n }\n\n .expression-content {\n padding: 0 12px 12px 12px;\n }\n\n .expression-text {\n font-size: 12px;\n padding: 12px;\n }\n}\n"] }]
|
|
464
|
+
}], () => [{ type: i1.DomSanitizer }], { fields: [{
|
|
465
|
+
type: Input
|
|
466
|
+
}], filter: [{
|
|
467
|
+
type: Input
|
|
468
|
+
}], config: [{
|
|
469
|
+
type: Input
|
|
470
|
+
}], disabled: [{
|
|
471
|
+
type: Input
|
|
472
|
+
}], showSummary: [{
|
|
473
|
+
type: Input
|
|
474
|
+
}], filterChange: [{
|
|
475
|
+
type: Output
|
|
476
|
+
}], apply: [{
|
|
477
|
+
type: Output
|
|
478
|
+
}], clear: [{
|
|
479
|
+
type: Output
|
|
480
|
+
}] }); })();
|
|
481
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(FilterBuilderComponent, { className: "FilterBuilderComponent", filePath: "src/lib/filter-builder/filter-builder.component.ts", lineNumber: 45 }); })();
|
|
482
|
+
//# sourceMappingURL=filter-builder.component.js.map
|