vue2-excel-grid 1.3.0 → 1.4.0

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.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Excel-style grid component for Vue 2.6.14 with virtual scrolling",
5
5
  "license": "MIT",
6
6
  "author": "Fozilbek Raimberdiyev",
@@ -3,6 +3,7 @@
3
3
  :class="dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'"
4
4
  :style="dropdownOnly ? overlayStyle : null"
5
5
  @mousedown="onRootMouseDown"
6
+ @scroll.passive="dropdownOnly && _hideTooltip()"
6
7
  >
7
8
  <!-- Standalone mode: trigger button -->
8
9
  <div
@@ -15,7 +16,8 @@
15
16
  <span
16
17
  class="eg-select-trigger-label"
17
18
  :class="{ 'eg-select-placeholder': !selectedOpt }"
18
- :title="selectedOpt ? selectedOpt.tooltip : null"
19
+ @mouseenter="_showTooltip(selectedOpt && selectedOpt.tooltip, $event.currentTarget)"
20
+ @mouseleave="_hideTooltip"
19
21
  >
20
22
  {{ selectedOpt ? selectedOpt.label : placeholder }}
21
23
  </span>
@@ -46,9 +48,10 @@
46
48
  'eg-select-option--active': String(opt.value) === String(value),
47
49
  'eg-select-option--highlighted': idx === highlight
48
50
  }"
49
- :title="opt.tooltip"
50
51
  @mousedown.prevent="pickOption(opt)"
51
52
  @mouseover="highlight = idx"
53
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
54
+ @mouseleave="_hideTooltip"
52
55
  >
53
56
  <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
54
57
  {{ opt.label }}
@@ -61,6 +64,7 @@
61
64
  v-else-if="open"
62
65
  class="eg-select-dropdown eg-select-dropdown--floating"
63
66
  @mousedown.stop.prevent
67
+ @scroll.passive="_hideTooltip"
64
68
  >
65
69
  <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
66
70
  <input
@@ -84,15 +88,18 @@
84
88
  'eg-select-option--active': String(opt.value) === String(value),
85
89
  'eg-select-option--highlighted': idx === highlight
86
90
  }"
87
- :title="opt.tooltip"
88
91
  @mousedown.prevent="pickOption(opt)"
89
92
  @mouseover="highlight = idx"
93
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
94
+ @mouseleave="_hideTooltip"
90
95
  >
91
96
  <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
92
97
  {{ opt.label }}
93
98
  </div>
94
99
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
95
100
  </div>
101
+
102
+ <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
96
103
  </div>
97
104
  </template>
98
105
 
@@ -132,7 +139,12 @@ export default {
132
139
  // True right after Arrow/Page keyboard navigation — lets Space commit the
133
140
  // highlighted option instead of typing a literal space into the search box.
134
141
  // Reset on every text edit so normal multi-word search typing is unaffected.
135
- _navigatedByKeyboard: false
142
+ _navigatedByKeyboard: false,
143
+ // { text, rect } while a themed option tooltip should be visible — see
144
+ // _showTooltip/_hideTooltip. rect is a snapshot from getBoundingClientRect()
145
+ // at the moment the hover delay elapsed.
146
+ hoverTooltip: null,
147
+ _tooltipTimer: null
136
148
  }
137
149
  },
138
150
  computed: {
@@ -171,6 +183,15 @@ export default {
171
183
  maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
172
184
  zIndex: 9999
173
185
  }
186
+ },
187
+ hoverTooltipStyle() {
188
+ if (!this.hoverTooltip) return {}
189
+ const { rect, text } = this.hoverTooltip
190
+ const estH = 20 + Math.ceil(text.length / 40) * 16
191
+ const spaceBelow = window.innerHeight - rect.bottom
192
+ const top = spaceBelow >= estH + 8 ? rect.bottom + 6 : rect.top - estH - 6
193
+ const left = Math.min(rect.left, window.innerWidth - 280 - 8)
194
+ return { position: 'fixed', top: top + 'px', left: Math.max(8, left) + 'px', zIndex: 10000 }
174
195
  }
175
196
  },
176
197
  watch: {
@@ -194,6 +215,7 @@ export default {
194
215
  }
195
216
  },
196
217
  beforeDestroy() {
218
+ this._clearTooltipTimer()
197
219
  if (!this.dropdownOnly) {
198
220
  document.removeEventListener('click', this._onDocClick)
199
221
  }
@@ -218,6 +240,7 @@ export default {
218
240
  }
219
241
  },
220
242
  onSearch(e) {
243
+ this._hideTooltip()
221
244
  this.searchQuery = e.target.value
222
245
  this.highlight = 0
223
246
  this._navigatedByKeyboard = false
@@ -332,6 +355,27 @@ export default {
332
355
  if (!this.open) return
333
356
  if (this.$el && this.$el.contains(e.target)) return
334
357
  this.open = false
358
+ },
359
+ // Shared by option rows and the closed trigger label — shows a themed
360
+ // popover (see .eg-option-tooltip) after a short hover delay, instead of
361
+ // the browser's native title tooltip.
362
+ _showTooltip(text, el) {
363
+ this._clearTooltipTimer()
364
+ if (!text) return
365
+ const rect = el.getBoundingClientRect()
366
+ this._tooltipTimer = setTimeout(() => {
367
+ this.hoverTooltip = { text, rect }
368
+ }, 400)
369
+ },
370
+ _hideTooltip() {
371
+ this._clearTooltipTimer()
372
+ this.hoverTooltip = null
373
+ },
374
+ _clearTooltipTimer() {
375
+ if (this._tooltipTimer) {
376
+ clearTimeout(this._tooltipTimer)
377
+ this._tooltipTimer = null
378
+ }
335
379
  }
336
380
  }
337
381
  }
@@ -490,4 +534,24 @@ export default {
490
534
  display: inline-block;
491
535
  margin-top: 3px;
492
536
  }
537
+
538
+ /* Themed option/trigger tooltip — same recipe as .eg-select-dropdown
539
+ (cell-bg/border/radius) plus .eg-validation-tooltip's shadow, so it reads
540
+ as part of the grid's own chrome in every built-in theme instead of a
541
+ generic OS tooltip box. */
542
+ .eg-option-tooltip {
543
+ position: fixed;
544
+ max-width: 280px;
545
+ background: var(--eg-color-cell-bg, var(--eg-color-cell-bg-default));
546
+ border: 1px solid var(--eg-color-border, var(--eg-color-border-default));
547
+ border-radius: var(--eg-radius, var(--eg-radius-default));
548
+ box-shadow: var(--eg-shadow-tooltip, var(--eg-shadow-tooltip-default));
549
+ color: var(--eg-color-text, var(--eg-color-text-default));
550
+ font-size: var(--eg-font-size, var(--eg-font-size-default));
551
+ padding: 6px 10px;
552
+ pointer-events: none;
553
+ white-space: normal;
554
+ word-break: break-word;
555
+ z-index: 10000;
556
+ }
493
557
  </style>
@@ -3,6 +3,7 @@
3
3
  :class="dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'"
4
4
  :style="dropdownOnly ? overlayStyle : null"
5
5
  @mousedown="onRootMouseDown"
6
+ @scroll.passive="dropdownOnly && _hideTooltip()"
6
7
  >
7
8
  <!-- Standalone mode: trigger button -->
8
9
  <div
@@ -14,7 +15,8 @@
14
15
  <span
15
16
  class="eg-select-trigger-label"
16
17
  :class="{ 'eg-select-placeholder': selectedLabel == null }"
17
- :title="selectedTooltip"
18
+ @mouseenter="_showTooltip(selectedTooltip, $event.currentTarget)"
19
+ @mouseleave="_hideTooltip"
18
20
  >
19
21
  {{ selectedLabel != null ? selectedLabel : placeholder }}
20
22
  </span>
@@ -46,9 +48,10 @@
46
48
  'eg-select-option--highlighted': idx === highlight
47
49
  }"
48
50
  :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
49
- :title="opt.tooltip"
50
51
  @mousedown.prevent="pickOption(opt)"
51
52
  @mouseover="highlight = idx"
53
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
54
+ @mouseleave="_hideTooltip"
52
55
  >
53
56
  <span
54
57
  v-if="!searchQuery"
@@ -67,6 +70,7 @@
67
70
  v-else-if="open"
68
71
  class="eg-select-dropdown eg-select-dropdown--floating"
69
72
  @mousedown.stop.prevent
73
+ @scroll.passive="_hideTooltip"
70
74
  >
71
75
  <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
72
76
  <input
@@ -91,9 +95,10 @@
91
95
  'eg-select-option--highlighted': idx === highlight
92
96
  }"
93
97
  :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
94
- :title="opt.tooltip"
95
98
  @mousedown.prevent="pickOption(opt)"
96
99
  @mouseover="highlight = idx"
100
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
101
+ @mouseleave="_hideTooltip"
97
102
  >
98
103
  <span
99
104
  v-if="!searchQuery"
@@ -106,6 +111,8 @@
106
111
  </div>
107
112
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
108
113
  </div>
114
+
115
+ <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
109
116
  </div>
110
117
  </template>
111
118
 
@@ -144,7 +151,12 @@ export default {
144
151
  // True right after Arrow/Page keyboard navigation — lets Space commit the
145
152
  // highlighted option instead of typing a literal space into the search box.
146
153
  // Reset on every text edit so normal multi-word search typing is unaffected.
147
- _navigatedByKeyboard: false
154
+ _navigatedByKeyboard: false,
155
+ // { text, rect } while a themed option tooltip should be visible — see
156
+ // _showTooltip/_hideTooltip. rect is a snapshot from getBoundingClientRect()
157
+ // at the moment the hover delay elapsed.
158
+ hoverTooltip: null,
159
+ _tooltipTimer: null
148
160
  }
149
161
  },
150
162
  computed: {
@@ -195,6 +207,15 @@ export default {
195
207
  maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
196
208
  zIndex: 9999
197
209
  }
210
+ },
211
+ hoverTooltipStyle() {
212
+ if (!this.hoverTooltip) return {}
213
+ const { rect, text } = this.hoverTooltip
214
+ const estH = 20 + Math.ceil(text.length / 40) * 16
215
+ const spaceBelow = window.innerHeight - rect.bottom
216
+ const top = spaceBelow >= estH + 8 ? rect.bottom + 6 : rect.top - estH - 6
217
+ const left = Math.min(rect.left, window.innerWidth - 280 - 8)
218
+ return { position: 'fixed', top: top + 'px', left: Math.max(8, left) + 'px', zIndex: 10000 }
198
219
  }
199
220
  },
200
221
  watch: {
@@ -216,6 +237,7 @@ export default {
216
237
  }
217
238
  },
218
239
  beforeDestroy() {
240
+ this._clearTooltipTimer()
219
241
  if (!this.dropdownOnly) {
220
242
  document.removeEventListener('click', this._onDocClick)
221
243
  }
@@ -259,6 +281,7 @@ export default {
259
281
  this.highlight = newIdx >= 0 ? newIdx : this.highlight
260
282
  },
261
283
  onSearch(e) {
284
+ this._hideTooltip()
262
285
  this.searchQuery = e.target.value
263
286
  this.highlight = 0
264
287
  this._navigatedByKeyboard = false
@@ -438,6 +461,27 @@ export default {
438
461
  }
439
462
  }
440
463
  return null
464
+ },
465
+ // Shared by option rows and the closed trigger label — shows a themed
466
+ // popover (see .eg-option-tooltip) after a short hover delay, instead of
467
+ // the browser's native title tooltip.
468
+ _showTooltip(text, el) {
469
+ this._clearTooltipTimer()
470
+ if (!text) return
471
+ const rect = el.getBoundingClientRect()
472
+ this._tooltipTimer = setTimeout(() => {
473
+ this.hoverTooltip = { text, rect }
474
+ }, 400)
475
+ },
476
+ _hideTooltip() {
477
+ this._clearTooltipTimer()
478
+ this.hoverTooltip = null
479
+ },
480
+ _clearTooltipTimer() {
481
+ if (this._tooltipTimer) {
482
+ clearTimeout(this._tooltipTimer)
483
+ this._tooltipTimer = null
484
+ }
441
485
  }
442
486
  }
443
487
  }
@@ -917,6 +917,7 @@ import EgSelect from './EgSelect.vue'
917
917
  import EgTreeSelect from './EgTreeSelect.vue'
918
918
  import EgDatePicker from './EgDatePicker.vue'
919
919
  import { parseDateValue, formatDate } from '../utils/dateUtils.js'
920
+ import { formatNumber as _formatNumber } from '../utils/numberFormat.js'
920
921
  import { Mask, MaskInput } from '../utils/mask.js'
921
922
 
922
923
  const SIZE_CONFIG = {
@@ -6116,6 +6117,15 @@ export default {
6116
6117
  this.$refs.wrapper && this.$refs.wrapper.focus()
6117
6118
  },
6118
6119
 
6120
+ // Public, ref-callable: formats a number with a custom thousands
6121
+ // separator (decimal point stays '.', no forced/padded decimals). Any
6122
+ // value that isn't a finite number — already-formatted text like
6123
+ // "6 000 000,00", arbitrary strings, etc. — is returned unchanged, so
6124
+ // hosts can call this on raw cell values without checking the type first.
6125
+ formatNumber(value, separator) {
6126
+ return _formatNumber(value, separator)
6127
+ },
6128
+
6119
6129
  toggleLock() {
6120
6130
  this.internalLocked = !this.internalLocked
6121
6131
  this.$emit('update:locked', this.internalLocked)
@@ -6866,6 +6876,13 @@ export default {
6866
6876
  }
6867
6877
  .eg-cell-align-right { justify-content: flex-end; }
6868
6878
  .eg-cell-align-center { justify-content: center; }
6879
+ /* .eg-cell-value is flex:1 (v1.1.126, to shrink/ellipsize correctly), so it
6880
+ always fills the cell's full width — .eg-cell-align-*'s justify-content
6881
+ above has no leftover space left to act on and never visibly moves it.
6882
+ Same root cause .eg-cell-action-wrap already works around below; apply the
6883
+ same fix here via text-align on the value span itself. */
6884
+ .eg-cell-align-right .eg-cell-value { text-align: right; }
6885
+ .eg-cell-align-center .eg-cell-value { text-align: center; }
6869
6886
  .eg-cell--has-suffix.eg-cell-select::after { display: none; }
6870
6887
  .eg-cell-select-trigger-wrap {
6871
6888
  flex: 1;
@@ -7661,6 +7678,21 @@ export default {
7661
7678
  color: var(--eg-color-text-muted, var(--eg-color-text-muted-default));
7662
7679
  user-select: none;
7663
7680
  }
7681
+ .eg-option-tooltip {
7682
+ position: fixed;
7683
+ max-width: 280px;
7684
+ background: var(--eg-color-cell-bg, var(--eg-color-cell-bg-default));
7685
+ border: 1px solid var(--eg-color-border, var(--eg-color-border-default));
7686
+ border-radius: var(--eg-radius, var(--eg-radius-default));
7687
+ box-shadow: var(--eg-shadow-tooltip, var(--eg-shadow-tooltip-default));
7688
+ color: var(--eg-color-text, var(--eg-color-text-default));
7689
+ font-size: var(--eg-font-size, var(--eg-font-size-default));
7690
+ padding: 6px 10px;
7691
+ pointer-events: none;
7692
+ white-space: normal;
7693
+ word-break: break-word;
7694
+ z-index: 10000;
7695
+ }
7664
7696
  /* Grid `size` overrides — must live in this *unscoped* style block, not the
7665
7697
  scoped one above: .eg-select-option is rendered by the separate EgSelect /
7666
7698
  EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this
@@ -0,0 +1,15 @@
1
+ // Formats a JS number with a custom thousands separator, keeping the
2
+ // decimal point as '.' and showing only as many decimal digits as the
3
+ // number itself already has (no padding, no rounding). Any input that
4
+ // isn't a finite number — an already-formatted string like "6 000 000,00",
5
+ // arbitrary text, null, NaN, etc. — is returned unchanged, so callers can
6
+ // pass through mixed number/string cell values safely without pre-checking
7
+ // the type themselves.
8
+ export function formatNumber(value, separator) {
9
+ if (typeof value !== 'number' || !isFinite(value)) return value
10
+ const sep = separator == null ? '' : String(separator)
11
+ const sign = value < 0 ? '-' : ''
12
+ const [intPart, fracPart] = Math.abs(value).toString().split('.')
13
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, sep)
14
+ return sign + grouped + (fracPart ? '.' + fracPart : '')
15
+ }