jobdone-shared-files 1.1.21 → 1.1.22
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/autocompleteSelect.vue +120 -57
- package/package.json +1 -1
package/autocompleteSelect.vue
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
selectedPreviewText
|
|
7
7
|
}}</button>
|
|
8
8
|
<!-- search input -->
|
|
9
|
-
<input class='form-control autocomplete-select-component-keyword-filter-input'
|
|
10
|
-
type="text" ref="keywordFilterInput" v-model="keyword"
|
|
9
|
+
<input class='form-control autocomplete-select-component-keyword-filter-input'
|
|
10
|
+
:class="[triggerClass, searchInput]" type="text" ref="keywordFilterInput" v-model="keyword"
|
|
11
11
|
:placeholder="searchPlaceholder == '' ? selectedPreviewText : searchPlaceholder" maxlength="50"
|
|
12
12
|
@keydown.enter="keyboardSelectConfirm($event)" @keydown.up="keyboardSwitch($event, -1)"
|
|
13
13
|
@keydown.down="keyboardSwitch($event, 1)" @change="keyboardSwitchIndexReset()">
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
</li>
|
|
29
29
|
<template v-if="active">
|
|
30
|
-
<li v-for="(opt, idx) in filterList" :key="idx" @click.stop="selectConfirm(opt)"
|
|
31
|
-
:id="`autocomplete-select-component-opt-item_${idx}`">
|
|
30
|
+
<li v-for="(opt, idx) in filterList" :key="idx" @click.stop="selectConfirm(opt)"
|
|
31
|
+
:title="opt?.name" :id="`autocomplete-select-component-opt-item_${idx}`">
|
|
32
32
|
<button class="autocomplete-select-component-opt-item" type="button"
|
|
33
33
|
:class="`${idx == keyboardSwitchIndex ? 'active' : ''} ${optClass}`">
|
|
34
34
|
<template v-if="htmlOption">
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
// enum Data & Functions
|
|
56
56
|
|
|
57
57
|
// vue & bootstrap
|
|
58
|
-
import { ref, onMounted, onUnmounted, computed,
|
|
58
|
+
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
|
59
59
|
|
|
60
60
|
// plugins
|
|
61
61
|
|
|
@@ -106,7 +106,6 @@ const props = defineProps({
|
|
|
106
106
|
default: false
|
|
107
107
|
},
|
|
108
108
|
|
|
109
|
-
|
|
110
109
|
previewKey: {
|
|
111
110
|
type: String,
|
|
112
111
|
default: '',
|
|
@@ -140,22 +139,15 @@ const componentContentInput = ref(null)
|
|
|
140
139
|
const componentContentList = ref(null)
|
|
141
140
|
const keywordFilterInput = ref(null)
|
|
142
141
|
|
|
143
|
-
const valueIsUnselected = computed(()=>{
|
|
142
|
+
const valueIsUnselected = computed(() => {
|
|
144
143
|
const defaultPlaceholderValue = [null, undefined, 0, '']
|
|
145
|
-
|
|
146
|
-
for (let index = 0; index < defaultPlaceholderValue.length; index++) {
|
|
147
|
-
const element = defaultPlaceholderValue[index];
|
|
148
|
-
if(!props.isUnselectedExtendValue.includes(element)){
|
|
149
|
-
ary.push(element)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return ary || []
|
|
144
|
+
return defaultPlaceholderValue.filter(v => !props.isUnselectedExtendValue.includes(v))
|
|
153
145
|
})
|
|
154
146
|
const checkValueIsUnselected = (checkValue) => {
|
|
155
|
-
if(valueIsUnselected.value.includes(checkValue)){
|
|
147
|
+
if (valueIsUnselected.value.includes(checkValue)) {
|
|
156
148
|
return true
|
|
157
149
|
}
|
|
158
|
-
if((typeof checkValue) === 'object' && checkValue !== null && Object.keys(checkValue).length === 0){
|
|
150
|
+
if ((typeof checkValue) === 'object' && checkValue !== null && Object.keys(checkValue).length === 0) {
|
|
159
151
|
return true
|
|
160
152
|
}
|
|
161
153
|
return false
|
|
@@ -163,36 +155,110 @@ const checkValueIsUnselected = (checkValue) => {
|
|
|
163
155
|
|
|
164
156
|
// 是否開啟下拉選單
|
|
165
157
|
const active = ref(false)
|
|
166
|
-
const domPosition = ref({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 })
|
|
167
|
-
function activeSelector() {
|
|
168
|
-
if (props.disabled) {
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
active.value = true
|
|
172
|
-
keywordFilterInput.value.focus()
|
|
173
|
-
domPosition.value = componentContentInput.value.getBoundingClientRect()
|
|
174
|
-
}
|
|
175
158
|
const listIsOnTop = ref(false)
|
|
176
|
-
const positionStyle =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
159
|
+
const positionStyle = ref('')
|
|
160
|
+
|
|
161
|
+
// 自製定位:position fixed + getBoundingClientRect,不依賴外部套件
|
|
162
|
+
const OFFSET = 4 // 下拉與 input 的間距(px)
|
|
163
|
+
|
|
164
|
+
function updatePosition() {
|
|
165
|
+
if (!componentContentInput.value || !componentContentList.value) return
|
|
166
|
+
|
|
167
|
+
const rect = componentContentInput.value.getBoundingClientRect()
|
|
168
|
+
const listEl = componentContentList.value
|
|
169
|
+
const screenH = window.innerHeight
|
|
170
|
+
const spaceBelow = screenH - rect.bottom
|
|
171
|
+
const spaceAbove = rect.top
|
|
172
|
+
const listH = listEl.scrollHeight || 200
|
|
173
|
+
|
|
174
|
+
// flip: 下方空間不夠且上方比較多 → 顯示在上面
|
|
175
|
+
listIsOnTop.value = spaceBelow < listH && spaceAbove > spaceBelow
|
|
176
|
+
|
|
177
|
+
let style = `position:fixed;left:${rect.left}px;min-width:${rect.width}px;max-width:calc(90vw - ${rect.left}px);`
|
|
181
178
|
if (listIsOnTop.value) {
|
|
182
|
-
|
|
179
|
+
style += `bottom:${screenH - rect.top + OFFSET}px;max-height:${spaceAbove - OFFSET}px;`
|
|
183
180
|
} else {
|
|
184
|
-
|
|
181
|
+
style += `top:${rect.bottom + OFFSET}px;max-height:${spaceBelow - OFFSET}px;`
|
|
185
182
|
}
|
|
186
|
-
|
|
187
|
-
}
|
|
183
|
+
positionStyle.value = style
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 追蹤監聽器的清理函式
|
|
187
|
+
let cleanupListeners = null
|
|
188
|
+
let visibilityObserver = null
|
|
189
|
+
|
|
190
|
+
function startTracking() {
|
|
191
|
+
// 收集所有可滾動的祖層
|
|
192
|
+
const scrollParents = getScrollParents(componentContentInput.value)
|
|
193
|
+
let rafId = null
|
|
194
|
+
const onUpdate = () => {
|
|
195
|
+
if (rafId) return
|
|
196
|
+
rafId = requestAnimationFrame(() => {
|
|
197
|
+
updatePosition()
|
|
198
|
+
rafId = null
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
scrollParents.forEach(el => el.addEventListener('scroll', onUpdate, { passive: true }))
|
|
203
|
+
window.addEventListener('resize', onUpdate, { passive: true })
|
|
204
|
+
|
|
205
|
+
// 偵測 input 離開可視區域時自動關閉
|
|
206
|
+
visibilityObserver = new IntersectionObserver((entries) => {
|
|
207
|
+
if (!entries[0].isIntersecting && active.value) {
|
|
208
|
+
leave()
|
|
209
|
+
}
|
|
210
|
+
}, { threshold: 0 })
|
|
211
|
+
visibilityObserver.observe(componentContentInput.value)
|
|
212
|
+
|
|
213
|
+
cleanupListeners = () => {
|
|
214
|
+
scrollParents.forEach(el => el.removeEventListener('scroll', onUpdate))
|
|
215
|
+
window.removeEventListener('resize', onUpdate)
|
|
216
|
+
if (visibilityObserver) {
|
|
217
|
+
visibilityObserver.disconnect()
|
|
218
|
+
visibilityObserver = null
|
|
219
|
+
}
|
|
220
|
+
cleanupListeners = null
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function stopTracking() {
|
|
225
|
+
if (cleanupListeners) cleanupListeners()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 找出所有可滾動的祖層元素
|
|
229
|
+
function getScrollParents(el) {
|
|
230
|
+
const parents = []
|
|
231
|
+
let current = el?.parentElement
|
|
232
|
+
while (current) {
|
|
233
|
+
const style = getComputedStyle(current)
|
|
234
|
+
if (/(auto|scroll|overlay)/.test(style.overflow + style.overflowY + style.overflowX)) {
|
|
235
|
+
parents.push(current)
|
|
236
|
+
}
|
|
237
|
+
current = current.parentElement
|
|
238
|
+
}
|
|
239
|
+
parents.push(window)
|
|
240
|
+
return parents
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function activeSelector() {
|
|
244
|
+
if (props.disabled) return
|
|
245
|
+
const wasActive = active.value
|
|
246
|
+
active.value = true
|
|
247
|
+
keywordFilterInput.value.focus()
|
|
248
|
+
if (!wasActive) {
|
|
249
|
+
await nextTick()
|
|
250
|
+
updatePosition()
|
|
251
|
+
startTracking()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
188
254
|
|
|
189
255
|
function leave() {
|
|
256
|
+
stopTracking()
|
|
190
257
|
listIsOnTop.value = false
|
|
191
258
|
active.value = false
|
|
192
259
|
keyword.value = ''
|
|
193
|
-
// TODO SCROLL CLOSE
|
|
194
260
|
keyboardSwitchIndexReset()
|
|
195
|
-
keywordFilterInput.value
|
|
261
|
+
keywordFilterInput.value?.blur()
|
|
196
262
|
}
|
|
197
263
|
|
|
198
264
|
// 確認傳入選單列表內的選項Type,只檢查第一個,請內容統一,不要傳奇怪的東西進來
|
|
@@ -206,7 +272,7 @@ const optItemType = computed(() => {
|
|
|
206
272
|
if (!allowOptItemType.includes(type)) {
|
|
207
273
|
return ''
|
|
208
274
|
}
|
|
209
|
-
if ((sampling instanceof Date)
|
|
275
|
+
if ((sampling instanceof Date) || (sampling instanceof RegExp) || Array.isArray(sampling)) {
|
|
210
276
|
return ''
|
|
211
277
|
}
|
|
212
278
|
return type?.toLowerCase()
|
|
@@ -223,23 +289,14 @@ const filterList = computed(() => {
|
|
|
223
289
|
if (keyword.value == '') {
|
|
224
290
|
return props.opts
|
|
225
291
|
}
|
|
226
|
-
let kwReplace = keyword.value.replace(/[.*+?^${}()|[\]\\]/g, "");
|
|
292
|
+
let kwReplace = keyword.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
227
293
|
let regex = new RegExp(kwReplace, "i");
|
|
228
294
|
if (optItemType.value === 'object') {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return i
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
return final
|
|
295
|
+
return props.opts.filter(i =>
|
|
296
|
+
props.filterKeys.some(key => regex.test(i[key.toString()]))
|
|
297
|
+
)
|
|
237
298
|
}
|
|
238
|
-
return props.opts.filter(i =>
|
|
239
|
-
if (regex.test(i)) {
|
|
240
|
-
return i
|
|
241
|
-
}
|
|
242
|
-
})
|
|
299
|
+
return props.opts.filter(i => regex.test(i))
|
|
243
300
|
})
|
|
244
301
|
|
|
245
302
|
// 從選項列表中挖出整個選中的item,因選中的值(可能是某個key)
|
|
@@ -262,6 +319,7 @@ function previewAdjust(itemObj) {
|
|
|
262
319
|
let strAry = props.previewKey.split('+')
|
|
263
320
|
let isUndefined = false
|
|
264
321
|
|
|
322
|
+
const undefinedKeys = []
|
|
265
323
|
let finalStr = strAry.reduce((acc, cur) => {
|
|
266
324
|
let v = cur.trim()
|
|
267
325
|
if (v.match(/'[^']+'/g)) {
|
|
@@ -269,11 +327,17 @@ function previewAdjust(itemObj) {
|
|
|
269
327
|
} else {
|
|
270
328
|
if (itemObj[v] === undefined) {
|
|
271
329
|
isUndefined = true
|
|
330
|
+
undefinedKeys.push(v)
|
|
272
331
|
}
|
|
273
332
|
return acc + itemObj[v]
|
|
274
333
|
}
|
|
275
334
|
}, '')
|
|
276
335
|
if (isUndefined) {
|
|
336
|
+
console.error(
|
|
337
|
+
`[autocompleteSelect] previewKey 中找不到對應欄位: [${undefinedKeys.join(', ')}],` +
|
|
338
|
+
`物件實際擁有的 key: [${Object.keys(itemObj).join(', ')}]`,
|
|
339
|
+
itemObj
|
|
340
|
+
)
|
|
277
341
|
return props.isUndefinedHint
|
|
278
342
|
}
|
|
279
343
|
return finalStr ? finalStr : props.placeholder
|
|
@@ -360,9 +424,10 @@ const searchInput = computed(() => {
|
|
|
360
424
|
return ''
|
|
361
425
|
})
|
|
362
426
|
function initTrigger(event) {
|
|
363
|
-
if (componentContentInput.value
|
|
427
|
+
if (!componentContentInput.value) return
|
|
428
|
+
if (componentContentInput.value.contains(event.target) || componentContentList.value?.contains(event.target)) {
|
|
364
429
|
activeSelector()
|
|
365
|
-
} else {
|
|
430
|
+
} else if (active.value) {
|
|
366
431
|
leave()
|
|
367
432
|
}
|
|
368
433
|
}
|
|
@@ -372,6 +437,7 @@ onMounted(() => {
|
|
|
372
437
|
|
|
373
438
|
onUnmounted(() => {
|
|
374
439
|
window.removeEventListener("click", initTrigger);
|
|
440
|
+
stopTracking()
|
|
375
441
|
})
|
|
376
442
|
|
|
377
443
|
</script>
|
|
@@ -424,10 +490,7 @@ onUnmounted(() => {
|
|
|
424
490
|
.autocomplete-select-component-selector-content {
|
|
425
491
|
visibility: hidden;
|
|
426
492
|
display: none;
|
|
427
|
-
position: fixed;
|
|
428
493
|
z-index: 2000;
|
|
429
|
-
margin-top: 0.25rem;
|
|
430
|
-
margin-bottom: 0.25rem;
|
|
431
494
|
max-height: 76vh;
|
|
432
495
|
overflow: auto;
|
|
433
496
|
|