Refactor new datepicker so that we can use a custom grid cell, and fix issues with timezone offsets
This commit is contained in:
parent
0aeddfa029
commit
4d24b2ba1c
|
@ -3,10 +3,11 @@
|
||||||
import Select from "../../Select.svelte"
|
import Select from "../../Select.svelte"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import NumberInput from "./NumberInput.svelte"
|
import NumberInput from "./NumberInput.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let onChange
|
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
const DaysOfWeek = [
|
const DaysOfWeek = [
|
||||||
"Monday",
|
"Monday",
|
||||||
"Tuesday",
|
"Tuesday",
|
||||||
|
@ -58,7 +59,10 @@
|
||||||
|
|
||||||
const handleDateChange = date => {
|
const handleDateChange = date => {
|
||||||
const base = value || now
|
const base = value || now
|
||||||
onChange(base.year(date.year()).month(date.month()).date(date.date()))
|
dispatch(
|
||||||
|
"change",
|
||||||
|
base.year(date.year()).month(date.month()).date(date.date())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setDate = date => {
|
export const setDate = date => {
|
||||||
|
@ -224,6 +228,9 @@
|
||||||
.spectrum-Calendar-date.is-selected {
|
.spectrum-Calendar-date.is-selected {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.spectrum-Calendar-dayOfWeek {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
/* Style select */
|
/* Style select */
|
||||||
.month-selector :global(.spectrum-Picker) {
|
.month-selector :global(.spectrum-Picker) {
|
||||||
|
|
|
@ -3,13 +3,10 @@
|
||||||
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||||
import "@spectrum-css/textfield/dist/index-vars.css"
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
import Popover from "../../../Popover/Popover.svelte"
|
import Popover from "../../../Popover/Popover.svelte"
|
||||||
import dayjs from "dayjs"
|
import { onMount } from "svelte"
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
|
||||||
import TimePicker from "./TimePicker.svelte"
|
|
||||||
import Calendar from "./Calendar.svelte"
|
|
||||||
import DateInput from "./DateInput.svelte"
|
import DateInput from "./DateInput.svelte"
|
||||||
import ActionButton from "../../../ActionButton/ActionButton.svelte"
|
|
||||||
import { parseDate } from "../../../helpers"
|
import { parseDate } from "../../../helpers"
|
||||||
|
import DatePickerPopoverContents from "./DatePickerPopoverContents.svelte"
|
||||||
|
|
||||||
export let id = null
|
export let id = null
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
@ -25,74 +22,18 @@
|
||||||
export let api = null
|
export let api = null
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
let isOpen = false
|
let isOpen = false
|
||||||
let anchor
|
let anchor
|
||||||
let popover
|
let popover
|
||||||
let calendar
|
|
||||||
|
|
||||||
$: parsedValue = parseDate(value, { timeOnly, dateOnly: !enableTime })
|
$: parsedValue = parseDate(value, { timeOnly, enableTime })
|
||||||
$: showCalendar = !timeOnly
|
|
||||||
$: showTime = enableTime || timeOnly
|
|
||||||
|
|
||||||
const clearDateOnBackspace = event => {
|
|
||||||
// Ignore if we're typing a value
|
|
||||||
if (document.activeElement?.tagName.toLowerCase() === "input") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (["Backspace", "Clear", "Delete"].includes(event.key)) {
|
|
||||||
handleChange(null)
|
|
||||||
popover?.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
isOpen = true
|
isOpen = true
|
||||||
if (useKeyboardShortcuts) {
|
|
||||||
document.addEventListener("keyup", clearDateOnBackspace)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
isOpen = false
|
isOpen = false
|
||||||
if (useKeyboardShortcuts) {
|
|
||||||
document.removeEventListener("keyup", clearDateOnBackspace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = date => {
|
|
||||||
if (!date) {
|
|
||||||
dispatch("change", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let newValue = date.toISOString()
|
|
||||||
|
|
||||||
// Time only fields always ignore timezones, otherwise they make no sense.
|
|
||||||
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
|
|
||||||
// time picked, without timezone
|
|
||||||
const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
|
|
||||||
if (offsetForTimezone) {
|
|
||||||
const offset = new Date().getTimezoneOffset() * 60000
|
|
||||||
newValue = new Date(date.valueOf() - offset).toISOString().slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For date-only fields, construct a manual timestamp string without a time
|
|
||||||
// or time zone
|
|
||||||
else if (!enableTime) {
|
|
||||||
const year = date.year()
|
|
||||||
const month = `${date.month() + 1}`.padStart(2, "0")
|
|
||||||
const day = `${date.date()}`.padStart(2, "0")
|
|
||||||
newValue = `${year}-${month}-${day}T00:00:00.000`
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch("change", newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setToNow = () => {
|
|
||||||
const now = dayjs()
|
|
||||||
calendar?.setDate(now)
|
|
||||||
handleChange(now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -130,54 +71,13 @@
|
||||||
{align}
|
{align}
|
||||||
>
|
>
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="date-time-popover">
|
<DatePickerPopoverContents
|
||||||
{#if showCalendar}
|
{useKeyboardShortcuts}
|
||||||
<Calendar
|
{ignoreTimezones}
|
||||||
value={parsedValue}
|
{enableTime}
|
||||||
onChange={handleChange}
|
{timeOnly}
|
||||||
bind:this={calendar}
|
value={parsedValue}
|
||||||
/>
|
on:change
|
||||||
{/if}
|
/>
|
||||||
<div class="footer" class:spaced={showCalendar}>
|
|
||||||
{#if showTime}
|
|
||||||
<TimePicker value={parsedValue} onChange={handleChange} />
|
|
||||||
{/if}
|
|
||||||
<div class="actions">
|
|
||||||
<ActionButton
|
|
||||||
disabled={!value}
|
|
||||||
size="S"
|
|
||||||
on:click={() => handleChange(null)}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton size="S" on:click={setToNow}>
|
|
||||||
{showTime ? "Now" : "Today"}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<style>
|
|
||||||
.date-time-popover {
|
|
||||||
padding: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 60px;
|
|
||||||
}
|
|
||||||
.footer.spaced {
|
|
||||||
padding-top: 14px;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
padding: 4px 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<script>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import TimePicker from "./TimePicker.svelte"
|
||||||
|
import Calendar from "./Calendar.svelte"
|
||||||
|
import ActionButton from "../../../ActionButton/ActionButton.svelte"
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
import { stringifyDate } from "../../../helpers"
|
||||||
|
|
||||||
|
export let useKeyboardShortcuts = true
|
||||||
|
export let ignoreTimezones
|
||||||
|
export let enableTime
|
||||||
|
export let timeOnly
|
||||||
|
export let value
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let calendar
|
||||||
|
|
||||||
|
$: showCalendar = !timeOnly
|
||||||
|
$: showTime = enableTime || timeOnly
|
||||||
|
|
||||||
|
const setToNow = () => {
|
||||||
|
const now = dayjs()
|
||||||
|
calendar?.setDate(now)
|
||||||
|
handleChange(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = date => {
|
||||||
|
dispatch(
|
||||||
|
"change",
|
||||||
|
stringifyDate(date, { enableTime, timeOnly, ignoreTimezones })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDateOnBackspace = event => {
|
||||||
|
// Ignore if we're typing a value
|
||||||
|
if (document.activeElement?.tagName.toLowerCase() === "input") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (["Backspace", "Clear", "Delete"].includes(event.key)) {
|
||||||
|
dispatch("change", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (useKeyboardShortcuts) {
|
||||||
|
document.addEventListener("keyup", clearDateOnBackspace)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keyup", clearDateOnBackspace)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="date-time-popover">
|
||||||
|
{#if showCalendar}
|
||||||
|
<Calendar
|
||||||
|
{value}
|
||||||
|
on:change={e => handleChange(e.detail)}
|
||||||
|
bind:this={calendar}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="footer" class:spaced={showCalendar}>
|
||||||
|
{#if showTime}
|
||||||
|
<TimePicker {value} on:change={e => handleChange(e.detail)} />
|
||||||
|
{/if}
|
||||||
|
<div class="actions">
|
||||||
|
<ActionButton
|
||||||
|
disabled={!value}
|
||||||
|
size="S"
|
||||||
|
on:click={() => dispatch("change", null)}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton size="S" on:click={setToNow}>
|
||||||
|
{showTime ? "Now" : "Today"}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.date-time-popover {
|
||||||
|
padding: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 60px;
|
||||||
|
}
|
||||||
|
.footer.spaced {
|
||||||
|
padding-top: 14px;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
padding: 4px 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -33,7 +33,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box !important;
|
||||||
}
|
}
|
||||||
input:focus,
|
input:focus,
|
||||||
input:hover {
|
input:hover {
|
||||||
|
|
|
@ -2,18 +2,20 @@
|
||||||
import { cleanInput } from "./utils"
|
import { cleanInput } from "./utils"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import NumberInput from "./NumberInput.svelte"
|
import NumberInput from "./NumberInput.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let onChange
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: displayValue = value || dayjs()
|
$: displayValue = value || dayjs()
|
||||||
|
|
||||||
const handleHourChange = e => {
|
const handleHourChange = e => {
|
||||||
onChange(displayValue.hour(parseInt(e.target.value)))
|
dispatch("change", displayValue.hour(parseInt(e.target.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMinuteChange = e => {
|
const handleMinuteChange = e => {
|
||||||
onChange(displayValue.minute(parseInt(e.target.value)))
|
dispatch("change", displayValue.minute(parseInt(e.target.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
|
const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
|
||||||
|
@ -51,7 +53,7 @@
|
||||||
.time-picker span {
|
.time-picker span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
z-index: -1;
|
z-index: 0;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -9,6 +9,7 @@ export { default as CoreCombobox } from "./Combobox.svelte"
|
||||||
export { default as CoreSwitch } from "./Switch.svelte"
|
export { default as CoreSwitch } from "./Switch.svelte"
|
||||||
export { default as CoreSearch } from "./Search.svelte"
|
export { default as CoreSearch } from "./Search.svelte"
|
||||||
export { default as CoreDatePicker } from "./DatePicker/DatePicker.svelte"
|
export { default as CoreDatePicker } from "./DatePicker/DatePicker.svelte"
|
||||||
|
export { default as CoreDatePickerPopoverContents } from "./DatePicker/DatePickerPopoverContents.svelte"
|
||||||
export { default as CoreDateRangePicker } from "./DateRangePicker.svelte"
|
export { default as CoreDateRangePicker } from "./DateRangePicker.svelte"
|
||||||
export { default as CoreDropzone } from "./Dropzone.svelte"
|
export { default as CoreDropzone } from "./Dropzone.svelte"
|
||||||
export { default as CoreStepper } from "./Stepper.svelte"
|
export { default as CoreStepper } from "./Stepper.svelte"
|
||||||
|
|
|
@ -117,7 +117,9 @@ export const copyToClipboard = value => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseDate = (value, { dateOnly } = {}) => {
|
// Parsed a date value. This is usually an ISO string, but can be a
|
||||||
|
// bunch of different formats and shapes depending on schema flags.
|
||||||
|
export const parseDate = (value, { enableTime = true }) => {
|
||||||
// If empty then invalid
|
// If empty then invalid
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return null
|
return null
|
||||||
|
@ -131,7 +133,7 @@ export const parseDate = (value, { dateOnly } = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If date only, check for cases where we received a UTC string
|
// If date only, check for cases where we received a UTC string
|
||||||
else if (dateOnly && value.endsWith("Z")) {
|
else if (!enableTime && value.endsWith("Z")) {
|
||||||
value = value.split("Z")[0]
|
value = value.split("Z")[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,7 +150,42 @@ export const parseDate = (value, { dateOnly } = {}) => {
|
||||||
return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
|
return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDateDisplayValue = (value, { enableTime, timeOnly }) => {
|
// Stringifies a dayjs object to create an ISO string that respects the various
|
||||||
|
// schema flags
|
||||||
|
export const stringifyDate = (
|
||||||
|
value,
|
||||||
|
{ enableTime = true, timeOnly = false, ignoreTimezones = false }
|
||||||
|
) => {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time only fields always ignore timezones, otherwise they make no sense.
|
||||||
|
// For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
|
||||||
|
// time picked, without timezone
|
||||||
|
const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
|
||||||
|
if (offsetForTimezone) {
|
||||||
|
// Ensure we use the correct offset for the date
|
||||||
|
const referenceDate = timeOnly ? new Date() : value.toDate()
|
||||||
|
const offset = referenceDate.getTimezoneOffset() * 60000
|
||||||
|
return new Date(value.valueOf() - offset).toISOString().slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For date-only fields, construct a manual timestamp string without a time
|
||||||
|
// or time zone
|
||||||
|
else if (!enableTime) {
|
||||||
|
const year = value.year()
|
||||||
|
const month = `${value.month() + 1}`.padStart(2, "0")
|
||||||
|
const day = `${value.date()}`.padStart(2, "0")
|
||||||
|
return `${year}-${month}-${day}T00:00:00.000`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats a dayjs date according to schema flags
|
||||||
|
export const getDateDisplayValue = (
|
||||||
|
value,
|
||||||
|
{ enableTime = true, timeOnly = false }
|
||||||
|
) => {
|
||||||
if (!value?.isValid()) {
|
if (!value?.isValid()) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { CoreDatePicker, Icon, Helpers } from "@budibase/bbui"
|
import {
|
||||||
|
CoreDatePickerPopoverContents,
|
||||||
|
Icon,
|
||||||
|
Helpers,
|
||||||
|
clickOutside,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { debounce } from "../../../utils/utils"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -9,44 +16,89 @@
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let api
|
export let api
|
||||||
|
|
||||||
let datePickerAPI
|
|
||||||
let isOpen
|
let isOpen
|
||||||
|
|
||||||
$: timeOnly = schema?.timeOnly
|
$: timeOnly = schema?.timeOnly
|
||||||
$: dateOnly = schema?.dateOnly
|
$: enableTime = !schema?.dateOnly
|
||||||
|
$: ignoreTimezones = schema?.ignoreTimezones
|
||||||
$: editable = focused && !readonly
|
$: editable = focused && !readonly
|
||||||
$: displayValue = getDisplayValue(value, timeOnly, dateOnly)
|
$: parsedValue = Helpers.parseDate(value, {
|
||||||
|
timeOnly,
|
||||||
|
enableTime,
|
||||||
|
ignoreTimezones,
|
||||||
|
})
|
||||||
|
$: displayValue = getDisplayValue(parsedValue, timeOnly, enableTime)
|
||||||
|
// Ensure open state matches desired state
|
||||||
|
$: {
|
||||||
|
if (!focused && isOpen) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getDisplayValue = (value, timeOnly, dateOnly) => {
|
const getDisplayValue = (value, timeOnly, enableTime) => {
|
||||||
const parsedDate = Helpers.parseDate(value, { dateOnly })
|
return Helpers.getDateDisplayValue(value, {
|
||||||
return Helpers.getDateDisplayValue(parsedDate, {
|
enableTime,
|
||||||
enableTime: !dateOnly,
|
|
||||||
timeOnly,
|
timeOnly,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we close flatpickr when unselected
|
const open = () => {
|
||||||
$: {
|
isOpen = true
|
||||||
if (!focused) {
|
|
||||||
datePickerAPI?.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyDown = () => {
|
const close = () => {
|
||||||
return false
|
isOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = e => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
changeDate(-1, "week")
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
changeDate(1, "week")
|
||||||
|
} else if (e.key === "ArrowLeft") {
|
||||||
|
changeDate(-1, "day")
|
||||||
|
} else if (e.key === "ArrowRight") {
|
||||||
|
changeDate(1, "day")
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeDate = (quantity, unit) => {
|
||||||
|
if (!value) {
|
||||||
|
value = dayjs()
|
||||||
|
} else {
|
||||||
|
value = dayjs(value).add(quantity, unit)
|
||||||
|
}
|
||||||
|
debouncedOnChange(
|
||||||
|
Helpers.stringifyDate(value, {
|
||||||
|
enableTime,
|
||||||
|
timeOnly,
|
||||||
|
ignoreTimezones,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedOnChange = debounce(onChange, 250)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
focus: () => datePickerAPI?.open(),
|
focus: open,
|
||||||
blur: () => datePickerAPI?.close(),
|
blur: close,
|
||||||
isActive: () => isOpen,
|
isActive: () => isOpen,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="container" class:editable on:click={editable ? open : null}>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
{displayValue}
|
{displayValue}
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,17 +107,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if editable}
|
{#if isOpen}
|
||||||
<div class="picker">
|
<div class="picker" use:clickOutside={close}>
|
||||||
<CoreDatePicker
|
<CoreDatePickerPopoverContents
|
||||||
{value}
|
value={parsedValue}
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
enableTime={!dateOnly}
|
{enableTime}
|
||||||
{timeOnly}
|
{timeOnly}
|
||||||
ignoreTimezones={schema.ignoreTimezones}
|
{ignoreTimezones}
|
||||||
bind:api={datePickerAPI}
|
|
||||||
on:open={() => (isOpen = true)}
|
|
||||||
on:close={() => (isOpen = false)}
|
|
||||||
useKeyboardShortcuts={false}
|
useKeyboardShortcuts={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -80,6 +129,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
gap: var(--cell-spacing);
|
gap: var(--cell-spacing);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.container.editable:hover {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.value {
|
.value {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -92,9 +145,10 @@
|
||||||
}
|
}
|
||||||
.picker {
|
.picker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
top: 100%;
|
||||||
}
|
left: -1px;
|
||||||
.picker :global(.spectrum-Textfield-input) {
|
background: var(--grid-background-alt);
|
||||||
width: 100%;
|
border: var(--cell-border);
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -103,8 +103,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: calc(100% + var(--max-cell-render-width-overflow));
|
width: calc(100% + var(--max-cell-render-verflow));
|
||||||
height: calc(var(--row-height) + var(--max-cell-render-height));
|
height: calc(var(--row-height) + var(--max-cell-render-overflow));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
||||||
$: {
|
$: {
|
||||||
// Close when deselected
|
// Close when deselected
|
||||||
if (!focused) {
|
if (!focused && isOpen) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +219,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
max-height: var(--max-cell-render-height);
|
max-height: var(--max-cell-render-overflow);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
$: lookupMap = buildLookupMap(value, isOpen)
|
$: lookupMap = buildLookupMap(value, isOpen)
|
||||||
$: debouncedSearch(searchString)
|
$: debouncedSearch(searchString)
|
||||||
$: {
|
$: {
|
||||||
if (!focused) {
|
if (!focused && isOpen) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -451,7 +451,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: calc(
|
max-height: calc(
|
||||||
var(--max-cell-render-height) + var(--row-height) - var(--values-height)
|
var(--max-cell-render-overflow) + var(--row-height) - var(--values-height)
|
||||||
);
|
);
|
||||||
background: var(--grid-background-alt);
|
background: var(--grid-background-alt);
|
||||||
border: var(--cell-border);
|
border: var(--cell-border);
|
||||||
|
|
|
@ -22,8 +22,7 @@
|
||||||
import NewRow from "./NewRow.svelte"
|
import NewRow from "./NewRow.svelte"
|
||||||
import { createGridWebsocket } from "../lib/websocket"
|
import { createGridWebsocket } from "../lib/websocket"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderOverflow,
|
||||||
MaxCellRenderWidthOverflow,
|
|
||||||
GutterWidth,
|
GutterWidth,
|
||||||
DefaultRowHeight,
|
DefaultRowHeight,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
@ -78,6 +77,7 @@
|
||||||
contentLines,
|
contentLines,
|
||||||
gridFocused,
|
gridFocused,
|
||||||
error,
|
error,
|
||||||
|
focusedCellId,
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Keep config store up to date with props
|
// Keep config store up to date with props
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
class:quiet
|
class:quiet
|
||||||
on:mouseenter={() => gridFocused.set(true)}
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
on:mouseleave={() => gridFocused.set(false)}
|
on:mouseleave={() => gridFocused.set(false)}
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines};"
|
||||||
>
|
>
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export const Padding = 246
|
export const Padding = 246
|
||||||
export const MaxCellRenderHeight = 222
|
|
||||||
export const ScrollBarSize = 8
|
export const ScrollBarSize = 8
|
||||||
export const GutterWidth = 72
|
export const GutterWidth = 72
|
||||||
export const DefaultColumnWidth = 200
|
export const DefaultColumnWidth = 200
|
||||||
|
@ -12,4 +11,4 @@ export const NewRowID = "new"
|
||||||
export const BlankRowID = "blank"
|
export const BlankRowID = "blank"
|
||||||
export const RowPageSize = 100
|
export const RowPageSize = 100
|
||||||
export const FocusedCellMinOffset = 48
|
export const FocusedCellMinOffset = 48
|
||||||
export const MaxCellRenderWidthOverflow = Padding - 3 * ScrollBarSize
|
export const MaxCellRenderOverflow = Padding - 3 * ScrollBarSize
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderOverflow,
|
||||||
MaxCellRenderWidthOverflow,
|
|
||||||
MinColumnWidth,
|
MinColumnWidth,
|
||||||
ScrollBarSize,
|
ScrollBarSize,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
@ -95,11 +94,11 @@ export const deriveStores = context => {
|
||||||
|
|
||||||
// Compute the last row index with space to render popovers below it
|
// Compute the last row index with space to render popovers below it
|
||||||
const minBottom =
|
const minBottom =
|
||||||
$height - ScrollBarSize * 3 - MaxCellRenderHeight + offset
|
$height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
|
||||||
const lastIdx = Math.floor(minBottom / $rowHeight)
|
const lastIdx = Math.floor(minBottom / $rowHeight)
|
||||||
|
|
||||||
// Compute the first row index with space to render popovers above it
|
// Compute the first row index with space to render popovers above it
|
||||||
const minTop = MaxCellRenderHeight + offset
|
const minTop = MaxCellRenderOverflow + offset
|
||||||
const firstIdx = Math.ceil(minTop / $rowHeight)
|
const firstIdx = Math.ceil(minTop / $rowHeight)
|
||||||
|
|
||||||
// Use the greater of the two indices so that we prefer content below,
|
// Use the greater of the two indices so that we prefer content below,
|
||||||
|
@ -117,7 +116,7 @@ export const deriveStores = context => {
|
||||||
let inversionIdx = $visibleColumns.length
|
let inversionIdx = $visibleColumns.length
|
||||||
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
||||||
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
|
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
|
||||||
if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) {
|
if (rightEdge + MaxCellRenderOverflow <= cutoff) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue