@budibase/bbui 2.23.12 → 2.24.1

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.
@@ -0,0 +1,83 @@
1
+ <script>
2
+ import "@spectrum-css/calendar/dist/index-vars.css"
3
+ import "@spectrum-css/inputgroup/dist/index-vars.css"
4
+ import "@spectrum-css/textfield/dist/index-vars.css"
5
+ import Popover from "../../../Popover/Popover.svelte"
6
+ import { onMount } from "svelte"
7
+ import DateInput from "./DateInput.svelte"
8
+ import { parseDate } from "../../../helpers"
9
+ import DatePickerPopoverContents from "./DatePickerPopoverContents.svelte"
10
+
11
+ export let id = null
12
+ export let disabled = false
13
+ export let readonly = false
14
+ export let error = null
15
+ export let enableTime = true
16
+ export let value = null
17
+ export let placeholder = null
18
+ export let timeOnly = false
19
+ export let ignoreTimezones = false
20
+ export let useKeyboardShortcuts = true
21
+ export let appendTo = null
22
+ export let api = null
23
+ export let align = "left"
24
+
25
+ let isOpen = false
26
+ let anchor
27
+ let popover
28
+
29
+ $: parsedValue = parseDate(value, { timeOnly, enableTime })
30
+
31
+ const onOpen = () => {
32
+ isOpen = true
33
+ }
34
+
35
+ const onClose = () => {
36
+ isOpen = false
37
+ }
38
+
39
+ onMount(() => {
40
+ api = {
41
+ open: () => popover?.show(),
42
+ close: () => popover?.hide(),
43
+ }
44
+ })
45
+ </script>
46
+
47
+ <DateInput
48
+ bind:anchor
49
+ {disabled}
50
+ {readonly}
51
+ {error}
52
+ {placeholder}
53
+ {id}
54
+ {enableTime}
55
+ {timeOnly}
56
+ focused={isOpen}
57
+ value={parsedValue}
58
+ on:click={popover?.show}
59
+ icon={timeOnly ? "Clock" : "Calendar"}
60
+ />
61
+
62
+ <Popover
63
+ bind:this={popover}
64
+ on:open
65
+ on:close
66
+ on:open={onOpen}
67
+ on:close={onClose}
68
+ portalTarget={appendTo}
69
+ {anchor}
70
+ {align}
71
+ resizable={false}
72
+ >
73
+ {#if isOpen}
74
+ <DatePickerPopoverContents
75
+ {useKeyboardShortcuts}
76
+ {ignoreTimezones}
77
+ {enableTime}
78
+ {timeOnly}
79
+ value={parsedValue}
80
+ on:change
81
+ />
82
+ {/if}
83
+ </Popover>
@@ -0,0 +1,102 @@
1
+ <script>
2
+ import dayjs from "dayjs"
3
+ import TimePicker from "./TimePicker.svelte"
4
+ import Calendar from "./Calendar.svelte"
5
+ import ActionButton from "../../../ActionButton/ActionButton.svelte"
6
+ import { createEventDispatcher, onMount } from "svelte"
7
+ import { stringifyDate } from "../../../helpers"
8
+
9
+ export let useKeyboardShortcuts = true
10
+ export let ignoreTimezones
11
+ export let enableTime
12
+ export let timeOnly
13
+ export let value
14
+
15
+ const dispatch = createEventDispatcher()
16
+ let calendar
17
+
18
+ $: showCalendar = !timeOnly
19
+ $: showTime = enableTime || timeOnly
20
+
21
+ const setToNow = () => {
22
+ const now = dayjs()
23
+ calendar?.setDate(now)
24
+ handleChange(now)
25
+ }
26
+
27
+ const handleChange = date => {
28
+ dispatch(
29
+ "change",
30
+ stringifyDate(date, { enableTime, timeOnly, ignoreTimezones })
31
+ )
32
+ }
33
+
34
+ const clearDateOnBackspace = event => {
35
+ // Ignore if we're typing a value
36
+ if (document.activeElement?.tagName.toLowerCase() === "input") {
37
+ return
38
+ }
39
+ if (["Backspace", "Clear", "Delete"].includes(event.key)) {
40
+ dispatch("change", null)
41
+ }
42
+ }
43
+
44
+ onMount(() => {
45
+ if (useKeyboardShortcuts) {
46
+ document.addEventListener("keyup", clearDateOnBackspace)
47
+ }
48
+ return () => {
49
+ document.removeEventListener("keyup", clearDateOnBackspace)
50
+ }
51
+ })
52
+ </script>
53
+
54
+ <div class="date-time-popover">
55
+ {#if showCalendar}
56
+ <Calendar
57
+ {value}
58
+ on:change={e => handleChange(e.detail)}
59
+ bind:this={calendar}
60
+ />
61
+ {/if}
62
+ <div class="footer" class:spaced={showCalendar}>
63
+ {#if showTime}
64
+ <TimePicker {value} on:change={e => handleChange(e.detail)} />
65
+ {/if}
66
+ <div class="actions">
67
+ <ActionButton
68
+ disabled={!value}
69
+ size="S"
70
+ on:click={() => dispatch("change", null)}
71
+ >
72
+ Clear
73
+ </ActionButton>
74
+ <ActionButton size="S" on:click={setToNow}>
75
+ {showTime ? "Now" : "Today"}
76
+ </ActionButton>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <style>
82
+ .date-time-popover {
83
+ padding: 8px;
84
+ overflow: hidden;
85
+ }
86
+ .footer {
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ gap: 60px;
91
+ }
92
+ .footer.spaced {
93
+ padding-top: 14px;
94
+ }
95
+ .actions {
96
+ padding: 4px 0;
97
+ flex: 1 1 auto;
98
+ display: flex;
99
+ justify-content: flex-end;
100
+ gap: 6px;
101
+ }
102
+ </style>
@@ -0,0 +1,54 @@
1
+ <script>
2
+ export let value
3
+ export let min
4
+ export let max
5
+ export let hideArrows = false
6
+ export let width
7
+
8
+ $: style = width ? `width:${width}px;` : ""
9
+ </script>
10
+
11
+ <input
12
+ class:hide-arrows={hideArrows}
13
+ type="number"
14
+ {style}
15
+ {value}
16
+ {min}
17
+ {max}
18
+ onclick="this.select()"
19
+ on:change
20
+ on:input
21
+ />
22
+
23
+ <style>
24
+ input {
25
+ background: none;
26
+ border: none;
27
+ outline: none;
28
+ color: var(--spectrum-alias-text-color);
29
+ padding: 4px 6px 5px 6px;
30
+ border-radius: 4px;
31
+ transition: background 130ms ease-out;
32
+ font-size: 18px;
33
+ font-weight: bold;
34
+ font-family: var(--font-sans);
35
+ -webkit-font-smoothing: antialiased;
36
+ box-sizing: content-box !important;
37
+ }
38
+ input:focus,
39
+ input:hover {
40
+ --space: 30px;
41
+ background: var(--spectrum-global-color-gray-200);
42
+ z-index: 1;
43
+ }
44
+
45
+ /* Hide built-in arrows */
46
+ input.hide-arrows::-webkit-outer-spin-button,
47
+ input.hide-arrows::-webkit-inner-spin-button {
48
+ -webkit-appearance: none;
49
+ margin: 0;
50
+ }
51
+ input.hide-arrows {
52
+ -moz-appearance: textfield;
53
+ }
54
+ </style>
@@ -0,0 +1,59 @@
1
+ <script>
2
+ import { cleanInput } from "./utils"
3
+ import dayjs from "dayjs"
4
+ import NumberInput from "./NumberInput.svelte"
5
+ import { createEventDispatcher } from "svelte"
6
+
7
+ export let value
8
+
9
+ const dispatch = createEventDispatcher()
10
+
11
+ $: displayValue = value || dayjs()
12
+
13
+ const handleHourChange = e => {
14
+ dispatch("change", displayValue.hour(parseInt(e.target.value)))
15
+ }
16
+
17
+ const handleMinuteChange = e => {
18
+ dispatch("change", displayValue.minute(parseInt(e.target.value)))
19
+ }
20
+
21
+ const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
22
+ const cleanMinute = cleanInput({ max: 59, pad: 2, fallback: "00" })
23
+ </script>
24
+
25
+ <div class="time-picker">
26
+ <NumberInput
27
+ hideArrows
28
+ value={displayValue.hour().toString().padStart(2, "0")}
29
+ min={0}
30
+ max={23}
31
+ width={20}
32
+ on:input={cleanHour}
33
+ on:change={handleHourChange}
34
+ />
35
+ <span>:</span>
36
+ <NumberInput
37
+ hideArrows
38
+ value={displayValue.minute().toString().padStart(2, "0")}
39
+ min={0}
40
+ max={59}
41
+ width={20}
42
+ on:input={cleanMinute}
43
+ on:change={handleMinuteChange}
44
+ />
45
+ </div>
46
+
47
+ <style>
48
+ .time-picker {
49
+ display: flex;
50
+ flex-direction: row;
51
+ align-items: center;
52
+ }
53
+ .time-picker span {
54
+ font-weight: bold;
55
+ font-size: 18px;
56
+ z-index: 0;
57
+ margin-bottom: 1px;
58
+ }
59
+ </style>
@@ -0,0 +1,14 @@
1
+ export const cleanInput = ({ max, pad, fallback }) => {
2
+ return e => {
3
+ if (e.target.value) {
4
+ const value = parseInt(e.target.value)
5
+ if (isNaN(value)) {
6
+ e.target.value = fallback
7
+ } else {
8
+ e.target.value = Math.min(max, value).toString().padStart(pad, "0")
9
+ }
10
+ } else {
11
+ e.target.value = fallback
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,69 @@
1
+ <script>
2
+ import CoreDatePicker from "./DatePicker/DatePicker.svelte"
3
+ import Icon from "../../Icon/Icon.svelte"
4
+
5
+ export let value = null
6
+ export let disabled = false
7
+ export let readonly = false
8
+ export let error = null
9
+ export let appendTo = undefined
10
+ export let ignoreTimezones = false
11
+
12
+ let fromDate
13
+ let toDate
14
+ </script>
15
+
16
+ <div class="date-range">
17
+ <CoreDatePicker
18
+ value={fromDate}
19
+ on:change={e => (fromDate = e.detail)}
20
+ enableTime={false}
21
+ />
22
+ <div class="arrow">
23
+ <Icon name="ChevronRight" />
24
+ </div>
25
+ <CoreDatePicker
26
+ value={toDate}
27
+ on:change={e => (toDate = e.detail)}
28
+ enableTime={false}
29
+ />
30
+ </div>
31
+
32
+ <style>
33
+ .date-range {
34
+ display: flex;
35
+ flex-direction: row;
36
+ border: 1px solid var(--spectrum-alias-border-color);
37
+ border-radius: 4px;
38
+ }
39
+ .date-range :global(.spectrum-InputGroup),
40
+ .date-range :global(.spectrum-Textfield),
41
+ .date-range :global(input) {
42
+ min-width: 0 !important;
43
+ width: 150px !important;
44
+ }
45
+ .date-range :global(input) {
46
+ border: none;
47
+ text-align: center;
48
+ }
49
+ .date-range :global(button) {
50
+ display: none;
51
+ }
52
+ .date-range :global(> :first-child input),
53
+ .date-range :global(> :first-child) {
54
+ border-top-right-radius: 0;
55
+ border-bottom-right-radius: 0;
56
+ }
57
+ .date-range :global(> :last-child input),
58
+ .date-range :global(> :last-child) {
59
+ border-top-left-radius: 0;
60
+ border-bottom-left-radius: 0;
61
+ }
62
+ .arrow {
63
+ position: absolute;
64
+ top: 50%;
65
+ left: 50%;
66
+ transform: translateX(-50%) translateY(-50%);
67
+ z-index: 1;
68
+ }
69
+ </style>
@@ -155,6 +155,7 @@
155
155
  useAnchorWidth={!autoWidth}
156
156
  maxWidth={autoWidth ? 400 : null}
157
157
  customHeight={customPopoverHeight}
158
+ maxHeight={240}
158
159
  >
159
160
  <div
160
161
  class="popover-content"
@@ -8,7 +8,9 @@ export { default as CoreTextArea } from "./TextArea.svelte"
8
8
  export { default as CoreCombobox } from "./Combobox.svelte"
9
9
  export { default as CoreSwitch } from "./Switch.svelte"
10
10
  export { default as CoreSearch } from "./Search.svelte"
11
- export { default as CoreDatePicker } from "./DatePicker.svelte"
11
+ export { default as CoreDatePicker } from "./DatePicker/DatePicker.svelte"
12
+ export { default as CoreDatePickerPopoverContents } from "./DatePicker/DatePickerPopoverContents.svelte"
13
+ export { default as CoreDateRangePicker } from "./DateRangePicker.svelte"
12
14
  export { default as CoreDropzone } from "./Dropzone.svelte"
13
15
  export { default as CoreStepper } from "./Stepper.svelte"
14
16
  export { default as CoreRichTextField } from "./RichTextField.svelte"
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import Field from "./Field.svelte"
3
- import DatePicker from "./Core/DatePicker.svelte"
3
+ import DatePicker from "./Core/DatePicker/DatePicker.svelte"
4
4
  import { createEventDispatcher } from "svelte"
5
5
 
6
6
  export let value = null
@@ -11,22 +11,15 @@
11
11
  export let error = null
12
12
  export let enableTime = true
13
13
  export let timeOnly = false
14
- export let time24hr = false
15
14
  export let placeholder = null
16
15
  export let appendTo = undefined
17
16
  export let ignoreTimezones = false
18
- export let range = false
19
17
  export let helpText = null
18
+
20
19
  const dispatch = createEventDispatcher()
21
20
 
22
21
  const onChange = e => {
23
- if (range) {
24
- // Flatpickr cant take two dates and work out what to display, needs to be provided a string.
25
- // Like - "Date1 to Date2". Hence passing in that specifically from the array
26
- value = e?.detail[1]
27
- } else {
28
- value = e.detail
29
- }
22
+ value = e.detail
30
23
  dispatch("change", e.detail)
31
24
  }
32
25
  </script>
@@ -40,10 +33,8 @@
40
33
  {placeholder}
41
34
  {enableTime}
42
35
  {timeOnly}
43
- {time24hr}
44
36
  {appendTo}
45
37
  {ignoreTimezones}
46
- {range}
47
38
  on:change={onChange}
48
39
  />
49
40
  </Field>
@@ -0,0 +1,34 @@
1
+ <script>
2
+ import Field from "./Field.svelte"
3
+ import DateRangePicker from "./Core/DateRangePicker.svelte"
4
+ import { createEventDispatcher } from "svelte"
5
+
6
+ export let value = null
7
+ export let label = null
8
+ export let labelPosition = "above"
9
+ export let disabled = false
10
+ export let readonly = false
11
+ export let error = null
12
+ export let helpText = null
13
+ export let appendTo = undefined
14
+ export let ignoreTimezones = false
15
+
16
+ const dispatch = createEventDispatcher()
17
+
18
+ const onChange = e => {
19
+ value = e.detail
20
+ dispatch("change", e.detail)
21
+ }
22
+ </script>
23
+
24
+ <Field {helpText} {label} {labelPosition} {error}>
25
+ <DateRangePicker
26
+ {error}
27
+ {disabled}
28
+ {readonly}
29
+ {value}
30
+ {appendTo}
31
+ {ignoreTimezones}
32
+ on:change={onChange}
33
+ />
34
+ </Field>
@@ -7,11 +7,11 @@
7
7
  export let narrower = false
8
8
  export let noPadding = false
9
9
 
10
- let sidePanelVisble = false
10
+ let sidePanelVisible = false
11
11
 
12
12
  setContext("side-panel", {
13
- open: () => (sidePanelVisble = true),
14
- close: () => (sidePanelVisble = false),
13
+ open: () => (sidePanelVisible = true),
14
+ close: () => (sidePanelVisible = false),
15
15
  })
16
16
  </script>
17
17
 
@@ -24,9 +24,9 @@
24
24
  </div>
25
25
  <div
26
26
  id="side-panel"
27
- class:visible={sidePanelVisble}
27
+ class:visible={sidePanelVisible}
28
28
  use:clickOutside={() => {
29
- sidePanelVisble = false
29
+ sidePanelVisible = false
30
30
  }}
31
31
  >
32
32
  <slot name="side-panel" />
@@ -18,13 +18,15 @@
18
18
  export let open = false
19
19
  export let useAnchorWidth = false
20
20
  export let dismissible = true
21
- export let offset = 5
21
+ export let offset = 4
22
22
  export let customHeight
23
23
  export let animate = true
24
24
  export let customZindex
25
25
  export let handlePostionUpdate
26
26
  export let showPopover = true
27
27
  export let clickOutsideOverride = false
28
+ export let resizable = true
29
+ export let wrap = false
28
30
 
29
31
  $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
30
32
 
@@ -91,6 +93,8 @@
91
93
  useAnchorWidth,
92
94
  offset,
93
95
  customUpdate: handlePostionUpdate,
96
+ resizable,
97
+ wrap,
94
98
  }}
95
99
  use:clickOutside={{
96
100
  callback: dismissible ? handleOutsideClick : () => {},
@@ -116,12 +120,11 @@
116
120
  min-width: var(--spectrum-global-dimension-size-2000);
117
121
  border-color: var(--spectrum-global-color-gray-300);
118
122
  overflow: auto;
119
- transition: opacity 260ms ease-out, transform 260ms ease-out;
123
+ transition: opacity 260ms ease-out;
120
124
  }
121
125
  .hidden {
122
126
  opacity: 0;
123
127
  pointer-events: none;
124
- transform: translateY(-20px);
125
128
  }
126
129
  .customZindex {
127
130
  z-index: var(--customZindex) !important;
package/src/helpers.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { helpers } from "@budibase/shared-core"
2
+ import dayjs from "dayjs"
2
3
 
3
4
  export const deepGet = helpers.deepGet
4
5
 
@@ -115,3 +116,110 @@ export const copyToClipboard = value => {
115
116
  }
116
117
  })
117
118
  }
119
+
120
+ // Parsed a date value. This is usually an ISO string, but can be a
121
+ // bunch of different formats and shapes depending on schema flags.
122
+ export const parseDate = (value, { enableTime = true }) => {
123
+ // If empty then invalid
124
+ if (!value) {
125
+ return null
126
+ }
127
+
128
+ // Certain string values need transformed
129
+ if (typeof value === "string") {
130
+ // Check for time only values
131
+ if (!isNaN(new Date(`0-${value}`))) {
132
+ value = `0-${value}`
133
+ }
134
+
135
+ // If date only, check for cases where we received a UTC string
136
+ else if (!enableTime && value.endsWith("Z")) {
137
+ value = value.split("Z")[0]
138
+ }
139
+ }
140
+
141
+ // Parse value and check for validity
142
+ const parsedDate = dayjs(value)
143
+ if (!parsedDate.isValid()) {
144
+ return null
145
+ }
146
+
147
+ // By rounding to the nearest second we avoid locking up in an endless
148
+ // loop in the builder, caused by potentially enriching {{ now }} to every
149
+ // millisecond.
150
+ return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
151
+ }
152
+
153
+ // Stringifies a dayjs object to create an ISO string that respects the various
154
+ // schema flags
155
+ export const stringifyDate = (
156
+ value,
157
+ { enableTime = true, timeOnly = false, ignoreTimezones = false } = {}
158
+ ) => {
159
+ if (!value) {
160
+ return null
161
+ }
162
+
163
+ // Time only fields always ignore timezones, otherwise they make no sense.
164
+ // For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
165
+ // time picked, without timezone
166
+ const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
167
+ if (offsetForTimezone) {
168
+ // Ensure we use the correct offset for the date
169
+ const referenceDate = timeOnly ? new Date() : value.toDate()
170
+ const offset = referenceDate.getTimezoneOffset() * 60000
171
+ return new Date(value.valueOf() - offset).toISOString().slice(0, -1)
172
+ }
173
+
174
+ // For date-only fields, construct a manual timestamp string without a time
175
+ // or time zone
176
+ else if (!enableTime) {
177
+ const year = value.year()
178
+ const month = `${value.month() + 1}`.padStart(2, "0")
179
+ const day = `${value.date()}`.padStart(2, "0")
180
+ return `${year}-${month}-${day}T00:00:00.000`
181
+ }
182
+
183
+ // Otherwise use a normal ISO string with time and timezone
184
+ else {
185
+ return value.toISOString()
186
+ }
187
+ }
188
+
189
+ // Determine the dayjs-compatible format of the browser's default locale
190
+ const getPatternForPart = part => {
191
+ switch (part.type) {
192
+ case "day":
193
+ return "D".repeat(part.value.length)
194
+ case "month":
195
+ return "M".repeat(part.value.length)
196
+ case "year":
197
+ return "Y".repeat(part.value.length)
198
+ case "literal":
199
+ return part.value
200
+ default:
201
+ console.log("Unsupported date part", part)
202
+ return ""
203
+ }
204
+ }
205
+ const localeDateFormat = new Intl.DateTimeFormat()
206
+ .formatToParts(new Date("2021-01-01"))
207
+ .map(getPatternForPart)
208
+ .join("")
209
+
210
+ // Formats a dayjs date according to schema flags
211
+ export const getDateDisplayValue = (
212
+ value,
213
+ { enableTime = true, timeOnly = false } = {}
214
+ ) => {
215
+ if (!value?.isValid()) {
216
+ return ""
217
+ }
218
+ if (timeOnly) {
219
+ return value.format("HH:mm")
220
+ } else if (!enableTime) {
221
+ return value.format(localeDateFormat)
222
+ } else {
223
+ return value.format(`${localeDateFormat} HH:mm`)
224
+ }
225
+ }