@skyservice-developers/vue-dev-kit 1.5.9 → 1.5.10

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyservice-developers/vue-dev-kit",
3
- "version": "1.5.9",
3
+ "version": "1.5.10",
4
4
  "description": "Vue 2 and Vue 3 developer toolkit - components and helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/vue3/vue-dev-kit.cjs",
@@ -26,24 +26,49 @@
26
26
  </svg>
27
27
  </button>
28
28
 
29
- <div v-if="open" class="sky-select-dropdown">
30
- <div
31
- v-for="(option, idx) in normalizedOptions"
32
- :key="option.value"
33
- class="sky-select-option"
34
- :class="{
35
- 'sky-select-option-selected': option.value === value,
36
- 'sky-select-option-focused': idx === focusedIdx,
37
- }"
38
- @click="select(option)"
39
- @mouseenter="focusedIdx = idx"
40
- >
41
- <span>{{ option.label }}</span>
42
- <svg v-if="option.value === value" class="sky-select-check" viewBox="0 0 16 16" fill="none" aria-hidden="true">
43
- <path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
44
- </svg>
29
+ <template v-if="open">
30
+ <portal v-if="teleport" to="sky-select-portal">
31
+ <div
32
+ class="sky-select-dropdown sky-select-dropdown-teleported"
33
+ :style="dropdownStyle"
34
+ >
35
+ <div
36
+ v-for="(option, idx) in normalizedOptions"
37
+ :key="option.value"
38
+ class="sky-select-option"
39
+ :class="{
40
+ 'sky-select-option-selected': option.value === value,
41
+ 'sky-select-option-focused': idx === focusedIdx,
42
+ }"
43
+ @click="select(option)"
44
+ @mouseenter="focusedIdx = idx"
45
+ >
46
+ <span>{{ option.label }}</span>
47
+ <svg v-if="option.value === value" class="sky-select-check" viewBox="0 0 16 16" fill="none" aria-hidden="true">
48
+ <path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
49
+ </svg>
50
+ </div>
51
+ </div>
52
+ </portal>
53
+ <div v-else class="sky-select-dropdown">
54
+ <div
55
+ v-for="(option, idx) in normalizedOptions"
56
+ :key="option.value"
57
+ class="sky-select-option"
58
+ :class="{
59
+ 'sky-select-option-selected': option.value === value,
60
+ 'sky-select-option-focused': idx === focusedIdx,
61
+ }"
62
+ @click="select(option)"
63
+ @mouseenter="focusedIdx = idx"
64
+ >
65
+ <span>{{ option.label }}</span>
66
+ <svg v-if="option.value === value" class="sky-select-check" viewBox="0 0 16 16" fill="none" aria-hidden="true">
67
+ <path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
68
+ </svg>
69
+ </div>
45
70
  </div>
46
- </div>
71
+ </template>
47
72
  </div>
48
73
  </template>
49
74
 
@@ -71,11 +96,16 @@ export default {
71
96
  type: Boolean,
72
97
  default: false,
73
98
  },
99
+ teleport: {
100
+ type: Boolean,
101
+ default: false,
102
+ },
74
103
  },
75
104
  data() {
76
105
  return {
77
106
  open: false,
78
107
  focusedIdx: -1,
108
+ dropdownStyle: {},
79
109
  };
80
110
  },
81
111
  computed: {
@@ -89,6 +119,17 @@ export default {
89
119
  },
90
120
  },
91
121
  methods: {
122
+ calcDropdownStyle() {
123
+ if (!this.teleport || !this.$refs.root) return;
124
+ const rect = this.$refs.root.getBoundingClientRect();
125
+ this.dropdownStyle = {
126
+ position: 'fixed',
127
+ top: `${rect.bottom + 4}px`,
128
+ left: `${rect.left}px`,
129
+ width: `${rect.width}px`,
130
+ zIndex: 'var(--sky-select-dropdown-z-index, 9999)',
131
+ };
132
+ },
92
133
  toggle() {
93
134
  if (this.disabled) return;
94
135
  this.open ? this.close() : this.openDropdown();
@@ -96,11 +137,18 @@ export default {
96
137
  openDropdown() {
97
138
  this.open = true;
98
139
  this.focusedIdx = this.normalizedOptions.findIndex((o) => o.value === this.value);
140
+ this.calcDropdownStyle();
99
141
  this.$nextTick(() => document.addEventListener('mousedown', this.onOutsideClick));
142
+ if (this.teleport) {
143
+ window.addEventListener('scroll', this.onScrollOrResize, true);
144
+ window.addEventListener('resize', this.onScrollOrResize);
145
+ }
100
146
  },
101
147
  close() {
102
148
  this.open = false;
103
149
  document.removeEventListener('mousedown', this.onOutsideClick);
150
+ window.removeEventListener('scroll', this.onScrollOrResize, true);
151
+ window.removeEventListener('resize', this.onScrollOrResize);
104
152
  },
105
153
  select(option) {
106
154
  this.$emit('input', option.value);
@@ -109,6 +157,9 @@ export default {
109
157
  onOutsideClick(e) {
110
158
  if (!this.$refs.root.contains(e.target)) this.close();
111
159
  },
160
+ onScrollOrResize() {
161
+ this.calcDropdownStyle();
162
+ },
112
163
  onKeydown(e) {
113
164
  if (!this.open) {
114
165
  if (e.key === 'Enter' || e.key === ' ') {
@@ -133,6 +184,8 @@ export default {
133
184
  },
134
185
  beforeDestroy() {
135
186
  document.removeEventListener('mousedown', this.onOutsideClick);
187
+ window.removeEventListener('scroll', this.onScrollOrResize, true);
188
+ window.removeEventListener('resize', this.onScrollOrResize);
136
189
  },
137
190
  };
138
191
  </script>
@@ -233,6 +286,17 @@ export default {
233
286
  padding: 4px;
234
287
  }
235
288
 
289
+ .sky-select-dropdown-teleported {
290
+ position: fixed;
291
+ background: var(--sky-select-dropdown-bg, #fff);
292
+ border: var(--sky-select-dropdown-border, 1px solid #d1d5db);
293
+ border-radius: var(--sky-select-dropdown-radius, 6px);
294
+ box-shadow: var(--sky-select-dropdown-shadow, 0 4px 12px rgba(0, 0, 0, 0.1));
295
+ max-height: var(--sky-select-dropdown-max-height, 220px);
296
+ overflow-y: auto;
297
+ padding: 4px;
298
+ }
299
+
236
300
  /* Option */
237
301
  .sky-select-option {
238
302
  display: flex;
@@ -21,34 +21,94 @@
21
21
  >
22
22
  {{ selectedOption ? selectedOption.label : placeholder }}
23
23
  </span>
24
- <svg class="sky-select-chevron" viewBox="0 0 16 16" fill="none" aria-hidden="true">
25
- <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
24
+ <svg
25
+ class="sky-select-chevron"
26
+ viewBox="0 0 16 16"
27
+ fill="none"
28
+ aria-hidden="true"
29
+ >
30
+ <path
31
+ d="M4 6l4 4 4-4"
32
+ stroke="currentColor"
33
+ stroke-width="1.5"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ />
26
37
  </svg>
27
38
  </button>
28
39
 
29
- <div v-if="open" class="sky-select-dropdown">
30
- <div
31
- v-for="(option, idx) in normalizedOptions"
32
- :key="option.value"
33
- class="sky-select-option"
34
- :class="{
35
- 'sky-select-option-selected': option.value === modelValue,
36
- 'sky-select-option-focused': idx === focusedIdx,
37
- }"
38
- @click="select(option)"
39
- @mouseenter="focusedIdx = idx"
40
- >
41
- <span>{{ option.label }}</span>
42
- <svg v-if="option.value === modelValue" class="sky-select-check" viewBox="0 0 16 16" fill="none" aria-hidden="true">
43
- <path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
44
- </svg>
40
+ <template v-if="open">
41
+ <Teleport v-if="teleport" to="body">
42
+ <div
43
+ class="sky-select-dropdown sky-select-dropdown-teleported"
44
+ :style="dropdownStyle"
45
+ >
46
+ <div
47
+ v-for="(option, idx) in normalizedOptions"
48
+ :key="option.value"
49
+ class="sky-select-option"
50
+ :class="{
51
+ 'sky-select-option-selected': option.value === modelValue,
52
+ 'sky-select-option-focused': idx === focusedIdx,
53
+ }"
54
+ @click="select(option)"
55
+ @mouseenter="focusedIdx = idx"
56
+ >
57
+ <span>{{ option.label }}</span>
58
+ <svg
59
+ v-if="option.value === modelValue"
60
+ class="sky-select-check"
61
+ viewBox="0 0 16 16"
62
+ fill="none"
63
+ aria-hidden="true"
64
+ >
65
+ <path
66
+ d="M3 8l4 4 6-6"
67
+ stroke="currentColor"
68
+ stroke-width="1.5"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ />
72
+ </svg>
73
+ </div>
74
+ </div>
75
+ </Teleport>
76
+ <div v-else class="sky-select-dropdown">
77
+ <div
78
+ v-for="(option, idx) in normalizedOptions"
79
+ :key="option.value"
80
+ class="sky-select-option"
81
+ :class="{
82
+ 'sky-select-option-selected': option.value === modelValue,
83
+ 'sky-select-option-focused': idx === focusedIdx,
84
+ }"
85
+ @click="select(option)"
86
+ @mouseenter="focusedIdx = idx"
87
+ >
88
+ <span>{{ option.label }}</span>
89
+ <svg
90
+ v-if="option.value === modelValue"
91
+ class="sky-select-check"
92
+ viewBox="0 0 16 16"
93
+ fill="none"
94
+ aria-hidden="true"
95
+ >
96
+ <path
97
+ d="M3 8l4 4 6-6"
98
+ stroke="currentColor"
99
+ stroke-width="1.5"
100
+ stroke-linecap="round"
101
+ stroke-linejoin="round"
102
+ />
103
+ </svg>
104
+ </div>
45
105
  </div>
46
- </div>
106
+ </template>
47
107
  </div>
48
108
  </template>
49
109
 
50
110
  <script setup>
51
- import { ref, computed, onBeforeUnmount } from 'vue';
111
+ import { ref, computed, onBeforeUnmount } from "vue";
52
112
 
53
113
  const props = defineProps({
54
114
  modelValue: {
@@ -60,7 +120,7 @@ const props = defineProps({
60
120
  },
61
121
  placeholder: {
62
122
  type: String,
63
- default: '',
123
+ default: "",
64
124
  },
65
125
  disabled: {
66
126
  type: Boolean,
@@ -70,24 +130,42 @@ const props = defineProps({
70
130
  type: Boolean,
71
131
  default: false,
72
132
  },
133
+ teleport: {
134
+ type: Boolean,
135
+ default: false,
136
+ },
73
137
  });
74
138
 
75
- const emit = defineEmits(['update:modelValue']);
139
+ const emit = defineEmits(["update:modelValue"]);
76
140
 
77
141
  const root = ref(null);
78
142
  const open = ref(false);
79
143
  const focusedIdx = ref(-1);
144
+ const dropdownStyle = ref({});
80
145
 
81
146
  const normalizedOptions = computed(() =>
82
147
  props.options.map((opt) =>
83
- typeof opt === 'string' ? { label: opt, value: opt } : opt
84
- )
148
+ typeof opt === "string" ? { label: opt, value: opt } : opt,
149
+ ),
85
150
  );
86
151
 
87
- const selectedOption = computed(() =>
88
- normalizedOptions.value.find((o) => o.value === props.modelValue) ?? null
152
+ const selectedOption = computed(
153
+ () =>
154
+ normalizedOptions.value.find((o) => o.value === props.modelValue) ?? null,
89
155
  );
90
156
 
157
+ function calcDropdownStyle() {
158
+ if (!props.teleport || !root.value) return;
159
+ const rect = root.value.getBoundingClientRect();
160
+ dropdownStyle.value = {
161
+ position: "fixed",
162
+ top: `${rect.bottom + 4}px`,
163
+ left: `${rect.left}px`,
164
+ width: `${rect.width}px`,
165
+ zIndex: "var(--sky-select-dropdown-z-index, 9999)",
166
+ };
167
+ }
168
+
91
169
  function toggle() {
92
170
  if (props.disabled) return;
93
171
  open.value ? close() : openDropdown();
@@ -95,17 +173,26 @@ function toggle() {
95
173
 
96
174
  function openDropdown() {
97
175
  open.value = true;
98
- focusedIdx.value = normalizedOptions.value.findIndex((o) => o.value === props.modelValue);
99
- document.addEventListener('mousedown', onOutsideClick);
176
+ focusedIdx.value = normalizedOptions.value.findIndex(
177
+ (o) => o.value === props.modelValue,
178
+ );
179
+ calcDropdownStyle();
180
+ document.addEventListener("mousedown", onOutsideClick);
181
+ if (props.teleport) {
182
+ window.addEventListener("scroll", onScrollOrResize, true);
183
+ window.addEventListener("resize", onScrollOrResize);
184
+ }
100
185
  }
101
186
 
102
187
  function close() {
103
188
  open.value = false;
104
- document.removeEventListener('mousedown', onOutsideClick);
189
+ document.removeEventListener("mousedown", onOutsideClick);
190
+ window.removeEventListener("scroll", onScrollOrResize, true);
191
+ window.removeEventListener("resize", onScrollOrResize);
105
192
  }
106
193
 
107
194
  function select(option) {
108
- emit('update:modelValue', option.value);
195
+ emit("update:modelValue", option.value);
109
196
  close();
110
197
  }
111
198
 
@@ -113,30 +200,40 @@ function onOutsideClick(e) {
113
200
  if (!root.value?.contains(e.target)) close();
114
201
  }
115
202
 
203
+ function onScrollOrResize() {
204
+ calcDropdownStyle();
205
+ }
206
+
116
207
  function onKeydown(e) {
117
208
  if (!open.value) {
118
- if (e.key === 'Enter' || e.key === ' ') {
209
+ if (e.key === "Enter" || e.key === " ") {
119
210
  e.preventDefault();
120
211
  openDropdown();
121
212
  }
122
213
  return;
123
214
  }
124
- if (e.key === 'Escape') {
215
+ if (e.key === "Escape") {
125
216
  close();
126
- } else if (e.key === 'ArrowDown') {
217
+ } else if (e.key === "ArrowDown") {
127
218
  e.preventDefault();
128
- focusedIdx.value = Math.min(focusedIdx.value + 1, normalizedOptions.value.length - 1);
129
- } else if (e.key === 'ArrowUp') {
219
+ focusedIdx.value = Math.min(
220
+ focusedIdx.value + 1,
221
+ normalizedOptions.value.length - 1,
222
+ );
223
+ } else if (e.key === "ArrowUp") {
130
224
  e.preventDefault();
131
225
  focusedIdx.value = Math.max(focusedIdx.value - 1, 0);
132
- } else if (e.key === 'Enter') {
226
+ } else if (e.key === "Enter") {
133
227
  e.preventDefault();
134
- if (focusedIdx.value >= 0) select(normalizedOptions.value[focusedIdx.value]);
228
+ if (focusedIdx.value >= 0)
229
+ select(normalizedOptions.value[focusedIdx.value]);
135
230
  }
136
231
  }
137
232
 
138
233
  onBeforeUnmount(() => {
139
- document.removeEventListener('mousedown', onOutsideClick);
234
+ document.removeEventListener("mousedown", onOutsideClick);
235
+ window.removeEventListener("scroll", onScrollOrResize, true);
236
+ window.removeEventListener("resize", onScrollOrResize);
140
237
  });
141
238
  </script>
142
239
 
@@ -236,6 +333,17 @@ onBeforeUnmount(() => {
236
333
  padding: 4px;
237
334
  }
238
335
 
336
+ .sky-select-dropdown-teleported {
337
+ position: fixed;
338
+ background: var(--sky-select-dropdown-bg, #fff);
339
+ border: var(--sky-select-dropdown-border, 1px solid #d1d5db);
340
+ border-radius: var(--sky-select-dropdown-radius, 6px);
341
+ box-shadow: var(--sky-select-dropdown-shadow, 0 4px 12px rgba(0, 0, 0, 0.1));
342
+ max-height: var(--sky-select-dropdown-max-height, 220px);
343
+ overflow-y: auto;
344
+ padding: 4px;
345
+ }
346
+
239
347
  /* Option */
240
348
  .sky-select-option {
241
349
  display: flex;