@bagelink/vue 1.17.2 → 1.17.6
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/dist/assetTransport.d.ts +9 -31
- package/dist/assetTransport.d.ts.map +1 -1
- package/dist/assets.d.ts +39 -20
- package/dist/assets.d.ts.map +1 -1
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +4 -2
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +4 -1
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.mjs +18459 -18281
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +2 -0
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +33 -83
- package/src/assetTransport.ts +24 -90
- package/src/assets.test.ts +58 -99
- package/src/assets.ts +78 -161
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +1 -1
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +2 -2
- package/src/components/index.ts +3 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/form-flow.ts +8 -3
- package/src/form-flow/schema-fields.ts +9 -4
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
<!-- prettier-ignore-start -->
|
|
2
2
|
<!-- eslint-disable -->
|
|
3
3
|
<script setup lang="ts">
|
|
4
|
+
import type { VirtualElement } from '@floating-ui/dom'
|
|
4
5
|
import type { IconType, ThemeType } from '../index'
|
|
5
|
-
import {
|
|
6
|
+
import type { AlignedPlacement, AnchorPoint, DropdownItem, TriggerEvent } from './Dropdown.types'
|
|
7
|
+
import { Btn, Divider, useDevice, useEscapeKey } from '../index'
|
|
6
8
|
import { autoPlacement, computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
7
|
-
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'
|
|
8
|
-
|
|
9
|
-
export type Side = 'top' | 'right' | 'bottom' | 'left'
|
|
10
|
-
export type Alignment = 'start' | 'end'
|
|
11
|
-
export type AlignedPlacement = 'auto' | Side | `${Side}-${Alignment}`
|
|
12
|
-
export type TriggerEvent = 'click' | 'hover'
|
|
9
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'
|
|
13
10
|
|
|
14
11
|
defineOptions({ inheritAttrs: false, name: 'DropDown' })
|
|
15
12
|
const props = withDefaults(defineProps<{
|
|
@@ -26,9 +23,7 @@ const props = withDefaults(defineProps<{
|
|
|
26
23
|
icon?: IconType
|
|
27
24
|
color?: ThemeType
|
|
28
25
|
triggerClass?: string
|
|
29
|
-
/** Extra class(es) applied to the teleported popover element.
|
|
30
|
-
* a theme onto the popup, which renders outside the trigger's DOM subtree
|
|
31
|
-
* (e.g. `popup-class="bgl-dark-mode"` for an always-dark recreation). */
|
|
26
|
+
/** Extra class(es) applied to the teleported popover element. */
|
|
32
27
|
popupClass?: string
|
|
33
28
|
iconEnd?: IconType
|
|
34
29
|
outline?: boolean
|
|
@@ -39,18 +34,20 @@ const props = withDefaults(defineProps<{
|
|
|
39
34
|
noAutoFocus?: boolean
|
|
40
35
|
autoHide?: boolean
|
|
41
36
|
triggers?: TriggerEvent[]
|
|
42
|
-
showTriggers?: TriggerEvent[]
|
|
43
|
-
hideTriggers?: TriggerEvent[]
|
|
44
37
|
popperTriggers?: TriggerEvent[]
|
|
45
|
-
popperShowTriggers?: TriggerEvent[]
|
|
46
|
-
popperHideTriggers?: TriggerEvent[]
|
|
47
38
|
showGroup?: string
|
|
48
39
|
card?: boolean
|
|
49
40
|
/** Remove all popup content padding, whether or not the slot contains a Card. */
|
|
50
41
|
noPadding?: boolean
|
|
51
42
|
delay?: number | { show: number; hide: number }
|
|
52
|
-
/**
|
|
53
|
-
|
|
43
|
+
/** Declarative menu items. Can be used instead of or alongside the default slot. */
|
|
44
|
+
items?: DropdownItem[]
|
|
45
|
+
/** Alias for `items` for backward compatibility. */
|
|
46
|
+
options?: DropdownItem[]
|
|
47
|
+
/** Override the anchor element (or Floating UI VirtualElement) used for positioning. */
|
|
48
|
+
referenceEl?: Element | VirtualElement | null
|
|
49
|
+
/** Position the dropdown at explicit viewport coordinates `{ x, y, width?, height? }`. */
|
|
50
|
+
anchorPoint?: AnchorPoint | null
|
|
54
51
|
}>(), {
|
|
55
52
|
thin: true,
|
|
56
53
|
flat: true,
|
|
@@ -60,26 +57,24 @@ const props = withDefaults(defineProps<{
|
|
|
60
57
|
autoHide: true,
|
|
61
58
|
triggers: () => ['click'],
|
|
62
59
|
popperTriggers: () => [],
|
|
63
|
-
popperShowTriggers: () => [],
|
|
64
|
-
popperHideTriggers: () => [],
|
|
65
60
|
})
|
|
66
61
|
|
|
67
|
-
const emit = defineEmits<{
|
|
62
|
+
const emit = defineEmits<{
|
|
63
|
+
show: []
|
|
64
|
+
hide: []
|
|
65
|
+
select: [value: string | number | DropdownItem, item: DropdownItem]
|
|
66
|
+
}>()
|
|
68
67
|
|
|
68
|
+
const slots = useSlots()
|
|
69
69
|
const shown = defineModel('shown', { type: Boolean, default: false })
|
|
70
70
|
const hasOpened = ref(shown.value)
|
|
71
71
|
|
|
72
72
|
const triggerWrapRef = ref<HTMLElement | null>(null)
|
|
73
73
|
const popoverRef = ref<HTMLElement | null>(null)
|
|
74
|
+
const runtimeAnchorPoint = ref<AnchorPoint | null>(null)
|
|
74
75
|
|
|
75
|
-
// Tracks whether the popover was opened via keyboard (Enter/Space/Arrow) so we know
|
|
76
|
-
// whether to move focus into the menu on open.
|
|
77
76
|
let openedViaKeyboard = false
|
|
78
77
|
|
|
79
|
-
// Unique id for this popover instance, used to track parent–child relationships
|
|
80
|
-
// across teleported popovers. `useId()` is SSR-stable and collision-free — the
|
|
81
|
-
// old module-level counter reset per SSR render, producing duplicate ids like
|
|
82
|
-
// `bgl-dd-1` on multiple dropdowns (an a11y failure: duplicate ARIA IDs).
|
|
83
78
|
const bglId = `bgl-dd-${useId()}`
|
|
84
79
|
const POPOVER_ROOT = '#bagelink-popover-root'
|
|
85
80
|
const teleportTarget = ref<string | Element>(POPOVER_ROOT)
|
|
@@ -89,6 +84,11 @@ const { isMobile } = useDevice()
|
|
|
89
84
|
const shouldDisablePositioning = computed(() => props.disablePlacement && isMobile.value)
|
|
90
85
|
const isSmallScreen = isMobile
|
|
91
86
|
|
|
87
|
+
const resolvedItems = computed(() => props.items ?? props.options ?? [])
|
|
88
|
+
const effectiveAnchorPoint = computed(() => props.anchorPoint ?? runtimeAnchorPoint.value)
|
|
89
|
+
const hasExternalAnchor = computed(() => Boolean(props.referenceEl || effectiveAnchorPoint.value))
|
|
90
|
+
const shouldRenderTrigger = computed(() => !hasExternalAnchor.value || Boolean(slots.trigger))
|
|
91
|
+
|
|
92
92
|
const effectiveTriggers = computed(() =>
|
|
93
93
|
isSmallScreen.value
|
|
94
94
|
? props.triggers.map(t => (t === 'hover' ? 'click' : t))
|
|
@@ -121,8 +121,38 @@ const resolvedDelay = computed((): { show: number; hide: number } => {
|
|
|
121
121
|
// ShowGroup registry — shared across all Dropdown instances on the page
|
|
122
122
|
const groupRegistry = new Map<string, Set<() => void>>()
|
|
123
123
|
|
|
124
|
+
function toVirtualElement(pt: AnchorPoint): VirtualElement {
|
|
125
|
+
const width = pt.width ?? 0
|
|
126
|
+
const height = pt.height ?? 0
|
|
127
|
+
return {
|
|
128
|
+
getBoundingClientRect() {
|
|
129
|
+
return {
|
|
130
|
+
width,
|
|
131
|
+
height,
|
|
132
|
+
x: pt.x,
|
|
133
|
+
y: pt.y,
|
|
134
|
+
top: pt.y,
|
|
135
|
+
left: pt.x,
|
|
136
|
+
right: pt.x + width,
|
|
137
|
+
bottom: pt.y + height,
|
|
138
|
+
toJSON() { return this },
|
|
139
|
+
} as DOMRect
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveAnchorElement(): Element | VirtualElement | null {
|
|
145
|
+
if (effectiveAnchorPoint.value) {
|
|
146
|
+
return toVirtualElement(effectiveAnchorPoint.value)
|
|
147
|
+
}
|
|
148
|
+
if (props.referenceEl) {
|
|
149
|
+
return props.referenceEl
|
|
150
|
+
}
|
|
151
|
+
return (triggerWrapRef.value?.firstElementChild ?? triggerWrapRef.value) as Element | null
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
async function updatePosition() {
|
|
125
|
-
const triggerEl = (
|
|
155
|
+
const triggerEl = resolveAnchorElement()
|
|
126
156
|
if (!popoverRef.value || !triggerEl || shouldDisablePositioning.value) return
|
|
127
157
|
|
|
128
158
|
const isAuto = props.placement === 'auto'
|
|
@@ -147,33 +177,33 @@ function isOpen(): boolean {
|
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
async function internalShow() {
|
|
150
|
-
if (props.disabled
|
|
180
|
+
if (props.disabled) return
|
|
181
|
+
if (isOpen()) {
|
|
182
|
+
await updatePosition()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
151
185
|
|
|
152
186
|
hasOpened.value = true
|
|
153
|
-
|
|
187
|
+
const hostEl = props.referenceEl instanceof Element ? props.referenceEl : triggerWrapRef.value
|
|
188
|
+
teleportTarget.value = hostEl?.closest('dialog[open]') ?? POPOVER_ROOT
|
|
154
189
|
|
|
155
190
|
if (props.showGroup && groupRegistry.has(props.showGroup)) {
|
|
156
191
|
groupRegistry.get(props.showGroup)!.forEach(fn => fn())
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
// Wait for Teleport to move the element to the new target before promoting to top layer
|
|
160
194
|
await nextTick()
|
|
161
195
|
if (!popoverRef.value) return
|
|
162
196
|
|
|
163
|
-
// Tag with our own id and, if the trigger lives inside another open popover, tag with that owner's id.
|
|
164
|
-
// This lets ancestor Dropdowns skip closing when a click lands inside our (teleported) popover.
|
|
165
197
|
popoverRef.value.dataset.bglId = bglId
|
|
166
|
-
const ownerPopover =
|
|
198
|
+
const ownerPopover = hostEl?.closest<HTMLElement>('[data-bgl-id]')
|
|
167
199
|
if (ownerPopover?.dataset.bglId) popoverRef.value.dataset.bglOwner = ownerPopover.dataset.bglId
|
|
168
200
|
else delete popoverRef.value.dataset.bglOwner
|
|
169
201
|
|
|
170
|
-
// Show hidden first so floating-ui can measure it, then position, then reveal
|
|
171
202
|
popoverRef.value.style.opacity = '0'
|
|
172
203
|
popoverRef.value.showPopover()
|
|
173
204
|
await updatePosition()
|
|
174
205
|
popoverRef.value.style.opacity = ''
|
|
175
206
|
|
|
176
|
-
// For keyboard-driven opens, move focus into the menu so arrow keys work immediately.
|
|
177
207
|
if (openedViaKeyboard) {
|
|
178
208
|
focusFirstItem()
|
|
179
209
|
openedViaKeyboard = false
|
|
@@ -182,9 +212,6 @@ async function internalShow() {
|
|
|
182
212
|
|
|
183
213
|
function internalHide(opts?: { restoreFocus?: boolean }) {
|
|
184
214
|
if (!isOpen()) return
|
|
185
|
-
// Only restore focus to the trigger for keyboard-driven closes (Esc/click).
|
|
186
|
-
// Hover-driven closes must NOT steal focus back, otherwise the trigger gets a
|
|
187
|
-
// stray :focus-visible ring after the pointer has already moved elsewhere.
|
|
188
215
|
const restoreFocus = opts?.restoreFocus ?? true
|
|
189
216
|
if (restoreFocus && !props.noAutoFocus) {
|
|
190
217
|
const focusable = triggerWrapRef.value?.querySelector<HTMLElement>(
|
|
@@ -195,6 +222,24 @@ function internalHide(opts?: { restoreFocus?: boolean }) {
|
|
|
195
222
|
popoverRef.value?.hidePopover()
|
|
196
223
|
}
|
|
197
224
|
|
|
225
|
+
function openAt(target: AnchorPoint | MouseEvent) {
|
|
226
|
+
if ('clientX' in target && 'clientY' in target) {
|
|
227
|
+
runtimeAnchorPoint.value = { x: target.clientX, y: target.clientY, width: 0, height: 0 }
|
|
228
|
+
} else {
|
|
229
|
+
runtimeAnchorPoint.value = { ...target }
|
|
230
|
+
}
|
|
231
|
+
internalShow()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function handleItemClick(item: DropdownItem) {
|
|
235
|
+
if (item.disabled) return
|
|
236
|
+
item.action?.()
|
|
237
|
+
emit('select', item.value ?? item.label ?? item, item)
|
|
238
|
+
if (props.autoHide) {
|
|
239
|
+
internalHide()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
198
243
|
// --- Keyboard accessibility helpers -------------------------------------------------
|
|
199
244
|
|
|
200
245
|
const FOCUSABLE_SELECTOR = [
|
|
@@ -219,13 +264,11 @@ function focusItemAt(index: number) {
|
|
|
219
264
|
items[wrapped]?.focus()
|
|
220
265
|
}
|
|
221
266
|
|
|
222
|
-
// Move focus into the menu (first focusable item) — used for keyboard-driven opens.
|
|
223
267
|
function focusFirstItem() {
|
|
224
268
|
if (props.noAutoFocus) return
|
|
225
269
|
focusItemAt(0)
|
|
226
270
|
}
|
|
227
271
|
|
|
228
|
-
// Arrow-key navigation while the popover is open.
|
|
229
272
|
function onPopoverKeydown(e: KeyboardEvent) {
|
|
230
273
|
const items = getFocusableItems()
|
|
231
274
|
if (items.length === 0) return
|
|
@@ -260,6 +303,20 @@ watch(shown, (val) => {
|
|
|
260
303
|
else if (!val && isOpen()) internalHide()
|
|
261
304
|
})
|
|
262
305
|
|
|
306
|
+
// Reposition live when anchorPoint or referenceEl changes while open
|
|
307
|
+
watch(
|
|
308
|
+
() => [
|
|
309
|
+
effectiveAnchorPoint.value?.x,
|
|
310
|
+
effectiveAnchorPoint.value?.y,
|
|
311
|
+
effectiveAnchorPoint.value?.width,
|
|
312
|
+
effectiveAnchorPoint.value?.height,
|
|
313
|
+
props.referenceEl,
|
|
314
|
+
],
|
|
315
|
+
() => {
|
|
316
|
+
if (isOpen()) updatePosition()
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
|
|
263
320
|
// Sync popover state → shown model
|
|
264
321
|
function onPopoverToggle(e: Event) {
|
|
265
322
|
const te = e as ToggleEvent
|
|
@@ -280,19 +337,13 @@ function onDocumentPointerDown(e: PointerEvent) {
|
|
|
280
337
|
if (!props.autoHide || !isOpen()) return
|
|
281
338
|
const target = e.target as Node
|
|
282
339
|
if (popoverRef.value?.contains(target) || triggerWrapRef.value?.contains(target)) return
|
|
283
|
-
// External anchor (referenceEl) counts as the trigger — don't close when
|
|
284
|
-
// the user interacts with it (e.g. re-clicking the chip that opened us).
|
|
285
340
|
if (props.referenceEl instanceof Node && props.referenceEl.contains(target)) return
|
|
286
|
-
// Don't close if the click landed inside a descendant popover (e.g. a SelectInput inside this
|
|
287
|
-
// dropdown that teleports its popup to <body>). We detect this via a data-bgl-owner attribute
|
|
288
|
-
// that each Dropdown sets on its popover element to point to the nearest ancestor popover's id.
|
|
289
341
|
const openPopovers = document.querySelectorAll<HTMLElement>('[popover]:popover-open')
|
|
290
342
|
const myId = popoverRef.value?.dataset.bglId
|
|
291
343
|
for (const p of Array.from(openPopovers)) {
|
|
292
344
|
if (p === popoverRef.value) continue
|
|
293
345
|
if (p.contains(target) && myId && p.dataset.bglOwner === myId) return
|
|
294
346
|
}
|
|
295
|
-
// Pointer landed elsewhere — let the click move focus naturally, don't yank it back to the trigger.
|
|
296
347
|
internalHide({ restoreFocus: false })
|
|
297
348
|
}
|
|
298
349
|
|
|
@@ -304,11 +355,8 @@ let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
|
304
355
|
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
305
356
|
function clearTimers() { clearTimeout(showTimer); clearTimeout(hideTimer) }
|
|
306
357
|
|
|
307
|
-
// Guard against double toggles within the same tick. The <Btn> trigger handles keydown.enter/space
|
|
308
|
-
// itself AND the browser also fires a native click for Enter/Space on a <button>, which would
|
|
309
|
-
// otherwise toggle the popover twice (open → immediately close, the "opens for a second" bug).
|
|
310
358
|
let lastToggleAt = 0
|
|
311
|
-
function toggle(viaKeyboard
|
|
359
|
+
function toggle(viaKeyboard = false) {
|
|
312
360
|
const now = Date.now()
|
|
313
361
|
if (now - lastToggleAt < 50) return
|
|
314
362
|
lastToggleAt = now
|
|
@@ -318,12 +366,9 @@ function toggle(viaKeyboard: boolean) {
|
|
|
318
366
|
|
|
319
367
|
function onTriggerClick(e?: MouseEvent) {
|
|
320
368
|
if (!isClickTrigger.value) return
|
|
321
|
-
// A click via keyboard (Enter/Space) reports detail === 0; a real pointer click reports >= 1.
|
|
322
369
|
toggle(!!e && e.detail === 0)
|
|
323
370
|
}
|
|
324
371
|
|
|
325
|
-
// Keyboard activation on the trigger. Works for ALL trigger types (including hover-only) so the
|
|
326
|
-
// dropdown is reachable by keyboard users — WCAG requires hover-revealed content to also open on focus.
|
|
327
372
|
function onTriggerKeydown(e: KeyboardEvent) {
|
|
328
373
|
const target = e.target as HTMLElement | null
|
|
329
374
|
if (
|
|
@@ -345,8 +390,6 @@ function onTriggerKeydown(e: KeyboardEvent) {
|
|
|
345
390
|
case 'ArrowDown':
|
|
346
391
|
e.preventDefault()
|
|
347
392
|
openedViaKeyboard = true
|
|
348
|
-
// Arrow keys are a deliberate request to navigate, so move focus into the menu
|
|
349
|
-
// even when noAutoFocus is set (noAutoFocus only suppresses focus-on-open).
|
|
350
393
|
if (isOpen()) focusItemAt(0)
|
|
351
394
|
else internalShow()
|
|
352
395
|
break
|
|
@@ -401,7 +444,6 @@ onMounted(() => {
|
|
|
401
444
|
if (!groupRegistry.has(props.showGroup)) groupRegistry.set(props.showGroup, new Set())
|
|
402
445
|
groupRegistry.get(props.showGroup)!.add(internalHide)
|
|
403
446
|
}
|
|
404
|
-
// Honour an already-true v-model:shown on mount (watch is not immediate).
|
|
405
447
|
if (shown.value) internalShow()
|
|
406
448
|
})
|
|
407
449
|
|
|
@@ -412,14 +454,14 @@ onBeforeUnmount(() => {
|
|
|
412
454
|
if (isOpen()) popoverRef.value?.hidePopover()
|
|
413
455
|
})
|
|
414
456
|
|
|
415
|
-
defineExpose({ show, hide, shown })
|
|
457
|
+
defineExpose({ show, hide, toggle, openAt, updatePosition, shown })
|
|
416
458
|
</script>
|
|
417
459
|
|
|
418
460
|
<template>
|
|
419
461
|
<div style="display: contents">
|
|
420
|
-
<!-- Trigger wrapper — omitted when
|
|
462
|
+
<!-- Trigger wrapper — omitted when anchored externally without a #trigger slot -->
|
|
421
463
|
<div
|
|
422
|
-
v-if="
|
|
464
|
+
v-if="shouldRenderTrigger"
|
|
423
465
|
ref="triggerWrapRef" v-bind="$attrs"
|
|
424
466
|
@mouseenter="onTriggerMouseenter" @mouseleave="onTriggerMouseleave" @focus="onTriggerMouseenter"
|
|
425
467
|
@blur="onTriggerMouseleave" @keydown="onTriggerKeydown"
|
|
@@ -456,7 +498,31 @@ defineExpose({ show, hide, shown })
|
|
|
456
498
|
'bgl-dropdown__content--no-padding': props.noPadding,
|
|
457
499
|
}"
|
|
458
500
|
style="border-radius: calc(var(--bgl-card-border-radius) * 0.7);">
|
|
459
|
-
<
|
|
501
|
+
<template v-if="resolvedItems.length > 0">
|
|
502
|
+
<div class="bgl-dropdown__items flex flex-column gap-0125">
|
|
503
|
+
<template v-for="(item, idx) in resolvedItems" :key="item.value ?? item.label ?? idx">
|
|
504
|
+
<Divider v-if="item.divider" class="my-025" />
|
|
505
|
+
<div v-else-if="item.header" class="bgl-dropdown__header px-05 py-025 txt11 semi opacity-5 uppercase">
|
|
506
|
+
{{ item.header }}
|
|
507
|
+
</div>
|
|
508
|
+
<Btn
|
|
509
|
+
v-else
|
|
510
|
+
thin
|
|
511
|
+
flat
|
|
512
|
+
full-width
|
|
513
|
+
align-txt="start"
|
|
514
|
+
role="menuitem"
|
|
515
|
+
:icon="item.icon"
|
|
516
|
+
:value="item.label"
|
|
517
|
+
:color="item.danger ? 'red' : item.color"
|
|
518
|
+
:disabled="item.disabled"
|
|
519
|
+
:class="[item.class, { active: item.active, 'bgl-dropdown__item--danger': item.danger }]"
|
|
520
|
+
@click="handleItemClick(item)"
|
|
521
|
+
/>
|
|
522
|
+
</template>
|
|
523
|
+
</div>
|
|
524
|
+
</template>
|
|
525
|
+
<slot :hide :show :shown :open-at="openAt" />
|
|
460
526
|
</div>
|
|
461
527
|
</div>
|
|
462
528
|
</Teleport>
|
|
@@ -533,15 +599,10 @@ animation: bgl-dropdown-enter 0.18s ease 0.04s both;
|
|
|
533
599
|
transform-origin: top left;
|
|
534
600
|
}
|
|
535
601
|
|
|
536
|
-
/* Default menu breathing room so items don't touch the rounded top/bottom edges.
|
|
537
|
-
Applies to the card content itself so plain menu content (e.g. a list of Btns)
|
|
538
|
-
gets the same padding as content wrapped in a .bgl_card — no consumer py-* needed. */
|
|
539
602
|
.bgl-dropdown__content--card {
|
|
540
603
|
padding-block: 0.5rem;
|
|
541
604
|
padding-inline: 0.375rem;
|
|
542
605
|
}
|
|
543
|
-
/* A direct Card is the popup's main surface, so neither wrapper nor Card gets
|
|
544
|
-
the default menu padding. */
|
|
545
606
|
.bgl-dropdown__content--card:has(> .bgl_card) {
|
|
546
607
|
padding: 0;
|
|
547
608
|
}
|
|
@@ -554,6 +615,10 @@ padding: 0;
|
|
|
554
615
|
padding: 0;
|
|
555
616
|
}
|
|
556
617
|
|
|
618
|
+
.bgl-dropdown__items {
|
|
619
|
+
min-width: 150px;
|
|
620
|
+
}
|
|
621
|
+
|
|
557
622
|
</style>
|
|
558
623
|
<!-- prettier-ignore-end -->
|
|
559
624
|
<!-- eslint-enable -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { FilterField, QueryOption, OptionsSource } from './FilterQuery.types'
|
|
3
|
-
import type { AlignedPlacement } from './Dropdown.
|
|
3
|
+
import type { AlignedPlacement } from './Dropdown.types'
|
|
4
4
|
export type { FilterField, QueryOption, OptionsSource }
|
|
5
5
|
</script>
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { IconType } from '../../index'
|
|
3
3
|
import { useDevice } from '../../index'
|
|
4
4
|
import { computed, ref, onMounted, watchEffect } from 'vue'
|
|
5
|
-
import {
|
|
5
|
+
import { MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
|
|
6
6
|
|
|
7
7
|
type AnimateType = 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'slide-up' | 'rotate-in'
|
|
8
8
|
type TransitionType = 'slide-up' | 'rotate' | 'scale' | 'fade'
|
|
@@ -21,7 +21,6 @@ const props = withDefaults(defineProps<{
|
|
|
21
21
|
Uses `color`, and defaults to black when no color is provided. */
|
|
22
22
|
soft?: boolean
|
|
23
23
|
weight?: number | string
|
|
24
|
-
fontAwesome?: boolean
|
|
25
24
|
fill?: boolean
|
|
26
25
|
/** Entrance / looping effect. Pure CSS, opt-in. e.g. `animate="spin"`. */
|
|
27
26
|
animate?: AnimateType
|
|
@@ -50,17 +49,15 @@ const computedSize = computed(() => {
|
|
|
50
49
|
})
|
|
51
50
|
|
|
52
51
|
const iconRenderType = computed(() => {
|
|
53
|
-
|
|
52
|
+
// Only brand glyphs use Font Awesome. Regular/solid icons are intentionally
|
|
53
|
+
// unsupported; their names fall back to Material Symbols.
|
|
54
|
+
if (FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value)) { return 'font-awesome-brand' }
|
|
54
55
|
if (MATERIAL_ICONS.includes(iconRender.value)) { return 'material' }
|
|
55
|
-
if (FONT_AWESOME_ICONS.includes(iconRender.value) || FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value)) { return 'font-awesome' }
|
|
56
56
|
return 'material'
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
const isFaBrand = computed(() => FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value))
|
|
60
|
-
|
|
61
59
|
// Hide until required font is ready to avoid ligature text/fallback flash
|
|
62
60
|
const isMaterialReady = ref(true)
|
|
63
|
-
const isFAFreeReady = ref(true)
|
|
64
61
|
const isFABrandsReady = ref(true)
|
|
65
62
|
|
|
66
63
|
function getFontFaceSet(): FontFaceSet | null {
|
|
@@ -92,11 +89,6 @@ async function loadMaterialIfNeeded() {
|
|
|
92
89
|
isMaterialReady.value = ok
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
async function loadFAFreeIfNeeded() {
|
|
96
|
-
const ok = await ensureFontLoaded('Font Awesome 6 Free')
|
|
97
|
-
isFAFreeReady.value = ok
|
|
98
|
-
}
|
|
99
|
-
|
|
100
92
|
async function loadFABrandsIfNeeded() {
|
|
101
93
|
const ok = await ensureFontLoaded('Font Awesome 6 Brands')
|
|
102
94
|
isFABrandsReady.value = ok
|
|
@@ -108,14 +100,10 @@ onMounted(() => {
|
|
|
108
100
|
const fonts = getFontFaceSet()
|
|
109
101
|
isMaterialReady.value = fonts ? fonts.check('1em "Material Symbols Outlined"') : true
|
|
110
102
|
void loadMaterialIfNeeded()
|
|
111
|
-
} else
|
|
103
|
+
} else {
|
|
112
104
|
const fonts = getFontFaceSet()
|
|
113
105
|
isFABrandsReady.value = fonts ? fonts.check('1em "Font Awesome 6 Brands"') : true
|
|
114
106
|
void loadFABrandsIfNeeded()
|
|
115
|
-
} else {
|
|
116
|
-
const fonts = getFontFaceSet()
|
|
117
|
-
isFAFreeReady.value = fonts ? fonts.check('1em "Font Awesome 6 Free"') : true
|
|
118
|
-
void loadFAFreeIfNeeded()
|
|
119
107
|
}
|
|
120
108
|
})
|
|
121
109
|
|
|
@@ -123,16 +111,14 @@ watchEffect(() => {
|
|
|
123
111
|
if (!supportsFontLoading()) { return }
|
|
124
112
|
if (iconRenderType.value === 'material') {
|
|
125
113
|
void loadMaterialIfNeeded()
|
|
126
|
-
} else if (isFaBrand.value) {
|
|
127
|
-
void loadFABrandsIfNeeded()
|
|
128
114
|
} else {
|
|
129
|
-
void
|
|
115
|
+
void loadFABrandsIfNeeded()
|
|
130
116
|
}
|
|
131
117
|
})
|
|
132
118
|
|
|
133
119
|
const isCurrentFontReady = computed(() => {
|
|
134
120
|
if (iconRenderType.value === 'material') { return isMaterialReady.value }
|
|
135
|
-
return
|
|
121
|
+
return isFABrandsReady.value
|
|
136
122
|
})
|
|
137
123
|
|
|
138
124
|
// Entrance / loop animation class + timing.
|
|
@@ -213,16 +199,13 @@ const iconStyle = computed(() => {
|
|
|
213
199
|
{{ iconRender }}
|
|
214
200
|
</span>
|
|
215
201
|
<span
|
|
216
|
-
v-else-if="iconRenderType === 'font-awesome'" :key="`font-awesome-${iconRender}`" class="fa bgl_icon-font notranslate" :class="[
|
|
202
|
+
v-else-if="iconRenderType === 'font-awesome-brand'" :key="`font-awesome-brand-${iconRender}`" class="fa fa-brands bgl_icon-font notranslate" :class="[
|
|
217
203
|
`fa-${iconRender}`,
|
|
218
204
|
animateClass,
|
|
219
205
|
roundClass,
|
|
220
206
|
roundPairClass,
|
|
221
207
|
softClass,
|
|
222
208
|
{
|
|
223
|
-
'fa-brands': isFaBrand,
|
|
224
|
-
'fa-solid': fill,
|
|
225
|
-
'far': !fill && !isFaBrand,
|
|
226
209
|
'color-white keep-white': round && !soft && isRawCssColor,
|
|
227
210
|
},
|
|
228
211
|
]" :style="iconStyle" aria-hidden="true"
|
|
@@ -239,7 +222,6 @@ const iconStyle = computed(() => {
|
|
|
239
222
|
* RANGES shrinks the served font dramatically (the full ~3.8MB font is the
|
|
240
223
|
* single biggest asset on the page) while keeping weight/fill working. */
|
|
241
224
|
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,100..700,0..1,0');
|
|
242
|
-
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css');
|
|
243
225
|
|
|
244
226
|
[dir='rtl'] .data-row .bgl_icon-font,
|
|
245
227
|
[dir='rtl'] .embedded-field .bgl_icon-font,
|
|
@@ -264,10 +246,6 @@ const iconStyle = computed(() => {
|
|
|
264
246
|
box-sizing: border-box;
|
|
265
247
|
}
|
|
266
248
|
|
|
267
|
-
.bgl_icon-font.fa {
|
|
268
|
-
font-family: 'Font Awesome 6 Free', serif !important;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
249
|
.bgl_icon-font.fa-brands {
|
|
272
250
|
font-family: 'Font Awesome 6 Brands', serif !important;
|
|
273
251
|
}
|
|
@@ -1,169 +1,3 @@
|
|
|
1
|
-
export const FONT_AWESOME_ICONS = [
|
|
2
|
-
'trash-can',
|
|
3
|
-
'message',
|
|
4
|
-
'file-lines',
|
|
5
|
-
'calendar-days',
|
|
6
|
-
'hand-point-right',
|
|
7
|
-
'face-smile-beam',
|
|
8
|
-
'face-grin-stars',
|
|
9
|
-
'address-book',
|
|
10
|
-
'comments',
|
|
11
|
-
'paste',
|
|
12
|
-
'face-grin-tongue-squint',
|
|
13
|
-
'face-flushed',
|
|
14
|
-
'square-caret-right',
|
|
15
|
-
'square-minus',
|
|
16
|
-
'compass',
|
|
17
|
-
'square-caret-down',
|
|
18
|
-
'face-kiss-beam',
|
|
19
|
-
'lightbulb',
|
|
20
|
-
'flag',
|
|
21
|
-
'square-check',
|
|
22
|
-
'circle-dot',
|
|
23
|
-
'face-dizzy',
|
|
24
|
-
'futbol',
|
|
25
|
-
'pen-to-square',
|
|
26
|
-
'hourglass-half',
|
|
27
|
-
'eye-slash',
|
|
28
|
-
'hand',
|
|
29
|
-
'hand-spock',
|
|
30
|
-
'face-kiss',
|
|
31
|
-
'face-grin-tongue',
|
|
32
|
-
'chess-bishop',
|
|
33
|
-
'face-grin-wink',
|
|
34
|
-
'face-grin-wide',
|
|
35
|
-
'face-frown-open',
|
|
36
|
-
'hand-point-up',
|
|
37
|
-
'bookmark',
|
|
38
|
-
'hand-point-down',
|
|
39
|
-
'folder',
|
|
40
|
-
'user',
|
|
41
|
-
'square-caret-left',
|
|
42
|
-
'star',
|
|
43
|
-
'chess-knight',
|
|
44
|
-
'face-laugh-squint',
|
|
45
|
-
'face-laugh',
|
|
46
|
-
'folder-open',
|
|
47
|
-
'clipboard',
|
|
48
|
-
'chess-queen',
|
|
49
|
-
'hand-back-fist',
|
|
50
|
-
'square-caret-up',
|
|
51
|
-
'chart-bar',
|
|
52
|
-
'window-restore',
|
|
53
|
-
'square-plus',
|
|
54
|
-
'image',
|
|
55
|
-
'folder-closed',
|
|
56
|
-
'lemon',
|
|
57
|
-
'handshake',
|
|
58
|
-
'gem',
|
|
59
|
-
'circle-play',
|
|
60
|
-
'circle-check',
|
|
61
|
-
'circle-stop',
|
|
62
|
-
'id-badge',
|
|
63
|
-
'face-laugh-beam',
|
|
64
|
-
'registered',
|
|
65
|
-
'address-card',
|
|
66
|
-
'face-tired',
|
|
67
|
-
'font-awesome',
|
|
68
|
-
'face-smile-wink',
|
|
69
|
-
'file-word',
|
|
70
|
-
'file-powerpoint',
|
|
71
|
-
'envelope-open',
|
|
72
|
-
'file-zipper',
|
|
73
|
-
'square',
|
|
74
|
-
'snowflake',
|
|
75
|
-
'newspaper',
|
|
76
|
-
'face-kiss-wink-heart',
|
|
77
|
-
'star-half-stroke',
|
|
78
|
-
'file-excel',
|
|
79
|
-
'face-grin-beam',
|
|
80
|
-
'object-ungroup',
|
|
81
|
-
'circle-right',
|
|
82
|
-
'face-rolling-eyes',
|
|
83
|
-
'object-group',
|
|
84
|
-
'heart',
|
|
85
|
-
'face-surprise',
|
|
86
|
-
'circle-pause',
|
|
87
|
-
'circle',
|
|
88
|
-
'circle-up',
|
|
89
|
-
'file-audio',
|
|
90
|
-
'file-image',
|
|
91
|
-
'circle-question',
|
|
92
|
-
'face-meh-blank',
|
|
93
|
-
'eye',
|
|
94
|
-
'face-sad-cry',
|
|
95
|
-
'file-code',
|
|
96
|
-
'window-maximize',
|
|
97
|
-
'face-frown',
|
|
98
|
-
'floppy-disk',
|
|
99
|
-
'comment-dots',
|
|
100
|
-
'face-grin-squint',
|
|
101
|
-
'hand-pointer',
|
|
102
|
-
'hand-scissors',
|
|
103
|
-
'face-grin-tears',
|
|
104
|
-
'calendar-xmark',
|
|
105
|
-
'file-video',
|
|
106
|
-
'file-pdf',
|
|
107
|
-
'comment',
|
|
108
|
-
'envelope',
|
|
109
|
-
'hourglass',
|
|
110
|
-
'calendar-check',
|
|
111
|
-
'hard-drive',
|
|
112
|
-
'face-grin-squint-tears',
|
|
113
|
-
'rectangle-list',
|
|
114
|
-
'calendar-plus',
|
|
115
|
-
'circle-left',
|
|
116
|
-
'money-bill-1',
|
|
117
|
-
'clock',
|
|
118
|
-
'keyboard',
|
|
119
|
-
'closed-captioning',
|
|
120
|
-
'images',
|
|
121
|
-
'face-grin',
|
|
122
|
-
'face-meh',
|
|
123
|
-
'id-card',
|
|
124
|
-
'sun',
|
|
125
|
-
'face-laugh-wink',
|
|
126
|
-
'circle-down',
|
|
127
|
-
'thumbs-down',
|
|
128
|
-
'chess-pawn',
|
|
129
|
-
'credit-card',
|
|
130
|
-
'bell',
|
|
131
|
-
'file',
|
|
132
|
-
'hospital',
|
|
133
|
-
'chess-rook',
|
|
134
|
-
'star-half',
|
|
135
|
-
'chess-king',
|
|
136
|
-
'circle-user',
|
|
137
|
-
'copy',
|
|
138
|
-
'share-from-square',
|
|
139
|
-
'copyright',
|
|
140
|
-
'map',
|
|
141
|
-
'bell-slash',
|
|
142
|
-
'hand-lizard',
|
|
143
|
-
'face-smile',
|
|
144
|
-
'hand-peace',
|
|
145
|
-
'face-grin-hearts',
|
|
146
|
-
'building',
|
|
147
|
-
'face-grin-beam-sweat',
|
|
148
|
-
'moon',
|
|
149
|
-
'calendar',
|
|
150
|
-
'face-grin-tongue-wink',
|
|
151
|
-
'clone',
|
|
152
|
-
'face-angry',
|
|
153
|
-
'rectangle-xmark',
|
|
154
|
-
'paper-plane',
|
|
155
|
-
'life-ring',
|
|
156
|
-
'face-grimace',
|
|
157
|
-
'calendar-minus',
|
|
158
|
-
'circle-xmark',
|
|
159
|
-
'thumbs-up',
|
|
160
|
-
'window-minimize',
|
|
161
|
-
'square-full',
|
|
162
|
-
'note-sticky',
|
|
163
|
-
'face-sad-tear',
|
|
164
|
-
'hand-point-left'
|
|
165
|
-
]
|
|
166
|
-
|
|
167
1
|
export const FONT_AWESOME_BRANDS_ICONS = [
|
|
168
2
|
'monero',
|
|
169
3
|
'hooli',
|