@budibase/bbui 2.26.3 → 2.27.2

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": "2.26.3",
4
+ "version": "2.27.2",
5
5
  "license": "MPL-2.0",
6
6
  "svelte": "src/index.js",
7
7
  "module": "dist/bbui.es.js",
@@ -35,8 +35,8 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@adobe/spectrum-css-workflow-icons": "1.2.1",
38
- "@budibase/shared-core": "2.26.3",
39
- "@budibase/string-templates": "2.26.3",
38
+ "@budibase/shared-core": "2.27.2",
39
+ "@budibase/string-templates": "2.27.2",
40
40
  "@spectrum-css/accordion": "3.0.24",
41
41
  "@spectrum-css/actionbutton": "1.0.1",
42
42
  "@spectrum-css/actiongroup": "1.0.1",
@@ -103,5 +103,5 @@
103
103
  }
104
104
  }
105
105
  },
106
- "gitHead": "4be0c2e7f1698fe76235b87109a8c2e4a56c5470"
106
+ "gitHead": "9e50a9ac2616fc848aa9132965452bedf88a07e3"
107
107
  }
@@ -71,8 +71,8 @@ const handleMouseDown = e => {
71
71
 
72
72
  // Clear any previous listeners in case of multiple down events, and register
73
73
  // a single mouse up listener
74
- document.removeEventListener("mouseup", handleMouseUp)
75
- document.addEventListener("mouseup", handleMouseUp, true)
74
+ document.removeEventListener("click", handleMouseUp)
75
+ document.addEventListener("click", handleMouseUp, true)
76
76
  }
77
77
 
78
78
  // Global singleton listeners for our events
@@ -155,6 +155,8 @@ export default function positionDropdown(element, opts) {
155
155
  applyXStrategy(Strategies.StartToEnd)
156
156
  } else if (align === "left-outside") {
157
157
  applyXStrategy(Strategies.EndToStart)
158
+ } else if (align === "center") {
159
+ applyXStrategy(Strategies.MidPoint)
158
160
  } else {
159
161
  applyXStrategy(Strategies.StartToStart)
160
162
  }
@@ -4,13 +4,14 @@
4
4
  export let max
5
5
  export let hideArrows = false
6
6
  export let width
7
+ export let type = "number"
7
8
 
8
9
  $: style = width ? `width:${width}px;` : ""
9
10
  </script>
10
11
 
11
12
  <input
12
13
  class:hide-arrows={hideArrows}
13
- type="number"
14
+ {type}
14
15
  {style}
15
16
  {value}
16
17
  {min}
@@ -51,4 +52,7 @@
51
52
  input.hide-arrows {
52
53
  -moz-appearance: textfield;
53
54
  }
55
+ input[type="time"]::-webkit-calendar-picker-indicator {
56
+ display: none;
57
+ }
54
58
  </style>
@@ -1,5 +1,4 @@
1
1
  <script>
2
- import { cleanInput } from "./utils"
3
2
  import dayjs from "dayjs"
4
3
  import NumberInput from "./NumberInput.svelte"
5
4
  import { createEventDispatcher } from "svelte"
@@ -8,39 +7,26 @@
8
7
 
9
8
  const dispatch = createEventDispatcher()
10
9
 
11
- $: displayValue = value || dayjs()
10
+ $: displayValue = value?.format("HH:mm")
12
11
 
13
- const handleHourChange = e => {
14
- dispatch("change", displayValue.hour(parseInt(e.target.value)))
15
- }
12
+ const handleChange = e => {
13
+ if (!e.target.value) {
14
+ dispatch("change", undefined)
15
+ return
16
+ }
16
17
 
17
- const handleMinuteChange = e => {
18
- dispatch("change", displayValue.minute(parseInt(e.target.value)))
18
+ const [hour, minute] = e.target.value.split(":").map(x => parseInt(x))
19
+ dispatch("change", (value || dayjs()).hour(hour).minute(minute))
19
20
  }
20
-
21
- const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
22
- const cleanMinute = cleanInput({ max: 59, pad: 2, fallback: "00" })
23
21
  </script>
24
22
 
25
23
  <div class="time-picker">
26
24
  <NumberInput
27
25
  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}
26
+ type={"time"}
27
+ value={displayValue}
28
+ on:input={handleChange}
29
+ on:change={handleChange}
44
30
  />
45
31
  </div>
46
32
 
@@ -50,10 +36,4 @@
50
36
  flex-direction: row;
51
37
  align-items: center;
52
38
  }
53
- .time-picker span {
54
- font-weight: bold;
55
- font-size: 18px;
56
- z-index: 0;
57
- margin-bottom: 1px;
58
- }
59
39
  </style>
@@ -17,6 +17,8 @@
17
17
  export let customPopoverHeight
18
18
  export let open = false
19
19
  export let loading
20
+ export let onOptionMouseenter = () => {}
21
+ export let onOptionMouseleave = () => {}
20
22
 
21
23
  const dispatch = createEventDispatcher()
22
24
 
@@ -97,4 +99,6 @@
97
99
  {autoWidth}
98
100
  {customPopoverHeight}
99
101
  {loading}
102
+ {onOptionMouseenter}
103
+ {onOptionMouseleave}
100
104
  />
@@ -41,6 +41,8 @@
41
41
  export let footer = null
42
42
  export let customAnchor = null
43
43
  export let loading
44
+ export let onOptionMouseenter = () => {}
45
+ export let onOptionMouseleave = () => {}
44
46
 
45
47
  const dispatch = createEventDispatcher()
46
48
 
@@ -199,6 +201,8 @@
199
201
  aria-selected="true"
200
202
  tabindex="0"
201
203
  on:click={() => onSelectOption(getOptionValue(option, idx))}
204
+ on:mouseenter={e => onOptionMouseenter(e, option)}
205
+ on:mouseleave={e => onOptionMouseleave(e, option)}
202
206
  class:is-disabled={!isOptionEnabled(option)}
203
207
  >
204
208
  {#if getOptionIcon(option, idx)}
@@ -26,6 +26,8 @@
26
26
  export let tag = null
27
27
  export let searchTerm = null
28
28
  export let loading
29
+ export let onOptionMouseenter = () => {}
30
+ export let onOptionMouseleave = () => {}
29
31
 
30
32
  const dispatch = createEventDispatcher()
31
33
 
@@ -95,6 +97,8 @@
95
97
  {autocomplete}
96
98
  {sort}
97
99
  {tag}
100
+ {onOptionMouseenter}
101
+ {onOptionMouseleave}
98
102
  isPlaceholder={value == null || value === ""}
99
103
  placeholderOption={placeholder === false ? null : placeholder}
100
104
  isOptionSelected={option => compareOptionAndValue(option, value)}
@@ -0,0 +1,267 @@
1
+ <script>
2
+ import { onMount, createEventDispatcher } from "svelte"
3
+ import Atrament from "atrament"
4
+ import Icon from "../../Icon/Icon.svelte"
5
+
6
+ const dispatch = createEventDispatcher()
7
+
8
+ let last
9
+
10
+ export let value
11
+ export let disabled = false
12
+ export let editable = true
13
+ export let width = 400
14
+ export let height = 220
15
+ export let saveIcon = false
16
+ export let darkMode
17
+
18
+ export function toDataUrl() {
19
+ // PNG to preserve transparency
20
+ return canvasRef.toDataURL("image/png")
21
+ }
22
+
23
+ export function toFile() {
24
+ const data = canvasContext
25
+ .getImageData(0, 0, width, height)
26
+ .data.some(channel => channel !== 0)
27
+
28
+ if (!data) {
29
+ return
30
+ }
31
+
32
+ let dataURIParts = toDataUrl().split(",")
33
+ if (!dataURIParts.length) {
34
+ console.error("Could not retrieve signature data")
35
+ }
36
+
37
+ // Pull out the base64 encoded byte data
38
+ let binaryVal = atob(dataURIParts[1])
39
+ let blobArray = new Uint8Array(binaryVal.length)
40
+ let pos = 0
41
+ while (pos < binaryVal.length) {
42
+ blobArray[pos] = binaryVal.charCodeAt(pos)
43
+ pos++
44
+ }
45
+
46
+ const signatureBlob = new Blob([blobArray], {
47
+ type: "image/png",
48
+ })
49
+
50
+ return new File([signatureBlob], "signature.png", {
51
+ type: signatureBlob.type,
52
+ })
53
+ }
54
+
55
+ export function clearCanvas() {
56
+ return canvasContext.clearRect(0, 0, canvasWidth, canvasHeight)
57
+ }
58
+
59
+ let canvasRef
60
+ let canvasContext
61
+ let canvasWrap
62
+ let canvasWidth
63
+ let canvasHeight
64
+ let signature
65
+
66
+ let updated = false
67
+ let signatureFile
68
+ let urlFailed
69
+
70
+ $: if (value) {
71
+ signatureFile = value
72
+ }
73
+
74
+ $: if (signatureFile?.url) {
75
+ updated = false
76
+ }
77
+
78
+ $: if (last) {
79
+ dispatch("update")
80
+ }
81
+
82
+ onMount(() => {
83
+ if (!editable) {
84
+ return
85
+ }
86
+
87
+ const getPos = e => {
88
+ var rect = canvasRef.getBoundingClientRect()
89
+ const canvasX = e.offsetX || e.targetTouches?.[0].pageX - rect.left
90
+ const canvasY = e.offsetY || e.targetTouches?.[0].pageY - rect.top
91
+
92
+ return { x: canvasX, y: canvasY }
93
+ }
94
+
95
+ const checkUp = e => {
96
+ last = getPos(e)
97
+ }
98
+
99
+ canvasRef.addEventListener("pointerdown", e => {
100
+ const current = getPos(e)
101
+ //If the cursor didn't move at all, block the default pointerdown
102
+ if (last?.x === current?.x && last?.y === current?.y) {
103
+ e.preventDefault()
104
+ e.stopImmediatePropagation()
105
+ }
106
+ })
107
+
108
+ document.addEventListener("pointerup", checkUp)
109
+
110
+ signature = new Atrament(canvasRef, {
111
+ width,
112
+ height,
113
+ color: "white",
114
+ })
115
+
116
+ signature.weight = 4
117
+ signature.smoothing = 2
118
+
119
+ canvasWrap.style.width = `${width}px`
120
+ canvasWrap.style.height = `${height}px`
121
+
122
+ const { width: wrapWidth, height: wrapHeight } =
123
+ canvasWrap.getBoundingClientRect()
124
+
125
+ canvasHeight = wrapHeight
126
+ canvasWidth = wrapWidth
127
+
128
+ canvasContext = canvasRef.getContext("2d")
129
+
130
+ return () => {
131
+ signature.destroy()
132
+ document.removeEventListener("pointerup", checkUp)
133
+ }
134
+ })
135
+ </script>
136
+
137
+ <div class="signature" class:light={!darkMode} class:image-error={urlFailed}>
138
+ {#if !disabled}
139
+ <div class="overlay">
140
+ {#if updated && saveIcon}
141
+ <span class="save">
142
+ <Icon
143
+ name="Checkmark"
144
+ hoverable
145
+ tooltip={"Save"}
146
+ tooltipPosition={"top"}
147
+ tooltipType={"info"}
148
+ on:click={() => {
149
+ dispatch("change", toDataUrl())
150
+ }}
151
+ />
152
+ </span>
153
+ {/if}
154
+ {#if signatureFile?.url && !updated}
155
+ <span class="delete">
156
+ <Icon
157
+ name="DeleteOutline"
158
+ hoverable
159
+ tooltip={"Delete"}
160
+ tooltipPosition={"top"}
161
+ tooltipType={"info"}
162
+ on:click={() => {
163
+ if (editable) {
164
+ clearCanvas()
165
+ }
166
+ dispatch("clear")
167
+ }}
168
+ />
169
+ </span>
170
+ {/if}
171
+ </div>
172
+ {/if}
173
+ {#if !editable && signatureFile?.url}
174
+ <!-- svelte-ignore a11y-missing-attribute -->
175
+ {#if !urlFailed}
176
+ <img
177
+ src={signatureFile?.url}
178
+ on:error={() => {
179
+ urlFailed = true
180
+ }}
181
+ />
182
+ {:else}
183
+ Could not load signature
184
+ {/if}
185
+ {:else}
186
+ <div bind:this={canvasWrap} class="canvas-wrap">
187
+ <canvas
188
+ id="signature-canvas"
189
+ bind:this={canvasRef}
190
+ style="--max-sig-width: {width}px; --max-sig-height: {height}px"
191
+ />
192
+ {#if editable}
193
+ <div class="indicator-overlay">
194
+ <div class="sign-here">
195
+ <Icon name="Close" />
196
+ <hr />
197
+ </div>
198
+ </div>
199
+ {/if}
200
+ </div>
201
+ {/if}
202
+ </div>
203
+
204
+ <style>
205
+ .indicator-overlay {
206
+ position: absolute;
207
+ width: 100%;
208
+ height: 100%;
209
+ top: 0px;
210
+ display: flex;
211
+ flex-direction: column;
212
+ justify-content: end;
213
+ padding: var(--spectrum-global-dimension-size-150);
214
+ box-sizing: border-box;
215
+ pointer-events: none;
216
+ }
217
+ .sign-here {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ gap: var(--spectrum-global-dimension-size-150);
222
+ }
223
+ .sign-here hr {
224
+ border: 0;
225
+ border-top: 2px solid var(--spectrum-global-color-gray-200);
226
+ width: 100%;
227
+ }
228
+ .canvas-wrap {
229
+ position: relative;
230
+ margin: auto;
231
+ }
232
+ .signature img {
233
+ max-width: 100%;
234
+ }
235
+ #signature-canvas {
236
+ max-width: var(--max-sig-width);
237
+ max-height: var(--max-sig-height);
238
+ }
239
+ .signature.light img,
240
+ .signature.light #signature-canvas {
241
+ -webkit-filter: invert(100%);
242
+ filter: invert(100%);
243
+ }
244
+ .signature.image-error .overlay {
245
+ padding-top: 0px;
246
+ }
247
+ .signature {
248
+ width: 100%;
249
+ height: 100%;
250
+ position: relative;
251
+ text-align: center;
252
+ }
253
+ .overlay {
254
+ position: absolute;
255
+ width: 100%;
256
+ height: 100%;
257
+ pointer-events: none;
258
+ padding: var(--spectrum-global-dimension-size-150);
259
+ text-align: right;
260
+ z-index: 2;
261
+ box-sizing: border-box;
262
+ }
263
+ .save,
264
+ .delete {
265
+ display: inline-block;
266
+ }
267
+ </style>
@@ -16,3 +16,4 @@ export { default as CoreStepper } from "./Stepper.svelte"
16
16
  export { default as CoreRichTextField } from "./RichTextField.svelte"
17
17
  export { default as CoreSlider } from "./Slider.svelte"
18
18
  export { default as CoreFile } from "./File.svelte"
19
+ export { default as CoreSignature } from "./Signature.svelte"
@@ -19,6 +19,8 @@
19
19
  export let searchTerm = null
20
20
  export let customPopoverHeight
21
21
  export let helpText = null
22
+ export let onOptionMouseenter = () => {}
23
+ export let onOptionMouseleave = () => {}
22
24
 
23
25
  const dispatch = createEventDispatcher()
24
26
  const onChange = e => {
@@ -41,6 +43,8 @@
41
43
  {autoWidth}
42
44
  {autocomplete}
43
45
  {customPopoverHeight}
46
+ {onOptionMouseenter}
47
+ {onOptionMouseleave}
44
48
  bind:searchTerm
45
49
  on:change={onChange}
46
50
  on:click
@@ -29,6 +29,9 @@
29
29
  export let tag = null
30
30
  export let helpText = null
31
31
  export let compare
32
+ export let onOptionMouseenter = () => {}
33
+ export let onOptionMouseleave = () => {}
34
+
32
35
  const dispatch = createEventDispatcher()
33
36
  const onChange = e => {
34
37
  value = e.detail
@@ -67,6 +70,8 @@
67
70
  {customPopoverHeight}
68
71
  {tag}
69
72
  {compare}
73
+ {onOptionMouseenter}
74
+ {onOptionMouseleave}
70
75
  on:change={onChange}
71
76
  on:click
72
77
  />
@@ -173,6 +173,7 @@
173
173
  }
174
174
 
175
175
  .spectrum-Modal {
176
+ border: 2px solid var(--spectrum-global-color-gray-200);
176
177
  overflow: visible;
177
178
  max-height: none;
178
179
  margin: 40px 0;
@@ -27,6 +27,7 @@
27
27
  export let secondaryButtonText = undefined
28
28
  export let secondaryAction = undefined
29
29
  export let secondaryButtonWarning = false
30
+ export let custom = false
30
31
 
31
32
  const { hide, cancel } = getContext(Context.Modal)
32
33
  let loading = false
@@ -63,12 +64,13 @@
63
64
  class:spectrum-Dialog--medium={size === "M"}
64
65
  class:spectrum-Dialog--large={size === "L"}
65
66
  class:spectrum-Dialog--extraLarge={size === "XL"}
67
+ class:no-grid={custom}
66
68
  style="position: relative;"
67
69
  role="dialog"
68
70
  tabindex="-1"
69
71
  aria-modal="true"
70
72
  >
71
- <div class="spectrum-Dialog-grid">
73
+ <div class="modal-core" class:spectrum-Dialog-grid={!custom}>
72
74
  {#if title || $$slots.header}
73
75
  <h1
74
76
  class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
@@ -153,6 +155,25 @@
153
155
  .spectrum-Dialog-content {
154
156
  overflow: visible;
155
157
  }
158
+
159
+ .no-grid .spectrum-Dialog-content {
160
+ border-top: 2px solid var(--spectrum-global-color-gray-200);
161
+ border-bottom: 2px solid var(--spectrum-global-color-gray-200);
162
+ }
163
+
164
+ .no-grid .spectrum-Dialog-heading {
165
+ margin-top: 12px;
166
+ margin-left: 12px;
167
+ }
168
+
169
+ .spectrum-Dialog.no-grid {
170
+ width: 100%;
171
+ }
172
+
173
+ .spectrum-Dialog.no-grid .spectrum-Dialog-buttonGroup {
174
+ padding: 12px;
175
+ }
176
+
156
177
  .spectrum-Dialog-heading {
157
178
  font-family: var(--font-accent);
158
179
  font-weight: 600;