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/dist/vue2-excel-grid.esm.js +132 -54
- package/dist/vue2-excel-grid.umd.js +2 -2
- package/package.json +1 -1
- package/src/components/EgDatePicker.vue +6 -11
- package/src/components/ExcelGrid.vue +51 -12
- package/src/utils/dateUtils.js +73 -20
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
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
|
|
1277
|
-
*
|
|
1278
|
-
* a
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
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:
|
|
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 ||
|
|
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 ||
|
|
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;
|
package/src/utils/dateUtils.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Display/storage format controlled by column config `format` prop
|
|
6
|
+
* (default 'DD-MM-YYYY').
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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 =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if ((m =
|
|
36
|
-
return
|
|
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=
|
|
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 ||
|
|
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))
|