mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 10:04:52 +00:00
feat: improve analytics dashboard (#5897)
* feat: implement cancel/apply for custom timeframe range picker
* feat: implement dot for showing todays date
* feat: add max date to be today and show todays date
* feat: if ratio mode, dont show total
* feat: implement show more batching excess lines into "Other" bucket
* refactor: pnpm prepr
* feat: add pick and plop for date range start/end dates
* feat: implement reset query button
* feat: clear button to clear breakdown
* feat: more aggressively trim allowed minimum group by option
* fix: dont show project status filter when from project settings/analytics
* fix: clear selected X above number when appropriate
* feat: graph style updates and dont show year in x axis unless more than 2 year timeframe
* fix: loading state to include legend in blur
* feat: add project icon to project select
* feat: filter out draft projects from analytics
* feat: implement multiselect sections headers, project select org sections, and project options icons
* feat: implement click and drag to select date range
* feat: implement windows history for query builder
* revert: no longer switch breakdown/filter option if same category
* feat: implement showing project for project version breakdown/filter when there are multiple projects
* feat: implement modrinth sided events
* fix: border radius
* feat: implement analytics range highlight
* fix: loading state showing empty state text
* refactor: pnpm prepr
* feat: improve dropdown filter bar and multiselect performance
* fix: multiselect keyboard use
* fix: graph overflow issues
* fix: loading state text on table
* feat: implement tooltip scroll
* fix: adjust charts event tooltip
* feat: shorten time to not repeat am/pm
* feat: implement query params for graph component settings
* fix: qa
* feat: add reset timeframe button
* fix: legend colors moving between metric by determining color based on only downloads metric index
* feat: implement auto switching temporarily to group by day for renvenue metric and disable revenue metric for time range < 2 days
* fix: change to > 1 day
* fix: custom timeframe picker
* feat: implement big performance improvement for table
* feat: implement hover on legend to highlight graph
* fix: defer commit in query builder/filter and style fixes
* feat: more performance optimization to analytics dashboard state, chart, and table
* feat: add tooltip for other item
* feat: improve custom time frame range select
* feat: implement analytics events admin page
* fix: switch column order
* pnpm prepr
* feat: implement mock analytics events
* feat: improve analytics events admin page
* feat: focus title input on analytics create event modal
* fix: remove labels annoying
* feat: hook up analytics events backend
* fix: type error
* feat: reduce combobox padding
* feat: reduce padding on multiselect
* feat: add overlay scrollbar for combobox
* feat: a bunch of style fixes to combobox, multiselect and dropdown filter bar
* feat: MORE PADDING fixes
* feat: use user_agent for download source
* Revert "feat: use user_agent for download source"
This reverts commit d6dc8a99f1.
* fix: query filter project version lag and borked virtualization
* feat: rename breakdown "none" to "project"
* feat: implement right side checkmark for multiselect
* feat: keep crossed out legend items still shown in tooltip but also crossed out
* fix: focus styles
* fix: focus styles pt2
* feat: implement filter by top 8
* fix: preview is incorrect when selecting same date in range date picker
* feat (playtest): cross out legend items in tooltip and allow hide/show in tooltip
* feat (playtest): table component controls what graph shows
* feat: change download source to use user_agent
* feat: fix click to cross out in legend
* feat: add hover legend item to highlight line in tooltip
* fix: export csv to always be dropdown
* feat: implement breakdown = none
* performance: frontend memory reduction
* performance: reduce memory usage from project versions query by keeping only whats necessary
* fix: table checked items not in graph if 0
* feat: add shift click to select a range in table
* performance: add caching for metric types so switching between them is snappy
* performance: batch analytics requests by 15 project ids, with 150 ms delay between, so backend is happy
* feat: add analytics table search
* refactor: pnpm prepr
* fix: query filter options not coming in from analytics fetch
* feat: remove breakdown = none when there are multiple projects
* feat: improve table sorting
* feat: sort projects in project dropdown
* fix: getting project name for project versions
* fix: add loading state for filter and parallel fetch
* performance: use precomputed map for project version options to remove first hover lag
* feat: dropdown filter always open on one side and improve styles
* fix: custom time range picker being weird
* refactor: pnpm prepr
* fix: add back in batch with 300ms interval for projects to prevent backend rate limiting
* performance: only do queries to populate graph first before other analytics queries
* fix: QA polish issues around style and copy
* feat: dont show select all when its just one item in section
* fix: bugs with ratio mode and hiding chart lines
* fix: adjust padding in combobox and multiselect and fix not unfocusing when deselect
* fix: small styles
* fix: polish admin analytic events
* fix: keep scroll position with selection action row appearing when selecting one
* feat: add subheading in graph for showing N items from table
* feat: add unmonetized explaination tooltip
* performance: implement limit on how many lines can be shown in graph
* feat: mobile pass
* refactor: pnpm prepr
* add clear button
* feat: add time in analytics event and normalize date/time so its correct to timezones
* fix: padding
* feat: implement show prev period toggle
* feat: extract TimeFramePicker to packages/ui
* fix: adjust style
* feat: keep table selected persisted in query parameter
* fix: style on prev item value in legend
* fix: when breakdown switches, reset selected series
* fix: tooltip styles
* feat: change project selection to reset to show top 8 only if reconciled down to 0 items
* feat: implement show top 8 button in graph subheading
* fix: rename download type to download reason
* fix: formatting label for table
* feat: persist table sort by and sort direction
* fix: show top 8 button in graph not defaulting to top 8 for other metrics
* feat: implement prev period analytics fetch into the same current period fetch by shifting start date
* refactor: pnpm prepr
* fix: remove number if its just top 1
* fix: brief select items empty state when switch breakdown
* feat: implement format table playtime column
* feat: update export csv filename
* feat: change playtime column to display in hours
* refactor: pnpm prepr
* fix: still download type in filter
* feat: update analytics tooltip
* fix: wrong all projects icon
* feat: force legend order and graph colour for monetization
* refactor: pnpm prepr
* fix: multiselect and combobox sizes
* fix: chart icon add hover delay
* feat: (to playtest) implement multiple breakdowns
* fix: couple UX things for multiple breakdown
* fix: cannot unpin on page click
* fix: multiple breakdown legend and tooltip labels
* feat: add right side checkmark for dropdown filtr bar
* feat: enabling prev period will cross out prev for current ones already crossed out
* feat-mobile: remove drag to select time frame in graph
* feat-mobile: dropdown filter to replace dropdown for submenus on small screen
* feat-mobile: time frame picker to use different start and end date pickers for mobile
* fix-mobile: fix multiselect scroll on mobile
* feat: consolidate is mobile ref into context
* fix-mobile: combobox and multiselect scroll bug when mobile search bar open, fix timeframe picker mobile pick date, and dropdown filter bar click outside to close
* fix-mobile: smaller metric card font
* fix: dropdown filter bar scroll while search
* feat: implement project side events
* feat: implement better mobile view design for query builder
* feat: handle events overflow
* small: add select none
* feat: remove clear project and breakdown
* fix: event icon hover color
* feat: default hide project events if there are multiple projects, and default show if only 1 project
* feat: implement analytics performance updates, including facets, and v3 user projects
* feat: grey out dimmed lined on legend item hover
* feat-mobile: style fixes
* add close on select prop
* feat: add close on select for time frame picker mobile
* feat: date picker default read only
* refactor: pnpm prepr
* feat: default to projects breakdown instead of no breakdown with multiple projects
* fix-mobile: improve graph touch interactions
* small: 2 sig figs on playtime
* feat: deduplicate version uploads that have same version number and are uploaded on same day
* fix: analytics events grouping causing overflow
* feat: improve performance on analytics events grouping
* fix: tooltip expanding page width briefly
* fix: prevent double tap to zoom on inputs
* feat: add click to show chart event for mobile
* fix: toggle not having touch manipulation
* fix: chart tooltip scroll in mobile
* fix: remove project breakdownoption as it is default breakdown when none are selected
* fix: dropdown filter bar briefly empty when switching pages in mobile
* feat: keep tooltip open after drag in mobile
* fix: using plural instead of single for project breakdown
* fix: date picker scrolling page after picking date in mobile by suppressing focus
* fix: callback to Organization instead of org id
* feat: improve chart tooltip date range label formatter to be much more consistent
* feat: tap to toggle event tooltip
* fix: add user select none on graph and fix zoom into download threshold input
* fix: frontend still filtering after backend already filters
* feat: fix emptys state height content shift
* fix: qa issues
* fix: a number of qa issues
- Hide project events based on visible project legend/table selection
- Filter project status events by end status and add explicit copy for approved, private, and unlisted
- Style Modrinth analytics events with blue icon, marker, guide, and range borders
- Add scroll fade shadows to analytics chart and event tooltips
- Show previous-period date range in the chart tooltip
- Make project breakdown conditional on multiple selected projects and allow no breakdown when none are selected
- Add breakdown selection actions and fix “Group by day” copy
* feat: implement graph controls dropdown
* fix date picker typing into time input
* fix: styles in events table
* small: style
* feat: implement using new backend facets route
* feat: implement user get all projects
* performance: deter non-critical fetches to after analytics is in
* fix: refreshing causes multiple projects to do breakdown=none
* performance: cache project version options to fix lag on open sub menu
* refactor: remove chart event height being controlled by parent
* feat: update controls dropdown to have fainter border
* fix: loading bar not fading away
* fix: cannot click in graph
* feat: dont conditionally show multiselect selection actions
* fix: z-index and padding issues
* fix: project events incorrectly toggling on for first page load
* feat: remove show more and show less in legend, always show all
* fix: playtime y axis labels
* feat: improve y axis formatting for playtime and others
* feat: use tabs for game version select, and remove prev period when change breakdown or project selection
* refactor: pnpm prepr
* feat: change hidden legend items to not contribute to ratio percentages
* feat: event icon consume scroll for tooltip panel
* feat: remove gap inside chart tooltip
* feat: add gap for date picker 2 calendar view
* feat: improve analytics events grouping logic for modrinth events to be close to target
* pnpm prepr
* fix: cant click in gap in toggle
* fix: bugs around selected series from table not persisting with timeframe or filter changes
* refactor: kabab case
* refactor: split up large analytics chart and table component files into smaller components and ts modules
* fix: legend is stale after resetting query
* refactor: split up giant analytics provider with utils
* i18n pass
* revert: format number composable change
* fix: playtime was choosing y axis ticks in seconds instead of hours
* refactor: rename folder that with components to match main component name
* refactor: same rename for analytics table for consistency
* refactor: name main components to index.vue and keep folder name as component name
* refactor: pnpm prepr
* refactor: rename types
* refactor: move query builder types into types file and move components into components/analytics-dashboard
* refactor: colocate query builder url with analytics-dashboard component
* refactor: pnpm prepr:frontend
* fix: download threshold not width fit
* fix: no option to see release/all game versions in selected filter dropdown
* fix: game version dropdown width
---------
Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -32,13 +32,22 @@
|
||||
:aria-hidden="calendarOnly ? 'true' : undefined"
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
v-if="hasClearButton"
|
||||
type="button"
|
||||
class="absolute right-0.5 z-[1] touch-manipulation cursor-pointer select-none border-none bg-transparent p-2 text-secondary transition-colors hover:text-contrast"
|
||||
aria-label="Clear date"
|
||||
@click.stop="clearValue"
|
||||
>
|
||||
<XIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import { CalendarIcon } from '@modrinth/assets'
|
||||
import { CalendarIcon, XIcon } from '@modrinth/assets'
|
||||
import chevronLeftIcon from '@modrinth/assets/icons/chevron-left.svg?raw'
|
||||
import chevronRightIcon from '@modrinth/assets/icons/chevron-right.svg?raw'
|
||||
import flatpickr from 'flatpickr'
|
||||
@@ -127,6 +136,7 @@ const props = withDefaults(
|
||||
showIcon?: boolean
|
||||
showToday?: boolean
|
||||
calendarOnly?: boolean
|
||||
closeOnSelect?: boolean
|
||||
/**
|
||||
* Controls where the calendar opens relative to the input. Use `above`
|
||||
* to force the calendar to open above the input.
|
||||
@@ -146,7 +156,7 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
readonly: true,
|
||||
enableTime: false,
|
||||
mode: 'single',
|
||||
showMonths: 1,
|
||||
@@ -156,6 +166,7 @@ const props = withDefaults(
|
||||
showIcon: true,
|
||||
showToday: false,
|
||||
calendarOnly: false,
|
||||
closeOnSelect: false,
|
||||
position: 'auto',
|
||||
preserveDay: false,
|
||||
viewDateAlignment: 'left',
|
||||
@@ -176,13 +187,18 @@ const intendedViewMonth = ref<CalendarViewMonth | null>(null)
|
||||
const preserveViewOnNextModelSync = ref(false)
|
||||
const intendedDay = ref<number | null>(null)
|
||||
const isPreservingDay = ref(false)
|
||||
const timeInputDigits = ref(new WeakMap<HTMLInputElement, string>())
|
||||
const rangeDragState = ref<RangeDragState | null>(null)
|
||||
const rangeEndpointMoveState = ref<RangeEndpointMoveState | null>(null)
|
||||
const suppressNextRangeClick = ref(false)
|
||||
let rangeClickSuppressionTimeout: number | null = null
|
||||
let monthSelectSyncFrame: number | null = null
|
||||
let calendarPortal: HTMLElement | null = null
|
||||
let inputFocusScrollSuppressionTarget: HTMLInputElement | null = null
|
||||
let originalInputFocus: HTMLInputElement['focus'] | null = null
|
||||
let suppressNextInputFocusScroll = false
|
||||
const calendarBaseClass = 'modrinth-date-picker-calendar'
|
||||
const twoCalendarClass = 'has-two-calendars'
|
||||
const calendarStateClasses = [
|
||||
'calendar-only',
|
||||
'show-today',
|
||||
@@ -299,6 +315,7 @@ function syncCalendarClasses(instance?: Instance) {
|
||||
if (!container) return
|
||||
|
||||
container.classList.add(calendarBaseClass)
|
||||
container.classList.toggle(twoCalendarClass, resolvedShowMonths.value === 2)
|
||||
|
||||
for (const cls of appliedCalendarClasses) {
|
||||
container.classList.remove(cls)
|
||||
@@ -868,7 +885,7 @@ function getTimeInputDigits(value: string) {
|
||||
|
||||
function sanitizeTimeInputValue(input: HTMLInputElement) {
|
||||
const nextValue = getTimeInputDigits(input.value)
|
||||
if (input.value === nextValue) return
|
||||
if (input.value === nextValue) return nextValue
|
||||
|
||||
const cursorPosition = input.selectionStart ?? nextValue.length
|
||||
const removedBeforeCursor =
|
||||
@@ -878,6 +895,7 @@ function sanitizeTimeInputValue(input: HTMLInputElement) {
|
||||
|
||||
input.value = nextValue
|
||||
input.setSelectionRange(nextCursorPosition, nextCursorPosition)
|
||||
return nextValue
|
||||
}
|
||||
|
||||
function normalizeTimeInputValue(input: HTMLInputElement) {
|
||||
@@ -917,14 +935,81 @@ function preventNonNumericTimeKeydown(event: KeyboardEvent) {
|
||||
if (event.key.length === 1 && /\D/.test(event.key)) event.preventDefault()
|
||||
}
|
||||
|
||||
function sanitizeNumericTimeInput(event: Event) {
|
||||
if (isTimeInput(event.target)) sanitizeTimeInputValue(event.target)
|
||||
function getTimeInputs(instance: Instance) {
|
||||
return [instance.hourElement, instance.minuteElement, instance.secondElement].filter(
|
||||
(input): input is HTMLInputElement => Boolean(input),
|
||||
)
|
||||
}
|
||||
|
||||
function getTimeInputDraft(input: HTMLInputElement) {
|
||||
return document.activeElement === input
|
||||
? (timeInputDigits.value.get(input) ?? getTimeInputDigits(input.value))
|
||||
: getTimeInputDigits(input.value)
|
||||
}
|
||||
|
||||
function getNormalizedTimeInputDraft(input: HTMLInputElement) {
|
||||
const draft = getTimeInputDraft(input)
|
||||
if (!draft) return null
|
||||
if (draft.length === 2) return draft
|
||||
|
||||
const minValue = Number.parseInt(input.min, 10)
|
||||
const maxValue = Number.parseInt(input.max, 10)
|
||||
let nextValue = Number.parseInt(draft, 10)
|
||||
|
||||
if (Number.isFinite(minValue)) nextValue = Math.max(nextValue, minValue)
|
||||
if (Number.isFinite(maxValue)) nextValue = Math.min(nextValue, maxValue)
|
||||
|
||||
return String(nextValue).padStart(2, '0')
|
||||
}
|
||||
|
||||
function prepareTimeInputValuesForCommit(instance: Instance) {
|
||||
for (const input of getTimeInputs(instance)) {
|
||||
const nextValue = getNormalizedTimeInputDraft(input)
|
||||
if (nextValue === null) return false
|
||||
|
||||
input.value = nextValue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function syncTimeInputDrafts(instance: Instance) {
|
||||
for (const input of getTimeInputs(instance)) {
|
||||
timeInputDigits.value.set(input, getTimeInputDigits(input.value))
|
||||
}
|
||||
}
|
||||
|
||||
function commitTimeInputForInput(event: Event) {
|
||||
if (!isTimeInput(event.target)) return
|
||||
|
||||
const instance = picker.value
|
||||
if (!instance) return
|
||||
|
||||
const previousValue = instance.input.value
|
||||
const nextDigits = sanitizeTimeInputValue(event.target)
|
||||
timeInputDigits.value.set(event.target, nextDigits)
|
||||
if (!prepareTimeInputValuesForCommit(instance)) return
|
||||
|
||||
event.target.dispatchEvent(new Event('increment', { bubbles: true }))
|
||||
if (nextDigits.length === 1 && document.activeElement === event.target) {
|
||||
event.target.value = nextDigits
|
||||
event.target.setSelectionRange(nextDigits.length, nextDigits.length)
|
||||
}
|
||||
syncTimeInputDrafts(instance)
|
||||
if (instance.input.value === previousValue) return
|
||||
|
||||
const nextValue =
|
||||
props.mode === 'single'
|
||||
? instance.input.value || null
|
||||
: instance.selectedDates.map((date) => instance.formatDate(date, resolvedDateFormat.value))
|
||||
model.value = nextValue
|
||||
emit('change', nextValue)
|
||||
}
|
||||
|
||||
function syncTimeInputTypes(instance: Instance) {
|
||||
const timeInputs = [instance.hourElement, instance.minuteElement, instance.secondElement].filter(
|
||||
(input): input is HTMLInputElement => Boolean(input),
|
||||
)
|
||||
const timeInputs = getTimeInputs(instance)
|
||||
const activeTimeInput = timeInputs.find((input) => document.activeElement === input)
|
||||
const activeDraft = activeTimeInput ? timeInputDigits.value.get(activeTimeInput) : undefined
|
||||
|
||||
for (const input of timeInputs) {
|
||||
input.type = 'text'
|
||||
@@ -932,6 +1017,71 @@ function syncTimeInputTypes(instance: Instance) {
|
||||
input.pattern = '[0-9]*'
|
||||
normalizeTimeInputValue(input)
|
||||
}
|
||||
|
||||
if (activeTimeInput && activeDraft !== undefined && activeDraft.length < 2) {
|
||||
activeTimeInput.value = activeDraft
|
||||
activeTimeInput.setSelectionRange(activeDraft.length, activeDraft.length)
|
||||
}
|
||||
|
||||
syncTimeInputDrafts(instance)
|
||||
}
|
||||
|
||||
function openPickerWithoutInputFocus(event: PointerEvent) {
|
||||
if (!props.readonly || props.disabled || props.calendarOnly) return
|
||||
if (event.button !== 0) return
|
||||
if (event.pointerType === 'mouse') return
|
||||
|
||||
event.preventDefault()
|
||||
suppressNextInputFocusScroll = true
|
||||
picker.value?.open()
|
||||
}
|
||||
|
||||
function suppressInputFocusScrollForCalendarPointer(event: PointerEvent) {
|
||||
if (!props.readonly || props.disabled || props.calendarOnly || !props.closeOnSelect) return
|
||||
if (event.button !== 0) return
|
||||
if (event.pointerType === 'mouse') return
|
||||
|
||||
const dayElem = getRangeDayElement(event.target)
|
||||
if (!dayElem || !isSelectableDay(dayElem)) return
|
||||
|
||||
suppressNextInputFocusScroll = true
|
||||
}
|
||||
|
||||
function patchInputFocusScrollSuppression(target: HTMLInputElement) {
|
||||
originalInputFocus = target.focus
|
||||
target.focus = ((options?: FocusOptions) => {
|
||||
if (suppressNextInputFocusScroll) {
|
||||
originalInputFocus?.call(target, { preventScroll: true })
|
||||
return
|
||||
}
|
||||
|
||||
originalInputFocus?.call(target, options)
|
||||
}) as HTMLInputElement['focus']
|
||||
}
|
||||
|
||||
function teardownInputFocusScrollSuppression() {
|
||||
inputFocusScrollSuppressionTarget?.removeEventListener('pointerdown', openPickerWithoutInputFocus)
|
||||
if (inputFocusScrollSuppressionTarget && originalInputFocus) {
|
||||
inputFocusScrollSuppressionTarget.focus = originalInputFocus
|
||||
}
|
||||
inputFocusScrollSuppressionTarget = null
|
||||
originalInputFocus = null
|
||||
suppressNextInputFocusScroll = false
|
||||
}
|
||||
|
||||
function syncInputFocusScrollSuppression() {
|
||||
const target = picker.value?.altInput ?? inputRef.value ?? null
|
||||
if (!target || !props.readonly || props.disabled || props.calendarOnly) {
|
||||
teardownInputFocusScrollSuppression()
|
||||
return
|
||||
}
|
||||
|
||||
if (inputFocusScrollSuppressionTarget === target) return
|
||||
|
||||
teardownInputFocusScrollSuppression()
|
||||
target.addEventListener('pointerdown', openPickerWithoutInputFocus)
|
||||
patchInputFocusScrollSuppression(target)
|
||||
inputFocusScrollSuppressionTarget = target
|
||||
}
|
||||
|
||||
const resolvedDateFormat = computed(
|
||||
@@ -944,19 +1094,6 @@ const resolvedShowMonths = computed(() =>
|
||||
Number.isFinite(props.showMonths) ? Math.max(1, Math.floor(props.showMonths)) : 1,
|
||||
)
|
||||
|
||||
const inputClasses = computed(() => [
|
||||
props.calendarOnly
|
||||
? 'sr-only pointer-events-none absolute h-0 w-0 opacity-0'
|
||||
: 'w-full text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none focus:ring-4 focus:ring-brand-shadow !outline-0',
|
||||
!props.calendarOnly && props.showIcon ? 'pl-10' : '',
|
||||
!props.calendarOnly && !props.showIcon ? 'pl-3' : '',
|
||||
!props.calendarOnly
|
||||
? 'pr-3 h-9 py-2 text-base outline-none bg-surface-4 border-none rounded-xl'
|
||||
: '',
|
||||
props.disabled && !props.calendarOnly ? 'cursor-not-allowed' : '',
|
||||
props.inputClass,
|
||||
])
|
||||
|
||||
const selectedDates = computed(() => {
|
||||
const value = model.value
|
||||
if (Array.isArray(value)) {
|
||||
@@ -966,6 +1103,28 @@ const selectedDates = computed(() => {
|
||||
return value ? [value] : []
|
||||
})
|
||||
|
||||
const hasClearButton = computed(
|
||||
() =>
|
||||
!props.calendarOnly &&
|
||||
props.clearable &&
|
||||
!props.disabled &&
|
||||
!props.readonly &&
|
||||
selectedDates.value.length > 0,
|
||||
)
|
||||
|
||||
const inputClasses = computed(() => [
|
||||
props.calendarOnly
|
||||
? 'sr-only pointer-events-none absolute h-0 w-0 opacity-0'
|
||||
: 'w-full touch-manipulation text-primary placeholder:text-secondary focus:text-contrast font-medium transition-[shadow,color] appearance-none shadow-none focus:ring-4 focus:ring-brand-shadow !outline-0',
|
||||
!props.calendarOnly && props.showIcon ? 'pl-10' : '',
|
||||
!props.calendarOnly && !props.showIcon ? 'pl-3' : '',
|
||||
!props.calendarOnly
|
||||
? `${hasClearButton.value ? 'pr-10' : 'pr-3'} h-9 py-2 text-base outline-none bg-surface-4 border-none rounded-xl`
|
||||
: '',
|
||||
props.disabled && !props.calendarOnly ? 'cursor-not-allowed' : '',
|
||||
props.inputClass,
|
||||
])
|
||||
|
||||
watch(
|
||||
() => [
|
||||
model.value,
|
||||
@@ -982,6 +1141,7 @@ watch(
|
||||
props.altFormat,
|
||||
props.time24hr,
|
||||
props.calendarOnly,
|
||||
props.closeOnSelect,
|
||||
props.position,
|
||||
],
|
||||
() => {
|
||||
@@ -1042,6 +1202,11 @@ onMounted(async () => {
|
||||
onReady: (_selectedDates, _dateStr, instance) => {
|
||||
syncCalendarView(instance)
|
||||
|
||||
instance.calendarContainer.addEventListener(
|
||||
'pointerdown',
|
||||
suppressInputFocusScrollForCalendarPointer,
|
||||
true,
|
||||
)
|
||||
instance.calendarContainer.addEventListener('pointerdown', startRangeDrag, true)
|
||||
instance.calendarContainer.addEventListener('mousedown', stopRangeEndpointMouseEvent, true)
|
||||
instance.calendarContainer.addEventListener('mouseup', stopRangeEndpointMouseEvent, true)
|
||||
@@ -1061,7 +1226,7 @@ onMounted(async () => {
|
||||
)
|
||||
instance.calendarContainer.addEventListener('beforeinput', preventNonNumericTimeInput, true)
|
||||
instance.calendarContainer.addEventListener('keydown', preventNonNumericTimeKeydown, true)
|
||||
instance.calendarContainer.addEventListener('input', sanitizeNumericTimeInput, true)
|
||||
instance.calendarContainer.addEventListener('input', commitTimeInputForInput, true)
|
||||
|
||||
instance.calendarContainer.addEventListener('mousedown', (event) => {
|
||||
if (props.mode !== 'range') return
|
||||
@@ -1119,6 +1284,7 @@ onMounted(async () => {
|
||||
syncCalendarStateClasses(instance)
|
||||
syncRangeEndpointMoveState(instance)
|
||||
syncMultiMonthSelects(instance)
|
||||
syncInputFocusScrollSuppression()
|
||||
},
|
||||
onChange: (_selectedDates, dateStr, instance) => {
|
||||
if (isSyncingFromModel.value) return
|
||||
@@ -1145,6 +1311,12 @@ onMounted(async () => {
|
||||
},
|
||||
onClose: (_selectedDates, dateStr, instance) => {
|
||||
cancelRangeEndpointMovePreview()
|
||||
if (suppressNextInputFocusScroll) {
|
||||
const focusTarget = instance.altInput ?? inputRef.value
|
||||
focusTarget?.blur()
|
||||
suppressNextInputFocusScroll = false
|
||||
}
|
||||
|
||||
if (hasCompleteModelRange()) {
|
||||
syncPickerFromModel()
|
||||
return
|
||||
@@ -1192,6 +1364,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
syncAltInputState()
|
||||
syncInputFocusScrollSuppression()
|
||||
syncPickerFromModel()
|
||||
syncHeaderControlState(picker.value)
|
||||
syncRangeEndpointMoveState(picker.value)
|
||||
@@ -1204,6 +1377,7 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('pointermove', updateRangeDrag, true)
|
||||
document.removeEventListener('pointerup', stopRangeDrag, true)
|
||||
document.removeEventListener('pointercancel', stopRangeDrag, true)
|
||||
teardownInputFocusScrollSuppression()
|
||||
picker.value?.destroy()
|
||||
destroyCalendarPortal()
|
||||
})
|
||||
@@ -1215,7 +1389,7 @@ function flatpickrOptions(): Options {
|
||||
altInputClass: props.calendarOnly ? undefined : inputClasses.value.filter(Boolean).join(' '),
|
||||
altFormat: resolvedAltFormat.value,
|
||||
appendTo: ensureCalendarPortal(),
|
||||
closeOnSelect: false,
|
||||
closeOnSelect: props.closeOnSelect,
|
||||
dateFormat: resolvedDateFormat.value,
|
||||
disableMobile: true,
|
||||
enableTime: props.enableTime,
|
||||
@@ -1267,25 +1441,31 @@ function syncPickerFromModel() {
|
||||
}
|
||||
|
||||
function syncAltInputState() {
|
||||
if (!picker.value?.altInput) return
|
||||
if (!picker.value?.altInput) {
|
||||
syncInputFocusScrollSuppression()
|
||||
return
|
||||
}
|
||||
|
||||
picker.value.altInput.disabled = props.disabled
|
||||
picker.value.altInput.readOnly = props.readonly
|
||||
syncInputFocusScrollSuppression()
|
||||
}
|
||||
|
||||
function clearValue() {
|
||||
const nextValue = props.mode === 'single' ? null : []
|
||||
model.value = nextValue
|
||||
picker.value?.clear(false)
|
||||
setRangeEndpointMoveState(null)
|
||||
syncMissingRangeEndState()
|
||||
emit('clear')
|
||||
emit('change', nextValue)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus: () => picker.value?.altInput?.focus() ?? inputRef.value?.focus(),
|
||||
open: () => picker.value?.open(),
|
||||
close: () => picker.value?.close(),
|
||||
clear: () => {
|
||||
const nextValue = props.mode === 'single' ? null : []
|
||||
model.value = nextValue
|
||||
picker.value?.clear(false)
|
||||
setRangeEndpointMoveState(null)
|
||||
syncMissingRangeEndState()
|
||||
emit('clear')
|
||||
emit('change', nextValue)
|
||||
},
|
||||
clear: clearValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1295,7 +1475,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar) {
|
||||
@apply mt-2 rounded-2xl border border-solid border-surface-5 bg-surface-3 shadow-none p-3 text-primary select-none;
|
||||
@apply mt-2 touch-manipulation rounded-2xl border border-solid border-surface-5 bg-surface-3 shadow-none p-3 text-primary select-none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
@@ -1333,6 +1513,27 @@ defineExpose({
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars) {
|
||||
width: calc(615.75px + 0.75rem) !important;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-months),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdays),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-days) {
|
||||
@apply gap-3;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-rContainer),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdays),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-days) {
|
||||
width: calc(615.75px + 0.75rem) !important;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-month),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar.has-two-calendars .flatpickr-weekdaycontainer) {
|
||||
@apply max-w-[307.875px] min-w-[307.875px] flex-none;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar::before),
|
||||
.modrinth-date-picker :deep(.flatpickr-calendar::after) {
|
||||
display: none;
|
||||
@@ -1356,7 +1557,7 @@ defineExpose({
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-current-month input.cur-year),
|
||||
.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months) {
|
||||
@apply rounded-xl bg-surface-4 py-1 font-semibold text-contrast hover:bg-surface-5 min-h-10;
|
||||
@apply touch-manipulation rounded-xl bg-surface-4 py-1 font-semibold text-contrast hover:bg-surface-5 min-h-10;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-current-month .flatpickr-monthDropdown-months) {
|
||||
@@ -1414,7 +1615,7 @@ defineExpose({
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-prev-month),
|
||||
.modrinth-date-picker :deep(.flatpickr-next-month) {
|
||||
@apply top-2.5 mx-3.5 flex h-10 w-10 items-center justify-center rounded-full p-0 text-secondary hover:bg-surface-4 hover:text-contrast;
|
||||
@apply top-2.5 mx-3.5 flex h-10 w-10 touch-manipulation items-center justify-center rounded-full p-0 text-secondary hover:bg-surface-4 hover:text-contrast;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-prev-month.flatpickr-disabled),
|
||||
@@ -1439,7 +1640,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-day) {
|
||||
@apply relative z-0 m-0 max-w-none rounded-full border border-solid border-transparent text-primary hover:bg-surface-4 hover:text-contrast font-semibold aspect-square h-auto;
|
||||
@apply relative z-0 m-0 max-w-none touch-manipulation rounded-full border border-solid border-transparent text-primary hover:bg-surface-4 hover:text-contrast font-semibold aspect-square h-auto;
|
||||
}
|
||||
.modrinth-date-picker
|
||||
:deep(
|
||||
@@ -1625,7 +1826,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-time) {
|
||||
@apply mt-2 flex h-11 max-h-none items-center gap-2 border-0 border-t border-solid border-surface-5 px-1 pt-2 leading-none;
|
||||
@apply mt-2 flex h-11 max-h-none items-center gap-2 border-0 border-t border-solid border-surface-5 px-1 pt-2 overflow-visible leading-none;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-time .numInputWrapper) {
|
||||
@@ -1634,7 +1835,7 @@ defineExpose({
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-time input),
|
||||
.modrinth-date-picker :deep(.flatpickr-time .flatpickr-am-pm) {
|
||||
@apply h-full rounded-xl bg-transparent px-2 text-center font-semibold text-primary hover:bg-surface-5 focus:bg-surface-5;
|
||||
@apply h-full touch-manipulation rounded-xl bg-transparent px-2 text-center font-semibold text-primary hover:bg-surface-5 focus:bg-surface-5;
|
||||
}
|
||||
|
||||
.modrinth-date-picker :deep(.flatpickr-time .flatpickr-time-separator) {
|
||||
|
||||
Reference in New Issue
Block a user