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.
@@ -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' :class="[triggerClass, searchInput]"
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)" :title="opt?.name"
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, toRaw } from 'vue'
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
- let ary =[]
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 = computed(() => {
177
- let screenHeight = window.innerHeight;
178
- let info = toRaw(domPosition.value)
179
- listIsOnTop.value = info.top >= (screenHeight - info.bottom)
180
- let str = ''
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
- str = `bottom:${screenHeight - info.top}px;max-height:calc(${info.top}px - 10%);`
179
+ style += `bottom:${screenH - rect.top + OFFSET}px;max-height:${spaceAbove - OFFSET}px;`
183
180
  } else {
184
- str = `top:${info.bottom}px;max-height:calc(${screenHeight - info.bottom}px - 10%);`
181
+ style += `top:${rect.bottom + OFFSET}px;max-height:${spaceBelow - OFFSET}px;`
185
182
  }
186
- return `${str}left:${info.x}px;max-width:calc(90% - ${info.x}px);min-width:${Math.abs(info.x - info.right)}px;`
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.blur()
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) && (sampling instanceof RegExp) && Array.isArray(sampling)) {
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
- let final = props.opts.filter(i => {
230
- for (var idx = 0; idx < props.filterKeys.length; idx++) {
231
- if (regex.test(i[props.filterKeys[idx].toString()])) {
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.contains(event.target) || componentContentList.value.contains(event.target)) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobdone-shared-files",
3
- "version": "1.1.21",
3
+ "version": "1.1.22",
4
4
  "description": "Shared JS and SCSS for Jobdone Enterprise.",
5
5
  "main": "index.js",
6
6
  "scripts": {