vue2-excel-grid 2.0.2 → 2.2.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": "2.0.2",
3
+ "version": "2.2.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",
@@ -57,6 +57,9 @@
57
57
  {{ opt.label }}
58
58
  </div>
59
59
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
60
+ <div v-if="clearable && selectedOpt" class="eg-select-footer">
61
+ <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
62
+ </div>
60
63
  </template>
61
64
 
62
65
  <!-- Options panel in standalone mode (floating dropdown below trigger) -->
@@ -97,6 +100,9 @@
97
100
  {{ opt.label }}
98
101
  </div>
99
102
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
103
+ <div v-if="clearable && selectedOpt" class="eg-select-footer">
104
+ <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
105
+ </div>
100
106
  </div>
101
107
 
102
108
  <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
@@ -129,7 +135,8 @@ export default {
129
135
  dropdownOnly: { type: Boolean, default: false },
130
136
  triggerRect: { type: Object, default: null },
131
137
  size: { type: String, default: 'middle' },
132
- locale: { type: String, default: 'en' }
138
+ locale: { type: String, default: 'en' },
139
+ clearable: { type: Boolean, default: true }
133
140
  },
134
141
  data() {
135
142
  return {
@@ -239,6 +246,11 @@ export default {
239
246
  this.highlight = -1
240
247
  }
241
248
  },
249
+ // Reuses pickOption's write path (same input/select emit + dropdown-close
250
+ // handling) — a clear is just "pick the empty option", not a separate flow.
251
+ pickClear() {
252
+ this.pickOption({ value: '', label: '' })
253
+ },
242
254
  onSearch(e) {
243
255
  this._hideTooltip()
244
256
  this.searchQuery = e.target.value
@@ -535,6 +547,29 @@ export default {
535
547
  margin-top: 3px;
536
548
  }
537
549
 
550
+ /* "Clear" footer — same recipe as EgDatePicker.vue's .eg-dp-footer/.eg-dp-btn,
551
+ just a single right-aligned button (no "Today" equivalent here). Also
552
+ duplicated in ExcelGrid.vue's own <style> — see the comment above
553
+ .eg-select-wrapper there for why, and keep both copies token-identical. */
554
+ .eg-select-footer {
555
+ display: flex;
556
+ justify-content: flex-end;
557
+ padding: 4px 6px 6px;
558
+ border-top: 1px solid var(--eg-color-border, var(--eg-color-border-default));
559
+ }
560
+ .eg-select-btn {
561
+ background: none;
562
+ border: 1px solid var(--eg-color-border, var(--eg-color-border-default));
563
+ border-radius: var(--eg-radius, var(--eg-radius-default));
564
+ padding: 4px 10px;
565
+ cursor: pointer;
566
+ font-size: 12px;
567
+ color: var(--eg-color-text, var(--eg-color-text-default));
568
+ }
569
+ .eg-select-btn:hover { background: var(--eg-color-option-hover-bg, var(--eg-color-option-hover-bg-default)); }
570
+ .eg-select-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0); }
571
+ .eg-select-btn--clear:hover { background: #fff0f0; }
572
+
538
573
  /* Themed option/trigger tooltip — same recipe as .eg-select-dropdown
539
574
  (cell-bg/border/radius) plus .eg-validation-tooltip's shadow, so it reads
540
575
  as part of the grid's own chrome in every built-in theme instead of a
@@ -63,6 +63,9 @@
63
63
  {{ opt.label }}
64
64
  </div>
65
65
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
66
+ <div v-if="clearable && selectedLabel != null" class="eg-select-footer">
67
+ <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
68
+ </div>
66
69
  </template>
67
70
 
68
71
  <!-- Options panel in standalone mode -->
@@ -110,6 +113,9 @@
110
113
  {{ opt.label }}
111
114
  </div>
112
115
  <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
116
+ <div v-if="clearable && selectedLabel != null" class="eg-select-footer">
117
+ <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
118
+ </div>
113
119
  </div>
114
120
 
115
121
  <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
@@ -140,7 +146,8 @@ export default {
140
146
  dropdownOnly: { type: Boolean, default: false },
141
147
  triggerRect: { type: Object, default: null },
142
148
  size: { type: String, default: 'middle' },
143
- locale: { type: String, default: 'en' }
149
+ locale: { type: String, default: 'en' },
150
+ clearable: { type: Boolean, default: true }
144
151
  },
145
152
  data() {
146
153
  return {
@@ -269,6 +276,11 @@ export default {
269
276
  this.highlight = -1
270
277
  }
271
278
  },
279
+ // Reuses pickOption's write path (same input/select emit + dropdown-close
280
+ // handling) — a clear is just "pick the empty option", not a separate flow.
281
+ pickClear() {
282
+ this.pickOption({ value: '', label: '' })
283
+ },
272
284
  toggleNode(idx) {
273
285
  const opt = this.flatOptions[idx]
274
286
  if (!opt || !opt.hasChildren) return
@@ -854,6 +854,7 @@
854
854
  :dropdown-only="true"
855
855
  :size="internalSize"
856
856
  :locale="locale"
857
+ :clearable="selectDropdown.clearable"
857
858
  @select="onComponentSelect"
858
859
  @close="closeSelectDropdown"
859
860
  />
@@ -871,6 +872,7 @@
871
872
  :dropdown-only="true"
872
873
  :size="internalSize"
873
874
  :locale="locale"
875
+ :clearable="selectDropdown.clearable"
874
876
  @select="onComponentSelect"
875
877
  @close="closeSelectDropdown"
876
878
  />
@@ -3667,6 +3669,17 @@ const ExcelGridComponent = {
3667
3669
  return
3668
3670
  }
3669
3671
 
3672
+ if (key === 'd' && (e.ctrlKey || e.metaKey)) {
3673
+ e.preventDefault()
3674
+ this.fillDown()
3675
+ return
3676
+ }
3677
+ if (key === 'r' && (e.ctrlKey || e.metaKey)) {
3678
+ e.preventDefault()
3679
+ this.fillRight()
3680
+ return
3681
+ }
3682
+
3670
3683
  if (key === 'Delete' || key === 'Backspace') {
3671
3684
  e.preventDefault()
3672
3685
  this.clearSelection()
@@ -4287,7 +4300,8 @@ const ExcelGridComponent = {
4287
4300
  rawOptions: this.resolveOptionsArray(col, ri, ci),
4288
4301
  currentValue,
4289
4302
  rect: rectObj,
4290
- enableSearch: !!col.enableSearch
4303
+ enableSearch: !!col.enableSearch,
4304
+ clearable: col.clearable !== false
4291
4305
  }
4292
4306
  },
4293
4307
 
@@ -4319,7 +4333,10 @@ const ExcelGridComponent = {
4319
4333
  const { row, col } = this.selectDropdown
4320
4334
  const colDef = this.leafCols[col]
4321
4335
  if (colDef) {
4322
- if (!this.isCellValueValid(row, col, value)) {
4336
+ // Clearing (value === '') always goes through, same as onDatePick()'s
4337
+ // empty-string bypass — "no selection" is a valid state regardless of
4338
+ // what cellValidator requires of a real picked value.
4339
+ if (value !== '' && !this.isCellValueValid(row, col, value)) {
4323
4340
  this._showInvalidCell(row, col, value)
4324
4341
  this.closeSelectDropdown()
4325
4342
  return
@@ -5051,6 +5068,107 @@ const ExcelGridComponent = {
5051
5068
  if (skipped.length > 0) this._emitBulkValidationError(skipped, false)
5052
5069
  },
5053
5070
 
5071
+ // =========================================================
5072
+ // Ctrl+D / Ctrl+R — Excel's standard fill-down/fill-right (3.96)
5073
+ // =========================================================
5074
+ // Plain copy, NOT the fill-handle's series continuation (generateFillValues,
5075
+ // 1,2,3->4,5,6) — matches real Excel: Ctrl+D/Ctrl+R just duplicates a row/
5076
+ // column's values, it never extrapolates a pattern.
5077
+ async _fillAxis(direction) {
5078
+ if (this.internalLocked) return
5079
+ if (!this.selectionRanges.length) return
5080
+ const oldData = this.data
5081
+ const newData = oldData.slice()
5082
+ const skipped = []
5083
+ const appliedRanges = []
5084
+
5085
+ for (const range of this.selectionRanges) {
5086
+ const r = normalizeRange(range)
5087
+ if (direction === 'down') {
5088
+ let srcRow, firstTargetRow, lastTargetRow
5089
+ if (r.startRow === r.endRow) {
5090
+ // Single-row selection: Excel fills from the row directly above.
5091
+ srcRow = r.startRow - 1
5092
+ if (srcRow < 0) continue
5093
+ firstTargetRow = r.startRow
5094
+ lastTargetRow = r.startRow
5095
+ } else {
5096
+ srcRow = r.startRow
5097
+ firstTargetRow = r.startRow + 1
5098
+ lastTargetRow = r.endRow
5099
+ }
5100
+ for (let ci = r.startCol; ci <= r.endCol; ci++) {
5101
+ const col = this.leafCols[ci]
5102
+ if (!col) continue
5103
+ const srcVal = oldData[srcRow] ? oldData[srcRow][col.key] : ''
5104
+ for (let ri = firstTargetRow; ri <= lastTargetRow; ri++) {
5105
+ if (!this.isCellEditable(ri, ci)) continue
5106
+ const v = this._coerceCellValue(ci, this._applyColMask(ci, srcVal))
5107
+ if (!this.isCellValueValid(ri, ci, v)) { skipped.push({ row: ri, col: ci, value: v }); continue }
5108
+ if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }
5109
+ newData[ri][col.key] = v
5110
+ }
5111
+ }
5112
+ appliedRanges.push({
5113
+ sourceRange: { startRow: srcRow, endRow: srcRow, startCol: r.startCol, endCol: r.endCol },
5114
+ targetRange: { startRow: firstTargetRow, endRow: lastTargetRow, startCol: r.startCol, endCol: r.endCol }
5115
+ })
5116
+ } else {
5117
+ let srcCol, firstTargetCol, lastTargetCol
5118
+ if (r.startCol === r.endCol) {
5119
+ // Single-column selection: Excel fills from the column directly to the left.
5120
+ srcCol = r.startCol - 1
5121
+ if (srcCol < 0) continue
5122
+ firstTargetCol = r.startCol
5123
+ lastTargetCol = r.startCol
5124
+ } else {
5125
+ srcCol = r.startCol
5126
+ firstTargetCol = r.startCol + 1
5127
+ lastTargetCol = r.endCol
5128
+ }
5129
+ const srcColDef = this.leafCols[srcCol]
5130
+ for (let ri = r.startRow; ri <= r.endRow; ri++) {
5131
+ const srcVal = srcColDef && oldData[ri] ? oldData[ri][srcColDef.key] : ''
5132
+ for (let ci = firstTargetCol; ci <= lastTargetCol; ci++) {
5133
+ const col = this.leafCols[ci]
5134
+ if (!col || !this.isCellEditable(ri, ci)) continue
5135
+ const v = this._coerceCellValue(ci, this._applyColMask(ci, srcVal))
5136
+ if (!this.isCellValueValid(ri, ci, v)) { skipped.push({ row: ri, col: ci, value: v }); continue }
5137
+ if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }
5138
+ newData[ri][col.key] = v
5139
+ }
5140
+ }
5141
+ appliedRanges.push({
5142
+ sourceRange: { startRow: r.startRow, endRow: r.endRow, startCol: srcCol, endCol: srcCol },
5143
+ targetRange: { startRow: r.startRow, endRow: r.endRow, startCol: firstTargetCol, endCol: lastTargetCol }
5144
+ })
5145
+ }
5146
+ }
5147
+
5148
+ if (!appliedRanges.length) return
5149
+
5150
+ if (this.strictBulkValidation && skipped.length > 0) {
5151
+ this._emitBulkValidationError(skipped, true)
5152
+ return
5153
+ }
5154
+
5155
+ this._emitDataChange(oldData, newData)
5156
+ for (const ar of appliedRanges) {
5157
+ this.$emit('fill', { sourceRange: ar.sourceRange, targetRange: ar.targetRange, values: newData })
5158
+ }
5159
+ if (skipped.length > 0) this._emitBulkValidationError(skipped, false)
5160
+
5161
+ // See pasteAt's/commitFillDrag's _fireCellCallbacksForRange call — same
5162
+ // nextTick deferral reasoning (col.onInput/onSelect after the DOM settles).
5163
+ await this.$nextTick()
5164
+ for (const ar of appliedRanges) {
5165
+ this._fireCellCallbacksForRange(oldData, newData, ar.targetRange.startRow, ar.targetRange.endRow, ar.targetRange.startCol, ar.targetRange.endCol)
5166
+ }
5167
+ },
5168
+
5169
+ fillDown() { return this._fillAxis('down') },
5170
+ fillRight() { return this._fillAxis('right') },
5171
+
5054
5172
  // =========================================================
5055
5173
  // COPY / PASTE (3.4)
5056
5174
  // =========================================================
@@ -8156,6 +8274,24 @@ export default ExcelGridComponent
8156
8274
  word-break: break-word;
8157
8275
  z-index: 10000;
8158
8276
  }
8277
+ .eg-select-footer {
8278
+ display: flex;
8279
+ justify-content: flex-end;
8280
+ padding: 4px 6px 6px;
8281
+ border-top: 1px solid var(--eg-color-border, var(--eg-color-border-default));
8282
+ }
8283
+ .eg-select-btn {
8284
+ background: none;
8285
+ border: 1px solid var(--eg-color-border, var(--eg-color-border-default));
8286
+ border-radius: var(--eg-radius, var(--eg-radius-default));
8287
+ padding: 4px 10px;
8288
+ cursor: pointer;
8289
+ font-size: 12px;
8290
+ color: var(--eg-color-text, var(--eg-color-text-default));
8291
+ }
8292
+ .eg-select-btn:hover { background: var(--eg-color-option-hover-bg, var(--eg-color-option-hover-bg-default)); }
8293
+ .eg-select-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0); }
8294
+ .eg-select-btn--clear:hover { background: #fff0f0; }
8159
8295
  /* Grid `size` overrides — must live in this *unscoped* style block, not the
8160
8296
  scoped one above: .eg-select-option is rendered by the separate EgSelect /
8161
8297
  EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this