From 8f1a7de9bd2395435ae48234344fab7da8280605 Mon Sep 17 00:00:00 2001 From: Alexandre Capt Date: Fri, 13 Sep 2024 11:22:47 +0200 Subject: [PATCH 01/20] feat: first shot of time range picker --- tools/rum/elements/timerange-picker.css | 133 +++++++++++++ tools/rum/elements/timerange-picker.js | 245 ++++++++++++++++++++++++ tools/rum/elements/url-selector.js | 6 +- 3 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 tools/rum/elements/timerange-picker.css create mode 100644 tools/rum/elements/timerange-picker.js diff --git a/tools/rum/elements/timerange-picker.css b/tools/rum/elements/timerange-picker.css new file mode 100644 index 00000000..84e4e2c5 --- /dev/null +++ b/tools/rum/elements/timerange-picker.css @@ -0,0 +1,133 @@ +.form-field.picker-field { + position: relative; + display: block; +} + +.form-field.picker-field input { + width: 100%; + font: inherit; + border-color: var(--gray-100); + border: 2px solid var(--gray-300); + padding: 0.4em 0.85em; + background-color: var(--gray-100); + cursor: pointer; + transition: border-color 0.2s, background-color 0.2s; + border-radius: 4px; + min-width: 200px; +} + +@media (width >= 900px) { + .timeframe-wrapper { + grid-area: timeframe; + } + + @media (width >= 900px) { + .log-viewer form#timeframe-form > .timeframe-wrapper { + grid-area: timeframe; + } + } +} + +.timeframe-wrapper { + display: grid; + + /* grid-template-columns: auto 1fr auto; */ + align-items: end; + gap: var(--spacing-l); +} + +.picker-field ul.menu li:last-child { + position: relative; + margin-top: 16px; +} + +.picker-field ul.menu li:last-child::before { + content: ''; + position: absolute; + top: calc((-0.5 * 16px) - (2px / 2)); + left: 0; + right: 0; + height: 2px; + background-color: var(--gray-200); +} + +.picker-field .datetime-wrapper { + display: none; + min-width: 500px; + background-color: white; +} + +.picker-field input[data-custom='true'] ~ .datetime-wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 16px 12px; + left: 0; + right: 0; + top: calc(100% + 8px); + margin-top: 4px; + border-radius: 4px; + padding: calc(0.4em + 2px); + background-color: white; + box-shadow: var(--grey-700); + z-index: 10; +} + +.picker-field ul { + list-style: none; + position: absolute; + left: 0; + right: 0; + top: calc(100% + 8px); + margin: 0; + border-radius: 8px; + padding: calc(0.4em + 2px); + background-color: white; + box-shadow: var(--grey-700); + z-index: 20; +} + +.picker-field ul.menu:not([hidden]) + .datetime-wrapper { + display: none; +} + +.picker-field input ~ .datetime-wrapper .form-field { + margin-top: 0; +} + +@media (width >= 740px) { + .datetime-wrapper { + grid-template-columns: repeat(2, 1fr); + } + + .picker-field input[data-custom='true'] ~ .datetime-wrapper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (width >= 900px) { + .picker-field input[data-custom='true'] ~ .datetime-wrapper { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (width >= 1200px) { + .picker-field input[data-custom='true'] ~ .datetime-wrapper { + grid-template-columns: repeat(2, minmax(0, 1fr)); + position: absolute; + } +} + +.picker-field .datetime-wrapper .datetime-field { + display: block; +} + +.picker-field .datetime-wrapper .datetime-field label { + display: block; + margin-bottom: 0.5em; +} + +.form-field.picker-field input ~ ul li { + padding: 0.4em 0; + padding-left: 2rem; + cursor: pointer; +} \ No newline at end of file diff --git a/tools/rum/elements/timerange-picker.js b/tools/rum/elements/timerange-picker.js new file mode 100644 index 00000000..0b3d16e6 --- /dev/null +++ b/tools/rum/elements/timerange-picker.js @@ -0,0 +1,245 @@ +// date management +function pad(number) { + return number.toString().padStart(2, '0'); +} + +function toDateTimeLocal(date) { + // convert date + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + // convert time + const hours = pad(date.getHours()); + const minutes = pad(date.getMinutes()); + + return `${year}-${month}-${day}T${hours}:${minutes}`; +} + +function calculatePastDate(days, hours, mins, now = new Date()) { + const newDate = now; + if (days > 0) newDate.setDate(newDate.getDate() - days); + if (hours > 0) newDate.setHours(newDate.getHours() - hours); + if (mins > 0) newDate.seMinutes(newDate.geMinutes() - mins); + return newDate; +} + +const TEMPLATE = ` +
+ + + +
+`; + +export default class TimeRangePicker extends HTMLElement { + constructor() { + super(); + + this.inputElement = null; + this.dropdownElement = null; + this.fromElement = null; + this.toElement = null; + this.datetimeWrapperElement = null; + + this.id = this.getAttribute('id'); + console.log('id', this.id); + } + + connectedCallback() { + this.initDOM(); + this.initValue(); + } + + initDOM() { + console.log('initDOM'); + + const section = document.createElement('section'); + section.className = 'form-field timeframe-wrapper'; + section.innerHTML = TEMPLATE; + + const sul = section.querySelector('ul'); + const options = this.querySelectorAll('ul li'); + + let defaultValue = null; + options.forEach((option) => { + if (option.getAttribute('aria-selected') === 'true') { + defaultValue = option.dataset.value; + } else { + option.setAttribute('aria-selected', false); + } + option.dataset.role = 'option'; + sul.appendChild(option); + }); + + console.log('defaultValue', defaultValue); + + this.innerHTML = ''; + this.appendChild(section); + + this.inputElement = this.querySelector('input'); + this.dropdownElement = this.querySelector('ul'); + this.fromElement = this.querySelector('[name="date-from"]'); + this.toElement = this.querySelector('[name="date-to"]'); + this.datetimeWrapperElement = this.querySelector('.datetime-wrapper'); + + this.registerListeners(); + + if (defaultValue) { + // defaultOption.dispatchEvent(new Event('click')); + // this.updateTimeframe(value); + this.value = defaultValue; + } + } + + initValue() { + const { toElement } = this; + toElement.value = toDateTimeLocal(new Date()); + } + + registerListeners() { + const { inputElement, dropdownElement } = this; + const options = dropdownElement.querySelectorAll('li'); + + inputElement.addEventListener('click', () => { + const expanded = inputElement.getAttribute('aria-expanded') === 'true'; + inputElement.setAttribute('aria-expanded', !expanded); + dropdownElement.hidden = expanded; + }); + + const $this = this; + options.forEach((option) => { + option.addEventListener('click', () => { + $this.value = option.dataset.value; + + // update to and from + // $this.updateTimeframe(option.dataset.value); + + // inputElement.value = option.textContent; + // inputElement.setAttribute('aria-expanded', false); + // dropdownElement.hidden = true; + // options.forEach((o) => o.setAttribute('aria-selected', o === option)); + + // inputElement.dispatchEvent(new Event('change', { detail: option.dataset.value })); + }); + }); + } + + get value() { + return this.inputElement.dataset.value; + } + + set value(value) { + console.log('set value', value); + + const { inputElement, dropdownElement } = this; + const option = dropdownElement.querySelector(`li[data-value="${value}"]`); + if (!option) { + console.error('Invalid value', value); + return; + } + + inputElement.value = option.textContent; + inputElement.dataset.value = option.dataset.value; + inputElement.setAttribute('aria-expanded', false); + + const options = dropdownElement.querySelectorAll('li'); + options.forEach((o) => o.setAttribute('aria-selected', o === option)); + + dropdownElement.hidden = true; + + this.updateTimeframe(value); + + this.dispatchEvent(new Event('change', { + detail: { + value, + from: this.fromElement.value, + to: this.toElement.value, + }, + })); + } + + updateTimeframe(value) { + console.log('updateTimeframe', value); + const { fromElement, toElement } = this; + + const now = new Date(); + + [fromElement, toElement].forEach((field) => { + field.readOnly = true; + }); + toElement.value = toDateTimeLocal(now); + this.toggleCustomTimeframe(value === 'custom'); + if (value.includes(':')) { + const [days, hours, mins] = value.split(':').map((v) => parseInt(v, 10)); + const date = calculatePastDate(days, hours, mins); + fromElement.value = toDateTimeLocal(date); + } else if (value === 'today') { + const midnight = now; + midnight.setHours(0, 0, 0, 0); + fromElement.value = toDateTimeLocal(midnight); + } else if (value === 'week') { + const lastWeek = now; + lastWeek.setHours(7 * 24, 0, 0, 0); + fromElement.value = toDateTimeLocal(lastWeek); + } else if (value === 'month') { + const lastMonth = now; + lastMonth.setMonth(now.getMonth() - 1); + fromElement.value = toDateTimeLocal(lastMonth); + } else if (value === 'year') { + const lastYear = now; + lastYear.setFullYear(now.getFullYear() - 1); + fromElement.value = toDateTimeLocal(lastYear); + } else if (value === 'custom') { + [fromElement, toElement].forEach((field) => { + field.removeAttribute('readonly'); + }); + } + } + + toggleCustomTimeframe(enabled) { + const { inputElement, datetimeWrapperElement } = this; + + inputElement.dataset.custom = enabled; + datetimeWrapperElement.hidden = !enabled; + [...datetimeWrapperElement.children].forEach((child) => { + child.setAttribute('aria-hidden', !enabled); + }); + } +} + +// function keepToFromCurrent(doc) { +// const to = doc.getElementById('date-to'); +// to.setAttribute('max', toDateTimeLocal(new Date())); +// const timeframe = doc.getElementById('view'); +// if (timeframe.value !== 'Custom') { +// const options = [...timeframe.parentElement.querySelectorAll('ul > li')]; +// const { value } = options.find((o) => o.textContent === timeframe.value).dataset; +// updateTimeframe(value); +// } +// } + +// registerListeners(document); + +// function initDateTo(doc) { +// const to = doc.getElementById('date-to'); +// to.value = toDateTimeLocal(new Date()); + +// // setInterval(() => { +// // keepToFromCurrent(doc); +// // }, 60 * 100); +// } + +// initDateTo(document); +// updateTimeframe('1:00:00'); diff --git a/tools/rum/elements/url-selector.js b/tools/rum/elements/url-selector.js index ec5c7655..738a51a8 100644 --- a/tools/rum/elements/url-selector.js +++ b/tools/rum/elements/url-selector.js @@ -7,12 +7,12 @@ export default class URLSelector extends HTMLElement { super(); this.template = `