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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-excel-grid",
3
- "version": "1.1.13",
3
+ "version": "1.1.16",
4
4
  "description": "Excel-style grid component for Vue 2.6.14 with virtual scrolling",
5
5
  "main": "dist/vue2-excel-grid.umd.js",
6
6
  "module": "dist/vue2-excel-grid.esm.js",
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <label class="eg-checkbox" :class="{ 'eg-checkbox--disabled': disabled }">
3
+ <input
4
+ type="checkbox"
5
+ class="eg-checkbox-input"
6
+ :checked="isChecked"
7
+ :disabled="disabled"
8
+ @change="onChange"
9
+ />
10
+ <span class="eg-checkbox-box"></span>
11
+ </label>
12
+ </template>
13
+
14
+ <script>
15
+ export default {
16
+ name: 'EgCheckbox',
17
+ model: { prop: 'value', event: 'input' },
18
+ props: {
19
+ value: { default: false },
20
+ trueValue: { default: true },
21
+ falseValue: { default: false },
22
+ disabled: { type: Boolean, default: false }
23
+ },
24
+ computed: {
25
+ isChecked() {
26
+ return this.value === this.trueValue
27
+ }
28
+ },
29
+ methods: {
30
+ onChange(e) {
31
+ const val = e.target.checked ? this.trueValue : this.falseValue
32
+ this.$emit('input', val)
33
+ this.$emit('change', val)
34
+ }
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <style>
40
+ .eg-checkbox {
41
+ position: relative;
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 20px;
46
+ height: 20px;
47
+ cursor: pointer;
48
+ }
49
+ .eg-checkbox-input {
50
+ position: absolute;
51
+ inset: 0;
52
+ width: 100%;
53
+ height: 100%;
54
+ margin: 0;
55
+ opacity: 0;
56
+ cursor: pointer;
57
+ }
58
+ .eg-checkbox-box {
59
+ position: relative;
60
+ display: block;
61
+ width: 19px;
62
+ height: 19px;
63
+ box-sizing: border-box;
64
+ background-color: #fff;
65
+ background-repeat: no-repeat;
66
+ background-position: center;
67
+ background-size: 0 0;
68
+ border: 1.5px solid #c2c8d1;
69
+ border-radius: 4px;
70
+ transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;
71
+ }
72
+ .eg-checkbox:hover .eg-checkbox-box {
73
+ border-color: #1a73e8;
74
+ }
75
+ .eg-checkbox-input:checked + .eg-checkbox-box {
76
+ background-color: #1a73e8;
77
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==");
78
+ background-size: 70% 70%;
79
+ border-color: #1a73e8;
80
+ }
81
+ .eg-checkbox-input:focus-visible + .eg-checkbox-box {
82
+ box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);
83
+ }
84
+ .eg-checkbox--disabled,
85
+ .eg-checkbox--disabled .eg-checkbox-input {
86
+ cursor: default;
87
+ }
88
+ .eg-checkbox-input:disabled + .eg-checkbox-box {
89
+ background-color: #f0f1f3;
90
+ border-color: #dadde2;
91
+ }
92
+ .eg-checkbox-input:disabled:checked + .eg-checkbox-box {
93
+ background-color: #b9c2ce;
94
+ border-color: #b9c2ce;
95
+ }
96
+ </style>
@@ -0,0 +1,377 @@
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 v-if="selectedOpt && selectedOpt.color" class="eg-select-dot" :style="{ background: selectedOpt.color }"></span>
15
+ <span class="eg-select-trigger-label" :class="{ 'eg-select-placeholder': !selectedOpt }">
16
+ {{ selectedOpt ? selectedOpt.label : placeholder }}
17
+ </span>
18
+ <span class="eg-select-caret">▾</span>
19
+ </div>
20
+
21
+ <!-- Options panel in grid dropdown-only mode (root IS the dropdown panel) -->
22
+ <template v-if="dropdownOnly">
23
+ <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
24
+ <input
25
+ ref="searchInput"
26
+ class="eg-select-search-input"
27
+ :value="searchQuery"
28
+ placeholder="Qidirish..."
29
+ autocomplete="off"
30
+ autocorrect="off"
31
+ autocapitalize="off"
32
+ spellcheck="false"
33
+ @input="onSearch"
34
+ @keydown.stop="onSearchKey"
35
+ />
36
+ </div>
37
+ <div
38
+ v-for="(opt, idx) in filteredOptions"
39
+ :key="String(opt.value)"
40
+ class="eg-select-option"
41
+ :class="{
42
+ 'eg-select-option--active': String(opt.value) === String(value),
43
+ 'eg-select-option--highlighted': idx === highlight
44
+ }"
45
+ @mousedown.prevent="pickOption(opt)"
46
+ @mouseover="highlight = idx"
47
+ >
48
+ <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
49
+ {{ opt.label }}
50
+ </div>
51
+ </template>
52
+
53
+ <!-- Options panel in standalone mode (floating dropdown below trigger) -->
54
+ <div
55
+ v-else-if="open"
56
+ class="eg-select-dropdown eg-select-dropdown--floating"
57
+ @mousedown.stop.prevent
58
+ >
59
+ <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
60
+ <input
61
+ ref="searchInput"
62
+ class="eg-select-search-input"
63
+ :value="searchQuery"
64
+ placeholder="Qidirish..."
65
+ autocomplete="off"
66
+ autocorrect="off"
67
+ autocapitalize="off"
68
+ spellcheck="false"
69
+ @input="onSearch"
70
+ @keydown.stop="onSearchKey"
71
+ />
72
+ </div>
73
+ <div
74
+ v-for="(opt, idx) in filteredOptions"
75
+ :key="String(opt.value)"
76
+ class="eg-select-option"
77
+ :class="{
78
+ 'eg-select-option--active': String(opt.value) === String(value),
79
+ 'eg-select-option--highlighted': idx === highlight
80
+ }"
81
+ @mousedown.prevent="pickOption(opt)"
82
+ @mouseover="highlight = idx"
83
+ >
84
+ <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
85
+ {{ opt.label }}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </template>
90
+
91
+ <script>
92
+ export default {
93
+ name: 'EgSelect',
94
+ model: { prop: 'value', event: 'input' },
95
+ props: {
96
+ value: { default: null },
97
+ options: { type: Array, default: () => [] },
98
+ enableSearch: { type: Boolean, default: false },
99
+ placeholder: { type: String, default: '' },
100
+ disabled: { type: Boolean, default: false },
101
+ dropdownOnly: { type: Boolean, default: false },
102
+ triggerRect: { type: Object, default: null }
103
+ },
104
+ data() {
105
+ return {
106
+ open: false,
107
+ searchQuery: '',
108
+ highlight: -1
109
+ }
110
+ },
111
+ computed: {
112
+ normalizedOptions() {
113
+ return (this.options || []).map(o =>
114
+ typeof o === 'string'
115
+ ? { value: o, label: o }
116
+ : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }
117
+ )
118
+ },
119
+ filteredOptions() {
120
+ if (!this.enableSearch || !this.searchQuery.trim()) return this.normalizedOptions
121
+ const q = this.searchQuery.trim().toLowerCase()
122
+ return this.normalizedOptions.filter(o => String(o.label || '').toLowerCase().includes(q))
123
+ },
124
+ selectedOpt() {
125
+ const v = String(this.value == null ? '' : this.value)
126
+ return this.normalizedOptions.find(o => String(o.value) === v) || null
127
+ },
128
+ overlayStyle() {
129
+ if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
130
+ const rect = this.triggerRect
131
+ const searchH = this.enableSearch ? 42 : 0
132
+ const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320)
133
+ const spaceBelow = window.innerHeight - rect.bottom
134
+ const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH
135
+ return {
136
+ position: 'fixed',
137
+ top: top + 'px',
138
+ left: rect.left + 'px',
139
+ minWidth: Math.max(rect.width, 120) + 'px',
140
+ maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
141
+ zIndex: 9999
142
+ }
143
+ }
144
+ },
145
+ watch: {
146
+ open(val) {
147
+ if (val && this.enableSearch) {
148
+ this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
149
+ }
150
+ }
151
+ },
152
+ mounted() {
153
+ if (this.dropdownOnly) {
154
+ // Portal: move to document.body so position:fixed is always relative to viewport,
155
+ // regardless of any CSS transform on ancestor elements in the host project.
156
+ // Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.
157
+ if (this.$el && this.$el.parentNode) {
158
+ document.body.appendChild(this.$el)
159
+ }
160
+ const v = String(this.value == null ? '' : this.value)
161
+ this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)
162
+ if (this.enableSearch) {
163
+ this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
164
+ }
165
+ } else {
166
+ document.addEventListener('click', this._onDocClick)
167
+ }
168
+ },
169
+ beforeDestroy() {
170
+ if (!this.dropdownOnly) {
171
+ document.removeEventListener('click', this._onDocClick)
172
+ }
173
+ },
174
+ methods: {
175
+ toggle() {
176
+ if (this.disabled) return
177
+ this.open = !this.open
178
+ if (this.open) {
179
+ const v = String(this.value == null ? '' : this.value)
180
+ this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)
181
+ this.searchQuery = ''
182
+ }
183
+ },
184
+ pickOption(opt) {
185
+ this.$emit('input', opt.value)
186
+ this.$emit('select', { value: opt.value, label: opt.label })
187
+ if (!this.dropdownOnly) {
188
+ this.open = false
189
+ this.searchQuery = ''
190
+ this.highlight = -1
191
+ }
192
+ },
193
+ onSearch(e) {
194
+ this.searchQuery = e.target.value
195
+ this.highlight = 0
196
+ },
197
+ onSearchKey(e) {
198
+ const key = e.key
199
+ if (key === 'Escape') {
200
+ e.preventDefault()
201
+ if (this.dropdownOnly) this.$emit('close')
202
+ else { this.open = false; this.searchQuery = '' }
203
+ } else if (key === 'ArrowDown') {
204
+ e.preventDefault()
205
+ this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1)
206
+ } else if (key === 'ArrowUp') {
207
+ e.preventDefault()
208
+ this.highlight = Math.max((this.highlight || 0) - 1, 0)
209
+ } else if (key === 'Enter') {
210
+ e.preventDefault()
211
+ const opt = this.filteredOptions[this.highlight]
212
+ if (opt) this.pickOption(opt)
213
+ }
214
+ },
215
+ handleKey(e) {
216
+ const key = e.key
217
+ if (key === 'Escape') {
218
+ e.preventDefault()
219
+ this.$emit('close')
220
+ } else if (key === 'Enter' || key === ' ') {
221
+ e.preventDefault()
222
+ const opt = this.filteredOptions[this.highlight]
223
+ if (opt) this.pickOption(opt)
224
+ else this.$emit('close')
225
+ } else if (key === 'ArrowDown') {
226
+ e.preventDefault()
227
+ this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1)
228
+ } else if (key === 'ArrowUp') {
229
+ e.preventDefault()
230
+ this.highlight = Math.max((this.highlight || 0) - 1, 0)
231
+ }
232
+ },
233
+ onRootMouseDown(e) {
234
+ if (this.dropdownOnly) e.preventDefault()
235
+ },
236
+ _onDocClick(e) {
237
+ if (!this.open) return
238
+ if (this.$el && this.$el.contains(e.target)) return
239
+ this.open = false
240
+ }
241
+ }
242
+ }
243
+ </script>
244
+
245
+ <style>
246
+ /* Standalone wrapper */
247
+ .eg-select-wrapper {
248
+ position: relative;
249
+ display: inline-block;
250
+ min-width: 120px;
251
+ font-size: 13px;
252
+ font-family: inherit;
253
+ }
254
+ .eg-select-trigger {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 6px;
258
+ width: 100%;
259
+ min-height: 30px;
260
+ padding: 4px 8px;
261
+ box-sizing: border-box;
262
+ border: 1px solid #d1d5db;
263
+ border-radius: 4px;
264
+ background: #fff;
265
+ cursor: pointer;
266
+ user-select: none;
267
+ color: #333;
268
+ transition: border-color .15s ease, box-shadow .15s ease;
269
+ }
270
+ .eg-select-trigger:hover {
271
+ border-color: #1a73e8;
272
+ }
273
+ .eg-select-trigger--open {
274
+ border-color: #1a73e8;
275
+ box-shadow: 0 0 0 2px rgba(26,115,232,0.15);
276
+ }
277
+ .eg-select-trigger--disabled {
278
+ background: #f5f5f5;
279
+ cursor: default;
280
+ color: #aaa;
281
+ }
282
+ .eg-select-trigger-label {
283
+ flex: 1;
284
+ overflow: hidden;
285
+ text-overflow: ellipsis;
286
+ white-space: nowrap;
287
+ }
288
+ .eg-select-placeholder {
289
+ color: #aaa;
290
+ }
291
+ .eg-select-caret {
292
+ flex-shrink: 0;
293
+ color: #888;
294
+ font-size: 12px;
295
+ }
296
+
297
+ /* Dropdown panel */
298
+ .eg-select-dropdown {
299
+ background: #fff;
300
+ border: 1px solid #d0d0d0;
301
+ border-radius: 4px;
302
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15);
303
+ overflow-y: auto;
304
+ max-height: 320px;
305
+ padding: 4px 0;
306
+ }
307
+ .eg-select-dropdown--floating {
308
+ position: absolute;
309
+ top: 100%;
310
+ left: 0;
311
+ min-width: 100%;
312
+ z-index: 999;
313
+ margin-top: 2px;
314
+ }
315
+
316
+ /* Search input inside dropdown */
317
+ .eg-select-search {
318
+ padding: 6px 8px;
319
+ border-bottom: 1px solid #ebebeb;
320
+ position: sticky;
321
+ top: 0;
322
+ background: #fff;
323
+ z-index: 1;
324
+ }
325
+ .eg-select-search-input {
326
+ width: 100%;
327
+ box-sizing: border-box;
328
+ border: 1px solid #d1d5db;
329
+ border-radius: 4px;
330
+ padding: 4px 8px;
331
+ font-size: 13px;
332
+ outline: none;
333
+ color: #333;
334
+ background: #fff;
335
+ font-family: inherit;
336
+ }
337
+ .eg-select-search-input:focus {
338
+ border-color: #1a73e8;
339
+ box-shadow: 0 0 0 2px rgba(26,115,232,0.15);
340
+ }
341
+
342
+ /* Option rows */
343
+ .eg-select-option {
344
+ display: flex;
345
+ align-items: flex-start;
346
+ gap: 8px;
347
+ padding: 6px 12px;
348
+ font-size: 13px;
349
+ cursor: pointer;
350
+ white-space: normal;
351
+ word-break: break-word;
352
+ user-select: none;
353
+ color: #333;
354
+ }
355
+ .eg-select-option:hover,
356
+ .eg-select-option--highlighted {
357
+ background: #f0f4ff;
358
+ }
359
+ .eg-select-option--active {
360
+ font-weight: 600;
361
+ color: #1a73e8;
362
+ }
363
+ .eg-select-option--active.eg-select-option--highlighted,
364
+ .eg-select-option--active:hover {
365
+ background: #e8f0fe;
366
+ }
367
+
368
+ /* Color dot */
369
+ .eg-select-dot {
370
+ width: 9px;
371
+ height: 9px;
372
+ border-radius: 50%;
373
+ flex-shrink: 0;
374
+ display: inline-block;
375
+ margin-top: 3px;
376
+ }
377
+ </style>