@bagelink/vue 1.4.124 → 1.4.128

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": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.4.124",
4
+ "version": "1.4.128",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Neveh Allon",
@@ -36,6 +36,7 @@ interface PropTypes {
36
36
  }
37
37
 
38
38
  const searchInput = $ref<HTMLElement | undefined>()
39
+ const triggerBtn = $ref<HTMLButtonElement | undefined>()
39
40
 
40
41
  let selectedItems = $ref<Option[]>([])
41
42
 
@@ -145,7 +146,15 @@ function select(option: Option) {
145
146
  selectedItems.splice(0, selectedItemCount, option)
146
147
  }
147
148
 
148
- if (!props.multiselect) open = false
149
+ // Move focus away from popper content before it gets aria-hidden
150
+ const active = document.activeElement as HTMLElement | null
151
+ if (active && selectOptions?.contains(active)) active.blur()
152
+
153
+ if (!props.multiselect) {
154
+ open = false
155
+ // After closing, return focus to the trigger for accessibility
156
+ setTimeout(() => { triggerBtn?.focus() }, 0)
157
+ }
149
158
  emitUpdate()
150
159
  }
151
160
 
@@ -241,7 +250,7 @@ onMounted(() => {
241
250
  @keydown.down.prevent="navigate('down')" @keydown.up.prevent="navigate('up')"
242
251
  />
243
252
  <button
244
- v-else :disabled="disabled" type="button" class="selectinput-btn"
253
+ v-else ref="triggerBtn" :disabled="disabled" type="button" class="selectinput-btn"
245
254
  :class="{ isEmpty: selectedItemCount === 0 }" @keydown.down.prevent="navigate('down')"
246
255
  @keydown.up.prevent="navigate('up')"
247
256
  >
@@ -235,11 +235,20 @@ export function useSchemaField<T extends { [key: string]: any }>(optns: UseSchem
235
235
  props.options = (query: string) => {
236
236
  const row = getFormData?.()
237
237
  const val = currentValue
238
+ // If author expects (query) only, call directly
239
+ if (typeof fn === 'function' && fn.length === 1) {
240
+ return fn(query)
241
+ }
242
+ // If author expects (query, val, row), pass it directly
243
+ if (typeof fn === 'function' && fn.length >= 3) {
244
+ return fn(query, val, row)
245
+ }
246
+ // Otherwise evaluate (val,row) → array | (query)=>Promise | Promise
238
247
  const out = fn(val, row)
239
248
  if (Array.isArray(out)) return out
240
249
  if (typeof out === 'function') return out(query)
241
250
  if (out && typeof out.then === 'function') return out
242
- return fn(query, val, row)
251
+ return []
243
252
  }
244
253
  } else {
245
254
  // Non-search components (e.g., RadioGroup): return a zero-arg async loader using latest row
@@ -20,6 +20,7 @@ const DEFAULT_PATTERNS: Record<string, PatternValue> = {
20
20
  lower: { pattern: /[a-z]/i, transform: (v: string) => v.toLowerCase() },
21
21
  upper: { pattern: /[A-Z]/i, transform: (v: string) => v.toUpperCase() },
22
22
  slug: { pattern: /[a-z0-9-]/i, transform: (v: string) => v.toLowerCase().replace(/\s/g, '-') },
23
+ snake: { pattern: /[a-z_]/, transform: (v: string) => v.toLowerCase().replace(/\s/g, '_') },
23
24
  }
24
25
 
25
26
  interface PatternState {
@@ -27,7 +28,11 @@ interface PatternState {
27
28
  pattern?: RegExp
28
29
  modifier?: string
29
30
  keydownEvent?: EventListenerOrEventListenerObject
30
- pasteEvent?: EventListenerOrEventListenerObject
31
+ inputEvent?: EventListenerOrEventListenerObject
32
+ compositionStartEvent?: EventListenerOrEventListenerObject
33
+ compositionEndEvent?: EventListenerOrEventListenerObject
34
+ composing?: boolean
35
+ reformatting?: boolean
31
36
  }
32
37
 
33
38
  const stateMap = new WeakMap<HTMLElement, PatternState>()
@@ -41,85 +46,155 @@ function getTarget(el: HTMLElement): HTMLInputElement | HTMLTextAreaElement | nu
41
46
  }
42
47
 
43
48
  function getModifiers(binding: DirectiveBinding): string {
44
- const modifiers = Object.keys(binding.modifiers || {})
45
- return modifiers.length ? modifiers[modifiers.length - 1] : ''
49
+ const modifiers = Object.keys(binding.modifiers)
50
+ return modifiers.length > 0 ? modifiers[modifiers.length - 1] : ''
46
51
  }
47
52
 
48
53
  function getRegex(state: PatternState): RegExp {
49
- if (state.pattern) return state.pattern
50
- if (state.modifier) return DEFAULT_PATTERNS[state.modifier].pattern || /./
54
+ if (state.pattern != null) return state.pattern
55
+ if (state.modifier != null && state.modifier !== '') {
56
+ const presetPattern = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.pattern
57
+ return presetPattern ?? /./
58
+ }
51
59
  return /./
52
60
  }
53
61
 
54
62
  function bindEvents(el: HTMLElement, target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
55
- state.keydownEvent = ((event: KeyboardEvent) => { onKeydown(event, target, state) }) as EventListener
56
- state.pasteEvent = ((event: ClipboardEvent) => { onPaste(event, target, state) }) as EventListener
63
+ state.keydownEvent = ((event: KeyboardEvent) => {
64
+ onKeydown(event, target, state)
65
+ }) as EventListener
66
+ state.inputEvent = ((event: InputEvent) => {
67
+ onInput(event, target, state)
68
+ }) as EventListener
69
+ state.compositionStartEvent = (() => {
70
+ onCompositionStart(state)
71
+ }) as EventListener
72
+ state.compositionEndEvent = ((event: CompositionEvent) => {
73
+ onCompositionEnd(event, target, state)
74
+ }) as EventListener
57
75
 
58
76
  target.addEventListener('keydown', state.keydownEvent)
59
- target.addEventListener('paste', state.pasteEvent)
77
+ target.addEventListener('input', state.inputEvent, { capture: true })
78
+ target.addEventListener('compositionstart', state.compositionStartEvent)
79
+ target.addEventListener('compositionend', state.compositionEndEvent)
60
80
  }
61
81
 
62
82
  function unbindEvents(target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
63
83
  if (state.keydownEvent) target.removeEventListener('keydown', state.keydownEvent)
64
- if (state.pasteEvent) target.removeEventListener('paste', state.pasteEvent)
84
+ if (state.inputEvent) target.removeEventListener('input', state.inputEvent, { capture: true })
85
+ if (state.compositionStartEvent) target.removeEventListener('compositionstart', state.compositionStartEvent)
86
+ if (state.compositionEndEvent) target.removeEventListener('compositionend', state.compositionEndEvent)
65
87
 
66
88
  state.keydownEvent = undefined
67
- state.pasteEvent = undefined
89
+ state.inputEvent = undefined
90
+ state.compositionStartEvent = undefined
91
+ state.compositionEndEvent = undefined
92
+ }
93
+
94
+ function isNavigationOrControlKey(event: KeyboardEvent): boolean {
95
+ const allowedKeys = [
96
+ 'Tab',
97
+ 'Backspace',
98
+ 'Delete',
99
+ 'ArrowLeft',
100
+ 'ArrowRight',
101
+ 'ArrowUp',
102
+ 'ArrowDown',
103
+ 'Enter',
104
+ ]
105
+ if (event.ctrlKey || event.altKey || event.metaKey) return true
106
+ return allowedKeys.includes(event.key)
68
107
  }
69
108
 
70
109
  function onKeydown(event: KeyboardEvent, target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
71
- if (event.ctrlKey || event.altKey || event.metaKey || event.key === 'Tab' || event.key === 'Backspace' || event.key === 'Delete' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Enter') return
110
+ if (state.composing === true) return
111
+ if (isNavigationOrControlKey(event)) return
112
+
113
+ // If a transform is configured (e.g., slug/snake/lower/upper),
114
+ // do not block keystrokes; normalization happens on input.
115
+ if (state.modifier != null && state.modifier !== '') {
116
+ const transformFn = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.transform
117
+ if (typeof transformFn === 'function') return
118
+ }
72
119
 
73
120
  const regex = getRegex(state)
74
121
  const testKey = event.key
75
122
 
76
123
  if (!regex.test(testKey)) {
77
124
  event.preventDefault()
78
- } else if (state.modifier) {
79
- const { transform } = DEFAULT_PATTERNS[state.modifier]
80
- if (transform) {
81
- console.log('transform', transform)
82
- event.preventDefault()
83
- const transformedValue = transform(testKey)
84
- const cursorPos = target.selectionStart ?? 0
85
- const newValue = target.value.slice(0, cursorPos) + transformedValue + target.value.slice(cursorPos)
86
- target.value = newValue
87
- target.setSelectionRange(cursorPos + transformedValue.length, cursorPos + transformedValue.length)
88
- }
89
125
  }
90
126
  }
91
127
 
92
- function onPaste(event: ClipboardEvent, target: HTMLInputElement | HTMLTextAreaElement, state: PatternState) {
93
- const regex = getRegex(state)
94
- const clipboardData = event.clipboardData?.getData('text') || ''
128
+ function onCompositionStart(state: PatternState) {
129
+ state.composing = true
130
+ }
95
131
 
96
- if (!regex.test(clipboardData)) {
97
- event.preventDefault()
98
- } else if (state.modifier) {
99
- const { transform } = DEFAULT_PATTERNS[state.modifier]
100
- if (transform) {
101
- event.preventDefault()
102
- const transformedValue = transform(clipboardData)
103
- const cursorPos = target.selectionStart ?? 0
104
- const newValue = target.value.slice(0, cursorPos) + transformedValue + target.value.slice(cursorPos)
105
- target.value = newValue
106
- target.setSelectionRange(cursorPos + transformedValue.length, cursorPos + transformedValue.length)
107
- }
132
+ function onCompositionEnd(
133
+ event: CompositionEvent,
134
+ target: HTMLInputElement | HTMLTextAreaElement,
135
+ state: PatternState,
136
+ ) {
137
+ state.composing = false
138
+ // Normalize after IME commits text
139
+ onInput(event as unknown as InputEvent, target, state)
140
+ }
141
+
142
+ function sanitizeTransformed(value: string, state: PatternState): string {
143
+ const regex = getRegex(state)
144
+ let transformed: string
145
+ if (state.modifier != null && state.modifier !== '') {
146
+ const transformFn = (DEFAULT_PATTERNS as Record<string, PatternValue | undefined>)[state.modifier]?.transform
147
+ transformed = transformFn ? transformFn(value) : value
148
+ } else {
149
+ transformed = value
150
+ }
151
+ let result = ''
152
+ for (let i = 0; i < transformed.length; i++) {
153
+ const ch = transformed[i]
154
+ if (regex.test(ch)) result += ch
108
155
  }
156
+ return result
157
+ }
158
+
159
+ function onInput(
160
+ event: InputEvent,
161
+ target: HTMLInputElement | HTMLTextAreaElement,
162
+ state: PatternState,
163
+ ) {
164
+ if (state.reformatting === true) return
165
+ if (state.composing === true) return
166
+
167
+ const originalValue = target.value
168
+ const selectionEnd = target.selectionEnd ?? originalValue.length
169
+
170
+ const left = originalValue.slice(0, selectionEnd)
171
+
172
+ const sanitizedFull = sanitizeTransformed(originalValue, state)
173
+ if (sanitizedFull === originalValue) return
174
+
175
+ const leftSanitized = sanitizeTransformed(left, state)
176
+ const newCursor = leftSanitized.length
177
+
178
+ state.reformatting = true
179
+ target.value = sanitizedFull
180
+ target.setSelectionRange(newCursor, newCursor)
181
+ state.reformatting = false
109
182
  }
110
183
 
184
+ // Paste is handled naturally via the input event normalization
185
+
111
186
  const pattern = {
112
187
  beforeMount(el: HTMLElement, binding: DirectiveBinding) {
113
188
  const target = getTarget(el)
114
- if (!target) return
189
+ if (target === null) return
115
190
 
116
191
  const state: PatternState = {
117
192
  modifier: getModifiers(binding),
118
193
  }
119
194
 
120
- if (typeof binding.value === 'object') {
121
- state.pattern = binding.value.pattern || /./
122
- state.validateOnly = binding.value.validateOnly || false
195
+ if (typeof binding.value === 'object' && binding.value !== null) {
196
+ state.pattern = binding.value.pattern ?? /./
197
+ state.validateOnly = binding.value.validateOnly === true
123
198
  }
124
199
 
125
200
  stateMap.set(el, state)
@@ -127,18 +202,18 @@ const pattern = {
127
202
  },
128
203
  updated(el: HTMLElement, binding: DirectiveBinding) {
129
204
  const target = getTarget(el)
130
- if (!target) return
205
+ if (target === null) return
131
206
 
132
207
  const state = stateMap.get(el)
133
- if (!state) return
208
+ if (state === undefined) return
134
209
 
135
210
  unbindEvents(target, state)
136
211
 
137
212
  state.modifier = getModifiers(binding)
138
213
 
139
- if (typeof binding.value === 'object') {
140
- state.pattern = binding.value.pattern || /./
141
- state.validateOnly = binding.value.validateOnly || false
214
+ if (typeof binding.value === 'object' && binding.value !== null) {
215
+ state.pattern = binding.value.pattern ?? /./
216
+ state.validateOnly = binding.value.validateOnly === true
142
217
  }
143
218
 
144
219
  bindEvents(el, target, state)
@@ -147,7 +222,7 @@ const pattern = {
147
222
  const target = getTarget(el)
148
223
  const state = stateMap.get(el)
149
224
 
150
- if (target && state) {
225
+ if (target !== null && state !== undefined) {
151
226
  unbindEvents(target, state)
152
227
  }
153
228