jobdone-shared-files 1.0.49 → 1.0.51

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.
Files changed (40) hide show
  1. package/ModuleInfo/LayoutNav.vue +251 -251
  2. package/ModuleInfo/logo-with-text.svg +22 -22
  3. package/ModuleInfo/navButton.vue +218 -218
  4. package/ProjectManagement/projectNavbar.vue +363 -363
  5. package/ProjectManagement/projectNavbarV2.vue +2 -2
  6. package/README.md +1 -0
  7. package/autocompleteSelect.vue +487 -465
  8. package/common/directives/collapse.js +12 -12
  9. package/common/directives/popovers.js +10 -10
  10. package/common/directives/selectPlaceholder.js +52 -52
  11. package/common/directives/textareaAutoHeight.js +10 -10
  12. package/common/directives/tooltip.js +10 -10
  13. package/common/format.js +26 -26
  14. package/index.js +14 -14
  15. package/lightboxWithOverview.vue +156 -156
  16. package/package.json +19 -19
  17. package/paginate.vue +141 -141
  18. package/style/css/vue-loading-overlay/index.css +40 -40
  19. package/style/scss/Common/Animation.scss +9 -9
  20. package/style/scss/Common/SelectableTable.scss +36 -36
  21. package/style/scss/Common/Tree.scss +281 -281
  22. package/style/scss/Common/filepond.scss +31 -31
  23. package/style/scss/Common/thumbnail-group.scss +14 -14
  24. package/style/scss/Layout/LayoutBase.scss +1032 -1032
  25. package/style/scss/Layout/LayoutInnerColumn.scss +263 -263
  26. package/style/scss/Layout/LayoutProject.scss +126 -126
  27. package/style/scss/Layout/LayoutSinglePage.scss +17 -17
  28. package/style/scss/Layout/LayoutTwoColumn.scss +60 -60
  29. package/style/scss/Settings/_Mixins.scss +232 -232
  30. package/style/scss/Settings/_MobileVariables.scss +11 -11
  31. package/style/scss/Settings/_bs-variables-dark.scss +70 -70
  32. package/style/scss/Settings/_bs-variables.scss +1743 -1743
  33. package/style/scss/Settings/_color-mode.scss +122 -122
  34. package/style/scss/Settings/_custom-variables.scss +10 -10
  35. package/tagEditor.vue +249 -249
  36. package/tree.vue +71 -71
  37. package/treeItem.vue +358 -358
  38. package/treeItemV2.vue +78 -78
  39. package/treeV2.vue +71 -71
  40. package/vueLoadingOverlay.vue +74 -74
@@ -1,466 +1,488 @@
1
- <template>
2
- <div class="autocomplete-select-component-trigger-content" ref="componentContentInput">
3
- <!-- preview input -->
4
- <button type="button" class='form-control autocomplete-select-component-display-selecting' :disabled="disabled"
5
- :class="[triggerClass, previewInput]">{{
6
- selectedPreviewText
7
- }}</button>
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"
11
- :placeholder="searchPlaceholder == '' ? selectedPreviewText : searchPlaceholder" maxlength="50"
12
- @keydown.enter="keyboardSelectConfirm($event)" @keydown.up="keyboardSwitch($event, -1)"
13
- @keydown.down="keyboardSwitch($event, 1)" @change="keyboardSwitchIndexReset()">
14
- <Teleport :to="listPut">
15
- <div class="autocomplete-select-component-selector-content" :class="{ 'active': active }"
16
- ref="componentContentList" :style="positionStyle">
17
- <ul class="autocomplete-select-component-opt-list" ref="opt_list">
18
- <li v-if="!listIsOnTop && clearBtn" class="border-bottom" :title="clearPlaceholder">
19
- <button class="autocomplete-select-component-opt-item is-clear" type="button"
20
- @click.stop="clearSelected()">
21
- {{ clearPlaceholder }}
22
- </button>
23
- </li>
24
- <li v-if="filterList?.length == 0">
25
- <div class="autocomplete-select-component-opt-item is-nothing">
26
- 沒有符合條件的選項
27
- </div>
28
- </li>
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}`">
32
- <button class="autocomplete-select-component-opt-item" type="button"
33
- :class="`${idx == keyboardSwitchIndex ? 'active' : ''} ${optClass}`">
34
- <template v-if="htmlOption">
35
- <slot name="option" :optData="opt"></slot>
36
- </template>
37
- <template v-else>
38
- {{ previewAdjust(opt) }}
39
- </template>
40
- </button>
41
- </li>
42
- </template>
43
- <li v-if="listIsOnTop && clearBtn" class="border-top" :title="clearPlaceholder">
44
- <button class="autocomplete-select-component-opt-item is-clear" type="button"
45
- @click.stop="clearSelected()">
46
- {{ clearPlaceholder }}
47
- </button>
48
- </li>
49
- </ul>
50
- </div>
51
- </Teleport>
52
- </div>
53
- </template>
54
- <script setup>
55
- // enum Data & Functions
56
-
57
- // vue & bootstrap
58
- import { ref, onMounted, onUnmounted, computed, toRaw } from 'vue'
59
-
60
- // plugins
61
-
62
- // vue components
63
-
64
- const props = defineProps({
65
- selectedData: {
66
- required: true
67
- },
68
- opts: {
69
- type: Array,
70
- default: []
71
- },
72
- optClass: {
73
- type: String,
74
- default: '',
75
- },
76
- placeholder: {
77
- type: String,
78
- default: '請選擇'
79
- },
80
- searchPlaceholder: {
81
- type: String,
82
- default: ''
83
- },
84
- triggerClass: {
85
- type: String,
86
- default: ''
87
- },
88
- listPut: {
89
- type: String,
90
- default: 'body'
91
- },
92
-
93
- clearBtn: {
94
- type: Boolean,
95
- default: true
96
- },
97
- clearPlaceholder: {
98
- type: String,
99
- default: '清除選擇'
100
- },
101
- resetValue: {
102
- default: ''
103
- },
104
- disabled: {
105
- type: Boolean,
106
- default: false
107
- },
108
-
109
-
110
- previewKey: {
111
- type: String,
112
- default: '',
113
- },
114
- bindingKey: {
115
- type: String,
116
- default: '',
117
- },
118
- filterKeys: {
119
- type: Array,
120
- default: ['name'],
121
- },
122
- isUndefinedHint: {
123
- type: String,
124
- default: '⚠️ 未找到符合原選擇的選項,可能相關資料曾發生異動',
125
- },
126
- htmlOption: {
127
- type: Boolean,
128
- default: false
129
- },
130
-
131
-
132
- });
133
-
134
- const emit = defineEmits(['select'])
135
-
136
- // Just DOM
137
- const componentContentInput = ref(null)
138
- const componentContentList = ref(null)
139
- const keywordFilterInput = ref(null)
140
-
141
-
142
- // 是否開啟下拉選單
143
- const active = ref(false)
144
- const domPosition = ref({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 })
145
- function activeSelector() {
146
- if (props.disabled) {
147
- return
148
- }
149
- active.value = true
150
- keywordFilterInput.value.focus()
151
- domPosition.value = componentContentInput.value.getBoundingClientRect()
152
- }
153
- const listIsOnTop = ref(false)
154
- const positionStyle = computed(() => {
155
- let screenHeight = window.innerHeight;
156
- let info = toRaw(domPosition.value)
157
- listIsOnTop.value = info.top >= (screenHeight - info.bottom)
158
- let str = ''
159
- if (listIsOnTop.value) {
160
- str = `bottom:${screenHeight - info.top}px;max-height:calc(${info.top}px - 10%);`
161
- } else {
162
- str = `top:${info.bottom}px;max-height:calc(${screenHeight - info.bottom}px - 10%);`
163
- }
164
- return `${str}left:${info.x}px;max-width:calc(90% - ${info.x}px);min-width:${Math.abs(info.x - info.right)}px;`
165
- })
166
-
167
- function leave() {
168
- listIsOnTop.value = false
169
- active.value = false
170
- keyword.value = ''
171
- // TODO SCROLL CLOSE
172
- keyboardSwitchIndexReset()
173
- keywordFilterInput.value.blur()
174
- }
175
-
176
- // 確認傳入選單列表內的選項Type,只檢查第一個,請內容統一,不要傳奇怪的東西進來
177
- const allowOptItemType = ['string', 'number', 'object']
178
- const optItemType = computed(() => {
179
- if (!Array.isArray(props.opts) || props.opts?.length < 1) {
180
- return ''
181
- }
182
- let sampling = props.opts[0]
183
- let type = typeof sampling
184
- if (!allowOptItemType.includes(type)) {
185
- return ''
186
- }
187
- if ((sampling instanceof Date) && (sampling instanceof RegExp) && Array.isArray(sampling)) {
188
- return ''
189
- }
190
- return type?.toLowerCase()
191
-
192
- })
193
-
194
- //
195
- // 篩選
196
- const keyword = ref("")
197
- const filterList = computed(() => {
198
- if (optItemType.value === '') {
199
- return []
200
- }
201
- if (keyword.value == '') {
202
- return props.opts
203
- }
204
- let kwReplace = keyword.value.replace(/[.*+?^${}()|[\]\\]/g, "");
205
- let regex = new RegExp(kwReplace, "i");
206
- if (optItemType.value === 'object') {
207
- let final = props.opts.filter(i => {
208
- for (var idx = 0; idx < props.filterKeys.length; idx++) {
209
- if (regex.test(i[props.filterKeys[idx].toString()])) {
210
- return i
211
- };
212
- }
213
- })
214
- return final
215
- }
216
- return props.opts.filter(i => {
217
- if (regex.test(i)) {
218
- return i
219
- }
220
- })
221
- })
222
-
223
- // 從選項列表中挖出整個選中的item,因選中的值(可能是某個key)
224
- const selectedItem = computed(() => {
225
- if (props.selectedData === null || props.selectedData === undefined || ((typeof props.selectedData) === 'object' && Object.keys(props.selectedData).length === 0)) {
226
- return
227
- }
228
-
229
- // 如果選中的值是某個Key,需自行指定bindingKey
230
- if (props.bindingKey !== '') {
231
- return props.opts.find(i => i[props.bindingKey.toString()] == props.selectedData) ?? props.selectedData
232
- }
233
- return props.selectedData
234
- })
235
-
236
-
237
- // 整理 選項與選中值 的顯示文字
238
- function previewAdjust(itemObj) {
239
- if (props.previewKey !== '') {
240
- let strAry = props.previewKey.split('+')
241
- let isUndefined = false
242
-
243
- let finalStr = strAry.reduce((acc, cur) => {
244
- let v = cur.trim()
245
- if (v.match(/'[^']+'/g)) {
246
- return acc + v.substring(1, v.length - 1)
247
- } else {
248
- if (itemObj[v] === undefined) {
249
- isUndefined = true
250
- }
251
- return acc + itemObj[v]
252
- }
253
- }, '')
254
- if (isUndefined) {
255
- return props.isUndefinedHint
256
- }
257
- return finalStr ? finalStr : props.placeholder
258
- }
259
- return itemObj
260
- }
261
-
262
-
263
- // 選中的值的顯示
264
- const selectedPreviewText = computed(() => {
265
- if (selectedItem.value === '' || selectedItem.value === null || selectedItem.value === undefined || ((typeof selectedItem.value) === 'object' && Object.keys(selectedItem.value).length === 0)) {
266
- return props.placeholder
267
- }
268
- return previewAdjust(selectedItem.value)
269
- })
270
-
271
- // 清除選擇內容
272
- function clearSelected() {
273
- emit('select', props.resetValue)
274
- leave()
275
- }
276
-
277
- // 選擇送出
278
- function selectConfirm(v) {
279
- if (v === null || v === undefined || ((typeof v) === 'object' && Object.keys(v).length === 0)) {
280
- return
281
- }
282
- emit('select', JSON.parse(JSON.stringify(v)))
283
- leave()
284
- keyboardSwitchIndexReset()
285
- }
286
-
287
- // 鍵盤送出
288
- function keyboardSelectConfirm(event) {
289
- if (event.isComposing) {
290
- return
291
- }
292
- if (filterList.value?.length) {
293
- selectConfirm(filterList.value[keyboardSwitchIndex.value])
294
- }
295
- }
296
-
297
- // 鍵盤挑選
298
- const keyboardSwitchIndex = ref(0)
299
- function keyboardSwitch(event, v) {
300
- if (event.isComposing) {
301
- return
302
- }
303
- if (filterList.value?.length < 1) {
304
- keyboardSwitchIndexReset()
305
- return
306
- }
307
- let idx = Number(keyboardSwitchIndex.value) + v
308
-
309
- if (idx < 0) {
310
- keyboardSwitchIndex.value = filterList.value.length - 1
311
- document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
312
- return
313
- }
314
- if (idx > filterList.value.length - 1) {
315
- keyboardSwitchIndexReset()
316
- document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
317
- return
318
- }
319
- keyboardSwitchIndex.value = idx
320
- document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
321
- }
322
-
323
- function keyboardSwitchIndexReset() {
324
- keyboardSwitchIndex.value = 0
325
- }
326
-
327
- // 樣式
328
- const previewInput = computed(() => {
329
- if (selectedPreviewText.value == props.placeholder) {
330
- return 'autocomplete-select-component-value-null'
331
- }
332
- return ''
333
- })
334
- const searchInput = computed(() => {
335
- if (active.value) {
336
- return 'active'
337
- }
338
- return ''
339
- })
340
- function initTrigger(event) {
341
- if (componentContentInput.value.contains(event.target) || componentContentList.value.contains(event.target)) {
342
- activeSelector()
343
- } else {
344
- leave()
345
- }
346
- }
347
- onMounted(() => {
348
- window.addEventListener("click", initTrigger);
349
- })
350
-
351
- onUnmounted(() => {
352
- window.removeEventListener("click", initTrigger);
353
- })
354
-
355
- </script>
356
- <style lang="scss" scoped>
357
- * {
358
- word-break: break-word;
359
- overflow-wrap: break-word;
360
- }
361
-
362
- .autocomplete-select-component-trigger-content {
363
- position: relative;
364
- width: 100%;
365
-
366
- .form-control {
367
- background-image: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%232D4155%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e");
368
- background-repeat: no-repeat;
369
- background-position: right 0.75rem center;
370
- background-size: 16px 12px;
371
- text-align: start;
372
- width: 100%;
373
- }
374
-
375
- .autocomplete-select-component-value-null {
376
- color: var(--bs-secondary-color);
377
- }
378
-
379
- .autocomplete-select-component-display-selecting {
380
- position: absolute;
381
- top: 0;
382
- left: 0;
383
- z-index: 1;
384
- white-space: nowrap;
385
- overflow: hidden;
386
- text-overflow: ellipsis;
387
- padding-right: 2rem;
388
- }
389
-
390
- .autocomplete-select-component-keyword-filter-input {
391
- opacity: 0;
392
- position: relative;
393
- padding-right: 2rem;
394
-
395
- &.active {
396
- opacity: 1;
397
- z-index: 2;
398
- }
399
- }
400
- }
401
-
402
- .autocomplete-select-component-selector-content {
403
- visibility: hidden;
404
- display: none;
405
- position: fixed;
406
- z-index: 2000;
407
- margin-top: 0.25rem;
408
- margin-bottom: 0.25rem;
409
- max-height: 76vh;
410
- overflow: auto;
411
-
412
- ul.autocomplete-select-component-opt-list {
413
- background: #fff;
414
- list-style: none;
415
- border: 1px solid var(--gray-400);
416
- border-radius: var(--bs-border-radius);
417
- margin: 0;
418
- padding: 0;
419
- overflow: auto;
420
- padding: .25rem 0;
421
-
422
- >li {
423
- width: 100%;
424
- overflow: auto;
425
- }
426
-
427
- .autocomplete-select-component-opt-item {
428
- background-color: transparent;
429
- border: none;
430
- padding: .25rem .5rem;
431
- word-break: break-word;
432
- width: 100%;
433
- overflow: hidden;
434
- text-align: start;
435
- text-overflow: ellipsis;
436
- color: var(--gray-600);
437
-
438
- &.is-nothing {
439
- color: var(--bs-danger);
440
- }
441
-
442
- &.is-clear {
443
- color: var(--gray-500);
444
- }
445
-
446
- &:hover,
447
- &.is-clear:hover {
448
- background: rgba(var(--bs-primary-rgb), 1) !important;
449
- color: #fff !important;
450
- }
451
-
452
- &.active {
453
- background: rgba(var(--bs-primary-rgb), .1);
454
- color: var(--bs-primary);
455
- }
456
- }
457
- }
458
-
459
- &.active {
460
- display: block;
461
- box-shadow: var(--bs-box-shadow);
462
- visibility: visible;
463
- z-index: 2000;
464
- }
465
- }
1
+ <template>
2
+ <div class="autocomplete-select-component-trigger-content" ref="componentContentInput">
3
+ <!-- preview input -->
4
+ <button type="button" class='form-control autocomplete-select-component-display-selecting' :disabled="disabled"
5
+ :class="[triggerClass, previewInput]">{{
6
+ selectedPreviewText
7
+ }}</button>
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"
11
+ :placeholder="searchPlaceholder == '' ? selectedPreviewText : searchPlaceholder" maxlength="50"
12
+ @keydown.enter="keyboardSelectConfirm($event)" @keydown.up="keyboardSwitch($event, -1)"
13
+ @keydown.down="keyboardSwitch($event, 1)" @change="keyboardSwitchIndexReset()">
14
+ <Teleport :to="listPut">
15
+ <div class="autocomplete-select-component-selector-content" :class="{ 'active': active }"
16
+ ref="componentContentList" :style="positionStyle">
17
+ <ul class="autocomplete-select-component-opt-list" ref="opt_list">
18
+ <li v-if="!listIsOnTop && clearBtn" class="border-bottom" :title="clearPlaceholder">
19
+ <button class="autocomplete-select-component-opt-item is-clear" type="button"
20
+ @click.stop="clearSelected()">
21
+ {{ clearPlaceholder }}
22
+ </button>
23
+ </li>
24
+ <li v-if="filterList?.length == 0">
25
+ <div class="autocomplete-select-component-opt-item is-nothing">
26
+ 沒有符合條件的選項
27
+ </div>
28
+ </li>
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}`">
32
+ <button class="autocomplete-select-component-opt-item" type="button"
33
+ :class="`${idx == keyboardSwitchIndex ? 'active' : ''} ${optClass}`">
34
+ <template v-if="htmlOption">
35
+ <slot name="option" :optData="opt"></slot>
36
+ </template>
37
+ <template v-else>
38
+ {{ previewAdjust(opt) }}
39
+ </template>
40
+ </button>
41
+ </li>
42
+ </template>
43
+ <li v-if="listIsOnTop && clearBtn" class="border-top" :title="clearPlaceholder">
44
+ <button class="autocomplete-select-component-opt-item is-clear" type="button"
45
+ @click.stop="clearSelected()">
46
+ {{ clearPlaceholder }}
47
+ </button>
48
+ </li>
49
+ </ul>
50
+ </div>
51
+ </Teleport>
52
+ </div>
53
+ </template>
54
+ <script setup>
55
+ // enum Data & Functions
56
+
57
+ // vue & bootstrap
58
+ import { ref, onMounted, onUnmounted, computed, toRaw } from 'vue'
59
+
60
+ // plugins
61
+
62
+ // vue components
63
+
64
+ const props = defineProps({
65
+ selectedData: {
66
+ required: true
67
+ },
68
+ opts: {
69
+ type: Array,
70
+ default: []
71
+ },
72
+ optClass: {
73
+ type: String,
74
+ default: '',
75
+ },
76
+ placeholder: {
77
+ type: String,
78
+ default: '請選擇'
79
+ },
80
+ searchPlaceholder: {
81
+ type: String,
82
+ default: ''
83
+ },
84
+ triggerClass: {
85
+ type: String,
86
+ default: ''
87
+ },
88
+ listPut: {
89
+ type: String,
90
+ default: 'body'
91
+ },
92
+
93
+ clearBtn: {
94
+ type: Boolean,
95
+ default: true
96
+ },
97
+ clearPlaceholder: {
98
+ type: String,
99
+ default: '清除選擇'
100
+ },
101
+ resetValue: {
102
+ default: ''
103
+ },
104
+ disabled: {
105
+ type: Boolean,
106
+ default: false
107
+ },
108
+
109
+
110
+ previewKey: {
111
+ type: String,
112
+ default: '',
113
+ },
114
+ bindingKey: {
115
+ type: String,
116
+ default: '',
117
+ },
118
+ filterKeys: {
119
+ type: Array,
120
+ default: ['name'],
121
+ },
122
+ isUndefinedHint: {
123
+ type: String,
124
+ default: '⚠️ 未找到符合原選擇的選項,可能相關資料曾發生異動',
125
+ },
126
+ htmlOption: {
127
+ type: Boolean,
128
+ default: false
129
+ },
130
+ isUnselectedExtendValue: {
131
+ type: Array,
132
+ default: []
133
+ }
134
+ });
135
+
136
+ const emit = defineEmits(['select'])
137
+
138
+ // Just DOM
139
+ const componentContentInput = ref(null)
140
+ const componentContentList = ref(null)
141
+ const keywordFilterInput = ref(null)
142
+
143
+ const valueIsUnselected = computed(()=>{
144
+ 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 || []
153
+ })
154
+ const checkValueIsUnselected = (checkValue) => {
155
+ if(valueIsUnselected.value.includes(checkValue)){
156
+ return true
157
+ }
158
+ if((typeof props.selectedData) === 'object' && checkValue !== null && Object.keys(props.selectedData).length === 0){
159
+ return true
160
+ }
161
+ return false
162
+ }
163
+
164
+ // 是否開啟下拉選單
165
+ 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
+ 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 = ''
181
+ if (listIsOnTop.value) {
182
+ str = `bottom:${screenHeight - info.top}px;max-height:calc(${info.top}px - 10%);`
183
+ } else {
184
+ str = `top:${info.bottom}px;max-height:calc(${screenHeight - info.bottom}px - 10%);`
185
+ }
186
+ return `${str}left:${info.x}px;max-width:calc(90% - ${info.x}px);min-width:${Math.abs(info.x - info.right)}px;`
187
+ })
188
+
189
+ function leave() {
190
+ listIsOnTop.value = false
191
+ active.value = false
192
+ keyword.value = ''
193
+ // TODO SCROLL CLOSE
194
+ keyboardSwitchIndexReset()
195
+ keywordFilterInput.value.blur()
196
+ }
197
+
198
+ // 確認傳入選單列表內的選項Type,只檢查第一個,請內容統一,不要傳奇怪的東西進來
199
+ const allowOptItemType = ['string', 'number', 'object']
200
+ const optItemType = computed(() => {
201
+ if (!Array.isArray(props.opts) || props.opts?.length < 1) {
202
+ return ''
203
+ }
204
+ let sampling = props.opts[0]
205
+ let type = typeof sampling
206
+ if (!allowOptItemType.includes(type)) {
207
+ return ''
208
+ }
209
+ if ((sampling instanceof Date) && (sampling instanceof RegExp) && Array.isArray(sampling)) {
210
+ return ''
211
+ }
212
+ return type?.toLowerCase()
213
+
214
+ })
215
+
216
+ //
217
+ // 篩選
218
+ const keyword = ref("")
219
+ const filterList = computed(() => {
220
+ if (optItemType.value === '') {
221
+ return []
222
+ }
223
+ if (keyword.value == '') {
224
+ return props.opts
225
+ }
226
+ let kwReplace = keyword.value.replace(/[.*+?^${}()|[\]\\]/g, "");
227
+ let regex = new RegExp(kwReplace, "i");
228
+ 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
237
+ }
238
+ return props.opts.filter(i => {
239
+ if (regex.test(i)) {
240
+ return i
241
+ }
242
+ })
243
+ })
244
+
245
+ // 從選項列表中挖出整個選中的item,因選中的值(可能是某個key)
246
+ const selectedItem = computed(() => {
247
+ if (checkValueIsUnselected(props.selectedData)) {
248
+ return
249
+ }
250
+
251
+ // 如果選中的值是某個Key,需自行指定bindingKey
252
+ if (props.bindingKey !== '') {
253
+ return props.opts.find(i => i[props.bindingKey.toString()] == props.selectedData) ?? props.selectedData
254
+ }
255
+ return props.selectedData
256
+ })
257
+
258
+
259
+ // 整理 選項與選中值 的顯示文字
260
+ function previewAdjust(itemObj) {
261
+ if (props.previewKey !== '') {
262
+ let strAry = props.previewKey.split('+')
263
+ let isUndefined = false
264
+
265
+ let finalStr = strAry.reduce((acc, cur) => {
266
+ let v = cur.trim()
267
+ if (v.match(/'[^']+'/g)) {
268
+ return acc + v.substring(1, v.length - 1)
269
+ } else {
270
+ if (itemObj[v] === undefined) {
271
+ isUndefined = true
272
+ }
273
+ return acc + itemObj[v]
274
+ }
275
+ }, '')
276
+ if (isUndefined) {
277
+ return props.isUndefinedHint
278
+ }
279
+ return finalStr ? finalStr : props.placeholder
280
+ }
281
+ return itemObj
282
+ }
283
+
284
+
285
+ // 選中的值的顯示
286
+ const selectedPreviewText = computed(() => {
287
+ if (checkValueIsUnselected(selectedItem.value)) {
288
+ return props.placeholder
289
+ }
290
+ return previewAdjust(selectedItem.value)
291
+ })
292
+
293
+ // 清除選擇內容
294
+ function clearSelected() {
295
+ emit('select', props.resetValue)
296
+ leave()
297
+ }
298
+
299
+ // 選擇送出
300
+ function selectConfirm(v) {
301
+ if (checkValueIsUnselected(v)) {
302
+ return props.resetValue
303
+ }
304
+ emit('select', JSON.parse(JSON.stringify(v)))
305
+ leave()
306
+ keyboardSwitchIndexReset()
307
+ }
308
+
309
+ // 鍵盤送出
310
+ function keyboardSelectConfirm(event) {
311
+ if (event.isComposing) {
312
+ return
313
+ }
314
+ if (filterList.value?.length) {
315
+ selectConfirm(filterList.value[keyboardSwitchIndex.value])
316
+ }
317
+ }
318
+
319
+ // 鍵盤挑選
320
+ const keyboardSwitchIndex = ref(0)
321
+ function keyboardSwitch(event, v) {
322
+ if (event.isComposing) {
323
+ return
324
+ }
325
+ if (filterList.value?.length < 1) {
326
+ keyboardSwitchIndexReset()
327
+ return
328
+ }
329
+ let idx = Number(keyboardSwitchIndex.value) + v
330
+
331
+ if (idx < 0) {
332
+ keyboardSwitchIndex.value = filterList.value.length - 1
333
+ document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
334
+ return
335
+ }
336
+ if (idx > filterList.value.length - 1) {
337
+ keyboardSwitchIndexReset()
338
+ document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
339
+ return
340
+ }
341
+ keyboardSwitchIndex.value = idx
342
+ document.getElementById(`autocomplete-select-component-opt-item_${keyboardSwitchIndex.value}`)?.scrollIntoView()
343
+ }
344
+
345
+ function keyboardSwitchIndexReset() {
346
+ keyboardSwitchIndex.value = 0
347
+ }
348
+
349
+ // 樣式
350
+ const previewInput = computed(() => {
351
+ if (selectedPreviewText.value == props.placeholder) {
352
+ return 'autocomplete-select-component-value-null'
353
+ }
354
+ return ''
355
+ })
356
+ const searchInput = computed(() => {
357
+ if (active.value) {
358
+ return 'active'
359
+ }
360
+ return ''
361
+ })
362
+ function initTrigger(event) {
363
+ if (componentContentInput.value.contains(event.target) || componentContentList.value.contains(event.target)) {
364
+ activeSelector()
365
+ } else {
366
+ leave()
367
+ }
368
+ }
369
+ onMounted(() => {
370
+ window.addEventListener("click", initTrigger);
371
+ })
372
+
373
+ onUnmounted(() => {
374
+ window.removeEventListener("click", initTrigger);
375
+ })
376
+
377
+ </script>
378
+ <style lang="scss" scoped>
379
+ * {
380
+ word-break: break-word;
381
+ overflow-wrap: break-word;
382
+ }
383
+
384
+ .autocomplete-select-component-trigger-content {
385
+ position: relative;
386
+ width: 100%;
387
+
388
+ .form-control {
389
+ background-image: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%232D4155%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e");
390
+ background-repeat: no-repeat;
391
+ background-position: right 0.75rem center;
392
+ background-size: 16px 12px;
393
+ text-align: start;
394
+ width: 100%;
395
+ }
396
+
397
+ .autocomplete-select-component-value-null {
398
+ color: var(--bs-secondary-color);
399
+ }
400
+
401
+ .autocomplete-select-component-display-selecting {
402
+ position: absolute;
403
+ top: 0;
404
+ left: 0;
405
+ z-index: 1;
406
+ white-space: nowrap;
407
+ overflow: hidden;
408
+ text-overflow: ellipsis;
409
+ padding-right: 2rem;
410
+ }
411
+
412
+ .autocomplete-select-component-keyword-filter-input {
413
+ opacity: 0;
414
+ position: relative;
415
+ padding-right: 2rem;
416
+
417
+ &.active {
418
+ opacity: 1;
419
+ z-index: 2;
420
+ }
421
+ }
422
+ }
423
+
424
+ .autocomplete-select-component-selector-content {
425
+ visibility: hidden;
426
+ display: none;
427
+ position: fixed;
428
+ z-index: 2000;
429
+ margin-top: 0.25rem;
430
+ margin-bottom: 0.25rem;
431
+ max-height: 76vh;
432
+ overflow: auto;
433
+
434
+ ul.autocomplete-select-component-opt-list {
435
+ background: #fff;
436
+ list-style: none;
437
+ border: 1px solid var(--gray-400);
438
+ border-radius: var(--bs-border-radius);
439
+ margin: 0;
440
+ padding: 0;
441
+ overflow: auto;
442
+ padding: .25rem 0;
443
+
444
+ >li {
445
+ width: 100%;
446
+ overflow: auto;
447
+ }
448
+
449
+ .autocomplete-select-component-opt-item {
450
+ background-color: transparent;
451
+ border: none;
452
+ padding: .25rem .5rem;
453
+ word-break: break-word;
454
+ width: 100%;
455
+ overflow: hidden;
456
+ text-align: start;
457
+ text-overflow: ellipsis;
458
+ color: var(--gray-600);
459
+
460
+ &.is-nothing {
461
+ color: var(--bs-danger);
462
+ }
463
+
464
+ &.is-clear {
465
+ color: var(--gray-500);
466
+ }
467
+
468
+ &:hover,
469
+ &.is-clear:hover {
470
+ background: rgba(var(--bs-primary-rgb), 1) !important;
471
+ color: #fff !important;
472
+ }
473
+
474
+ &.active {
475
+ background: rgba(var(--bs-primary-rgb), .1);
476
+ color: var(--bs-primary);
477
+ }
478
+ }
479
+ }
480
+
481
+ &.active {
482
+ display: block;
483
+ box-shadow: var(--bs-box-shadow);
484
+ visibility: visible;
485
+ z-index: 2000;
486
+ }
487
+ }
466
488
  </style>