@emuanalytics/flow-rdf-ui 4.1.28 → 4.2.0

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.
@@ -13,6 +13,9 @@ const styles = css`
13
13
  --day-color-hover: var(--flow-ui-calendar-day-color-hover);
14
14
  --day-color-selected: var(--flow-ui-calendar-day-color-selected);
15
15
  --day-color-today: var(--flow-ui-calendar-day-color-today);
16
+ --day-color-in-range: var(--flow-ui-color-primary-100);
17
+ --day-gap: var(--flow-ui-spacing-2x-small);
18
+ --month-gap: var(--flow-ui-spacing-large);
16
19
  }
17
20
 
18
21
  .calendar {
@@ -43,21 +46,52 @@ const styles = css`
43
46
  --day-font-size: var(--flow-ui-font-size-medium);
44
47
  }
45
48
 
49
+ /* Two-month view: each month is --width wide */
50
+ .calendar--two-months {
51
+ width: calc(var(--width) * 2 + var(--month-gap));
52
+ }
53
+
54
+ .calendar__months {
55
+ flex: 1;
56
+ display: flex;
57
+ gap: var(--month-gap);
58
+ }
59
+
60
+ .calendar__month {
61
+ flex: 1;
62
+ min-width: 0;
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--flow-ui-spacing-x-small);
66
+ }
67
+
68
+ /* The months can have different numbers of weeks, so don't stretch the shorter one */
69
+ .calendar--two-months .calendar__months {
70
+ align-items: start;
71
+ }
72
+
46
73
  .calendar__header {
47
74
  display: flex;
48
75
  align-items: center;
49
76
  }
50
77
 
78
+ /* Keeps the space of the previous or next button that the other month has, so the month label stays centered */
79
+ .calendar__header-button--hidden {
80
+ visibility: hidden;
81
+ }
82
+
51
83
  .calendar__header-month {
52
84
  flex: 1;
53
85
  text-align: center;
86
+ /* Match the month and year selects in the first month */
87
+ font-size: var(--flow-ui-control-font-size-medium);
54
88
  }
55
89
 
56
90
  .calendar__grid {
57
91
  flex: 1;
58
92
  display: grid;
59
93
  grid-template-columns: repeat(7, 1fr);
60
- gap: var(--flow-ui-spacing-2x-small);
94
+ gap: var(--day-gap);
61
95
  }
62
96
 
63
97
  .calendar__header-day {
@@ -79,10 +113,22 @@ const styles = css`
79
113
  font-size: var(--day-font-size);
80
114
  }
81
115
 
82
- .calendar__day:hover:not(.calendar__day--selected) {
116
+ .calendar__day:hover:not(
117
+ .calendar__day--selected,
118
+ .calendar__day--range-start,
119
+ .calendar__day--range-last,
120
+ .calendar__day--disabled
121
+ ) {
83
122
  background-color: var(--day-color-hover);
84
123
  }
85
124
 
125
+ /* Outside min or max, or too far from the pending day for max-range */
126
+ .calendar__day--disabled {
127
+ opacity: 0.4;
128
+ cursor: not-allowed;
129
+ text-decoration: line-through;
130
+ }
131
+
86
132
  .calendar__day--filler {
87
133
  color: var(--day-color-filler);
88
134
  }
@@ -96,6 +142,71 @@ const styles = css`
96
142
  background-color: var(--day-color-selected);
97
143
  }
98
144
 
145
+ /*
146
+ * Date range. Each day in the range draws its part of the range band with a pseudo-element behind the day button, so
147
+ * the day's own background (the hover color, or the selected color at the ends of the range) is drawn on top. The
148
+ * band extends into the grid gap on both sides so that it is continuous across a week row, and has rounded ends at
149
+ * the start and end of each row.
150
+ */
151
+
152
+ .calendar__grid {
153
+ /* Keep the range band (z-index: -1) above the backgrounds of the calendar's ancestors */
154
+ isolation: isolate;
155
+ }
156
+
157
+ .calendar__day--in-range {
158
+ position: relative;
159
+ }
160
+
161
+ .calendar__day--in-range::before {
162
+ content: '';
163
+ position: absolute;
164
+ z-index: -1;
165
+ inset-block: 0;
166
+ inset-inline: calc(var(--day-gap) / -2);
167
+ background-color: var(--day-color-in-range);
168
+ }
169
+
170
+ .calendar__day--in-range.calendar__day--row-start::before {
171
+ inset-inline-start: 0;
172
+ border-start-start-radius: var(--flow-ui-border-radius-pill);
173
+ border-end-start-radius: var(--flow-ui-border-radius-pill);
174
+ }
175
+
176
+ .calendar__day--in-range.calendar__day--row-end::before {
177
+ inset-inline-end: 0;
178
+ border-start-end-radius: var(--flow-ui-border-radius-pill);
179
+ border-end-end-radius: var(--flow-ui-border-radius-pill);
180
+ }
181
+
182
+ /* The band starts and ends at the center of the range's end days, under the selected color */
183
+ .calendar__day--in-range.calendar__day--range-start::before {
184
+ inset-inline-start: 50%;
185
+ border-start-start-radius: 0;
186
+ border-end-start-radius: 0;
187
+ }
188
+
189
+ .calendar__day--in-range.calendar__day--range-last::before {
190
+ inset-inline-end: 50%;
191
+ border-start-end-radius: 0;
192
+ border-end-end-radius: 0;
193
+ }
194
+
195
+ .calendar__day--range-start.calendar__day--range-last::before {
196
+ content: none;
197
+ }
198
+
199
+ /* The filler color doesn't contrast enough with the range band, so mix it with the text color */
200
+ .calendar__day--filler.calendar__day--in-range:not(.calendar__day--range-start, .calendar__day--range-last) {
201
+ color: color-mix(in oklab, var(--day-color-filler), currentColor);
202
+ }
203
+
204
+ .calendar__day--range-start,
205
+ .calendar__day--range-last {
206
+ color: var(--flow-ui-color-background);
207
+ background-color: var(--day-color-selected);
208
+ }
209
+
99
210
  .calendar__header {
100
211
  justify-content: space-between;
101
212
  padding-inline: var(--flow-ui-spacing-small);
@@ -111,6 +222,35 @@ const styles = css`
111
222
  align-items: center;
112
223
  }
113
224
 
225
+ /* The Start and End rows of a datetime range share one grid, so their labels and inputs line up */
226
+ .calendar__time-rows {
227
+ display: grid;
228
+ grid-template-columns: auto 1fr auto 1fr auto 1fr;
229
+ align-items: center;
230
+ gap: var(--flow-ui-spacing-2x-small);
231
+ }
232
+
233
+ .calendar__time-container--range {
234
+ display: contents;
235
+ }
236
+
237
+ /* Two-month datetime range: the Start row under the first month and the End row under the second */
238
+ .calendar__time-rows--split {
239
+ grid-template-columns: 1fr 1fr;
240
+ column-gap: var(--month-gap);
241
+ }
242
+
243
+ .calendar__time-rows--split .calendar__time-container--range {
244
+ display: flex;
245
+ min-width: 0;
246
+ }
247
+
248
+ .calendar__time-label {
249
+ padding-inline-end: var(--flow-ui-spacing-x-small);
250
+ font-size: var(--day-font-size);
251
+ font-weight: var(--flow-ui-font-weight-bold);
252
+ }
253
+
114
254
  .calendar__time-input {
115
255
  flex: 1;
116
256
  /* Allow the input to shrink to accommodate narrow calendars in sidebars */
@@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
5
5
  import { DateTime } from 'luxon';
6
6
  import { stopAnimations, animateTo } from '../../internal/animate.mjs';
7
7
  import { boolean } from '../../internal/boolean.mjs';
8
+ import { parseDateRange, lastDayOfRange, endTimeOfRange, formatEndOfDay, parseDateLimits, checkDateLimits } from '../../internal/date-range.mjs';
8
9
  import { defaultValue } from '../../internal/default-value.mjs';
9
10
  import { waitForEvent } from '../../internal/event.mjs';
10
11
  import { FlowRdfFormControl } from '../../internal/flow-ui-rdf-element.mjs';
@@ -27,6 +28,11 @@ var __decorateClass = (decorators, target, key, kind) => {
27
28
  __defProp(target, key, result);
28
29
  return result;
29
30
  };
31
+ const defaultFormats = {
32
+ date: "DD",
33
+ time: "HH:mm",
34
+ datetime: "FFF"
35
+ };
30
36
  const popupOptions = {
31
37
  small: { distance: -1, skidding: -4 },
32
38
  medium: { distance: -3, skidding: -9 },
@@ -45,9 +51,10 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
45
51
  this.name = "";
46
52
  this.value = "";
47
53
  this.displayMode = "datetime";
48
- this.format = "FFF";
54
+ this.range = false;
49
55
  this.defaultValue = "";
50
56
  this.size = "medium";
57
+ this.months = 1;
51
58
  this.placeholder = "";
52
59
  this.disabled = false;
53
60
  this.clearable = false;
@@ -62,6 +69,16 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
62
69
  this.required = false;
63
70
  this.inline = false;
64
71
  this.divider = false;
72
+ /** The message set with `setCustomValidity()`. It takes precedence over the limits messages. */
73
+ this.customValidityMessage = "";
74
+ }
75
+ get format() {
76
+ return this._format ?? defaultFormats[this.displayMode] ?? defaultFormats.datetime;
77
+ }
78
+ set format(format) {
79
+ const oldFormat = this._format;
80
+ this._format = isStringFull(format) ? format : void 0;
81
+ this.requestUpdate("format", oldFormat);
65
82
  }
66
83
  /** Gets the validity state object */
67
84
  get validity() {
@@ -80,6 +97,7 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
80
97
  this.filled = boolean(this.filled, true);
81
98
  this.pill = boolean(this.pill, true);
82
99
  this.inline = boolean(this.inline, true);
100
+ this.range = boolean(this.range, true);
83
101
  this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
84
102
  this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
85
103
  this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
@@ -95,19 +113,48 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
95
113
  document.removeEventListener("keydown", this.handleDocumentKeyDown);
96
114
  document.removeEventListener("mousedown", this.handleDocumentMouseDown);
97
115
  }
116
+ /** Whether the picker selects a range: a date range, a datetime range or a time range. */
117
+ get isRangeMode() {
118
+ return this.range;
119
+ }
98
120
  updateDisplayLabel() {
99
121
  try {
100
- if (isStringFull(this.value)) {
101
- const zone = isStringFull(this.timezone) ? this.timezone : void 0;
102
- const locale = isStringFull(this.locale) ? this.locale : void 0;
103
- this.displayLabel = DateTime.fromISO(this.value, { zone, locale }).toFormat(this.format);
104
- } else {
122
+ const zone = isStringFull(this.timezone) ? this.timezone : void 0;
123
+ const locale = isStringFull(this.locale) ? this.locale : void 0;
124
+ if (!isStringFull(this.value)) {
105
125
  this.displayLabel = "";
126
+ } else if (this.isRangeMode) {
127
+ this.displayLabel = this.rangeLabel(zone, locale);
128
+ } else {
129
+ this.displayLabel = DateTime.fromISO(this.value, { zone, locale }).toFormat(this.format);
106
130
  }
107
131
  } catch (e) {
108
132
  console.warn(e.message);
109
133
  }
110
134
  }
135
+ /**
136
+ * The label for a range. For a date range, it shows the range start and the last day, both included, and a range of
137
+ * one day shows that day once. For a datetime range, it shows the start and end instants. For a time range, it shows
138
+ * the start and end times, with an end at the end of the day shown as 24:00. An invalid range shows an empty label,
139
+ * as the calendar shows no selection.
140
+ */
141
+ rangeLabel(zone, locale) {
142
+ const isDateRange = this.displayMode === "date";
143
+ const range = parseDateRange(this.value, { zone, locale, wholeDays: isDateRange });
144
+ if (!range) {
145
+ return "";
146
+ }
147
+ const first = range.start.toFormat(this.format);
148
+ let last;
149
+ if (isDateRange) {
150
+ last = lastDayOfRange(range).toFormat(this.format);
151
+ } else if (this.displayMode === "time" && endTimeOfRange(range).hour === 24) {
152
+ last = formatEndOfDay(lastDayOfRange(range), this.format);
153
+ } else {
154
+ last = range.end.toFormat(this.format);
155
+ }
156
+ return first === last ? first : `${first} \u2013 ${last}`;
157
+ }
111
158
  handleFocus() {
112
159
  this.hasFocus = true;
113
160
  this.displayInput.setSelectionRange(0, 0);
@@ -133,10 +180,17 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
133
180
  if (event.key === "Escape" && this.open) {
134
181
  event.preventDefault();
135
182
  event.stopPropagation();
183
+ const calendar = this.shadowRoot?.querySelector("flow-ui-calendar");
184
+ if (calendar?.cancelPendingRange()) {
185
+ return;
186
+ }
136
187
  this.hide();
137
188
  this.displayInput.focus({ preventScroll: true });
138
189
  }
139
190
  if (event.key === "Enter") {
191
+ if (this.open && event.composedPath().includes(this.calendarContainer)) {
192
+ return;
193
+ }
140
194
  event.preventDefault();
141
195
  event.stopImmediatePropagation();
142
196
  if (!this.open) {
@@ -185,6 +239,9 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
185
239
  event.stopPropagation();
186
240
  event.preventDefault();
187
241
  }
242
+ handleCalendarInput(event) {
243
+ event.stopPropagation();
244
+ }
188
245
  handleCalendarChange(event) {
189
246
  event.stopPropagation();
190
247
  const target = event.target;
@@ -196,6 +253,10 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
196
253
  this.emit("flow-ui-change");
197
254
  });
198
255
  }
256
+ if (this.isRangeMode && this.displayMode === "date") {
257
+ this.hide();
258
+ this.displayInput.focus({ preventScroll: true });
259
+ }
199
260
  }
200
261
  handleInvalid(event) {
201
262
  this.formControlController.setValidity(false);
@@ -262,9 +323,43 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
262
323
  }
263
324
  /** Sets a custom validation message. Pass an empty string to restore validity. */
264
325
  setCustomValidity(message) {
265
- this.valueInput.setCustomValidity(message);
326
+ this.customValidityMessage = message;
327
+ this.updateCustomValidity();
328
+ }
329
+ updated(changedProperties) {
330
+ super.updated(changedProperties);
331
+ const limitProperties = ["value", "min", "max", "maxRange", "range", "displayMode", "timezone", "locale"];
332
+ if (limitProperties.some((name) => changedProperties.has(name))) {
333
+ this.updateCustomValidity();
334
+ }
335
+ }
336
+ /** Set the value input's custom validity: the message from `setCustomValidity()`, or a limits message. */
337
+ updateCustomValidity() {
338
+ this.valueInput.setCustomValidity(this.customValidityMessage || this.limitsValidityMessage());
266
339
  this.formControlController.updateValidity();
267
340
  }
341
+ /** The validation message for a value outside `min`, `max` or `max-range`, or an empty string. */
342
+ limitsValidityMessage() {
343
+ if (this.displayMode === "time") {
344
+ return "";
345
+ }
346
+ const zone = isStringFull(this.timezone) ? this.timezone : void 0;
347
+ const locale = isStringFull(this.locale) ? this.locale : void 0;
348
+ const wholeDays = this.displayMode === "date";
349
+ const limits = parseDateLimits(this.min, this.max, this.maxRange, { zone, locale });
350
+ const violation = checkDateLimits(this.value, limits, { zone, locale, range: this.isRangeMode, wholeDays });
351
+ const formatLimit = (date) => date.toLocaleString(wholeDays ? DateTime.DATE_MED : DateTime.DATETIME_MED);
352
+ switch (violation) {
353
+ case "min":
354
+ return this.localize.term("dateBeforeMin", formatLimit(limits.min));
355
+ case "max":
356
+ return this.localize.term("dateAfterMax", formatLimit(limits.max));
357
+ case "maxRange":
358
+ return this.localize.term("dateRangeTooLong", limits.maxRange.toHuman());
359
+ default:
360
+ return "";
361
+ }
362
+ }
268
363
  /** Sets focus on the control. */
269
364
  focus(options) {
270
365
  this.displayInput.focus(options);
@@ -419,9 +514,15 @@ class FlowUiDateTimePicker extends FlowRdfFormControl {
419
514
  <flow-ui-calendar
420
515
  value=${this.value}
421
516
  display-mode=${this.displayMode}
517
+ ?range=${this.range}
518
+ min=${ifDefined(this.min)}
519
+ max=${ifDefined(this.max)}
520
+ max-range=${ifDefined(this.maxRange)}
422
521
  size=${this.size}
522
+ months=${this.months}
423
523
  timezone=${ifDefined(this.timezone)}
424
524
  locale=${ifDefined(this.locale)}
525
+ @flow-ui-input=${this.handleCalendarInput}
425
526
  @flow-ui-change=${this.handleCalendarChange}
426
527
  ></flow-ui-calendar>
427
528
  ` : nothing}
@@ -462,9 +563,12 @@ __decorateClass([
462
563
  __decorateClass([
463
564
  property({ attribute: "display-mode" })
464
565
  ], FlowUiDateTimePicker.prototype, "displayMode", 2);
566
+ __decorateClass([
567
+ property({ type: Boolean, reflect: true })
568
+ ], FlowUiDateTimePicker.prototype, "range", 2);
465
569
  __decorateClass([
466
570
  property()
467
- ], FlowUiDateTimePicker.prototype, "format", 2);
571
+ ], FlowUiDateTimePicker.prototype, "format", 1);
468
572
  __decorateClass([
469
573
  property()
470
574
  ], FlowUiDateTimePicker.prototype, "timezone", 2);
@@ -477,6 +581,18 @@ __decorateClass([
477
581
  __decorateClass([
478
582
  property({ reflect: true })
479
583
  ], FlowUiDateTimePicker.prototype, "size", 2);
584
+ __decorateClass([
585
+ property({ type: Number })
586
+ ], FlowUiDateTimePicker.prototype, "months", 2);
587
+ __decorateClass([
588
+ property()
589
+ ], FlowUiDateTimePicker.prototype, "min", 2);
590
+ __decorateClass([
591
+ property()
592
+ ], FlowUiDateTimePicker.prototype, "max", 2);
593
+ __decorateClass([
594
+ property({ attribute: "max-range" })
595
+ ], FlowUiDateTimePicker.prototype, "maxRange", 2);
480
596
  __decorateClass([
481
597
  property()
482
598
  ], FlowUiDateTimePicker.prototype, "placeholder", 2);
@@ -520,7 +636,7 @@ __decorateClass([
520
636
  property({ type: Boolean, reflect: true })
521
637
  ], FlowUiDateTimePicker.prototype, "divider", 2);
522
638
  __decorateClass([
523
- watch(["value", "format", "timezone", "locale"])
639
+ watch(["value", "format", "timezone", "locale", "range", "displayMode"])
524
640
  ], FlowUiDateTimePicker.prototype, "updateDisplayLabel", 1);
525
641
  __decorateClass([
526
642
  watch("disabled", { waitUntilFirstUpdate: true })
@@ -1,7 +1,7 @@
1
1
  import { DateTime } from 'luxon';
2
2
  import { isNil, isObjectFull, isFunction } from '../../../internal/type-checks.mjs';
3
3
  import { escape } from '../../../utilities/regex.mjs';
4
- import { filterRuleSet, isRule, isRuleSet } from '../helpers.mjs';
4
+ import { filterRuleSet, isRule, isRuleSet, betweenRuleValues } from '../helpers.mjs';
5
5
 
6
6
  function isStringType(field) {
7
7
  return field.type === "string";
@@ -24,6 +24,9 @@ function whereClausesFromRuleSet(rule, config, options) {
24
24
  if (options?.renameFields && fieldName in options.renameFields) {
25
25
  fieldName = options.renameFields[fieldName];
26
26
  }
27
+ if (rule.operator === "between") {
28
+ return betweenClause(fieldName, rule, field, options);
29
+ }
27
30
  let clause = ["is null", "not null"].includes(rule.operator) || !isNil(rule.value) ? { [fieldName]: predicateFromRule(rule, config) } : {};
28
31
  if ((rule.operator === "!=" || rule.operator === "not like") && isObjectFull(clause)) {
29
32
  clause = { $or: [{ [fieldName]: { $is: null } }, clause] };
@@ -35,6 +38,22 @@ function whereClausesFromRuleSet(rule, config, options) {
35
38
  return rules.length ? { [condition]: rules } : {};
36
39
  } else ;
37
40
  }
41
+ function betweenClause(fieldName, rule, field, options) {
42
+ const values = betweenRuleValues(rule, field);
43
+ if (!values) {
44
+ const message = `flowQueryConverter: Skipping 'between' rule for field '${rule.field}': value must be [start, end]`;
45
+ if (options.throwOnError) {
46
+ throw new Error(message);
47
+ }
48
+ console.warn(message, rule);
49
+ return void 0;
50
+ }
51
+ const [start, end] = field.type === "datetime" ? values.map((v) => DateTime.fromISO(v).toISO()) : values;
52
+ if (end === void 0) {
53
+ return { [fieldName]: { $gte: start } };
54
+ }
55
+ return { $and: [{ [fieldName]: { $gte: start } }, { [fieldName]: { $lt: end } }] };
56
+ }
38
57
  function predicateFromRule(rule, config) {
39
58
  const fieldConfig = config.fields[rule.field];
40
59
  const field = config.fields[rule.field];
@@ -105,18 +124,11 @@ function predicateFromRule(rule, config) {
105
124
  operator = "$not";
106
125
  value = null;
107
126
  break;
108
- case "between":
109
- operator = "$between";
110
- break;
111
127
  default:
112
128
  throw new Error(`Unrecognized query operator '${rule.operator}'`);
113
129
  }
114
130
  if (config.fields?.[rule.field]?.type === "datetime") {
115
- if (rule.operator === "between") {
116
- value = [DateTime.fromISO(value[0] || 0).toISO(), DateTime.fromISO(value[1] || 0).toISO()];
117
- } else {
118
- value = DateTime.fromISO(value || 0).toISO();
119
- }
131
+ value = DateTime.fromISO(value || 0).toISO();
120
132
  }
121
133
  return { [operator]: value };
122
134
  }
@@ -2,7 +2,7 @@ import { DateTime } from 'luxon';
2
2
  import { ensureArray } from '../../../internal/ensure-array.mjs';
3
3
  import { isNil, isStringFull, isFunction } from '../../../internal/type-checks.mjs';
4
4
  import { escape } from '../../../utilities/regex.mjs';
5
- import { filterRuleSet, isRule, isRuleSet } from '../helpers.mjs';
5
+ import { filterRuleSet, isRule, isRuleSet, betweenRuleValues } from '../helpers.mjs';
6
6
 
7
7
  class JavaScriptFilterError extends Error {
8
8
  constructor(msg) {
@@ -62,6 +62,22 @@ function predicateFromRule(rule, config, options) {
62
62
  let field = `get("${fieldName}", data)`;
63
63
  let operator = "";
64
64
  let value = rule.value;
65
+ if (rule.operator === "between") {
66
+ const values = betweenRuleValues(rule, fieldConfig);
67
+ if (!values) {
68
+ const message = `JavascriptFilterConverter: Skipping 'between' rule for field '${rule.field}': value must be [start, end]`;
69
+ if (options.throwOnError) {
70
+ throw new JavaScriptFilterError(message);
71
+ }
72
+ console.warn(message, rule);
73
+ return void 0;
74
+ }
75
+ const [start, end] = isDateType(fieldConfig) ? values.map((v) => v === void 0 ? v : convertDateTime(v, fieldConfig)) : values;
76
+ if (end === void 0) {
77
+ return `${field} >= ${JSON.stringify(start)}`;
78
+ }
79
+ return `(${field} >= ${JSON.stringify(start)}) && (${field} < ${JSON.stringify(end)})`;
80
+ }
65
81
  if (isFunction(fieldConfig.convertValue)) {
66
82
  value = fieldConfig.convertValue(value);
67
83
  }
@@ -1,7 +1,7 @@
1
1
  import { ensureArray } from '../../../internal/ensure-array.mjs';
2
2
  import { isNil, isStringFull, isFunction } from '../../../internal/type-checks.mjs';
3
3
  import { escape } from '../../../utilities/regex.mjs';
4
- import { filterRuleSet, isRule, isRuleSet } from '../helpers.mjs';
4
+ import { filterRuleSet, isRule, isRuleSet, betweenRuleValues } from '../helpers.mjs';
5
5
 
6
6
  function isStringType(field) {
7
7
  return field.type === "string";
@@ -42,6 +42,21 @@ function predicateFromRule(rule, config, options) {
42
42
  let operator = "";
43
43
  let value = rule.value;
44
44
  let negate = false;
45
+ if (rule.operator === "between") {
46
+ const values = betweenRuleValues(rule, fieldConfig);
47
+ if (!values) {
48
+ const message = `mapLayerFilterConverter: Skipping 'between' rule for field '${rule.field}': value must be [start, end]`;
49
+ if (options.throwOnError) {
50
+ throw new Error(message);
51
+ }
52
+ console.warn(message, rule);
53
+ return void 0;
54
+ }
55
+ if (values[1] === void 0) {
56
+ return `${field} >= ${JSON.stringify(values[0])}`;
57
+ }
58
+ return `(${field} >= ${JSON.stringify(values[0])}) && (${field} < ${JSON.stringify(values[1])})`;
59
+ }
45
60
  if (isFunction(fieldConfig.convertValue)) {
46
61
  value = fieldConfig.convertValue(value);
47
62
  }