vue2-excel-grid 2.6.7 → 2.8.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": "2.6.7",
3
+ "version": "2.8.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",
@@ -54,7 +54,7 @@
54
54
  </template>
55
55
 
56
56
  <script>
57
- import { parseDateValue, formatDate, toISO, todayISO } from '../utils/dateUtils.js'
57
+ import { parseDateValue, parseDateStrict, getFormatSpec, formatDate, toISO, todayISO, DEFAULT_DATE_FORMAT } from '../utils/dateUtils.js'
58
58
  import { getMessages } from '../utils/i18n.js'
59
59
 
60
60
  export default {
@@ -62,7 +62,7 @@ export default {
62
62
 
63
63
  props: {
64
64
  value: { default: null },
65
- format: { type: String, default: 'YYYY-MM-DD' },
65
+ format: { type: String, default: DEFAULT_DATE_FORMAT },
66
66
  locale: { type: String, default: 'en' },
67
67
  triggerRect: { type: Object, default: null }
68
68
  },
@@ -167,14 +167,7 @@ export default {
167
167
  },
168
168
 
169
169
  _autoFillSep(raw) {
170
- const fmt = this.format
171
- let sep, segments
172
- if (fmt === 'YYYY-MM-DD') { sep = '-'; segments = [4, 2, 2] }
173
- else if (fmt === 'DD.MM.YYYY') { sep = '.'; segments = [2, 2, 4] }
174
- else if (fmt === 'DD/MM/YYYY') { sep = '/'; segments = [2, 2, 4] }
175
- else if (fmt === 'MM/DD/YYYY') { sep = '/'; segments = [2, 2, 4] }
176
- else return raw
177
-
170
+ const { sep, segments } = getFormatSpec(this.format)
178
171
  const maxDigits = segments.reduce((a, b) => a + b, 0)
179
172
  const digits = raw.replace(/\D/g, '').slice(0, maxDigits)
180
173
 
@@ -207,7 +200,9 @@ export default {
207
200
  _commitInput() {
208
201
  const val = this.inputVal.trim()
209
202
  if (val === '') { this.$emit('pick', ''); return }
210
- const d = parseDateValue(val)
203
+ // Strict: must match THIS column's own format exactly (not just any
204
+ // recognized format) and be a real calendar date — see parseDateStrict.
205
+ const d = parseDateStrict(val, this.format)
211
206
  if (!d) { this.inputError = true; return }
212
207
  this.$emit('pick', formatDate(d, this.format))
213
208
  },
@@ -144,6 +144,8 @@
144
144
  tabindex="0"
145
145
  @keydown="onKeyDown"
146
146
  @keypress="onKeyPress"
147
+ @mousemove="onGridMouseMove"
148
+ @mouseleave="onGridMouseLeave"
147
149
  ref="wrapper"
148
150
  >
149
151
  <!-- Context menu -->
@@ -508,6 +510,7 @@
508
510
  :key="'srh-' + ri"
509
511
  class="eg-row-header-cell"
510
512
  :style="getRowHeaderStyle(ri)"
513
+ :data-row="ri"
511
514
  @mousedown="onRowHeaderMouseDown($event, ri)"
512
515
  @contextmenu.prevent="onRowContextMenu($event, ri)"
513
516
  >
@@ -548,7 +551,7 @@
548
551
  <div :style="{ height: totalVirtualHeight + 'px', position: 'relative' }">
549
552
  <div :style="{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }">
550
553
  <template v-for="ri in visibleRowRange">
551
- <div v-if="!isRowHidden(ri)" :key="'fc-row-' + ri" class="eg-row" :class="{ 'eg-row--even': ri % 2 === 0 }" :style="getRowStyle(ri)">
554
+ <div v-if="!isRowHidden(ri)" :key="'fc-row-' + ri" class="eg-row" :class="{ 'eg-row--even': ri % 2 === 0 }" :style="getRowStyle(ri)" :data-row="ri">
552
555
  <template v-for="(col, ci) in leafCols">
553
556
  <template v-if="isColumnPinnedLeft(ci) && !isColHidden(ci)">
554
557
  <div
@@ -667,7 +670,7 @@
667
670
  <div :style="{ height: totalVirtualHeight + 'px', position: 'relative' }">
668
671
  <div :style="{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }">
669
672
  <template v-for="ri in visibleRowRange">
670
- <div v-if="!isRowHidden(ri)" :key="'pr-row-' + ri" class="eg-row" :class="{ 'eg-row--even': ri % 2 === 0 }" :style="getRowStyle(ri)">
673
+ <div v-if="!isRowHidden(ri)" :key="'pr-row-' + ri" class="eg-row" :class="{ 'eg-row--even': ri % 2 === 0 }" :style="getRowStyle(ri)" :data-row="ri">
671
674
  <template v-for="col in pinnedRightCols">
672
675
  <div
673
676
  :key="'pr-cell-' + ri + '-' + col._flatIndex"
@@ -978,7 +981,7 @@ import EgSelect from './EgSelect.vue'
978
981
  import EgTreeSelect from './EgTreeSelect.vue'
979
982
  import EgDatePicker from './EgDatePicker.vue'
980
983
  import EgLocalSyncModal from './EgLocalSyncModal.vue'
981
- import { parseDateValue, formatDate } from '../utils/dateUtils.js'
984
+ import { parseDateValue, formatDate, DEFAULT_DATE_FORMAT } from '../utils/dateUtils.js'
982
985
  import { formatNumber as _formatNumber } from '../utils/numberFormat.js'
983
986
  import { Mask, MaskInput } from '../utils/mask.js'
984
987
  import { listVersions, getVersion, deleteVersion, saveOrTouchVersion, putBuffer, getBuffer, clearBuffer } from '../utils/idbStore.js'
@@ -2622,6 +2625,7 @@ const ExcelGridComponent = {
2622
2625
  // iterating all 50,000 rows each time. With 1200 cells per render that's
2623
2626
  // 60 million iterations per render (~600ms). _dataRaw bypasses this.
2624
2627
  this._dataRaw = this.data || []
2628
+ this._hoveredRi = -1
2625
2629
 
2626
2630
  // Non-reactive caches: same (w,h) → same style object → Vue 2 skips DOM style writes
2627
2631
  this._cellStyleCache = Object.create(null)
@@ -3824,6 +3828,45 @@ const ExcelGridComponent = {
3824
3828
  this.hideSortMenu()
3825
3829
  },
3826
3830
 
3831
+
3832
+ // =========================================================
3833
+ // HOVER HIGHLIGHT
3834
+ // =========================================================
3835
+ onGridMouseMove(e) {
3836
+ const rowNode = e.target.closest('[data-row]')
3837
+ const newRi = rowNode ? parseInt(rowNode.getAttribute('data-row'), 10) : -1
3838
+
3839
+ if (this._hoveredRi !== newRi) {
3840
+ if (this._hoveredRi >= 0) {
3841
+ const oldEls = this.$refs.wrapper.querySelectorAll(`[data-row="${this._hoveredRi}"]`)
3842
+ for (let i = 0; i < oldEls.length; i++) {
3843
+ oldEls[i].classList.remove('eg-row--hover')
3844
+ }
3845
+ }
3846
+
3847
+ this._hoveredRi = newRi
3848
+
3849
+ if (this._hoveredRi >= 0) {
3850
+ const newEls = this.$refs.wrapper.querySelectorAll(`[data-row="${this._hoveredRi}"]`)
3851
+ for (let i = 0; i < newEls.length; i++) {
3852
+ newEls[i].classList.add('eg-row--hover')
3853
+ }
3854
+ }
3855
+ }
3856
+ },
3857
+
3858
+ onGridMouseLeave() {
3859
+ if (this._hoveredRi >= 0) {
3860
+ if (this.$refs.wrapper) {
3861
+ const oldEls = this.$refs.wrapper.querySelectorAll(`[data-row="${this._hoveredRi}"]`)
3862
+ for (let i = 0; i < oldEls.length; i++) {
3863
+ oldEls[i].classList.remove('eg-row--hover')
3864
+ }
3865
+ }
3866
+ this._hoveredRi = -1
3867
+ }
3868
+ },
3869
+
3827
3870
  // =========================================================
3828
3871
  // KEYBOARD NAVIGATION (3.2)
3829
3872
  // =========================================================
@@ -4536,7 +4579,9 @@ const ExcelGridComponent = {
4536
4579
  rawOptions: this.resolveOptionsArray(col, ri, ci),
4537
4580
  currentValue,
4538
4581
  rect: rectObj,
4539
- enableSearch: !!col.enableSearch,
4582
+ // Defaults to on (opt out with enableSearch: false) — same convention
4583
+ // as clearable below.
4584
+ enableSearch: col.enableSearch !== false,
4540
4585
  clearable: col.clearable !== false
4541
4586
  }
4542
4587
  },
@@ -4620,7 +4665,7 @@ const ExcelGridComponent = {
4620
4665
  if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)
4621
4666
  if (col.type === 'date') {
4622
4667
  const d = parseDateValue(val)
4623
- return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)
4668
+ return d ? formatDate(d, col.format || DEFAULT_DATE_FORMAT) : (val == null ? '' : val)
4624
4669
  }
4625
4670
  return val == null ? '' : val
4626
4671
  },
@@ -4950,7 +4995,7 @@ const ExcelGridComponent = {
4950
4995
  const val = this.getCellValue(row, col)
4951
4996
  this.datePickerOverlay = {
4952
4997
  row, col,
4953
- format: colDef.format || 'YYYY-MM-DD',
4998
+ format: colDef.format || DEFAULT_DATE_FORMAT,
4954
4999
  value: val == null ? '' : String(val),
4955
5000
  rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }
4956
5001
  }
@@ -8440,6 +8485,35 @@ export default ExcelGridComponent
8440
8485
  line-height: 1;
8441
8486
  }
8442
8487
 
8488
+ /* ===== DATE CELL ICON ===== */
8489
+ /* Same reasoning/placement as the select caret above — reserve room at the
8490
+ cell level so col.align:'right'/centered text never reaches the icon's
8491
+ zone, regardless of alignment. mask-image (not content:url()/an emoji) so
8492
+ the icon is a single flat color that follows --eg-color-text-muted in
8493
+ every theme, light and dark alike, instead of a fixed-color raster/emoji
8494
+ glyph that wouldn't match. */
8495
+ .eg-cell-date:not(.eg-cell--has-suffix) {
8496
+ padding-right: calc(var(--eg-cell-padding-x, var(--eg-cell-padding-x-default)) + 16px);
8497
+ }
8498
+ .eg-cell--has-suffix.eg-cell-date::after { display: none; }
8499
+ .eg-cell-date::after {
8500
+ content: '';
8501
+ position: absolute;
8502
+ right: 5px;
8503
+ top: 50%;
8504
+ width: 12px;
8505
+ height: 12px;
8506
+ transform: translateY(-50%);
8507
+ background-color: var(--eg-color-text-muted, var(--eg-color-text-muted-default));
8508
+ pointer-events: none;
8509
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.3' stroke-linecap='round'%3E%3Crect x='1.5' y='3' width='13' height='11' rx='1.5'/%3E%3Cline x1='1.5' y1='6.3' x2='14.5' y2='6.3'/%3E%3Cline x1='4.5' y1='1.2' x2='4.5' y2='4.2'/%3E%3Cline x1='11.5' y1='1.2' x2='11.5' y2='4.2'/%3E%3C/svg%3E");
8510
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.3' stroke-linecap='round'%3E%3Crect x='1.5' y='3' width='13' height='11' rx='1.5'/%3E%3Cline x1='1.5' y1='6.3' x2='14.5' y2='6.3'/%3E%3Cline x1='4.5' y1='1.2' x2='4.5' y2='4.2'/%3E%3Cline x1='11.5' y1='1.2' x2='11.5' y2='4.2'/%3E%3C/svg%3E");
8511
+ -webkit-mask-repeat: no-repeat;
8512
+ mask-repeat: no-repeat;
8513
+ -webkit-mask-size: contain;
8514
+ mask-size: contain;
8515
+ }
8516
+
8443
8517
  /* ===== CHECKBOX CELL ===== */
8444
8518
  .eg-cell-checkbox {
8445
8519
  justify-content: center;
@@ -8475,7 +8549,12 @@ export default ExcelGridComponent
8475
8549
  .eg-row--even .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {
8476
8550
  background: var(--eg-color-row-stripe, var(--eg-color-row-stripe-default));
8477
8551
  }
8478
- .eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {
8552
+ .eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview),
8553
+ .eg-row.eg-row--hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {
8554
+ background: var(--eg-color-row-hover, var(--eg-color-row-hover-default));
8555
+ }
8556
+ .eg-row-header-cell:hover,
8557
+ .eg-row-header-cell.eg-row--hover {
8479
8558
  background: var(--eg-color-row-hover, var(--eg-color-row-hover-default));
8480
8559
  }
8481
8560
 
@@ -2,24 +2,54 @@
2
2
  * Date parsing and formatting utilities for the date cell type.
3
3
  * No external dependencies — pure JS.
4
4
  *
5
- * Internal storage format: ISO 8601 'YYYY-MM-DD'
6
- * Display format controlled by column config `format` prop.
5
+ * Display/storage format controlled by column config `format` prop
6
+ * (default 'DD-MM-YYYY').
7
7
  */
8
8
 
9
- const ISO_RE = /^(\d{4})-(\d{2})-(\d{2})$/
10
- const DMY_DOT = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/ // DD.MM.YYYY
11
- const DMY_SLASH = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/ // DD/MM/YYYY
12
- const MDY_SLASH = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/ // MM/DD/YYYY (same regex, resolved by format hint)
9
+ // One definition per supported format string: which regex a candidate string
10
+ // must match, in what order its captured groups map to year/month/day, and
11
+ // (sep/segments) how manual typing auto-inserts separators as digits are
12
+ // entered — see EgDatePicker.vue's _autoFillSep, which reads this table
13
+ // directly so the two never drift apart.
14
+ const FORMATS = {
15
+ 'YYYY-MM-DD': { re: /^(\d{4})-(\d{2})-(\d{2})$/, order: ['Y', 'M', 'D'], sep: '-', segments: [4, 2, 2] },
16
+ 'DD-MM-YYYY': { re: /^(\d{1,2})-(\d{1,2})-(\d{4})$/, order: ['D', 'M', 'Y'], sep: '-', segments: [2, 2, 4] },
17
+ 'DD.MM.YYYY': { re: /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/, order: ['D', 'M', 'Y'], sep: '.', segments: [2, 2, 4] },
18
+ 'DD/MM/YYYY': { re: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, order: ['D', 'M', 'Y'], sep: '/', segments: [2, 2, 4] },
19
+ 'MM/DD/YYYY': { re: /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/, order: ['M', 'D', 'Y'], sep: '/', segments: [2, 2, 4] }
20
+ }
21
+
22
+ export const DEFAULT_DATE_FORMAT = 'DD-MM-YYYY'
13
23
 
14
24
  function pad(n) { return String(n).padStart(2, '0') }
15
25
 
26
+ // Real calendar-day check — rejects e.g. day 30 of February, month 13, day 0 —
27
+ // instead of relying on `new Date(y, m-1, d)`'s silent overflow-rollover
28
+ // (which would otherwise turn "30.02.2024" into a DIFFERENT, wrong date
29
+ // instead of failing). Leap years handled correctly via the day-0-of-next-
30
+ // month trick (JS Date's own calendar math, not reimplemented here).
31
+ function isValidYMD(y, m, d) {
32
+ if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return false
33
+ if (m < 1 || m > 12 || d < 1) return false
34
+ return d <= new Date(y, m, 0).getDate()
35
+ }
36
+
37
+ function extract(match, order) {
38
+ const parts = {}
39
+ order.forEach((field, i) => { parts[field] = +match[i + 1] })
40
+ if (!isValidYMD(parts.Y, parts.M, parts.D)) return null
41
+ return new Date(parts.Y, parts.M - 1, parts.D)
42
+ }
43
+
16
44
  /**
17
45
  * Parse any common date string into a Date object (local midnight).
18
- * Accepts: 'YYYY-MM-DD', 'DD.MM.YYYY', 'DD/MM/YYYY'.
19
- * Returns null if unparseable.
46
+ * Lenient — tries every known format (not just `hint`/a specific column's
47
+ * own format), for reading already-stored values that may come from an API
48
+ * or a different format than the column's current config. Returns null if
49
+ * unparseable OR not a real calendar date (see isValidYMD).
20
50
  *
21
51
  * @param {string|number|Date|null} val
22
- * @param {string} [hint] - 'MDY' to treat slash format as MM/DD/YYYY
52
+ * @param {string} [hint] - 'MDY' to treat an ambiguous slash format as MM/DD/YYYY
23
53
  * @returns {Date|null}
24
54
  */
25
55
  export function parseDateValue(val, hint) {
@@ -29,30 +59,53 @@ export function parseDateValue(val, hint) {
29
59
  const s = String(val).trim()
30
60
 
31
61
  let m
32
- if ((m = ISO_RE.exec(s))) {
33
- return new Date(+m[1], +m[2] - 1, +m[3])
34
- }
35
- if ((m = DMY_DOT.exec(s))) {
36
- return new Date(+m[3], +m[2] - 1, +m[1])
37
- }
38
- if ((m = DMY_SLASH.exec(s))) {
39
- if (hint === 'MDY') return new Date(+m[3], +m[1] - 1, +m[2])
40
- return new Date(+m[3], +m[2] - 1, +m[1])
62
+ if ((m = FORMATS['YYYY-MM-DD'].re.exec(s))) return extract(m, FORMATS['YYYY-MM-DD'].order)
63
+ if ((m = FORMATS['DD-MM-YYYY'].re.exec(s))) return extract(m, FORMATS['DD-MM-YYYY'].order)
64
+ if ((m = FORMATS['DD.MM.YYYY'].re.exec(s))) return extract(m, FORMATS['DD.MM.YYYY'].order)
65
+ if ((m = FORMATS['DD/MM/YYYY'].re.exec(s))) {
66
+ return extract(m, hint === 'MDY' ? FORMATS['MM/DD/YYYY'].order : FORMATS['DD/MM/YYYY'].order)
41
67
  }
42
68
  return null
43
69
  }
44
70
 
71
+ /**
72
+ * Parse a string strictly against ONE specific format — used to validate
73
+ * manual text-input before it's allowed to commit (EgDatePicker's
74
+ * _commitInput). Unlike parseDateValue(), a string shaped like a DIFFERENT
75
+ * known format (e.g. ISO typed into a 'DD.MM.YYYY' column) is rejected, not
76
+ * silently accepted — and, like parseDateValue(), an out-of-range calendar
77
+ * date (Feb 30, month 13, ...) is rejected rather than rolled over.
78
+ *
79
+ * @param {string} val
80
+ * @param {string} [fmt] - defaults to DEFAULT_DATE_FORMAT if unrecognized/omitted
81
+ * @returns {Date|null}
82
+ */
83
+ export function parseDateStrict(val, fmt) {
84
+ if (val == null || val === '') return null
85
+ const spec = FORMATS[fmt] || FORMATS[DEFAULT_DATE_FORMAT]
86
+ const m = spec.re.exec(String(val).trim())
87
+ if (!m) return null
88
+ return extract(m, spec.order)
89
+ }
90
+
91
+ // Exposed so EgDatePicker's manual-typing auto-separator logic (_autoFillSep)
92
+ // shares the exact same per-format segment lengths as parsing/validation,
93
+ // instead of keeping a second, driftable copy of the same table.
94
+ export function getFormatSpec(fmt) {
95
+ return FORMATS[fmt] || FORMATS[DEFAULT_DATE_FORMAT]
96
+ }
97
+
45
98
  /**
46
99
  * Format a Date object according to the format string.
47
100
  * Supported tokens: YYYY MM DD
48
101
  *
49
102
  * @param {Date} date
50
- * @param {string} [fmt='YYYY-MM-DD']
103
+ * @param {string} [fmt=DEFAULT_DATE_FORMAT]
51
104
  * @returns {string}
52
105
  */
53
106
  export function formatDate(date, fmt) {
54
107
  if (!date || isNaN(date.getTime())) return ''
55
- const f = fmt || 'YYYY-MM-DD'
108
+ const f = fmt || DEFAULT_DATE_FORMAT
56
109
  return f
57
110
  .replace('YYYY', date.getFullYear())
58
111
  .replace('MM', pad(date.getMonth() + 1))