vue2-excel-grid 2.6.7 → 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
|
@@ -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'
|
|
@@ -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
|
}
|
|
@@ -8440,6 +8442,35 @@ export default ExcelGridComponent
|
|
|
8440
8442
|
line-height: 1;
|
|
8441
8443
|
}
|
|
8442
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
|
+
|
|
8443
8474
|
/* ===== CHECKBOX CELL ===== */
|
|
8444
8475
|
.eg-cell-checkbox {
|
|
8445
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))
|