@aquera/nile-visualization 2.9.1 → 2.9.3
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/src/nile-chart/nile-chart.css.js +6 -0
- package/dist/src/nile-filter-chart/nile-filter-chart.css.js +274 -4
- package/dist/src/nile-filter-chart/nile-filter-chart.d.ts +59 -206
- package/dist/src/nile-filter-chart/nile-filter-chart.js +330 -436
- package/dist/src/nile-filter-chart/utils/badge.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/badge.js +33 -0
- package/dist/src/nile-filter-chart/utils/comparison.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/comparison.js +24 -0
- package/dist/src/nile-filter-chart/utils/dropdown.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/dropdown.js +24 -0
- package/dist/src/nile-filter-chart/utils/preset.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/preset.js +16 -0
- package/dist/src/nile-filter-chart/utils/prompt.d.ts +12 -0
- package/dist/src/nile-filter-chart/utils/prompt.js +676 -0
- package/dist/src/nile-filter-chart/utils/radio.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/radio.js +13 -0
- package/dist/src/nile-filter-chart/utils/search.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/search.js +12 -0
- package/dist/src/nile-filter-chart/utils/segmented.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/segmented.js +15 -0
- package/dist/src/nile-filter-chart/utils/threshold.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/threshold.js +58 -0
- package/dist/src/nile-filter-chart/utils/toggle.d.ts +3 -0
- package/dist/src/nile-filter-chart/utils/toggle.js +19 -0
- package/dist/src/nile-filter-chart/utils/types.d.ts +334 -0
- package/dist/src/nile-filter-chart/utils/types.js +2 -0
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +62 -4
- package/package.json +1 -1
|
@@ -4,6 +4,19 @@ import { html, nothing } from 'lit';
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
5
|
import { styles } from './nile-filter-chart.css.js';
|
|
6
6
|
import NileElement from '../internal/nile-element.js';
|
|
7
|
+
import { renderBadge } from './utils/badge.js';
|
|
8
|
+
import { renderDropdown } from './utils/dropdown.js';
|
|
9
|
+
import { renderSegmented } from './utils/segmented.js';
|
|
10
|
+
import { renderRadio } from './utils/radio.js';
|
|
11
|
+
import { renderToggle } from './utils/toggle.js';
|
|
12
|
+
import { renderSearch } from './utils/search.js';
|
|
13
|
+
import { renderComparison } from './utils/comparison.js';
|
|
14
|
+
import { renderThreshold } from './utils/threshold.js';
|
|
15
|
+
import { renderPreset } from './utils/preset.js';
|
|
16
|
+
import { renderPrompt, validatePromptExpression } from './utils/prompt.js';
|
|
17
|
+
// Lowercase + ASCII-safe slug used to derive a control's `id` from its
|
|
18
|
+
// `label` when the author hasn't provided one explicitly.
|
|
19
|
+
const slugify = (s) => s.toLowerCase().trim().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
7
20
|
let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElement {
|
|
8
21
|
constructor() {
|
|
9
22
|
super(...arguments);
|
|
@@ -11,30 +24,163 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
11
24
|
this.selectedValues = new Map();
|
|
12
25
|
this.collapsedGroups = new Set();
|
|
13
26
|
/** Currently displayed (animated) placeholder text per prompt-variant control id. */
|
|
14
|
-
this.
|
|
27
|
+
this.promptPlaceholder = new Map();
|
|
28
|
+
/** Validation messages per prompt control id (strict-mode parse failures). */
|
|
29
|
+
this.promptErrors = new Map();
|
|
30
|
+
/** Runtime mode per prompt control id. Only populated for controls with `queryLanguage.enabled`. */
|
|
31
|
+
this.promptModes = new Map();
|
|
32
|
+
/** Highlighted suggestion index per prompt control id (-1 = none highlighted). */
|
|
33
|
+
this.promptActiveIndex = new Map();
|
|
15
34
|
/** Active typewriter timers per prompt control id (so we can stop them). */
|
|
16
35
|
this._promptTimers = new Map();
|
|
36
|
+
/** Compiled filtrex predicates per prompt control id (strict-mode successes). */
|
|
37
|
+
this._promptEvaluators = new Map();
|
|
38
|
+
/** Parsed AST per prompt control id (strict-mode successes). */
|
|
39
|
+
this._promptAsts = new Map();
|
|
40
|
+
/** Monotonic per-control token used to ignore stale async validations. */
|
|
41
|
+
this._validationTokens = new Map();
|
|
42
|
+
/** Pending debounce timers for prompt input — gates validation + nile-change emission. */
|
|
43
|
+
this._promptDebounce = new Map();
|
|
17
44
|
}
|
|
18
45
|
static get styles() {
|
|
19
46
|
return [styles];
|
|
20
47
|
}
|
|
21
48
|
connectedCallback() {
|
|
22
49
|
super.connectedCallback();
|
|
50
|
+
this._normalizeIds();
|
|
23
51
|
this._initValues();
|
|
24
52
|
this.emit('nile-init');
|
|
25
53
|
}
|
|
26
54
|
disconnectedCallback() {
|
|
27
55
|
super.disconnectedCallback();
|
|
28
56
|
this._stopAllPromptAnimations();
|
|
57
|
+
for (const t of this._promptDebounce.values())
|
|
58
|
+
clearTimeout(t);
|
|
59
|
+
this._promptDebounce.clear();
|
|
29
60
|
this.emit('nile-destroy');
|
|
30
61
|
}
|
|
31
62
|
updated(changed) {
|
|
32
63
|
super.updated(changed);
|
|
33
64
|
if (changed.has('config')) {
|
|
65
|
+
this._normalizeIds();
|
|
34
66
|
this._initValues();
|
|
35
67
|
this._syncPromptAnimations();
|
|
36
68
|
}
|
|
37
69
|
}
|
|
70
|
+
// ── FilterChartHost surface ─────────────────────────────────────────────────
|
|
71
|
+
setValue(id, value) {
|
|
72
|
+
this.selectedValues = new Map(this.selectedValues).set(id, value);
|
|
73
|
+
this._emitChange();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Update a prompt control's value. The value lands in `selectedValues`
|
|
77
|
+
* immediately so the input stays responsive, but validation, error
|
|
78
|
+
* rendering, and the `nile-change` event are debounced — they only
|
|
79
|
+
* fire after `_PROMPT_DEBOUNCE_MS` of keyboard quiet.
|
|
80
|
+
*/
|
|
81
|
+
handlePromptInput(ctrl, value) {
|
|
82
|
+
this.selectedValues = new Map(this.selectedValues).set(ctrl.id, value);
|
|
83
|
+
const existing = this._promptDebounce.get(ctrl.id);
|
|
84
|
+
if (existing)
|
|
85
|
+
clearTimeout(existing);
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
this._promptDebounce.delete(ctrl.id);
|
|
88
|
+
this._validateOrClear(ctrl, value);
|
|
89
|
+
}, NileFilterChart_1._PROMPT_DEBOUNCE_MS);
|
|
90
|
+
this._promptDebounce.set(ctrl.id, timer);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Switch the runtime mode for a prompt control. Leaving NQL clears
|
|
94
|
+
* validation; entering NQL re-validates the current value. The mode
|
|
95
|
+
* toggle is an explicit user action, so any pending typing debounce
|
|
96
|
+
* is cancelled and the change applies immediately (regardless of
|
|
97
|
+
* `validateOn`).
|
|
98
|
+
*/
|
|
99
|
+
setPromptMode(ctrl, mode) {
|
|
100
|
+
if (this.promptModes.get(ctrl.id) === mode)
|
|
101
|
+
return;
|
|
102
|
+
this.promptModes = new Map(this.promptModes).set(ctrl.id, mode);
|
|
103
|
+
const pending = this._promptDebounce.get(ctrl.id);
|
|
104
|
+
if (pending) {
|
|
105
|
+
clearTimeout(pending);
|
|
106
|
+
this._promptDebounce.delete(ctrl.id);
|
|
107
|
+
}
|
|
108
|
+
const value = String(this.selectedValues.get(ctrl.id) ?? '');
|
|
109
|
+
this._validateOrClear(ctrl, value, { explicit: true });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Force-validate a prompt's current value. Used by `validateOn: 'submit'`
|
|
113
|
+
* on Enter — bypasses the debounce and runs validation right away.
|
|
114
|
+
*/
|
|
115
|
+
submitPrompt(ctrl) {
|
|
116
|
+
const pending = this._promptDebounce.get(ctrl.id);
|
|
117
|
+
if (pending) {
|
|
118
|
+
clearTimeout(pending);
|
|
119
|
+
this._promptDebounce.delete(ctrl.id);
|
|
120
|
+
}
|
|
121
|
+
const value = String(this.selectedValues.get(ctrl.id) ?? '');
|
|
122
|
+
this._validateOrClear(ctrl, value, { explicit: true });
|
|
123
|
+
}
|
|
124
|
+
setPromptActiveIndex(id, idx) {
|
|
125
|
+
this.promptActiveIndex = new Map(this.promptActiveIndex).set(id, idx);
|
|
126
|
+
}
|
|
127
|
+
_validateOrClear(ctrl, value, opts = {}) {
|
|
128
|
+
const isNql = this.promptModes.get(ctrl.id) === 'nql';
|
|
129
|
+
if (value.trim() === '') {
|
|
130
|
+
this._clearPromptValidation(ctrl.id);
|
|
131
|
+
this._emitChange();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// In `validateOn: 'submit'` mode, automatic (debounced) ticks just
|
|
135
|
+
// emit the change without validating. Validation runs only when the
|
|
136
|
+
// call is `explicit` — i.e. Enter was pressed or the mode toggled.
|
|
137
|
+
if (isNql && ctrl.queryLanguage?.validateOn === 'submit' && !opts.explicit) {
|
|
138
|
+
this._emitChange();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Bump the validation token so any earlier in-flight parse is ignored.
|
|
142
|
+
const token = (this._validationTokens.get(ctrl.id) ?? 0) + 1;
|
|
143
|
+
this._validationTokens.set(ctrl.id, token);
|
|
144
|
+
this._emitChange();
|
|
145
|
+
validatePromptExpression(value, ctrl).then(({ ast, evaluate }) => {
|
|
146
|
+
if (this._validationTokens.get(ctrl.id) !== token)
|
|
147
|
+
return;
|
|
148
|
+
const errs = new Map(this.promptErrors);
|
|
149
|
+
errs.delete(ctrl.id);
|
|
150
|
+
this.promptErrors = errs;
|
|
151
|
+
this._promptEvaluators.set(ctrl.id, evaluate);
|
|
152
|
+
this._promptAsts.set(ctrl.id, ast);
|
|
153
|
+
this._emitChange();
|
|
154
|
+
}, err => {
|
|
155
|
+
if (this._validationTokens.get(ctrl.id) !== token)
|
|
156
|
+
return;
|
|
157
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
158
|
+
// Basic mode swallows parse errors (suggestions can be partial / unfinished
|
|
159
|
+
// expressions); NQL surfaces them so the user sees what's wrong.
|
|
160
|
+
const errs = new Map(this.promptErrors);
|
|
161
|
+
if (isNql) {
|
|
162
|
+
console.error(`[nile-filter-chart] "${ctrl.id}" parse error:`, error.message);
|
|
163
|
+
errs.set(ctrl.id, error.message);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
errs.delete(ctrl.id);
|
|
167
|
+
}
|
|
168
|
+
this.promptErrors = errs;
|
|
169
|
+
this._promptEvaluators.delete(ctrl.id);
|
|
170
|
+
this._promptAsts.delete(ctrl.id);
|
|
171
|
+
this._emitChange();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
_clearPromptValidation(id) {
|
|
175
|
+
this._validationTokens.set(id, (this._validationTokens.get(id) ?? 0) + 1);
|
|
176
|
+
if (this.promptErrors.has(id)) {
|
|
177
|
+
const errs = new Map(this.promptErrors);
|
|
178
|
+
errs.delete(id);
|
|
179
|
+
this.promptErrors = errs;
|
|
180
|
+
}
|
|
181
|
+
this._promptEvaluators.delete(id);
|
|
182
|
+
this._promptAsts.delete(id);
|
|
183
|
+
}
|
|
38
184
|
// ── Prompt typewriter ────────────────────────────────────────────────────────
|
|
39
185
|
_syncPromptAnimations() {
|
|
40
186
|
const wanted = new Set();
|
|
@@ -52,9 +198,9 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
52
198
|
}
|
|
53
199
|
}
|
|
54
200
|
_startPromptAnimation(ctrl) {
|
|
55
|
-
const phrases =
|
|
201
|
+
const phrases = ctrl.placeholders && ctrl.placeholders.length
|
|
56
202
|
? ctrl.placeholders
|
|
57
|
-
: [
|
|
203
|
+
: ['Ask anything…'];
|
|
58
204
|
const typeSpeed = ctrl.typeSpeedMs ?? 60;
|
|
59
205
|
const pauseAfterType = ctrl.pauseBeforeDeleteMs ?? 1400;
|
|
60
206
|
const pauseBetween = ctrl.pauseBetweenMs ?? 400;
|
|
@@ -66,8 +212,8 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
66
212
|
let nextDelay;
|
|
67
213
|
if (!deleting) {
|
|
68
214
|
charIdx++;
|
|
69
|
-
const next = new Map(this.
|
|
70
|
-
this.
|
|
215
|
+
const next = new Map(this.promptPlaceholder).set(ctrl.id, phrase.slice(0, charIdx));
|
|
216
|
+
this.promptPlaceholder = next;
|
|
71
217
|
if (charIdx >= phrase.length) {
|
|
72
218
|
deleting = true;
|
|
73
219
|
nextDelay = pauseAfterType;
|
|
@@ -78,8 +224,8 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
78
224
|
}
|
|
79
225
|
else {
|
|
80
226
|
charIdx--;
|
|
81
|
-
const next = new Map(this.
|
|
82
|
-
this.
|
|
227
|
+
const next = new Map(this.promptPlaceholder).set(ctrl.id, phrase.slice(0, Math.max(0, charIdx)));
|
|
228
|
+
this.promptPlaceholder = next;
|
|
83
229
|
if (charIdx <= 0) {
|
|
84
230
|
deleting = false;
|
|
85
231
|
phraseIdx = (phraseIdx + 1) % phrases.length;
|
|
@@ -92,7 +238,7 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
92
238
|
this._promptTimers.set(ctrl.id, setTimeout(tick, nextDelay));
|
|
93
239
|
};
|
|
94
240
|
// Kick it off
|
|
95
|
-
this.
|
|
241
|
+
this.promptPlaceholder = new Map(this.promptPlaceholder).set(ctrl.id, '');
|
|
96
242
|
this._promptTimers.set(ctrl.id, setTimeout(tick, 100));
|
|
97
243
|
}
|
|
98
244
|
_stopPromptAnimation(id) {
|
|
@@ -100,14 +246,50 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
100
246
|
if (t)
|
|
101
247
|
clearTimeout(t);
|
|
102
248
|
this._promptTimers.delete(id);
|
|
103
|
-
const next = new Map(this.
|
|
249
|
+
const next = new Map(this.promptPlaceholder);
|
|
104
250
|
next.delete(id);
|
|
105
|
-
this.
|
|
251
|
+
this.promptPlaceholder = next;
|
|
106
252
|
}
|
|
107
253
|
_stopAllPromptAnimations() {
|
|
108
254
|
for (const id of [...this._promptTimers.keys()])
|
|
109
255
|
this._stopPromptAnimation(id);
|
|
110
256
|
}
|
|
257
|
+
// ── Config helpers ───────────────────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Walk the raw config and ensure every control has a unique string `id`.
|
|
260
|
+
* Authors may omit `id`; this fills it in from `label` (slugified) with a
|
|
261
|
+
* collision suffix, or `ctrl_<index>` if `label` is also missing. Mutates
|
|
262
|
+
* the raw control objects in place — consistent with existing behavior
|
|
263
|
+
* (e.g. `aq.autocomplete` is fanned out onto controls below).
|
|
264
|
+
*/
|
|
265
|
+
_normalizeIds() {
|
|
266
|
+
const used = new Set();
|
|
267
|
+
let idx = 0;
|
|
268
|
+
const ensure = (c) => {
|
|
269
|
+
if (typeof c.id === 'string' && c.id.length > 0) {
|
|
270
|
+
used.add(c.id);
|
|
271
|
+
idx++;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const base = slugify(c.label ?? '') || `ctrl_${idx}`;
|
|
275
|
+
let pick = base;
|
|
276
|
+
let n = 2;
|
|
277
|
+
while (used.has(pick))
|
|
278
|
+
pick = `${base}_${n++}`;
|
|
279
|
+
used.add(pick);
|
|
280
|
+
c.id = pick;
|
|
281
|
+
idx++;
|
|
282
|
+
};
|
|
283
|
+
for (const entry of this.config?.chart?.controls ?? []) {
|
|
284
|
+
if (entry.type === 'group') {
|
|
285
|
+
for (const c of entry.controls)
|
|
286
|
+
ensure(c);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
ensure(entry);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
111
293
|
_flatControls() {
|
|
112
294
|
const entries = this.config?.chart?.controls ?? [];
|
|
113
295
|
const out = [];
|
|
@@ -124,7 +306,33 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
124
306
|
_initValues() {
|
|
125
307
|
const controls = this._flatControls();
|
|
126
308
|
const map = new Map();
|
|
309
|
+
const modes = new Map();
|
|
310
|
+
// Single source of truth for suggestion catalogs: `aq.autocomplete`.
|
|
311
|
+
// Fan it out into every prompt-like control's `suggestions` so the
|
|
312
|
+
// renderer (which reads `ctrl.suggestions`) sees the same items the
|
|
313
|
+
// backend shipped under `aq`. Per-control overrides still win — only
|
|
314
|
+
// controls without their own `suggestions` get hydrated.
|
|
315
|
+
const acItems = this.config
|
|
316
|
+
?.aq?.autocomplete;
|
|
317
|
+
if (Array.isArray(acItems)) {
|
|
318
|
+
for (const ctrl of controls) {
|
|
319
|
+
const isPromptLike = ctrl.variant === 'prompt'
|
|
320
|
+
|| ctrl.variant === 'expression'
|
|
321
|
+
|| ctrl.variant === 'hybrid';
|
|
322
|
+
if (isPromptLike && !Array.isArray(ctrl.suggestions)) {
|
|
323
|
+
ctrl.suggestions = acItems;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
127
327
|
for (const ctrl of controls) {
|
|
328
|
+
if ((ctrl.variant === 'prompt' && ctrl.queryLanguage?.enabled)
|
|
329
|
+
|| ctrl.variant === 'expression'
|
|
330
|
+
|| ctrl.variant === 'hybrid') {
|
|
331
|
+
const initialMode = ctrl.variant === 'expression'
|
|
332
|
+
? 'nql'
|
|
333
|
+
: (ctrl.queryLanguage?.mode ?? 'strict') === 'strict' ? 'nql' : 'basic';
|
|
334
|
+
modes.set(ctrl.id, initialMode);
|
|
335
|
+
}
|
|
128
336
|
if (ctrl.value !== undefined) {
|
|
129
337
|
map.set(ctrl.id, ctrl.value);
|
|
130
338
|
if (ctrl.variant === 'comparison') {
|
|
@@ -136,10 +344,11 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
136
344
|
}
|
|
137
345
|
}
|
|
138
346
|
else {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
347
|
+
// slider variant: not yet implemented
|
|
348
|
+
// if (ctrl.variant === 'slider') {
|
|
349
|
+
// map.set(ctrl.id, [ctrl.min ?? 0, ctrl.max ?? 100]);
|
|
350
|
+
// } else
|
|
351
|
+
if (ctrl.variant === 'toggle') {
|
|
143
352
|
const defaults = {};
|
|
144
353
|
(ctrl.options ?? []).forEach(o => { defaults[o.value] = false; });
|
|
145
354
|
map.set(ctrl.id, defaults);
|
|
@@ -159,433 +368,103 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
159
368
|
}
|
|
160
369
|
}
|
|
161
370
|
this.selectedValues = map;
|
|
162
|
-
|
|
163
|
-
_set(id, value) {
|
|
164
|
-
this.selectedValues = new Map(this.selectedValues).set(id, value);
|
|
165
|
-
this._emitChange();
|
|
371
|
+
this.promptModes = modes;
|
|
166
372
|
}
|
|
167
373
|
_emitChange() {
|
|
168
374
|
const filters = {};
|
|
169
|
-
this.selectedValues.forEach((val, id) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
const arr = Array.isArray(current) ? [...current] : [];
|
|
192
|
-
const idx = arr.indexOf(opt.value);
|
|
193
|
-
if (idx === -1)
|
|
194
|
-
arr.push(opt.value);
|
|
195
|
-
else
|
|
196
|
-
arr.splice(idx, 1);
|
|
197
|
-
this._set(ctrl.id, arr);
|
|
198
|
-
}
|
|
199
|
-
}}"
|
|
200
|
-
>${opt.label}</nile-tag>`;
|
|
201
|
-
})}
|
|
202
|
-
</div>`;
|
|
203
|
-
}
|
|
204
|
-
// ── Dropdown ─────────────────────────────────────────────────────────────────
|
|
205
|
-
_renderDropdown(ctrl) {
|
|
206
|
-
const raw = this.selectedValues.get(ctrl.id);
|
|
207
|
-
const current = Array.isArray(raw) ? raw : (raw ? [raw] : []);
|
|
208
|
-
const isMulti = ctrl.selection === 'multi';
|
|
209
|
-
return html `
|
|
210
|
-
<nile-select
|
|
211
|
-
.value="${isMulti ? current : (current[0] ?? '')}"
|
|
212
|
-
?multiple="${isMulti}"
|
|
213
|
-
searchEnabled
|
|
214
|
-
placeholder="${ctrl.placeholder ?? `Select ${ctrl.label || 'option'}…`}"
|
|
215
|
-
@nile-change="${(e) => {
|
|
216
|
-
e.stopPropagation();
|
|
217
|
-
this._set(ctrl.id, e.detail.value);
|
|
218
|
-
}}"
|
|
219
|
-
>
|
|
220
|
-
${(ctrl.options ?? []).map(opt => html `
|
|
221
|
-
<nile-option
|
|
222
|
-
value="${opt.value}"
|
|
223
|
-
?selected="${isMulti ? current.includes(opt.value) : current[0] === opt.value}"
|
|
224
|
-
>${opt.label}</nile-option>`)}
|
|
225
|
-
</nile-select>`;
|
|
226
|
-
}
|
|
227
|
-
// ── Segmented ────────────────────────────────────────────────────────────────
|
|
228
|
-
_renderSegmented(ctrl) {
|
|
229
|
-
return html `
|
|
230
|
-
<div class="fc-segmented-scroll">
|
|
231
|
-
<nile-button-toggle-group
|
|
232
|
-
.value="${this.selectedValues.get(ctrl.id) ?? ''}"
|
|
233
|
-
?multiple="${ctrl.selection === 'multi'}"
|
|
234
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
|
|
235
|
-
>
|
|
236
|
-
${(ctrl.options ?? []).map(opt => html `
|
|
237
|
-
<nile-button-toggle value="${opt.value}">${opt.label}</nile-button-toggle>`)}
|
|
238
|
-
</nile-button-toggle-group>
|
|
239
|
-
</div>`;
|
|
240
|
-
}
|
|
241
|
-
// ── Radio ────────────────────────────────────────────────────────────────────
|
|
242
|
-
_renderRadio(ctrl) {
|
|
243
|
-
const current = this.selectedValues.get(ctrl.id) ?? '';
|
|
244
|
-
return html `
|
|
245
|
-
<nile-radio-group
|
|
246
|
-
.value="${current}"
|
|
247
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
|
|
248
|
-
>
|
|
249
|
-
${(ctrl.options ?? []).map(opt => html `
|
|
250
|
-
<nile-radio value="${opt.value}" ?checked="${current === opt.value}">${opt.label}</nile-radio>`)}
|
|
251
|
-
</nile-radio-group>`;
|
|
252
|
-
}
|
|
253
|
-
// ── Toggle ───────────────────────────────────────────────────────────────────
|
|
254
|
-
_renderToggle(ctrl) {
|
|
255
|
-
const current = this.selectedValues.get(ctrl.id) ?? {};
|
|
256
|
-
return html `
|
|
257
|
-
<div class="fc-toggle-group">
|
|
258
|
-
${(ctrl.options ?? []).map(opt => html `
|
|
259
|
-
<nile-slide-toggle
|
|
260
|
-
label="${opt.label}"
|
|
261
|
-
?isChecked="${!!current[opt.value]}"
|
|
262
|
-
fullWidth
|
|
263
|
-
@nile-change="${(e) => {
|
|
264
|
-
e.stopPropagation();
|
|
265
|
-
const updated = { ...current, [opt.value]: e.detail.checked };
|
|
266
|
-
this._set(ctrl.id, updated);
|
|
267
|
-
}}"
|
|
268
|
-
></nile-slide-toggle>`)}
|
|
269
|
-
</div>`;
|
|
270
|
-
}
|
|
271
|
-
// ── Slider ───────────────────────────────────────────────────────────────────
|
|
272
|
-
// private _renderSlider(ctrl: FilterControl): TemplateResult {
|
|
273
|
-
// const min = ctrl.min ?? 0;
|
|
274
|
-
// const max = ctrl.max ?? 100;
|
|
275
|
-
// const val: number[] = this.selectedValues.get(ctrl.id) ?? [min, max];
|
|
276
|
-
// const fmt = (n: number) => `${ctrl.prefix ?? ''}${n.toLocaleString()}${ctrl.suffix ?? ''}`;
|
|
277
|
-
//
|
|
278
|
-
// return html`
|
|
279
|
-
// <div class="fc-slider-wrap">
|
|
280
|
-
// <div class="fc-slider-label">
|
|
281
|
-
// <span class="fc-slider-range">${fmt(val[0])} — <strong>${fmt(val[1])}</strong></span>
|
|
282
|
-
// </div>
|
|
283
|
-
// <nile-slider
|
|
284
|
-
// .minValue="${min}"
|
|
285
|
-
// .maxValue="${max}"
|
|
286
|
-
// ?rangeSlider="${true}"
|
|
287
|
-
// .rangeOneValue="${val[0]}"
|
|
288
|
-
// .rangeTwoValue="${val[1]}"
|
|
289
|
-
// @nile-button-first-change-end="${(e: CustomEvent) => {
|
|
290
|
-
// e.stopPropagation();
|
|
291
|
-
// this._set(ctrl.id, [e.detail.value, val[1]]);
|
|
292
|
-
// }}"
|
|
293
|
-
// @nile-button-last-change-end="${(e: CustomEvent) => {
|
|
294
|
-
// e.stopPropagation();
|
|
295
|
-
// this._set(ctrl.id, [val[0], e.detail.value]);
|
|
296
|
-
// }}"
|
|
297
|
-
// >
|
|
298
|
-
// <span slot="prefix">${fmt(min)}</span>
|
|
299
|
-
// <span slot="suffix">${fmt(max)}</span>
|
|
300
|
-
// </nile-slider>
|
|
301
|
-
// </div>`;
|
|
302
|
-
// }
|
|
303
|
-
// ── Search ───────────────────────────────────────────────────────────────────
|
|
304
|
-
_renderSearch(ctrl) {
|
|
305
|
-
return html `
|
|
306
|
-
<nile-input
|
|
307
|
-
type="search"
|
|
308
|
-
placeholder="${ctrl.placeholder ?? `Search ${ctrl.label}…`}"
|
|
309
|
-
.value="${this.selectedValues.get(ctrl.id) ?? ''}"
|
|
310
|
-
clearable
|
|
311
|
-
@nile-input="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
|
|
312
|
-
></nile-input>`;
|
|
313
|
-
}
|
|
314
|
-
// ── Comparison ───────────────────────────────────────────────────────────────
|
|
315
|
-
_renderComparison(ctrl) {
|
|
316
|
-
const valA = this.selectedValues.get(ctrl.id) ?? '';
|
|
317
|
-
const valB = this.selectedValues.get(`${ctrl.id}__b`) ?? '';
|
|
318
|
-
return html `
|
|
319
|
-
<div class="fc-comparison">
|
|
320
|
-
<nile-select
|
|
321
|
-
.value="${valA}"
|
|
322
|
-
placeholder="Period A"
|
|
323
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
|
|
324
|
-
>
|
|
325
|
-
${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
326
|
-
</nile-select>
|
|
327
|
-
<span class="fc-vs">VS</span>
|
|
328
|
-
<nile-select
|
|
329
|
-
.value="${valB}"
|
|
330
|
-
placeholder="Period B"
|
|
331
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__b`, e.detail.value); }}"
|
|
332
|
-
>
|
|
333
|
-
${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
334
|
-
</nile-select>
|
|
335
|
-
</div>`;
|
|
336
|
-
}
|
|
337
|
-
// ── Threshold ────────────────────────────────────────────────────────────────
|
|
338
|
-
_renderThreshold(ctrl) {
|
|
339
|
-
const metric = this.selectedValues.get(ctrl.id) ?? '';
|
|
340
|
-
const op = this.selectedValues.get(`${ctrl.id}__op`) ?? '>';
|
|
341
|
-
const val = this.selectedValues.get(`${ctrl.id}__val`) ?? '';
|
|
342
|
-
const operators = [
|
|
343
|
-
{ value: '>', label: '> (greater)' },
|
|
344
|
-
{ value: '>=', label: '>= (min)' },
|
|
345
|
-
{ value: '<', label: '< (less)' },
|
|
346
|
-
{ value: '<=', label: '<= (max)' },
|
|
347
|
-
{ value: '=', label: '= (equals)' },
|
|
348
|
-
{ value: '!=', label: '≠ (not)' },
|
|
349
|
-
];
|
|
350
|
-
const metricLabel = (ctrl.options ?? []).find(o => o.value === metric)?.label ?? metric;
|
|
351
|
-
const hasPreview = metric && val !== '';
|
|
352
|
-
const previewText = hasPreview ? `${metricLabel} ${op} ${val}` : 'Set metric, condition, and value above';
|
|
353
|
-
return html `
|
|
354
|
-
<div class="fc-threshold">
|
|
355
|
-
<div class="fc-threshold-metric-row">
|
|
356
|
-
<span class="fc-threshold-where">WHERE</span>
|
|
357
|
-
<div class="fc-threshold-field fc-threshold-field--metric">
|
|
358
|
-
<span class="fc-threshold-field-label">Metric</span>
|
|
359
|
-
<nile-select
|
|
360
|
-
.value="${metric}"
|
|
361
|
-
placeholder="Select metric…"
|
|
362
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(ctrl.id, e.detail.value); }}"
|
|
363
|
-
>
|
|
364
|
-
${(ctrl.options ?? []).map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
365
|
-
</nile-select>
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
<div class="fc-threshold-cond-row">
|
|
369
|
-
<div class="fc-threshold-field fc-threshold-field--op">
|
|
370
|
-
<span class="fc-threshold-field-label">Condition</span>
|
|
371
|
-
<nile-select
|
|
372
|
-
.value="${op}"
|
|
373
|
-
@nile-change="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__op`, e.detail.value); }}"
|
|
374
|
-
>
|
|
375
|
-
${operators.map(o => html `<nile-option value="${o.value}">${o.label}</nile-option>`)}
|
|
376
|
-
</nile-select>
|
|
377
|
-
</div>
|
|
378
|
-
<div class="fc-threshold-field fc-threshold-field--val">
|
|
379
|
-
<span class="fc-threshold-field-label">Value</span>
|
|
380
|
-
<nile-input
|
|
381
|
-
type="number"
|
|
382
|
-
placeholder="e.g. 10000"
|
|
383
|
-
.value="${String(val)}"
|
|
384
|
-
@nile-input="${(e) => { e.stopPropagation(); this._set(`${ctrl.id}__val`, e.detail.value); }}"
|
|
385
|
-
></nile-input>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
<div class="fc-threshold-preview ${hasPreview ? '' : 'fc-threshold-preview--empty'}">
|
|
389
|
-
${hasPreview ? html `<strong>Filter:</strong>` : nothing}
|
|
390
|
-
${previewText}
|
|
391
|
-
</div>
|
|
392
|
-
</div>`;
|
|
393
|
-
}
|
|
394
|
-
// ── Tree ─────────────────────────────────────────────────────────────────────
|
|
395
|
-
// private _renderTree(ctrl: FilterControl): TemplateResult {
|
|
396
|
-
// const renderNodes = (nodes: TreeNode[]): TemplateResult[] =>
|
|
397
|
-
// nodes.map(node => html`
|
|
398
|
-
// <nile-tree-item value="${node.value}" ?expanded="${node.expanded ?? false}">
|
|
399
|
-
// <span>${node.label}</span>
|
|
400
|
-
// ${node.children ? renderNodes(node.children) : nothing}
|
|
401
|
-
// </nile-tree-item>`);
|
|
402
|
-
//
|
|
403
|
-
// return html`
|
|
404
|
-
// <nile-tree
|
|
405
|
-
// selection="${ctrl.selection === 'multi' ? 'multiple' : 'single'}"
|
|
406
|
-
// @nile-selection-change="${(e: CustomEvent) => {
|
|
407
|
-
// e.stopPropagation();
|
|
408
|
-
// this._set(ctrl.id, e.detail?.detail?.selection ?? []);
|
|
409
|
-
// }}"
|
|
410
|
-
// >
|
|
411
|
-
// ${renderNodes(ctrl.treeData ?? [])}
|
|
412
|
-
// </nile-tree>`;
|
|
413
|
-
// }
|
|
414
|
-
// ── Preset ───────────────────────────────────────────────────────────────────
|
|
415
|
-
_renderPreset(ctrl) {
|
|
416
|
-
const current = this.selectedValues.get(ctrl.id);
|
|
417
|
-
return html `
|
|
418
|
-
<div class="fc-preset-list">
|
|
419
|
-
${(ctrl.options ?? []).map(opt => html `
|
|
420
|
-
<button
|
|
421
|
-
class="fc-preset-item ${current === opt.value ? 'fc-preset-item--selected' : ''}"
|
|
422
|
-
@click="${() => this._set(ctrl.id, opt.value)}"
|
|
423
|
-
>
|
|
424
|
-
${opt.icon ? html `<span class="fc-preset-icon">${opt.icon}</span>` : nothing}
|
|
425
|
-
${opt.label}
|
|
426
|
-
</button>`)}
|
|
427
|
-
</div>`;
|
|
428
|
-
}
|
|
429
|
-
static async _getCompileExpression() {
|
|
430
|
-
if (!NileFilterChart_1._filtrexLoading) {
|
|
431
|
-
NileFilterChart_1._filtrexLoading = import('filtrex').then(m => {
|
|
432
|
-
const fn = m.compileExpression
|
|
433
|
-
?? m.default?.compileExpression;
|
|
434
|
-
if (typeof fn !== 'function') {
|
|
435
|
-
throw new Error('filtrex: compileExpression export not found');
|
|
436
|
-
}
|
|
437
|
-
return fn;
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
return NileFilterChart_1._filtrexLoading;
|
|
441
|
-
}
|
|
442
|
-
static async _getJsep() {
|
|
443
|
-
if (!NileFilterChart_1._jsepLoading) {
|
|
444
|
-
NileFilterChart_1._jsepLoading = import('jsep').then(m => {
|
|
445
|
-
const candidate = m.default ?? m;
|
|
446
|
-
if (typeof candidate !== 'function') {
|
|
447
|
-
throw new Error('jsep: function export not found');
|
|
448
|
-
}
|
|
449
|
-
const fn = candidate;
|
|
450
|
-
try {
|
|
451
|
-
fn.addBinaryOp?.('and', 2);
|
|
452
|
-
fn.addBinaryOp?.('or', 1);
|
|
453
|
-
fn.addBinaryOp?.('in', 6);
|
|
454
|
-
fn.addUnaryOp?.('not');
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
}
|
|
458
|
-
return fn;
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
return NileFilterChart_1._jsepLoading;
|
|
462
|
-
}
|
|
463
|
-
async _handleSubmit(value, ctrl) {
|
|
464
|
-
const ql = ctrl.queryLanguage;
|
|
465
|
-
if (!ql?.enabled || (ql.mode ?? 'auto') === 'auto') {
|
|
466
|
-
this.emit('nile-prompt-submit', { id: ctrl.id, value });
|
|
467
|
-
if (typeof ctrl.onSubmit === 'function')
|
|
468
|
-
ctrl.onSubmit(value, ctrl.id);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
try {
|
|
472
|
-
const jsep = await NileFilterChart_1._getJsep();
|
|
473
|
-
const ast = jsep(value);
|
|
474
|
-
const compileExpression = await NileFilterChart_1._getCompileExpression();
|
|
475
|
-
const extraFunctions = {
|
|
476
|
-
...NileFilterChart_1._builtInFiltrexFns,
|
|
477
|
-
...(ql.extraFunctions ?? {}),
|
|
478
|
-
};
|
|
479
|
-
const evaluate = compileExpression(value, {
|
|
480
|
-
extraFunctions,
|
|
481
|
-
...(ql.customProp ? { customProp: ql.customProp } : {}),
|
|
482
|
-
});
|
|
483
|
-
const json = { source: value, ast };
|
|
484
|
-
this.emit('nile-prompt-submit', { id: ctrl.id, value: json, evaluate });
|
|
485
|
-
if (typeof ctrl.onSubmit === 'function')
|
|
486
|
-
ctrl.onSubmit(json, ctrl.id, evaluate);
|
|
375
|
+
this.selectedValues.forEach((val, id) => {
|
|
376
|
+
// Prompt controls in NQL mode with a successful parse emit the
|
|
377
|
+
// structured `{ source, ast }` payload so downstream consumers get
|
|
378
|
+
// both the typed text and the parsed shape. Basic mode (and NQL
|
|
379
|
+
// pre-validation / parse-failure) emit the raw string — the same
|
|
380
|
+
// shape every other variant uses.
|
|
381
|
+
const mode = this.promptModes.get(id);
|
|
382
|
+
const ast = this._promptAsts.get(id);
|
|
383
|
+
if (mode === 'nql' && ast && typeof val === 'string' && val.trim() !== '') {
|
|
384
|
+
filters[id] = { source: val, ast };
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
filters[id] = val;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
const detail = { filters };
|
|
391
|
+
if (this.promptErrors.size > 0) {
|
|
392
|
+
const errors = {};
|
|
393
|
+
this.promptErrors.forEach((msg, id) => { errors[id] = msg; });
|
|
394
|
+
detail.errors = errors;
|
|
487
395
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
396
|
+
if (this._promptEvaluators.size > 0) {
|
|
397
|
+
// Only surface evaluators for controls whose active mode is NQL.
|
|
398
|
+
// In Basic / Prompt mode the consumer expects a plain-string payload
|
|
399
|
+
// — leaking the cached evaluator from the last NQL pass would lie
|
|
400
|
+
// about what the user has selected.
|
|
401
|
+
const evaluators = {};
|
|
402
|
+
this._promptEvaluators.forEach((fn, id) => {
|
|
403
|
+
if (this.promptModes.get(id) === 'nql')
|
|
404
|
+
evaluators[id] = fn;
|
|
494
405
|
});
|
|
495
|
-
if (
|
|
496
|
-
|
|
497
|
-
}
|
|
406
|
+
if (Object.keys(evaluators).length > 0)
|
|
407
|
+
detail.evaluators = evaluators;
|
|
498
408
|
}
|
|
409
|
+
this.emit('nile-change', detail);
|
|
499
410
|
}
|
|
500
|
-
|
|
501
|
-
const value = String(this.selectedValues.get(ctrl.id) ?? '');
|
|
502
|
-
const animated = this._promptPlaceholder.get(ctrl.id) ?? '';
|
|
503
|
-
const styleParts = [];
|
|
504
|
-
if (ctrl.gradientColors && ctrl.gradientColors.length > 0) {
|
|
505
|
-
const dir = ctrl.gradientDirection ?? '90deg';
|
|
506
|
-
styleParts.push(`--fc-prompt-gradient: linear-gradient(${dir}, ${ctrl.gradientColors.join(', ')})`);
|
|
507
|
-
}
|
|
508
|
-
else if (ctrl.gradientDirection) {
|
|
509
|
-
// Direction-only override: keep the default palette but rotate it.
|
|
510
|
-
styleParts.push(`--fc-prompt-gradient: linear-gradient(${ctrl.gradientDirection},` +
|
|
511
|
-
' #2563eb 0%, #3b82f6 18%, #06b6d4 36%, #6366f1 54%,' +
|
|
512
|
-
' #8b5cf6 72%, #2563eb 100%)');
|
|
513
|
-
}
|
|
514
|
-
if (typeof ctrl.gradientSpeedMs === 'number' && ctrl.gradientSpeedMs > 0) {
|
|
515
|
-
styleParts.push(`--fc-prompt-gradient-speed: ${ctrl.gradientSpeedMs}ms`);
|
|
516
|
-
}
|
|
517
|
-
const inlineStyle = styleParts.length ? styleParts.join('; ') + ';' : '';
|
|
518
|
-
return html `
|
|
519
|
-
<div class="fc-prompt" part="filter-prompt" style="${inlineStyle}">
|
|
520
|
-
<div class="fc-prompt__inner">
|
|
521
|
-
<input
|
|
522
|
-
class="fc-prompt__input"
|
|
523
|
-
type="text"
|
|
524
|
-
autocomplete="off"
|
|
525
|
-
spellcheck="false"
|
|
526
|
-
.value="${value}"
|
|
527
|
-
placeholder="${animated}"
|
|
528
|
-
@input="${(e) => {
|
|
529
|
-
const t = e.target;
|
|
530
|
-
// Update the internal map (also fires the bulk `nile-change`
|
|
531
|
-
// event with every control's value).
|
|
532
|
-
this._set(ctrl.id, t.value);
|
|
533
|
-
// Dedicated, prompt-specific stream — every keystroke emits
|
|
534
|
-
// just this control's typed string so consumers don't have to
|
|
535
|
-
// sift through the full filters map for live updates (e.g.
|
|
536
|
-
// debounced AI suggestions, auto-complete, character counters).
|
|
537
|
-
this.emit('nile-prompt-input', { id: ctrl.id, value: t.value });
|
|
538
|
-
// Optional inline callback supplied via the config — same
|
|
539
|
-
// payload, shaped as a function call instead of an event.
|
|
540
|
-
if (typeof ctrl.onType === 'function') {
|
|
541
|
-
ctrl.onType(t.value, ctrl.id);
|
|
542
|
-
}
|
|
543
|
-
}}"
|
|
544
|
-
@keydown="${(e) => {
|
|
545
|
-
if (e.key === 'Enter') {
|
|
546
|
-
const t = e.target;
|
|
547
|
-
this._handleSubmit(t.value, ctrl);
|
|
548
|
-
}
|
|
549
|
-
}}"
|
|
550
|
-
/>
|
|
551
|
-
</div>
|
|
552
|
-
</div>`;
|
|
553
|
-
}
|
|
554
|
-
// ── Card wrapper ─────────────────────────────────────────────────────────────
|
|
411
|
+
// ── Render ───────────────────────────────────────────────────────────────────
|
|
555
412
|
_renderControl(ctrl) {
|
|
556
413
|
let body;
|
|
557
414
|
switch (ctrl.variant) {
|
|
558
415
|
case 'badge':
|
|
559
|
-
body = this
|
|
416
|
+
body = renderBadge(this, ctrl);
|
|
560
417
|
break;
|
|
561
418
|
case 'dropdown':
|
|
562
|
-
body = this
|
|
419
|
+
body = renderDropdown(this, ctrl);
|
|
563
420
|
break;
|
|
564
421
|
case 'segmented':
|
|
565
|
-
body = this
|
|
422
|
+
body = renderSegmented(this, ctrl);
|
|
566
423
|
break;
|
|
567
424
|
case 'radio':
|
|
568
|
-
body = this
|
|
425
|
+
body = renderRadio(this, ctrl);
|
|
569
426
|
break;
|
|
570
427
|
case 'toggle':
|
|
571
|
-
body = this
|
|
428
|
+
body = renderToggle(this, ctrl);
|
|
572
429
|
break;
|
|
573
|
-
// case 'slider': body = this._renderSlider(ctrl); break;
|
|
574
430
|
case 'search':
|
|
575
|
-
body = this
|
|
431
|
+
body = renderSearch(this, ctrl);
|
|
576
432
|
break;
|
|
577
433
|
case 'comparison':
|
|
578
|
-
body = this
|
|
434
|
+
body = renderComparison(this, ctrl);
|
|
579
435
|
break;
|
|
580
436
|
case 'threshold':
|
|
581
|
-
body = this
|
|
437
|
+
body = renderThreshold(this, ctrl);
|
|
582
438
|
break;
|
|
583
|
-
// case 'tree': body = this._renderTree(ctrl); break;
|
|
584
439
|
case 'preset':
|
|
585
|
-
body = this
|
|
440
|
+
body = renderPreset(this, ctrl);
|
|
586
441
|
break;
|
|
587
442
|
case 'prompt':
|
|
588
|
-
body = this
|
|
443
|
+
body = renderPrompt(this, ctrl);
|
|
444
|
+
break;
|
|
445
|
+
case 'expression':
|
|
446
|
+
body = renderPrompt(this, {
|
|
447
|
+
...ctrl,
|
|
448
|
+
queryLanguage: {
|
|
449
|
+
...(ctrl.queryLanguage ?? {}),
|
|
450
|
+
enabled: true,
|
|
451
|
+
mode: 'strict',
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
break;
|
|
455
|
+
case 'hybrid':
|
|
456
|
+
body = renderPrompt(this, {
|
|
457
|
+
...ctrl,
|
|
458
|
+
queryLanguage: {
|
|
459
|
+
...(ctrl.queryLanguage ?? {}),
|
|
460
|
+
enabled: true,
|
|
461
|
+
mode: ctrl.queryLanguage?.mode ?? 'strict',
|
|
462
|
+
toggle: {
|
|
463
|
+
basic: { label: ctrl.queryLanguage?.toggle?.basic?.label ?? 'Prompt' },
|
|
464
|
+
nql: { label: ctrl.queryLanguage?.toggle?.nql?.label ?? 'Expression' },
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
});
|
|
589
468
|
break;
|
|
590
469
|
default: body = html ``;
|
|
591
470
|
}
|
|
@@ -596,6 +475,18 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
596
475
|
<div class="fc-control__body">${body}</div>
|
|
597
476
|
</div>`;
|
|
598
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* If any prompt control in `controls` has its toggle enabled and is currently
|
|
480
|
+
* in NQL mode, collapse the visible set down to just that prompt — Basic mode
|
|
481
|
+
* shows everything, JQL mode shows only the expression input.
|
|
482
|
+
*/
|
|
483
|
+
_filterByPromptMode(controls) {
|
|
484
|
+
const nqlPrompt = controls.find(c => ((c.variant === 'prompt' && c.queryLanguage?.enabled)
|
|
485
|
+
|| c.variant === 'hybrid'
|
|
486
|
+
|| c.variant === 'expression')
|
|
487
|
+
&& this.promptModes.get(c.id) === 'nql');
|
|
488
|
+
return nqlPrompt ? [nqlPrompt] : controls;
|
|
489
|
+
}
|
|
599
490
|
_renderGroup(group) {
|
|
600
491
|
const collapsed = this.collapsedGroups.has(group.label);
|
|
601
492
|
const toggle = () => {
|
|
@@ -606,6 +497,7 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
606
497
|
next.add(group.label);
|
|
607
498
|
this.collapsedGroups = next;
|
|
608
499
|
};
|
|
500
|
+
const visible = this._filterByPromptMode(group.controls);
|
|
609
501
|
return html `
|
|
610
502
|
<div class="fc-group" part="filter-group">
|
|
611
503
|
<div class="fc-group__header ${group.collapsible ? 'fc-group__header--collapsible' : ''}"
|
|
@@ -620,39 +512,32 @@ let NileFilterChart = NileFilterChart_1 = class NileFilterChart extends NileElem
|
|
|
620
512
|
</div>
|
|
621
513
|
${collapsed ? nothing : html `
|
|
622
514
|
<div class="fc-group__body">
|
|
623
|
-
${
|
|
515
|
+
${visible.map(ctrl => this._renderControl(ctrl))}
|
|
624
516
|
</div>`}
|
|
625
517
|
</div>`;
|
|
626
518
|
}
|
|
627
519
|
render() {
|
|
520
|
+
this._normalizeIds();
|
|
628
521
|
const entries = this.config?.chart?.controls ?? [];
|
|
522
|
+
// Top-level prompt-mode collapse: if a top-level prompt is in NQL mode,
|
|
523
|
+
// hide its top-level sibling controls (groups stay; their own collapse
|
|
524
|
+
// happens inside `_renderGroup`).
|
|
525
|
+
const topLevelControls = entries.filter(e => e.type !== 'group');
|
|
526
|
+
const visibleTop = new Set(this._filterByPromptMode(topLevelControls));
|
|
629
527
|
return html `
|
|
630
528
|
<div class="fc-root" part="filter-root">
|
|
631
|
-
${entries.map(entry =>
|
|
632
|
-
|
|
633
|
-
|
|
529
|
+
${entries.map(entry => {
|
|
530
|
+
if (entry.type === 'group') {
|
|
531
|
+
return this._renderGroup(entry);
|
|
532
|
+
}
|
|
533
|
+
const ctrl = entry;
|
|
534
|
+
return visibleTop.has(ctrl) ? this._renderControl(ctrl) : nothing;
|
|
535
|
+
})}
|
|
634
536
|
</div>`;
|
|
635
537
|
}
|
|
636
538
|
};
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
startsWith: (s, p) => String(s ?? '').startsWith(String(p ?? '')),
|
|
640
|
-
endsWith: (s, p) => String(s ?? '').endsWith(String(p ?? '')),
|
|
641
|
-
lower: (s) => String(s ?? '').toLowerCase(),
|
|
642
|
-
upper: (s) => String(s ?? '').toUpperCase(),
|
|
643
|
-
len: (s) => (s == null ? 0 : Array.isArray(s) ? s.length : String(s).length),
|
|
644
|
-
isEmpty: (s) => s == null || s === '' || (Array.isArray(s) && s.length === 0),
|
|
645
|
-
isNotEmpty: (s) => !(s == null || s === '' || (Array.isArray(s) && s.length === 0)),
|
|
646
|
-
between: (n, lo, hi) => Number(n) >= Number(lo) && Number(n) <= Number(hi),
|
|
647
|
-
year: (d) => new Date(d).getFullYear(),
|
|
648
|
-
month: (d) => new Date(d).getMonth() + 1,
|
|
649
|
-
day: (d) => new Date(d).getDate(),
|
|
650
|
-
daysAgo: (d) => (Date.now() - +new Date(d)) / 86400000,
|
|
651
|
-
matches: (s, re) => new RegExp(String(re ?? '')).test(String(s ?? '')),
|
|
652
|
-
coalesce: (...xs) => xs.find(x => x != null && x !== '') ?? null,
|
|
653
|
-
};
|
|
654
|
-
NileFilterChart._filtrexLoading = null;
|
|
655
|
-
NileFilterChart._jsepLoading = null;
|
|
539
|
+
/** Pause after the last keystroke before validating and emitting nile-change. */
|
|
540
|
+
NileFilterChart._PROMPT_DEBOUNCE_MS = 500;
|
|
656
541
|
__decorate([
|
|
657
542
|
property({ attribute: false })
|
|
658
543
|
], NileFilterChart.prototype, "config", void 0);
|
|
@@ -664,7 +549,16 @@ __decorate([
|
|
|
664
549
|
], NileFilterChart.prototype, "collapsedGroups", void 0);
|
|
665
550
|
__decorate([
|
|
666
551
|
state()
|
|
667
|
-
], NileFilterChart.prototype, "
|
|
552
|
+
], NileFilterChart.prototype, "promptPlaceholder", void 0);
|
|
553
|
+
__decorate([
|
|
554
|
+
state()
|
|
555
|
+
], NileFilterChart.prototype, "promptErrors", void 0);
|
|
556
|
+
__decorate([
|
|
557
|
+
state()
|
|
558
|
+
], NileFilterChart.prototype, "promptModes", void 0);
|
|
559
|
+
__decorate([
|
|
560
|
+
state()
|
|
561
|
+
], NileFilterChart.prototype, "promptActiveIndex", void 0);
|
|
668
562
|
NileFilterChart = NileFilterChart_1 = __decorate([
|
|
669
563
|
customElement('nile-filter-chart')
|
|
670
564
|
], NileFilterChart);
|