vue2-excel-grid 1.2.0 → 1.3.1

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.2.0",
3
+ "version": "1.3.1",
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
@@ -12,7 +13,12 @@
12
13
  @click.stop="toggle"
13
14
  >
14
15
  <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
+ <span
17
+ class="eg-select-trigger-label"
18
+ :class="{ 'eg-select-placeholder': !selectedOpt }"
19
+ @mouseenter="_showTooltip(selectedOpt && selectedOpt.tooltip, $event.currentTarget)"
20
+ @mouseleave="_hideTooltip"
21
+ >
16
22
  {{ selectedOpt ? selectedOpt.label : placeholder }}
17
23
  </span>
18
24
  <span class="eg-select-caret">▾</span>
@@ -44,6 +50,8 @@
44
50
  }"
45
51
  @mousedown.prevent="pickOption(opt)"
46
52
  @mouseover="highlight = idx"
53
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
54
+ @mouseleave="_hideTooltip"
47
55
  >
48
56
  <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
49
57
  {{ opt.label }}
@@ -56,6 +64,7 @@
56
64
  v-else-if="open"
57
65
  class="eg-select-dropdown eg-select-dropdown--floating"
58
66
  @mousedown.stop.prevent
67
+ @scroll.passive="_hideTooltip"
59
68
  >
60
69
  <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
61
70
  <input
@@ -81,12 +90,16 @@
81
90
  }"
82
91
  @mousedown.prevent="pickOption(opt)"
83
92
  @mouseover="highlight = idx"
93
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
94
+ @mouseleave="_hideTooltip"
84
95
  >
85
96
  <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
86
97
  {{ opt.label }}
87
98
  </div>
88
99
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
89
100
  </div>
101
+
102
+ <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
90
103
  </div>
91
104
  </template>
92
105
 
@@ -126,7 +139,12 @@ export default {
126
139
  // True right after Arrow/Page keyboard navigation — lets Space commit the
127
140
  // highlighted option instead of typing a literal space into the search box.
128
141
  // Reset on every text edit so normal multi-word search typing is unaffected.
129
- _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
130
148
  }
131
149
  },
132
150
  computed: {
@@ -134,11 +152,11 @@ export default {
134
152
  normalizedOptions() {
135
153
  const vk = this.valueKey
136
154
  const lk = this.labelKey
137
- return (this.options || []).map(o =>
138
- typeof o === 'string'
139
- ? { value: o, label: o }
140
- : { value: o[vk], label: o[lk] != null ? o[lk] : String(o[vk]), color: o.color }
141
- )
155
+ return (this.options || []).map(o => {
156
+ if (typeof o === 'string') return { value: o, label: o, tooltip: o }
157
+ const label = o[lk] != null ? o[lk] : String(o[vk])
158
+ return { value: o[vk], label, color: o.color, tooltip: o.tooltip != null ? o.tooltip : label }
159
+ })
142
160
  },
143
161
  filteredOptions() {
144
162
  if (!this.enableSearch || !this.searchQuery.trim()) return this.normalizedOptions
@@ -165,6 +183,15 @@ export default {
165
183
  maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
166
184
  zIndex: 9999
167
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 }
168
195
  }
169
196
  },
170
197
  watch: {
@@ -188,6 +215,7 @@ export default {
188
215
  }
189
216
  },
190
217
  beforeDestroy() {
218
+ this._clearTooltipTimer()
191
219
  if (!this.dropdownOnly) {
192
220
  document.removeEventListener('click', this._onDocClick)
193
221
  }
@@ -212,6 +240,7 @@ export default {
212
240
  }
213
241
  },
214
242
  onSearch(e) {
243
+ this._hideTooltip()
215
244
  this.searchQuery = e.target.value
216
245
  this.highlight = 0
217
246
  this._navigatedByKeyboard = false
@@ -326,6 +355,27 @@ export default {
326
355
  if (!this.open) return
327
356
  if (this.$el && this.$el.contains(e.target)) return
328
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
+ }
329
379
  }
330
380
  }
331
381
  }
@@ -484,4 +534,24 @@ export default {
484
534
  display: inline-block;
485
535
  margin-top: 3px;
486
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
+ }
487
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
@@ -11,7 +12,12 @@
11
12
  :class="{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }"
12
13
  @click.stop="toggle"
13
14
  >
14
- <span class="eg-select-trigger-label" :class="{ 'eg-select-placeholder': selectedLabel == null }">
15
+ <span
16
+ class="eg-select-trigger-label"
17
+ :class="{ 'eg-select-placeholder': selectedLabel == null }"
18
+ @mouseenter="_showTooltip(selectedTooltip, $event.currentTarget)"
19
+ @mouseleave="_hideTooltip"
20
+ >
15
21
  {{ selectedLabel != null ? selectedLabel : placeholder }}
16
22
  </span>
17
23
  <span class="eg-select-caret">▾</span>
@@ -44,6 +50,8 @@
44
50
  :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
45
51
  @mousedown.prevent="pickOption(opt)"
46
52
  @mouseover="highlight = idx"
53
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
54
+ @mouseleave="_hideTooltip"
47
55
  >
48
56
  <span
49
57
  v-if="!searchQuery"
@@ -62,6 +70,7 @@
62
70
  v-else-if="open"
63
71
  class="eg-select-dropdown eg-select-dropdown--floating"
64
72
  @mousedown.stop.prevent
73
+ @scroll.passive="_hideTooltip"
65
74
  >
66
75
  <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
67
76
  <input
@@ -88,6 +97,8 @@
88
97
  :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
89
98
  @mousedown.prevent="pickOption(opt)"
90
99
  @mouseover="highlight = idx"
100
+ @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
101
+ @mouseleave="_hideTooltip"
91
102
  >
92
103
  <span
93
104
  v-if="!searchQuery"
@@ -100,6 +111,8 @@
100
111
  </div>
101
112
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
102
113
  </div>
114
+
115
+ <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
103
116
  </div>
104
117
  </template>
105
118
 
@@ -138,7 +151,12 @@ export default {
138
151
  // True right after Arrow/Page keyboard navigation — lets Space commit the
139
152
  // highlighted option instead of typing a literal space into the search box.
140
153
  // Reset on every text edit so normal multi-word search typing is unaffected.
141
- _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
142
160
  }
143
161
  },
144
162
  computed: {
@@ -165,6 +183,14 @@ export default {
165
183
  const found = this._treeFindLabel(this.options || [], this.keys, v)
166
184
  return found != null ? found : null
167
185
  },
186
+ selectedTooltip() {
187
+ const v = String(this.value == null ? '' : this.value)
188
+ if (!v) return null
189
+ const node = this._treeFindNode(this.options || [], this.keys, v)
190
+ if (!node) return null
191
+ const label = node[this.keys.labelKey] != null ? node[this.keys.labelKey] : String(node[this.keys.valueKey])
192
+ return node.tooltip != null ? node.tooltip : label
193
+ },
168
194
  overlayStyle() {
169
195
  if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
170
196
  const rect = this.triggerRect
@@ -181,6 +207,15 @@ export default {
181
207
  maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
182
208
  zIndex: 9999
183
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 }
184
219
  }
185
220
  },
186
221
  watch: {
@@ -202,6 +237,7 @@ export default {
202
237
  }
203
238
  },
204
239
  beforeDestroy() {
240
+ this._clearTooltipTimer()
205
241
  if (!this.dropdownOnly) {
206
242
  document.removeEventListener('click', this._onDocClick)
207
243
  }
@@ -245,6 +281,7 @@ export default {
245
281
  this.highlight = newIdx >= 0 ? newIdx : this.highlight
246
282
  },
247
283
  onSearch(e) {
284
+ this._hideTooltip()
248
285
  this.searchQuery = e.target.value
249
286
  this.highlight = 0
250
287
  this._navigatedByKeyboard = false
@@ -375,7 +412,8 @@ export default {
375
412
  const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
376
413
  const hasChildren = children.length > 0
377
414
  const expanded = hasChildren && !!expandedMap[id]
378
- 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 })
415
+ const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
416
+ out.push({ value: node[keys.valueKey], label, color: node.color, tooltip: node.tooltip != null ? node.tooltip : label, id, depth, hasChildren, expanded })
379
417
  if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))
380
418
  })
381
419
  return out
@@ -385,7 +423,8 @@ export default {
385
423
  const walk = (list) => {
386
424
  ;(list || []).forEach(node => {
387
425
  const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
388
- 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 })
426
+ const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
427
+ result.push({ value: node[keys.valueKey], label, color: node.color, tooltip: node.tooltip != null ? node.tooltip : label, id: String(node[keys.valueKey]), depth: 0, hasChildren: false, expanded: false })
389
428
  if (children.length) walk(children)
390
429
  })
391
430
  }
@@ -402,6 +441,14 @@ export default {
402
441
  }
403
442
  return null
404
443
  },
444
+ _treeFindNode(nodes, keys, value) {
445
+ for (const node of (nodes || [])) {
446
+ if (String(node[keys.valueKey]) === value) return node
447
+ const found = this._treeFindNode(node[keys.childrenKey], keys, value)
448
+ if (found != null) return found
449
+ }
450
+ return null
451
+ },
405
452
  _treeFindPath(nodes, keys, value, pathArr) {
406
453
  for (let i = 0; i < (nodes || []).length; i++) {
407
454
  const node = nodes[i]
@@ -414,6 +461,27 @@ export default {
414
461
  }
415
462
  }
416
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
+ }
417
485
  }
418
486
  }
419
487
  }
@@ -7661,6 +7661,21 @@ export default {
7661
7661
  color: var(--eg-color-text-muted, var(--eg-color-text-muted-default));
7662
7662
  user-select: none;
7663
7663
  }
7664
+ .eg-option-tooltip {
7665
+ position: fixed;
7666
+ max-width: 280px;
7667
+ background: var(--eg-color-cell-bg, var(--eg-color-cell-bg-default));
7668
+ border: 1px solid var(--eg-color-border, var(--eg-color-border-default));
7669
+ border-radius: var(--eg-radius, var(--eg-radius-default));
7670
+ box-shadow: var(--eg-shadow-tooltip, var(--eg-shadow-tooltip-default));
7671
+ color: var(--eg-color-text, var(--eg-color-text-default));
7672
+ font-size: var(--eg-font-size, var(--eg-font-size-default));
7673
+ padding: 6px 10px;
7674
+ pointer-events: none;
7675
+ white-space: normal;
7676
+ word-break: break-word;
7677
+ z-index: 10000;
7678
+ }
7664
7679
  /* Grid `size` overrides — must live in this *unscoped* style block, not the
7665
7680
  scoped one above: .eg-select-option is rendered by the separate EgSelect /
7666
7681
  EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this