@internetarchive/elements 0.4.2-webdev-9107.0 → 1.0.1-webdev-9121.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.
Files changed (57) hide show
  1. package/dist/src/elements/ia-histogram-date-range/ia-histogram-date-range.d.ts +205 -0
  2. package/dist/src/elements/ia-histogram-date-range/ia-histogram-date-range.d.ts.map +1 -0
  3. package/dist/src/elements/ia-histogram-date-range/ia-histogram-date-range.js +1016 -0
  4. package/dist/src/elements/ia-histogram-date-range/ia-histogram-date-range.js.map +1 -0
  5. package/dist/src/elements/ia-histogram-date-range/models.d.ts +13 -0
  6. package/dist/src/elements/ia-histogram-date-range/models.d.ts.map +1 -0
  7. package/dist/src/elements/ia-histogram-date-range/models.js +2 -0
  8. package/dist/src/elements/ia-histogram-date-range/models.js.map +1 -0
  9. package/dist/src/elements/ia-histogram-date-range/plugins/fix-first-century-years.d.ts +24 -0
  10. package/dist/src/elements/ia-histogram-date-range/plugins/fix-first-century-years.d.ts.map +1 -0
  11. package/dist/src/elements/ia-histogram-date-range/plugins/fix-first-century-years.js +43 -0
  12. package/dist/src/elements/ia-histogram-date-range/plugins/fix-first-century-years.js.map +1 -0
  13. package/dist/src/elements/index.d.ts +2 -3
  14. package/dist/src/elements/index.d.ts.map +1 -1
  15. package/dist/src/elements/index.js +2 -3
  16. package/dist/src/elements/index.js.map +1 -1
  17. package/package.json +2 -7
  18. package/dist/src/elements/ia-reviews/assets/delete-icon.svg +0 -8
  19. package/dist/src/elements/ia-reviews/assets/star-basic.svg +0 -6
  20. package/dist/src/elements/ia-reviews/assets/star-selected.svg +0 -8
  21. package/dist/src/elements/ia-reviews/assets/star-unselected.svg +0 -8
  22. package/dist/src/elements/ia-reviews/ia-review-form.d.ts +0 -104
  23. package/dist/src/elements/ia-reviews/ia-review-form.d.ts.map +0 -1
  24. package/dist/src/elements/ia-reviews/ia-review-form.js +0 -678
  25. package/dist/src/elements/ia-reviews/ia-review-form.js.map +0 -1
  26. package/dist/src/elements/ia-reviews/ia-review.d.ts +0 -49
  27. package/dist/src/elements/ia-reviews/ia-review.d.ts.map +0 -1
  28. package/dist/src/elements/ia-reviews/ia-review.js +0 -349
  29. package/dist/src/elements/ia-reviews/ia-review.js.map +0 -1
  30. package/dist/src/elements/ia-reviews/ia-reviews.d.ts +0 -63
  31. package/dist/src/elements/ia-reviews/ia-reviews.d.ts.map +0 -1
  32. package/dist/src/elements/ia-reviews/ia-reviews.js +0 -357
  33. package/dist/src/elements/ia-reviews/ia-reviews.js.map +0 -1
  34. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.d.ts +0 -8
  35. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.d.ts.map +0 -1
  36. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.js +0 -15
  37. package/dist/src/elements/ia-reviews/mocks/mock-fetch-handler.js.map +0 -1
  38. package/dist/src/elements/ia-reviews/utils/collapse-space.d.ts +0 -13
  39. package/dist/src/elements/ia-reviews/utils/collapse-space.d.ts.map +0 -1
  40. package/dist/src/elements/ia-reviews/utils/collapse-space.js +0 -19
  41. package/dist/src/elements/ia-reviews/utils/collapse-space.js.map +0 -1
  42. package/dist/src/elements/ia-reviews/utils/friendly-truncate.d.ts +0 -14
  43. package/dist/src/elements/ia-reviews/utils/friendly-truncate.d.ts.map +0 -1
  44. package/dist/src/elements/ia-reviews/utils/friendly-truncate.js +0 -65
  45. package/dist/src/elements/ia-reviews/utils/friendly-truncate.js.map +0 -1
  46. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.d.ts +0 -9
  47. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.d.ts.map +0 -1
  48. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.js +0 -40
  49. package/dist/src/elements/ia-reviews/utils/link-urls-in-text.js.map +0 -1
  50. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.d.ts +0 -9
  51. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.d.ts.map +0 -1
  52. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.js +0 -29
  53. package/dist/src/elements/ia-reviews/utils/sanitize-review-body.js.map +0 -1
  54. package/dist/src/elements/ia-reviews/utils/truncate-screenname.d.ts +0 -13
  55. package/dist/src/elements/ia-reviews/utils/truncate-screenname.d.ts.map +0 -1
  56. package/dist/src/elements/ia-reviews/utils/truncate-screenname.js +0 -19
  57. package/dist/src/elements/ia-reviews/utils/truncate-screenname.js.map +0 -1
@@ -0,0 +1,1016 @@
1
+ import { __decorate } from "tslib";
2
+ import '../../elements/ia-status-indicator/ia-status-indicator.js';
3
+ import dayjs from 'dayjs/esm';
4
+ import customParseFormat from 'dayjs/esm/plugin/customParseFormat';
5
+ import fixFirstCenturyYears from './plugins/fix-first-century-years.js';
6
+ import { css, html, LitElement, nothing, svg, } from 'lit';
7
+ import { property, state } from 'lit/decorators.js';
8
+ import { live } from 'lit/directives/live.js';
9
+ import { classMap } from 'lit/directives/class-map.js';
10
+ import { customElement } from '../../util/custom-element.js';
11
+ dayjs.extend(customParseFormat);
12
+ dayjs.extend(fixFirstCenturyYears);
13
+ // these values can be overridden via the component's HTML (camelCased) attributes
14
+ const WIDTH = 180;
15
+ const HEIGHT = 40;
16
+ const SLIDER_WIDTH = 10;
17
+ const TOOLTIP_WIDTH = 125;
18
+ const TOOLTIP_HEIGHT = 30;
19
+ const DATE_FORMAT = 'YYYY';
20
+ const MISSING_DATA = 'no data';
21
+ const UPDATE_DEBOUNCE_DELAY_MS = 0;
22
+ // this constant is not set up to be overridden
23
+ const SLIDER_CORNER_SIZE = 4;
24
+ // these CSS custom props can be overridden from the HTML that is invoking this component
25
+ const sliderColor = css `var(--histogramDateRangeSliderColor, #4B65FE)`;
26
+ const selectedRangeColor = css `var(--histogramDateRangeSelectedRangeColor, #DBE0FF)`;
27
+ const barIncludedFill = css `var(--histogramDateRangeBarIncludedFill, #2C2C2C)`;
28
+ const activityIndicatorColor = css `var(--histogramDateRangeActivityIndicator, #2C2C2C)`;
29
+ const barExcludedFill = css `var(--histogramDateRangeBarExcludedFill, #CCCCCC)`;
30
+ const inputRowMargin = css `var(--histogramDateRangeInputRowMargin, 0)`;
31
+ const inputBorder = css `var(--histogramDateRangeInputBorder, 0.5px solid #2C2C2C)`;
32
+ const inputWidth = css `var(--histogramDateRangeInputWidth, 35px)`;
33
+ const inputFontSize = css `var(--histogramDateRangeInputFontSize, 1.2rem)`;
34
+ const inputFontFamily = css `var(--histogramDateRangeInputFontFamily, sans-serif)`;
35
+ const tooltipBackgroundColor = css `var(--histogramDateRangeTooltipBackgroundColor, #2C2C2C)`;
36
+ const tooltipTextColor = css `var(--histogramDateRangeTooltipTextColor, #FFFFFF)`;
37
+ const tooltipFontSize = css `var(--histogramDateRangeTooltipFontSize, 1.1rem)`;
38
+ const tooltipFontFamily = css `var(--histogramDateRangeTooltipFontFamily, sans-serif)`;
39
+ let IAHistogramDateRange = class IAHistogramDateRange extends LitElement {
40
+ constructor() {
41
+ super(...arguments);
42
+ // public reactive properties that can be set via HTML attributes
43
+ this.width = WIDTH;
44
+ this.height = HEIGHT;
45
+ this.sliderWidth = SLIDER_WIDTH;
46
+ this.tooltipWidth = TOOLTIP_WIDTH;
47
+ this.tooltipHeight = TOOLTIP_HEIGHT;
48
+ this.updateDelay = UPDATE_DEBOUNCE_DELAY_MS;
49
+ this.dateFormat = DATE_FORMAT;
50
+ this.missingDataMessage = MISSING_DATA;
51
+ this.minDate = '';
52
+ this.maxDate = '';
53
+ this.disabled = false;
54
+ this.bins = [];
55
+ /** If true, update events will not be canceled by the date inputs receiving focus */
56
+ this.updateWhileFocused = false;
57
+ this.binSnapping = 'none';
58
+ // internal reactive properties not exposed as attributes
59
+ this._tooltipOffset = 0;
60
+ this._tooltipVisible = false;
61
+ this._isDragging = false;
62
+ this._isLoading = false;
63
+ // non-reactive properties (changes don't auto-trigger re-rendering)
64
+ this._minSelectedDate = '';
65
+ this._maxSelectedDate = '';
66
+ this._minDateMS = 0;
67
+ this._maxDateMS = 0;
68
+ this._dragOffset = 0;
69
+ this._histWidth = 0;
70
+ this._binWidth = 0;
71
+ this._histData = [];
72
+ this._previousDateRange = '';
73
+ this._updateDeferredWhileFocused = false;
74
+ // use arrow functions (rather than standard JS class instance methods) so
75
+ // that `this` is bound to the histogramDateRange object and not the event
76
+ // target. for more info see
77
+ // https://lit-element.polymer-project.org/guide/events#using-this-in-event-listeners
78
+ this.drag = (e) => {
79
+ // prevent selecting text or other ranges while dragging, especially in Safari
80
+ e.preventDefault();
81
+ if (this.disabled) {
82
+ return;
83
+ }
84
+ this.setDragOffset(e);
85
+ this._isDragging = true;
86
+ this.addListeners();
87
+ this.cancelPendingUpdateEvent();
88
+ };
89
+ this.drop = () => {
90
+ if (this._isDragging) {
91
+ this.removeListeners();
92
+ this.beginEmitUpdateProcess();
93
+ }
94
+ this._isDragging = false;
95
+ };
96
+ /**
97
+ * Adjust the date range based on slider movement
98
+ *
99
+ * @param e PointerEvent from the slider being moved
100
+ */
101
+ this.move = (e) => {
102
+ const histogramClientX = this.getBoundingClientRect().x;
103
+ const newX = e.clientX - histogramClientX - this._dragOffset;
104
+ const slider = this._currentSlider;
105
+ if (slider.id === 'slider-min') {
106
+ this.minSelectedDate = this.translatePositionToDate(this.validMinSliderX(newX));
107
+ }
108
+ else {
109
+ this.maxSelectedDate = this.translatePositionToDate(this.validMaxSliderX(newX));
110
+ if (this.getMSFromString(this.maxSelectedDate) > this._maxDateMS) {
111
+ this.maxSelectedDate = this.maxDate;
112
+ }
113
+ }
114
+ };
115
+ }
116
+ disconnectedCallback() {
117
+ this.removeListeners();
118
+ super.disconnectedCallback();
119
+ }
120
+ willUpdate(changedProps) {
121
+ // check for changes that would affect bin data calculations. Note:
122
+ // minSelectedDate/maxSelectedDate deliberately aren't in this list even
123
+ // though handleDataUpdate() defaults them from minDate/maxDate — the
124
+ // recalculation here (histWidth/minDateMS/maxDateMS/binWidth/histData)
125
+ // doesn't depend on either, and refreshing _previousDateRange as a side
126
+ // effect of a date being picked would make beginEmitUpdateProcess's
127
+ // "did anything change" check always see no change, since it compares
128
+ // against a value this same pick had just overwritten.
129
+ if (changedProps.has('bins') ||
130
+ changedProps.has('minDate') ||
131
+ changedProps.has('maxDate') ||
132
+ changedProps.has('width') ||
133
+ changedProps.has('height') ||
134
+ changedProps.has('binSnapping')) {
135
+ this.handleDataUpdate();
136
+ }
137
+ }
138
+ /**
139
+ * Set private properties that depend on the attribute bin data
140
+ *
141
+ * We're caching these values and not using getters to avoid recalculating all
142
+ * of the hist data every time the user drags a slider or hovers over a bar
143
+ * creating a tooltip.
144
+ */
145
+ handleDataUpdate() {
146
+ if (!this.hasBinData) {
147
+ return;
148
+ }
149
+ this._histWidth = this.width - this.sliderWidth * 2;
150
+ this._minDateMS = this.snapTimestamp(this.getMSFromString(this.minDate));
151
+ // NB: The max date string, converted as-is to ms, represents the *start* of the
152
+ // final date interval; we want the *end*, so we add any snap interval/offset.
153
+ this._maxDateMS =
154
+ this.snapTimestamp(this.getMSFromString(this.maxDate) + this.snapInterval) + this.snapEndOffset;
155
+ this._binWidth = this._histWidth / this._numBins;
156
+ this._previousDateRange = this.currentDateRangeString;
157
+ this._histData = this.calculateHistData();
158
+ this.minSelectedDate = this.minSelectedDate
159
+ ? this.minSelectedDate
160
+ : this.minDate;
161
+ this.maxSelectedDate = this.maxSelectedDate
162
+ ? this.maxSelectedDate
163
+ : this.maxDate;
164
+ }
165
+ /**
166
+ * Rounds the given timestamp to the next full second.
167
+ */
168
+ snapToNextSecond(timestamp) {
169
+ return Math.ceil(timestamp / 1000) * 1000;
170
+ }
171
+ /**
172
+ * Rounds the given timestamp to the (approximate) nearest start of a month,
173
+ * such that dates up to and including the 15th of the month are rounded down,
174
+ * while dates past the 15th are rounded up.
175
+ */
176
+ snapToMonth(timestamp) {
177
+ const d = dayjs(timestamp);
178
+ const monthsToAdd = d.date() < 16 ? 0 : 1;
179
+ const snapped = d
180
+ .add(monthsToAdd, 'month')
181
+ .date(1)
182
+ .hour(0)
183
+ .minute(0)
184
+ .second(0)
185
+ .millisecond(0); // First millisecond of the month
186
+ return snapped.valueOf();
187
+ }
188
+ /**
189
+ * Rounds the given timestamp to the (approximate) nearest start of a year,
190
+ * such that dates up to the end of June are rounded down, while dates in
191
+ * July or later are rounded up.
192
+ */
193
+ snapToYear(timestamp) {
194
+ const d = dayjs(timestamp);
195
+ const yearsToAdd = d.month() < 6 ? 0 : 1;
196
+ const snapped = d
197
+ .add(yearsToAdd, 'year')
198
+ .month(0)
199
+ .date(1)
200
+ .hour(0)
201
+ .minute(0)
202
+ .second(0)
203
+ .millisecond(0); // First millisecond of the year
204
+ return snapped.valueOf();
205
+ }
206
+ /**
207
+ * Rounds the given timestamp according to the `binSnapping` property.
208
+ * Default is simply to snap to the nearest full second.
209
+ */
210
+ snapTimestamp(timestamp) {
211
+ switch (this.binSnapping) {
212
+ case 'year':
213
+ return this.snapToYear(timestamp);
214
+ case 'month':
215
+ return this.snapToMonth(timestamp);
216
+ case 'none':
217
+ default:
218
+ // We still align it to second boundaries to resolve minor discrepancies
219
+ return this.snapToNextSecond(timestamp);
220
+ }
221
+ }
222
+ calculateHistData() {
223
+ const { bins, height, dateRangeMS, _numBins, _minDateMS } = this;
224
+ const minValue = Math.min(...this.bins);
225
+ const maxValue = Math.max(...this.bins);
226
+ // if there is no difference between the min and max values, use a range of
227
+ // 1 because log scaling will fail if the range is 0
228
+ const valueRange = minValue === maxValue ? 1 : Math.log1p(maxValue);
229
+ const valueScale = height / valueRange;
230
+ const dateScale = dateRangeMS / _numBins;
231
+ return bins.map((v, i) => {
232
+ const binStartMS = this.snapTimestamp(i * dateScale + _minDateMS);
233
+ const binStart = this.formatDate(binStartMS);
234
+ const binEndMS = this.snapTimestamp((i + 1) * dateScale + _minDateMS) +
235
+ this.snapEndOffset;
236
+ const binEnd = this.formatDate(binEndMS);
237
+ const tooltipStart = this.formatDate(binStartMS, this.tooltipDateFormat);
238
+ const tooltipEnd = this.formatDate(binEndMS, this.tooltipDateFormat);
239
+ // If start/end are the same, just render a single value
240
+ const tooltip = tooltipStart === tooltipEnd
241
+ ? tooltipStart
242
+ : `${tooltipStart} - ${tooltipEnd}`;
243
+ return {
244
+ value: v,
245
+ // use log scaling for the height of the bar to prevent tall bars from
246
+ // making the smaller ones too small to see
247
+ height: Math.floor(Math.log1p(v) * valueScale),
248
+ binStart,
249
+ binEnd,
250
+ tooltip,
251
+ };
252
+ });
253
+ }
254
+ get hasBinData() {
255
+ return this._numBins > 0;
256
+ }
257
+ get _numBins() {
258
+ if (!this.bins || !this.bins.length) {
259
+ return 0;
260
+ }
261
+ return this.bins.length;
262
+ }
263
+ get histogramLeftEdgeX() {
264
+ return this.sliderWidth;
265
+ }
266
+ get histogramRightEdgeX() {
267
+ return this.width - this.sliderWidth;
268
+ }
269
+ /**
270
+ * Approximate size in ms of the interval to which bins are snapped.
271
+ */
272
+ get snapInterval() {
273
+ const yearMS = 31_536_000_000; // A 365-day approximation of ms in a year
274
+ const monthMS = 2_592_000_000; // A 30-day approximation of ms in a month
275
+ switch (this.binSnapping) {
276
+ case 'year':
277
+ return yearMS;
278
+ case 'month':
279
+ return monthMS;
280
+ case 'none':
281
+ default:
282
+ return 0;
283
+ }
284
+ }
285
+ /**
286
+ * Offset added to the end of each bin to ensure disjoint intervals,
287
+ * depending on whether snapping is enabled and there are multiple bins.
288
+ */
289
+ get snapEndOffset() {
290
+ return this.binSnapping !== 'none' && this._numBins > 1 ? -1 : 0;
291
+ }
292
+ /**
293
+ * Optional date format to use for tooltips only.
294
+ * Falls back to `dateFormat` if not provided.
295
+ */
296
+ get tooltipDateFormat() {
297
+ return this._tooltipDateFormat ?? this.dateFormat;
298
+ }
299
+ set tooltipDateFormat(value) {
300
+ this._tooltipDateFormat = value;
301
+ }
302
+ /** component's loading (and disabled) state */
303
+ get loading() {
304
+ return this._isLoading;
305
+ }
306
+ set loading(value) {
307
+ this.disabled = value;
308
+ this._isLoading = value;
309
+ }
310
+ /** formatted minimum date of selected date range */
311
+ get minSelectedDate() {
312
+ return this.formatDate(this.getMSFromString(this._minSelectedDate));
313
+ }
314
+ /** updates minSelectedDate if new date is valid */
315
+ set minSelectedDate(rawDate) {
316
+ if (!this._minSelectedDate) {
317
+ // because the values needed to calculate valid max/min values are not
318
+ // available during the lit init when it's populating properties from
319
+ // attributes, fall back to just the raw date if nothing is already set
320
+ this._minSelectedDate = rawDate;
321
+ return;
322
+ }
323
+ const proposedDateMS = this.getMSFromString(rawDate);
324
+ const isValidDate = !Number.isNaN(proposedDateMS);
325
+ const isNotTooRecent = proposedDateMS <= this.getMSFromString(this.maxSelectedDate);
326
+ if (isValidDate && isNotTooRecent) {
327
+ this._minSelectedDate = this.formatDate(proposedDateMS);
328
+ }
329
+ this.requestUpdate();
330
+ }
331
+ /** formatted maximum date of selected date range */
332
+ get maxSelectedDate() {
333
+ return this.formatDate(this.getMSFromString(this._maxSelectedDate));
334
+ }
335
+ /** updates maxSelectedDate if new date is valid */
336
+ set maxSelectedDate(rawDate) {
337
+ if (!this._maxSelectedDate) {
338
+ // because the values needed to calculate valid max/min values are not
339
+ // available during the lit init when it's populating properties from
340
+ // attributes, fall back to just the raw date if nothing is already set
341
+ this._maxSelectedDate = rawDate;
342
+ return;
343
+ }
344
+ const proposedDateMS = this.getMSFromString(rawDate);
345
+ const isValidDate = !Number.isNaN(proposedDateMS);
346
+ const isNotTooOld = proposedDateMS >= this.getMSFromString(this.minSelectedDate);
347
+ if (isValidDate && isNotTooOld) {
348
+ this._maxSelectedDate = this.formatDate(proposedDateMS);
349
+ }
350
+ this.requestUpdate();
351
+ }
352
+ /** horizontal position of min date slider */
353
+ get minSliderX() {
354
+ const x = this.translateDateToPosition(this.minSelectedDate);
355
+ return this.validMinSliderX(x);
356
+ }
357
+ /** horizontal position of max date slider */
358
+ get maxSliderX() {
359
+ const maxSelectedDateMS = this.snapTimestamp(this.getMSFromString(this.maxSelectedDate) + this.snapInterval);
360
+ const x = this.translateDateToPosition(this.formatDate(maxSelectedDateMS));
361
+ return this.validMaxSliderX(x);
362
+ }
363
+ get dateRangeMS() {
364
+ return this._maxDateMS - this._minDateMS;
365
+ }
366
+ showTooltip(e) {
367
+ if (this._isDragging || this.disabled) {
368
+ return;
369
+ }
370
+ const target = e.currentTarget;
371
+ const x = target.x.baseVal.value + this.sliderWidth / 2;
372
+ const dataset = target.dataset;
373
+ const itemsText = `item${dataset.numItems !== '1' ? 's' : ''}`;
374
+ const formattedNumItems = Number(dataset.numItems).toLocaleString();
375
+ this._tooltipOffset =
376
+ x + (this._binWidth - this.sliderWidth - this.tooltipWidth) / 2;
377
+ this._tooltipContent = html `
378
+ ${formattedNumItems} ${itemsText}<br />
379
+ ${dataset.tooltip}
380
+ `;
381
+ this._tooltipVisible = true;
382
+ }
383
+ hideTooltip() {
384
+ this._tooltipContent = undefined;
385
+ this._tooltipVisible = false;
386
+ }
387
+ /**
388
+ * Constrain a proposed value for the minimum (left) slider
389
+ *
390
+ * If the value is less than the leftmost valid position, then set it to the
391
+ * left edge of the histogram (ie the slider width). If the value is greater
392
+ * than the rightmost valid position (the position of the max slider), then
393
+ * set it to the position of the max slider
394
+ */
395
+ validMinSliderX(newX) {
396
+ // allow the left slider to go right only to the right slider, even if the
397
+ // max selected date is out of range
398
+ const rightLimit = Math.min(this.translateDateToPosition(this.maxSelectedDate), this.histogramRightEdgeX);
399
+ newX = this.clamp(newX, this.histogramLeftEdgeX, rightLimit);
400
+ const isInvalid = Number.isNaN(newX) || rightLimit < this.histogramLeftEdgeX;
401
+ return isInvalid ? this.histogramLeftEdgeX : newX;
402
+ }
403
+ /**
404
+ * Constrain a proposed value for the maximum (right) slider
405
+ *
406
+ * If the value is greater than the rightmost valid position, then set it to
407
+ * the right edge of the histogram (ie histogram width - slider width). If the
408
+ * value is less than the leftmost valid position (the position of the min
409
+ * slider), then set it to the position of the min slider
410
+ */
411
+ validMaxSliderX(newX) {
412
+ // allow the right slider to go left only to the left slider, even if the
413
+ // min selected date is out of range
414
+ const leftLimit = Math.max(this.histogramLeftEdgeX, this.translateDateToPosition(this.minSelectedDate));
415
+ newX = this.clamp(newX, leftLimit, this.histogramRightEdgeX);
416
+ const isInvalid = Number.isNaN(newX) || leftLimit > this.histogramRightEdgeX;
417
+ return isInvalid ? this.histogramRightEdgeX : newX;
418
+ }
419
+ addListeners() {
420
+ window.addEventListener('pointermove', this.move);
421
+ window.addEventListener('pointerup', this.drop);
422
+ window.addEventListener('pointercancel', this.drop);
423
+ }
424
+ removeListeners() {
425
+ window.removeEventListener('pointermove', this.move);
426
+ window.removeEventListener('pointerup', this.drop);
427
+ window.removeEventListener('pointercancel', this.drop);
428
+ }
429
+ /**
430
+ * start a timer to emit an update event. this timer can be canceled (and the
431
+ * event not emitted) if user drags a slider or focuses a date input within
432
+ * the update delay
433
+ */
434
+ beginEmitUpdateProcess() {
435
+ this.cancelPendingUpdateEvent();
436
+ this._updateDeferredWhileFocused = false;
437
+ this._emitUpdatedEventTimer = setTimeout(() => {
438
+ // the timer has run, so there is no longer a pending event to cancel
439
+ this._emitUpdatedEventTimer = undefined;
440
+ if (this.currentDateRangeString === this._previousDateRange) {
441
+ // don't emit duplicate event if no change since last emitted event
442
+ return;
443
+ }
444
+ this._previousDateRange = this.currentDateRangeString;
445
+ const options = {
446
+ detail: {
447
+ minDate: this.minSelectedDate,
448
+ maxDate: this.maxSelectedDate,
449
+ },
450
+ bubbles: true,
451
+ composed: true,
452
+ };
453
+ this.dispatchEvent(new CustomEvent('histogramDateRangeUpdated', options));
454
+ }, this.updateDelay);
455
+ }
456
+ /** @returns whether there was a pending update event to cancel */
457
+ cancelPendingUpdateEvent() {
458
+ if (this._emitUpdatedEventTimer === undefined) {
459
+ return false;
460
+ }
461
+ clearTimeout(this._emitUpdatedEventTimer);
462
+ this._emitUpdatedEventTimer = undefined;
463
+ return true;
464
+ }
465
+ /**
466
+ * find position of pointer in relation to the current slider
467
+ */
468
+ setDragOffset(e) {
469
+ this._currentSlider = e.currentTarget;
470
+ const sliderX = this._currentSlider.id === 'slider-min'
471
+ ? this.minSliderX
472
+ : this.maxSliderX;
473
+ const histogramClientX = this.getBoundingClientRect().x;
474
+ this._dragOffset = e.clientX - histogramClientX - sliderX;
475
+ }
476
+ /**
477
+ * @param x horizontal position of slider
478
+ * @returns string representation of date
479
+ */
480
+ translatePositionToDate(x) {
481
+ // Snap to the nearest second, fixing the case where input like 1/1/2010
482
+ // would get translated to 12/31/2009 due to slight discrepancies from
483
+ // pixel boundaries and floating point error.
484
+ const milliseconds = this.snapToNextSecond(((x - this.sliderWidth) * this.dateRangeMS) / this._histWidth);
485
+ return this.formatDate(this._minDateMS + milliseconds);
486
+ }
487
+ /**
488
+ * Returns slider x-position corresponding to given date
489
+ *
490
+ * @param date
491
+ * @returns x-position of slider
492
+ */
493
+ translateDateToPosition(date) {
494
+ const milliseconds = this.getMSFromString(date);
495
+ return (this.sliderWidth +
496
+ ((milliseconds - this._minDateMS) * this._histWidth) / this.dateRangeMS);
497
+ }
498
+ /** ensure that the returned value is between minValue and maxValue */
499
+ clamp(x, minValue, maxValue) {
500
+ return Math.min(Math.max(x, minValue), maxValue);
501
+ }
502
+ handleInputFocus() {
503
+ if (!this.updateWhileFocused) {
504
+ // Tabbing from one date input to the other lands focus here in between
505
+ // the first input's blur and any chance for its update to be emitted.
506
+ // Remember that so the second input's blur can pick it back up, rather
507
+ // than the typed date never being reported at all.
508
+ this._updateDeferredWhileFocused = this.cancelPendingUpdateEvent();
509
+ }
510
+ }
511
+ handleMinDateInput(e) {
512
+ const target = e.currentTarget;
513
+ const isChanged = target.value !== this.minSelectedDate;
514
+ if (isChanged) {
515
+ this.minSelectedDate = target.value;
516
+ }
517
+ if (isChanged || this._updateDeferredWhileFocused) {
518
+ this.beginEmitUpdateProcess();
519
+ }
520
+ }
521
+ handleMaxDateInput(e) {
522
+ const target = e.currentTarget;
523
+ const isChanged = target.value !== this.maxSelectedDate;
524
+ if (isChanged) {
525
+ this.maxSelectedDate = target.value;
526
+ }
527
+ if (isChanged || this._updateDeferredWhileFocused) {
528
+ this.beginEmitUpdateProcess();
529
+ }
530
+ }
531
+ handleKeyUp(e) {
532
+ if (e.key === 'Enter') {
533
+ const target = e.currentTarget;
534
+ target.blur();
535
+ if (target.id === 'date-min') {
536
+ this.handleMinDateInput(e);
537
+ }
538
+ else if (target.id === 'date-max') {
539
+ this.handleMaxDateInput(e);
540
+ }
541
+ }
542
+ }
543
+ get currentDateRangeString() {
544
+ return `${this.minSelectedDate}:${this.maxSelectedDate}`;
545
+ }
546
+ getMSFromString(date) {
547
+ // It's possible that `date` is not a string in certain situations.
548
+ // For instance if you use LitElement bindings and the date is `2000`,
549
+ // it will be treated as a number instead of a string. This just makes sure
550
+ // we're dealing with a string.
551
+ const stringified = typeof date === 'string' ? date : String(date);
552
+ const digitGroupCount = (stringified.split(/(\d+)/).length - 1) / 2;
553
+ if (digitGroupCount === 1) {
554
+ // if there's just a single set of digits, assume it's a year
555
+ const dateObj = new Date(0, 0); // start at January 1, 1900
556
+ dateObj.setFullYear(Number(stringified)); // override year
557
+ return dateObj.getTime(); // get time in milliseconds
558
+ }
559
+ return dayjs(stringified, [this.dateFormat, DATE_FORMAT]).valueOf();
560
+ }
561
+ /**
562
+ * expand or narrow the selected range by moving the slider nearest the
563
+ * clicked bar to the outer edge of the clicked bar
564
+ *
565
+ * @param e Event click event from a histogram bar
566
+ */
567
+ handleBarClick(e) {
568
+ const dataset = e.currentTarget.dataset;
569
+ // use the midpoint of the width of the clicked bar to determine which is
570
+ // the nearest slider
571
+ const clickPosition = (this.getMSFromString(dataset.binStart) +
572
+ this.getMSFromString(dataset.binEnd)) /
573
+ 2;
574
+ const distanceFromMinSlider = Math.abs(clickPosition - this.getMSFromString(this.minSelectedDate));
575
+ const distanceFromMaxSlider = Math.abs(clickPosition - this.getMSFromString(this.maxSelectedDate));
576
+ // update the selected range by moving the nearer slider
577
+ if (distanceFromMinSlider < distanceFromMaxSlider) {
578
+ this.minSelectedDate = dataset.binStart;
579
+ }
580
+ else {
581
+ this.maxSelectedDate = dataset.binEnd;
582
+ }
583
+ this.beginEmitUpdateProcess();
584
+ }
585
+ get minSliderTemplate() {
586
+ // width/height in pixels of curved part of the sliders (like
587
+ // border-radius); used as part of a SVG quadratic curve. see
588
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#curve_commands
589
+ const cs = SLIDER_CORNER_SIZE;
590
+ const sliderShape = `
591
+ M${this.minSliderX},0
592
+ h-${this.sliderWidth - cs}
593
+ q-${cs},0 -${cs},${cs}
594
+ v${this.height - cs * 2}
595
+ q0,${cs} ${cs},${cs}
596
+ h${this.sliderWidth - cs}
597
+ `;
598
+ return this.generateSliderSVG(this.minSliderX, 'slider-min', sliderShape);
599
+ }
600
+ get maxSliderTemplate() {
601
+ const cs = SLIDER_CORNER_SIZE;
602
+ const sliderShape = `
603
+ M${this.maxSliderX},0
604
+ h${this.sliderWidth - cs}
605
+ q${cs},0 ${cs},${cs}
606
+ v${this.height - cs * 2}
607
+ q0,${cs} -${cs},${cs}
608
+ h-${this.sliderWidth - cs}
609
+ `;
610
+ return this.generateSliderSVG(this.maxSliderX, 'slider-max', sliderShape);
611
+ }
612
+ generateSliderSVG(sliderPositionX, id, sliderShape) {
613
+ // whether the curved part of the slider is facing towards the left (1), ie
614
+ // minimum, or facing towards the right (-1), ie maximum
615
+ const k = id === 'slider-min' ? 1 : -1;
616
+ const sliderClasses = classMap({
617
+ slider: true,
618
+ draggable: !this.disabled,
619
+ dragging: this._isDragging,
620
+ });
621
+ return svg `
622
+ <svg
623
+ id=${id}
624
+ class=${sliderClasses}
625
+ @pointerdown=${this.drag}
626
+ >
627
+ <path d="${sliderShape} z" fill="${sliderColor}" />
628
+ <rect
629
+ x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.4 * k}"
630
+ y="${this.height / 3}"
631
+ width="1"
632
+ height="${this.height / 3}"
633
+ fill="white"
634
+ />
635
+ <rect
636
+ x="${sliderPositionX - this.sliderWidth * k + this.sliderWidth * 0.6 * k}"
637
+ y="${this.height / 3}"
638
+ width="1"
639
+ height="${this.height / 3}"
640
+ fill="white"
641
+ />
642
+ </svg>
643
+ `;
644
+ }
645
+ get selectedRangeTemplate() {
646
+ return svg `
647
+ <rect
648
+ x="${this.minSliderX}"
649
+ y="0"
650
+ width="${this.maxSliderX - this.minSliderX}"
651
+ height="${this.height}"
652
+ fill="${selectedRangeColor}"
653
+ />`;
654
+ }
655
+ get histogramTemplate() {
656
+ const xScale = this._histWidth / this._numBins;
657
+ const barWidth = xScale - 1;
658
+ let x = this.sliderWidth; // start at the left edge of the histogram
659
+ return this._histData.map((data) => {
660
+ const { minSelectedDate, maxSelectedDate } = this;
661
+ const barHeight = data.height;
662
+ const binIsBeforeMin = this.isBefore(data.binEnd, minSelectedDate);
663
+ const binIsAfterMax = this.isAfter(data.binStart, maxSelectedDate);
664
+ const barFill = binIsBeforeMin || binIsAfterMax ? barExcludedFill : barIncludedFill;
665
+ // the stroke-dasharray style below creates a transparent border around the
666
+ // right edge of the bar, which prevents user from encountering a gap
667
+ // between adjacent bars (eg when viewing the tooltips or when trying to
668
+ // extend the range by clicking on a bar)
669
+ const barStyle = `stroke-dasharray: 0 ${barWidth} ${barHeight} ${barWidth} 0 ${barHeight}`;
670
+ const bar = svg `
671
+ <rect
672
+ class="bar"
673
+ style=${barStyle}
674
+ x=${x}
675
+ y=${this.height - barHeight}
676
+ width=${barWidth}
677
+ height=${barHeight}
678
+ @pointerenter=${this.showTooltip}
679
+ @pointerleave=${this.hideTooltip}
680
+ @click=${this.handleBarClick}
681
+ fill=${barFill}
682
+ data-num-items=${data.value}
683
+ data-bin-start=${data.binStart}
684
+ data-bin-end=${data.binEnd}
685
+ data-tooltip=${data.tooltip}
686
+ />`;
687
+ x += xScale;
688
+ return bar;
689
+ });
690
+ }
691
+ /** Whether the first arg represents a date strictly before the second arg */
692
+ isBefore(date1, date2) {
693
+ const date1MS = this.getMSFromString(date1);
694
+ const date2MS = this.getMSFromString(date2);
695
+ return date1MS < date2MS;
696
+ }
697
+ /** Whether the first arg represents a date strictly after the second arg */
698
+ isAfter(date1, date2) {
699
+ const date1MS = this.getMSFromString(date1);
700
+ const date2MS = this.getMSFromString(date2);
701
+ return date1MS > date2MS;
702
+ }
703
+ formatDate(dateMS, format = this.dateFormat) {
704
+ if (Number.isNaN(dateMS)) {
705
+ return '';
706
+ }
707
+ const date = dayjs(dateMS);
708
+ if (date.year() < 1000) {
709
+ // years before 1000 don't play well with dayjs custom formatting, so work around dayjs
710
+ // by setting the year to a sentinel value and then replacing it instead.
711
+ // this is a bit hacky but it does the trick for essentially all reasonable cases
712
+ // until such time as we replace dayjs.
713
+ const tmpDate = date.year(199999);
714
+ return tmpDate.format(format).replace(/199999/g, date.year().toString());
715
+ }
716
+ return date.format(format);
717
+ }
718
+ /**
719
+ * NOTE: we are relying on the lit `live` directive in the template to
720
+ * ensure that the change to minSelectedDate is noticed and the input value
721
+ * gets properly re-rendered. see
722
+ * https://lit.dev/docs/templates/directives/#live
723
+ */
724
+ get minInputTemplate() {
725
+ return html `
726
+ <input
727
+ id="date-min"
728
+ placeholder=${this.dateFormat}
729
+ type="text"
730
+ @focus=${this.handleInputFocus}
731
+ @blur=${this.handleMinDateInput}
732
+ @keyup=${this.handleKeyUp}
733
+ .value=${live(this.minSelectedDate)}
734
+ ?disabled=${this.disabled}
735
+ />
736
+ `;
737
+ }
738
+ get maxInputTemplate() {
739
+ return html `
740
+ <input
741
+ id="date-max"
742
+ placeholder=${this.dateFormat}
743
+ type="text"
744
+ @focus=${this.handleInputFocus}
745
+ @blur=${this.handleMaxDateInput}
746
+ @keyup=${this.handleKeyUp}
747
+ .value=${live(this.maxSelectedDate)}
748
+ ?disabled=${this.disabled}
749
+ />
750
+ `;
751
+ }
752
+ get minLabelTemplate() {
753
+ return html `<label for="date-min" class="sr-only">Minimum date:</label>`;
754
+ }
755
+ get maxLabelTemplate() {
756
+ return html `<label for="date-max" class="sr-only">Maximum date:</label>`;
757
+ }
758
+ get tooltipTemplate() {
759
+ return html `
760
+ <style>
761
+ #tooltip {
762
+ width: ${this.tooltipWidth}px;
763
+ height: ${this.tooltipHeight}px;
764
+ top: ${-9 - this.tooltipHeight}px;
765
+ left: ${this._tooltipOffset}px;
766
+ display: ${this._tooltipVisible ? 'block' : 'none'};
767
+ }
768
+ #tooltip:after {
769
+ left: ${this.tooltipWidth / 2}px;
770
+ }
771
+ </style>
772
+ <div id="tooltip">${this._tooltipContent}</div>
773
+ `;
774
+ }
775
+ get noDataTemplate() {
776
+ return html `
777
+ <div class="missing-data-message">${this.missingDataMessage}</div>
778
+ `;
779
+ }
780
+ get activityIndicatorTemplate() {
781
+ if (!this.loading) {
782
+ return nothing;
783
+ }
784
+ return html `
785
+ <ia-status-indicator mode="loading" ?hideDots=${true}>
786
+ </ia-status-indicator>
787
+ `;
788
+ }
789
+ render() {
790
+ if (!this.hasBinData) {
791
+ return this.noDataTemplate;
792
+ }
793
+ return html `
794
+ <div
795
+ id="container"
796
+ class="
797
+ noselect
798
+ ${this._isDragging ? 'dragging' : ''}
799
+ "
800
+ style="width: ${this.width}px"
801
+ >
802
+ ${this.activityIndicatorTemplate} ${this.tooltipTemplate}
803
+ <div
804
+ class="inner-container
805
+ ${this.disabled ? 'disabled' : ''}"
806
+ >
807
+ <svg
808
+ width="${this.width}"
809
+ height="${this.height}"
810
+ @pointerleave="${this.drop}"
811
+ >
812
+ ${this.selectedRangeTemplate}
813
+ <svg id="histogram">${this.histogramTemplate}</svg>
814
+ ${this.minSliderTemplate} ${this.maxSliderTemplate}
815
+ </svg>
816
+ <div id="inputs">
817
+ ${this.minLabelTemplate} ${this.minInputTemplate}
818
+ <div class="dash">-</div>
819
+ ${this.maxLabelTemplate} ${this.maxInputTemplate}
820
+ <slot name="inputs-right-side"></slot>
821
+ </div>
822
+ </div>
823
+ </div>
824
+ `;
825
+ }
826
+ };
827
+ IAHistogramDateRange.styles = css `
828
+ .missing-data-message {
829
+ text-align: center;
830
+ }
831
+ #container {
832
+ margin: 0;
833
+ touch-action: none;
834
+ position: relative;
835
+ }
836
+ .disabled {
837
+ opacity: 0.3;
838
+ }
839
+ ia-status-indicator {
840
+ position: absolute;
841
+ left: calc(50% - 10px);
842
+ top: 10px;
843
+ --icon-width: 20px;
844
+ --loading-ring-color--: ${activityIndicatorColor};
845
+ }
846
+
847
+ /* prevent selection from interfering with tooltip, especially on mobile */
848
+ /* https://stackoverflow.com/a/4407335/1163042 */
849
+ .noselect {
850
+ -webkit-touch-callout: none; /* iOS Safari */
851
+ -webkit-user-select: none; /* Safari */
852
+ -moz-user-select: none; /* Old versions of Firefox */
853
+ -ms-user-select: none; /* Internet Explorer/Edge */
854
+ user-select: none; /* current Chrome, Edge, Opera and Firefox */
855
+ }
856
+ .bar {
857
+ /* create a transparent border around the hist bars to prevent "gaps" and
858
+ flickering when moving around between bars. this also helps with handling
859
+ clicks on the bars, preventing users from being able to click in between
860
+ bars */
861
+ stroke: rgba(0, 0, 0, 0);
862
+ /* ensure transparent stroke wide enough to cover gap between bars */
863
+ stroke-width: 2px;
864
+ }
865
+ .bar:hover {
866
+ /* highlight currently hovered bar */
867
+ fill-opacity: 0.7;
868
+ }
869
+ .disabled .bar:hover {
870
+ /* ensure no visual hover interaction when disabled */
871
+ fill-opacity: 1;
872
+ }
873
+ /****** histogram ********/
874
+ #tooltip {
875
+ position: absolute;
876
+ background: ${tooltipBackgroundColor};
877
+ color: ${tooltipTextColor};
878
+ text-align: center;
879
+ border-radius: 3px;
880
+ padding: 2px;
881
+ font-size: ${tooltipFontSize};
882
+ font-family: ${tooltipFontFamily};
883
+ touch-action: none;
884
+ pointer-events: none;
885
+ }
886
+ #tooltip:after {
887
+ content: '';
888
+ position: absolute;
889
+ margin-left: -5px;
890
+ top: 100%;
891
+ /* arrow */
892
+ border: 5px solid ${tooltipTextColor};
893
+ border-color: ${tooltipBackgroundColor} transparent transparent
894
+ transparent;
895
+ }
896
+ /****** slider ********/
897
+ .slider {
898
+ shape-rendering: crispEdges; /* So the slider doesn't get blurry if dragged between pixels */
899
+ }
900
+ .draggable:hover {
901
+ cursor: grab;
902
+ }
903
+ .dragging {
904
+ cursor: grabbing !important;
905
+ }
906
+ /****** inputs ********/
907
+ #inputs {
908
+ display: flex;
909
+ justify-content: center;
910
+ margin: ${inputRowMargin};
911
+ }
912
+ #inputs .dash {
913
+ position: relative;
914
+ bottom: -1px;
915
+ align-self: center; /* Otherwise the dash sticks to the top while the inputs grow */
916
+ }
917
+ input {
918
+ width: ${inputWidth};
919
+ margin: 0 3px;
920
+ border: ${inputBorder};
921
+ border-radius: 2px !important;
922
+ text-align: center;
923
+ font-size: ${inputFontSize};
924
+ font-family: ${inputFontFamily};
925
+ }
926
+ .sr-only {
927
+ position: absolute !important;
928
+ width: 1px !important;
929
+ height: 1px !important;
930
+ margin: 0 !important;
931
+ padding: 0 !important;
932
+ border: 0 !important;
933
+ overflow: hidden !important;
934
+ white-space: nowrap !important;
935
+ clip: rect(1px, 1px, 1px, 1px) !important;
936
+ -webkit-clip-path: inset(50%) !important;
937
+ clip-path: inset(50%) !important;
938
+ }
939
+ `;
940
+ __decorate([
941
+ property({ type: Number })
942
+ ], IAHistogramDateRange.prototype, "width", void 0);
943
+ __decorate([
944
+ property({ type: Number })
945
+ ], IAHistogramDateRange.prototype, "height", void 0);
946
+ __decorate([
947
+ property({ type: Number })
948
+ ], IAHistogramDateRange.prototype, "sliderWidth", void 0);
949
+ __decorate([
950
+ property({ type: Number })
951
+ ], IAHistogramDateRange.prototype, "tooltipWidth", void 0);
952
+ __decorate([
953
+ property({ type: Number })
954
+ ], IAHistogramDateRange.prototype, "tooltipHeight", void 0);
955
+ __decorate([
956
+ property({ type: Number })
957
+ ], IAHistogramDateRange.prototype, "updateDelay", void 0);
958
+ __decorate([
959
+ property({ type: String })
960
+ ], IAHistogramDateRange.prototype, "dateFormat", void 0);
961
+ __decorate([
962
+ property({ type: String })
963
+ ], IAHistogramDateRange.prototype, "missingDataMessage", void 0);
964
+ __decorate([
965
+ property({ type: String })
966
+ ], IAHistogramDateRange.prototype, "minDate", void 0);
967
+ __decorate([
968
+ property({ type: String })
969
+ ], IAHistogramDateRange.prototype, "maxDate", void 0);
970
+ __decorate([
971
+ property({ type: Boolean })
972
+ ], IAHistogramDateRange.prototype, "disabled", void 0);
973
+ __decorate([
974
+ property({ type: Array })
975
+ ], IAHistogramDateRange.prototype, "bins", void 0);
976
+ __decorate([
977
+ property({ type: Boolean })
978
+ ], IAHistogramDateRange.prototype, "updateWhileFocused", void 0);
979
+ __decorate([
980
+ property({ type: String })
981
+ ], IAHistogramDateRange.prototype, "binSnapping", void 0);
982
+ __decorate([
983
+ state()
984
+ ], IAHistogramDateRange.prototype, "_tooltipOffset", void 0);
985
+ __decorate([
986
+ state()
987
+ ], IAHistogramDateRange.prototype, "_tooltipContent", void 0);
988
+ __decorate([
989
+ state()
990
+ ], IAHistogramDateRange.prototype, "_tooltipVisible", void 0);
991
+ __decorate([
992
+ state()
993
+ ], IAHistogramDateRange.prototype, "_tooltipDateFormat", void 0);
994
+ __decorate([
995
+ state()
996
+ ], IAHistogramDateRange.prototype, "_isDragging", void 0);
997
+ __decorate([
998
+ state()
999
+ ], IAHistogramDateRange.prototype, "_isLoading", void 0);
1000
+ __decorate([
1001
+ property({ type: String })
1002
+ ], IAHistogramDateRange.prototype, "tooltipDateFormat", null);
1003
+ __decorate([
1004
+ property({ type: Boolean })
1005
+ ], IAHistogramDateRange.prototype, "loading", null);
1006
+ __decorate([
1007
+ property()
1008
+ ], IAHistogramDateRange.prototype, "minSelectedDate", null);
1009
+ __decorate([
1010
+ property()
1011
+ ], IAHistogramDateRange.prototype, "maxSelectedDate", null);
1012
+ IAHistogramDateRange = __decorate([
1013
+ customElement('ia-histogram-date-range')
1014
+ ], IAHistogramDateRange);
1015
+ export { IAHistogramDateRange };
1016
+ //# sourceMappingURL=ia-histogram-date-range.js.map