vue2-excel-grid 2.6.6 → 2.8.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.6.6",
3
+ "version": "2.8.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",
@@ -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
  },
@@ -978,7 +978,7 @@ import EgSelect from './EgSelect.vue'
978
978
  import EgTreeSelect from './EgTreeSelect.vue'
979
979
  import EgDatePicker from './EgDatePicker.vue'
980
980
  import EgLocalSyncModal from './EgLocalSyncModal.vue'
981
- import { parseDateValue, formatDate } from '../utils/dateUtils.js'
981
+ import { parseDateValue, formatDate, DEFAULT_DATE_FORMAT } from '../utils/dateUtils.js'
982
982
  import { formatNumber as _formatNumber } from '../utils/numberFormat.js'
983
983
  import { Mask, MaskInput } from '../utils/mask.js'
984
984
  import { listVersions, getVersion, deleteVersion, saveOrTouchVersion, putBuffer, getBuffer, clearBuffer } from '../utils/idbStore.js'
@@ -1273,13 +1273,13 @@ const ExcelGridComponent = {
1273
1273
  * shrinking columns illegibly thin.
1274
1274
  *
1275
1275
  * Highest-priority width control: overrides `col.width` as an absolute
1276
- * value, `autoWidth`, and double-click auto-fit (still disabled while
1277
- * this is on). Manual column resize (drag handle) IS allowed — dragging
1278
- * a column's handle turns that column into a fixed px width, excluded
1279
- * from the fit; the rest keep scaling proportionally to fill what's
1280
- * left, which can reintroduce horizontal scroll if the fixed column(s)
1281
- * alone exceed the viewport. Pinned columns count toward the fit too —
1282
- * the goal is the whole row fitting, regardless of pin state.
1276
+ * value and `autoWidth`. Manual column resize (drag handle) and
1277
+ * double-click auto-fit ARE both allowed — either one turns that column
1278
+ * into a fixed px width, excluded from the fit; the rest keep scaling
1279
+ * proportionally to fill what's left, which can reintroduce horizontal
1280
+ * scroll if the fixed column(s) alone exceed the viewport. Pinned
1281
+ * columns count toward the fit too — the goal is the whole row fitting,
1282
+ * regardless of pin state.
1283
1283
  * @type {Boolean}
1284
1284
  * @default false
1285
1285
  * @example
@@ -4536,7 +4536,9 @@ const ExcelGridComponent = {
4536
4536
  rawOptions: this.resolveOptionsArray(col, ri, ci),
4537
4537
  currentValue,
4538
4538
  rect: rectObj,
4539
- enableSearch: !!col.enableSearch,
4539
+ // Defaults to on (opt out with enableSearch: false) — same convention
4540
+ // as clearable below.
4541
+ enableSearch: col.enableSearch !== false,
4540
4542
  clearable: col.clearable !== false
4541
4543
  }
4542
4544
  },
@@ -4620,7 +4622,7 @@ const ExcelGridComponent = {
4620
4622
  if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)
4621
4623
  if (col.type === 'date') {
4622
4624
  const d = parseDateValue(val)
4623
- return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)
4625
+ return d ? formatDate(d, col.format || DEFAULT_DATE_FORMAT) : (val == null ? '' : val)
4624
4626
  }
4625
4627
  return val == null ? '' : val
4626
4628
  },
@@ -4950,7 +4952,7 @@ const ExcelGridComponent = {
4950
4952
  const val = this.getCellValue(row, col)
4951
4953
  this.datePickerOverlay = {
4952
4954
  row, col,
4953
- format: colDef.format || 'YYYY-MM-DD',
4955
+ format: colDef.format || DEFAULT_DATE_FORMAT,
4954
4956
  value: val == null ? '' : String(val),
4955
4957
  rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }
4956
4958
  }
@@ -6036,8 +6038,12 @@ const ExcelGridComponent = {
6036
6038
  this.resizing.active = false
6037
6039
  },
6038
6040
 
6041
+ // Double-click-the-header "AutoFit column width" — same idea as Excel/Sheets'
6042
+ // double-click-the-border shortcut. Under fitColumns, same override
6043
+ // treatment as a manual drag-resize (see commitResize/_applyFitColumns):
6044
+ // this column becomes a fixed px width excluded from the proportional
6045
+ // fit, the rest keep scaling in what's left.
6039
6046
  autoFitColumn(ci) {
6040
- if (this.fitColumns) return
6041
6047
  const col = this.leafCols[ci]
6042
6048
  if (!col) return
6043
6049
  let maxLen = (col.title || '').length
@@ -6049,6 +6055,10 @@ const ExcelGridComponent = {
6049
6055
  this.$set(this.colWidths, ci, newWidth)
6050
6056
  this.$delete(this.relativeColSpecs, ci)
6051
6057
  this._cellStyleCache = Object.create(null)
6058
+ if (this.fitColumns) {
6059
+ this.$set(this.fitColumnsOverrides, col.key, true)
6060
+ this._applyFitColumns()
6061
+ }
6052
6062
  this.$emit('column-resize', { index: ci, newSize: newWidth })
6053
6063
  },
6054
6064
 
@@ -8432,6 +8442,35 @@ export default ExcelGridComponent
8432
8442
  line-height: 1;
8433
8443
  }
8434
8444
 
8445
+ /* ===== DATE CELL ICON ===== */
8446
+ /* Same reasoning/placement as the select caret above — reserve room at the
8447
+ cell level so col.align:'right'/centered text never reaches the icon's
8448
+ zone, regardless of alignment. mask-image (not content:url()/an emoji) so
8449
+ the icon is a single flat color that follows --eg-color-text-muted in
8450
+ every theme, light and dark alike, instead of a fixed-color raster/emoji
8451
+ glyph that wouldn't match. */
8452
+ .eg-cell-date:not(.eg-cell--has-suffix) {
8453
+ padding-right: calc(var(--eg-cell-padding-x, var(--eg-cell-padding-x-default)) + 16px);
8454
+ }
8455
+ .eg-cell--has-suffix.eg-cell-date::after { display: none; }
8456
+ .eg-cell-date::after {
8457
+ content: '';
8458
+ position: absolute;
8459
+ right: 5px;
8460
+ top: 50%;
8461
+ width: 12px;
8462
+ height: 12px;
8463
+ transform: translateY(-50%);
8464
+ background-color: var(--eg-color-text-muted, var(--eg-color-text-muted-default));
8465
+ pointer-events: none;
8466
+ -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");
8467
+ 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");
8468
+ -webkit-mask-repeat: no-repeat;
8469
+ mask-repeat: no-repeat;
8470
+ -webkit-mask-size: contain;
8471
+ mask-size: contain;
8472
+ }
8473
+
8435
8474
  /* ===== CHECKBOX CELL ===== */
8436
8475
  .eg-cell-checkbox {
8437
8476
  justify-content: center;
@@ -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))