@budibase/bbui 3.25.4 → 3.26.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/bbui",
3
3
  "description": "A UI solution used in the different Budibase projects.",
4
- "version": "3.25.4",
4
+ "version": "3.26.1",
5
5
  "license": "MPL-2.0",
6
6
  "module": "dist/bbui.mjs",
7
7
  "exports": {
@@ -107,5 +107,5 @@
107
107
  }
108
108
  }
109
109
  },
110
- "gitHead": "4a51aa9f4675ffa6555788aeaf6c31b522a952b3"
110
+ "gitHead": "3ee1bf769467a216a2e44443e01810cb1b076dfc"
111
111
  }
@@ -27,6 +27,7 @@
27
27
  export let fieldText: string = ""
28
28
  export let fieldIcon: PickerIconInput = undefined
29
29
  export let fieldColour: string = ""
30
+ export let fieldSubtitle: string | null = null
30
31
  export let isPlaceholder: boolean = false
31
32
  export let placeholderOption: string | undefined | boolean = undefined
32
33
  export let options: O[] = []
@@ -250,8 +251,12 @@
250
251
  class="spectrum-Picker-label"
251
252
  class:is-placeholder={isPlaceholder}
252
253
  class:auto-width={autoWidth}
254
+ class:has-subtitle={!!fieldSubtitle}
253
255
  >
254
- {fieldText}
256
+ <span class="picker-label-text">{fieldText}</span>
257
+ {#if fieldSubtitle}
258
+ <span class="picker-label-subtitle">{fieldSubtitle}</span>
259
+ {/if}
255
260
  </span>
256
261
  {#if !hideChevron}
257
262
  <Icon name="caret-down" size="S" />
@@ -534,6 +539,26 @@
534
539
  display: block;
535
540
  margin-top: var(--spacing-s);
536
541
  }
542
+ .spectrum-Picker-label.has-subtitle {
543
+ display: flex;
544
+ flex-direction: column;
545
+ align-items: flex-start;
546
+ gap: var(--spacing-xs);
547
+ white-space: normal;
548
+ overflow: visible;
549
+ height: auto;
550
+ line-height: normal;
551
+ }
552
+ .spectrum-Picker-label.has-subtitle .picker-label-text {
553
+ font-size: 12px;
554
+ line-height: 15px;
555
+ }
556
+ .picker-label-subtitle {
557
+ font-size: 12px;
558
+ line-height: 15px;
559
+ color: var(--spectrum-global-color-gray-600);
560
+ font-weight: 500;
561
+ }
537
562
 
538
563
  .select-all-item {
539
564
  border-bottom: 1px solid var(--spectrum-global-color-gray-200);
@@ -11,6 +11,8 @@
11
11
  export let getOptionLabel = option => option
12
12
  export let getOptionValue = option => option
13
13
  export let getOptionTitle = option => option
14
+ export let getOptionSubtitle = option => option?.subtitle ?? undefined
15
+ export let getOptionDisabled = option => option?.disabled ?? false
14
16
  export let sort = false
15
17
 
16
18
  const dispatch = createEventDispatcher()
@@ -36,6 +38,7 @@
36
38
  <div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
37
39
  {#if parsedOptions && Array.isArray(parsedOptions)}
38
40
  {#each parsedOptions as option}
41
+ {@const isOptionDisabled = disabled || getOptionDisabled(option)}
39
42
  <div
40
43
  title={getOptionTitle(option)}
41
44
  class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
@@ -47,11 +50,16 @@
47
50
  value={getOptionValue(option)}
48
51
  type="radio"
49
52
  class="spectrum-Radio-input"
50
- {disabled}
53
+ disabled={isOptionDisabled}
51
54
  />
52
55
  <span class="spectrum-Radio-button"></span>
53
- <label for="" class="spectrum-Radio-label">
54
- {getOptionLabel(option)}
56
+ <label for="" class="spectrum-Radio-label radio-label">
57
+ <span class="radio-label-text">{getOptionLabel(option)}</span>
58
+ {#if getOptionSubtitle(option)}
59
+ <span class="radio-label-subtitle">
60
+ {getOptionSubtitle(option)}
61
+ </span>
62
+ {/if}
55
63
  </label>
56
64
  </div>
57
65
  {/each}
@@ -65,4 +73,8 @@
65
73
  .readonly {
66
74
  pointer-events: none;
67
75
  }
76
+ .radio-label-subtitle {
77
+ font-size: 12px;
78
+ color: var(--spectrum-global-color-gray-600);
79
+ }
68
80
  </style>
@@ -22,6 +22,7 @@
22
22
  option?.colour ?? undefined
23
23
  export let getOptionSubtitle = (option: O, _index?: number) =>
24
24
  option?.subtitle ?? undefined
25
+ export let showSelectedSubtitle: boolean = false
25
26
  export let compare = (option: O, value: V) => option === value
26
27
  export let useOptionIconImage = false
27
28
  export let isOptionEnabled = (option: O, _index?: number) =>
@@ -49,6 +50,9 @@
49
50
  const dispatch = createEventDispatcher()
50
51
 
51
52
  $: fieldText = getFieldText(value, options, placeholder)
53
+ $: fieldSubtitle = showSelectedSubtitle
54
+ ? getFieldAttribute(getOptionSubtitle, value, options)
55
+ : null
52
56
  $: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
53
57
  $: fieldColour = getFieldAttribute(getOptionColour, value, options)
54
58
 
@@ -105,6 +109,7 @@
105
109
  {disabled}
106
110
  {readonly}
107
111
  {fieldText}
112
+ {fieldSubtitle}
108
113
  {fieldIcon}
109
114
  {fieldColour}
110
115
  {options}
@@ -0,0 +1,251 @@
1
+ <script lang="ts">
2
+ import "@spectrum-css/textfield/dist/index-vars.css"
3
+ import Field from "./Field.svelte"
4
+ import Tag from "../Tags/Tag.svelte"
5
+ import type { LabelPosition } from "../types"
6
+ import { createEventDispatcher } from "svelte"
7
+
8
+ export let value: string[] = []
9
+ export let label: string | undefined = undefined
10
+ export let labelPosition: LabelPosition = "above"
11
+ export let error: string | undefined = undefined
12
+ export let helpText: string | undefined = undefined
13
+ export let placeholder: string | undefined = undefined
14
+ export let disabled: boolean = false
15
+ export let readonly: boolean = false
16
+ export let id: string | undefined = undefined
17
+ export let delimiter = ","
18
+ export let splitOnSpace = false
19
+ export let allowDuplicates = false
20
+ export let maxItems: number | undefined = undefined
21
+
22
+ let inputValue = ""
23
+ let focused = false
24
+ let inputEl: HTMLInputElement | null = null
25
+
26
+ const dispatch = createEventDispatcher()
27
+
28
+ const updateValue = (next: string[]) => {
29
+ value = next
30
+ dispatch("change", next)
31
+ }
32
+
33
+ const notifyMax = () => {
34
+ if (maxItems != null) {
35
+ dispatch("max", maxItems)
36
+ }
37
+ }
38
+
39
+ const addTokens = (tokens: string[]) => {
40
+ const cleaned = tokens.map(token => token.trim()).filter(Boolean)
41
+ if (!cleaned.length) {
42
+ return
43
+ }
44
+
45
+ if (maxItems != null && value.length >= maxItems) {
46
+ notifyMax()
47
+ return
48
+ }
49
+
50
+ const available =
51
+ maxItems != null ? Math.max(maxItems - value.length, 0) : cleaned.length
52
+ if (available === 0) {
53
+ notifyMax()
54
+ return
55
+ }
56
+
57
+ const next = cleaned.slice(0, available).reduce(
58
+ (acc, token) => {
59
+ if (allowDuplicates || !acc.includes(token)) {
60
+ acc.push(token)
61
+ }
62
+ return acc
63
+ },
64
+ [...value]
65
+ )
66
+
67
+ updateValue(next)
68
+
69
+ if (maxItems != null && cleaned.length > available) {
70
+ notifyMax()
71
+ }
72
+ }
73
+
74
+ const removeToken = (index: number) => {
75
+ updateValue(value.filter((_, idx) => idx !== index))
76
+ }
77
+
78
+ const escapeRegExp = (value: string) =>
79
+ value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
80
+
81
+ const getSplitPattern = () => {
82
+ const parts = [escapeRegExp(delimiter)]
83
+ if (splitOnSpace) {
84
+ parts.push("\\s")
85
+ }
86
+ return new RegExp(`(?:${parts.join("|")})+`)
87
+ }
88
+
89
+ const shouldSplit = (value: string) => {
90
+ const pattern = getSplitPattern()
91
+ return pattern.test(value)
92
+ }
93
+
94
+ const handleInput = (event: Event) => {
95
+ if (readonly || disabled) {
96
+ return
97
+ }
98
+ const target = event.target as HTMLInputElement
99
+ inputValue = target.value
100
+ if (shouldSplit(inputValue)) {
101
+ const pattern = getSplitPattern()
102
+ const endsWithSeparator = new RegExp(`${pattern.source}$`).test(
103
+ inputValue
104
+ )
105
+ const hasDelimiterSpace = new RegExp(
106
+ `${escapeRegExp(delimiter)}\\s+`
107
+ ).test(inputValue)
108
+ const parts = inputValue.split(pattern).filter(Boolean)
109
+ const shouldCommitAll =
110
+ hasDelimiterSpace && parts.length > 1 && !endsWithSeparator
111
+ const trailing =
112
+ endsWithSeparator || shouldCommitAll ? "" : (parts.pop() ?? "")
113
+ addTokens(parts)
114
+ inputValue = trailing
115
+ }
116
+ }
117
+
118
+ const handleBlur = () => {
119
+ if (inputValue.trim()) {
120
+ addTokens([inputValue])
121
+ inputValue = ""
122
+ }
123
+ dispatch("blur", value)
124
+ }
125
+
126
+ const handleKeydown = (event: KeyboardEvent) => {
127
+ if (readonly || disabled) {
128
+ return
129
+ }
130
+ if (
131
+ event.key === delimiter ||
132
+ (splitOnSpace && event.key === " " && !event.shiftKey)
133
+ ) {
134
+ event.preventDefault()
135
+ addTokens([inputValue])
136
+ inputValue = ""
137
+ }
138
+ if (event.key === "Backspace" && !inputValue && value.length) {
139
+ removeToken(value.length - 1)
140
+ }
141
+ }
142
+ </script>
143
+
144
+ <Field {helpText} {label} {labelPosition} {error}>
145
+ <div
146
+ class="pill-input spectrum-Textfield"
147
+ class:is-disabled={disabled}
148
+ class:is-focused={focused}
149
+ class:is-invalid={!!error}
150
+ on:click={() => {
151
+ if (!disabled && !readonly) {
152
+ inputEl?.focus()
153
+ }
154
+ }}
155
+ >
156
+ <div class="pill-list">
157
+ {#each value as pill, index (pill + index)}
158
+ <Tag closable emphasized on:remove={() => removeToken(index)}>
159
+ {pill}
160
+ </Tag>
161
+ {/each}
162
+ </div>
163
+ <input
164
+ class="spectrum-Textfield-input pill-input-field"
165
+ bind:value={inputValue}
166
+ bind:this={inputEl}
167
+ {id}
168
+ {disabled}
169
+ {readonly}
170
+ placeholder={placeholder ?? ""}
171
+ on:input={handleInput}
172
+ on:keydown={handleKeydown}
173
+ on:focus={() => (focused = true)}
174
+ on:blur={() => {
175
+ focused = false
176
+ handleBlur()
177
+ }}
178
+ />
179
+ </div>
180
+ </Field>
181
+
182
+ <style>
183
+ .pill-input {
184
+ width: 100%;
185
+ min-width: 0;
186
+ min-height: var(--spectrum-textfield-height);
187
+ padding: var(--spectrum-textfield-padding-top)
188
+ var(--spectrum-textfield-padding-right)
189
+ var(--spectrum-textfield-padding-bottom)
190
+ calc(var(--spectrum-textfield-padding-left) - 1px);
191
+ border: var(--spectrum-textfield-border-size) solid
192
+ var(
193
+ --spectrum-textfield-m-border-color,
194
+ var(--spectrum-alias-border-color)
195
+ );
196
+ border-radius: var(--spectrum-textfield-border-radius);
197
+ background-color: var(
198
+ --spectrum-textfield-m-background-color,
199
+ var(--spectrum-global-color-gray-50)
200
+ );
201
+ display: flex;
202
+ box-sizing: border-box;
203
+ align-items: center;
204
+ gap: var(--spacing-xs);
205
+ flex-wrap: wrap;
206
+ }
207
+ .pill-input.is-focused {
208
+ border-color: var(
209
+ --spectrum-textfield-m-border-color-down,
210
+ var(--spectrum-alias-border-color-mouse-focus)
211
+ );
212
+ }
213
+ .pill-input.is-invalid {
214
+ border-color: var(
215
+ --spectrum-textfield-m-border-color-error,
216
+ var(--spectrum-semantic-negative-color-default)
217
+ );
218
+ }
219
+ .pill-input.is-disabled {
220
+ background-color: var(
221
+ --spectrum-textfield-m-background-color-disabled,
222
+ var(--spectrum-global-color-gray-200)
223
+ );
224
+ border-color: var(
225
+ --spectrum-textfield-m-border-color-disabled,
226
+ var(--spectrum-global-color-gray-400)
227
+ );
228
+ }
229
+ .pill-input-field {
230
+ min-width: 120px;
231
+ flex: 1 1 120px;
232
+ padding: 0;
233
+ height: auto;
234
+ line-height: 20px;
235
+ border: none;
236
+ background: transparent;
237
+ outline: none;
238
+ }
239
+ .pill-input :global(.spectrum-Tags-item) {
240
+ margin: 0;
241
+ --spectrum-tag-height: var(--spectrum-global-dimension-size-300);
242
+ }
243
+ .pill-input :global(.spectrum-Tags-itemLabel) {
244
+ line-height: 1;
245
+ display: inline-flex;
246
+ align-items: center;
247
+ }
248
+ .pill-list {
249
+ display: contents;
250
+ }
251
+ </style>
@@ -13,6 +13,8 @@
13
13
  export let getOptionLabel = option => extractProperty(option, "label")
14
14
  export let getOptionValue = option => extractProperty(option, "value")
15
15
  export let getOptionTitle = option => extractProperty(option, "label")
16
+ export let getOptionSubtitle = option => extractProperty(option, "subtitle")
17
+ export let getOptionDisabled = option => extractProperty(option, "disabled")
16
18
  export let helpText = undefined
17
19
 
18
20
  const dispatch = createEventDispatcher()
@@ -38,6 +40,8 @@
38
40
  {getOptionLabel}
39
41
  {getOptionValue}
40
42
  {getOptionTitle}
43
+ {getOptionSubtitle}
44
+ {getOptionDisabled}
41
45
  on:change={onChange}
42
46
  />
43
47
  </Field>
@@ -19,6 +19,7 @@
19
19
  extractProperty(option, "value")
20
20
  export let getOptionSubtitle = (option: O, _index?: number) =>
21
21
  (option as any)?.subtitle
22
+ export let showSelectedSubtitle = false
22
23
  export let getOptionIcon = (option: O, _index?: number) =>
23
24
  (option as any)?.icon
24
25
  export let getOptionColour = (option: O, _index?: number) =>
@@ -81,6 +82,7 @@
81
82
  {getOptionIcon}
82
83
  {getOptionColour}
83
84
  {getOptionSubtitle}
85
+ {showSelectedSubtitle}
84
86
  {useOptionIconImage}
85
87
  {isOptionEnabled}
86
88
  {autocomplete}
@@ -30,6 +30,7 @@
30
30
  undefined
31
31
  export let secondaryButtonWarning: boolean = false
32
32
  export let custom: boolean = false
33
+ export let disableCancelOnConfirm: boolean = false
33
34
 
34
35
  const { hide, cancel } = getContext(Context.Modal)
35
36
 
@@ -120,7 +121,11 @@
120
121
  {/if}
121
122
 
122
123
  {#if showCancelButton}
123
- <Button secondary on:click={close}>
124
+ <Button
125
+ secondary
126
+ on:click={close}
127
+ disabled={loading && disableCancelOnConfirm}
128
+ >
124
129
  {cancelText}
125
130
  </Button>
126
131
  {/if}
@@ -47,6 +47,7 @@
47
47
  export let snippets: any[] = []
48
48
  export let defaultSortColumn: string | undefined = undefined
49
49
  export let defaultSortOrder: "Ascending" | "Descending" = "Ascending"
50
+ export let stickyHeader: boolean = true
50
51
 
51
52
  const dispatch = createEventDispatcher()
52
53
 
@@ -286,15 +287,17 @@
286
287
  const toggleSelectAll = (e: CustomEvent): void => {
287
288
  const select = !!e.detail
288
289
  if (select) {
290
+ const next = [...selectedRows]
289
291
  // Add any rows which are not already in selected rows
290
292
  rows.forEach(row => {
291
293
  if (
292
294
  row.__selectable !== false &&
293
- selectedRows.findIndex(x => x._id === row._id) === -1
295
+ next.findIndex(x => x._id === row._id) === -1
294
296
  ) {
295
- selectedRows.push(row)
297
+ next.push(row)
296
298
  }
297
299
  })
300
+ selectedRows = next
298
301
  } else {
299
302
  // Remove any rows from selected rows that are in the current data set
300
303
  selectedRows = selectedRows.filter(el =>
@@ -366,6 +369,7 @@
366
369
  class="wrapper"
367
370
  class:wrapper--quiet={quiet}
368
371
  class:wrapper--compact={compact}
372
+ class:wrapper--sticky-header={stickyHeader}
369
373
  style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
370
374
  >
371
375
  {#if loading}
@@ -577,8 +581,7 @@
577
581
  }
578
582
  .spectrum-Table-headCell {
579
583
  height: var(--header-height);
580
- position: sticky;
581
- top: 0;
584
+ position: relative;
582
585
  text-overflow: ellipsis;
583
586
  white-space: nowrap;
584
587
  background-color: var(--spectrum-alias-background-color-secondary);
@@ -617,13 +620,20 @@
617
620
  justify-content: flex-end;
618
621
  }
619
622
  .spectrum-Table-headCell--edit {
620
- position: sticky;
621
- left: 0;
623
+ position: relative;
622
624
  z-index: 3;
623
625
  justify-content: center;
624
626
  padding-left: calc(var(--cell-padding) / 1.33);
625
627
  padding-right: calc(var(--cell-padding) / 1.33);
626
628
  }
629
+ .wrapper--sticky-header .spectrum-Table-headCell {
630
+ position: sticky;
631
+ top: 0;
632
+ }
633
+ .wrapper--sticky-header .spectrum-Table-headCell--edit {
634
+ position: sticky;
635
+ left: 0;
636
+ }
627
637
  .spectrum-Table-headCell .title {
628
638
  overflow: visible;
629
639
  text-overflow: ellipsis;
@@ -3,6 +3,7 @@
3
3
  import Avatar from "../Avatar/Avatar.svelte"
4
4
  import ClearButton from "../ClearButton/ClearButton.svelte"
5
5
  import Icon from "../Icon/Icon.svelte"
6
+ import { createEventDispatcher } from "svelte"
6
7
 
7
8
  export let icon: string = ""
8
9
  export let avatar: string = ""
@@ -10,6 +11,13 @@
10
11
  export let disabled: boolean = false
11
12
  export let closable: boolean = false
12
13
  export let emphasized: boolean = false
14
+
15
+ const dispatch = createEventDispatcher()
16
+
17
+ const onRemove = (event: MouseEvent) => {
18
+ event.stopPropagation()
19
+ dispatch("remove")
20
+ }
13
21
  </script>
14
22
 
15
23
  <div
@@ -27,7 +35,7 @@
27
35
  {/if}
28
36
  <span class="spectrum-Tags-itemLabel"><slot /></span>
29
37
  {#if closable}
30
- <ClearButton on:click />
38
+ <ClearButton on:click={onRemove} />
31
39
  {/if}
32
40
  </div>
33
41
 
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export { default as CollapsibleSearch } from "./Form/CollapsibleSearch.svelte"
16
16
  export { default as Input } from "./Form/Input.svelte"
17
17
  export { default as InputDropdown } from "./Form/InputDropdown.svelte"
18
18
  export { default as Multiselect } from "./Form/Multiselect.svelte"
19
+ export { default as PillInput } from "./Form/PillInput.svelte"
19
20
  export { default as RadioGroup } from "./Form/RadioGroup.svelte"
20
21
  export { default as RichTextField } from "./Form/RichTextField.svelte"
21
22
  export { default as Search } from "./Form/Search.svelte"