vue2-excel-grid 1.1.13 → 1.1.16

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.
@@ -0,0 +1,357 @@
1
+ <template>
2
+ <div
3
+ :class="dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'"
4
+ :style="dropdownOnly ? overlayStyle : null"
5
+ @mousedown="onRootMouseDown"
6
+ >
7
+ <!-- Standalone mode: trigger button -->
8
+ <div
9
+ v-if="!dropdownOnly"
10
+ class="eg-select-trigger"
11
+ :class="{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }"
12
+ @click.stop="toggle"
13
+ >
14
+ <span class="eg-select-trigger-label" :class="{ 'eg-select-placeholder': selectedLabel == null }">
15
+ {{ selectedLabel != null ? selectedLabel : placeholder }}
16
+ </span>
17
+ <span class="eg-select-caret">▾</span>
18
+ </div>
19
+
20
+ <!-- Options panel in grid dropdown-only mode -->
21
+ <template v-if="dropdownOnly">
22
+ <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
23
+ <input
24
+ ref="searchInput"
25
+ class="eg-select-search-input"
26
+ :value="searchQuery"
27
+ placeholder="Qidirish..."
28
+ autocomplete="off"
29
+ autocorrect="off"
30
+ autocapitalize="off"
31
+ spellcheck="false"
32
+ @input="onSearch"
33
+ @keydown.stop="onSearchKey"
34
+ />
35
+ </div>
36
+ <div
37
+ v-for="(opt, idx) in filteredOptions"
38
+ :key="opt.id != null ? opt.id : String(opt.value)"
39
+ class="eg-select-option"
40
+ :class="{
41
+ 'eg-select-option--active': String(opt.value) === String(value),
42
+ 'eg-select-option--highlighted': idx === highlight
43
+ }"
44
+ :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
45
+ @mousedown.prevent="pickOption(opt)"
46
+ @mouseover="highlight = idx"
47
+ >
48
+ <span
49
+ v-if="!searchQuery"
50
+ class="eg-tree-toggle"
51
+ :class="{ 'eg-tree-toggle--leaf': !opt.hasChildren }"
52
+ @mousedown.prevent.stop="toggleNode(idx)"
53
+ >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>
54
+ <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
55
+ {{ opt.label }}
56
+ </div>
57
+ </template>
58
+
59
+ <!-- Options panel in standalone mode -->
60
+ <div
61
+ v-else-if="open"
62
+ class="eg-select-dropdown eg-select-dropdown--floating"
63
+ @mousedown.stop.prevent
64
+ >
65
+ <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
66
+ <input
67
+ ref="searchInput"
68
+ class="eg-select-search-input"
69
+ :value="searchQuery"
70
+ placeholder="Qidirish..."
71
+ autocomplete="off"
72
+ autocorrect="off"
73
+ autocapitalize="off"
74
+ spellcheck="false"
75
+ @input="onSearch"
76
+ @keydown.stop="onSearchKey"
77
+ />
78
+ </div>
79
+ <div
80
+ v-for="(opt, idx) in filteredOptions"
81
+ :key="opt.id != null ? opt.id : String(opt.value)"
82
+ class="eg-select-option"
83
+ :class="{
84
+ 'eg-select-option--active': String(opt.value) === String(value),
85
+ 'eg-select-option--highlighted': idx === highlight
86
+ }"
87
+ :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
88
+ @mousedown.prevent="pickOption(opt)"
89
+ @mouseover="highlight = idx"
90
+ >
91
+ <span
92
+ v-if="!searchQuery"
93
+ class="eg-tree-toggle"
94
+ :class="{ 'eg-tree-toggle--leaf': !opt.hasChildren }"
95
+ @mousedown.prevent.stop="toggleNode(idx)"
96
+ >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>
97
+ <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
98
+ {{ opt.label }}
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </template>
103
+
104
+ <script>
105
+ export default {
106
+ name: 'EgTreeSelect',
107
+ model: { prop: 'value', event: 'input' },
108
+ props: {
109
+ value: { default: null },
110
+ options: { type: Array, default: () => [] },
111
+ valueKey: { type: String, default: 'value' },
112
+ labelKey: { type: String, default: 'label' },
113
+ childrenKey: { type: String, default: 'children' },
114
+ enableSearch: { type: Boolean, default: false },
115
+ placeholder: { type: String, default: '' },
116
+ disabled: { type: Boolean, default: false },
117
+ dropdownOnly: { type: Boolean, default: false },
118
+ triggerRect: { type: Object, default: null }
119
+ },
120
+ data() {
121
+ return {
122
+ open: false,
123
+ searchQuery: '',
124
+ highlight: -1,
125
+ expandedMap: {}
126
+ }
127
+ },
128
+ computed: {
129
+ keys() {
130
+ return { valueKey: this.valueKey, labelKey: this.labelKey, childrenKey: this.childrenKey }
131
+ },
132
+ flatOptions() {
133
+ return this._treeFlatten(this.options || [], this.keys, this.expandedMap, 0, [])
134
+ },
135
+ allFlatOptions() {
136
+ if (!this.enableSearch) return null
137
+ return this._treeFlattenAll(this.options || [], this.keys)
138
+ },
139
+ filteredOptions() {
140
+ if (!this.enableSearch || !this.searchQuery.trim()) return this.flatOptions
141
+ const q = this.searchQuery.trim().toLowerCase()
142
+ const pool = this.allFlatOptions || this.flatOptions
143
+ return pool.filter(o => String(o.label || '').toLowerCase().includes(q))
144
+ },
145
+ selectedLabel() {
146
+ const v = String(this.value == null ? '' : this.value)
147
+ if (!v) return null
148
+ const found = this._treeFindLabel(this.options || [], this.keys, v)
149
+ return found != null ? found : null
150
+ },
151
+ overlayStyle() {
152
+ if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
153
+ const rect = this.triggerRect
154
+ const searchH = this.enableSearch ? 42 : 0
155
+ const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320)
156
+ const spaceBelow = window.innerHeight - rect.bottom
157
+ const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH
158
+ return {
159
+ position: 'fixed',
160
+ top: top + 'px',
161
+ left: rect.left + 'px',
162
+ minWidth: Math.max(rect.width, 120) + 'px',
163
+ maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
164
+ zIndex: 9999
165
+ }
166
+ }
167
+ },
168
+ watch: {
169
+ open(val) {
170
+ if (val && this.enableSearch) {
171
+ this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
172
+ }
173
+ }
174
+ },
175
+ mounted() {
176
+ if (this.dropdownOnly) {
177
+ // Portal: move to document.body so position:fixed is always relative to viewport,
178
+ // regardless of any CSS transform on ancestor elements in the host project.
179
+ // Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.
180
+ if (this.$el && this.$el.parentNode) {
181
+ document.body.appendChild(this.$el)
182
+ }
183
+ this._initExpanded()
184
+ if (this.enableSearch) {
185
+ this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
186
+ }
187
+ } else {
188
+ document.addEventListener('click', this._onDocClick)
189
+ }
190
+ },
191
+ beforeDestroy() {
192
+ if (!this.dropdownOnly) {
193
+ document.removeEventListener('click', this._onDocClick)
194
+ }
195
+ },
196
+ methods: {
197
+ _initExpanded() {
198
+ const v = String(this.value == null ? '' : this.value)
199
+ const path = this._treeFindPath(this.options || [], this.keys, v, []) || []
200
+ const expandedMap = {}
201
+ path.forEach(id => { expandedMap[id] = true })
202
+ this.expandedMap = expandedMap
203
+ const flat = this.flatOptions
204
+ this.highlight = flat.findIndex(o => String(o.value) === v)
205
+ },
206
+ toggle() {
207
+ if (this.disabled) return
208
+ this.open = !this.open
209
+ if (this.open) {
210
+ this.searchQuery = ''
211
+ this._initExpanded()
212
+ }
213
+ },
214
+ pickOption(opt) {
215
+ this.$emit('input', opt.value)
216
+ this.$emit('select', { value: opt.value, label: opt.label })
217
+ if (!this.dropdownOnly) {
218
+ this.open = false
219
+ this.searchQuery = ''
220
+ this.highlight = -1
221
+ }
222
+ },
223
+ toggleNode(idx) {
224
+ const opt = this.flatOptions[idx]
225
+ if (!opt || !opt.hasChildren) return
226
+ const next = { ...this.expandedMap }
227
+ if (next[opt.id]) delete next[opt.id]
228
+ else next[opt.id] = true
229
+ this.expandedMap = next
230
+ const newFlat = this.flatOptions
231
+ const newIdx = newFlat.findIndex(o => o.id === opt.id)
232
+ this.highlight = newIdx >= 0 ? newIdx : this.highlight
233
+ },
234
+ onSearch(e) {
235
+ this.searchQuery = e.target.value
236
+ this.highlight = 0
237
+ },
238
+ onSearchKey(e) {
239
+ const key = e.key
240
+ if (key === 'Escape') {
241
+ e.preventDefault()
242
+ if (this.dropdownOnly) this.$emit('close')
243
+ else { this.open = false; this.searchQuery = '' }
244
+ } else if (key === 'ArrowDown') {
245
+ e.preventDefault()
246
+ this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1)
247
+ } else if (key === 'ArrowUp') {
248
+ e.preventDefault()
249
+ this.highlight = Math.max((this.highlight || 0) - 1, 0)
250
+ } else if (key === 'Enter') {
251
+ e.preventDefault()
252
+ const opt = this.filteredOptions[this.highlight]
253
+ if (opt) this.pickOption(opt)
254
+ }
255
+ },
256
+ handleKey(e) {
257
+ const key = e.key
258
+ if (key === 'Escape') {
259
+ e.preventDefault()
260
+ this.$emit('close')
261
+ } else if (key === 'Enter' || key === ' ') {
262
+ e.preventDefault()
263
+ const opt = this.filteredOptions[this.highlight]
264
+ if (opt) this.pickOption(opt)
265
+ else this.$emit('close')
266
+ } else if (key === 'ArrowDown') {
267
+ e.preventDefault()
268
+ this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1)
269
+ } else if (key === 'ArrowUp') {
270
+ e.preventDefault()
271
+ this.highlight = Math.max((this.highlight || 0) - 1, 0)
272
+ } else if (!this.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {
273
+ e.preventDefault()
274
+ const opt = this.flatOptions[this.highlight]
275
+ if (opt && opt.hasChildren) {
276
+ if (key === 'ArrowRight' && !opt.expanded) this.toggleNode(this.highlight)
277
+ if (key === 'ArrowLeft' && opt.expanded) this.toggleNode(this.highlight)
278
+ }
279
+ }
280
+ },
281
+ onRootMouseDown(e) {
282
+ if (this.dropdownOnly) e.preventDefault()
283
+ },
284
+ _onDocClick(e) {
285
+ if (!this.open) return
286
+ if (this.$el && this.$el.contains(e.target)) return
287
+ this.open = false
288
+ },
289
+ _treeFlatten(nodes, keys, expandedMap, depth, pathArr) {
290
+ let out = []
291
+ ;(nodes || []).forEach((node, idx) => {
292
+ const path = pathArr.concat(idx)
293
+ const id = path.join('-')
294
+ const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
295
+ const hasChildren = children.length > 0
296
+ const expanded = hasChildren && !!expandedMap[id]
297
+ out.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id, depth, hasChildren, expanded })
298
+ if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))
299
+ })
300
+ return out
301
+ },
302
+ _treeFlattenAll(nodes, keys) {
303
+ const result = []
304
+ const walk = (list) => {
305
+ ;(list || []).forEach(node => {
306
+ const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
307
+ result.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id: String(node[keys.valueKey]), depth: 0, hasChildren: false, expanded: false })
308
+ if (children.length) walk(children)
309
+ })
310
+ }
311
+ walk(nodes)
312
+ return result
313
+ },
314
+ _treeFindLabel(nodes, keys, value) {
315
+ for (const node of (nodes || [])) {
316
+ if (String(node[keys.valueKey]) === value) {
317
+ return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
318
+ }
319
+ const found = this._treeFindLabel(node[keys.childrenKey], keys, value)
320
+ if (found != null) return found
321
+ }
322
+ return null
323
+ },
324
+ _treeFindPath(nodes, keys, value, pathArr) {
325
+ for (let i = 0; i < (nodes || []).length; i++) {
326
+ const node = nodes[i]
327
+ const path = pathArr.concat(i)
328
+ if (String(node[keys.valueKey]) === value) return [path.join('-')]
329
+ const children = node[keys.childrenKey]
330
+ if (Array.isArray(children)) {
331
+ const sub = this._treeFindPath(children, keys, value, path)
332
+ if (sub) return [path.join('-')].concat(sub)
333
+ }
334
+ }
335
+ return null
336
+ }
337
+ }
338
+ }
339
+ </script>
340
+
341
+ <style>
342
+ /* Tree toggle */
343
+ .eg-tree-toggle {
344
+ display: inline-flex;
345
+ align-items: center;
346
+ justify-content: center;
347
+ width: 14px;
348
+ height: 14px;
349
+ flex-shrink: 0;
350
+ font-size: 10px;
351
+ color: #888;
352
+ margin-top: 1px;
353
+ }
354
+ .eg-tree-toggle--leaf {
355
+ visibility: hidden;
356
+ }
357
+ </style>