vue2-excel-grid 1.1.75 → 1.1.77
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 +280 -80
- package/dist/vue2-excel-grid.umd.js +1 -1
- package/package.json +1 -1
- package/src/components/EgDatePicker.vue +4 -0
- package/src/components/ExcelGrid.vue +196 -27
- package/src/utils/i18n.js +16 -12
|
@@ -1316,9 +1316,10 @@ const messages = {
|
|
|
1316
1316
|
filterChecked: 'Checked',
|
|
1317
1317
|
filterUnchecked: 'Unchecked',
|
|
1318
1318
|
// column sorting
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1319
|
+
sortMenuTooltip: 'Sort options',
|
|
1320
|
+
sortMenuAsc: 'A → Z',
|
|
1321
|
+
sortMenuDesc: 'Z → A',
|
|
1322
|
+
sortMenuCancel: 'Clear sort'
|
|
1322
1323
|
},
|
|
1323
1324
|
|
|
1324
1325
|
ru: {
|
|
@@ -1368,9 +1369,10 @@ const messages = {
|
|
|
1368
1369
|
filterChecked: 'Отмечено',
|
|
1369
1370
|
filterUnchecked: 'Не отмечено',
|
|
1370
1371
|
// column sorting
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1372
|
+
sortMenuTooltip: 'Параметры сортировки',
|
|
1373
|
+
sortMenuAsc: 'A → Z',
|
|
1374
|
+
sortMenuDesc: 'Z → A',
|
|
1375
|
+
sortMenuCancel: 'Очистить сортировку'
|
|
1374
1376
|
},
|
|
1375
1377
|
|
|
1376
1378
|
uz: {
|
|
@@ -1420,9 +1422,10 @@ const messages = {
|
|
|
1420
1422
|
filterChecked: 'Belgilangan',
|
|
1421
1423
|
filterUnchecked: 'Belgilanmagan',
|
|
1422
1424
|
// column sorting
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1425
|
+
sortMenuTooltip: 'Saralash parametrlari',
|
|
1426
|
+
sortMenuAsc: 'A → Z',
|
|
1427
|
+
sortMenuDesc: 'Z → A',
|
|
1428
|
+
sortMenuCancel: 'Sortni bekor qilish'
|
|
1426
1429
|
},
|
|
1427
1430
|
|
|
1428
1431
|
'uz-Cyrl': {
|
|
@@ -1472,9 +1475,10 @@ const messages = {
|
|
|
1472
1475
|
filterChecked: 'Белгиланган',
|
|
1473
1476
|
filterUnchecked: 'Белгиланмаган',
|
|
1474
1477
|
// column sorting
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
+
sortMenuTooltip: 'Саралаш параметрлари',
|
|
1479
|
+
sortMenuAsc: 'A → Z',
|
|
1480
|
+
sortMenuDesc: 'Z → A',
|
|
1481
|
+
sortMenuCancel: 'Сортни бекор қилиш'
|
|
1478
1482
|
}
|
|
1479
1483
|
};
|
|
1480
1484
|
|
|
@@ -4502,6 +4506,10 @@ var script$1 = {
|
|
|
4502
4506
|
this.viewMonth = d.getMonth();
|
|
4503
4507
|
this.focusDay = d.getDate();
|
|
4504
4508
|
}
|
|
4509
|
+
|
|
4510
|
+
// Raw, not-yet-committed keystroke — host debounces this itself
|
|
4511
|
+
// (col.onInput); 'pick' below is the final, validated commit.
|
|
4512
|
+
this.$emit('typing', this.inputVal);
|
|
4505
4513
|
},
|
|
4506
4514
|
|
|
4507
4515
|
_autoFillSep(raw) {
|
|
@@ -4800,7 +4808,7 @@ __vue_render__$1._withStripped = true;
|
|
|
4800
4808
|
/* style */
|
|
4801
4809
|
const __vue_inject_styles__$1 = function (inject) {
|
|
4802
4810
|
if (!inject) return
|
|
4803
|
-
inject("data-v-38614f57_0", { source: "\n.eg-datepicker {\n background: var(--eg-color-bg, #fff);\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 6px;\n box-shadow: 0 4px 16px rgba(0,0,0,.14);\n width: 216px;\n user-select: none;\n outline: none;\n font-size: 13px;\n}\n\n/* Text input */\n.eg-dp-input-wrap {\n padding: 7px 7px 5px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 5px 8px;\n font-size: 13px;\n outline: none;\n color: var(--eg-color-text, #333);\n background: var(--eg-color-bg, #fff);\n user-select: text;\n}\n.eg-dp-input:focus {\n border-color: var(--eg-color-primary, #1565c0);\n box-shadow: 0 0 0 2px rgba(21,101,192,.15);\n}\n.eg-dp-input--error {\n border-color: var(--eg-color-danger, #c62828);\n box-shadow: 0 0 0 2px rgba(198,40,40,.12);\n}\n\n/* Header */\n.eg-dp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 6px 3px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-nav {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n padding: 0 6px;\n color: var(--eg-color-text, #333);\n border-radius: 3px;\n}\n.eg-dp-nav:hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-title { font-weight: 600; color: var(--eg-color-text, #333);\n}\n\n/* Grid */\n.eg-dp-grid {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n padding: 4px 4px 2px;\n gap: 1px;\n}\n.eg-dp-wd {\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-muted, #888);\n padding: 3px 0 4px;\n font-weight: 600;\n}\n.eg-dp-day {\n text-align: center;\n padding: 5px 2px;\n border-radius: 4px;\n cursor: pointer;\n color: var(--eg-color-text, #333);\n font-size: 12px;\n line-height: 1.4;\n}\n.eg-dp-day--empty { cursor: default;\n}\n.eg-dp-day:not(.eg-dp-day--empty):hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-day--today { font-weight: 700; color: var(--eg-color-primary, #1565c0);\n}\n.eg-dp-day--selected {\n background: var(--eg-color-primary, #1565c0);\n color: #fff;\n font-weight: 600;\n}\n.eg-dp-day--focused:not(.eg-dp-day--selected) {\n outline: 2px solid var(--eg-color-primary, #1565c0);\n outline-offset: -2px;\n}\n\n/* Footer */\n.eg-dp-footer {\n display: flex;\n justify-content: space-between;\n padding: 4px 6px 6px;\n border-top: 1px solid var(--eg-color-border, #e8e8e8);\n gap: 4px;\n}\n.eg-dp-btn {\n flex: 1;\n background: none;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 4px 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--eg-color-text, #333);\n}\n.eg-dp-btn:hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0);\n}\n.eg-dp-btn--clear:hover { background: #fff0f0;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgDatePicker.vue"],"names":[],"mappings":";AAiSA;EACA,oCAAA;EACA,iDAAA;EACA,kBAAA;EACA,sCAAA;EACA,YAAA;EACA,iBAAA;EACA,aAAA;EACA,eAAA;AACA;;AAEA,eAAA;AACA;EACA,oBAAA;EACA,wDAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,iDAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,aAAA;EACA,iCAAA;EACA,oCAAA;EACA,iBAAA;AACA;AACA;EACA,8CAAA;EACA,0CAAA;AACA;AACA;EACA,6CAAA;EACA,yCAAA;AACA;;AAEA,WAAA;AACA;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,oBAAA;EACA,wDAAA;AACA;AACA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,cAAA;EACA,iCAAA;EACA,kBAAA;AACA;AACA,mBAAA,0CAAA;AAAA;AACA,eAAA,gBAAA,EAAA,iCAAA;AAAA;;AAEA,SAAA;AACA;EACA,aAAA;EACA,qCAAA;EACA,oBAAA;EACA,QAAA;AACA;AACA;EACA,kBAAA;EACA,eAAA;EACA,uCAAA;EACA,kBAAA;EACA,gBAAA;AACA;AACA;EACA,kBAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;AACA;AACA,oBAAA,eAAA;AAAA;AACA,0CAAA,0CAAA;AAAA;AACA,oBAAA,gBAAA,EAAA,uCAAA;AAAA;AACA;EACA,4CAAA;EACA,WAAA;EACA,gBAAA;AACA;AACA;EACA,mDAAA;EACA,oBAAA;AACA;;AAEA,WAAA;AACA;EACA,aAAA;EACA,8BAAA;EACA,oBAAA;EACA,qDAAA;EACA,QAAA;AACA;AACA;EACA,OAAA;EACA,gBAAA;EACA,iDAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,eAAA;EACA,iCAAA;AACA;AACA,mBAAA,0CAAA;AAAA;AACA,oBAAA,sCAAA,EAAA,6CAAA;AAAA;AACA,0BAAA,mBAAA;AAAA","file":"EgDatePicker.vue","sourcesContent":["<template>\n <div\n class=\"eg-datepicker\"\n :style=\"overlayStyle\"\n @mousedown.prevent\n @keydown.stop=\"onKeyDown\"\n >\n <!-- Text input — qo'lda sana yozish uchun, avtofocus -->\n <div class=\"eg-dp-input-wrap\" @mousedown.stop>\n <input\n ref=\"textInput\"\n class=\"eg-dp-input\"\n :class=\"{ 'eg-dp-input--error': inputError }\"\n :value=\"inputVal\"\n :placeholder=\"format\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n @input=\"onTextInput\"\n @keydown.stop=\"onInputKeyDown\"\n @mousedown.stop\n />\n </div>\n\n <!-- Header: prev / Month Year / next -->\n <div class=\"eg-dp-header\">\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"prevMonth\">‹</button>\n <span class=\"eg-dp-title\">{{ t.months[viewMonth] }} {{ viewYear }}</span>\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"nextMonth\">›</button>\n </div>\n\n <!-- Weekday labels + Day cells -->\n <div class=\"eg-dp-grid\">\n <div v-for=\"wd in t.weekdays\" :key=\"wd\" class=\"eg-dp-wd\">{{ wd }}</div>\n <div\n v-for=\"(cell, idx) in calCells\"\n :key=\"idx\"\n class=\"eg-dp-day\"\n :class=\"{\n 'eg-dp-day--empty': !cell.day,\n 'eg-dp-day--today': cell.isToday,\n 'eg-dp-day--selected': cell.isSelected,\n 'eg-dp-day--focused': cell.isFocused\n }\"\n @mousedown.prevent=\"cell.day && pickDay(cell.day)\"\n >{{ cell.day || '' }}</div>\n </div>\n\n <!-- Footer: Today / Clear -->\n <div class=\"eg-dp-footer\">\n <button class=\"eg-dp-btn\" tabindex=\"-1\" @mousedown.prevent=\"pickToday\">{{ t.today }}</button>\n <button class=\"eg-dp-btn eg-dp-btn--clear\" tabindex=\"-1\" @mousedown.prevent=\"pickClear\">{{ t.clear }}</button>\n </div>\n </div>\n</template>\n\n<script>\nimport { parseDateValue, formatDate, toISO, todayISO } from '../utils/dateUtils.js'\nimport { getMessages } from '../utils/i18n.js'\n\nexport default {\n name: 'EgDatePicker',\n\n props: {\n value: { default: null },\n format: { type: String, default: 'YYYY-MM-DD' },\n locale: { type: String, default: 'en' },\n triggerRect: { type: Object, default: null }\n },\n\n data() {\n const parsed = parseDateValue(this.value)\n const base = parsed || new Date()\n return {\n viewYear: base.getFullYear(),\n viewMonth: base.getMonth(),\n focusDay: parsed ? parsed.getDate() : null,\n inputVal: parsed ? formatDate(parsed, this.format) : '',\n inputError: false\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n // ISO string of the currently typed/selected date (for calendar highlighting)\n _inputParsedISO() {\n const d = parseDateValue(this.inputVal)\n return d ? toISO(d) : null\n },\n\n calCells() {\n const year = this.viewYear\n const month = this.viewMonth\n const today = todayISO()\n const selISO = this._inputParsedISO\n\n const offset = (new Date(year, month, 1).getDay() + 6) % 7\n const daysInMonth = new Date(year, month + 1, 0).getDate()\n\n const cells = []\n for (let i = 0; i < offset; i++) cells.push({ day: 0 })\n for (let d = 1; d <= daysInMonth; d++) {\n const iso = `${year}-${String(month + 1).padStart(2,'0')}-${String(d).padStart(2,'0')}`\n cells.push({\n day: d,\n isToday: iso === today,\n isSelected: iso === selISO,\n isFocused: this.focusDay === d && iso !== selISO\n })\n }\n while (cells.length % 7 !== 0) cells.push({ day: 0 })\n return cells\n },\n\n overlayStyle() {\n const rect = this.triggerRect\n if (!rect) return { position: 'fixed', top: '50px', left: '50px', zIndex: 9999 }\n const calH = 330\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= calH ? rect.bottom + 2 : rect.top - calH - 2\n const left = Math.min(rect.left, window.innerWidth - 224)\n return {\n position: 'fixed',\n top: top + 'px',\n left: Math.max(4, left) + 'px',\n zIndex: 9999\n }\n }\n },\n\n mounted() {\n this.$nextTick(() => {\n if (this.$refs.textInput) {\n this.$refs.textInput.focus()\n this.$refs.textInput.select()\n }\n })\n },\n\n methods: {\n // ── Text input ──────────────────────────────────────────────\n onTextInput(e) {\n const isDeleting = e.inputType\n ? e.inputType.startsWith('delete')\n : e.target.value.length < this.inputVal.length\n\n this.inputVal = isDeleting ? e.target.value : this._autoFillSep(e.target.value)\n this.inputError = false\n\n if (!isDeleting) {\n this.$nextTick(() => {\n const el = this.$refs.textInput\n if (el) el.setSelectionRange(this.inputVal.length, this.inputVal.length)\n })\n }\n\n const d = parseDateValue(this.inputVal)\n if (d) {\n this.viewYear = d.getFullYear()\n this.viewMonth = d.getMonth()\n this.focusDay = d.getDate()\n }\n },\n\n _autoFillSep(raw) {\n const fmt = this.format\n let sep, segments\n if (fmt === 'YYYY-MM-DD') { sep = '-'; segments = [4, 2, 2] }\n else if (fmt === 'DD.MM.YYYY') { sep = '.'; segments = [2, 2, 4] }\n else if (fmt === 'DD/MM/YYYY') { sep = '/'; segments = [2, 2, 4] }\n else if (fmt === 'MM/DD/YYYY') { sep = '/'; segments = [2, 2, 4] }\n else return raw\n\n const maxDigits = segments.reduce((a, b) => a + b, 0)\n const digits = raw.replace(/\\D/g, '').slice(0, maxDigits)\n\n let result = ''\n let pos = 0\n for (let i = 0; i < segments.length; i++) {\n const len = segments[i]\n const chunk = digits.slice(pos, pos + len)\n result += chunk\n pos += len\n if (chunk.length === len && i < segments.length - 1) result += sep\n }\n return result\n },\n\n onInputKeyDown(e) {\n if (e.key === 'Enter') {\n e.preventDefault()\n this._commitInput()\n } else if (e.key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this._commitInput()\n }\n // Stop all keys from bubbling to the grid wrapper\n },\n\n _commitInput() {\n const val = this.inputVal.trim()\n if (val === '') { this.$emit('pick', ''); return }\n const d = parseDateValue(val)\n if (!d) { this.inputError = true; return }\n this.$emit('pick', formatDate(d, this.format))\n },\n\n // ── Month navigation ────────────────────────────────────────\n prevMonth() {\n if (this.viewMonth === 0) { this.viewMonth = 11; this.viewYear-- }\n else this.viewMonth--\n this._clampFocusDay()\n },\n nextMonth() {\n if (this.viewMonth === 11) { this.viewMonth = 0; this.viewYear++ }\n else this.viewMonth++\n this._clampFocusDay()\n },\n\n // ── Calendar day pick ───────────────────────────────────────\n pickDay(day) {\n if (!day) return\n const iso = `${this.viewYear}-${String(this.viewMonth + 1).padStart(2,'0')}-${String(day).padStart(2,'0')}`\n const d = parseDateValue(iso)\n const formatted = formatDate(d, this.format)\n this.inputVal = formatted\n this.inputError = false\n this.$emit('pick', formatted)\n },\n\n pickToday() {\n const d = new Date()\n const formatted = formatDate(d, this.format)\n this.inputVal = formatted\n this.inputError = false\n this.$emit('pick', formatted)\n },\n\n pickClear() {\n this.inputVal = ''\n this.inputError = false\n this.$emit('pick', '')\n },\n\n // ── Outer div keyboard (when input is NOT focused) ──────────\n onKeyDown(e) {\n const key = e.key\n if (key === 'Escape') { e.preventDefault(); this.$emit('close'); return }\n if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n if (this.focusDay) this.pickDay(this.focusDay)\n return\n }\n if (key === 'PageUp') { e.preventDefault(); this.prevMonth(); return }\n if (key === 'PageDown') { e.preventDefault(); this.nextMonth(); return }\n if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(key)) {\n e.preventDefault()\n this._moveFocus(key)\n }\n },\n\n _moveFocus(key) {\n const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()\n let day = this.focusDay || 1\n const delta = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : key === 'ArrowUp' ? -7 : 7\n day += delta\n if (day < 1) {\n this.prevMonth()\n this.focusDay = Math.max(1, new Date(this.viewYear, this.viewMonth + 1, 0).getDate() + day + 1)\n } else if (day > daysInMonth) {\n this.nextMonth()\n this.focusDay = Math.min(day - daysInMonth, new Date(this.viewYear, this.viewMonth + 1, 0).getDate())\n } else {\n this.focusDay = day\n }\n },\n\n _clampFocusDay() {\n const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()\n if (this.focusDay && this.focusDay > daysInMonth) this.focusDay = daysInMonth\n }\n }\n}\n</script>\n\n<style>\n.eg-datepicker {\n background: var(--eg-color-bg, #fff);\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 6px;\n box-shadow: 0 4px 16px rgba(0,0,0,.14);\n width: 216px;\n user-select: none;\n outline: none;\n font-size: 13px;\n}\n\n/* Text input */\n.eg-dp-input-wrap {\n padding: 7px 7px 5px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 5px 8px;\n font-size: 13px;\n outline: none;\n color: var(--eg-color-text, #333);\n background: var(--eg-color-bg, #fff);\n user-select: text;\n}\n.eg-dp-input:focus {\n border-color: var(--eg-color-primary, #1565c0);\n box-shadow: 0 0 0 2px rgba(21,101,192,.15);\n}\n.eg-dp-input--error {\n border-color: var(--eg-color-danger, #c62828);\n box-shadow: 0 0 0 2px rgba(198,40,40,.12);\n}\n\n/* Header */\n.eg-dp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 6px 3px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-nav {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n padding: 0 6px;\n color: var(--eg-color-text, #333);\n border-radius: 3px;\n}\n.eg-dp-nav:hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-title { font-weight: 600; color: var(--eg-color-text, #333); }\n\n/* Grid */\n.eg-dp-grid {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n padding: 4px 4px 2px;\n gap: 1px;\n}\n.eg-dp-wd {\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-muted, #888);\n padding: 3px 0 4px;\n font-weight: 600;\n}\n.eg-dp-day {\n text-align: center;\n padding: 5px 2px;\n border-radius: 4px;\n cursor: pointer;\n color: var(--eg-color-text, #333);\n font-size: 12px;\n line-height: 1.4;\n}\n.eg-dp-day--empty { cursor: default; }\n.eg-dp-day:not(.eg-dp-day--empty):hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-day--today { font-weight: 700; color: var(--eg-color-primary, #1565c0); }\n.eg-dp-day--selected {\n background: var(--eg-color-primary, #1565c0);\n color: #fff;\n font-weight: 600;\n}\n.eg-dp-day--focused:not(.eg-dp-day--selected) {\n outline: 2px solid var(--eg-color-primary, #1565c0);\n outline-offset: -2px;\n}\n\n/* Footer */\n.eg-dp-footer {\n display: flex;\n justify-content: space-between;\n padding: 4px 6px 6px;\n border-top: 1px solid var(--eg-color-border, #e8e8e8);\n gap: 4px;\n}\n.eg-dp-btn {\n flex: 1;\n background: none;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 4px 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--eg-color-text, #333);\n}\n.eg-dp-btn:hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0); }\n.eg-dp-btn--clear:hover { background: #fff0f0; }\n</style>\n"]}, media: undefined });
|
|
4811
|
+
inject("data-v-6da101b3_0", { source: "\n.eg-datepicker {\n background: var(--eg-color-bg, #fff);\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 6px;\n box-shadow: 0 4px 16px rgba(0,0,0,.14);\n width: 216px;\n user-select: none;\n outline: none;\n font-size: 13px;\n}\n\n/* Text input */\n.eg-dp-input-wrap {\n padding: 7px 7px 5px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 5px 8px;\n font-size: 13px;\n outline: none;\n color: var(--eg-color-text, #333);\n background: var(--eg-color-bg, #fff);\n user-select: text;\n}\n.eg-dp-input:focus {\n border-color: var(--eg-color-primary, #1565c0);\n box-shadow: 0 0 0 2px rgba(21,101,192,.15);\n}\n.eg-dp-input--error {\n border-color: var(--eg-color-danger, #c62828);\n box-shadow: 0 0 0 2px rgba(198,40,40,.12);\n}\n\n/* Header */\n.eg-dp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 6px 3px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-nav {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n padding: 0 6px;\n color: var(--eg-color-text, #333);\n border-radius: 3px;\n}\n.eg-dp-nav:hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-title { font-weight: 600; color: var(--eg-color-text, #333);\n}\n\n/* Grid */\n.eg-dp-grid {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n padding: 4px 4px 2px;\n gap: 1px;\n}\n.eg-dp-wd {\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-muted, #888);\n padding: 3px 0 4px;\n font-weight: 600;\n}\n.eg-dp-day {\n text-align: center;\n padding: 5px 2px;\n border-radius: 4px;\n cursor: pointer;\n color: var(--eg-color-text, #333);\n font-size: 12px;\n line-height: 1.4;\n}\n.eg-dp-day--empty { cursor: default;\n}\n.eg-dp-day:not(.eg-dp-day--empty):hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-day--today { font-weight: 700; color: var(--eg-color-primary, #1565c0);\n}\n.eg-dp-day--selected {\n background: var(--eg-color-primary, #1565c0);\n color: #fff;\n font-weight: 600;\n}\n.eg-dp-day--focused:not(.eg-dp-day--selected) {\n outline: 2px solid var(--eg-color-primary, #1565c0);\n outline-offset: -2px;\n}\n\n/* Footer */\n.eg-dp-footer {\n display: flex;\n justify-content: space-between;\n padding: 4px 6px 6px;\n border-top: 1px solid var(--eg-color-border, #e8e8e8);\n gap: 4px;\n}\n.eg-dp-btn {\n flex: 1;\n background: none;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 4px 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--eg-color-text, #333);\n}\n.eg-dp-btn:hover { background: var(--eg-color-hover, #f0f0f0);\n}\n.eg-dp-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0);\n}\n.eg-dp-btn--clear:hover { background: #fff0f0;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgDatePicker.vue"],"names":[],"mappings":";AAqSA;EACA,oCAAA;EACA,iDAAA;EACA,kBAAA;EACA,sCAAA;EACA,YAAA;EACA,iBAAA;EACA,aAAA;EACA,eAAA;AACA;;AAEA,eAAA;AACA;EACA,oBAAA;EACA,wDAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,iDAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,aAAA;EACA,iCAAA;EACA,oCAAA;EACA,iBAAA;AACA;AACA;EACA,8CAAA;EACA,0CAAA;AACA;AACA;EACA,6CAAA;EACA,yCAAA;AACA;;AAEA,WAAA;AACA;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,oBAAA;EACA,wDAAA;AACA;AACA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,cAAA;EACA,iCAAA;EACA,kBAAA;AACA;AACA,mBAAA,0CAAA;AAAA;AACA,eAAA,gBAAA,EAAA,iCAAA;AAAA;;AAEA,SAAA;AACA;EACA,aAAA;EACA,qCAAA;EACA,oBAAA;EACA,QAAA;AACA;AACA;EACA,kBAAA;EACA,eAAA;EACA,uCAAA;EACA,kBAAA;EACA,gBAAA;AACA;AACA;EACA,kBAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;AACA;AACA,oBAAA,eAAA;AAAA;AACA,0CAAA,0CAAA;AAAA;AACA,oBAAA,gBAAA,EAAA,uCAAA;AAAA;AACA;EACA,4CAAA;EACA,WAAA;EACA,gBAAA;AACA;AACA;EACA,mDAAA;EACA,oBAAA;AACA;;AAEA,WAAA;AACA;EACA,aAAA;EACA,8BAAA;EACA,oBAAA;EACA,qDAAA;EACA,QAAA;AACA;AACA;EACA,OAAA;EACA,gBAAA;EACA,iDAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,eAAA;EACA,iCAAA;AACA;AACA,mBAAA,0CAAA;AAAA;AACA,oBAAA,sCAAA,EAAA,6CAAA;AAAA;AACA,0BAAA,mBAAA;AAAA","file":"EgDatePicker.vue","sourcesContent":["<template>\n <div\n class=\"eg-datepicker\"\n :style=\"overlayStyle\"\n @mousedown.prevent\n @keydown.stop=\"onKeyDown\"\n >\n <!-- Text input — qo'lda sana yozish uchun, avtofocus -->\n <div class=\"eg-dp-input-wrap\" @mousedown.stop>\n <input\n ref=\"textInput\"\n class=\"eg-dp-input\"\n :class=\"{ 'eg-dp-input--error': inputError }\"\n :value=\"inputVal\"\n :placeholder=\"format\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n @input=\"onTextInput\"\n @keydown.stop=\"onInputKeyDown\"\n @mousedown.stop\n />\n </div>\n\n <!-- Header: prev / Month Year / next -->\n <div class=\"eg-dp-header\">\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"prevMonth\">‹</button>\n <span class=\"eg-dp-title\">{{ t.months[viewMonth] }} {{ viewYear }}</span>\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"nextMonth\">›</button>\n </div>\n\n <!-- Weekday labels + Day cells -->\n <div class=\"eg-dp-grid\">\n <div v-for=\"wd in t.weekdays\" :key=\"wd\" class=\"eg-dp-wd\">{{ wd }}</div>\n <div\n v-for=\"(cell, idx) in calCells\"\n :key=\"idx\"\n class=\"eg-dp-day\"\n :class=\"{\n 'eg-dp-day--empty': !cell.day,\n 'eg-dp-day--today': cell.isToday,\n 'eg-dp-day--selected': cell.isSelected,\n 'eg-dp-day--focused': cell.isFocused\n }\"\n @mousedown.prevent=\"cell.day && pickDay(cell.day)\"\n >{{ cell.day || '' }}</div>\n </div>\n\n <!-- Footer: Today / Clear -->\n <div class=\"eg-dp-footer\">\n <button class=\"eg-dp-btn\" tabindex=\"-1\" @mousedown.prevent=\"pickToday\">{{ t.today }}</button>\n <button class=\"eg-dp-btn eg-dp-btn--clear\" tabindex=\"-1\" @mousedown.prevent=\"pickClear\">{{ t.clear }}</button>\n </div>\n </div>\n</template>\n\n<script>\nimport { parseDateValue, formatDate, toISO, todayISO } from '../utils/dateUtils.js'\nimport { getMessages } from '../utils/i18n.js'\n\nexport default {\n name: 'EgDatePicker',\n\n props: {\n value: { default: null },\n format: { type: String, default: 'YYYY-MM-DD' },\n locale: { type: String, default: 'en' },\n triggerRect: { type: Object, default: null }\n },\n\n data() {\n const parsed = parseDateValue(this.value)\n const base = parsed || new Date()\n return {\n viewYear: base.getFullYear(),\n viewMonth: base.getMonth(),\n focusDay: parsed ? parsed.getDate() : null,\n inputVal: parsed ? formatDate(parsed, this.format) : '',\n inputError: false\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n // ISO string of the currently typed/selected date (for calendar highlighting)\n _inputParsedISO() {\n const d = parseDateValue(this.inputVal)\n return d ? toISO(d) : null\n },\n\n calCells() {\n const year = this.viewYear\n const month = this.viewMonth\n const today = todayISO()\n const selISO = this._inputParsedISO\n\n const offset = (new Date(year, month, 1).getDay() + 6) % 7\n const daysInMonth = new Date(year, month + 1, 0).getDate()\n\n const cells = []\n for (let i = 0; i < offset; i++) cells.push({ day: 0 })\n for (let d = 1; d <= daysInMonth; d++) {\n const iso = `${year}-${String(month + 1).padStart(2,'0')}-${String(d).padStart(2,'0')}`\n cells.push({\n day: d,\n isToday: iso === today,\n isSelected: iso === selISO,\n isFocused: this.focusDay === d && iso !== selISO\n })\n }\n while (cells.length % 7 !== 0) cells.push({ day: 0 })\n return cells\n },\n\n overlayStyle() {\n const rect = this.triggerRect\n if (!rect) return { position: 'fixed', top: '50px', left: '50px', zIndex: 9999 }\n const calH = 330\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= calH ? rect.bottom + 2 : rect.top - calH - 2\n const left = Math.min(rect.left, window.innerWidth - 224)\n return {\n position: 'fixed',\n top: top + 'px',\n left: Math.max(4, left) + 'px',\n zIndex: 9999\n }\n }\n },\n\n mounted() {\n this.$nextTick(() => {\n if (this.$refs.textInput) {\n this.$refs.textInput.focus()\n this.$refs.textInput.select()\n }\n })\n },\n\n methods: {\n // ── Text input ──────────────────────────────────────────────\n onTextInput(e) {\n const isDeleting = e.inputType\n ? e.inputType.startsWith('delete')\n : e.target.value.length < this.inputVal.length\n\n this.inputVal = isDeleting ? e.target.value : this._autoFillSep(e.target.value)\n this.inputError = false\n\n if (!isDeleting) {\n this.$nextTick(() => {\n const el = this.$refs.textInput\n if (el) el.setSelectionRange(this.inputVal.length, this.inputVal.length)\n })\n }\n\n const d = parseDateValue(this.inputVal)\n if (d) {\n this.viewYear = d.getFullYear()\n this.viewMonth = d.getMonth()\n this.focusDay = d.getDate()\n }\n\n // Raw, not-yet-committed keystroke — host debounces this itself\n // (col.onInput); 'pick' below is the final, validated commit.\n this.$emit('typing', this.inputVal)\n },\n\n _autoFillSep(raw) {\n const fmt = this.format\n let sep, segments\n if (fmt === 'YYYY-MM-DD') { sep = '-'; segments = [4, 2, 2] }\n else if (fmt === 'DD.MM.YYYY') { sep = '.'; segments = [2, 2, 4] }\n else if (fmt === 'DD/MM/YYYY') { sep = '/'; segments = [2, 2, 4] }\n else if (fmt === 'MM/DD/YYYY') { sep = '/'; segments = [2, 2, 4] }\n else return raw\n\n const maxDigits = segments.reduce((a, b) => a + b, 0)\n const digits = raw.replace(/\\D/g, '').slice(0, maxDigits)\n\n let result = ''\n let pos = 0\n for (let i = 0; i < segments.length; i++) {\n const len = segments[i]\n const chunk = digits.slice(pos, pos + len)\n result += chunk\n pos += len\n if (chunk.length === len && i < segments.length - 1) result += sep\n }\n return result\n },\n\n onInputKeyDown(e) {\n if (e.key === 'Enter') {\n e.preventDefault()\n this._commitInput()\n } else if (e.key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this._commitInput()\n }\n // Stop all keys from bubbling to the grid wrapper\n },\n\n _commitInput() {\n const val = this.inputVal.trim()\n if (val === '') { this.$emit('pick', ''); return }\n const d = parseDateValue(val)\n if (!d) { this.inputError = true; return }\n this.$emit('pick', formatDate(d, this.format))\n },\n\n // ── Month navigation ────────────────────────────────────────\n prevMonth() {\n if (this.viewMonth === 0) { this.viewMonth = 11; this.viewYear-- }\n else this.viewMonth--\n this._clampFocusDay()\n },\n nextMonth() {\n if (this.viewMonth === 11) { this.viewMonth = 0; this.viewYear++ }\n else this.viewMonth++\n this._clampFocusDay()\n },\n\n // ── Calendar day pick ───────────────────────────────────────\n pickDay(day) {\n if (!day) return\n const iso = `${this.viewYear}-${String(this.viewMonth + 1).padStart(2,'0')}-${String(day).padStart(2,'0')}`\n const d = parseDateValue(iso)\n const formatted = formatDate(d, this.format)\n this.inputVal = formatted\n this.inputError = false\n this.$emit('pick', formatted)\n },\n\n pickToday() {\n const d = new Date()\n const formatted = formatDate(d, this.format)\n this.inputVal = formatted\n this.inputError = false\n this.$emit('pick', formatted)\n },\n\n pickClear() {\n this.inputVal = ''\n this.inputError = false\n this.$emit('pick', '')\n },\n\n // ── Outer div keyboard (when input is NOT focused) ──────────\n onKeyDown(e) {\n const key = e.key\n if (key === 'Escape') { e.preventDefault(); this.$emit('close'); return }\n if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n if (this.focusDay) this.pickDay(this.focusDay)\n return\n }\n if (key === 'PageUp') { e.preventDefault(); this.prevMonth(); return }\n if (key === 'PageDown') { e.preventDefault(); this.nextMonth(); return }\n if (['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(key)) {\n e.preventDefault()\n this._moveFocus(key)\n }\n },\n\n _moveFocus(key) {\n const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()\n let day = this.focusDay || 1\n const delta = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : key === 'ArrowUp' ? -7 : 7\n day += delta\n if (day < 1) {\n this.prevMonth()\n this.focusDay = Math.max(1, new Date(this.viewYear, this.viewMonth + 1, 0).getDate() + day + 1)\n } else if (day > daysInMonth) {\n this.nextMonth()\n this.focusDay = Math.min(day - daysInMonth, new Date(this.viewYear, this.viewMonth + 1, 0).getDate())\n } else {\n this.focusDay = day\n }\n },\n\n _clampFocusDay() {\n const daysInMonth = new Date(this.viewYear, this.viewMonth + 1, 0).getDate()\n if (this.focusDay && this.focusDay > daysInMonth) this.focusDay = daysInMonth\n }\n }\n}\n</script>\n\n<style>\n.eg-datepicker {\n background: var(--eg-color-bg, #fff);\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 6px;\n box-shadow: 0 4px 16px rgba(0,0,0,.14);\n width: 216px;\n user-select: none;\n outline: none;\n font-size: 13px;\n}\n\n/* Text input */\n.eg-dp-input-wrap {\n padding: 7px 7px 5px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 5px 8px;\n font-size: 13px;\n outline: none;\n color: var(--eg-color-text, #333);\n background: var(--eg-color-bg, #fff);\n user-select: text;\n}\n.eg-dp-input:focus {\n border-color: var(--eg-color-primary, #1565c0);\n box-shadow: 0 0 0 2px rgba(21,101,192,.15);\n}\n.eg-dp-input--error {\n border-color: var(--eg-color-danger, #c62828);\n box-shadow: 0 0 0 2px rgba(198,40,40,.12);\n}\n\n/* Header */\n.eg-dp-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 6px 3px;\n border-bottom: 1px solid var(--eg-color-border, #e8e8e8);\n}\n.eg-dp-nav {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n padding: 0 6px;\n color: var(--eg-color-text, #333);\n border-radius: 3px;\n}\n.eg-dp-nav:hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-title { font-weight: 600; color: var(--eg-color-text, #333); }\n\n/* Grid */\n.eg-dp-grid {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n padding: 4px 4px 2px;\n gap: 1px;\n}\n.eg-dp-wd {\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-muted, #888);\n padding: 3px 0 4px;\n font-weight: 600;\n}\n.eg-dp-day {\n text-align: center;\n padding: 5px 2px;\n border-radius: 4px;\n cursor: pointer;\n color: var(--eg-color-text, #333);\n font-size: 12px;\n line-height: 1.4;\n}\n.eg-dp-day--empty { cursor: default; }\n.eg-dp-day:not(.eg-dp-day--empty):hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-day--today { font-weight: 700; color: var(--eg-color-primary, #1565c0); }\n.eg-dp-day--selected {\n background: var(--eg-color-primary, #1565c0);\n color: #fff;\n font-weight: 600;\n}\n.eg-dp-day--focused:not(.eg-dp-day--selected) {\n outline: 2px solid var(--eg-color-primary, #1565c0);\n outline-offset: -2px;\n}\n\n/* Footer */\n.eg-dp-footer {\n display: flex;\n justify-content: space-between;\n padding: 4px 6px 6px;\n border-top: 1px solid var(--eg-color-border, #e8e8e8);\n gap: 4px;\n}\n.eg-dp-btn {\n flex: 1;\n background: none;\n border: 1px solid var(--eg-color-border, #d0d0d0);\n border-radius: 4px;\n padding: 4px 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--eg-color-text, #333);\n}\n.eg-dp-btn:hover { background: var(--eg-color-hover, #f0f0f0); }\n.eg-dp-btn--clear { color: var(--eg-color-danger, #c62828); border-color: var(--eg-color-danger, #f1b0b0); }\n.eg-dp-btn--clear:hover { background: #fff0f0; }\n</style>\n"]}, media: undefined });
|
|
4804
4812
|
|
|
4805
4813
|
};
|
|
4806
4814
|
/* scoped */
|
|
@@ -5199,6 +5207,10 @@ var script = {
|
|
|
5199
5207
|
// doesn't cause a visible jump.
|
|
5200
5208
|
contextMenuPositioned: false,
|
|
5201
5209
|
|
|
5210
|
+
// Sort popup menu (header sort icon click) — { visible, x, y, key }
|
|
5211
|
+
sortMenu: { visible: false, x: 0, y: 0, key: null },
|
|
5212
|
+
sortMenuPositioned: false,
|
|
5213
|
+
|
|
5202
5214
|
// devMode — column/row config info modal ({ title, json } | null)
|
|
5203
5215
|
infoModal: null,
|
|
5204
5216
|
|
|
@@ -5901,6 +5913,7 @@ var script = {
|
|
|
5901
5913
|
|
|
5902
5914
|
beforeDestroy() {
|
|
5903
5915
|
clearTimeout(this._filterRecomputeTimer);
|
|
5916
|
+
clearTimeout(this._onInputTimer);
|
|
5904
5917
|
document.removeEventListener('mousemove', this.onGlobalMouseMove);
|
|
5905
5918
|
document.removeEventListener('mouseup', this.onGlobalMouseUp);
|
|
5906
5919
|
document.removeEventListener('keydown', this.onGlobalKeyDown);
|
|
@@ -6378,8 +6391,11 @@ var script = {
|
|
|
6378
6391
|
|
|
6379
6392
|
getFooterCellClass(ci) {
|
|
6380
6393
|
const col = this.leafCols[ci];
|
|
6381
|
-
|
|
6382
|
-
|
|
6394
|
+
if (!col) return null
|
|
6395
|
+
const classList = col.totalConfig && col.totalConfig.classList;
|
|
6396
|
+
const base = this.isColumnTotalVisible(col) ? 'eg-footer-cell-total' : null;
|
|
6397
|
+
if (!base) return classList || null
|
|
6398
|
+
return classList ? [base, classList] : base
|
|
6383
6399
|
},
|
|
6384
6400
|
|
|
6385
6401
|
isCellVisible(cell, rowIndex, isFrozen) {
|
|
@@ -6719,6 +6735,14 @@ var script = {
|
|
|
6719
6735
|
this.contextMenu.visible = false;
|
|
6720
6736
|
},
|
|
6721
6737
|
|
|
6738
|
+
// General click-anywhere-in-the-grid dismissal for both floating menus —
|
|
6739
|
+
// each menu item stops propagation (@click.stop) so a click *inside*
|
|
6740
|
+
// either menu never reaches this handler.
|
|
6741
|
+
hideFloatingMenus() {
|
|
6742
|
+
this.hideContextMenu();
|
|
6743
|
+
this.hideSortMenu();
|
|
6744
|
+
},
|
|
6745
|
+
|
|
6722
6746
|
// =========================================================
|
|
6723
6747
|
// KEYBOARD NAVIGATION (3.2)
|
|
6724
6748
|
// =========================================================
|
|
@@ -7034,15 +7058,62 @@ var script = {
|
|
|
7034
7058
|
const newData = oldData.slice();
|
|
7035
7059
|
newData[row] = { ...oldData[row], [colDef.key]: this.editValue };
|
|
7036
7060
|
this._emitDataChange(oldData, newData);
|
|
7061
|
+
this._flushOnInput(row, col, this.editValue, newData[row]);
|
|
7037
7062
|
this.editingCell = null;
|
|
7038
7063
|
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
7039
7064
|
},
|
|
7040
7065
|
|
|
7041
7066
|
cancelEdit() {
|
|
7067
|
+
clearTimeout(this._onInputTimer);
|
|
7042
7068
|
this.editingCell = null;
|
|
7043
7069
|
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
7044
7070
|
},
|
|
7045
7071
|
|
|
7072
|
+
// Fires while the user is actively typing in the plain-text edit input —
|
|
7073
|
+
// debounced (col.debounceMs, default 400ms) so col.onInput isn't called
|
|
7074
|
+
// on every keystroke. `row` is the pre-edit row (not yet committed);
|
|
7075
|
+
// `value` is the live in-progress text.
|
|
7076
|
+
onEditInput(e) {
|
|
7077
|
+
if (!this.editingCell) return
|
|
7078
|
+
const { row, col } = this.editingCell;
|
|
7079
|
+
this._scheduleOnInput(row, col, e.target.value);
|
|
7080
|
+
},
|
|
7081
|
+
|
|
7082
|
+
_scheduleOnInput(ri, ci, value) {
|
|
7083
|
+
const col = this.leafCols[ci];
|
|
7084
|
+
if (!col || typeof col.onInput !== 'function') return
|
|
7085
|
+
clearTimeout(this._onInputTimer);
|
|
7086
|
+
this._onInputPending = true;
|
|
7087
|
+
const delay = typeof col.debounceMs === 'number' ? col.debounceMs : 400;
|
|
7088
|
+
this._onInputTimer = setTimeout(() => {
|
|
7089
|
+
this._onInputPending = false;
|
|
7090
|
+
const row = this._dataRaw[ri];
|
|
7091
|
+
if (row) {
|
|
7092
|
+
this._lastFiredOnInput = { ri, ci, value };
|
|
7093
|
+
col.onInput(value, row, ri, col);
|
|
7094
|
+
}
|
|
7095
|
+
}, delay);
|
|
7096
|
+
},
|
|
7097
|
+
|
|
7098
|
+
// Cancels any pending debounced call and fires col.onInput immediately —
|
|
7099
|
+
// used at commit points (Enter/blur, date pick, checkbox toggle) so a
|
|
7100
|
+
// still-in-flight debounce timer never drops the final value. If nothing
|
|
7101
|
+
// is pending (the debounce already fired naturally with this exact
|
|
7102
|
+
// value), skip re-firing — commitEdit() calls this unconditionally, so
|
|
7103
|
+
// without this guard a slow typist would get onInput called twice for
|
|
7104
|
+
// the same settled value.
|
|
7105
|
+
_flushOnInput(ri, ci, value, row) {
|
|
7106
|
+
const col = this.leafCols[ci];
|
|
7107
|
+
if (!col || typeof col.onInput !== 'function') return
|
|
7108
|
+
const wasPending = this._onInputPending;
|
|
7109
|
+
clearTimeout(this._onInputTimer);
|
|
7110
|
+
this._onInputPending = false;
|
|
7111
|
+
const last = this._lastFiredOnInput;
|
|
7112
|
+
if (!wasPending && last && last.ri === ri && last.ci === ci && last.value === value) return
|
|
7113
|
+
this._lastFiredOnInput = { ri, ci, value };
|
|
7114
|
+
col.onInput(value, row || this._dataRaw[ri], ri, col);
|
|
7115
|
+
},
|
|
7116
|
+
|
|
7046
7117
|
hasCellSlot(col) {
|
|
7047
7118
|
return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])
|
|
7048
7119
|
},
|
|
@@ -7107,6 +7178,7 @@ var script = {
|
|
|
7107
7178
|
const newData = oldData.slice();
|
|
7108
7179
|
newData[ri] = { ...oldData[ri], [col.key]: value };
|
|
7109
7180
|
this._emitDataChange(oldData, newData);
|
|
7181
|
+
this._flushOnInput(ri, ci, value, newData[ri]);
|
|
7110
7182
|
},
|
|
7111
7183
|
|
|
7112
7184
|
toggleCheckbox(ri, ci) {
|
|
@@ -7313,7 +7385,7 @@ var script = {
|
|
|
7313
7385
|
newData[row] = { ...oldData[row], [colDef.key]: value };
|
|
7314
7386
|
this._emitDataChange(oldData, newData);
|
|
7315
7387
|
if (typeof colDef.onSelect === 'function') {
|
|
7316
|
-
// Deferred the same way as
|
|
7388
|
+
// Deferred the same way as _fireCellCallbacksForRange (paste/fill/drag-move) —
|
|
7317
7389
|
// a host watching the emitted data (e.g. mirroring it into its own
|
|
7318
7390
|
// store) needs that watcher to run first, or onSelect reads stale state.
|
|
7319
7391
|
this.$nextTick(() => colDef.onSelect({ value, label }, newData[row], row, colDef));
|
|
@@ -7503,13 +7575,60 @@ var script = {
|
|
|
7503
7575
|
return this.internalSort.direction === 'asc' ? 'eg-icon-sort-asc' : 'eg-icon-sort-desc'
|
|
7504
7576
|
},
|
|
7505
7577
|
|
|
7506
|
-
sortIconTitle(
|
|
7507
|
-
|
|
7508
|
-
|
|
7578
|
+
sortIconTitle() {
|
|
7579
|
+
return this.t.sortMenuTooltip
|
|
7580
|
+
},
|
|
7581
|
+
|
|
7582
|
+
// Opens the sort popup menu (A → Z / Z → A / Cancel) anchored to the
|
|
7583
|
+
// clicked header icon. Replaces the old direct cycle-on-click behavior.
|
|
7584
|
+
openSortMenu(e, col) {
|
|
7585
|
+
if (this.internalLocked) return
|
|
7586
|
+
this.sortMenu = { visible: true, x: e.clientX, y: e.clientY, key: col.key };
|
|
7587
|
+
this.sortMenuPositioned = false;
|
|
7588
|
+
this.$nextTick(this._positionSortMenu);
|
|
7589
|
+
},
|
|
7590
|
+
|
|
7591
|
+
// Same flip-to-fit reasoning as _positionContextMenu().
|
|
7592
|
+
_positionSortMenu() {
|
|
7593
|
+
const el = this.$refs.sortMenuEl;
|
|
7594
|
+
if (!el) return
|
|
7595
|
+
const rect = el.getBoundingClientRect();
|
|
7596
|
+
const margin = 4;
|
|
7597
|
+
let x = this.sortMenu.x;
|
|
7598
|
+
let y = this.sortMenu.y;
|
|
7599
|
+
if (x + rect.width > window.innerWidth - margin) {
|
|
7600
|
+
x = Math.max(margin, window.innerWidth - rect.width - margin);
|
|
7601
|
+
}
|
|
7602
|
+
if (y + rect.height > window.innerHeight - margin) {
|
|
7603
|
+
y = Math.max(margin, window.innerHeight - rect.height - margin);
|
|
7604
|
+
}
|
|
7605
|
+
this.sortMenu.x = x;
|
|
7606
|
+
this.sortMenu.y = y;
|
|
7607
|
+
this.sortMenuPositioned = true;
|
|
7608
|
+
},
|
|
7609
|
+
|
|
7610
|
+
hideSortMenu() {
|
|
7611
|
+
this.sortMenu.visible = false;
|
|
7509
7612
|
},
|
|
7510
7613
|
|
|
7511
|
-
|
|
7512
|
-
|
|
7614
|
+
sortMenuPickAsc() {
|
|
7615
|
+
this.setSort(this.sortMenu.key, 'asc');
|
|
7616
|
+
this.hideSortMenu();
|
|
7617
|
+
},
|
|
7618
|
+
|
|
7619
|
+
sortMenuPickDesc() {
|
|
7620
|
+
this.setSort(this.sortMenu.key, 'desc');
|
|
7621
|
+
this.hideSortMenu();
|
|
7622
|
+
},
|
|
7623
|
+
|
|
7624
|
+
sortMenuPickCancel() {
|
|
7625
|
+
this.clearSort();
|
|
7626
|
+
this.hideSortMenu();
|
|
7627
|
+
},
|
|
7628
|
+
|
|
7629
|
+
// Cycles a column through none → asc → desc → none. Kept as a public
|
|
7630
|
+
// method for programmatic/custom #header-{key} slot use — the built-in
|
|
7631
|
+
// header icon now opens the popup menu (openSortMenu) instead.
|
|
7513
7632
|
toggleSort(key) {
|
|
7514
7633
|
if (this.internalLocked) return
|
|
7515
7634
|
const current = this.internalSort;
|
|
@@ -7620,19 +7739,31 @@ var script = {
|
|
|
7620
7739
|
const newData = oldData.slice();
|
|
7621
7740
|
newData[row] = { ...oldData[row], [colDef.key]: formattedVal };
|
|
7622
7741
|
this._emitDataChange(oldData, newData);
|
|
7742
|
+
this._flushOnInput(row, col, formattedVal, newData[row]);
|
|
7623
7743
|
}
|
|
7624
7744
|
this.closeDatePicker();
|
|
7625
7745
|
},
|
|
7626
7746
|
|
|
7747
|
+
// Raw keystrokes while typing in the date picker's manual text field —
|
|
7748
|
+
// debounced, same as onEditInput(). onDatePick() above flushes the final,
|
|
7749
|
+
// validated value immediately when the date actually commits.
|
|
7750
|
+
onDateTyping(rawVal) {
|
|
7751
|
+
if (!this.datePickerOverlay) return
|
|
7752
|
+
const { row, col } = this.datePickerOverlay;
|
|
7753
|
+
this._scheduleOnInput(row, col, rawVal);
|
|
7754
|
+
},
|
|
7755
|
+
|
|
7627
7756
|
// See closeSelectDropdown() — same reasoning: every close path must return
|
|
7628
7757
|
// focus to the grid wrapper or keyboard navigation goes dead afterward.
|
|
7629
7758
|
closeDatePicker() {
|
|
7759
|
+
clearTimeout(this._onInputTimer);
|
|
7630
7760
|
this.datePickerOverlay = null;
|
|
7631
7761
|
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
7632
7762
|
},
|
|
7633
7763
|
|
|
7634
7764
|
_onWindowScrollForDropdown(e) {
|
|
7635
7765
|
if (this.contextMenu.visible) this.hideContextMenu();
|
|
7766
|
+
if (this.sortMenu.visible) this.hideSortMenu();
|
|
7636
7767
|
if (this.selectDropdown) {
|
|
7637
7768
|
const comp = this.$refs.selectDropdownComp;
|
|
7638
7769
|
const dd = comp && comp.$el ? comp.$el : null;
|
|
@@ -7884,9 +8015,9 @@ var script = {
|
|
|
7884
8015
|
this.fillDrag.sourceRange = null;
|
|
7885
8016
|
|
|
7886
8017
|
if (fireOnSelect) {
|
|
7887
|
-
// See pasteAt's
|
|
8018
|
+
// See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.
|
|
7888
8019
|
await this.$nextTick();
|
|
7889
|
-
this.
|
|
8020
|
+
this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol);
|
|
7890
8021
|
}
|
|
7891
8022
|
},
|
|
7892
8023
|
|
|
@@ -7960,21 +8091,31 @@ var script = {
|
|
|
7960
8091
|
},
|
|
7961
8092
|
|
|
7962
8093
|
// Bulk cell writes (paste, fill handle, drag-move) set row values directly
|
|
7963
|
-
// and skip the single-cell
|
|
7964
|
-
//
|
|
7965
|
-
//
|
|
7966
|
-
|
|
8094
|
+
// and skip the single-cell paths (onComponentSelect / commitEdit /
|
|
8095
|
+
// setCheckboxValue / onDatePick) — so col.onSelect / col.onInput never
|
|
8096
|
+
// fire for them unless we replay it here for every cell whose value
|
|
8097
|
+
// actually changed in the range. select/treeSelect columns get onSelect
|
|
8098
|
+
// (matching the single-pick shape); everything else gets onInput —
|
|
8099
|
+
// fired immediately, no debounce (this is already a single bulk commit,
|
|
8100
|
+
// not live keystrokes).
|
|
8101
|
+
_fireCellCallbacksForRange(oldData, newData, startRow, endRow, startCol, endCol) {
|
|
7967
8102
|
for (let ri = startRow; ri <= endRow; ri++) {
|
|
7968
8103
|
const oldRow = oldData[ri];
|
|
7969
8104
|
const newRow = newData[ri];
|
|
7970
8105
|
if (!newRow || newRow === oldRow) continue
|
|
7971
8106
|
for (let ci = startCol; ci <= endCol; ci++) {
|
|
7972
8107
|
const col = this.leafCols[ci];
|
|
7973
|
-
if (!col
|
|
8108
|
+
if (!col) continue
|
|
7974
8109
|
const oldVal = oldRow ? oldRow[col.key] : undefined;
|
|
7975
8110
|
const newVal = newRow[col.key];
|
|
7976
8111
|
if (newVal === oldVal) continue
|
|
7977
|
-
|
|
8112
|
+
if (this.isSelectLikeCol(col)) {
|
|
8113
|
+
if (typeof col.onSelect === 'function') {
|
|
8114
|
+
col.onSelect({ value: newVal, label: this.getSelectLabel(ci, newVal, ri) }, newRow, ri, col);
|
|
8115
|
+
}
|
|
8116
|
+
} else if (typeof col.onInput === 'function') {
|
|
8117
|
+
col.onInput(newVal, newRow, ri, col);
|
|
8118
|
+
}
|
|
7978
8119
|
}
|
|
7979
8120
|
}
|
|
7980
8121
|
},
|
|
@@ -8064,7 +8205,7 @@ var script = {
|
|
|
8064
8205
|
// mirror the row into an external store); firing onSelect synchronously
|
|
8065
8206
|
// would run it against that store's pre-paste, stale state.
|
|
8066
8207
|
await this.$nextTick();
|
|
8067
|
-
this.
|
|
8208
|
+
this._fireCellCallbacksForRange(oldData, newData, affectedRange.startRow, affectedRange.endRow, affectedRange.startCol, affectedRange.endCol);
|
|
8068
8209
|
},
|
|
8069
8210
|
|
|
8070
8211
|
ctxCopy() { this.hideContextMenu(); this.copySelection(); },
|
|
@@ -8637,9 +8778,9 @@ var script = {
|
|
|
8637
8778
|
this.isMouseDown = false;
|
|
8638
8779
|
|
|
8639
8780
|
if (proceed) {
|
|
8640
|
-
// See pasteAt's
|
|
8781
|
+
// See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.
|
|
8641
8782
|
await this.$nextTick();
|
|
8642
|
-
this.
|
|
8783
|
+
this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol);
|
|
8643
8784
|
}
|
|
8644
8785
|
},
|
|
8645
8786
|
|
|
@@ -9580,6 +9721,58 @@ var __vue_render__ = function () {
|
|
|
9580
9721
|
)
|
|
9581
9722
|
: _vm._e(),
|
|
9582
9723
|
_vm._v(" "),
|
|
9724
|
+
_vm.sortMenu.visible
|
|
9725
|
+
? _c(
|
|
9726
|
+
"div",
|
|
9727
|
+
{
|
|
9728
|
+
key: "eg-sort-menu",
|
|
9729
|
+
ref: "sortMenuEl",
|
|
9730
|
+
staticClass: "eg-context-menu eg-sort-menu",
|
|
9731
|
+
style: {
|
|
9732
|
+
top: _vm.sortMenu.y + "px",
|
|
9733
|
+
left:
|
|
9734
|
+
(_vm.sortMenuPositioned ? _vm.sortMenu.x : -9999) + "px",
|
|
9735
|
+
visibility: _vm.sortMenuPositioned ? "visible" : "hidden",
|
|
9736
|
+
},
|
|
9737
|
+
on: {
|
|
9738
|
+
click: function ($event) {
|
|
9739
|
+
$event.stopPropagation();
|
|
9740
|
+
},
|
|
9741
|
+
},
|
|
9742
|
+
},
|
|
9743
|
+
[
|
|
9744
|
+
_c(
|
|
9745
|
+
"div",
|
|
9746
|
+
{
|
|
9747
|
+
staticClass: "eg-context-item",
|
|
9748
|
+
on: { click: _vm.sortMenuPickAsc },
|
|
9749
|
+
},
|
|
9750
|
+
[_vm._v(_vm._s(_vm.t.sortMenuAsc))]
|
|
9751
|
+
),
|
|
9752
|
+
_vm._v(" "),
|
|
9753
|
+
_c(
|
|
9754
|
+
"div",
|
|
9755
|
+
{
|
|
9756
|
+
staticClass: "eg-context-item",
|
|
9757
|
+
on: { click: _vm.sortMenuPickDesc },
|
|
9758
|
+
},
|
|
9759
|
+
[_vm._v(_vm._s(_vm.t.sortMenuDesc))]
|
|
9760
|
+
),
|
|
9761
|
+
_vm._v(" "),
|
|
9762
|
+
_c("div", { staticClass: "eg-context-separator" }),
|
|
9763
|
+
_vm._v(" "),
|
|
9764
|
+
_c(
|
|
9765
|
+
"div",
|
|
9766
|
+
{
|
|
9767
|
+
staticClass: "eg-context-item",
|
|
9768
|
+
on: { click: _vm.sortMenuPickCancel },
|
|
9769
|
+
},
|
|
9770
|
+
[_vm._v(_vm._s(_vm.t.sortMenuCancel))]
|
|
9771
|
+
),
|
|
9772
|
+
]
|
|
9773
|
+
)
|
|
9774
|
+
: _vm._e(),
|
|
9775
|
+
_vm._v(" "),
|
|
9583
9776
|
_vm.infoModal
|
|
9584
9777
|
? _c(
|
|
9585
9778
|
"div",
|
|
@@ -9630,7 +9823,7 @@ var __vue_render__ = function () {
|
|
|
9630
9823
|
{
|
|
9631
9824
|
ref: "container",
|
|
9632
9825
|
staticClass: "eg-container",
|
|
9633
|
-
on: { scroll: _vm.onScroll, click: _vm.
|
|
9826
|
+
on: { scroll: _vm.onScroll, click: _vm.hideFloatingMenus },
|
|
9634
9827
|
},
|
|
9635
9828
|
[
|
|
9636
9829
|
_c(
|
|
@@ -9779,9 +9972,7 @@ var __vue_render__ = function () {
|
|
|
9779
9972
|
),
|
|
9780
9973
|
attrs: {
|
|
9781
9974
|
title:
|
|
9782
|
-
_vm.sortIconTitle(
|
|
9783
|
-
cell.column
|
|
9784
|
-
),
|
|
9975
|
+
_vm.sortIconTitle(),
|
|
9785
9976
|
},
|
|
9786
9977
|
on: {
|
|
9787
9978
|
mousedown: function (
|
|
@@ -9793,8 +9984,9 @@ var __vue_render__ = function () {
|
|
|
9793
9984
|
$event
|
|
9794
9985
|
) {
|
|
9795
9986
|
$event.stopPropagation();
|
|
9796
|
-
return _vm.
|
|
9797
|
-
|
|
9987
|
+
return _vm.openSortMenu(
|
|
9988
|
+
$event,
|
|
9989
|
+
cell.column
|
|
9798
9990
|
)
|
|
9799
9991
|
},
|
|
9800
9992
|
},
|
|
@@ -10194,9 +10386,7 @@ var __vue_render__ = function () {
|
|
|
10194
10386
|
cell.column
|
|
10195
10387
|
),
|
|
10196
10388
|
attrs: {
|
|
10197
|
-
title: _vm.sortIconTitle(
|
|
10198
|
-
cell.column
|
|
10199
|
-
),
|
|
10389
|
+
title: _vm.sortIconTitle(),
|
|
10200
10390
|
},
|
|
10201
10391
|
on: {
|
|
10202
10392
|
mousedown: function (
|
|
@@ -10206,8 +10396,9 @@ var __vue_render__ = function () {
|
|
|
10206
10396
|
},
|
|
10207
10397
|
click: function ($event) {
|
|
10208
10398
|
$event.stopPropagation();
|
|
10209
|
-
return _vm.
|
|
10210
|
-
|
|
10399
|
+
return _vm.openSortMenu(
|
|
10400
|
+
$event,
|
|
10401
|
+
cell.column
|
|
10211
10402
|
)
|
|
10212
10403
|
},
|
|
10213
10404
|
},
|
|
@@ -10594,9 +10785,7 @@ var __vue_render__ = function () {
|
|
|
10594
10785
|
),
|
|
10595
10786
|
attrs: {
|
|
10596
10787
|
title:
|
|
10597
|
-
_vm.sortIconTitle(
|
|
10598
|
-
cell.column
|
|
10599
|
-
),
|
|
10788
|
+
_vm.sortIconTitle(),
|
|
10600
10789
|
},
|
|
10601
10790
|
on: {
|
|
10602
10791
|
mousedown: function (
|
|
@@ -10608,8 +10797,9 @@ var __vue_render__ = function () {
|
|
|
10608
10797
|
$event
|
|
10609
10798
|
) {
|
|
10610
10799
|
$event.stopPropagation();
|
|
10611
|
-
return _vm.
|
|
10612
|
-
|
|
10800
|
+
return _vm.openSortMenu(
|
|
10801
|
+
$event,
|
|
10802
|
+
cell.column
|
|
10613
10803
|
)
|
|
10614
10804
|
},
|
|
10615
10805
|
},
|
|
@@ -11228,6 +11418,23 @@ var __vue_render__ = function () {
|
|
|
11228
11418
|
_vm.editValue,
|
|
11229
11419
|
},
|
|
11230
11420
|
on: {
|
|
11421
|
+
input:
|
|
11422
|
+
[
|
|
11423
|
+
function (
|
|
11424
|
+
$event
|
|
11425
|
+
) {
|
|
11426
|
+
if (
|
|
11427
|
+
$event
|
|
11428
|
+
.target
|
|
11429
|
+
.composing
|
|
11430
|
+
) {
|
|
11431
|
+
return
|
|
11432
|
+
}
|
|
11433
|
+
_vm.editValue =
|
|
11434
|
+
$event.target.value;
|
|
11435
|
+
},
|
|
11436
|
+
_vm.onEditInput,
|
|
11437
|
+
],
|
|
11231
11438
|
blur: _vm.commitEdit,
|
|
11232
11439
|
keydown:
|
|
11233
11440
|
function (
|
|
@@ -11239,20 +11446,6 @@ var __vue_render__ = function () {
|
|
|
11239
11446
|
arguments
|
|
11240
11447
|
)
|
|
11241
11448
|
},
|
|
11242
|
-
input:
|
|
11243
|
-
function (
|
|
11244
|
-
$event
|
|
11245
|
-
) {
|
|
11246
|
-
if (
|
|
11247
|
-
$event
|
|
11248
|
-
.target
|
|
11249
|
-
.composing
|
|
11250
|
-
) {
|
|
11251
|
-
return
|
|
11252
|
-
}
|
|
11253
|
-
_vm.editValue =
|
|
11254
|
-
$event.target.value;
|
|
11255
|
-
},
|
|
11256
11449
|
},
|
|
11257
11450
|
}
|
|
11258
11451
|
),
|
|
@@ -11782,6 +11975,23 @@ var __vue_render__ = function () {
|
|
|
11782
11975
|
_vm.editValue,
|
|
11783
11976
|
},
|
|
11784
11977
|
on: {
|
|
11978
|
+
input:
|
|
11979
|
+
[
|
|
11980
|
+
function (
|
|
11981
|
+
$event
|
|
11982
|
+
) {
|
|
11983
|
+
if (
|
|
11984
|
+
$event
|
|
11985
|
+
.target
|
|
11986
|
+
.composing
|
|
11987
|
+
) {
|
|
11988
|
+
return
|
|
11989
|
+
}
|
|
11990
|
+
_vm.editValue =
|
|
11991
|
+
$event.target.value;
|
|
11992
|
+
},
|
|
11993
|
+
_vm.onEditInput,
|
|
11994
|
+
],
|
|
11785
11995
|
blur: _vm.commitEdit,
|
|
11786
11996
|
keydown:
|
|
11787
11997
|
function (
|
|
@@ -11793,20 +12003,6 @@ var __vue_render__ = function () {
|
|
|
11793
12003
|
arguments
|
|
11794
12004
|
)
|
|
11795
12005
|
},
|
|
11796
|
-
input:
|
|
11797
|
-
function (
|
|
11798
|
-
$event
|
|
11799
|
-
) {
|
|
11800
|
-
if (
|
|
11801
|
-
$event
|
|
11802
|
-
.target
|
|
11803
|
-
.composing
|
|
11804
|
-
) {
|
|
11805
|
-
return
|
|
11806
|
-
}
|
|
11807
|
-
_vm.editValue =
|
|
11808
|
-
$event.target.value;
|
|
11809
|
-
},
|
|
11810
12006
|
},
|
|
11811
12007
|
}
|
|
11812
12008
|
),
|
|
@@ -12997,7 +13193,11 @@ var __vue_render__ = function () {
|
|
|
12997
13193
|
locale: _vm.locale,
|
|
12998
13194
|
"trigger-rect": _vm.datePickerOverlay.rect,
|
|
12999
13195
|
},
|
|
13000
|
-
on: {
|
|
13196
|
+
on: {
|
|
13197
|
+
pick: _vm.onDatePick,
|
|
13198
|
+
typing: _vm.onDateTyping,
|
|
13199
|
+
close: _vm.closeDatePicker,
|
|
13200
|
+
},
|
|
13001
13201
|
})
|
|
13002
13202
|
: _vm._e(),
|
|
13003
13203
|
_vm._v(" "),
|
|
@@ -13051,12 +13251,12 @@ __vue_render__._withStripped = true;
|
|
|
13051
13251
|
/* style */
|
|
13052
13252
|
const __vue_inject_styles__ = function (inject) {
|
|
13053
13253
|
if (!inject) return
|
|
13054
|
-
inject("data-v-64c6d39f_0", { source: "\n.eg-shell[data-v-64c6d39f] {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n.eg-shell-fullscreen[data-v-64c6d39f] {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material[data-v-64c6d39f] {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd[data-v-64c6d39f] {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent[data-v-64c6d39f] {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon[data-v-64c6d39f] {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n.eg-wrapper[data-v-64c6d39f] {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n.eg-toolbar[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n.eg-toolbar-group[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n.eg-toolbar-group + .eg-toolbar-group[data-v-64c6d39f] {\n border-left: 1px solid var(--eg-color-border);\n}\n.eg-toolbar-spacer[data-v-64c6d39f] {\n flex: 1 1 auto;\n}\n.eg-toolbar-btn[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n.eg-toolbar-btn[data-v-64c6d39f]:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n.eg-toolbar-btn[data-v-64c6d39f]:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.eg-toolbar-btn-active[data-v-64c6d39f] {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n.eg-toolbar-select[data-v-64c6d39f] {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n.eg-toolbar-select[data-v-64c6d39f]:hover {\n border-color: var(--eg-color-accent);\n}\n.eg-toolbar-select[data-v-64c6d39f]:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-toolbar-segmented[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n.eg-toolbar-segment[data-v-64c6d39f] {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon[data-v-64c6d39f] { width: 12px; height: 12px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon[data-v-64c6d39f] { width: 15px; height: 15px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon[data-v-64c6d39f] { width: 18px; height: 18px;\n}\n.eg-toolbar-icon[data-v-64c6d39f] {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n.eg-icon-undo[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\");\n}\n.eg-icon-redo[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\");\n}\n.eg-icon-export[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\");\n}\n.eg-icon-import[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\");\n}\n.eg-icon-lock[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\");\n}\n.eg-icon-unlock[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\");\n}\n.eg-icon-expand[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\");\n}\n.eg-icon-compress[data-v-64c6d39f]{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\");\n}\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\");\n}\n.eg-icon-sort-asc[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\");\n}\n.eg-icon-sort-desc[data-v-64c6d39f]{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\");\n}\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\");\n}\n.eg-icon-size-middle[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\");\n}\n.eg-icon-size-large[data-v-64c6d39f] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\");\n}\n.eg-container[data-v-64c6d39f] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-64c6d39f] {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n.eg-header-cell[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n.eg-header-cell[data-v-64c6d39f]:hover { background: var(--eg-color-border-cell);\n}\n.eg-header-group[data-v-64c6d39f] { background: var(--eg-color-header-group-bg);\n}\n.eg-sort-icon[data-v-64c6d39f] {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon[data-v-64c6d39f] { opacity: 0.65;\n}\n.eg-sort-icon[data-v-64c6d39f]:hover { opacity: 1;\n}\n.eg-sort-icon.eg-icon-sort-asc[data-v-64c6d39f],\n.eg-sort-icon.eg-icon-sort-desc[data-v-64c6d39f] {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell[data-v-64c6d39f] {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty[data-v-64c6d39f] { background: var(--eg-color-header-bg);\n}\n.eg-filter-input[data-v-64c6d39f],\n.eg-filter-select[data-v-64c6d39f] {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input[data-v-64c6d39f]:focus,\n.eg-filter-select[data-v-64c6d39f]:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select[data-v-64c6d39f] {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n.eg-col-resize-handle[data-v-64c6d39f] {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n.eg-body-area[data-v-64c6d39f] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-64c6d39f] {\n border-top: 1px solid var(--eg-color-border);\n}\n.eg-frozen-col-footer[data-v-64c6d39f],\n.eg-scroll-col-footer[data-v-64c6d39f],\n.eg-pinned-right-footer[data-v-64c6d39f] {\n position: relative;\n}\n.eg-footer-cell[data-v-64c6d39f] {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n.eg-row[data-v-64c6d39f] {\n position: relative;\n}\n.eg-cell[data-v-64c6d39f] {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n.eg-locked .eg-cell[data-v-64c6d39f] { cursor: not-allowed;\n}\n.eg-cell-value[data-v-64c6d39f] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right[data-v-64c6d39f] { justify-content: flex-end;\n}\n.eg-cell-align-center[data-v-64c6d39f] { justify-content: center;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-64c6d39f]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-64c6d39f] {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline[data-v-64c6d39f] {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap[data-v-64c6d39f] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-64c6d39f] {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-64c6d39f] {\n background: var(--eg-color-cell-selected) !important;\n}\n.eg-cell-fill-preview[data-v-64c6d39f] {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n.eg-cell-copy[data-v-64c6d39f] {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-64c6d39f] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-64c6d39f] {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-64c6d39f] {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n.eg-row-header-cell[data-v-64c6d39f] {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.eg-row-resize-handle[data-v-64c6d39f] {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n.eg-fill-handle[data-v-64c6d39f] {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n.eg-context-menu[data-v-64c6d39f] {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n.eg-context-item[data-v-64c6d39f] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n.eg-context-item[data-v-64c6d39f]:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n.eg-context-separator[data-v-64c6d39f] {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-64c6d39f]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-64c6d39f] {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n.eg-context-submenu-parent:hover .eg-context-submenu[data-v-64c6d39f] { display: block;\n}\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop[data-v-64c6d39f] {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.eg-info-modal[data-v-64c6d39f] {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n.eg-info-modal-header[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n.eg-info-modal-title[data-v-64c6d39f] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n.eg-info-modal-close[data-v-64c6d39f] {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n.eg-info-modal-close[data-v-64c6d39f]:hover { color: var(--eg-color-text);\n}\n.eg-info-modal-body[data-v-64c6d39f] {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n.eg-group-btn[data-v-64c6d39f] {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n.eg-group-btn.collapsed[data-v-64c6d39f] { background: var(--eg-color-group-bg-collapsed);\n}\n.eg-drag-ghost[data-v-64c6d39f] {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source[data-v-64c6d39f] {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source[data-v-64c6d39f] {\n opacity: 0.4;\n}\n.eg-col-drag-ghost[data-v-64c6d39f] {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n.eg-resize-line[data-v-64c6d39f] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-64c6d39f]::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-body[data-v-64c6d39f] {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n.eg-pinned-right-body[data-v-64c6d39f]::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-header[data-v-64c6d39f] {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n.eg-import-overlay[data-v-64c6d39f] {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n.eg-lock-flash[data-v-64c6d39f] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon[data-v-64c6d39f] {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n.eg-import-dialog[data-v-64c6d39f] {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n.eg-import-title[data-v-64c6d39f] {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n.eg-import-bar-wrap[data-v-64c6d39f] {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n.eg-import-bar[data-v-64c6d39f] {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n.eg-import-msg[data-v-64c6d39f] {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-64c6d39f] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner[data-v-64c6d39f] {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin-data-v-64c6d39f 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-64c6d39f {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-64c6d39f] {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-64c6d39f] {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active[data-v-64c6d39f] {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-64c6d39f] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-64c6d39f]::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox[data-v-64c6d39f] {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap[data-v-64c6d39f] {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap[data-v-64c6d39f] { justify-content: flex-end;\n}\n.eg-cell-align-center .eg-cell-action-wrap[data-v-64c6d39f] { justify-content: center;\n}\n.eg-validation-tooltip[data-v-64c6d39f] {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell[data-v-64c6d39f]:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell[data-v-64c6d39f]:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell[data-v-64c6d39f] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-64c6d39f] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-64c6d39f] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-64c6d39f] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-64c6d39f] {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu[data-v-64c6d39f],\n.eg-size-small .eg-context-item[data-v-64c6d39f] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-64c6d39f] {\n padding: 4px 14px;\n}\n.eg-size-small .eg-toolbar[data-v-64c6d39f] {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn[data-v-64c6d39f] {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon[data-v-64c6d39f] {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon[data-v-64c6d39f] {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input[data-v-64c6d39f],\n.eg-size-small .eg-filter-select[data-v-64c6d39f] {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon[data-v-64c6d39f] {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-64c6d39f] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-64c6d39f] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-64c6d39f] {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell[data-v-64c6d39f] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-64c6d39f] {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu[data-v-64c6d39f],\n.eg-size-large .eg-context-item[data-v-64c6d39f] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-64c6d39f] {\n padding: 8px 18px;\n}\n.eg-size-large .eg-toolbar[data-v-64c6d39f] {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn[data-v-64c6d39f] {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon[data-v-64c6d39f] {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon[data-v-64c6d39f] {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input[data-v-64c6d39f],\n.eg-size-large .eg-filter-select[data-v-64c6d39f] {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon[data-v-64c6d39f] {\n width: 13px;\n height: 13px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA68JA;EACA;;GAEA;EACA,mEAAA;;EAEA,uBAAA;EACA,sEAAA;EACA,kCAAA;EACA,kCAAA;;EAEA,kBAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,iCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qDAAA;;EAEA,+BAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,mBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,sCAAA;;EAEA,wBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,0CAAA;EACA,qCAAA;;EAEA,iBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oBAAA;EACA,sDAAA;EACA,qDAAA;EACA,oDAAA;EACA,6CAAA;;EAEA,uCAAA;EACA,4DAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qBAAA;EACA,6CAAA;EACA,6CAAA;;EAEA,yCAAA;EACA,kCAAA;EACA,kCAAA;;EAEA,sBAAA;EACA,kCAAA;EACA,wCAAA;EACA,+BAAA;EACA,gBAAA;AACA;AAEA;EACA,eAAA;EACA,QAAA;EACA,aAAA;EACA,mCAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;EACA,gBAAA;AACA;;AAEA,sEAAA;AACA;EACA,wEAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,8DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,+FAAA;EACA,8DAAA;EACA,6DAAA;EACA,mDAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,yBAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,oEAAA;AACA;EACA,wHAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,+DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,8CAAA;EACA,8CAAA;EACA,8CAAA;;EAEA,sHAAA;EACA,yDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,qEAAA;AACA;EACA,oEAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oDAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,8FAAA;EACA,yDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,uBAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,qEAAA;AACA;EACA,kFAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,kCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,yDAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,wDAAA;EACA,wDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;AACA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,gBAAA;EACA,sBAAA;EACA,kCAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,cAAA;EACA,YAAA;EACA,cAAA;EACA,sBAAA;EACA,mCAAA;EACA,+CAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,cAAA;AACA;AAEA;EACA,6CAAA;AACA;AAEA;EACA,cAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,UAAA;EACA,6BAAA;EACA,kBAAA;EACA,uBAAA;EACA,eAAA;EACA,qCAAA;AACA;AAEA;EACA,2CAAA;EACA,oCAAA;AACA;AAEA;EACA,YAAA;EACA,eAAA;AACA;AAEA;EACA,uCAAA;EACA,oCAAA;EACA,kCAAA;AACA;AAEA;EACA,YAAA;EACA,qBAAA;EACA,wCAAA;EACA,kBAAA;EACA,yCAAA;EACA,sLAAA;EACA,4BAAA;EACA,qCAAA;EACA,wBAAA;EACA,qCAAA;EACA,eAAA;EACA,kCAAA;EACA,eAAA;EACA,gBAAA;EACA,wBAAA;EACA,aAAA;AACA;AAEA;EACA,oCAAA;AACA;AAEA;EACA,oCAAA;EACA,iDAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,wCAAA;EACA,kBAAA;EACA,gBAAA;EACA,YAAA;EACA,QAAA;EACA,mCAAA;AACA;AAEA;EACA,kBAAA;AACA;;AAEA;gFACA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AAEA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,8BAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;AACA;AAEA,oCAAA,gRAAA,EAAA,wRAAA;AAAA;AACA,oCAAA,mRAAA,EAAA,2RAAA;AAAA;AACA,oCAAA,2SAAA,EAAA,mTAAA;AAAA;AACA,oCAAA,4SAAA,EAAA,oTAAA;AAAA;AACA,oCAAA,2SAAA,EAAA,mTAAA;AAAA;AACA,oCAAA,0SAAA,EAAA,kTAAA;AAAA;AACA,oCAAA,kTAAA,EAAA,0TAAA;AAAA;AACA,oCAAA,kTAAA,EAAA,0TAAA;AAAA;;AAEA,2HAAA;AACA,qCAAA,+KAAA,EAAA,uLAAA;AAAA;AACA,qCAAA,4IAAA,EAAA,oJAAA;AAAA;AACA,qCAAA,+IAAA,EAAA,uJAAA;AAAA;;AAEA,2GAAA;AACA,wCAAA,8QAAA,EAAA,sRAAA;AAAA;AACA,wCAAA,gRAAA,EAAA,wRAAA;AAAA;AACA,wCAAA,oRAAA,EAAA,4RAAA;AAAA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,OAAA;EACA,cAAA;EACA,kBAAA;AACA;AAEA;EACA,aAAA;EACA,cAAA;EACA,iDAAA;EACA,kBAAA;EACA,WAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,sBAAA;EACA,8CAAA;EACA,+CAAA;EACA,qCAAA;EACA,qCAAA;EACA,gBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;EACA,iBAAA;EACA,eAAA;AACA;AAEA,yCAAA,uCAAA;AAAA;AACA,oCAAA,2CAAA;AAAA;AAEA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;EACA,sBAAA;EACA,cAAA;EACA,8BAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;EACA,eAAA;EACA,YAAA;AACA;AACA,uDAAA,aAAA;AAAA;AACA,uCAAA,UAAA;AAAA;AACA;;EAEA,UAAA;EACA,6BAAA;AACA;;AAEA,2BAAA;AACA;EACA,sBAAA;EACA,gBAAA;EACA,mDAAA;EACA,+CAAA;EACA,qCAAA;EACA,kBAAA;EACA,aAAA;EACA,mBAAA;AACA;AACA,yCAAA,qCAAA;AAAA;AAEA;;EAEA,WAAA;EACA,YAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,mCAAA;EACA,2BAAA;EACA,qCAAA;EACA,kCAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;AACA;AACA;;EAEA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,mBAAA;EACA,sLAAA;EACA,4BAAA;EACA,qCAAA;EACA,wBAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA;EACA,kBAAA;EACA,UAAA;AACA;AAEA;EACA,aAAA;EACA,OAAA;EACA,gBAAA;AACA;;AAEA,4CAAA;AACA;EACA,4CAAA;AACA;AAEA;;;EAGA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,mDAAA;EACA,gBAAA;EACA,mBAAA;EACA,8BAAA;EACA,gBAAA;EACA,mCAAA;EACA,kBAAA;EACA,MAAA;EACA,qCAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;AACA;AAEA;EACA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,mDAAA;EACA,oDAAA;EACA,gBAAA;EACA,mBAAA;EACA,8BAAA;EACA,mCAAA;EACA,kBAAA;EACA,MAAA;EACA,mCAAA;EACA,eAAA;EACA,aAAA;EACA,mBAAA;AACA;AAEA,uCAAA,mBAAA;AAAA;AAEA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,YAAA;AACA;AACA,yCAAA,yBAAA;AAAA;AACA,yCAAA,uBAAA;AAAA;AACA,8DAAA,aAAA;AAAA;AACA;EACA,OAAA;EACA,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,YAAA;AACA;AACA;EACA,cAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;AACA;AACA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;AACA;AACA;EACA,oDAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,oDAAA;AACA;AAEA;EACA,wDAAA;EACA,+CAAA;EACA,oBAAA;AACA;AAEA;EACA,0CAAA;EACA,oBAAA;AACA;AAEA;EACA,wBAAA;AACA;AAEA;EACA,mDAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,WAAA;EACA,YAAA;EACA,YAAA;EACA,aAAA;EACA,mCAAA;EACA,8BAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,sBAAA;EACA,qCAAA;EACA,oDAAA;EACA,qDAAA;EACA,kBAAA;EACA,eAAA;EACA,qCAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;AACA;AAEA;EACA,kBAAA;EACA,SAAA;EACA,OAAA;EACA,QAAA;EACA,WAAA;EACA,kBAAA;EACA,UAAA;AACA;AAEA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,kCAAA;EACA,uBAAA;EACA,iBAAA;EACA,WAAA;AACA;AAEA;EACA,eAAA;EACA,4DAAA;EACA,wCAAA;EACA,4CAAA;EACA,+BAAA;EACA,qCAAA;EACA,aAAA;EACA,gBAAA;EACA,8BAAA;EACA,cAAA;EACA,2BAAA;AACA;AAEA;EACA,iBAAA;EACA,eAAA;EACA,kBAAA;EACA,mBAAA;AACA;AAEA;EACA,2CAAA;EACA,2BAAA;AACA;AAEA;EACA,WAAA;EACA,uCAAA;EACA,aAAA;AACA;AAEA,qDAAA,aAAA,EAAA,YAAA;AAAA;AAEA;EACA,aAAA;EACA,kBAAA;EACA,UAAA;EACA,SAAA;EACA,4DAAA;EACA,wCAAA;EACA,4CAAA;EACA,+BAAA;EACA,qCAAA;EACA,gBAAA;EACA,cAAA;EACA,cAAA;EACA,2BAAA;AACA;AAEA,wEAAA,cAAA;AAAA;;AAEA,oCAAA;AACA;EACA,eAAA;EACA,QAAA;EACA,cAAA;EACA,+BAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;AACA;AAEA;EACA,mCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,UAAA;EACA,gBAAA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,kCAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,SAAA;EACA,kBAAA;EACA,+CAAA;EACA,cAAA;AACA;AAEA;EACA,8BAAA;EACA,gBAAA;EACA,2BAAA;AACA;AAEA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,qCAAA;EACA,gBAAA;AACA;AAEA,8CAAA,2BAAA;AAAA;AAEA;EACA,SAAA;EACA,aAAA;EACA,cAAA;EACA,OAAA;EACA,4EAAA;EACA,eAAA;EACA,2BAAA;EACA,qBAAA;EACA,sBAAA;AACA;AAEA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,2CAAA;EACA,8CAAA;EACA,kBAAA;EACA,UAAA;EACA,iBAAA;AACA;AAEA,2CAAA,8CAAA;AAAA;AAEA;EACA,kBAAA;EACA,yCAAA;EACA,oBAAA;EACA,WAAA;AACA;;AAEA,yBAAA;AACA;EACA,YAAA;EACA,yCAAA;EACA,oBAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,eAAA;EACA,iBAAA;EACA,qCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,qCAAA;EACA,gBAAA;EACA,2BAAA;EACA,mBAAA;EACA,oBAAA;EACA,aAAA;EACA,aAAA;EACA,2BAAA;AACA;AAEA;EACA,oBAAA;EACA,aAAA;AACA;;AAEA,8BAAA;AACA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,iFAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,QAAA;EACA,WAAA;EACA,mCAAA;EACA,cAAA;AACA;AAEA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,UAAA;EACA,SAAA;EACA,UAAA;EACA,gFAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,cAAA;EACA,oDAAA;EACA,qCAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,sCAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,YAAA;EACA,+BAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,oBAAA;EACA,WAAA;AACA;AACA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,sCAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;EACA,mDAAA;AACA;AAEA;EACA,mCAAA;EACA,6CAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,mCAAA;EACA,kBAAA;AACA;AAEA;EACA,eAAA;EACA,gBAAA;EACA,2BAAA;EACA,mBAAA;AACA;AAEA;EACA,WAAA;EACA,uCAAA;EACA,+BAAA;EACA,gBAAA;EACA,mBAAA;AACA;AAEA;EACA,YAAA;EACA,sEAAA;EACA,+BAAA;EACA,4BAAA;AACA;AAEA;EACA,eAAA;EACA,qCAAA;EACA,kBAAA;AACA;AAEA;EACA,8BAAA;EACA,gBAAA;EACA,6BAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;EACA,mBAAA;EACA,6CAAA;EACA,wCAAA;EACA,kBAAA;EACA,uDAAA;AACA;AACA;AACA,KAAA,yBAAA;AAAA;AACA;;AAEA,8BAAA;AACA;EACA,yCAAA;EACA,eAAA;AACA;AACA;EACA,kDAAA;AACA;AACA;EACA,yCAAA;AACA;;AAEA,gCAAA;AACA;EACA,eAAA;AACA;AACA;EACA,YAAA;EACA,kBAAA;EACA,UAAA;EACA,QAAA;EACA,2BAAA;EACA,iCAAA;EACA,eAAA;EACA,oBAAA;EACA,cAAA;AACA;;AAEA,8BAAA;AACA;EACA,uBAAA;EACA,cAAA;AACA;;AAEA,4BAAA;AACA;EACA,aAAA;EACA,mBAAA;EACA,YAAA;EACA,WAAA;EACA,eAAA;AACA;AACA;;6EAEA;AACA,6DAAA,yBAAA;AAAA;AACA,8DAAA,uBAAA;AAAA;AACA;EACA,sCAAA;EACA,uCAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;EACA,+BAAA;EACA,oCAAA;EACA,mBAAA;EACA,oBAAA;AACA;;AAEA,mCAAA;AACA;EACA,sCAAA;AACA;AACA;EACA,qCAAA;AACA;;AAEA,8BAAA;;AAEA,UAAA;AACA;EACA,eAAA;EACA,cAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,iBAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,UAAA;EACA,WAAA;AACA;;AAEA,UAAA;AACA;EACA,eAAA;EACA,cAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,8BAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,iBAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,YAAA;EACA,aAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + internalTheme, { 'eg-shell-fullscreen': isFullscreen, 'eg-locked': internalLocked }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"_toolbarVisible\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canUndo\"\n :title=\"t.undo\"\n @click=\"undo\"\n ><span class=\"eg-toolbar-icon eg-icon-undo\"></span></button>\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canRedo\"\n :title=\"t.redo\"\n @click=\"redo\"\n ><span class=\"eg-toolbar-icon eg-icon-redo\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('export')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"t.export\"\n @click=\"toolbarExportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-export\"></span></button>\n <button\n v-if=\"toolbarHas('import')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked\"\n :title=\"t.import\"\n @click=\"toolbarImportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-import\"></span></button>\n <input\n v-if=\"toolbarHas('import')\"\n ref=\"toolbarImportInput\"\n type=\"file\"\n accept=\".xlsx,.xls,.csv\"\n style=\"display: none\"\n @change=\"onToolbarImportFile\"\n >\n </div>\n\n <slot name=\"toolbar-extra\" :locked=\"internalLocked\" :fullscreen=\"isFullscreen\"></slot>\n\n <div v-if=\"toolbarButtons.length\" class=\"eg-toolbar-group\">\n <button\n v-for=\"btn in toolbarButtons\"\n :key=\"btn.key\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"btn.title\"\n @click=\"onToolbarButtonClick(btn, $event)\"\n ><span class=\"eg-toolbar-icon\" :style=\"btn.icon ? { maskImage: 'url(' + btn.icon + ')', webkitMaskImage: 'url(' + btn.icon + ')' } : null\">{{ btn.icon ? '' : (btn.label || '') }}</span></button>\n </div>\n\n <div class=\"eg-toolbar-spacer\"></div>\n\n <div v-if=\"toolbarHas('theme')\" class=\"eg-toolbar-group\">\n <select\n class=\"eg-toolbar-select\"\n :value=\"internalTheme\"\n :title=\"t.theme\"\n @change=\"setTheme($event.target.value)\"\n >\n <option value=\"excel\">Excel</option>\n <option value=\"material\">Material</option>\n <option value=\"antd\">Ant Design</option>\n <option value=\"fluent\">Fluent</option>\n <option value=\"carbon\">Carbon</option>\n </select>\n </div>\n\n <div v-if=\"toolbarHas('size')\" class=\"eg-toolbar-segmented\" role=\"radiogroup\" :aria-label=\"t.size\">\n <button\n v-for=\"key in sizeOptions\"\n :key=\"key\"\n type=\"button\"\n role=\"radio\"\n class=\"eg-toolbar-btn eg-toolbar-segment\"\n :class=\"['eg-toolbar-segment-' + key, { 'eg-toolbar-btn-active': internalSize === key }]\"\n :aria-checked=\"internalSize === key\"\n :title=\"sizeTitle(key)\"\n @click=\"setSize(key)\"\n ><span class=\"eg-toolbar-icon\" :class=\"'eg-icon-size-' + key\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('lock')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': internalLocked }\"\n :title=\"internalLocked ? t.unlock : t.lock\"\n @click=\"toggleLock\"\n ><span class=\"eg-toolbar-icon\" :class=\"internalLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"></span></button>\n <button\n v-if=\"toolbarHas('fullscreen')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': isFullscreen }\"\n :title=\"isFullscreen ? t.exitFullscreen : t.fullscreen\"\n @click=\"toggleFullscreen\"\n ><span class=\"eg-toolbar-icon\" :class=\"isFullscreen ? 'eg-icon-compress' : 'eg-icon-expand'\"></span></button>\n </div>\n </div>\n\n <div\n class=\"eg-wrapper\"\n :class=\"'eg-size-' + internalSize\"\n :style=\"wrapperStyle\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n @keypress=\"onKeyPress\"\n ref=\"wrapper\"\n >\n <!-- Context menu -->\n <div\n v-if=\"contextMenu.visible\"\n key=\"eg-context-menu\"\n ref=\"contextMenuEl\"\n class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: (contextMenuPositioned ? contextMenu.x : -9999) + 'px', visibility: contextMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"ctxCopy\">{{ t.copy }}</div>\n <div class=\"eg-context-item\" @click=\"ctxCut\">{{ t.cut }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPaste\">{{ t.paste }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item eg-context-submenu-parent\">\n {{ t.pasteSpecial }}\n <div class=\"eg-context-submenu\">\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all')\">{{ t.pasteAll }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('values')\">{{ t.pasteValues }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('formats')\">{{ t.pasteFormats }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all', true)\">{{ t.transpose }}</div>\n </div>\n </div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxMerge\">{{ t.mergeCells }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnmerge\">{{ t.unmergeCells }}</div>\n <div class=\"eg-context-separator\"></div>\n <div v-if=\"contextMenu.row >= 0\" class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div v-if=\"contextMenu.col >= 0\" class=\"eg-context-item\" @click=\"ctxHideColumn\">{{ t.hideColumns }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideRows\">{{ t.unhideRows }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideColumns\">{{ t.unhideColumns }}</div>\n <template v-if=\"contextMenu.row < 0 && contextMenu.col >= 0\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxPinLeft\">{{ t.pinLeft }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPinRight\">{{ t.pinRight }}</div>\n <div class=\"eg-context-item\" v-if=\"isColPinned(contextMenu.col)\" @click=\"ctxUnpin\">{{ t.unpin }}</div>\n </template>\n <template v-if=\"devMode && (contextMenu.col >= 0 || contextMenu.row >= 0)\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.col >= 0\" @click=\"ctxShowColumnInfo\">{{ t.columnInfo }}</div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.row >= 0\" @click=\"ctxShowRowInfo\">{{ t.rowInfo }}</div>\n </template>\n </div>\n\n <!-- devMode: column/row info modal -->\n <div v-if=\"infoModal\" key=\"eg-info-modal-backdrop\" class=\"eg-info-modal-backdrop\" @click=\"closeInfoModal\">\n <div class=\"eg-info-modal\" @click.stop>\n <div class=\"eg-info-modal-header\">\n <span class=\"eg-info-modal-title\">{{ infoModal.title }}</span>\n <button type=\"button\" class=\"eg-info-modal-close\" @click=\"closeInfoModal\">×</button>\n </div>\n <pre class=\"eg-info-modal-body\">{{ infoModal.json }}</pre>\n </div>\n </div>\n\n <!-- Scroll container -->\n <div class=\"eg-container\" ref=\"container\" @scroll=\"onScroll\" @click=\"hideContextMenu\">\n\n <!-- ===== HEADER AREA ===== -->\n <div class=\"eg-header-area\" :style=\"headerAreaStyle\">\n\n <!-- Corner cell -->\n <div\n class=\"eg-corner\"\n :style=\"cornerStyle\"\n @click=\"selectAll\"\n ></div>\n\n <!-- Row group outline gutter (left) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-gutter\" :style=\"rowOutlineGutterStyle\"></div>\n\n <!-- Frozen columns header -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-header\" :style=\"frozenColHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, true)\">\n <div\n :key=\"'fh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, true)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'ffilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable columns header -->\n <div class=\"eg-scroll-col-header\" ref=\"scrollColHeader\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-header-inner\" :style=\"{ width: scrollContentWidth + 'px', height: (headerRowHeight * headerDepth + _filterRowH) + 'px', position: 'relative' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, false)\">\n <div\n :key=\"'sh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, false)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'sfilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns header -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-header\" :style=\"pinnedRightHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellInZone(cell, rowIdx, 'right')\">\n <div\n :key=\"'prh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyleForZone(cell, rowIdx, 'right')\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prfilter-' + col._flatIndex\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(col._flatIndex)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- ===== BODY AREA ===== -->\n <div class=\"eg-body-area\" :style=\"bodyAreaStyle\">\n\n <!-- Row number header column -->\n <div class=\"eg-row-header-col\" :style=\"rowHeaderColStyle\">\n <!-- Scrollable row headers -->\n <div class=\"eg-scroll-row-headers\" :style=\"scrollRowHeadersStyle\" ref=\"scrollRowHeaders\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'srh-' + ri\"\n class=\"eg-row-header-cell\"\n :style=\"getRowHeaderStyle(ri)\"\n @mousedown=\"onRowHeaderMouseDown($event, ri)\"\n @contextmenu.prevent=\"onRowContextMenu($event, ri)\"\n >\n {{ ri + 1 }}\n <div class=\"eg-row-resize-handle\" @mousedown.stop=\"startRowResize($event, ri)\"></div>\n </div>\n </template>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Row group outline gutter (left side of rows) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-col\" :style=\"rowOutlineColStyle\">\n <template v-for=\"group in rowGroups\">\n <div\n :key=\"'rg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getRowGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n <!-- Main scroll body -->\n <div class=\"eg-scroll-body\" ref=\"scrollBody\" :style=\"scrollBodyStyle\" @scroll=\"onBodyScroll\">\n <div class=\"eg-scroll-body-inner\" :style=\"{ width: scrollContentWidth + 'px', height: totalVirtualHeight + 'px', position: 'relative' }\">\n\n <!-- Pinned left column body -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-body\" :style=\"frozenColBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'fc-cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Main data cells -->\n <div class=\"eg-main-cells\" :style=\"mainCellsStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative', width: scrollableContentWidth + 'px' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\n :class=\"{ 'eg-row--even': ri % 2 === 0 }\"\n :style=\"getRowStyle(ri)\"\n :data-row=\"ri\"\n >\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input\n v-else\n ref=\"editInput\"\n class=\"eg-cell-input\"\n v-model=\"editValue\"\n @blur=\"commitEdit\"\n @keydown.stop=\"onEditKeyDown\"\n />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Pinned Right column body -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-body\" :style=\"pinnedRightBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\n :data-fi=\"col._flatIndex\"\n :class=\"getCellClasses(ri, col._flatIndex)\"\n :style=\"getCellStyle(ri, col._flatIndex)\"\n @mousedown=\"onCellMouseDown($event, ri, col._flatIndex)\"\n @mouseover=\"onCellMouseOver($event, ri, col._flatIndex)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, col._flatIndex)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, col._flatIndex)\"\n >\n <div v-if=\"isMergedAway(ri, col._flatIndex)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, col._flatIndex)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, col._flatIndex)\"\n :disabled=\"!isCellEditable(ri, col._flatIndex)\"\n @change=\"(v) => setCheckboxValue(ri, col._flatIndex, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, col._flatIndex, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span></template><template v-else>{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Fill handle (shown on primary range) -->\n <div\n v-if=\"fillHandleVisible\"\n class=\"eg-fill-handle\"\n :style=\"fillHandleStyle\"\n @mousedown.stop=\"startFillDrag\"\n ></div>\n\n <!-- Drag move ghost border -->\n <div\n v-if=\"dragMove.active\"\n class=\"eg-drag-ghost\"\n :style=\"dragGhostStyle\"\n ></div>\n </div>\n </div>\n\n <!-- Column group outline row (top, above body) -->\n <div v-if=\"colGroups.length\" class=\"eg-col-outline-row\" :style=\"colOutlineRowStyle\">\n <template v-for=\"group in colGroups\">\n <div\n :key=\"'cg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getColGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n </div><!-- end body-area -->\n\n <!-- ===== FOOTER AREA (column totals) ===== -->\n <div v-if=\"hasFooter\" class=\"eg-footer-area\" :style=\"footerAreaStyle\">\n\n <!-- \"Jami\" label cell (mirrors corner cell) -->\n <div class=\"eg-footer-label\" :style=\"footerLabelStyle\">{{ t.total }}</div>\n\n <!-- Frozen columns footer -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-footer\" :style=\"frozenColFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div :key=\"'ff-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable footer (horizontally synced with body via scrollFooter ref) -->\n <div class=\"eg-scroll-col-footer\" ref=\"scrollFooter\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-footer-inner\" :style=\"{ width: scrollContentWidth + 'px', height: footerRowHeight + 'px', position: 'relative' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div :key=\"'sf-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns footer -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-footer\" :style=\"pinnedRightFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prf-' + col._flatIndex\"\n class=\"eg-footer-cell\"\n :class=\"getFooterCellClass(col._flatIndex)\"\n :style=\"getFooterCellStyle(col._flatIndex)\"\n >{{ getColumnTotalDisplay(col._flatIndex) }}</div>\n </div>\n </div>\n </div><!-- end footer-area -->\n\n <!-- Resize indicator line -->\n <div v-if=\"resizing.active\" class=\"eg-resize-line\" :style=\"resizeLineStyle\"></div>\n </div><!-- end container -->\n\n <!-- Import progress overlay -->\n <div v-if=\"importState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl import qilinmoqda</div>\n <div class=\"eg-import-bar-wrap\">\n <div class=\"eg-import-bar\" :style=\"{ width: importState.percent + '%' }\"></div>\n </div>\n <div class=\"eg-import-msg\">{{ importState.message }}</div>\n <div class=\"eg-import-pct\">{{ importState.percent }}%</div>\n </div>\n </div>\n\n <!-- Export progress overlay -->\n <div v-if=\"exportState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl export qilinmoqda...</div>\n <div class=\"eg-export-spinner\"></div>\n </div>\n </div>\n\n <!-- Lock/unlock state feedback — brief centered icon flash -->\n <div v-if=\"lockFlashVisible\" class=\"eg-lock-flash\" aria-hidden=\"true\">\n <span\n ref=\"lockFlashIconEl\"\n class=\"eg-lock-flash-icon\"\n :class=\"lockFlashLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"\n ></span>\n </div>\n\n <!-- Select dropdown overlay (flat select) -->\n <eg-select\n v-if=\"selectDropdown && !selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n <!-- Tree select dropdown overlay -->\n <eg-tree-select\n v-if=\"selectDropdown && selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :value-key=\"selectDropdown.keys.valueKey\"\n :label-key=\"selectDropdown.keys.labelKey\"\n :children-key=\"selectDropdown.keys.childrenKey\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n\n <!-- Date picker overlay -->\n <eg-date-picker\n v-if=\"datePickerOverlay\"\n ref=\"datePickerComp\"\n :value=\"datePickerOverlay.value\"\n :format=\"datePickerOverlay.format\"\n :locale=\"locale\"\n :trigger-rect=\"datePickerOverlay.rect\"\n @pick=\"onDatePick\"\n @close=\"closeDatePicker\"\n />\n\n <!-- Validation error tooltip (cellValidator) -->\n <div\n v-if=\"invalidCell && invalidCell.rect\"\n class=\"eg-validation-tooltip\"\n :style=\"invalidTooltipStyle\"\n >{{ invalidCell.message }}</div>\n\n <!-- Column drag ghost (follows mouse — position updated via direct DOM in handleColDragMove) -->\n <div\n v-if=\"colDrag.active\"\n ref=\"colDragGhost\"\n class=\"eg-col-drag-ghost\"\n :style=\"{ left: (colDrag.startX + 16) + 'px', top: (colDrag.startY - 14) + 'px' }\"\n >{{ colDrag.ghostLabel }}</div>\n\n </div>\n </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth, moveColumnNodeInTree } from '../utils/headerTree.js'\nimport { calcVisibleRows, buildCumulativeHeights, findVisibleRange, getRowTop, getRowHeight } from '../utils/virtualScroll.js'\nimport { generateFillValues } from '../utils/fillHandle.js'\nimport {\n matrixToTSV, tsvToMatrix, extractMatrix, transposeMatrix, applyMatrix, tileMatrix\n} from '../utils/clipboard.js'\nimport {\n mergeHasConflict, findMergeAtCell, isMergeAnchor, addMerge,\n removeMergeAt, resolveMergeAnchor\n} from '../utils/merge.js'\nimport {\n addGroup, removeGroup, toggleGroupCollapse, isGroupCollapsed as _isGroupCollapsed,\n getCollapsedRowIndices, getCollapsedColIndices\n} from '../utils/outline.js'\nimport { exportToExcel as _exportToExcel } from '../utils/excelExport.js'\nimport { importFromFile as _importFromFile } from '../utils/excelImport.js'\nimport { createExcelImportWorker, createExcelExportWorker } from '../utils/workerFactory'\nimport { getMessages } from '../utils/i18n.js'\nimport EgCheckbox from './EgCheckbox.vue'\nimport anime from 'animejs'\nimport EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\nimport EgDatePicker from './EgDatePicker.vue'\nimport { parseDateValue, formatDate } from '../utils/dateUtils.js'\n\nconst SIZE_CONFIG = {\n small: { row: 22, header: 24, footer: 24, filter: 26 },\n middle: { row: 28, header: 28, footer: 28, filter: 32 },\n large: { row: 36, header: 36, footer: 36, filter: 40 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\n// Default toolbar tool list when `toolbar` is `true` or an object without\n// its own `list` — every group except 'theme', which stays opt-in since it\n// needs :theme.sync on the parent to do anything.\nconst DEFAULT_TOOLBAR_LIST = ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen']\nconst THEME_LIST = ['excel', 'material', 'antd', 'fluent', 'carbon']\n\nexport default {\n name: 'ExcelGrid',\n\n components: { EgCheckbox, EgSelect, EgTreeSelect, EgDatePicker },\n\n // v-model → :data + @input\n model: { prop: 'data', event: 'input' },\n\n props: {\n /** Array of row objects keyed by columns[].key */\n data: { type: Array, default: () => [] },\n\n /** Column definitions (may be nested via children[]) */\n columns: { type: Array, default: () => [] },\n\n /** Indices of hidden leaf columns */\n hiddenColumns: { type: Array, default: () => [] },\n\n /** Indices of hidden rows */\n hiddenRows: { type: Array, default: () => [] },\n\n /** Initial merge descriptors [{ startRow, startCol, endRow, endCol }] */\n merges: { type: Array, default: () => [] },\n\n /** Collapsed group descriptors [{ type, startIndex, endIndex }] */\n collapsedGroups: { type: Array, default: () => [] },\n\n /**\n * Height of the grid container in px.\n *\n * Omit it (or pass `null`) for auto-height: the grid sizes itself to fit\n * the header + filter row + all rows + footer for the current `data`\n * length, capped at 70% of the viewport height (`window.innerHeight`) —\n * beyond that it stops growing and scrolls internally, same as a fixed\n * `height` would. Recomputes reactively as `data` grows/shrinks and on\n * window resize. Does not apply in fullscreen mode (already sized to the\n * viewport there) and does not include the toolbar's own height.\n *\n * Pass an explicit `Number` to opt back into the old fixed-height behavior.\n */\n height: { type: Number, default: null },\n\n /** Width of the grid container (px or 'auto') */\n width: { type: [Number, String], default: '100%' },\n\n /**\n * Called before drag-drop: (sourceRange, targetRange) => bool\n * Return false to cancel the drop.\n */\n beforeDrop: { type: Function, default: null },\n\n /**\n * UI locale for the context menu.\n * Supported: 'en' | 'ru' | 'uz' | 'uz-Cyrl'\n */\n locale: { type: String, default: 'en' },\n\n /**\n * Pinned columns map: { [colKey]: 'left' | 'right' }\n * Pinned-left columns render as a fixed zone on the left.\n * Pinned-right columns render as a fixed zone on the right.\n * Emit 'update:pinnedColumns' to support .sync modifier.\n */\n pinnedColumns: { type: Object, default: () => ({}) },\n\n /**\n * Dynamic cell editability: (row, rowIndex, col, colIndex) => boolean\n * Overrides column-level editable when provided.\n * row = plain data object, col = column definition.\n */\n cellEditable: { type: Function, default: null },\n\n /**\n * Cell value validation: (value, row, rowIndex, col, colIndex) => boolean\n * Return false to reject the write — the cell keeps its old value and\n * an error tooltip is shown anchored to that cell.\n */\n cellValidator: { type: Function, default: null },\n\n /**\n * Error tooltip text for a rejected value.\n * Function: (value, row, rowIndex, col, colIndex) => string\n * String: same message for every rejected cell.\n * Default: locale's \"invalidValue\" message.\n */\n cellValidationMessage: { type: [Function, String], default: null },\n\n /**\n * Bulk-write strictness (paste / fill handle / Delete).\n * false (default): invalid cells in the affected range are skipped,\n * the rest of the operation still applies.\n * true: if ANY cell in the affected range fails validation, the whole\n * bulk operation is rejected — nothing is written.\n */\n strictBulkValidation: { type: Boolean, default: false },\n\n /** Visual size of the grid. Affects row/header height and font sizes. */\n size: { type: String, default: 'middle' },\n\n /** Auto-fit all column widths to content (same as double-clicking each resize handle). Re-applies on data/columns change. */\n autoWidth: { type: Boolean, default: false },\n\n /**\n * Allow columns to be reordered by dragging their headers.\n * On drop, emits 'update:columns' (compatible with .sync) and 'column-reorder'.\n */\n columnDraggable: { type: Boolean, default: true },\n\n /**\n * Controls the built-in toolbar above the grid — visibility as a whole,\n * and (optionally) which individual tool groups appear. Controlled\n * entirely by the parent — no in-component toggle for the bar itself.\n *\n * @type {Boolean|{visible?: Boolean, list?: Array<('undoRedo'|'export'|'import'|'size'|'lock'|'fullscreen'|'theme')>}}\n * @default false\n *\n * Two forms:\n * - `Boolean` — `false` (default) hides the toolbar entirely; `true`\n * shows it with the default tool list (every group except `'theme'`,\n * since that one needs `:theme.sync` on the parent to be useful).\n * - `Object { visible, list }` — fine-grained control:\n * - `visible` {Boolean} — defaults to `true` when an object is given\n * (passing an object is itself an opt-in to show the toolbar);\n * set explicitly to `false` to hide it while still describing a\n * `list` you intend to reuse later.\n * - `list` {Array<String>} — which tool groups render, using the\n * identifiers above. Unknown/misspelled strings are silently\n * ignored. Omitting `list` falls back to the same default set as\n * the `true` shorthand. **Order in `list` does not change the\n * toolbar's layout** — groups always render in a fixed position\n * (undo/redo → export/import → #toolbar-extra slot → toolbarButtons\n * → spacer → theme → size → lock/fullscreen); `list` is purely an\n * inclusion filter.\n *\n * `toolbarButtons` and the `#toolbar-extra` slot are independent of\n * this prop — they render whenever content is provided, regardless of\n * `list`.\n *\n * @example\n * // Whole default set\n * <excel-grid toolbar />\n * @example\n * // Only undo/redo and fullscreen\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'fullscreen'] }\" />\n * @example\n * // Toggle visibility from a data property while keeping a fixed list\n * <excel-grid :toolbar=\"{ visible: showToolbar, list: ['export', 'import'] }\" />\n * @example\n * // Opt into the theme picker (needs :theme.sync)\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen', 'theme'] }\" :theme.sync=\"myTheme\" />\n */\n toolbar: { type: [Boolean, Object], default: false },\n\n /** Extra custom toolbar buttons: [{ key, icon, title, onClick(event, gridApi) }] */\n toolbarButtons: { type: Array, default: () => [] },\n\n /**\n * Locks the entire grid: blocks all edits (typing, paste, fill, delete,\n * checkbox, select/treeSelect) and structural changes (resize, column\n * reorder, merge/unmerge, undo/redo). Independent from cellEditable/\n * col.editable, which remain per-cell rules. Emit 'update:locked' to\n * support the .sync modifier.\n */\n locked: { type: Boolean, default: false },\n\n /** Show \"Column info\" / \"Row info\" items in the context menu (dev/debug aid). */\n devMode: { type: Boolean, default: false },\n\n /**\n * Visual theme of the grid.\n * Built-in: 'excel' | 'material' | 'antd' | 'fluent' | 'carbon'\n * Default is the classic Excel 365 look. Use 'material' for Material Design 3.\n * Mirrors the `size` prop's convention: an internal `internalTheme` state\n * mirrors this prop and is what the toolbar's theme picker actually reads\n * from/writes to (via `setTheme()`), so the picker works standalone even\n * without `:theme.sync` — `.sync` is only needed to reflect the current\n * theme back into the host's own state.\n */\n theme: { type: String, default: 'excel' },\n\n /**\n * Shows a filter row directly beneath the column headers (inside the\n * sticky header area). Each leaf column gets a filter control matched to\n * its `type`:\n * - default (no type / 'date' / 'progress'): text search input,\n * matched case-insensitively against the column's *displayed* value\n * (so 'date' matches the formatted string, not the raw stored value).\n * - 'select' / 'treeSelect' *with a static `col.options` array*: a\n * native <select> populated from those options (tree options are\n * flattened, indented by depth), matched by exact raw value.\n * - 'select' / 'treeSelect' with function-based `col.options` (options\n * computed per-row): falls back to the text control, since a single\n * fixed option list can't be resolved outside a specific row.\n * - 'checkbox': a tri-state <select> — All / Checked / Unchecked.\n * - 'action': no filter control (not a data-bound column).\n *\n * Rows that don't match every active filter are hidden using the same\n * mechanism as `hiddenRows` — virtual scroll, keyboard navigation, and\n * `exportToExcel()` all skip them automatically, with no separate\n * filtering code path to keep in sync.\n *\n * Set `col.filterable: false` to omit the filter control for one column\n * (its filter cell renders empty). Set\n * `col.filterFn(cellValue, filterValue, row, rowIndex, column)` to\n * override the default match logic for that column entirely.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row />\n */\n filterRow: { type: Boolean, default: false },\n\n /**\n * Current filter values, keyed by column `key`. Optional — the filter\n * row works standalone via internal state (`internalFilters`) even\n * without `.sync`, mirroring the `size`/`theme` prop convention. Bind\n * `:filters.sync` only if the host needs to read or restore the current\n * filter state.\n * @type {Object}\n * @default {}\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row :filters.sync=\"myFilters\" />\n */\n filters: { type: Object, default: () => ({}) },\n\n /**\n * Shows a sort icon in every leaf column header. Clicking it cycles\n * none → ascending → descending → none for that column (clicking a\n * different column's icon replaces the active sort — single-column\n * sort only).\n *\n * Sorting **physically reorders** the `data` array (like a real\n * spreadsheet sort) and emits it through the normal v-model/`input`\n * event — it is recorded in the undo/redo history exactly like any\n * other data edit, so `undo()` restores the pre-sort row order.\n * Clicking a third time (back to \"none\") only clears the sort\n * indicator — it does **not** restore the previous order by itself;\n * use `undo()` for that.\n *\n * Comparison is matched to the column's `type`: plain/`'progress'`\n * columns compare the raw value (numeric-aware); `'date'` compares the\n * parsed date chronologically, not the formatted string; `'select'` /\n * `'treeSelect'` compare the displayed option label; `'checkbox'`\n * compares the boolean state. Empty/`null` values always sort last,\n * regardless of direction. `'action'` columns are never sortable.\n *\n * Set `col.sortable: false` to omit the icon for one column. Set\n * `col.sortFn(rowA, rowB, column) => number` to fully override the\n * comparison for that column.\n *\n * Blocked (no-op) while the grid is `locked`, or while any merged\n * cells or row outline groups exist — both are anchored to row\n * *position*, which a reorder would silently invalidate.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable />\n */\n sortable: { type: Boolean, default: false },\n\n /**\n * Current sort state — `{ key, direction }` (`direction` is `'asc'` or\n * `'desc'`) or `null` when unsorted. Optional — sorting works standalone\n * via internal state (`internalSort`) even without `.sync`, mirroring\n * the `size`/`theme`/`filters` prop convention. Bind `:sort.sync` only\n * if the host needs to read or restore the current sort state.\n * @type {Object}\n * @default null\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable :sort.sync=\"mySort\" />\n */\n sort: { type: Object, default: null }\n },\n\n data() {\n return {\n // Selection\n activeCell: { row: 0, col: 0 },\n selectionRanges: [], // [{ startRow, startCol, endRow, endCol }]\n isMouseDown: false,\n mouseDownRange: null,\n isCtrlDrag: false,\n\n // Edit\n editingCell: null, // { row, col }\n editValue: '',\n editOriginalValue: '',\n\n // Scroll\n scrollTop: 0,\n scrollLeft: 0,\n headerViewportWidth: null,\n\n // Auto-height (height prop omitted) — 70vh cap, kept in sync on resize\n windowInnerH: typeof window !== 'undefined' ? window.innerHeight : 800,\n\n // Virtual scroll\n virtualStartIndex: 0,\n virtualEndIndex: 0,\n virtualOffsetTop: 0,\n totalVirtualHeight: 0,\n\n // Column widths (flat index → px)\n colWidths: {},\n\n // Row heights (row index → px)\n rowHeights: {},\n\n // Import/export progress\n importState: { active: false, percent: 0, message: '' },\n exportState: { active: false },\n\n resizing: {\n active: false,\n type: null, // 'col' | 'row'\n index: -1,\n startX: 0,\n startY: 0,\n startSize: 0,\n currentPos: 0\n },\n\n // Fill handle\n fillDrag: {\n active: false,\n sourceRange: null,\n currentRow: -1,\n currentCol: -1\n },\n\n // Drag-move\n dragMove: {\n active: false,\n sourceRange: null,\n startX: 0,\n startY: 0,\n targetRow: -1,\n targetCol: -1,\n copyMode: false\n },\n\n // Clipboard (internal)\n clipboardMatrix: null,\n clipboardRange: null,\n clipboardMode: null, // 'copy' | 'cut'\n\n // Context menu\n contextMenu: { visible: false, x: 0, y: 0, row: -1, col: -1 },\n // false while the menu's real size hasn't been measured yet — kept hidden\n // (not painted) so the flip-to-fit repositioning in _positionContextMenu()\n // doesn't cause a visible jump.\n contextMenuPositioned: false,\n\n // devMode — column/row config info modal ({ title, json } | null)\n infoModal: null,\n\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, rect }\n\n // Date picker overlay\n datePickerOverlay: null, // { row, col, format, value, rect }\n\n // Cell value validation (cellValidator)\n invalidCell: null, // { row, col, message, rect }\n\n // Merges (controlled via prop + update:merges)\n internalMerges: [],\n\n // Toolbar (controlled via prop + update:locked / update:size / update:theme)\n internalLocked: false,\n internalSize: 'middle',\n internalTheme: 'excel',\n // Brief centered icon feedback whenever the lock state actually changes\n // (toolbar click, toggleLock(), or a host-driven :locked.sync change).\n lockFlashVisible: false,\n lockFlashLocked: false,\n isFullscreen: false,\n // Bumped whenever _history/_historyIndex change so canUndo/canRedo stay reactive.\n historyVersion: 0,\n\n // Groups\n internalGroups: [], // [{ type, startIndex, endIndex }]\n internalCollapsedGroups: [],\n\n // Header\n headerDepth: 1,\n headerRows: [],\n\n // Leaf columns cache\n leafCols: [],\n\n // Footer column totals (flat col index → number | null)\n columnTotals: {},\n\n // Filter row (controlled via prop + update:filters)\n internalFilters: {},\n // Row indices currently hidden by an active filter — merged into\n // hiddenRowSet, so virtual scroll/export/navigation need no separate path.\n filteredRowIndices: [],\n\n // Sort (controlled via prop + update:sort) — { key, direction } | null\n internalSort: null,\n\n // Column drag-and-drop state\n colDrag: {\n pending: false, // mousedown received, waiting for movement threshold\n active: false, // drag in progress\n sourceFirstLeaf: -1, // first leaf flat-index of the dragged node\n sourceLeafCount: 1, // number of leaves in the dragged node (1=leaf, N=group)\n dropIndex: -1, // insert-before flat index (0..N)\n startX: 0,\n startY: 0,\n ghostLabel: ''\n },\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n pinnedLeftSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'left') s.add(col._flatIndex) })\n return s\n },\n\n pinnedRightSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'right') s.add(col._flatIndex) })\n return s\n },\n\n pinnedLeftCols() { return this.leafCols.filter(col => this.pinnedLeftSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n\n hasPinnedLeft() { return this.pinnedLeftSet.size > 0 },\n hasPinnedRight() { return this.pinnedRightSet.size > 0 },\n\n hasFooter() { return this.leafCols.some(col => this.isColumnTotalVisible(col)) },\n\n pinnedLeftWidth() {\n let w = 0\n for (const col of this.pinnedLeftCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n pinnedRightWidth() {\n let w = 0\n for (const col of this.pinnedRightCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n totalRows() { return this.data.length },\n\n // See the `toolbar` prop's JSDoc for the full Boolean|Object contract.\n _toolbarVisible() {\n if (typeof this.toolbar === 'boolean') return this.toolbar\n return !!this.toolbar && this.toolbar.visible !== false\n },\n _toolbarList() {\n if (this.toolbar && Array.isArray(this.toolbar.list)) return this.toolbar.list\n return DEFAULT_TOOLBAR_LIST\n },\n\n _sizeConf() { return SIZE_CONFIG[this.internalSize] || SIZE_CONFIG.middle },\n\n sizeOptions() { return Object.keys(SIZE_CONFIG) },\n _rowH() { return this._sizeConf.row },\n _headerRowH() { return this._sizeConf.header },\n _footerRowH() { return this._sizeConf.footer },\n _filterRowH() { return this.filterRow ? this._sizeConf.filter : 0 },\n\n headerRowHeight() { return this._headerRowH },\n footerRowHeight() { return this._footerRowH },\n\n // Pre-built cumulative height array — O(n) once, then cached by Vue.\n // findVisibleRange() uses binary search against this cached array → O(log n) per scroll.\n // Invalidated only when row count, custom heights, or hidden rows change.\n cumulativeHeights() {\n return buildCumulativeHeights(this.totalRows, this._rowH, this.rowHeights, this.hiddenRowSet)\n },\n\n // height prop omitted → fit header + filter row + all (visible) rows + footer exactly.\n _autoContentHeight() {\n const headerH = this._headerRowH * this.headerDepth + this._filterRowH\n const bodyH = this.cumulativeHeights[this.totalRows] || 0\n const footerH = this.hasFooter ? this._footerRowH : 0\n return headerH + bodyH + footerH\n },\n\n // Resolved grid-area height: explicit `height` prop wins as-is (old\n // behavior); omitted → auto-fit content, capped at 70% of the viewport.\n effectiveHeight() {\n if (this.height != null) return this.height\n return Math.min(this._autoContentHeight, this.windowInnerH * 0.7)\n },\n\n hiddenColSet() { return new Set(this.hiddenColumns) },\n\n hiddenRowSet() {\n const s = new Set(this.hiddenRows)\n // Also add rows hidden by collapsed row groups\n const collapsed = getCollapsedRowIndices(this.internalGroups, this.internalCollapsedGroups)\n collapsed.forEach(i => s.add(i))\n // Also add rows hidden by the filter row (see filteredRowIndices/_recomputeRowFilters)\n this.filteredRowIndices.forEach(i => s.add(i))\n return s\n },\n\n hiddenColFromGroupSet() {\n return getCollapsedColIndices(this.internalGroups, this.internalCollapsedGroups)\n },\n\n effectiveHiddenColSet() {\n const s = new Set(this.hiddenColSet)\n this.hiddenColFromGroupSet.forEach(i => s.add(i))\n // col.visible === false — a static schema-level hide, independent of the\n // toggleable hiddenColumns list. \"Show All Columns\" never reveals it.\n this.leafCols.forEach(col => { if (col.visible === false) s.add(col._flatIndex) })\n return s\n },\n\n rowGroups() { return this.internalGroups.filter(g => g.type === 'row') },\n colGroups() { return this.internalGroups.filter(g => g.type === 'column') },\n\n visibleRowRange() {\n const rows = []\n for (let i = this.virtualStartIndex; i <= this.virtualEndIndex; i++) rows.push(i)\n return rows\n },\n\n primaryRange() {\n return getPrimaryRange(this.selectionRanges, this.activeCell)\n },\n\n fillHandleVisible() {\n return this.primaryRange && !this.editingCell\n },\n\n fillHandleStyle() {\n if (!this.primaryRange) return {}\n const r = normalizeRange(this.primaryRange)\n // O(1) lookup via cumulativeHeights instead of O(endRow) getRowTop loop\n const bottom = this.cumulativeHeights[r.endRow + 1] || 0\n const right = this.getColLeft(r.endCol + 1)\n return {\n position: 'absolute',\n width: '6px',\n height: '6px',\n background: 'var(--eg-color-accent)',\n border: '1px solid white',\n bottom: (this.totalVirtualHeight - bottom + 1) + 'px',\n right: (this.scrollContentWidth - right + 1) + 'px',\n cursor: 'crosshair',\n zIndex: 10\n }\n },\n\n\n invalidTooltipStyle() {\n if (!this.invalidCell || !this.invalidCell.rect) return {}\n const { rect } = this.invalidCell\n return {\n position: 'fixed',\n top: (rect.bottom + 4) + 'px',\n left: rect.left + 'px',\n zIndex: 9999\n }\n },\n\n dragGhostStyle() {\n if (!this.dragMove.active || this.dragMove.targetRow < 0) return {}\n const r = normalizeRange(this.dragMove.sourceRange)\n const rows = r.endRow - r.startRow\n const cols = r.endCol - r.startCol\n // O(1) lookup via cumulativeHeights instead of O(targetRow) getRowTop loop\n const top = this.cumulativeHeights[this.dragMove.targetRow] || 0\n const left = this.getColLeft(this.dragMove.targetCol)\n const width = this.getRangeWidth(this.dragMove.targetCol, this.dragMove.targetCol + cols)\n const height = this.getRangeHeight(this.dragMove.targetRow, this.dragMove.targetRow + rows)\n return {\n position: 'absolute',\n top: top + 'px',\n left: left + 'px',\n width: width + 'px',\n height: height + 'px',\n border: '2px dashed #1a73e8',\n pointerEvents: 'none',\n zIndex: 20\n }\n },\n\n wrapperStyle() {\n if (this.isFullscreen) {\n return {\n width: '100%',\n height: 'auto',\n flex: '1 1 auto',\n minHeight: 0,\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n }\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n height: this.effectiveHeight + 'px',\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n },\n\n shellStyle() {\n if (this.isFullscreen) return {}\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n display: 'flex',\n flexDirection: 'column'\n }\n },\n\n canUndo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex >= 0\n },\n\n canRedo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex < this._history.length - 1\n },\n\n headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n cornerStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: (this._headerRowH * this.headerDepth + this._filterRowH) + 'px',\n background: 'var(--eg-color-header-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderBottom: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0\n }\n },\n\n rowOutlineGutterStyle() {\n return { width: OUTLINE_GUTTER + 'px', flexShrink: 0 }\n },\n\n frozenColHeaderStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n scrollColHeaderStyle() {\n // Once measured, lock the header to the body's exact clientWidth (flex: 0 0 <px>)\n // instead of growing independently via flex:1 — this guarantees identical\n // scrollWidth-clientWidth (max scrollLeft) on both, so syncing scrollLeft never clamps\n // one before the other. (padding-right can't do this: clientWidth always includes\n // padding, so padding never actually shrinks it.)\n if (this.headerViewportWidth != null) {\n return { flex: '0 0 ' + this.headerViewportWidth + 'px', overflow: 'hidden' }\n }\n return { flex: 1, overflow: 'hidden' }\n },\n\n bodyAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flex: 1,\n position: 'relative'\n }\n },\n\n rowHeaderColStyle() {\n return {\n width: ROW_HEADER_WIDTH + 'px',\n minWidth: ROW_HEADER_WIDTH + 'px',\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n position: 'sticky',\n left: 0,\n zIndex: 20,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n rowOutlineColStyle() {\n return {\n width: OUTLINE_GUTTER + 'px',\n minWidth: OUTLINE_GUTTER + 'px',\n flexShrink: 0,\n position: 'relative',\n background: 'var(--eg-color-cell-bg)'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative',\n overflowAnchor: 'none' /* prevent Chrome scroll anchoring from fighting virtual scroll translateY */\n }\n },\n\n frozenColBodyStyle() {\n const w = this.getFrozenWidth()\n return {\n position: 'sticky',\n left: 0,\n width: w + 'px',\n zIndex: 10,\n background: 'white',\n overflow: 'hidden'\n }\n },\n\n mainCellsStyle() {\n const left = this.getFrozenWidth()\n return {\n position: 'absolute',\n left: left + 'px',\n top: 0,\n bottom: 0\n }\n },\n\n scrollContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && !this.pinnedRightSet.has(i)) w += this.getColW(i)\n }\n // Extra right padding so pinned-right zone doesn't cover the last scrollable column\n if (this.hasPinnedRight) w += this.pinnedRightWidth\n return w\n },\n\n scrollableContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && this.isColumnScrollable(i)) w += this.getColW(i)\n }\n return w\n },\n\n pinnedRightBodyStyle() {\n return {\n position: 'sticky',\n right: 0,\n width: this.pinnedRightWidth + 'px',\n zIndex: 10,\n background: 'white',\n flexShrink: 0\n }\n },\n\n pinnedRightHeaderStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative', overflowAnchor: 'none' }\n },\n\n footerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flexShrink: 0,\n position: 'sticky',\n bottom: 0,\n zIndex: 30,\n background: 'var(--eg-color-footer-bg)',\n boxShadow: '0 -2px 0 0 var(--eg-color-border-strong)'\n }\n },\n\n footerLabelStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: this._footerRowH + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--eg-color-footer-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderTop: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: 'var(--eg-font-size-header)',\n fontWeight: 600\n }\n },\n\n frozenColFooterStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n pinnedRightFooterStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n colOutlineRowStyle() {\n return {\n position: 'absolute',\n top: -OUTLINE_GUTTER + 'px',\n left: 0,\n right: 0,\n height: OUTLINE_GUTTER + 'px'\n }\n },\n\n resizeLineStyle() {\n if (!this.resizing.active) return {}\n if (this.resizing.type === 'col') {\n return {\n position: 'absolute',\n top: 0,\n left: (this.resizing.currentPos - (this.resizing.containerLeft || 0)) + 'px',\n width: '2px',\n height: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'absolute',\n left: 0,\n top: (this.resizing.currentPos - (this.resizing.containerTop || 0)) + 'px',\n height: '2px',\n width: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size: {\n immediate: true,\n handler(v) { this.internalSize = v || 'middle' }\n },\n\n theme: {\n immediate: true,\n handler(v) { this.internalTheme = v || 'excel' }\n },\n\n // Runs before the `columns`/`data` watchers below (both call\n // _recomputeRowFilters()) so internalFilters already reflects the prop\n // by the time either of them fires during the initial immediate pass.\n filters: {\n immediate: true,\n handler(v) { this.internalFilters = v ? { ...v } : {} }\n },\n\n internalFilters() { this._scheduleFilterRecompute() },\n\n filterRow() {\n this._recomputeRowFilters()\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n sort: {\n immediate: true,\n handler(v) { this.internalSort = v ? { key: v.key, direction: v.direction } : null }\n },\n\n internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n // Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:\n // true` prop doesn't flash the icon before the grid has even appeared.\n internalLocked(v) {\n if (!this._lockFlashArmed) return\n this._flashLock(v)\n },\n\n autoWidth(v) {\n if (v) this._applyAutoWidth()\n },\n\n columns: {\n immediate: true,\n handler(cols) {\n // Build key→width map from current state before rebuilding leafCols.\n // This preserves user-resized widths across column reorders: when columns\n // are moved, _flatIndex values shift but col.key stays the same.\n const keyToWidth = {}\n ;(this.leafCols || []).forEach(col => {\n if (this.colWidths[col._flatIndex] !== undefined) {\n keyToWidth[col.key] = this.colWidths[col._flatIndex]\n }\n })\n\n this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n\n // Rebuild colWidths by new flat index, using col.key to look up saved widths\n const newColWidths = {}\n this.leafCols.forEach(col => {\n newColWidths[col._flatIndex] = keyToWidth[col.key] !== undefined\n ? keyToWidth[col.key]\n : (col.width || DEFAULT_COL_WIDTH)\n })\n this.colWidths = newColWidths\n\n this.computeColumnTotals()\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this._applyAutoWidth()\n }\n },\n merges: {\n immediate: true,\n handler(v) { this.internalMerges = v ? [...v] : [] }\n },\n collapsedGroups: {\n immediate: true,\n handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }\n },\n locked: {\n immediate: true,\n handler(v) { this.internalLocked = !!v }\n },\n data: {\n immediate: true,\n handler(newData) {\n // Keep _dataRaw in sync so template uses fresh data without triggering dependArray\n this._dataRaw = newData || []\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this.computeColumnTotals()\n // Force re-render so cell values refresh even when virtualScroll indices don't change\n // (e.g. editing a cell: same row count, same scroll position, only content changed)\n this.$forceUpdate()\n this._applyAutoWidth()\n }\n },\n totalRows() { this.updateVirtualScroll() },\n hiddenRowSet() { this.updateVirtualScroll() },\n totalVirtualHeight() { this.$nextTick(this.syncHeaderViewportWidth) }\n },\n\n created() {\n // Non-reactive row data reference — avoids Vue 2 dependArray(data) O(n) loop.\n // Vue 2 calls dependArray() for EVERY `data[ri]` access in the template,\n // iterating all 50,000 rows each time. With 1200 cells per render that's\n // 60 million iterations per render (~600ms). _dataRaw bypasses this.\n this._dataRaw = this.data || []\n\n // Non-reactive caches: same (w,h) → same style object → Vue 2 skips DOM style writes\n this._cellStyleCache = Object.create(null)\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n // rAF handles for scroll/resize throttle\n this._scrollRAF = null\n this._resizeRAF = null\n\n // Undo/redo history — non-reactive to avoid Vue overhead.\n // Each entry: { type: 'data'|'merges', before: {...}, after: {...} }\n // 'data' stores only diff rows: { indices: number[], rows: object[] }\n this._history = []\n this._historyIndex = -1\n this._historySkip = false // true while undo/redo is in progress\n\n // Column drag preview offsets — non-reactive so direct DOM updates bypass Vue re-renders.\n // Keys are column keys, values are pixel offsets.\n this._dragOffsets = {}\n\n },\n\n mounted() {\n this.updateVirtualScroll()\n this.$nextTick(this.syncHeaderViewportWidth)\n document.addEventListener('mousemove', this.onGlobalMouseMove)\n document.addEventListener('mouseup', this.onGlobalMouseUp)\n document.addEventListener('keydown', this.onGlobalKeyDown)\n document.addEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.addEventListener('resize', this.syncHeaderViewportWidth)\n window.addEventListener('scroll', this._onWindowScrollForDropdown, true)\n this._lockFlashArmed = true\n },\n\n updated() {\n // After Vue re-renders (e.g., virtual scroll adds new rows during drag),\n // apply missing transforms to newly rendered cells that don't have one yet.\n if (!this.colDrag.active || !this.$el) return\n const offsets = this._dragOffsets\n if (!offsets || !Object.keys(offsets).length) return\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n if (el.style.transform) return\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n if (dx) el.style.transform = `translateX(${dx}px)`\n })\n },\n\n beforeDestroy() {\n clearTimeout(this._filterRecomputeTimer)\n document.removeEventListener('mousemove', this.onGlobalMouseMove)\n document.removeEventListener('mouseup', this.onGlobalMouseUp)\n document.removeEventListener('keydown', this.onGlobalKeyDown)\n document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.removeEventListener('resize', this.syncHeaderViewportWidth)\n window.removeEventListener('scroll', this._onWindowScrollForDropdown, true)\n if (this.isFullscreen) {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n },\n\n methods: {\n // =========================================================\n // VIRTUAL SCROLL\n // =========================================================\n // In fullscreen, .eg-scroll-body's real height comes from flexbox filling\n // the viewport (which can be far taller than the `height` prop the host\n // originally passed for the fixed-size layout) — measure it directly\n // instead of trusting `height`, otherwise virtual scroll only ever\n // renders enough rows for the old fixed size, leaving the rest of the\n // (visually stretched) container blank.\n _containerH() {\n if (this.isFullscreen && this.$refs.scrollBody) return this.$refs.scrollBody.clientHeight\n const footerH = this.hasFooter ? this._footerRowH : 0\n return this.effectiveHeight - (this._headerRowH * this.headerDepth) - this._filterRowH - footerH\n },\n\n // Same fullscreen-aware measurement as _containerH(), but mirrors PageUp/\n // PageDown's pre-existing (footer-agnostic) row-span formula exactly for\n // the non-fullscreen case, so normal-mode behavior is unchanged.\n _pageRowSpan() {\n const h = this.isFullscreen && this.$refs.scrollBody\n ? this.$refs.scrollBody.clientHeight\n : (this.effectiveHeight - this._headerRowH * this.headerDepth - this._filterRowH)\n return Math.floor(h / this._rowH)\n },\n\n updateVirtualScroll() {\n // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll.\n // overscan=8 (≈224px at default row height) gives enough buffer for fast scroll\n // without blank-band artifacts between rAF updates.\n const result = findVisibleRange(\n this.scrollTop, Math.max(this._containerH(), 100), this.totalRows, this.cumulativeHeights, 8\n )\n this.virtualStartIndex = result.startIndex\n this.virtualEndIndex = result.endIndex\n this.virtualOffsetTop = result.offsetTop\n this.totalVirtualHeight = result.totalHeight\n },\n\n onBodyScroll(e) {\n const target = e.target\n // Sync sibling scroll containers immediately (visual alignment, no layout trigger)\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = target.scrollLeft\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = target.scrollLeft\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = target.scrollTop\n\n // Close dropdown on scroll — fixed-position dropdown drifts away from the moving cell\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame.\n // Read scrollTop from the DOM ref at rAF time (not from the stale event closure)\n // so that multiple scroll events arriving between frames still resolve to the\n // final position — eliminates the flickering caused by render/scroll mismatch.\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n const sb = this.$refs.scrollBody\n if (!sb) return\n this.scrollTop = sb.scrollTop\n this.scrollLeft = sb.scrollLeft\n this.updateVirtualScroll()\n })\n }\n },\n\n onScroll(e) {},\n\n // .eg-scroll-col-header (overflow:hidden, synced via JS) has no native scrollbar, but\n // .eg-scroll-body (overflow:auto) does — its real vertical scrollbar eats into its\n // clientWidth. If both containers just grow via flex:1, the header ends up wider than\n // the body, so its max scrollLeft is smaller and syncing clamps it before the body\n // reaches its rightmost scroll position — column borders desync right at the scroll\n // end. Locking the header to the body's exact measured clientWidth (scrollColHeaderStyle)\n // makes both max scrollLeft values identical.\n syncHeaderViewportWidth() {\n // Auto-height's 70vh cap tracks the real viewport — keep it current on resize.\n if (typeof window !== 'undefined' && this.windowInnerH !== window.innerHeight) {\n this.windowInnerH = window.innerHeight\n }\n const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\n // Window resize while fullscreen changes the real available row-viewport\n // height too (see _containerH()) — recompute which rows are rendered.\n // Same for auto-height (height prop omitted): the 70vh cap just moved.\n if (this.isFullscreen || this.height == null) this.updateVirtualScroll()\n },\n\n // =========================================================\n // HELPERS\n // =========================================================\n isRowHidden(ri) { return this.hiddenRowSet.has(ri) },\n isColHidden(ci) { return this.effectiveHiddenColSet.has(ci) },\n\n getColW(ci) {\n return this.colWidths[ci] !== undefined ? this.colWidths[ci] : (this.leafCols[ci] ? this.leafCols[ci].width || DEFAULT_COL_WIDTH : DEFAULT_COL_WIDTH)\n },\n\n getRowH(ri) {\n if (this.isRowHidden(ri)) return 0\n return this.rowHeights[ri] || this._rowH\n },\n\n getColLeft(ci) {\n let left = 0\n for (let i = 0; i < ci && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) left += this.getColW(i)\n }\n return left\n },\n\n getFrozenWidth() {\n return this.hasPinnedLeft ? this.pinnedLeftWidth : 0\n },\n\n isColumnPinnedLeft(ci) {\n return this.pinnedLeftSet.has(ci)\n },\n\n isColumnPinnedRight(ci) {\n return this.pinnedRightSet.has(ci)\n },\n\n isColumnScrollable(ci) {\n return !this.isColumnPinnedLeft(ci) && !this.isColumnPinnedRight(ci)\n },\n\n getZoneColLeft(ci, zone) {\n let left = 0\n if (zone === 'left') {\n for (const col of this.pinnedLeftCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else if (zone === 'right') {\n for (const col of this.pinnedRightCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else {\n for (let i = 0; i < this.leafCols.length; i++) {\n const col = this.leafCols[i]\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex) && this.isColumnScrollable(col._flatIndex)) {\n left += this.getColW(col._flatIndex)\n }\n }\n }\n return left\n },\n\n isCellInZone(cell, rowIdx, zone) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n const z = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n return z === zone\n },\n\n getHeaderCellStyleForZone(cell, rowIndex, zone) {\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z !== zone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getRangeWidth(startCol, endCol) {\n let w = 0\n for (let i = startCol; i <= endCol && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) w += this.getColW(i)\n }\n return w\n },\n\n getRangeHeight(startRow, endRow) {\n let h = 0\n for (let i = startRow; i <= endRow && i < this.totalRows; i++) {\n if (!this.isRowHidden(i)) h += this.getRowH(i)\n }\n return h\n },\n\n getCellValue(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return ''\n const row = this._dataRaw[ri]\n if (!row) return ''\n const v = row[col.key]\n return v !== undefined ? v : ''\n },\n\n isEditing(ri, ci) {\n return this.editingCell && this.editingCell.row === ri && this.editingCell.col === ci\n },\n\n isMergedAway(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return false\n const nm = normalizeRange(merge)\n return !(nm.startRow === ri && nm.startCol === ci)\n },\n\n getMergeSpan(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return { rowspan: 1, colspan: 1 }\n const nm = normalizeRange(merge)\n return {\n rowspan: nm.endRow - nm.startRow + 1,\n colspan: nm.endCol - nm.startCol + 1\n }\n },\n\n isSelected(ri, ci) {\n return isCellInAnyRange(ri, ci, this.selectionRanges)\n },\n\n isActiveCell(ri, ci) {\n return this.activeCell.row === ri && this.activeCell.col === ci\n },\n\n isCopyHighlighted(ri, ci) {\n if (!this.clipboardRange || this.clipboardMode !== 'copy') return false\n return isCellInRange(ri, ci, this.clipboardRange)\n },\n\n // =========================================================\n // STYLES\n // =========================================================\n getRowStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowStyleCache[key]) return this._rowStyleCache[key]\n const style = { height: h + 'px', minHeight: h + 'px' }\n this._rowStyleCache[key] = style\n return style\n },\n\n getRowHeaderStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowHeaderStyleCache[key]) return this._rowHeaderStyleCache[key]\n const style = { height: h + 'px' }\n this._rowHeaderStyleCache[key] = style\n return style\n },\n\n getCellStyle(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n const nm = merge ? normalizeRange(merge) : null\n let w = this.getColW(ci)\n let h = this.getRowH(ri)\n\n if (nm && nm.startRow === ri && nm.startCol === ci) {\n w = this.getRangeWidth(nm.startCol, nm.endCol)\n h = this.getRangeHeight(nm.startRow, nm.endRow)\n }\n\n // Position cells with the same absolute-left arithmetic the header uses\n // (getZoneColLeft), instead of relying on flex layout to accumulate widths —\n // flex and a JS integer sum can drift apart by a few px over many columns,\n // desyncing the body's column borders from the header's.\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n\n // Cache by dimensions+position — same (w,h,left) → same object ref → Vue 2 skips DOM style writes\n const key = w + '_' + h + '_' + left\n if (this._cellStyleCache[key]) return this._cellStyleCache[key]\n const style = { position: 'absolute', left: left + 'px', top: '0px', width: w + 'px', minWidth: w + 'px', height: h + 'px' }\n this._cellStyleCache[key] = style\n return style\n },\n\n getCellClasses(ri, ci) {\n const col = this.leafCols[ci]\n return {\n 'eg-cell-active': this.isActiveCell(ri, ci),\n 'eg-cell-selected': this.isSelected(ri, ci) && !this.isActiveCell(ri, ci),\n 'eg-cell-fill-preview': this.isInFillPreview(ri, ci),\n 'eg-cell-copy': this.isCopyHighlighted(ri, ci),\n 'eg-cell-merged-hidden': this.isMergedAway(ri, ci),\n 'eg-cell-select': !!(col && this.isSelectLikeCol(col)),\n 'eg-cell-date': !!(col && col.type === 'date'),\n 'eg-cell-checkbox': !!(col && col.type === 'checkbox'),\n 'eg-cell-action': !!(col && col.type === 'action'),\n 'eg-cell-readonly': !this.isCellEditable(ri, ci),\n 'eg-cell-invalid': !!(this.invalidCell && this.invalidCell.row === ri && this.invalidCell.col === ci),\n 'eg-cell--has-suffix': !!(col && this.hasSuffixSlot(col)),\n 'eg-cell--drag-source': this.colDrag.active && ci >= this.colDrag.sourceFirstLeaf && ci < this.colDrag.sourceFirstLeaf + this.colDrag.sourceLeafCount,\n 'eg-cell-align-right': !!(col && col.align === 'right'),\n 'eg-cell-align-center': !!(col && col.align === 'center')\n }\n },\n\n isInFillPreview(ri, ci) {\n if (!this.fillDrag.active || !this.fillDrag.sourceRange) return false\n const src = this.fillDrag.sourceRange\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n if (targetRow > src.endRow) {\n return ri > src.endRow && ri <= targetRow && ci >= src.startCol && ci <= src.endCol\n }\n if (targetCol > src.endCol) {\n return ci > src.endCol && ci <= targetCol && ri >= src.startRow && ri <= src.endRow\n }\n return false\n },\n\n getHeaderCellStyle(cell, rowIndex, isFrozen) {\n const zone = isFrozen ? 'left' : 'scroll'\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n if (isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (!inZone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getHeaderDragTransform(cell) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking, no re-render on offset change\n let key\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n } else {\n key = cell.column.key\n }\n if (!key) return null\n const dx = this._dragOffsets[key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n getCellDragTransform(ci) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking\n const col = this.leafCols[ci]\n if (!col) return null\n const dx = this._dragOffsets[col.key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n\n // col.totalConfig.visible (boolean) takes priority over col.showTotal when present.\n isColumnTotalVisible(col) {\n if (col.totalConfig && typeof col.totalConfig.visible === 'boolean') return col.totalConfig.visible\n return !!col.showTotal\n },\n\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!this.isColumnTotalVisible(col)) return\n let sum = 0\n let hasNumber = false\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n const v = row ? row[col.key] : undefined\n if (typeof v === 'number' && isFinite(v)) {\n sum += v\n hasNumber = true\n }\n }\n totals[col._flatIndex] = hasNumber ? Math.round(sum * 1e6) / 1e6 : null\n })\n this.columnTotals = totals\n },\n\n getColumnTotalDisplay(ci) {\n const col = this.leafCols[ci]\n if (!col || !this.isColumnTotalVisible(col)) return ''\n const raw = this.columnTotals[ci]\n const cfg = col.totalConfig\n if (cfg && typeof cfg.formatter === 'function') {\n return cfg.formatter(raw, this._dataRaw, col, ci)\n }\n return raw === null || raw === undefined ? '-' : raw\n },\n\n getFooterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n const style = {\n position: 'absolute',\n left: left + 'px',\n top: '0px',\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px'\n }\n const col = this.leafCols[ci]\n const extra = col && col.totalConfig && col.totalConfig.style\n return extra ? { ...style, ...extra } : style\n },\n\n getFooterCellClass(ci) {\n const col = this.leafCols[ci]\n const classList = col && col.totalConfig && col.totalConfig.classList\n return classList || null\n },\n\n isCellVisible(cell, rowIndex, isFrozen) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (inZone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n return isFrozen ? this.isColumnPinnedLeft(ci) : this.isColumnScrollable(ci)\n },\n\n getRowGroupBtnStyle(group) {\n const top = getRowTop(group.startIndex, this._rowH, this.rowHeights) - this.scrollTop\n return {\n position: 'absolute',\n top: top + 'px',\n left: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px',\n zIndex: 5\n }\n },\n\n getColGroupBtnStyle(group) {\n const left = this.getColLeft(group.startIndex) - this.scrollLeft\n return {\n position: 'absolute',\n left: left + 'px',\n top: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px'\n }\n },\n\n // =========================================================\n // CELL SELECTION (3.1)\n // =========================================================\n selectCell(row, col, addToSelection = false) {\n if (row < 0 || row >= this.totalRows || col < 0 || col >= this.leafCols.length) return\n this.activeCell = { row, col }\n if (addToSelection) {\n this.selectionRanges.push(singleCellRange(row, col))\n } else {\n this.selectionRanges = [singleCellRange(row, col)]\n }\n this.emitSelectionChange()\n },\n\n selectRange(startRow, startCol, endRow, endCol, replace = true) {\n const range = { startRow, startCol, endRow, endCol }\n if (replace) {\n this.selectionRanges = [range]\n } else {\n this.selectionRanges.push(range)\n }\n this.emitSelectionChange()\n },\n\n selectRow(ri) {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [{ startRow: ri, endRow: ri, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n selectColumn(ci) {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n this.emitSelectionChange()\n },\n\n selectAll() {\n this.activeCell = { row: 0, col: 0 }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n emitSelectionChange() {\n this.$emit('selection-change', {\n activeCell: { ...this.activeCell },\n ranges: this.selectionRanges.map(r => normalizeRange(r))\n })\n },\n\n onCellMouseDown(e, ri, ci) {\n this.invalidCell = null\n if (this.editingCell) {\n const editCol = this.leafCols[this.editingCell.col]\n if (editCol && this._hasCustomEdit(editCol)) {\n this.cancelEdit()\n } else {\n this.commitEdit()\n }\n }\n this.hideContextMenu()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n\n if (e.ctrlKey || e.metaKey) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Ctrl+click: start new independent range\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges.push(this.mouseDownRange)\n this.isCtrlDrag = true\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Shift+click: extend primary range to this cell\n this.selectionRanges = [buildRange(this.activeCell, { row: ri, col: ci })]\n } else {\n const wasActive = this.activeCell.row === ri && this.activeCell.col === ci\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges = [this.mouseDownRange]\n this.isCtrlDrag = false\n\n // Open select/date dropdown on click if cell was already active (2nd click)\n const col = this.leafCols[ci]\n if (wasActive && col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openSelectDropdown(ri, ci, e)\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (wasActive && col && col.type === 'date' && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openDatePickerForActive()\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n }\n\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onCellMouseOver(e, ri, ci) {\n if (!this.isMouseDown) return\n if (this.fillDrag.active) return\n if (this.dragMove.active) {\n this.dragMove.targetRow = ri\n this.dragMove.targetCol = ci\n return\n }\n\n // Extend current drag range\n if (this.mouseDownRange) {\n this.mouseDownRange.endRow = ri\n this.mouseDownRange.endCol = ci\n this.emitSelectionChange()\n }\n },\n\n onCellMouseUp() {\n this.isMouseDown = false\n },\n\n onCellDblClick(ri, ci) {\n const col = this.leafCols[ci]\n if (!this.isCellEditable(ri, ci)) return\n if (col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col)) {\n this.openSelectDropdownForActive()\n return\n }\n if (col && col.type === 'date' && !this._hasCustomEdit(col)) {\n this.openDatePickerForActive()\n return\n }\n if (col && col.type === 'checkbox') return\n if (col && col.type === 'action') return\n this.startEditing(ri, ci, null, true)\n },\n\n onRowHeaderMouseDown(e, ri) {\n const endCol = this.leafCols.length - 1\n const fullRow = { startRow: ri, endRow: ri, startCol: 0, endCol }\n\n if (e.ctrlKey || e.metaKey) {\n if (e.shiftKey && this.selectionRanges.length > 0) {\n // Ctrl+Shift: active qatordan ri gacha yangi range qo'sh\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [...this.selectionRanges, { startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n // Ctrl+click: qatorni toggle qil\n const idx = this.selectionRanges.findIndex(r =>\n r.startRow === ri && r.endRow === ri && r.startCol === 0 && r.endCol === endCol\n )\n if (idx >= 0) {\n const next = this.selectionRanges.filter((_, i) => i !== idx)\n this.selectionRanges = next.length ? next : [fullRow]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [...this.selectionRanges, fullRow]\n }\n }\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [{ startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [fullRow]\n }\n\n this.mouseDownRange = this.selectionRanges[this.selectionRanges.length - 1]\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderMouseDown(e, cell) {\n // Start pending drag (activates after 5 px horizontal movement)\n // Works for both leaf and group headers\n if (this.columnDraggable && !this.internalLocked && !e.shiftKey && !e.ctrlKey && !e.metaKey) {\n this.colDrag.pending = true\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = cell._firstLeafFlatIndex\n this.colDrag.sourceLeafCount = cell.isGroup ? cell.colspan : 1\n this.colDrag.startX = e.clientX\n this.colDrag.startY = e.clientY\n this.colDrag.ghostLabel = cell.column.title || ''\n }\n\n if (cell.isGroup) {\n // Group header click: select all leaf columns within the group\n const startCol = cell._firstLeafFlatIndex\n const endCol = startCol + cell.colspan - 1\n this.activeCell = { row: 0, col: startCol }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol, endCol }]\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n return\n }\n\n const ci = cell.column._flatIndex\n if (e.shiftKey && this.selectionRanges.length > 0) {\n const sc = Math.min(this.activeCell.col, ci)\n const ec = Math.max(this.activeCell.col, ci)\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: sc, endCol: ec }]\n } else {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n }\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderDblClick(e, cell) {\n if (cell.isGroup) return\n this.autoFitColumn(cell.column._flatIndex)\n },\n\n onColHeaderContextMenu(e, cell) {\n if (!cell.isGroup) {\n this._openContextMenu(e, -1, cell.column._flatIndex)\n }\n },\n\n onRowContextMenu(e, ri) {\n this._openContextMenu(e, ri, -1)\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this._openContextMenu(e, ri, ci)\n },\n\n _openContextMenu(e, row, col) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row, col }\n this.contextMenuPositioned = false\n this.$nextTick(this._positionContextMenu)\n },\n\n // Flips the menu up/left if it would overflow the viewport at the raw\n // click coordinates — measured against the actual rendered size (item\n // count varies with devMode/pin/row-vs-col context, so an estimate isn't\n // reliable). Rendered off-screen (left:-9999px, see template) while\n // unpositioned so this first measurement reflects the menu's true\n // natural (single-line) width — measuring it in place near a viewport\n // edge would let the browser's own shrink-to-fit sizing wrap the text\n // to fit the cramped space, which then \"sticks\" even after flipping to\n // a roomier position since we only move it, we don't force a width.\n // Stays hidden (visibility, not v-if) until repositioned so there's no\n // visible jump.\n _positionContextMenu() {\n const el = this.$refs.contextMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.contextMenu.x\n let y = this.contextMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.contextMenu.x = x\n this.contextMenu.y = y\n this.contextMenuPositioned = true\n },\n\n hideContextMenu() {\n this.contextMenu.visible = false\n },\n\n // =========================================================\n // KEYBOARD NAVIGATION (3.2)\n // =========================================================\n onKeyDown(e) {\n // Delegate keyboard navigation to the active dropdown component\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n if (comp && comp.handleKey) comp.handleKey(e)\n return\n }\n\n if (this.infoModal) {\n if (e.key === 'Escape') this.closeInfoModal()\n return\n }\n\n if (this.editingCell) {\n // Normally handled by onEditKeyDown on the built-in <input> (which stops\n // propagation, so this branch never runs for it). Custom #cell-{key} edit\n // slots own their own input and focus it themselves — if that focus didn't\n // land (e.g. the host relies on the unreliable HTML `autofocus` attribute\n // on a freshly-inserted element), Escape falls through to here instead.\n // Without this, editingCell stays stuck and even arrow-key navigation is\n // swallowed by this same guard on every subsequent keydown.\n if (e.key === 'Escape') this.cancelEdit()\n return\n }\n const key = e.key\n\n if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {\n e.preventDefault()\n this.undo()\n return\n }\n if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {\n e.preventDefault()\n this.redo()\n return\n }\n\n if (key === 'a' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.selectAll()\n return\n }\n\n if (key === 'c' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.copySelection()\n return\n }\n if (key === 'x' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.cutSelection()\n return\n }\n if (key === 'v' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.pasteAt(this.activeCell.row, this.activeCell.col)\n return\n }\n\n if (key === 'Delete' || key === 'Backspace') {\n e.preventDefault()\n this.clearSelection()\n return\n }\n\n if (key === 'F2') {\n e.preventDefault()\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openDatePickerForActive()\n } else if (activeCol && activeCol.type === 'checkbox' && activeEditable) {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else if (activeCol && activeCol.type === 'action' && activeEditable) {\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n } else if (activeEditable) {\n this.startEditing(this.activeCell.row, this.activeCell.col, null, true)\n }\n return\n }\n\n if (key === ' ') {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n return\n }\n }\n\n if (key === 'Escape') {\n this.clipboardRange = null\n this.clipboardMatrix = null\n this.clipboardMode = null\n return\n }\n\n if (key === 'Tab') {\n e.preventDefault()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n return\n }\n if (key === 'Enter') {\n e.preventDefault()\n this.moveActive(e.shiftKey ? -1 : 1, 0, e)\n return\n }\n\n const arrows = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] }\n if (arrows[key]) {\n e.preventDefault()\n const [dr, dc] = arrows[key]\n if (e.shiftKey) {\n // Extend selection\n const pr = this.primaryRange || singleCellRange(this.activeCell.row, this.activeCell.col)\n const nm = normalizeRange(pr)\n // Determine which edge to extend\n if (dr === -1) pr.startRow = Math.max(0, nm.startRow - 1)\n if (dr === 1) pr.endRow = Math.min(this.totalRows - 1, nm.endRow + 1)\n if (dc === -1) pr.startCol = Math.max(0, nm.startCol - 1)\n if (dc === 1) pr.endCol = Math.min(this.leafCols.length - 1, nm.endCol + 1)\n this.selectionRanges = [pr]\n this.emitSelectionChange()\n } else {\n this.moveActive(dr, dc, e)\n }\n return\n }\n\n if (key === 'Home') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(0, 0)\n } else {\n this.selectCell(this.activeCell.row, 0)\n }\n return\n }\n if (key === 'End') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(this.totalRows - 1, this.leafCols.length - 1)\n } else {\n this.selectCell(this.activeCell.row, this.leafCols.length - 1)\n }\n return\n }\n if (key === 'PageUp') {\n e.preventDefault()\n this.moveActive(-this._pageRowSpan(), 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n this.moveActive(this._pageRowSpan(), 0, e)\n return\n }\n },\n\n onKeyPress(e) {\n if (this.editingCell) return\n if (this.selectDropdown) return\n if (this.datePickerOverlay) return\n if (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openDatePickerForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n return\n }\n this.startEditing(this.activeCell.row, this.activeCell.col, e.key, false)\n }\n },\n\n onGlobalKeyDown(e) {\n if (e.key === 'Escape' && (this.colDrag.active || this.colDrag.pending)) {\n this._resetColDrag()\n e.preventDefault()\n }\n },\n\n moveActive(dr, dc, e) {\n let row = this.activeCell.row + dr\n let col = this.activeCell.col + dc\n\n // Tab: wrap around rows\n if (e && e.key === 'Tab') {\n if (col >= this.leafCols.length) { col = 0; row++ }\n if (col < 0) { col = this.leafCols.length - 1; row-- }\n }\n\n row = Math.max(0, Math.min(this.totalRows - 1, row))\n col = Math.max(0, Math.min(this.leafCols.length - 1, col))\n\n // Skip hidden cols\n while (this.isColHidden(col) && col > 0 && col < this.leafCols.length - 1) {\n col += dc >= 0 ? 1 : -1\n }\n // Skip hidden rows\n while (this.isRowHidden(row) && row > 0 && row < this.totalRows - 1) {\n row += dr >= 0 ? 1 : -1\n }\n\n // Handle merge navigation\n const resolved = resolveMergeAnchor(row, col, this.internalMerges)\n row = resolved.row\n col = resolved.col\n\n this.activeCell = { row, col }\n this.selectionRanges = [singleCellRange(row, col)]\n this.emitSelectionChange()\n this.scrollToCell(row, col)\n },\n\n scrollToCell(ri, ci) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rowTop = getRowTop(ri, this._rowH, this.rowHeights)\n const rowBottom = rowTop + this.getRowH(ri)\n const colLeft = this.getColLeft(ci) - this.getFrozenWidth()\n const colRight = colLeft + this.getColW(ci)\n\n if (rowTop < scrollBody.scrollTop) scrollBody.scrollTop = rowTop\n if (rowBottom > scrollBody.scrollTop + scrollBody.clientHeight) {\n scrollBody.scrollTop = rowBottom - scrollBody.clientHeight\n }\n if (colLeft < scrollBody.scrollLeft) scrollBody.scrollLeft = colLeft\n if (colRight > scrollBody.scrollLeft + scrollBody.clientWidth) {\n scrollBody.scrollLeft = colRight - scrollBody.clientWidth\n }\n },\n\n // =========================================================\n // EDITING (used by 3.1 + 3.2)\n // =========================================================\n startEditing(ri, ci, initialChar = null, keepValue = false) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (ri < 0 || ri >= this.totalRows) return\n if (!this._hasCustomEdit(col)) {\n if (this.isSelectLikeCol(col)) { this.openSelectDropdownForActive(); return }\n if (col.type === 'date') { this.openDatePickerForActive(); return }\n if (col.type === 'checkbox') return\n if (col.type === 'action') return\n }\n\n this.invalidCell = null\n this.editingCell = { row: ri, col: ci }\n if (initialChar !== null) {\n this.editValue = initialChar\n } else if (keepValue) {\n this.editValue = String(this.getCellValue(ri, ci) || '')\n } else {\n this.editValue = ''\n }\n this.editOriginalValue = String(this.getCellValue(ri, ci) || '')\n\n this.$nextTick(() => {\n const inputs = this.$refs.editInput\n if (inputs) {\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) {\n input.focus()\n if (keepValue) {\n input.setSelectionRange(input.value.length, input.value.length)\n }\n }\n }\n })\n },\n\n commitEdit() {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n\n if (!this.isCellValueValid(row, col, this.editValue)) {\n this._showInvalidCell(row, col, this.editValue)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: this.editValue }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n cancelEdit() {\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n hasCellSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])\n },\n hasLegacyEditSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])\n },\n hasPrefixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['prefix-' + col.key])\n },\n hasSuffixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['suffix-' + col.key])\n },\n _hasCustomEdit(col) {\n // For select/treeSelect/date: cell-{key} slot only customizes display; picker still opens normally\n if (col.type === 'select' || col.type === 'treeSelect' || col.type === 'date') {\n return this.hasLegacyEditSlot(col)\n }\n return this.hasCellSlot(col) || this.hasLegacyEditSlot(col)\n },\n\n commitEditSlot(value) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // CHECKBOX CELLS\n // =========================================================\n\n isCheckboxChecked(ri, ci) {\n const v = this.getCellValue(ri, ci)\n if (v === true || v === 1) return true\n if (typeof v === 'string') return ['true', '1'].includes(v.trim().toLowerCase())\n return false\n },\n\n setCheckboxValue(ri, ci, value) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (!this.isCellValueValid(ri, ci, value)) {\n this._showInvalidCell(ri, ci, value)\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[ri] = { ...oldData[ri], [col.key]: value }\n this._emitDataChange(oldData, newData)\n },\n\n toggleCheckbox(ri, ci) {\n this.setCheckboxValue(ri, ci, !this.isCheckboxChecked(ri, ci))\n },\n\n // =========================================================\n // ACTION CELLS (col.type === 'action')\n // =========================================================\n\n // Dynamic v-on argument expressions can't contain quotes/spaces in Vue 2.6\n // templates, so the 'click' fallback is resolved here instead of inline.\n actionHandlerEvent(col) {\n return (col && col.handlerType) || 'click'\n },\n\n triggerCellAction(ri, ci, e) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || col.type !== 'action' || typeof col.handler !== 'function') return\n if (!this.isCellEditable(ri, ci)) return\n col.handler(this._dataRaw[ri], ri, col, ci, e)\n },\n\n // =========================================================\n // CELL EDITABILITY (3.17)\n // =========================================================\n\n isCellEditable(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return false\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellEditable === 'function') return !!this.cellEditable(row, ri, col, ci)\n if (typeof col.editable === 'function') return !!col.editable(row, ri)\n if (col.editable === false) return false\n return true\n },\n\n // =========================================================\n // CELL VALUE VALIDATION (cellValidator)\n // =========================================================\n\n isCellValueValid(ri, ci, value) {\n if (typeof this.cellValidator !== 'function') return true\n const col = this.leafCols[ci]\n if (!col) return true\n const row = this._dataRaw[ri] || {}\n return !!this.cellValidator(value, row, ri, col, ci)\n },\n\n _resolveValidationMessage(ri, ci, value) {\n const col = this.leafCols[ci]\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellValidationMessage === 'function') return this.cellValidationMessage(value, row, ri, col, ci)\n if (typeof this.cellValidationMessage === 'string' && this.cellValidationMessage) return this.cellValidationMessage\n return this.t.invalidValue\n },\n\n // Find the on-screen rect of the cell currently being committed — the edit\n // input if still mounted (commitEdit, before editingCell is cleared), else\n // the active cell element (checkbox/select commits, which never enter edit mode).\n _invalidCellRect() {\n const inputs = this.$refs.editInput\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) return input.getBoundingClientRect()\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n return el ? el.getBoundingClientRect() : null\n },\n\n _showInvalidCell(ri, ci, value) {\n const message = this._resolveValidationMessage(ri, ci, value)\n this.invalidCell = { row: ri, col: ci, message, rect: this._invalidCellRect() }\n this.$emit('cell-validation-error', { row: ri, col: ci, value, message })\n },\n\n // Resolve+emit messages for a batch of rejected { row, col, value } entries\n // from a bulk write (paste / fill / clear). blocked=true means the whole\n // operation was aborted (strictBulkValidation); false means these cells\n // were skipped but the rest of the write still applied.\n _emitBulkValidationError(skipped, blocked) {\n const cells = skipped.map(c => ({\n row: c.row, col: c.col, value: c.value,\n message: this._resolveValidationMessage(c.row, c.col, c.value)\n }))\n this.$emit('bulk-validation-error', { blocked, cells })\n },\n\n // =========================================================\n // SELECT DROPDOWN (3.16) + TREE SELECT (3.22)\n // =========================================================\n\n isSelectLikeCol(col) {\n return !!(col && (col.type === 'select' || col.type === 'treeSelect'))\n },\n\n treeOptionKeys(col) {\n return {\n valueKey: col.optionValueKey || 'value',\n labelKey: col.optionLabelKey || 'label',\n childrenKey: col.optionChildrenKey || 'children'\n }\n },\n\n _treeFindLabel(nodes, keys, value) {\n for (const node of (nodes || [])) {\n if (String(node[keys.valueKey]) === value) {\n return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])\n }\n const found = this._treeFindLabel(node[keys.childrenKey], keys, value)\n if (found != null) return found\n }\n return null\n },\n\n // col.options may be a static array OR a function (row, rowIndex, column, colIndex) => array,\n // letting hosts return a different option list per cell.\n hasOptionsSource(col) {\n return !!(col && (Array.isArray(col.options) || typeof col.options === 'function'))\n },\n\n resolveOptionsArray(col, ri, ci) {\n if (!col) return []\n if (typeof col.options === 'function') {\n const row = ri != null ? (this._dataRaw[ri] || {}) : {}\n const result = col.options(row, ri, col, ci != null ? ci : col._flatIndex)\n return Array.isArray(result) ? result : []\n }\n return Array.isArray(col.options) ? col.options : []\n },\n\n colOptions(ci, ri) {\n const col = this.leafCols[ci]\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return []\n return raw.map(o =>\n typeof o === 'string' ? { value: o, label: o } : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n\n getSelectLabel(ci, value, ri) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return v\n const found = this._treeFindLabel(raw, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci, ri)\n if (!opts.length) return v\n const found = opts.find(o => String(o.value) === v)\n return found ? found.label : v\n },\n\n _buildSelectDropdownState(ri, ci, col, currentValue, rect) {\n const rectObj = { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n return {\n row: ri, col: ci,\n tree: col.type === 'treeSelect',\n keys: col.type === 'treeSelect' ? this.treeOptionKeys(col) : null,\n rawOptions: this.resolveOptionsArray(col, ri, ci),\n currentValue,\n rect: rectObj,\n enableSearch: !!col.enableSearch\n }\n },\n\n openSelectDropdown(ri, ci, e) {\n const col = this.leafCols[ci]\n if (!this.hasOptionsSource(col)) return\n const el = e.currentTarget\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(ri, ci) != null ? this.getCellValue(ri, ci) : '')\n this.selectDropdown = this._buildSelectDropdownState(ri, ci, col, currentValue, rect)\n },\n\n openSelectDropdownForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!this.hasOptionsSource(colDef)) return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(row, col) != null ? this.getCellValue(row, col) : '')\n this.selectDropdown = this._buildSelectDropdownState(row, col, colDef, currentValue, rect)\n })\n },\n\n onComponentSelect({ value, label }) {\n if (this.internalLocked) { this.closeSelectDropdown(); return }\n if (!this.selectDropdown) return\n const { row, col } = this.selectDropdown\n const colDef = this.leafCols[col]\n if (colDef) {\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.closeSelectDropdown()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n if (typeof colDef.onSelect === 'function') {\n // Deferred the same way as _fireOnSelectForRange (paste/fill/drag-move) —\n // a host watching the emitted data (e.g. mirroring it into its own\n // store) needs that watcher to run first, or onSelect reads stale state.\n this.$nextTick(() => colDef.onSelect({ value, label }, newData[row], row, colDef))\n }\n }\n this.closeSelectDropdown()\n },\n\n // Always returns focus to the grid wrapper — the dropdown (especially with\n // enableSearch) can hold real DOM focus on its own search input, and every\n // close path (Escape, outside click, picking a value) must hand focus back\n // or keyboard navigation on the grid goes dead until the user clicks a cell.\n closeSelectDropdown() {\n this.selectDropdown = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // DATE PICKER\n // =========================================================\n\n getCellDisplayValue(ci, val, ri) {\n const col = this.leafCols[ci]\n if (!col) return val == null ? '' : val\n if (typeof col.formatter === 'function') {\n const out = col.formatter(val, this._dataRaw[ri], ri, col, ci)\n return out == null ? '' : out\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)\n if (col.type === 'date') {\n const d = parseDateValue(val)\n return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)\n }\n return val == null ? '' : val\n },\n\n // =========================================================\n // FILTER ROW\n // =========================================================\n\n // Which control the filter cell renders for this column.\n // 'none' cells still render (empty) so column alignment stays intact.\n filterControlType(col) {\n if (!col || col.filterable === false || col.type === 'action') return 'none'\n if (col.type === 'checkbox') return 'checkbox'\n // Function-based options can't be resolved into a fixed list outside a\n // specific row, so those columns fall back to the text control.\n if ((col.type === 'select' || col.type === 'treeSelect') && Array.isArray(col.options)) return 'select'\n return 'text'\n },\n\n // Flat { value, label } list for a select-control filter cell. treeSelect\n // options are recursively flattened, each depth indented for readability.\n filterOptionsFor(col) {\n if (!col) return []\n if (col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, null, col._flatIndex)\n const out = []\n this._flattenTreeOptionsForFilter(raw, this.treeOptionKeys(col), 0, out)\n return out\n }\n return this.colOptions(col._flatIndex, null)\n },\n\n _flattenTreeOptionsForFilter(nodes, keys, depth, out) {\n for (const node of (nodes || [])) {\n const value = node[keys.valueKey]\n const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(value)\n out.push({ value: String(value), label: ' '.repeat(depth) + label })\n this._flattenTreeOptionsForFilter(node[keys.childrenKey], keys, depth + 1, out)\n }\n },\n\n getFilterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n return {\n position: 'absolute',\n left: left + 'px',\n top: (this._headerRowH * this.headerDepth) + 'px',\n width: w + 'px',\n height: this._filterRowH + 'px',\n boxSizing: 'border-box'\n }\n },\n\n setFilter(key, value) {\n if (!this.leafCols.some(c => c.key === key)) return\n const next = { ...this.internalFilters }\n if (value === '' || value === null || value === undefined) {\n delete next[key]\n } else {\n next[key] = value\n }\n this.internalFilters = next\n this.$emit('update:filters', { ...next })\n this.$emit('filter-change', { key, value, filters: { ...next } })\n },\n\n clearFilters() {\n if (!Object.keys(this.internalFilters).length) return\n this.internalFilters = {}\n // A deliberate, infrequent action (unlike typing) — recompute immediately\n // rather than waiting out the debounce, and drop any timer already pending.\n clearTimeout(this._filterRecomputeTimer)\n this._recomputeRowFilters()\n this.$emit('update:filters', {})\n this.$emit('filter-change', { key: null, value: null, filters: {} })\n },\n\n // Debounced — recomputing filteredRowIndices scans every row, so typing\n // in the text filter shouldn't trigger it on every keystroke.\n _scheduleFilterRecompute() {\n clearTimeout(this._filterRecomputeTimer)\n this._filterRecomputeTimer = setTimeout(() => { this._recomputeRowFilters() }, 150)\n },\n\n _activeFilterCols() {\n const cols = []\n for (const col of this.leafCols) {\n if (this.filterControlType(col) === 'none') continue\n const v = this.internalFilters[col.key]\n if (v === undefined || v === null || v === '') continue\n cols.push(col)\n }\n return cols\n },\n\n _cellMatchesFilter(row, ri, col) {\n const ci = col._flatIndex\n const filterValue = this.internalFilters[col.key]\n if (typeof col.filterFn === 'function') {\n return !!col.filterFn(this.getCellValue(ri, ci), filterValue, row, ri, col)\n }\n const kind = this.filterControlType(col)\n if (kind === 'checkbox') {\n const checked = this.isCheckboxChecked(ri, ci)\n return filterValue === 'true' ? checked : !checked\n }\n if (kind === 'select') {\n const raw = this.getCellValue(ri, ci)\n return String(raw == null ? '' : raw) === String(filterValue)\n }\n const display = this.getCellDisplayValue(ci, this.getCellValue(ri, ci), ri)\n return String(display == null ? '' : display).toLowerCase().indexOf(String(filterValue).toLowerCase()) !== -1\n },\n\n _recomputeRowFilters() {\n // Can be reached (via the `columns`/`filterRow` watchers) during initWatch(),\n // before created() has set up _dataRaw — getCellValue() depends on it. The\n // `data` watcher's own immediate handler sets _dataRaw first, then always\n // calls this again, so the initial mount still ends up correct.\n if (!this._dataRaw) return\n if (!this.filterRow) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const activeCols = this._activeFilterCols()\n if (!activeCols.length) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const rows = this.data\n const out = []\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n let matches = true\n for (const col of activeCols) {\n if (!this._cellMatchesFilter(row, ri, col)) { matches = false; break }\n }\n if (!matches) out.push(ri)\n }\n this.filteredRowIndices = out\n },\n\n // =========================================================\n // COLUMN SORTING\n // =========================================================\n\n isColSortable(col) {\n return !!(this.sortable && col && col.sortable !== false && col.type !== 'action')\n },\n\n sortIconClass(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return 'eg-icon-sort'\n return this.internalSort.direction === 'asc' ? 'eg-icon-sort-asc' : 'eg-icon-sort-desc'\n },\n\n sortIconTitle(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return this.t.sortNone\n return this.internalSort.direction === 'asc' ? this.t.sortAsc : this.t.sortDesc\n },\n\n // Cycles a column through none → asc → desc → none. Used by the header\n // sort icon's click handler; also usable from a custom #header-{key} slot.\n toggleSort(key) {\n if (this.internalLocked) return\n const current = this.internalSort\n if (!current || current.key !== key) { this.setSort(key, 'asc'); return }\n if (current.direction === 'asc') { this.setSort(key, 'desc'); return }\n this.clearSort()\n },\n\n setSort(key, direction) {\n if (direction !== 'asc' && direction !== 'desc') { this.clearSort(); return }\n this.sortByColumn(key, direction)\n },\n\n // Clicking a third time (or calling this directly) only clears the sort\n // indicator — it does not restore the pre-sort row order by itself,\n // since sorting already reordered `data` in place. Use undo() for that.\n clearSort() {\n if (!this.internalSort) return\n this.internalSort = null\n this.$emit('update:sort', null)\n this.$emit('sort-change', { key: null, direction: null })\n },\n\n sortByColumn(key, direction) {\n if (this.internalLocked) return\n // Merges and row groups are anchored to row *position* — a reorder\n // would silently invalidate them, so sorting is blocked while either\n // exists (same \"blocked while locked\" no-op convention as elsewhere).\n if (this.internalMerges.length || this.rowGroups.length) return\n const col = this.leafCols.find(c => c.key === key)\n if (!this.isColSortable(col)) return\n\n const oldData = this.data\n const pairs = oldData.map((row, ri) => ({ row, ri }))\n pairs.sort((a, b) => this._compareForSort(a.row, a.ri, b.row, b.ri, col, direction))\n const newData = pairs.map(p => p.row)\n\n this.internalSort = { key, direction }\n this._emitDataChange(oldData, newData)\n this.$emit('update:sort', { key, direction })\n this.$emit('sort-change', { key, direction })\n\n this.scrollTop = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) this.$refs.scrollBody.scrollTop = 0\n })\n },\n\n _sortValueFor(row, ri, col) {\n const ci = col._flatIndex\n if (col.type === 'checkbox') return this.isCheckboxChecked(ri, ci) ? 1 : 0\n if (col.type === 'date') {\n const d = parseDateValue(this.getCellValue(ri, ci))\n return d ? d.getTime() : null\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, this.getCellValue(ri, ci), ri)\n return this.getCellValue(ri, ci)\n },\n\n _compareForSort(rowA, riA, rowB, riB, col, direction) {\n if (typeof col.sortFn === 'function') {\n const result = col.sortFn(rowA, rowB, col)\n return direction === 'desc' ? -result : result\n }\n const a = this._sortValueFor(rowA, riA, col)\n const b = this._sortValueFor(rowB, riB, col)\n const aEmpty = a === null || a === undefined || a === ''\n const bEmpty = b === null || b === undefined || b === ''\n if (aEmpty && bEmpty) return 0\n if (aEmpty) return 1\n if (bEmpty) return -1\n const cmp = (typeof a === 'number' && typeof b === 'number')\n ? a - b\n : String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' })\n return direction === 'desc' ? -cmp : cmp\n },\n\n openDatePickerForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!colDef || colDef.type !== 'date') return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const val = this.getCellValue(row, col)\n this.datePickerOverlay = {\n row, col,\n format: colDef.format || 'YYYY-MM-DD',\n value: val == null ? '' : String(val),\n rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n }\n })\n },\n\n onDatePick(formattedVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n const colDef = this.leafCols[col]\n if (colDef) {\n if (formattedVal !== '' && !this.isCellValueValid(row, col, formattedVal)) {\n this._showInvalidCell(row, col, formattedVal)\n this.closeDatePicker()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: formattedVal }\n this._emitDataChange(oldData, newData)\n }\n this.closeDatePicker()\n },\n\n // See closeSelectDropdown() — same reasoning: every close path must return\n // focus to the grid wrapper or keyboard navigation goes dead afterward.\n closeDatePicker() {\n this.datePickerOverlay = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n _onWindowScrollForDropdown(e) {\n if (this.contextMenu.visible) this.hideContextMenu()\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n _onDocMouseDownForDropdown(e) {\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n onEditKeyDown(e) {\n if (e.key === 'Escape') {\n e.preventDefault()\n this.cancelEdit()\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(1, 0, e)\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n }\n },\n\n clearSelection() {\n if (this.internalLocked) return\n if (!this.selectionRanges.length) return\n const oldData = this.data\n const newData = oldData.slice()\n let hasChanges = false\n const skipped = []\n for (const range of this.selectionRanges) {\n const r = {\n startRow: Math.min(range.startRow, range.endRow),\n endRow: Math.max(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endCol: Math.max(range.startCol, range.endCol)\n }\n for (let ri = r.startRow; ri <= r.endRow && ri < newData.length; ri++) {\n let rowUpdated = null\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, '')) { skipped.push({ row: ri, col: ci, value: '' }); continue }\n if (!rowUpdated) rowUpdated = { ...newData[ri] }\n rowUpdated[col.key] = ''\n hasChanges = true\n }\n }\n if (rowUpdated) newData[ri] = rowUpdated\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n if (hasChanges) this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // FILL HANDLE (3.3)\n // =========================================================\n startFillDrag(e) {\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n this.fillDrag.active = true\n this.fillDrag.sourceRange = normalizeRange(pr)\n this.fillDrag.currentRow = pr.endRow\n this.fillDrag.currentCol = pr.endCol\n e.preventDefault()\n },\n\n onGlobalMouseMove(e) {\n // Detect column drag threshold (5 px)\n if (this.colDrag.pending) {\n if (Math.abs(e.clientX - this.colDrag.startX) > 5) {\n this.colDrag.pending = false\n this.colDrag.active = true\n this.colDrag.dropIndex = this.colDrag.sourceFirstLeaf\n this.isMouseDown = false // cancel cell-selection drag\n }\n }\n if (this.colDrag.active) {\n this.handleColDragMove(e)\n return\n }\n\n if (this.resizing.active) {\n this.handleResizeMove(e)\n return\n }\n if (this.fillDrag.active) {\n this.handleFillDragMove(e)\n return\n }\n if (this.dragMove.active) {\n this.handleDragMoveMove(e)\n return\n }\n },\n\n onGlobalMouseUp(e) {\n if (this.colDrag.active) {\n this.commitColDrag()\n return\n }\n if (this.colDrag.pending) {\n this._resetColDrag()\n // fall through — normal mouseup\n }\n\n if (this.resizing.active) {\n this.commitResize()\n return\n }\n if (this.fillDrag.active) {\n this.commitFillDrag()\n return\n }\n if (this.dragMove.active) {\n this.commitDragMove(e)\n return\n }\n this.isMouseDown = false\n },\n\n handleFillDragMove(e) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rect = scrollBody.getBoundingClientRect()\n const relY = e.clientY - rect.top + scrollBody.scrollTop\n const relX = e.clientX - rect.left + scrollBody.scrollLeft\n\n // Binary search on cumulativeHeights — O(log n) instead of O(n)\n const cum = this.cumulativeHeights\n const total = this.totalRows\n let lo = 0, hi = total - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (cum[mid + 1] <= relY) lo = mid + 1\n else hi = mid\n }\n const row = Math.min(lo, total - 1)\n\n // Columns are few — O(columns) is fast\n let left = this.getFrozenWidth()\n let col = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColumnScrollable(i)) continue\n left += this.getColW(i)\n if (relX < left) { col = i; break }\n col = i\n }\n\n this.fillDrag.currentRow = row\n this.fillDrag.currentCol = col\n },\n\n async commitFillDrag() {\n if (this.internalLocked) { this.fillDrag.active = false; return }\n const src = this.fillDrag.sourceRange\n if (!src) { this.fillDrag.active = false; return }\n\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n\n const oldData = this.data\n const newData = oldData.slice() // only affected rows get new objects\n let targetRange\n const skipped = []\n\n if (targetRow > src.endRow) {\n // Fill down\n const count = targetRow - src.endRow\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n targetRange = { startRow: src.endRow + 1, endRow: targetRow, startCol: src.startCol, endCol: src.endCol }\n } else if (targetCol > src.endCol) {\n // Fill right\n const count = targetCol - src.endCol\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n const srcValues = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n srcValues.push(col && oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ci = src.endCol + 1 + k\n if (ci < this.leafCols.length) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n }\n targetRange = { startRow: src.startRow, endRow: src.endRow, startCol: src.endCol + 1, endCol: targetCol }\n }\n\n let fireOnSelect = false\n if (targetRange) {\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n } else {\n this._emitDataChange(oldData, newData)\n this.$emit('fill', { sourceRange: src, targetRange, values: newData })\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n fireOnSelect = true\n }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\n\n if (fireOnSelect) {\n // See pasteAt's _fireOnSelectForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n doubleFillDown() {\n const pr = this.primaryRange\n if (!pr) return\n const src = normalizeRange(pr)\n // Find adjacent column filled length\n const adjCol = src.startCol > 0 ? src.startCol - 1 : src.endCol + 1\n if (adjCol < 0 || adjCol >= this.leafCols.length) return\n const adjColDef = this.leafCols[adjCol]\n if (!adjColDef) return\n let lastFilledRow = src.endRow\n for (let ri = src.endRow + 1; ri < this.totalRows; ri++) {\n if (this.data[ri] && this.data[ri][adjColDef.key] !== '' && this.data[ri][adjColDef.key] !== undefined) {\n lastFilledRow = ri\n } else break\n }\n if (lastFilledRow <= src.endRow) return\n\n const count = lastFilledRow - src.endRow\n const oldData = this.data\n const newData = oldData.slice()\n const skipped = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // COPY / PASTE (3.4)\n // =========================================================\n copySelection() {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n this.clipboardMatrix = extractMatrix(this.data, this.leafCols, r)\n this.clipboardRange = r\n this.clipboardMode = 'copy'\n\n // Write to system clipboard as TSV\n const tsv = matrixToTSV(this.clipboardMatrix)\n if (navigator.clipboard) {\n navigator.clipboard.writeText(tsv).catch(() => {})\n }\n },\n\n cutSelection() {\n this.copySelection()\n this.clipboardMode = 'cut'\n },\n\n // Bulk cell writes (paste, fill handle, drag-move) set row values directly\n // and skip the single-cell dropdown path in onComponentSelect — so\n // col.onSelect never fires for them unless we replay it here for every\n // select/treeSelect cell whose value actually changed in the range.\n _fireOnSelectForRange(oldData, newData, startRow, endRow, startCol, endCol) {\n for (let ri = startRow; ri <= endRow; ri++) {\n const oldRow = oldData[ri]\n const newRow = newData[ri]\n if (!newRow || newRow === oldRow) continue\n for (let ci = startCol; ci <= endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col || !this.isSelectLikeCol(col) || typeof col.onSelect !== 'function') continue\n const oldVal = oldRow ? oldRow[col.key] : undefined\n const newVal = newRow[col.key]\n if (newVal === oldVal) continue\n col.onSelect({ value: newVal, label: this.getSelectLabel(ci, newVal, ri) }, newRow, ri, col)\n }\n }\n },\n\n async pasteAt(targetRow, targetCol, options = {}) {\n if (this.internalLocked) return\n const { mode = 'all', transpose = false } = options\n\n let matrix = null\n\n // Try system clipboard first\n if (navigator.clipboard) {\n try {\n const text = await navigator.clipboard.readText()\n if (text) matrix = tsvToMatrix(text)\n } catch (_) {}\n }\n\n // Fall back to internal clipboard\n if (!matrix && this.clipboardMatrix) {\n matrix = this.clipboardMatrix\n }\n\n if (!matrix) return\n\n if (transpose) matrix = transposeMatrix(matrix)\n\n // ── Excel-style selection broadcast / tiling ──────────────────────────\n // When pasting over a multi-cell selection and the clipboard matrix is\n // smaller than the selection, tile the matrix to fill the selection —\n // provided the selection dimensions are exact multiples of the matrix\n // dimensions (matches Excel's tiling rule exactly).\n // Cut-paste is never tiled (source must be cleared exactly once).\n const isCopyPaste = !this.clipboardMode || this.clipboardMode === 'copy'\n if (isCopyPaste) {\n const pr = this.primaryRange\n if (pr) {\n const sel = normalizeRange(pr)\n const selRows = sel.endRow - sel.startRow + 1\n const selCols = sel.endCol - sel.startCol + 1\n const matRows = matrix.length\n const matCols = matrix[0] ? matrix[0].length : 0\n\n // Only tile when selection is strictly larger than clipboard\n if ((selRows > matRows || selCols > matCols) &&\n selRows % matRows === 0 && selCols % matCols === 0) {\n matrix = tileMatrix(matrix, selRows, selCols)\n // Paste at selection origin, not just at the active cell\n targetRow = sel.startRow\n targetCol = sel.startCol\n }\n }\n }\n\n const oldData = this.data\n const { newData, affectedRange, skipped } = applyMatrix(\n oldData, this.leafCols, targetRow, targetCol, matrix, mode,\n (ri, ci) => this.isCellEditable(ri, ci),\n (ri, ci, v) => this.isCellValueValid(ri, ci, v)\n )\n\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n\n if (this.clipboardMode === 'cut' && this.clipboardRange) {\n // Clear source — ensure each source row is a new copy before mutation\n const src = this.clipboardRange\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci) && this.isCellValueValid(ri, ci, '')) {\n newData[ri][col.key] = ''\n }\n }\n }\n this.clipboardRange = null\n this.clipboardMode = null\n }\n\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n // Wait for Vue's reactivity queue to flush before firing onSelect — hosts\n // commonly react to the emitted 'input' with their own watcher (e.g. to\n // mirror the row into an external store); firing onSelect synchronously\n // would run it against that store's pre-paste, stale state.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, affectedRange.startRow, affectedRange.endRow, affectedRange.startCol, affectedRange.endCol)\n },\n\n ctxCopy() { this.hideContextMenu(); this.copySelection() },\n ctxCut() { this.hideContextMenu(); this.cutSelection() },\n ctxPaste() { this.hideContextMenu(); this.pasteAt(this.activeCell.row, this.activeCell.col) },\n ctxPasteSpecial(mode, transpose = false) {\n this.hideContextMenu()\n this.pasteAt(this.activeCell.row, this.activeCell.col, { mode, transpose })\n },\n\n // =========================================================\n // COLUMN DRAG-AND-DROP\n // =========================================================\n\n _computeDragOffsets(dropIndex) {\n const { sourceFirstLeaf, sourceLeafCount } = this.colDrag\n if (dropIndex < 0 || (dropIndex >= sourceFirstLeaf && dropIndex <= sourceFirstLeaf + sourceLeafCount)) return {}\n\n const previewCols = moveColumnNodeInTree(this.columns, sourceFirstLeaf, sourceLeafCount, dropIndex)\n const previewLeaves = flattenColumns(previewCols)\n\n const byKey = {}\n this.leafCols.forEach(c => { byKey[c.key] = c })\n\n const offsets = {}\n for (const zone of ['left', 'scroll', 'right']) {\n const inZone = zone === 'left'\n ? ci => this.isColumnPinnedLeft(ci)\n : zone === 'right'\n ? ci => this.isColumnPinnedRight(ci)\n : ci => this.isColumnScrollable(ci)\n\n const origPos = {}\n let acc = 0\n this.leafCols.forEach(col => {\n const ci = col._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) return\n origPos[col.key] = acc\n acc += this.getColW(ci)\n })\n\n const previewPos = {}\n acc = 0\n for (const col of previewLeaves) {\n const origCol = byKey[col.key]\n if (!origCol) continue\n const ci = origCol._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) continue\n previewPos[col.key] = acc\n acc += this.getColW(ci)\n }\n\n for (const key of Object.keys(origPos)) {\n offsets[key] = (previewPos[key] !== undefined ? previewPos[key] : origPos[key]) - origPos[key]\n }\n }\n return offsets\n },\n\n _applyDragTransforms(offsets) {\n if (!this.$el) return\n const targets = []\n const dxMap = new Map()\n\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n targets.push(el)\n dxMap.set(el, dx)\n })\n this.$el.querySelectorAll('[data-start]').forEach(el => {\n const start = parseInt(el.dataset.start)\n const end = parseInt(el.dataset.end)\n if (isNaN(start)) return\n let key = null\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n targets.push(el)\n dxMap.set(el, key ? (offsets[key] || 0) : 0)\n })\n if (!targets.length) return\n\n // anime.remove cancels any in-progress animation before starting the new one.\n // This prevents chaining/overlap when dropIndex changes rapidly (no flickering).\n anime.remove(targets)\n anime({\n targets,\n translateX: el => dxMap.get(el) || 0,\n duration: 80,\n easing: 'easeOutQuart'\n })\n },\n\n _clearDragTransforms() {\n if (!this.$el) return\n const els = Array.from(this.$el.querySelectorAll('[data-fi], [data-start]'))\n // Kill all anime animations first, then instantly clear inline styles\n anime.remove(els)\n els.forEach(el => {\n el.style.transform = ''\n el.style.transition = ''\n })\n },\n\n handleColDragMove(e) {\n // Update ghost position via direct DOM — avoids Vue re-render on every mousemove\n const ghost = this.$refs.colDragGhost\n if (ghost) {\n ghost.style.left = (e.clientX + 16) + 'px'\n ghost.style.top = (e.clientY - 14) + 'px'\n }\n\n const prevDrop = this.colDrag.dropIndex\n this._updateColDragDropIndex(e.clientX)\n const newDrop = this.colDrag.dropIndex\n\n if (newDrop !== prevDrop) {\n this._dragOffsets = this._computeDragOffsets(newDrop)\n this._applyDragTransforms(this._dragOffsets)\n }\n },\n\n _updateColDragDropIndex(clientX) {\n if (!this.$el) return\n const n = this.leafCols.length\n if (n === 0) { this.colDrag.dropIndex = 0; return }\n\n // Use DOM cell rects but subtract CSS transforms so we get ORIGINAL positions.\n // This avoids circular dependency: live-preview transforms don't affect drop calc.\n const cells = Array.from(this.$el.querySelectorAll('.eg-header-cell:not(.eg-header-group)'))\n .map(el => {\n const fi = parseInt(el.dataset.fi)\n if (isNaN(fi)) return null\n const rect = el.getBoundingClientRect()\n const m = (el.style.transform || '').match(/translateX\\((-?[\\d.]+)px\\)/)\n const dx = m ? parseFloat(m[1]) : 0\n return { fi, left: rect.left - dx, right: rect.right - dx }\n })\n .filter(Boolean)\n .sort((a, b) => a.left - b.left)\n\n if (!cells.length) { this.colDrag.dropIndex = n; return }\n\n let dropIndex = n\n for (const { fi, left, right } of cells) {\n if (clientX < right) {\n dropIndex = clientX < left + (right - left) / 2 ? fi : fi + 1\n break\n }\n }\n this.colDrag.dropIndex = dropIndex\n },\n\n commitColDrag() {\n const from = this.colDrag.sourceFirstLeaf\n const leafCount = this.colDrag.sourceLeafCount\n const to = this.colDrag.dropIndex\n\n if (from < 0 || to < 0 || leafCount < 1) { this._resetColDrag(); return }\n if (to >= from && to <= from + leafCount) { this._resetColDrag(); return }\n\n // Kill anime animations but KEEP transform values in DOM while Vue re-renders\n // with the new column order. After Vue updates left positions ($nextTick),\n // we instantly clear transforms — both steps run as microtasks before browser paints,\n // so there is never a visible intermediate state.\n if (this.$el) {\n anime.remove(Array.from(this.$el.querySelectorAll('[data-fi], [data-start]')))\n }\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n\n const newCols = moveColumnNodeInTree(this.columns, from, leafCount, to)\n this.$emit('update:columns', newCols)\n this.$emit('column-reorder', { fromIndex: from, toIndex: to, columns: newCols })\n\n // Clear transforms after Vue re-renders new column left positions\n this.$nextTick(() => { this._clearDragTransforms() })\n },\n\n _resetColDrag() {\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n this._clearDragTransforms()\n },\n\n // =========================================================\n // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'col',\n index: ci,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getColW(ci),\n currentPos: e.clientX,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'row',\n index: ri,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getRowH(ri),\n currentPos: e.clientY,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n handleResizeMove(e) {\n if (this.resizing.type === 'col') {\n this.resizing.currentPos = e.clientX\n const newWidth = Math.max(20, this.resizing.startSize + (e.clientX - this.resizing.startX))\n this.$set(this.colWidths, this.resizing.index, newWidth)\n this._cellStyleCache = Object.create(null)\n } else {\n this.resizing.currentPos = e.clientY\n this._resizePreviewHeight = Math.max(10, this.resizing.startSize + (e.clientY - this.resizing.startY))\n // Batch $set + updateVirtualScroll to rAF — prevents O(n) cumulativeHeights rebuild\n // on every mousemove (200+ Hz). At most 60 rebuilds/sec instead of 200+.\n if (!this._resizeRAF) {\n this._resizeRAF = requestAnimationFrame(() => {\n this._resizeRAF = null\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n this.updateVirtualScroll()\n })\n }\n }\n },\n\n commitResize() {\n if (this._resizeRAF) {\n cancelAnimationFrame(this._resizeRAF)\n this._resizeRAF = null\n // Apply the last pending preview height immediately on commit\n if (this.resizing.type === 'row' && this._resizePreviewHeight !== undefined) {\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n }\n }\n if (this.resizing.type === 'col') {\n this._cellStyleCache = Object.create(null)\n const newSize = this.colWidths[this.resizing.index]\n this.$emit('column-resize', { index: this.resizing.index, newSize })\n } else {\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n const newSize = this.rowHeights[this.resizing.index]\n this.$emit('row-resize', { index: this.resizing.index, newSize })\n this.updateVirtualScroll()\n }\n this.resizing.active = false\n },\n\n autoFitColumn(ci) {\n const col = this.leafCols[ci]\n if (!col) return\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n const newWidth = Math.max(50, maxLen * 8 + 16)\n this.$set(this.colWidths, ci, newWidth)\n this._cellStyleCache = Object.create(null)\n this.$emit('column-resize', { index: ci, newSize: newWidth })\n },\n\n _applyAutoWidth() {\n if (!this.autoWidth || !this.leafCols.length) return\n this.leafCols.forEach((col, ci) => {\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n this.$set(this.colWidths, ci, Math.max(50, maxLen * 8 + 16))\n })\n this._cellStyleCache = Object.create(null)\n },\n\n ctxHideRow() {\n const targetRow = this.contextMenu.row\n // Row header: row >= 0 AND col = -1 (no column context)\n const fromRowHeader = targetRow >= 0 && this.contextMenu.col < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenRows]\n if (fromRowHeader) {\n if (!newHidden.includes(targetRow)) newHidden.push(targetRow)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n const targetCol = this.contextMenu.col\n // Column header: col >= 0 AND row = -1 (no row context)\n const fromColHeader = targetCol >= 0 && this.contextMenu.row < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenColumns]\n if (fromColHeader) {\n if (!newHidden.includes(targetCol)) newHidden.push(targetCol)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\n }\n }\n this.$emit('update:hiddenColumns', newHidden)\n },\n\n ctxUnhideRows() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenColumns', [])\n },\n\n ctxPinLeft() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n },\n\n isColPinned(ci) {\n const col = this.leafCols[ci]\n return !!(col && this.pinnedColumns[col.key])\n },\n\n // =========================================================\n // DEV MODE — column/row config info (context menu → modal)\n // =========================================================\n _stringifyForInfo(value) {\n const seen = new WeakSet()\n return JSON.stringify(value, (key, val) => {\n if (typeof val === 'function') return 'ƒ ' + (val.name || 'anonymous') + '()'\n if (val && typeof val === 'object') {\n if (seen.has(val)) return '[Circular]'\n seen.add(val)\n }\n return val\n }, 2)\n },\n\n ctxShowColumnInfo() {\n const ci = this.contextMenu.col\n this.hideContextMenu()\n const col = this.leafCols[ci]\n if (!col) return\n const { _flatIndex, ...rest } = col\n this.infoModal = { title: `${this.t.columnInfo}: ${col.title || col.key}`, json: this._stringifyForInfo(rest) }\n },\n\n ctxShowRowInfo() {\n const ri = this.contextMenu.row\n this.hideContextMenu()\n const row = this._dataRaw[ri]\n if (!row) return\n this.infoModal = { title: `${this.t.rowInfo} #${ri + 1}`, json: this._stringifyForInfo(row) }\n },\n\n closeInfoModal() {\n this.infoModal = null\n },\n\n // =========================================================\n // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\n if (this.internalLocked) return false\n const oldMerges = this.internalMerges.slice()\n const { merges: newMerges, conflict } = addMerge(range, this.internalMerges)\n if (conflict) {\n this.$emit('merge-error', { range, reason: 'Overlaps existing merge' })\n return false\n }\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n return true\n },\n\n unmergeCells(row, col) {\n if (this.internalLocked) return\n const oldMerges = this.internalMerges.slice()\n const newMerges = removeMergeAt(row, col, this.internalMerges)\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n },\n\n ctxMerge() {\n this.hideContextMenu()\n const pr = this.primaryRange\n if (!pr) return\n this.mergeCells(normalizeRange(pr))\n },\n\n ctxUnmerge() {\n this.hideContextMenu()\n this.unmergeCells(this.activeCell.row, this.activeCell.col)\n },\n\n // =========================================================\n // GROUPING (3.7)\n // =========================================================\n groupRows(startIndex, endIndex) {\n this.internalGroups = addGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n groupColumns(startIndex, endIndex) {\n this.internalGroups = addGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupRows(startIndex, endIndex) {\n this.internalGroups = removeGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupColumns(startIndex, endIndex) {\n this.internalGroups = removeGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n toggleGroup(group) {\n this.internalCollapsedGroups = toggleGroupCollapse(group, this.internalCollapsedGroups)\n this.$emit('update:collapsedGroups', [...this.internalCollapsedGroups])\n this.$emit('group-toggle', { group, collapsed: _isGroupCollapsed(group, this.internalCollapsedGroups) })\n this.updateVirtualScroll()\n },\n\n isGroupCollapsed(group) {\n return _isGroupCollapsed(group, this.internalCollapsedGroups)\n },\n\n expandAll() {\n this.internalCollapsedGroups = []\n this.$emit('update:collapsedGroups', [])\n this.updateVirtualScroll()\n },\n\n collapseAll() {\n this.internalCollapsedGroups = [...this.internalGroups]\n this.$emit('update:collapsedGroups', [...this.internalGroups])\n this.updateVirtualScroll()\n },\n\n // =========================================================\n // DRAG AND DROP (3.9)\n // =========================================================\n startDragMove(e, ri, ci) {\n const pr = this.primaryRange\n if (!pr) return\n this.dragMove = {\n active: true,\n sourceRange: normalizeRange(pr),\n startX: e.clientX,\n startY: e.clientY,\n targetRow: ri,\n targetCol: ci,\n copyMode: e.ctrlKey || e.metaKey\n }\n this.$emit('cell-drag-start', { sourceRange: this.dragMove.sourceRange })\n e.preventDefault()\n },\n\n handleDragMoveMove(e) {\n this.dragMove.copyMode = e.ctrlKey || e.metaKey\n this.$emit('cell-drag-move', {\n sourceRange: this.dragMove.sourceRange,\n targetRow: this.dragMove.targetRow,\n targetCol: this.dragMove.targetCol\n })\n },\n\n async commitDragMove(e) {\n if (!this.dragMove.active) return\n const { sourceRange, targetRow, targetCol, copyMode } = this.dragMove\n\n const targetRange = {\n startRow: targetRow,\n startCol: targetCol,\n endRow: targetRow + (sourceRange.endRow - sourceRange.startRow),\n endCol: targetCol + (sourceRange.endCol - sourceRange.startCol)\n }\n\n let proceed = true\n if (this.beforeDrop) {\n proceed = await Promise.resolve(this.beforeDrop(sourceRange, targetRange))\n }\n\n let oldData = null\n let newData = null\n if (proceed) {\n oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n newData = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci)).newData\n\n if (!copyMode) {\n // Clear source — ensure each source row is a new copy before mutation\n for (let ri = sourceRange.startRow; ri <= sourceRange.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = sourceRange.startCol; ci <= sourceRange.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) newData[ri][col.key] = ''\n }\n }\n }\n this._emitDataChange(oldData, newData)\n this.$emit('cell-drop', { sourceRange, targetRange })\n }\n\n this.dragMove = { active: false, sourceRange: null, startX: 0, startY: 0, targetRow: -1, targetCol: -1, copyMode: false }\n this.isMouseDown = false\n\n if (proceed) {\n // See pasteAt's _fireOnSelectForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n // =========================================================\n // EXCEL EXPORT (3.11)\n // =========================================================\n async exportToExcel(options = {}) {\n const {\n filename = 'export.xlsx',\n sheetName = 'Sheet1',\n scope = 'all',\n includeHidden = false\n } = options\n\n this.$emit('export-start')\n this.exportState = { active: true }\n\n const workerPayload = {\n data: this.data,\n columns: this.columns,\n leafCols: this.leafCols,\n merges: this.internalMerges,\n hiddenCols: [...this.effectiveHiddenColSet],\n hiddenRows: [...this.hiddenRowSet],\n range: scope === 'selection' ? this.primaryRange : null,\n filename,\n sheetName,\n scope,\n includeHidden\n }\n\n try {\n const worker = createExcelExportWorker()\n if (worker) {\n await this._exportViaWorker(worker, workerPayload)\n } else {\n // Fallback: synchronous export on main thread\n _exportToExcel({\n ...workerPayload,\n hiddenColSet: this.effectiveHiddenColSet,\n hiddenRowSet: this.hiddenRowSet\n })\n }\n this.$emit('export-success')\n } catch (err) {\n this.$emit('export-error', err)\n } finally {\n this.exportState = { active: false }\n }\n },\n\n _exportViaWorker(worker, payload) {\n return new Promise((resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, buffer, filename, message } = e.data\n if (type === 'result') {\n // Trigger browser download from the main thread (DOM required)\n const blob = new Blob([new Uint8Array(buffer)], {\n type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n finish(resolve)\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Export worker xatosi'))\n\n worker.postMessage(payload)\n })\n },\n\n // =========================================================\n // EXCEL IMPORT (3.12)\n // =========================================================\n async importFromFile(file, options = {}) {\n this.importState = { active: true, percent: 5, message: \"Fayl o'qilmoqda...\" }\n\n try {\n let result\n const worker = createExcelImportWorker()\n\n if (worker) {\n result = await this._importViaWorker(worker, file, options)\n } else {\n // Fallback: chunked async on main thread\n result = await _importFromFile(\n file,\n this.columns,\n options,\n (percent, message) => { this.importState = { active: true, percent, message } }\n )\n }\n\n const { columns, merges } = result\n // Freeze each row so Vue 2 skips deep Object.defineProperty observation.\n // Vue 2 checks Object.isExtensible() — frozen objects are skipped entirely.\n // All edit paths already use spread ({ ...row }) so frozen rows are safe.\n const data = result.data.map(row => Object.freeze(row))\n this.$emit('input', data)\n this.$emit('update:columns', columns)\n\n const hasMerges = merges && merges.length > 0\n this.internalMerges = hasMerges ? merges : []\n this.$emit('update:merges', hasMerges ? merges : [])\n this.$emit('update:collapsedGroups', [])\n this.$emit('update:hiddenRows', [])\n this.$emit('import-success', { data, columns, merges })\n\n // Reset scroll state and re-derive virtual scroll after DOM settles.\n // Without this, stale scrollTop + old rowHeights cause blank bands on scroll.\n this.rowHeights = {}\n this.scrollTop = 0\n this.scrollLeft = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) {\n this.$refs.scrollBody.scrollTop = 0\n this.$refs.scrollBody.scrollLeft = 0\n }\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = 0\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = 0\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = 0\n this.updateVirtualScroll()\n })\n } catch (err) {\n this.$emit('import-error', err)\n } finally {\n this.importState = { active: false, percent: 0, message: '' }\n }\n },\n\n _importViaWorker(worker, file, options) {\n return new Promise(async (resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, percent, message, data, columns, merges } = e.data\n if (type === 'progress') {\n this.importState = { active: true, percent, message }\n } else if (type === 'result') {\n finish(resolve, { data, columns, merges })\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Import worker xatosi'))\n\n try {\n // Transfer the ArrayBuffer zero-copy into the worker\n const buffer = await file.arrayBuffer()\n worker.postMessage(\n { buffer, existingColumns: this.columns, options },\n [buffer]\n )\n } catch (err) {\n finish(reject, err)\n }\n })\n },\n\n // =========================================================\n // UNDO / REDO (3.13)\n // =========================================================\n\n // Push one entry onto the non-reactive history stack.\n // Truncates the redo tail, caps at 100 entries.\n _pushHistory(entry) {\n if (this._historyIndex < this._history.length - 1) {\n this._history.splice(this._historyIndex + 1)\n }\n this._history.push(entry)\n if (this._history.length > 100) {\n this._history.shift()\n // historyIndex stays the same — it now points to the new last entry\n } else {\n this._historyIndex++\n }\n this.historyVersion++\n },\n\n // Emit 'input' and record a data diff entry (only changed row indices).\n // Use slice() + per-row spread in callers so unchanged rows keep the\n // same reference — then the identity check here catches only real changes.\n _emitDataChange(oldData, newData) {\n if (!this._historySkip) {\n const indices = []\n const beforeRows = []\n const afterRows = []\n const len = Math.max(oldData.length, newData.length)\n for (let i = 0; i < len; i++) {\n if (oldData[i] !== newData[i]) {\n indices.push(i)\n beforeRows.push(oldData[i])\n afterRows.push(newData[i])\n }\n }\n if (indices.length > 0) {\n this._pushHistory({\n type: 'data',\n before: { indices, rows: beforeRows },\n after: { indices, rows: afterRows }\n })\n }\n }\n this.$emit('input', newData)\n },\n\n // Emit 'update:merges' and record a merges diff entry.\n _emitMergeChange(oldMerges, newMerges) {\n if (!this._historySkip) {\n this._pushHistory({\n type: 'merges',\n before: oldMerges,\n after: newMerges\n })\n }\n this.$emit('update:merges', newMerges)\n },\n\n undo() {\n if (this.internalLocked) return\n if (this._historyIndex < 0) return\n const entry = this._history[this._historyIndex--]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.before.indices.length; i++) {\n newData[entry.before.indices[i]] = entry.before.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.before.slice()\n this.$emit('update:merges', entry.before.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n redo() {\n if (this.internalLocked) return\n if (this._historyIndex >= this._history.length - 1) return\n const entry = this._history[++this._historyIndex]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.after.indices.length; i++) {\n newData[entry.after.indices[i]] = entry.after.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.after.slice()\n this.$emit('update:merges', entry.after.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n // =========================================================\n // TOOLBAR (fullscreen / lock / export / import / custom buttons)\n // =========================================================\n\n // Public, ref-callable: returns keyboard focus to the grid. Meant for\n // hosts whose action-cell `handler` opens a modal/popup outside the\n // grid's control — call this once that modal closes, otherwise the\n // browser leaves focus on document.body (its subtree was removed) and\n // arrow-key navigation goes dead until a cell is clicked again.\n focus() {\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n toggleLock() {\n this.internalLocked = !this.internalLocked\n this.$emit('update:locked', this.internalLocked)\n this.$emit('lock-change', { locked: this.internalLocked })\n },\n\n // Pops a big lock/unlock icon in the center of the grid and fades it\n // out — pure visual feedback, mirrors the video-player OSD pattern.\n // Triggered by the internalLocked watcher, so it covers every path that\n // can change the lock state (toolbar click, toggleLock(), :locked.sync).\n _flashLock(locked) {\n this.lockFlashLocked = locked\n this.lockFlashVisible = true\n this.$nextTick(() => {\n const el = this.$refs.lockFlashIconEl\n if (!el) return\n anime.remove(el)\n anime.set(el, { opacity: 0, scale: 0.5 })\n anime.timeline()\n .add({ targets: el, opacity: 1, scale: 1.12, duration: 180, easing: 'easeOutBack' })\n .add({ targets: el, scale: 1, duration: 120, easing: 'easeOutQuad' })\n .add({ targets: el, opacity: 0, duration: 350, easing: 'easeInQuad' }, '+=250')\n .finished.then(() => { this.lockFlashVisible = false })\n })\n },\n\n toolbarHas(key) {\n return this._toolbarList.indexOf(key) !== -1\n },\n\n setSize(newSize) {\n if (!SIZE_CONFIG[newSize] || newSize === this.internalSize) return\n this.internalSize = newSize\n this.$emit('update:size', newSize)\n this.$emit('size-change', { size: newSize })\n },\n\n setTheme(newTheme) {\n if (THEME_LIST.indexOf(newTheme) === -1 || newTheme === this.internalTheme) return\n this.internalTheme = newTheme\n this.$emit('update:theme', newTheme)\n this.$emit('theme-change', { theme: newTheme })\n },\n\n sizeTitle(key) {\n return { small: this.t.sizeSmall, middle: this.t.sizeMiddle, large: this.t.sizeLarge }[key] || key\n },\n\n toggleFullscreen() {\n this.isFullscreen = !this.isFullscreen\n if (this.isFullscreen) {\n this._prevBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n document.addEventListener('keydown', this._onFullscreenKeyDown)\n } else {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n this.$emit('fullscreen-change', { fullscreen: this.isFullscreen })\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n _onFullscreenKeyDown(e) {\n if (e.key === 'Escape' && this.isFullscreen) this.toggleFullscreen()\n },\n\n toolbarExportClick() {\n this.exportToExcel()\n },\n\n toolbarImportClick() {\n if (this.internalLocked) return\n const input = this.$refs.toolbarImportInput\n if (input) input.click()\n },\n\n onToolbarImportFile(e) {\n const file = e.target.files && e.target.files[0]\n e.target.value = ''\n if (!file || this.internalLocked) return\n this.importFromFile(file)\n },\n\n onToolbarButtonClick(btn, event) {\n if (typeof btn.onClick === 'function') btn.onClick(event, this)\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-shell {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n\n.eg-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n\n.eg-toolbar-group + .eg-toolbar-group {\n border-left: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-spacer {\n flex: 1 1 auto;\n}\n\n.eg-toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n\n.eg-toolbar-btn:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n\n.eg-toolbar-btn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.eg-toolbar-btn-active {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n\n.eg-toolbar-select {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n\n.eg-toolbar-select:hover {\n border-color: var(--eg-color-accent);\n}\n\n.eg-toolbar-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n\n.eg-toolbar-segmented {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-toolbar-segment {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon { width: 12px; height: 12px; }\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon { width: 15px; height: 15px; }\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon { width: 18px; height: 18px; }\n\n.eg-toolbar-icon {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n\n.eg-icon-undo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-redo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-export { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); }\n.eg-icon-import { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); }\n.eg-icon-lock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); }\n.eg-icon-unlock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); }\n.eg-icon-expand { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); }\n.eg-icon-compress{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); }\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-asc { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-desc{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); }\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-middle { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-large { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); }\n\n.eg-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n\n.eg-header-area {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n\n.eg-header-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n\n.eg-header-cell:hover { background: var(--eg-color-border-cell); }\n.eg-header-group { background: var(--eg-color-header-group-bg); }\n\n.eg-sort-icon {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon { opacity: 0.65; }\n.eg-sort-icon:hover { opacity: 1; }\n.eg-sort-icon.eg-icon-sort-asc,\n.eg-sort-icon.eg-icon-sort-desc {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty { background: var(--eg-color-header-bg); }\n\n.eg-filter-input,\n.eg-filter-select {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input:focus,\n.eg-filter-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n\n.eg-col-resize-handle {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n\n.eg-body-area {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area {\n border-top: 1px solid var(--eg-color-border);\n}\n\n.eg-frozen-col-footer,\n.eg-scroll-col-footer,\n.eg-pinned-right-footer {\n position: relative;\n}\n\n.eg-footer-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.eg-row {\n position: relative;\n}\n\n.eg-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n\n.eg-locked .eg-cell { cursor: not-allowed; }\n\n.eg-cell-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right { justify-content: flex-end; }\n.eg-cell-align-center { justify-content: center; }\n.eg-cell--has-suffix.eg-cell-select::after { display: none; }\n.eg-cell-select-trigger-wrap {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: var(--eg-color-cell-selected) !important;\n}\n\n.eg-cell-fill-preview {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n\n.eg-cell-merged-hidden {\n display: none !important;\n}\n\n.eg-cell-invalid {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n\n.eg-cell-input {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-row-resize-handle {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n\n.eg-fill-handle {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n\n.eg-context-item:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n\n.eg-context-separator {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n\n.eg-context-submenu-parent::after { content: ' ▶'; float: right; }\n\n.eg-context-submenu {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n\n.eg-context-submenu-parent:hover .eg-context-submenu { display: block; }\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-info-modal {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n\n.eg-info-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n\n.eg-info-modal-title {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n\n.eg-info-modal-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n\n.eg-info-modal-close:hover { color: var(--eg-color-text); }\n\n.eg-info-modal-body {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.eg-group-btn {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: var(--eg-color-group-bg-collapsed); }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source {\n opacity: 0.4;\n}\n.eg-col-drag-ghost {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n\n.eg-resize-line {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n\n.eg-pinned-right-body::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n\n.eg-lock-flash {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n\n.eg-import-dialog {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin 0.8s linear infinite;\n}\n@keyframes eg-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select {\n cursor: pointer;\n}\n.eg-cell-select::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap { justify-content: flex-end; }\n.eg-cell-align-center .eg-cell-action-wrap { justify-content: center; }\n.eg-validation-tooltip {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu,\n.eg-size-small .eg-context-item {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item {\n padding: 4px 14px;\n}\n.eg-size-small .eg-toolbar {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input,\n.eg-size-small .eg-filter-select {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu,\n.eg-size-large .eg-context-item {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item {\n padding: 8px 18px;\n}\n.eg-size-large .eg-toolbar {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input,\n.eg-size-large .eg-filter-select {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon {\n width: 13px;\n height: 13px;\n}\n</style>\n\n<style>\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent); }\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted); }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: var(--eg-color-text-muted); }\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px; }\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg); }\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent); }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg); }\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden; }\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent); }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell); }\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked); }\n</style>\n"]}, media: undefined })
|
|
13055
|
-
,inject("data-v-64c6d39f_1", { source: "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent);\n}\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.eg-select-placeholder { color: var(--eg-color-text-muted);\n}\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px;\n}\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg);\n}\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden;\n}\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft);\n}\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell);\n}\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked);\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwnMA,wFAAA;AACA;EACA,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,8BAAA;EACA,oBAAA;AACA;AACA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,gBAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,mCAAA;EACA,eAAA;EACA,iBAAA;EACA,2BAAA;EACA,wDAAA;AACA;AACA,2BAAA,oCAAA;AAAA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA,+BAAA,yCAAA,EAAA,eAAA,EAAA,iCAAA;AAAA;AACA,2BAAA,OAAA,EAAA,gBAAA,EAAA,uBAAA,EAAA,mBAAA;AAAA;AACA,yBAAA,iCAAA;AAAA;AACA,mBAAA,cAAA,EAAA,iCAAA,EAAA,eAAA;AAAA;AACA;EACA,mCAAA;EACA,6CAAA;EACA,+BAAA;EACA,qCAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;AACA;AACA;EACA,kBAAA;EACA,SAAA;EACA,OAAA;EACA,eAAA;EACA,YAAA;EACA,eAAA;AACA;AACA;EACA,gBAAA;EACA,oDAAA;EACA,gBAAA;EACA,MAAA;EACA,mCAAA;EACA,UAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,gBAAA;EACA,8BAAA;EACA,aAAA;EACA,2BAAA;EACA,mCAAA;EACA,oBAAA;AACA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,8BAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,2BAAA;AACA;AACA;iCACA,2CAAA;AAAA;AACA,4BAAA,gBAAA,EAAA,6BAAA;AAAA;AACA;kCACA,kDAAA;AAAA;AACA;;;;wBAIA;AACA;EACA,eAAA;EACA,iBAAA;AACA;AACA;EACA,eAAA;EACA,iBAAA;AACA;AACA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,eAAA;AACA;AACA,gBAAA;AACA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;AACA;AACA,wBAAA,kBAAA;AAAA;AACA,eAAA;AACA;EACA,kBAAA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;EACA,cAAA;EACA,WAAA;EACA,YAAA;EACA,sBAAA;EACA,yCAAA;EACA,4BAAA;EACA,2BAAA;EACA,oBAAA;EACA,mDAAA;EACA,+BAAA;EACA,yFAAA;AACA;AACA,sCAAA,oCAAA;AAAA;AACA;EACA,wCAAA;EACA,+SAAA;EACA,wBAAA;EACA,oCAAA;AACA;AACA,sDAAA,iDAAA;AAAA;AACA;4CACA,eAAA;AAAA;AACA,iDAAA,+CAAA,EAAA,yCAAA;AAAA;AACA;gCACA,mBAAA;AAAA;AACA,yDAAA,2DAAA,EAAA,uDAAA;AAAA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + internalTheme, { 'eg-shell-fullscreen': isFullscreen, 'eg-locked': internalLocked }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"_toolbarVisible\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canUndo\"\n :title=\"t.undo\"\n @click=\"undo\"\n ><span class=\"eg-toolbar-icon eg-icon-undo\"></span></button>\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canRedo\"\n :title=\"t.redo\"\n @click=\"redo\"\n ><span class=\"eg-toolbar-icon eg-icon-redo\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('export')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"t.export\"\n @click=\"toolbarExportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-export\"></span></button>\n <button\n v-if=\"toolbarHas('import')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked\"\n :title=\"t.import\"\n @click=\"toolbarImportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-import\"></span></button>\n <input\n v-if=\"toolbarHas('import')\"\n ref=\"toolbarImportInput\"\n type=\"file\"\n accept=\".xlsx,.xls,.csv\"\n style=\"display: none\"\n @change=\"onToolbarImportFile\"\n >\n </div>\n\n <slot name=\"toolbar-extra\" :locked=\"internalLocked\" :fullscreen=\"isFullscreen\"></slot>\n\n <div v-if=\"toolbarButtons.length\" class=\"eg-toolbar-group\">\n <button\n v-for=\"btn in toolbarButtons\"\n :key=\"btn.key\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"btn.title\"\n @click=\"onToolbarButtonClick(btn, $event)\"\n ><span class=\"eg-toolbar-icon\" :style=\"btn.icon ? { maskImage: 'url(' + btn.icon + ')', webkitMaskImage: 'url(' + btn.icon + ')' } : null\">{{ btn.icon ? '' : (btn.label || '') }}</span></button>\n </div>\n\n <div class=\"eg-toolbar-spacer\"></div>\n\n <div v-if=\"toolbarHas('theme')\" class=\"eg-toolbar-group\">\n <select\n class=\"eg-toolbar-select\"\n :value=\"internalTheme\"\n :title=\"t.theme\"\n @change=\"setTheme($event.target.value)\"\n >\n <option value=\"excel\">Excel</option>\n <option value=\"material\">Material</option>\n <option value=\"antd\">Ant Design</option>\n <option value=\"fluent\">Fluent</option>\n <option value=\"carbon\">Carbon</option>\n </select>\n </div>\n\n <div v-if=\"toolbarHas('size')\" class=\"eg-toolbar-segmented\" role=\"radiogroup\" :aria-label=\"t.size\">\n <button\n v-for=\"key in sizeOptions\"\n :key=\"key\"\n type=\"button\"\n role=\"radio\"\n class=\"eg-toolbar-btn eg-toolbar-segment\"\n :class=\"['eg-toolbar-segment-' + key, { 'eg-toolbar-btn-active': internalSize === key }]\"\n :aria-checked=\"internalSize === key\"\n :title=\"sizeTitle(key)\"\n @click=\"setSize(key)\"\n ><span class=\"eg-toolbar-icon\" :class=\"'eg-icon-size-' + key\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('lock')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': internalLocked }\"\n :title=\"internalLocked ? t.unlock : t.lock\"\n @click=\"toggleLock\"\n ><span class=\"eg-toolbar-icon\" :class=\"internalLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"></span></button>\n <button\n v-if=\"toolbarHas('fullscreen')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': isFullscreen }\"\n :title=\"isFullscreen ? t.exitFullscreen : t.fullscreen\"\n @click=\"toggleFullscreen\"\n ><span class=\"eg-toolbar-icon\" :class=\"isFullscreen ? 'eg-icon-compress' : 'eg-icon-expand'\"></span></button>\n </div>\n </div>\n\n <div\n class=\"eg-wrapper\"\n :class=\"'eg-size-' + internalSize\"\n :style=\"wrapperStyle\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n @keypress=\"onKeyPress\"\n ref=\"wrapper\"\n >\n <!-- Context menu -->\n <div\n v-if=\"contextMenu.visible\"\n key=\"eg-context-menu\"\n ref=\"contextMenuEl\"\n class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: (contextMenuPositioned ? contextMenu.x : -9999) + 'px', visibility: contextMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"ctxCopy\">{{ t.copy }}</div>\n <div class=\"eg-context-item\" @click=\"ctxCut\">{{ t.cut }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPaste\">{{ t.paste }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item eg-context-submenu-parent\">\n {{ t.pasteSpecial }}\n <div class=\"eg-context-submenu\">\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all')\">{{ t.pasteAll }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('values')\">{{ t.pasteValues }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('formats')\">{{ t.pasteFormats }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all', true)\">{{ t.transpose }}</div>\n </div>\n </div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxMerge\">{{ t.mergeCells }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnmerge\">{{ t.unmergeCells }}</div>\n <div class=\"eg-context-separator\"></div>\n <div v-if=\"contextMenu.row >= 0\" class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div v-if=\"contextMenu.col >= 0\" class=\"eg-context-item\" @click=\"ctxHideColumn\">{{ t.hideColumns }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideRows\">{{ t.unhideRows }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideColumns\">{{ t.unhideColumns }}</div>\n <template v-if=\"contextMenu.row < 0 && contextMenu.col >= 0\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxPinLeft\">{{ t.pinLeft }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPinRight\">{{ t.pinRight }}</div>\n <div class=\"eg-context-item\" v-if=\"isColPinned(contextMenu.col)\" @click=\"ctxUnpin\">{{ t.unpin }}</div>\n </template>\n <template v-if=\"devMode && (contextMenu.col >= 0 || contextMenu.row >= 0)\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.col >= 0\" @click=\"ctxShowColumnInfo\">{{ t.columnInfo }}</div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.row >= 0\" @click=\"ctxShowRowInfo\">{{ t.rowInfo }}</div>\n </template>\n </div>\n\n <!-- devMode: column/row info modal -->\n <div v-if=\"infoModal\" key=\"eg-info-modal-backdrop\" class=\"eg-info-modal-backdrop\" @click=\"closeInfoModal\">\n <div class=\"eg-info-modal\" @click.stop>\n <div class=\"eg-info-modal-header\">\n <span class=\"eg-info-modal-title\">{{ infoModal.title }}</span>\n <button type=\"button\" class=\"eg-info-modal-close\" @click=\"closeInfoModal\">×</button>\n </div>\n <pre class=\"eg-info-modal-body\">{{ infoModal.json }}</pre>\n </div>\n </div>\n\n <!-- Scroll container -->\n <div class=\"eg-container\" ref=\"container\" @scroll=\"onScroll\" @click=\"hideContextMenu\">\n\n <!-- ===== HEADER AREA ===== -->\n <div class=\"eg-header-area\" :style=\"headerAreaStyle\">\n\n <!-- Corner cell -->\n <div\n class=\"eg-corner\"\n :style=\"cornerStyle\"\n @click=\"selectAll\"\n ></div>\n\n <!-- Row group outline gutter (left) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-gutter\" :style=\"rowOutlineGutterStyle\"></div>\n\n <!-- Frozen columns header -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-header\" :style=\"frozenColHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, true)\">\n <div\n :key=\"'fh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, true)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'ffilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable columns header -->\n <div class=\"eg-scroll-col-header\" ref=\"scrollColHeader\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-header-inner\" :style=\"{ width: scrollContentWidth + 'px', height: (headerRowHeight * headerDepth + _filterRowH) + 'px', position: 'relative' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, false)\">\n <div\n :key=\"'sh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, false)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'sfilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns header -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-header\" :style=\"pinnedRightHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellInZone(cell, rowIdx, 'right')\">\n <div\n :key=\"'prh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyleForZone(cell, rowIdx, 'right')\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle(cell.column)\"\n @mousedown.stop\n @click.stop=\"toggleSort(cell.column.key)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prfilter-' + col._flatIndex\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(col._flatIndex)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- ===== BODY AREA ===== -->\n <div class=\"eg-body-area\" :style=\"bodyAreaStyle\">\n\n <!-- Row number header column -->\n <div class=\"eg-row-header-col\" :style=\"rowHeaderColStyle\">\n <!-- Scrollable row headers -->\n <div class=\"eg-scroll-row-headers\" :style=\"scrollRowHeadersStyle\" ref=\"scrollRowHeaders\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'srh-' + ri\"\n class=\"eg-row-header-cell\"\n :style=\"getRowHeaderStyle(ri)\"\n @mousedown=\"onRowHeaderMouseDown($event, ri)\"\n @contextmenu.prevent=\"onRowContextMenu($event, ri)\"\n >\n {{ ri + 1 }}\n <div class=\"eg-row-resize-handle\" @mousedown.stop=\"startRowResize($event, ri)\"></div>\n </div>\n </template>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Row group outline gutter (left side of rows) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-col\" :style=\"rowOutlineColStyle\">\n <template v-for=\"group in rowGroups\">\n <div\n :key=\"'rg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getRowGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n <!-- Main scroll body -->\n <div class=\"eg-scroll-body\" ref=\"scrollBody\" :style=\"scrollBodyStyle\" @scroll=\"onBodyScroll\">\n <div class=\"eg-scroll-body-inner\" :style=\"{ width: scrollContentWidth + 'px', height: totalVirtualHeight + 'px', position: 'relative' }\">\n\n <!-- Pinned left column body -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-body\" :style=\"frozenColBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'fc-cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Main data cells -->\n <div class=\"eg-main-cells\" :style=\"mainCellsStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative', width: scrollableContentWidth + 'px' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\n :class=\"{ 'eg-row--even': ri % 2 === 0 }\"\n :style=\"getRowStyle(ri)\"\n :data-row=\"ri\"\n >\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input\n v-else\n ref=\"editInput\"\n class=\"eg-cell-input\"\n v-model=\"editValue\"\n @blur=\"commitEdit\"\n @keydown.stop=\"onEditKeyDown\"\n />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Pinned Right column body -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-body\" :style=\"pinnedRightBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\n :data-fi=\"col._flatIndex\"\n :class=\"getCellClasses(ri, col._flatIndex)\"\n :style=\"getCellStyle(ri, col._flatIndex)\"\n @mousedown=\"onCellMouseDown($event, ri, col._flatIndex)\"\n @mouseover=\"onCellMouseOver($event, ri, col._flatIndex)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, col._flatIndex)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, col._flatIndex)\"\n >\n <div v-if=\"isMergedAway(ri, col._flatIndex)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, col._flatIndex)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, col._flatIndex)\"\n :disabled=\"!isCellEditable(ri, col._flatIndex)\"\n @change=\"(v) => setCheckboxValue(ri, col._flatIndex, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, col._flatIndex, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span></template><template v-else>{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Fill handle (shown on primary range) -->\n <div\n v-if=\"fillHandleVisible\"\n class=\"eg-fill-handle\"\n :style=\"fillHandleStyle\"\n @mousedown.stop=\"startFillDrag\"\n ></div>\n\n <!-- Drag move ghost border -->\n <div\n v-if=\"dragMove.active\"\n class=\"eg-drag-ghost\"\n :style=\"dragGhostStyle\"\n ></div>\n </div>\n </div>\n\n <!-- Column group outline row (top, above body) -->\n <div v-if=\"colGroups.length\" class=\"eg-col-outline-row\" :style=\"colOutlineRowStyle\">\n <template v-for=\"group in colGroups\">\n <div\n :key=\"'cg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getColGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n </div><!-- end body-area -->\n\n <!-- ===== FOOTER AREA (column totals) ===== -->\n <div v-if=\"hasFooter\" class=\"eg-footer-area\" :style=\"footerAreaStyle\">\n\n <!-- \"Jami\" label cell (mirrors corner cell) -->\n <div class=\"eg-footer-label\" :style=\"footerLabelStyle\">{{ t.total }}</div>\n\n <!-- Frozen columns footer -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-footer\" :style=\"frozenColFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div :key=\"'ff-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable footer (horizontally synced with body via scrollFooter ref) -->\n <div class=\"eg-scroll-col-footer\" ref=\"scrollFooter\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-footer-inner\" :style=\"{ width: scrollContentWidth + 'px', height: footerRowHeight + 'px', position: 'relative' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div :key=\"'sf-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns footer -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-footer\" :style=\"pinnedRightFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prf-' + col._flatIndex\"\n class=\"eg-footer-cell\"\n :class=\"getFooterCellClass(col._flatIndex)\"\n :style=\"getFooterCellStyle(col._flatIndex)\"\n >{{ getColumnTotalDisplay(col._flatIndex) }}</div>\n </div>\n </div>\n </div><!-- end footer-area -->\n\n <!-- Resize indicator line -->\n <div v-if=\"resizing.active\" class=\"eg-resize-line\" :style=\"resizeLineStyle\"></div>\n </div><!-- end container -->\n\n <!-- Import progress overlay -->\n <div v-if=\"importState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl import qilinmoqda</div>\n <div class=\"eg-import-bar-wrap\">\n <div class=\"eg-import-bar\" :style=\"{ width: importState.percent + '%' }\"></div>\n </div>\n <div class=\"eg-import-msg\">{{ importState.message }}</div>\n <div class=\"eg-import-pct\">{{ importState.percent }}%</div>\n </div>\n </div>\n\n <!-- Export progress overlay -->\n <div v-if=\"exportState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl export qilinmoqda...</div>\n <div class=\"eg-export-spinner\"></div>\n </div>\n </div>\n\n <!-- Lock/unlock state feedback — brief centered icon flash -->\n <div v-if=\"lockFlashVisible\" class=\"eg-lock-flash\" aria-hidden=\"true\">\n <span\n ref=\"lockFlashIconEl\"\n class=\"eg-lock-flash-icon\"\n :class=\"lockFlashLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"\n ></span>\n </div>\n\n <!-- Select dropdown overlay (flat select) -->\n <eg-select\n v-if=\"selectDropdown && !selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n <!-- Tree select dropdown overlay -->\n <eg-tree-select\n v-if=\"selectDropdown && selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :value-key=\"selectDropdown.keys.valueKey\"\n :label-key=\"selectDropdown.keys.labelKey\"\n :children-key=\"selectDropdown.keys.childrenKey\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n\n <!-- Date picker overlay -->\n <eg-date-picker\n v-if=\"datePickerOverlay\"\n ref=\"datePickerComp\"\n :value=\"datePickerOverlay.value\"\n :format=\"datePickerOverlay.format\"\n :locale=\"locale\"\n :trigger-rect=\"datePickerOverlay.rect\"\n @pick=\"onDatePick\"\n @close=\"closeDatePicker\"\n />\n\n <!-- Validation error tooltip (cellValidator) -->\n <div\n v-if=\"invalidCell && invalidCell.rect\"\n class=\"eg-validation-tooltip\"\n :style=\"invalidTooltipStyle\"\n >{{ invalidCell.message }}</div>\n\n <!-- Column drag ghost (follows mouse — position updated via direct DOM in handleColDragMove) -->\n <div\n v-if=\"colDrag.active\"\n ref=\"colDragGhost\"\n class=\"eg-col-drag-ghost\"\n :style=\"{ left: (colDrag.startX + 16) + 'px', top: (colDrag.startY - 14) + 'px' }\"\n >{{ colDrag.ghostLabel }}</div>\n\n </div>\n </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth, moveColumnNodeInTree } from '../utils/headerTree.js'\nimport { calcVisibleRows, buildCumulativeHeights, findVisibleRange, getRowTop, getRowHeight } from '../utils/virtualScroll.js'\nimport { generateFillValues } from '../utils/fillHandle.js'\nimport {\n matrixToTSV, tsvToMatrix, extractMatrix, transposeMatrix, applyMatrix, tileMatrix\n} from '../utils/clipboard.js'\nimport {\n mergeHasConflict, findMergeAtCell, isMergeAnchor, addMerge,\n removeMergeAt, resolveMergeAnchor\n} from '../utils/merge.js'\nimport {\n addGroup, removeGroup, toggleGroupCollapse, isGroupCollapsed as _isGroupCollapsed,\n getCollapsedRowIndices, getCollapsedColIndices\n} from '../utils/outline.js'\nimport { exportToExcel as _exportToExcel } from '../utils/excelExport.js'\nimport { importFromFile as _importFromFile } from '../utils/excelImport.js'\nimport { createExcelImportWorker, createExcelExportWorker } from '../utils/workerFactory'\nimport { getMessages } from '../utils/i18n.js'\nimport EgCheckbox from './EgCheckbox.vue'\nimport anime from 'animejs'\nimport EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\nimport EgDatePicker from './EgDatePicker.vue'\nimport { parseDateValue, formatDate } from '../utils/dateUtils.js'\n\nconst SIZE_CONFIG = {\n small: { row: 22, header: 24, footer: 24, filter: 26 },\n middle: { row: 28, header: 28, footer: 28, filter: 32 },\n large: { row: 36, header: 36, footer: 36, filter: 40 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\n// Default toolbar tool list when `toolbar` is `true` or an object without\n// its own `list` — every group except 'theme', which stays opt-in since it\n// needs :theme.sync on the parent to do anything.\nconst DEFAULT_TOOLBAR_LIST = ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen']\nconst THEME_LIST = ['excel', 'material', 'antd', 'fluent', 'carbon']\n\nexport default {\n name: 'ExcelGrid',\n\n components: { EgCheckbox, EgSelect, EgTreeSelect, EgDatePicker },\n\n // v-model → :data + @input\n model: { prop: 'data', event: 'input' },\n\n props: {\n /** Array of row objects keyed by columns[].key */\n data: { type: Array, default: () => [] },\n\n /** Column definitions (may be nested via children[]) */\n columns: { type: Array, default: () => [] },\n\n /** Indices of hidden leaf columns */\n hiddenColumns: { type: Array, default: () => [] },\n\n /** Indices of hidden rows */\n hiddenRows: { type: Array, default: () => [] },\n\n /** Initial merge descriptors [{ startRow, startCol, endRow, endCol }] */\n merges: { type: Array, default: () => [] },\n\n /** Collapsed group descriptors [{ type, startIndex, endIndex }] */\n collapsedGroups: { type: Array, default: () => [] },\n\n /**\n * Height of the grid container in px.\n *\n * Omit it (or pass `null`) for auto-height: the grid sizes itself to fit\n * the header + filter row + all rows + footer for the current `data`\n * length, capped at 70% of the viewport height (`window.innerHeight`) —\n * beyond that it stops growing and scrolls internally, same as a fixed\n * `height` would. Recomputes reactively as `data` grows/shrinks and on\n * window resize. Does not apply in fullscreen mode (already sized to the\n * viewport there) and does not include the toolbar's own height.\n *\n * Pass an explicit `Number` to opt back into the old fixed-height behavior.\n */\n height: { type: Number, default: null },\n\n /** Width of the grid container (px or 'auto') */\n width: { type: [Number, String], default: '100%' },\n\n /**\n * Called before drag-drop: (sourceRange, targetRange) => bool\n * Return false to cancel the drop.\n */\n beforeDrop: { type: Function, default: null },\n\n /**\n * UI locale for the context menu.\n * Supported: 'en' | 'ru' | 'uz' | 'uz-Cyrl'\n */\n locale: { type: String, default: 'en' },\n\n /**\n * Pinned columns map: { [colKey]: 'left' | 'right' }\n * Pinned-left columns render as a fixed zone on the left.\n * Pinned-right columns render as a fixed zone on the right.\n * Emit 'update:pinnedColumns' to support .sync modifier.\n */\n pinnedColumns: { type: Object, default: () => ({}) },\n\n /**\n * Dynamic cell editability: (row, rowIndex, col, colIndex) => boolean\n * Overrides column-level editable when provided.\n * row = plain data object, col = column definition.\n */\n cellEditable: { type: Function, default: null },\n\n /**\n * Cell value validation: (value, row, rowIndex, col, colIndex) => boolean\n * Return false to reject the write — the cell keeps its old value and\n * an error tooltip is shown anchored to that cell.\n */\n cellValidator: { type: Function, default: null },\n\n /**\n * Error tooltip text for a rejected value.\n * Function: (value, row, rowIndex, col, colIndex) => string\n * String: same message for every rejected cell.\n * Default: locale's \"invalidValue\" message.\n */\n cellValidationMessage: { type: [Function, String], default: null },\n\n /**\n * Bulk-write strictness (paste / fill handle / Delete).\n * false (default): invalid cells in the affected range are skipped,\n * the rest of the operation still applies.\n * true: if ANY cell in the affected range fails validation, the whole\n * bulk operation is rejected — nothing is written.\n */\n strictBulkValidation: { type: Boolean, default: false },\n\n /** Visual size of the grid. Affects row/header height and font sizes. */\n size: { type: String, default: 'middle' },\n\n /** Auto-fit all column widths to content (same as double-clicking each resize handle). Re-applies on data/columns change. */\n autoWidth: { type: Boolean, default: false },\n\n /**\n * Allow columns to be reordered by dragging their headers.\n * On drop, emits 'update:columns' (compatible with .sync) and 'column-reorder'.\n */\n columnDraggable: { type: Boolean, default: true },\n\n /**\n * Controls the built-in toolbar above the grid — visibility as a whole,\n * and (optionally) which individual tool groups appear. Controlled\n * entirely by the parent — no in-component toggle for the bar itself.\n *\n * @type {Boolean|{visible?: Boolean, list?: Array<('undoRedo'|'export'|'import'|'size'|'lock'|'fullscreen'|'theme')>}}\n * @default false\n *\n * Two forms:\n * - `Boolean` — `false` (default) hides the toolbar entirely; `true`\n * shows it with the default tool list (every group except `'theme'`,\n * since that one needs `:theme.sync` on the parent to be useful).\n * - `Object { visible, list }` — fine-grained control:\n * - `visible` {Boolean} — defaults to `true` when an object is given\n * (passing an object is itself an opt-in to show the toolbar);\n * set explicitly to `false` to hide it while still describing a\n * `list` you intend to reuse later.\n * - `list` {Array<String>} — which tool groups render, using the\n * identifiers above. Unknown/misspelled strings are silently\n * ignored. Omitting `list` falls back to the same default set as\n * the `true` shorthand. **Order in `list` does not change the\n * toolbar's layout** — groups always render in a fixed position\n * (undo/redo → export/import → #toolbar-extra slot → toolbarButtons\n * → spacer → theme → size → lock/fullscreen); `list` is purely an\n * inclusion filter.\n *\n * `toolbarButtons` and the `#toolbar-extra` slot are independent of\n * this prop — they render whenever content is provided, regardless of\n * `list`.\n *\n * @example\n * // Whole default set\n * <excel-grid toolbar />\n * @example\n * // Only undo/redo and fullscreen\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'fullscreen'] }\" />\n * @example\n * // Toggle visibility from a data property while keeping a fixed list\n * <excel-grid :toolbar=\"{ visible: showToolbar, list: ['export', 'import'] }\" />\n * @example\n * // Opt into the theme picker (needs :theme.sync)\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen', 'theme'] }\" :theme.sync=\"myTheme\" />\n */\n toolbar: { type: [Boolean, Object], default: false },\n\n /** Extra custom toolbar buttons: [{ key, icon, title, onClick(event, gridApi) }] */\n toolbarButtons: { type: Array, default: () => [] },\n\n /**\n * Locks the entire grid: blocks all edits (typing, paste, fill, delete,\n * checkbox, select/treeSelect) and structural changes (resize, column\n * reorder, merge/unmerge, undo/redo). Independent from cellEditable/\n * col.editable, which remain per-cell rules. Emit 'update:locked' to\n * support the .sync modifier.\n */\n locked: { type: Boolean, default: false },\n\n /** Show \"Column info\" / \"Row info\" items in the context menu (dev/debug aid). */\n devMode: { type: Boolean, default: false },\n\n /**\n * Visual theme of the grid.\n * Built-in: 'excel' | 'material' | 'antd' | 'fluent' | 'carbon'\n * Default is the classic Excel 365 look. Use 'material' for Material Design 3.\n * Mirrors the `size` prop's convention: an internal `internalTheme` state\n * mirrors this prop and is what the toolbar's theme picker actually reads\n * from/writes to (via `setTheme()`), so the picker works standalone even\n * without `:theme.sync` — `.sync` is only needed to reflect the current\n * theme back into the host's own state.\n */\n theme: { type: String, default: 'excel' },\n\n /**\n * Shows a filter row directly beneath the column headers (inside the\n * sticky header area). Each leaf column gets a filter control matched to\n * its `type`:\n * - default (no type / 'date' / 'progress'): text search input,\n * matched case-insensitively against the column's *displayed* value\n * (so 'date' matches the formatted string, not the raw stored value).\n * - 'select' / 'treeSelect' *with a static `col.options` array*: a\n * native <select> populated from those options (tree options are\n * flattened, indented by depth), matched by exact raw value.\n * - 'select' / 'treeSelect' with function-based `col.options` (options\n * computed per-row): falls back to the text control, since a single\n * fixed option list can't be resolved outside a specific row.\n * - 'checkbox': a tri-state <select> — All / Checked / Unchecked.\n * - 'action': no filter control (not a data-bound column).\n *\n * Rows that don't match every active filter are hidden using the same\n * mechanism as `hiddenRows` — virtual scroll, keyboard navigation, and\n * `exportToExcel()` all skip them automatically, with no separate\n * filtering code path to keep in sync.\n *\n * Set `col.filterable: false` to omit the filter control for one column\n * (its filter cell renders empty). Set\n * `col.filterFn(cellValue, filterValue, row, rowIndex, column)` to\n * override the default match logic for that column entirely.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row />\n */\n filterRow: { type: Boolean, default: false },\n\n /**\n * Current filter values, keyed by column `key`. Optional — the filter\n * row works standalone via internal state (`internalFilters`) even\n * without `.sync`, mirroring the `size`/`theme` prop convention. Bind\n * `:filters.sync` only if the host needs to read or restore the current\n * filter state.\n * @type {Object}\n * @default {}\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row :filters.sync=\"myFilters\" />\n */\n filters: { type: Object, default: () => ({}) },\n\n /**\n * Shows a sort icon in every leaf column header. Clicking it cycles\n * none → ascending → descending → none for that column (clicking a\n * different column's icon replaces the active sort — single-column\n * sort only).\n *\n * Sorting **physically reorders** the `data` array (like a real\n * spreadsheet sort) and emits it through the normal v-model/`input`\n * event — it is recorded in the undo/redo history exactly like any\n * other data edit, so `undo()` restores the pre-sort row order.\n * Clicking a third time (back to \"none\") only clears the sort\n * indicator — it does **not** restore the previous order by itself;\n * use `undo()` for that.\n *\n * Comparison is matched to the column's `type`: plain/`'progress'`\n * columns compare the raw value (numeric-aware); `'date'` compares the\n * parsed date chronologically, not the formatted string; `'select'` /\n * `'treeSelect'` compare the displayed option label; `'checkbox'`\n * compares the boolean state. Empty/`null` values always sort last,\n * regardless of direction. `'action'` columns are never sortable.\n *\n * Set `col.sortable: false` to omit the icon for one column. Set\n * `col.sortFn(rowA, rowB, column) => number` to fully override the\n * comparison for that column.\n *\n * Blocked (no-op) while the grid is `locked`, or while any merged\n * cells or row outline groups exist — both are anchored to row\n * *position*, which a reorder would silently invalidate.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable />\n */\n sortable: { type: Boolean, default: false },\n\n /**\n * Current sort state — `{ key, direction }` (`direction` is `'asc'` or\n * `'desc'`) or `null` when unsorted. Optional — sorting works standalone\n * via internal state (`internalSort`) even without `.sync`, mirroring\n * the `size`/`theme`/`filters` prop convention. Bind `:sort.sync` only\n * if the host needs to read or restore the current sort state.\n * @type {Object}\n * @default null\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable :sort.sync=\"mySort\" />\n */\n sort: { type: Object, default: null }\n },\n\n data() {\n return {\n // Selection\n activeCell: { row: 0, col: 0 },\n selectionRanges: [], // [{ startRow, startCol, endRow, endCol }]\n isMouseDown: false,\n mouseDownRange: null,\n isCtrlDrag: false,\n\n // Edit\n editingCell: null, // { row, col }\n editValue: '',\n editOriginalValue: '',\n\n // Scroll\n scrollTop: 0,\n scrollLeft: 0,\n headerViewportWidth: null,\n\n // Auto-height (height prop omitted) — 70vh cap, kept in sync on resize\n windowInnerH: typeof window !== 'undefined' ? window.innerHeight : 800,\n\n // Virtual scroll\n virtualStartIndex: 0,\n virtualEndIndex: 0,\n virtualOffsetTop: 0,\n totalVirtualHeight: 0,\n\n // Column widths (flat index → px)\n colWidths: {},\n\n // Row heights (row index → px)\n rowHeights: {},\n\n // Import/export progress\n importState: { active: false, percent: 0, message: '' },\n exportState: { active: false },\n\n resizing: {\n active: false,\n type: null, // 'col' | 'row'\n index: -1,\n startX: 0,\n startY: 0,\n startSize: 0,\n currentPos: 0\n },\n\n // Fill handle\n fillDrag: {\n active: false,\n sourceRange: null,\n currentRow: -1,\n currentCol: -1\n },\n\n // Drag-move\n dragMove: {\n active: false,\n sourceRange: null,\n startX: 0,\n startY: 0,\n targetRow: -1,\n targetCol: -1,\n copyMode: false\n },\n\n // Clipboard (internal)\n clipboardMatrix: null,\n clipboardRange: null,\n clipboardMode: null, // 'copy' | 'cut'\n\n // Context menu\n contextMenu: { visible: false, x: 0, y: 0, row: -1, col: -1 },\n // false while the menu's real size hasn't been measured yet — kept hidden\n // (not painted) so the flip-to-fit repositioning in _positionContextMenu()\n // doesn't cause a visible jump.\n contextMenuPositioned: false,\n\n // devMode — column/row config info modal ({ title, json } | null)\n infoModal: null,\n\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, rect }\n\n // Date picker overlay\n datePickerOverlay: null, // { row, col, format, value, rect }\n\n // Cell value validation (cellValidator)\n invalidCell: null, // { row, col, message, rect }\n\n // Merges (controlled via prop + update:merges)\n internalMerges: [],\n\n // Toolbar (controlled via prop + update:locked / update:size / update:theme)\n internalLocked: false,\n internalSize: 'middle',\n internalTheme: 'excel',\n // Brief centered icon feedback whenever the lock state actually changes\n // (toolbar click, toggleLock(), or a host-driven :locked.sync change).\n lockFlashVisible: false,\n lockFlashLocked: false,\n isFullscreen: false,\n // Bumped whenever _history/_historyIndex change so canUndo/canRedo stay reactive.\n historyVersion: 0,\n\n // Groups\n internalGroups: [], // [{ type, startIndex, endIndex }]\n internalCollapsedGroups: [],\n\n // Header\n headerDepth: 1,\n headerRows: [],\n\n // Leaf columns cache\n leafCols: [],\n\n // Footer column totals (flat col index → number | null)\n columnTotals: {},\n\n // Filter row (controlled via prop + update:filters)\n internalFilters: {},\n // Row indices currently hidden by an active filter — merged into\n // hiddenRowSet, so virtual scroll/export/navigation need no separate path.\n filteredRowIndices: [],\n\n // Sort (controlled via prop + update:sort) — { key, direction } | null\n internalSort: null,\n\n // Column drag-and-drop state\n colDrag: {\n pending: false, // mousedown received, waiting for movement threshold\n active: false, // drag in progress\n sourceFirstLeaf: -1, // first leaf flat-index of the dragged node\n sourceLeafCount: 1, // number of leaves in the dragged node (1=leaf, N=group)\n dropIndex: -1, // insert-before flat index (0..N)\n startX: 0,\n startY: 0,\n ghostLabel: ''\n },\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n pinnedLeftSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'left') s.add(col._flatIndex) })\n return s\n },\n\n pinnedRightSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'right') s.add(col._flatIndex) })\n return s\n },\n\n pinnedLeftCols() { return this.leafCols.filter(col => this.pinnedLeftSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n\n hasPinnedLeft() { return this.pinnedLeftSet.size > 0 },\n hasPinnedRight() { return this.pinnedRightSet.size > 0 },\n\n hasFooter() { return this.leafCols.some(col => this.isColumnTotalVisible(col)) },\n\n pinnedLeftWidth() {\n let w = 0\n for (const col of this.pinnedLeftCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n pinnedRightWidth() {\n let w = 0\n for (const col of this.pinnedRightCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n totalRows() { return this.data.length },\n\n // See the `toolbar` prop's JSDoc for the full Boolean|Object contract.\n _toolbarVisible() {\n if (typeof this.toolbar === 'boolean') return this.toolbar\n return !!this.toolbar && this.toolbar.visible !== false\n },\n _toolbarList() {\n if (this.toolbar && Array.isArray(this.toolbar.list)) return this.toolbar.list\n return DEFAULT_TOOLBAR_LIST\n },\n\n _sizeConf() { return SIZE_CONFIG[this.internalSize] || SIZE_CONFIG.middle },\n\n sizeOptions() { return Object.keys(SIZE_CONFIG) },\n _rowH() { return this._sizeConf.row },\n _headerRowH() { return this._sizeConf.header },\n _footerRowH() { return this._sizeConf.footer },\n _filterRowH() { return this.filterRow ? this._sizeConf.filter : 0 },\n\n headerRowHeight() { return this._headerRowH },\n footerRowHeight() { return this._footerRowH },\n\n // Pre-built cumulative height array — O(n) once, then cached by Vue.\n // findVisibleRange() uses binary search against this cached array → O(log n) per scroll.\n // Invalidated only when row count, custom heights, or hidden rows change.\n cumulativeHeights() {\n return buildCumulativeHeights(this.totalRows, this._rowH, this.rowHeights, this.hiddenRowSet)\n },\n\n // height prop omitted → fit header + filter row + all (visible) rows + footer exactly.\n _autoContentHeight() {\n const headerH = this._headerRowH * this.headerDepth + this._filterRowH\n const bodyH = this.cumulativeHeights[this.totalRows] || 0\n const footerH = this.hasFooter ? this._footerRowH : 0\n return headerH + bodyH + footerH\n },\n\n // Resolved grid-area height: explicit `height` prop wins as-is (old\n // behavior); omitted → auto-fit content, capped at 70% of the viewport.\n effectiveHeight() {\n if (this.height != null) return this.height\n return Math.min(this._autoContentHeight, this.windowInnerH * 0.7)\n },\n\n hiddenColSet() { return new Set(this.hiddenColumns) },\n\n hiddenRowSet() {\n const s = new Set(this.hiddenRows)\n // Also add rows hidden by collapsed row groups\n const collapsed = getCollapsedRowIndices(this.internalGroups, this.internalCollapsedGroups)\n collapsed.forEach(i => s.add(i))\n // Also add rows hidden by the filter row (see filteredRowIndices/_recomputeRowFilters)\n this.filteredRowIndices.forEach(i => s.add(i))\n return s\n },\n\n hiddenColFromGroupSet() {\n return getCollapsedColIndices(this.internalGroups, this.internalCollapsedGroups)\n },\n\n effectiveHiddenColSet() {\n const s = new Set(this.hiddenColSet)\n this.hiddenColFromGroupSet.forEach(i => s.add(i))\n // col.visible === false — a static schema-level hide, independent of the\n // toggleable hiddenColumns list. \"Show All Columns\" never reveals it.\n this.leafCols.forEach(col => { if (col.visible === false) s.add(col._flatIndex) })\n return s\n },\n\n rowGroups() { return this.internalGroups.filter(g => g.type === 'row') },\n colGroups() { return this.internalGroups.filter(g => g.type === 'column') },\n\n visibleRowRange() {\n const rows = []\n for (let i = this.virtualStartIndex; i <= this.virtualEndIndex; i++) rows.push(i)\n return rows\n },\n\n primaryRange() {\n return getPrimaryRange(this.selectionRanges, this.activeCell)\n },\n\n fillHandleVisible() {\n return this.primaryRange && !this.editingCell\n },\n\n fillHandleStyle() {\n if (!this.primaryRange) return {}\n const r = normalizeRange(this.primaryRange)\n // O(1) lookup via cumulativeHeights instead of O(endRow) getRowTop loop\n const bottom = this.cumulativeHeights[r.endRow + 1] || 0\n const right = this.getColLeft(r.endCol + 1)\n return {\n position: 'absolute',\n width: '6px',\n height: '6px',\n background: 'var(--eg-color-accent)',\n border: '1px solid white',\n bottom: (this.totalVirtualHeight - bottom + 1) + 'px',\n right: (this.scrollContentWidth - right + 1) + 'px',\n cursor: 'crosshair',\n zIndex: 10\n }\n },\n\n\n invalidTooltipStyle() {\n if (!this.invalidCell || !this.invalidCell.rect) return {}\n const { rect } = this.invalidCell\n return {\n position: 'fixed',\n top: (rect.bottom + 4) + 'px',\n left: rect.left + 'px',\n zIndex: 9999\n }\n },\n\n dragGhostStyle() {\n if (!this.dragMove.active || this.dragMove.targetRow < 0) return {}\n const r = normalizeRange(this.dragMove.sourceRange)\n const rows = r.endRow - r.startRow\n const cols = r.endCol - r.startCol\n // O(1) lookup via cumulativeHeights instead of O(targetRow) getRowTop loop\n const top = this.cumulativeHeights[this.dragMove.targetRow] || 0\n const left = this.getColLeft(this.dragMove.targetCol)\n const width = this.getRangeWidth(this.dragMove.targetCol, this.dragMove.targetCol + cols)\n const height = this.getRangeHeight(this.dragMove.targetRow, this.dragMove.targetRow + rows)\n return {\n position: 'absolute',\n top: top + 'px',\n left: left + 'px',\n width: width + 'px',\n height: height + 'px',\n border: '2px dashed #1a73e8',\n pointerEvents: 'none',\n zIndex: 20\n }\n },\n\n wrapperStyle() {\n if (this.isFullscreen) {\n return {\n width: '100%',\n height: 'auto',\n flex: '1 1 auto',\n minHeight: 0,\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n }\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n height: this.effectiveHeight + 'px',\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n },\n\n shellStyle() {\n if (this.isFullscreen) return {}\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n display: 'flex',\n flexDirection: 'column'\n }\n },\n\n canUndo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex >= 0\n },\n\n canRedo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex < this._history.length - 1\n },\n\n headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n cornerStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: (this._headerRowH * this.headerDepth + this._filterRowH) + 'px',\n background: 'var(--eg-color-header-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderBottom: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0\n }\n },\n\n rowOutlineGutterStyle() {\n return { width: OUTLINE_GUTTER + 'px', flexShrink: 0 }\n },\n\n frozenColHeaderStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n scrollColHeaderStyle() {\n // Once measured, lock the header to the body's exact clientWidth (flex: 0 0 <px>)\n // instead of growing independently via flex:1 — this guarantees identical\n // scrollWidth-clientWidth (max scrollLeft) on both, so syncing scrollLeft never clamps\n // one before the other. (padding-right can't do this: clientWidth always includes\n // padding, so padding never actually shrinks it.)\n if (this.headerViewportWidth != null) {\n return { flex: '0 0 ' + this.headerViewportWidth + 'px', overflow: 'hidden' }\n }\n return { flex: 1, overflow: 'hidden' }\n },\n\n bodyAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flex: 1,\n position: 'relative'\n }\n },\n\n rowHeaderColStyle() {\n return {\n width: ROW_HEADER_WIDTH + 'px',\n minWidth: ROW_HEADER_WIDTH + 'px',\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n position: 'sticky',\n left: 0,\n zIndex: 20,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n rowOutlineColStyle() {\n return {\n width: OUTLINE_GUTTER + 'px',\n minWidth: OUTLINE_GUTTER + 'px',\n flexShrink: 0,\n position: 'relative',\n background: 'var(--eg-color-cell-bg)'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative',\n overflowAnchor: 'none' /* prevent Chrome scroll anchoring from fighting virtual scroll translateY */\n }\n },\n\n frozenColBodyStyle() {\n const w = this.getFrozenWidth()\n return {\n position: 'sticky',\n left: 0,\n width: w + 'px',\n zIndex: 10,\n background: 'white',\n overflow: 'hidden'\n }\n },\n\n mainCellsStyle() {\n const left = this.getFrozenWidth()\n return {\n position: 'absolute',\n left: left + 'px',\n top: 0,\n bottom: 0\n }\n },\n\n scrollContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && !this.pinnedRightSet.has(i)) w += this.getColW(i)\n }\n // Extra right padding so pinned-right zone doesn't cover the last scrollable column\n if (this.hasPinnedRight) w += this.pinnedRightWidth\n return w\n },\n\n scrollableContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && this.isColumnScrollable(i)) w += this.getColW(i)\n }\n return w\n },\n\n pinnedRightBodyStyle() {\n return {\n position: 'sticky',\n right: 0,\n width: this.pinnedRightWidth + 'px',\n zIndex: 10,\n background: 'white',\n flexShrink: 0\n }\n },\n\n pinnedRightHeaderStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative', overflowAnchor: 'none' }\n },\n\n footerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flexShrink: 0,\n position: 'sticky',\n bottom: 0,\n zIndex: 30,\n background: 'var(--eg-color-footer-bg)',\n boxShadow: '0 -2px 0 0 var(--eg-color-border-strong)'\n }\n },\n\n footerLabelStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: this._footerRowH + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--eg-color-footer-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderTop: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: 'var(--eg-font-size-header)',\n fontWeight: 600\n }\n },\n\n frozenColFooterStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n pinnedRightFooterStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n colOutlineRowStyle() {\n return {\n position: 'absolute',\n top: -OUTLINE_GUTTER + 'px',\n left: 0,\n right: 0,\n height: OUTLINE_GUTTER + 'px'\n }\n },\n\n resizeLineStyle() {\n if (!this.resizing.active) return {}\n if (this.resizing.type === 'col') {\n return {\n position: 'absolute',\n top: 0,\n left: (this.resizing.currentPos - (this.resizing.containerLeft || 0)) + 'px',\n width: '2px',\n height: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'absolute',\n left: 0,\n top: (this.resizing.currentPos - (this.resizing.containerTop || 0)) + 'px',\n height: '2px',\n width: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size: {\n immediate: true,\n handler(v) { this.internalSize = v || 'middle' }\n },\n\n theme: {\n immediate: true,\n handler(v) { this.internalTheme = v || 'excel' }\n },\n\n // Runs before the `columns`/`data` watchers below (both call\n // _recomputeRowFilters()) so internalFilters already reflects the prop\n // by the time either of them fires during the initial immediate pass.\n filters: {\n immediate: true,\n handler(v) { this.internalFilters = v ? { ...v } : {} }\n },\n\n internalFilters() { this._scheduleFilterRecompute() },\n\n filterRow() {\n this._recomputeRowFilters()\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n sort: {\n immediate: true,\n handler(v) { this.internalSort = v ? { key: v.key, direction: v.direction } : null }\n },\n\n internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n // Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:\n // true` prop doesn't flash the icon before the grid has even appeared.\n internalLocked(v) {\n if (!this._lockFlashArmed) return\n this._flashLock(v)\n },\n\n autoWidth(v) {\n if (v) this._applyAutoWidth()\n },\n\n columns: {\n immediate: true,\n handler(cols) {\n // Build key→width map from current state before rebuilding leafCols.\n // This preserves user-resized widths across column reorders: when columns\n // are moved, _flatIndex values shift but col.key stays the same.\n const keyToWidth = {}\n ;(this.leafCols || []).forEach(col => {\n if (this.colWidths[col._flatIndex] !== undefined) {\n keyToWidth[col.key] = this.colWidths[col._flatIndex]\n }\n })\n\n this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n\n // Rebuild colWidths by new flat index, using col.key to look up saved widths\n const newColWidths = {}\n this.leafCols.forEach(col => {\n newColWidths[col._flatIndex] = keyToWidth[col.key] !== undefined\n ? keyToWidth[col.key]\n : (col.width || DEFAULT_COL_WIDTH)\n })\n this.colWidths = newColWidths\n\n this.computeColumnTotals()\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this._applyAutoWidth()\n }\n },\n merges: {\n immediate: true,\n handler(v) { this.internalMerges = v ? [...v] : [] }\n },\n collapsedGroups: {\n immediate: true,\n handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }\n },\n locked: {\n immediate: true,\n handler(v) { this.internalLocked = !!v }\n },\n data: {\n immediate: true,\n handler(newData) {\n // Keep _dataRaw in sync so template uses fresh data without triggering dependArray\n this._dataRaw = newData || []\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this.computeColumnTotals()\n // Force re-render so cell values refresh even when virtualScroll indices don't change\n // (e.g. editing a cell: same row count, same scroll position, only content changed)\n this.$forceUpdate()\n this._applyAutoWidth()\n }\n },\n totalRows() { this.updateVirtualScroll() },\n hiddenRowSet() { this.updateVirtualScroll() },\n totalVirtualHeight() { this.$nextTick(this.syncHeaderViewportWidth) }\n },\n\n created() {\n // Non-reactive row data reference — avoids Vue 2 dependArray(data) O(n) loop.\n // Vue 2 calls dependArray() for EVERY `data[ri]` access in the template,\n // iterating all 50,000 rows each time. With 1200 cells per render that's\n // 60 million iterations per render (~600ms). _dataRaw bypasses this.\n this._dataRaw = this.data || []\n\n // Non-reactive caches: same (w,h) → same style object → Vue 2 skips DOM style writes\n this._cellStyleCache = Object.create(null)\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n // rAF handles for scroll/resize throttle\n this._scrollRAF = null\n this._resizeRAF = null\n\n // Undo/redo history — non-reactive to avoid Vue overhead.\n // Each entry: { type: 'data'|'merges', before: {...}, after: {...} }\n // 'data' stores only diff rows: { indices: number[], rows: object[] }\n this._history = []\n this._historyIndex = -1\n this._historySkip = false // true while undo/redo is in progress\n\n // Column drag preview offsets — non-reactive so direct DOM updates bypass Vue re-renders.\n // Keys are column keys, values are pixel offsets.\n this._dragOffsets = {}\n\n },\n\n mounted() {\n this.updateVirtualScroll()\n this.$nextTick(this.syncHeaderViewportWidth)\n document.addEventListener('mousemove', this.onGlobalMouseMove)\n document.addEventListener('mouseup', this.onGlobalMouseUp)\n document.addEventListener('keydown', this.onGlobalKeyDown)\n document.addEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.addEventListener('resize', this.syncHeaderViewportWidth)\n window.addEventListener('scroll', this._onWindowScrollForDropdown, true)\n this._lockFlashArmed = true\n },\n\n updated() {\n // After Vue re-renders (e.g., virtual scroll adds new rows during drag),\n // apply missing transforms to newly rendered cells that don't have one yet.\n if (!this.colDrag.active || !this.$el) return\n const offsets = this._dragOffsets\n if (!offsets || !Object.keys(offsets).length) return\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n if (el.style.transform) return\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n if (dx) el.style.transform = `translateX(${dx}px)`\n })\n },\n\n beforeDestroy() {\n clearTimeout(this._filterRecomputeTimer)\n document.removeEventListener('mousemove', this.onGlobalMouseMove)\n document.removeEventListener('mouseup', this.onGlobalMouseUp)\n document.removeEventListener('keydown', this.onGlobalKeyDown)\n document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.removeEventListener('resize', this.syncHeaderViewportWidth)\n window.removeEventListener('scroll', this._onWindowScrollForDropdown, true)\n if (this.isFullscreen) {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n },\n\n methods: {\n // =========================================================\n // VIRTUAL SCROLL\n // =========================================================\n // In fullscreen, .eg-scroll-body's real height comes from flexbox filling\n // the viewport (which can be far taller than the `height` prop the host\n // originally passed for the fixed-size layout) — measure it directly\n // instead of trusting `height`, otherwise virtual scroll only ever\n // renders enough rows for the old fixed size, leaving the rest of the\n // (visually stretched) container blank.\n _containerH() {\n if (this.isFullscreen && this.$refs.scrollBody) return this.$refs.scrollBody.clientHeight\n const footerH = this.hasFooter ? this._footerRowH : 0\n return this.effectiveHeight - (this._headerRowH * this.headerDepth) - this._filterRowH - footerH\n },\n\n // Same fullscreen-aware measurement as _containerH(), but mirrors PageUp/\n // PageDown's pre-existing (footer-agnostic) row-span formula exactly for\n // the non-fullscreen case, so normal-mode behavior is unchanged.\n _pageRowSpan() {\n const h = this.isFullscreen && this.$refs.scrollBody\n ? this.$refs.scrollBody.clientHeight\n : (this.effectiveHeight - this._headerRowH * this.headerDepth - this._filterRowH)\n return Math.floor(h / this._rowH)\n },\n\n updateVirtualScroll() {\n // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll.\n // overscan=8 (≈224px at default row height) gives enough buffer for fast scroll\n // without blank-band artifacts between rAF updates.\n const result = findVisibleRange(\n this.scrollTop, Math.max(this._containerH(), 100), this.totalRows, this.cumulativeHeights, 8\n )\n this.virtualStartIndex = result.startIndex\n this.virtualEndIndex = result.endIndex\n this.virtualOffsetTop = result.offsetTop\n this.totalVirtualHeight = result.totalHeight\n },\n\n onBodyScroll(e) {\n const target = e.target\n // Sync sibling scroll containers immediately (visual alignment, no layout trigger)\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = target.scrollLeft\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = target.scrollLeft\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = target.scrollTop\n\n // Close dropdown on scroll — fixed-position dropdown drifts away from the moving cell\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame.\n // Read scrollTop from the DOM ref at rAF time (not from the stale event closure)\n // so that multiple scroll events arriving between frames still resolve to the\n // final position — eliminates the flickering caused by render/scroll mismatch.\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n const sb = this.$refs.scrollBody\n if (!sb) return\n this.scrollTop = sb.scrollTop\n this.scrollLeft = sb.scrollLeft\n this.updateVirtualScroll()\n })\n }\n },\n\n onScroll(e) {},\n\n // .eg-scroll-col-header (overflow:hidden, synced via JS) has no native scrollbar, but\n // .eg-scroll-body (overflow:auto) does — its real vertical scrollbar eats into its\n // clientWidth. If both containers just grow via flex:1, the header ends up wider than\n // the body, so its max scrollLeft is smaller and syncing clamps it before the body\n // reaches its rightmost scroll position — column borders desync right at the scroll\n // end. Locking the header to the body's exact measured clientWidth (scrollColHeaderStyle)\n // makes both max scrollLeft values identical.\n syncHeaderViewportWidth() {\n // Auto-height's 70vh cap tracks the real viewport — keep it current on resize.\n if (typeof window !== 'undefined' && this.windowInnerH !== window.innerHeight) {\n this.windowInnerH = window.innerHeight\n }\n const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\n // Window resize while fullscreen changes the real available row-viewport\n // height too (see _containerH()) — recompute which rows are rendered.\n // Same for auto-height (height prop omitted): the 70vh cap just moved.\n if (this.isFullscreen || this.height == null) this.updateVirtualScroll()\n },\n\n // =========================================================\n // HELPERS\n // =========================================================\n isRowHidden(ri) { return this.hiddenRowSet.has(ri) },\n isColHidden(ci) { return this.effectiveHiddenColSet.has(ci) },\n\n getColW(ci) {\n return this.colWidths[ci] !== undefined ? this.colWidths[ci] : (this.leafCols[ci] ? this.leafCols[ci].width || DEFAULT_COL_WIDTH : DEFAULT_COL_WIDTH)\n },\n\n getRowH(ri) {\n if (this.isRowHidden(ri)) return 0\n return this.rowHeights[ri] || this._rowH\n },\n\n getColLeft(ci) {\n let left = 0\n for (let i = 0; i < ci && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) left += this.getColW(i)\n }\n return left\n },\n\n getFrozenWidth() {\n return this.hasPinnedLeft ? this.pinnedLeftWidth : 0\n },\n\n isColumnPinnedLeft(ci) {\n return this.pinnedLeftSet.has(ci)\n },\n\n isColumnPinnedRight(ci) {\n return this.pinnedRightSet.has(ci)\n },\n\n isColumnScrollable(ci) {\n return !this.isColumnPinnedLeft(ci) && !this.isColumnPinnedRight(ci)\n },\n\n getZoneColLeft(ci, zone) {\n let left = 0\n if (zone === 'left') {\n for (const col of this.pinnedLeftCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else if (zone === 'right') {\n for (const col of this.pinnedRightCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else {\n for (let i = 0; i < this.leafCols.length; i++) {\n const col = this.leafCols[i]\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex) && this.isColumnScrollable(col._flatIndex)) {\n left += this.getColW(col._flatIndex)\n }\n }\n }\n return left\n },\n\n isCellInZone(cell, rowIdx, zone) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n const z = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n return z === zone\n },\n\n getHeaderCellStyleForZone(cell, rowIndex, zone) {\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z !== zone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getRangeWidth(startCol, endCol) {\n let w = 0\n for (let i = startCol; i <= endCol && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) w += this.getColW(i)\n }\n return w\n },\n\n getRangeHeight(startRow, endRow) {\n let h = 0\n for (let i = startRow; i <= endRow && i < this.totalRows; i++) {\n if (!this.isRowHidden(i)) h += this.getRowH(i)\n }\n return h\n },\n\n getCellValue(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return ''\n const row = this._dataRaw[ri]\n if (!row) return ''\n const v = row[col.key]\n return v !== undefined ? v : ''\n },\n\n isEditing(ri, ci) {\n return this.editingCell && this.editingCell.row === ri && this.editingCell.col === ci\n },\n\n isMergedAway(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return false\n const nm = normalizeRange(merge)\n return !(nm.startRow === ri && nm.startCol === ci)\n },\n\n getMergeSpan(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return { rowspan: 1, colspan: 1 }\n const nm = normalizeRange(merge)\n return {\n rowspan: nm.endRow - nm.startRow + 1,\n colspan: nm.endCol - nm.startCol + 1\n }\n },\n\n isSelected(ri, ci) {\n return isCellInAnyRange(ri, ci, this.selectionRanges)\n },\n\n isActiveCell(ri, ci) {\n return this.activeCell.row === ri && this.activeCell.col === ci\n },\n\n isCopyHighlighted(ri, ci) {\n if (!this.clipboardRange || this.clipboardMode !== 'copy') return false\n return isCellInRange(ri, ci, this.clipboardRange)\n },\n\n // =========================================================\n // STYLES\n // =========================================================\n getRowStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowStyleCache[key]) return this._rowStyleCache[key]\n const style = { height: h + 'px', minHeight: h + 'px' }\n this._rowStyleCache[key] = style\n return style\n },\n\n getRowHeaderStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowHeaderStyleCache[key]) return this._rowHeaderStyleCache[key]\n const style = { height: h + 'px' }\n this._rowHeaderStyleCache[key] = style\n return style\n },\n\n getCellStyle(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n const nm = merge ? normalizeRange(merge) : null\n let w = this.getColW(ci)\n let h = this.getRowH(ri)\n\n if (nm && nm.startRow === ri && nm.startCol === ci) {\n w = this.getRangeWidth(nm.startCol, nm.endCol)\n h = this.getRangeHeight(nm.startRow, nm.endRow)\n }\n\n // Position cells with the same absolute-left arithmetic the header uses\n // (getZoneColLeft), instead of relying on flex layout to accumulate widths —\n // flex and a JS integer sum can drift apart by a few px over many columns,\n // desyncing the body's column borders from the header's.\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n\n // Cache by dimensions+position — same (w,h,left) → same object ref → Vue 2 skips DOM style writes\n const key = w + '_' + h + '_' + left\n if (this._cellStyleCache[key]) return this._cellStyleCache[key]\n const style = { position: 'absolute', left: left + 'px', top: '0px', width: w + 'px', minWidth: w + 'px', height: h + 'px' }\n this._cellStyleCache[key] = style\n return style\n },\n\n getCellClasses(ri, ci) {\n const col = this.leafCols[ci]\n return {\n 'eg-cell-active': this.isActiveCell(ri, ci),\n 'eg-cell-selected': this.isSelected(ri, ci) && !this.isActiveCell(ri, ci),\n 'eg-cell-fill-preview': this.isInFillPreview(ri, ci),\n 'eg-cell-copy': this.isCopyHighlighted(ri, ci),\n 'eg-cell-merged-hidden': this.isMergedAway(ri, ci),\n 'eg-cell-select': !!(col && this.isSelectLikeCol(col)),\n 'eg-cell-date': !!(col && col.type === 'date'),\n 'eg-cell-checkbox': !!(col && col.type === 'checkbox'),\n 'eg-cell-action': !!(col && col.type === 'action'),\n 'eg-cell-readonly': !this.isCellEditable(ri, ci),\n 'eg-cell-invalid': !!(this.invalidCell && this.invalidCell.row === ri && this.invalidCell.col === ci),\n 'eg-cell--has-suffix': !!(col && this.hasSuffixSlot(col)),\n 'eg-cell--drag-source': this.colDrag.active && ci >= this.colDrag.sourceFirstLeaf && ci < this.colDrag.sourceFirstLeaf + this.colDrag.sourceLeafCount,\n 'eg-cell-align-right': !!(col && col.align === 'right'),\n 'eg-cell-align-center': !!(col && col.align === 'center')\n }\n },\n\n isInFillPreview(ri, ci) {\n if (!this.fillDrag.active || !this.fillDrag.sourceRange) return false\n const src = this.fillDrag.sourceRange\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n if (targetRow > src.endRow) {\n return ri > src.endRow && ri <= targetRow && ci >= src.startCol && ci <= src.endCol\n }\n if (targetCol > src.endCol) {\n return ci > src.endCol && ci <= targetCol && ri >= src.startRow && ri <= src.endRow\n }\n return false\n },\n\n getHeaderCellStyle(cell, rowIndex, isFrozen) {\n const zone = isFrozen ? 'left' : 'scroll'\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n if (isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (!inZone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getHeaderDragTransform(cell) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking, no re-render on offset change\n let key\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n } else {\n key = cell.column.key\n }\n if (!key) return null\n const dx = this._dragOffsets[key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n getCellDragTransform(ci) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking\n const col = this.leafCols[ci]\n if (!col) return null\n const dx = this._dragOffsets[col.key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n\n // col.totalConfig.visible (boolean) takes priority over col.showTotal when present.\n isColumnTotalVisible(col) {\n if (col.totalConfig && typeof col.totalConfig.visible === 'boolean') return col.totalConfig.visible\n return !!col.showTotal\n },\n\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!this.isColumnTotalVisible(col)) return\n let sum = 0\n let hasNumber = false\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n const v = row ? row[col.key] : undefined\n if (typeof v === 'number' && isFinite(v)) {\n sum += v\n hasNumber = true\n }\n }\n totals[col._flatIndex] = hasNumber ? Math.round(sum * 1e6) / 1e6 : null\n })\n this.columnTotals = totals\n },\n\n getColumnTotalDisplay(ci) {\n const col = this.leafCols[ci]\n if (!col || !this.isColumnTotalVisible(col)) return ''\n const raw = this.columnTotals[ci]\n const cfg = col.totalConfig\n if (cfg && typeof cfg.formatter === 'function') {\n return cfg.formatter(raw, this._dataRaw, col, ci)\n }\n return raw === null || raw === undefined ? '-' : raw\n },\n\n getFooterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n const style = {\n position: 'absolute',\n left: left + 'px',\n top: '0px',\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px'\n }\n const col = this.leafCols[ci]\n const extra = col && col.totalConfig && col.totalConfig.style\n return extra ? { ...style, ...extra } : style\n },\n\n getFooterCellClass(ci) {\n const col = this.leafCols[ci]\n const classList = col && col.totalConfig && col.totalConfig.classList\n return classList || null\n },\n\n isCellVisible(cell, rowIndex, isFrozen) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (inZone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n return isFrozen ? this.isColumnPinnedLeft(ci) : this.isColumnScrollable(ci)\n },\n\n getRowGroupBtnStyle(group) {\n const top = getRowTop(group.startIndex, this._rowH, this.rowHeights) - this.scrollTop\n return {\n position: 'absolute',\n top: top + 'px',\n left: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px',\n zIndex: 5\n }\n },\n\n getColGroupBtnStyle(group) {\n const left = this.getColLeft(group.startIndex) - this.scrollLeft\n return {\n position: 'absolute',\n left: left + 'px',\n top: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px'\n }\n },\n\n // =========================================================\n // CELL SELECTION (3.1)\n // =========================================================\n selectCell(row, col, addToSelection = false) {\n if (row < 0 || row >= this.totalRows || col < 0 || col >= this.leafCols.length) return\n this.activeCell = { row, col }\n if (addToSelection) {\n this.selectionRanges.push(singleCellRange(row, col))\n } else {\n this.selectionRanges = [singleCellRange(row, col)]\n }\n this.emitSelectionChange()\n },\n\n selectRange(startRow, startCol, endRow, endCol, replace = true) {\n const range = { startRow, startCol, endRow, endCol }\n if (replace) {\n this.selectionRanges = [range]\n } else {\n this.selectionRanges.push(range)\n }\n this.emitSelectionChange()\n },\n\n selectRow(ri) {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [{ startRow: ri, endRow: ri, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n selectColumn(ci) {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n this.emitSelectionChange()\n },\n\n selectAll() {\n this.activeCell = { row: 0, col: 0 }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n emitSelectionChange() {\n this.$emit('selection-change', {\n activeCell: { ...this.activeCell },\n ranges: this.selectionRanges.map(r => normalizeRange(r))\n })\n },\n\n onCellMouseDown(e, ri, ci) {\n this.invalidCell = null\n if (this.editingCell) {\n const editCol = this.leafCols[this.editingCell.col]\n if (editCol && this._hasCustomEdit(editCol)) {\n this.cancelEdit()\n } else {\n this.commitEdit()\n }\n }\n this.hideContextMenu()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n\n if (e.ctrlKey || e.metaKey) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Ctrl+click: start new independent range\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges.push(this.mouseDownRange)\n this.isCtrlDrag = true\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Shift+click: extend primary range to this cell\n this.selectionRanges = [buildRange(this.activeCell, { row: ri, col: ci })]\n } else {\n const wasActive = this.activeCell.row === ri && this.activeCell.col === ci\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges = [this.mouseDownRange]\n this.isCtrlDrag = false\n\n // Open select/date dropdown on click if cell was already active (2nd click)\n const col = this.leafCols[ci]\n if (wasActive && col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openSelectDropdown(ri, ci, e)\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (wasActive && col && col.type === 'date' && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openDatePickerForActive()\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n }\n\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onCellMouseOver(e, ri, ci) {\n if (!this.isMouseDown) return\n if (this.fillDrag.active) return\n if (this.dragMove.active) {\n this.dragMove.targetRow = ri\n this.dragMove.targetCol = ci\n return\n }\n\n // Extend current drag range\n if (this.mouseDownRange) {\n this.mouseDownRange.endRow = ri\n this.mouseDownRange.endCol = ci\n this.emitSelectionChange()\n }\n },\n\n onCellMouseUp() {\n this.isMouseDown = false\n },\n\n onCellDblClick(ri, ci) {\n const col = this.leafCols[ci]\n if (!this.isCellEditable(ri, ci)) return\n if (col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col)) {\n this.openSelectDropdownForActive()\n return\n }\n if (col && col.type === 'date' && !this._hasCustomEdit(col)) {\n this.openDatePickerForActive()\n return\n }\n if (col && col.type === 'checkbox') return\n if (col && col.type === 'action') return\n this.startEditing(ri, ci, null, true)\n },\n\n onRowHeaderMouseDown(e, ri) {\n const endCol = this.leafCols.length - 1\n const fullRow = { startRow: ri, endRow: ri, startCol: 0, endCol }\n\n if (e.ctrlKey || e.metaKey) {\n if (e.shiftKey && this.selectionRanges.length > 0) {\n // Ctrl+Shift: active qatordan ri gacha yangi range qo'sh\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [...this.selectionRanges, { startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n // Ctrl+click: qatorni toggle qil\n const idx = this.selectionRanges.findIndex(r =>\n r.startRow === ri && r.endRow === ri && r.startCol === 0 && r.endCol === endCol\n )\n if (idx >= 0) {\n const next = this.selectionRanges.filter((_, i) => i !== idx)\n this.selectionRanges = next.length ? next : [fullRow]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [...this.selectionRanges, fullRow]\n }\n }\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [{ startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [fullRow]\n }\n\n this.mouseDownRange = this.selectionRanges[this.selectionRanges.length - 1]\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderMouseDown(e, cell) {\n // Start pending drag (activates after 5 px horizontal movement)\n // Works for both leaf and group headers\n if (this.columnDraggable && !this.internalLocked && !e.shiftKey && !e.ctrlKey && !e.metaKey) {\n this.colDrag.pending = true\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = cell._firstLeafFlatIndex\n this.colDrag.sourceLeafCount = cell.isGroup ? cell.colspan : 1\n this.colDrag.startX = e.clientX\n this.colDrag.startY = e.clientY\n this.colDrag.ghostLabel = cell.column.title || ''\n }\n\n if (cell.isGroup) {\n // Group header click: select all leaf columns within the group\n const startCol = cell._firstLeafFlatIndex\n const endCol = startCol + cell.colspan - 1\n this.activeCell = { row: 0, col: startCol }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol, endCol }]\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n return\n }\n\n const ci = cell.column._flatIndex\n if (e.shiftKey && this.selectionRanges.length > 0) {\n const sc = Math.min(this.activeCell.col, ci)\n const ec = Math.max(this.activeCell.col, ci)\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: sc, endCol: ec }]\n } else {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n }\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderDblClick(e, cell) {\n if (cell.isGroup) return\n this.autoFitColumn(cell.column._flatIndex)\n },\n\n onColHeaderContextMenu(e, cell) {\n if (!cell.isGroup) {\n this._openContextMenu(e, -1, cell.column._flatIndex)\n }\n },\n\n onRowContextMenu(e, ri) {\n this._openContextMenu(e, ri, -1)\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this._openContextMenu(e, ri, ci)\n },\n\n _openContextMenu(e, row, col) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row, col }\n this.contextMenuPositioned = false\n this.$nextTick(this._positionContextMenu)\n },\n\n // Flips the menu up/left if it would overflow the viewport at the raw\n // click coordinates — measured against the actual rendered size (item\n // count varies with devMode/pin/row-vs-col context, so an estimate isn't\n // reliable). Rendered off-screen (left:-9999px, see template) while\n // unpositioned so this first measurement reflects the menu's true\n // natural (single-line) width — measuring it in place near a viewport\n // edge would let the browser's own shrink-to-fit sizing wrap the text\n // to fit the cramped space, which then \"sticks\" even after flipping to\n // a roomier position since we only move it, we don't force a width.\n // Stays hidden (visibility, not v-if) until repositioned so there's no\n // visible jump.\n _positionContextMenu() {\n const el = this.$refs.contextMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.contextMenu.x\n let y = this.contextMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.contextMenu.x = x\n this.contextMenu.y = y\n this.contextMenuPositioned = true\n },\n\n hideContextMenu() {\n this.contextMenu.visible = false\n },\n\n // =========================================================\n // KEYBOARD NAVIGATION (3.2)\n // =========================================================\n onKeyDown(e) {\n // Delegate keyboard navigation to the active dropdown component\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n if (comp && comp.handleKey) comp.handleKey(e)\n return\n }\n\n if (this.infoModal) {\n if (e.key === 'Escape') this.closeInfoModal()\n return\n }\n\n if (this.editingCell) {\n // Normally handled by onEditKeyDown on the built-in <input> (which stops\n // propagation, so this branch never runs for it). Custom #cell-{key} edit\n // slots own their own input and focus it themselves — if that focus didn't\n // land (e.g. the host relies on the unreliable HTML `autofocus` attribute\n // on a freshly-inserted element), Escape falls through to here instead.\n // Without this, editingCell stays stuck and even arrow-key navigation is\n // swallowed by this same guard on every subsequent keydown.\n if (e.key === 'Escape') this.cancelEdit()\n return\n }\n const key = e.key\n\n if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {\n e.preventDefault()\n this.undo()\n return\n }\n if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {\n e.preventDefault()\n this.redo()\n return\n }\n\n if (key === 'a' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.selectAll()\n return\n }\n\n if (key === 'c' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.copySelection()\n return\n }\n if (key === 'x' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.cutSelection()\n return\n }\n if (key === 'v' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.pasteAt(this.activeCell.row, this.activeCell.col)\n return\n }\n\n if (key === 'Delete' || key === 'Backspace') {\n e.preventDefault()\n this.clearSelection()\n return\n }\n\n if (key === 'F2') {\n e.preventDefault()\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openDatePickerForActive()\n } else if (activeCol && activeCol.type === 'checkbox' && activeEditable) {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else if (activeCol && activeCol.type === 'action' && activeEditable) {\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n } else if (activeEditable) {\n this.startEditing(this.activeCell.row, this.activeCell.col, null, true)\n }\n return\n }\n\n if (key === ' ') {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n return\n }\n }\n\n if (key === 'Escape') {\n this.clipboardRange = null\n this.clipboardMatrix = null\n this.clipboardMode = null\n return\n }\n\n if (key === 'Tab') {\n e.preventDefault()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n return\n }\n if (key === 'Enter') {\n e.preventDefault()\n this.moveActive(e.shiftKey ? -1 : 1, 0, e)\n return\n }\n\n const arrows = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] }\n if (arrows[key]) {\n e.preventDefault()\n const [dr, dc] = arrows[key]\n if (e.shiftKey) {\n // Extend selection\n const pr = this.primaryRange || singleCellRange(this.activeCell.row, this.activeCell.col)\n const nm = normalizeRange(pr)\n // Determine which edge to extend\n if (dr === -1) pr.startRow = Math.max(0, nm.startRow - 1)\n if (dr === 1) pr.endRow = Math.min(this.totalRows - 1, nm.endRow + 1)\n if (dc === -1) pr.startCol = Math.max(0, nm.startCol - 1)\n if (dc === 1) pr.endCol = Math.min(this.leafCols.length - 1, nm.endCol + 1)\n this.selectionRanges = [pr]\n this.emitSelectionChange()\n } else {\n this.moveActive(dr, dc, e)\n }\n return\n }\n\n if (key === 'Home') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(0, 0)\n } else {\n this.selectCell(this.activeCell.row, 0)\n }\n return\n }\n if (key === 'End') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(this.totalRows - 1, this.leafCols.length - 1)\n } else {\n this.selectCell(this.activeCell.row, this.leafCols.length - 1)\n }\n return\n }\n if (key === 'PageUp') {\n e.preventDefault()\n this.moveActive(-this._pageRowSpan(), 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n this.moveActive(this._pageRowSpan(), 0, e)\n return\n }\n },\n\n onKeyPress(e) {\n if (this.editingCell) return\n if (this.selectDropdown) return\n if (this.datePickerOverlay) return\n if (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openDatePickerForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n return\n }\n this.startEditing(this.activeCell.row, this.activeCell.col, e.key, false)\n }\n },\n\n onGlobalKeyDown(e) {\n if (e.key === 'Escape' && (this.colDrag.active || this.colDrag.pending)) {\n this._resetColDrag()\n e.preventDefault()\n }\n },\n\n moveActive(dr, dc, e) {\n let row = this.activeCell.row + dr\n let col = this.activeCell.col + dc\n\n // Tab: wrap around rows\n if (e && e.key === 'Tab') {\n if (col >= this.leafCols.length) { col = 0; row++ }\n if (col < 0) { col = this.leafCols.length - 1; row-- }\n }\n\n row = Math.max(0, Math.min(this.totalRows - 1, row))\n col = Math.max(0, Math.min(this.leafCols.length - 1, col))\n\n // Skip hidden cols\n while (this.isColHidden(col) && col > 0 && col < this.leafCols.length - 1) {\n col += dc >= 0 ? 1 : -1\n }\n // Skip hidden rows\n while (this.isRowHidden(row) && row > 0 && row < this.totalRows - 1) {\n row += dr >= 0 ? 1 : -1\n }\n\n // Handle merge navigation\n const resolved = resolveMergeAnchor(row, col, this.internalMerges)\n row = resolved.row\n col = resolved.col\n\n this.activeCell = { row, col }\n this.selectionRanges = [singleCellRange(row, col)]\n this.emitSelectionChange()\n this.scrollToCell(row, col)\n },\n\n scrollToCell(ri, ci) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rowTop = getRowTop(ri, this._rowH, this.rowHeights)\n const rowBottom = rowTop + this.getRowH(ri)\n const colLeft = this.getColLeft(ci) - this.getFrozenWidth()\n const colRight = colLeft + this.getColW(ci)\n\n if (rowTop < scrollBody.scrollTop) scrollBody.scrollTop = rowTop\n if (rowBottom > scrollBody.scrollTop + scrollBody.clientHeight) {\n scrollBody.scrollTop = rowBottom - scrollBody.clientHeight\n }\n if (colLeft < scrollBody.scrollLeft) scrollBody.scrollLeft = colLeft\n if (colRight > scrollBody.scrollLeft + scrollBody.clientWidth) {\n scrollBody.scrollLeft = colRight - scrollBody.clientWidth\n }\n },\n\n // =========================================================\n // EDITING (used by 3.1 + 3.2)\n // =========================================================\n startEditing(ri, ci, initialChar = null, keepValue = false) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (ri < 0 || ri >= this.totalRows) return\n if (!this._hasCustomEdit(col)) {\n if (this.isSelectLikeCol(col)) { this.openSelectDropdownForActive(); return }\n if (col.type === 'date') { this.openDatePickerForActive(); return }\n if (col.type === 'checkbox') return\n if (col.type === 'action') return\n }\n\n this.invalidCell = null\n this.editingCell = { row: ri, col: ci }\n if (initialChar !== null) {\n this.editValue = initialChar\n } else if (keepValue) {\n this.editValue = String(this.getCellValue(ri, ci) || '')\n } else {\n this.editValue = ''\n }\n this.editOriginalValue = String(this.getCellValue(ri, ci) || '')\n\n this.$nextTick(() => {\n const inputs = this.$refs.editInput\n if (inputs) {\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) {\n input.focus()\n if (keepValue) {\n input.setSelectionRange(input.value.length, input.value.length)\n }\n }\n }\n })\n },\n\n commitEdit() {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n\n if (!this.isCellValueValid(row, col, this.editValue)) {\n this._showInvalidCell(row, col, this.editValue)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: this.editValue }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n cancelEdit() {\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n hasCellSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])\n },\n hasLegacyEditSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])\n },\n hasPrefixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['prefix-' + col.key])\n },\n hasSuffixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['suffix-' + col.key])\n },\n _hasCustomEdit(col) {\n // For select/treeSelect/date: cell-{key} slot only customizes display; picker still opens normally\n if (col.type === 'select' || col.type === 'treeSelect' || col.type === 'date') {\n return this.hasLegacyEditSlot(col)\n }\n return this.hasCellSlot(col) || this.hasLegacyEditSlot(col)\n },\n\n commitEditSlot(value) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // CHECKBOX CELLS\n // =========================================================\n\n isCheckboxChecked(ri, ci) {\n const v = this.getCellValue(ri, ci)\n if (v === true || v === 1) return true\n if (typeof v === 'string') return ['true', '1'].includes(v.trim().toLowerCase())\n return false\n },\n\n setCheckboxValue(ri, ci, value) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (!this.isCellValueValid(ri, ci, value)) {\n this._showInvalidCell(ri, ci, value)\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[ri] = { ...oldData[ri], [col.key]: value }\n this._emitDataChange(oldData, newData)\n },\n\n toggleCheckbox(ri, ci) {\n this.setCheckboxValue(ri, ci, !this.isCheckboxChecked(ri, ci))\n },\n\n // =========================================================\n // ACTION CELLS (col.type === 'action')\n // =========================================================\n\n // Dynamic v-on argument expressions can't contain quotes/spaces in Vue 2.6\n // templates, so the 'click' fallback is resolved here instead of inline.\n actionHandlerEvent(col) {\n return (col && col.handlerType) || 'click'\n },\n\n triggerCellAction(ri, ci, e) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || col.type !== 'action' || typeof col.handler !== 'function') return\n if (!this.isCellEditable(ri, ci)) return\n col.handler(this._dataRaw[ri], ri, col, ci, e)\n },\n\n // =========================================================\n // CELL EDITABILITY (3.17)\n // =========================================================\n\n isCellEditable(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return false\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellEditable === 'function') return !!this.cellEditable(row, ri, col, ci)\n if (typeof col.editable === 'function') return !!col.editable(row, ri)\n if (col.editable === false) return false\n return true\n },\n\n // =========================================================\n // CELL VALUE VALIDATION (cellValidator)\n // =========================================================\n\n isCellValueValid(ri, ci, value) {\n if (typeof this.cellValidator !== 'function') return true\n const col = this.leafCols[ci]\n if (!col) return true\n const row = this._dataRaw[ri] || {}\n return !!this.cellValidator(value, row, ri, col, ci)\n },\n\n _resolveValidationMessage(ri, ci, value) {\n const col = this.leafCols[ci]\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellValidationMessage === 'function') return this.cellValidationMessage(value, row, ri, col, ci)\n if (typeof this.cellValidationMessage === 'string' && this.cellValidationMessage) return this.cellValidationMessage\n return this.t.invalidValue\n },\n\n // Find the on-screen rect of the cell currently being committed — the edit\n // input if still mounted (commitEdit, before editingCell is cleared), else\n // the active cell element (checkbox/select commits, which never enter edit mode).\n _invalidCellRect() {\n const inputs = this.$refs.editInput\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) return input.getBoundingClientRect()\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n return el ? el.getBoundingClientRect() : null\n },\n\n _showInvalidCell(ri, ci, value) {\n const message = this._resolveValidationMessage(ri, ci, value)\n this.invalidCell = { row: ri, col: ci, message, rect: this._invalidCellRect() }\n this.$emit('cell-validation-error', { row: ri, col: ci, value, message })\n },\n\n // Resolve+emit messages for a batch of rejected { row, col, value } entries\n // from a bulk write (paste / fill / clear). blocked=true means the whole\n // operation was aborted (strictBulkValidation); false means these cells\n // were skipped but the rest of the write still applied.\n _emitBulkValidationError(skipped, blocked) {\n const cells = skipped.map(c => ({\n row: c.row, col: c.col, value: c.value,\n message: this._resolveValidationMessage(c.row, c.col, c.value)\n }))\n this.$emit('bulk-validation-error', { blocked, cells })\n },\n\n // =========================================================\n // SELECT DROPDOWN (3.16) + TREE SELECT (3.22)\n // =========================================================\n\n isSelectLikeCol(col) {\n return !!(col && (col.type === 'select' || col.type === 'treeSelect'))\n },\n\n treeOptionKeys(col) {\n return {\n valueKey: col.optionValueKey || 'value',\n labelKey: col.optionLabelKey || 'label',\n childrenKey: col.optionChildrenKey || 'children'\n }\n },\n\n _treeFindLabel(nodes, keys, value) {\n for (const node of (nodes || [])) {\n if (String(node[keys.valueKey]) === value) {\n return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])\n }\n const found = this._treeFindLabel(node[keys.childrenKey], keys, value)\n if (found != null) return found\n }\n return null\n },\n\n // col.options may be a static array OR a function (row, rowIndex, column, colIndex) => array,\n // letting hosts return a different option list per cell.\n hasOptionsSource(col) {\n return !!(col && (Array.isArray(col.options) || typeof col.options === 'function'))\n },\n\n resolveOptionsArray(col, ri, ci) {\n if (!col) return []\n if (typeof col.options === 'function') {\n const row = ri != null ? (this._dataRaw[ri] || {}) : {}\n const result = col.options(row, ri, col, ci != null ? ci : col._flatIndex)\n return Array.isArray(result) ? result : []\n }\n return Array.isArray(col.options) ? col.options : []\n },\n\n colOptions(ci, ri) {\n const col = this.leafCols[ci]\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return []\n return raw.map(o =>\n typeof o === 'string' ? { value: o, label: o } : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n\n getSelectLabel(ci, value, ri) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return v\n const found = this._treeFindLabel(raw, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci, ri)\n if (!opts.length) return v\n const found = opts.find(o => String(o.value) === v)\n return found ? found.label : v\n },\n\n _buildSelectDropdownState(ri, ci, col, currentValue, rect) {\n const rectObj = { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n return {\n row: ri, col: ci,\n tree: col.type === 'treeSelect',\n keys: col.type === 'treeSelect' ? this.treeOptionKeys(col) : null,\n rawOptions: this.resolveOptionsArray(col, ri, ci),\n currentValue,\n rect: rectObj,\n enableSearch: !!col.enableSearch\n }\n },\n\n openSelectDropdown(ri, ci, e) {\n const col = this.leafCols[ci]\n if (!this.hasOptionsSource(col)) return\n const el = e.currentTarget\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(ri, ci) != null ? this.getCellValue(ri, ci) : '')\n this.selectDropdown = this._buildSelectDropdownState(ri, ci, col, currentValue, rect)\n },\n\n openSelectDropdownForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!this.hasOptionsSource(colDef)) return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(row, col) != null ? this.getCellValue(row, col) : '')\n this.selectDropdown = this._buildSelectDropdownState(row, col, colDef, currentValue, rect)\n })\n },\n\n onComponentSelect({ value, label }) {\n if (this.internalLocked) { this.closeSelectDropdown(); return }\n if (!this.selectDropdown) return\n const { row, col } = this.selectDropdown\n const colDef = this.leafCols[col]\n if (colDef) {\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.closeSelectDropdown()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n if (typeof colDef.onSelect === 'function') {\n // Deferred the same way as _fireOnSelectForRange (paste/fill/drag-move) —\n // a host watching the emitted data (e.g. mirroring it into its own\n // store) needs that watcher to run first, or onSelect reads stale state.\n this.$nextTick(() => colDef.onSelect({ value, label }, newData[row], row, colDef))\n }\n }\n this.closeSelectDropdown()\n },\n\n // Always returns focus to the grid wrapper — the dropdown (especially with\n // enableSearch) can hold real DOM focus on its own search input, and every\n // close path (Escape, outside click, picking a value) must hand focus back\n // or keyboard navigation on the grid goes dead until the user clicks a cell.\n closeSelectDropdown() {\n this.selectDropdown = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // DATE PICKER\n // =========================================================\n\n getCellDisplayValue(ci, val, ri) {\n const col = this.leafCols[ci]\n if (!col) return val == null ? '' : val\n if (typeof col.formatter === 'function') {\n const out = col.formatter(val, this._dataRaw[ri], ri, col, ci)\n return out == null ? '' : out\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)\n if (col.type === 'date') {\n const d = parseDateValue(val)\n return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)\n }\n return val == null ? '' : val\n },\n\n // =========================================================\n // FILTER ROW\n // =========================================================\n\n // Which control the filter cell renders for this column.\n // 'none' cells still render (empty) so column alignment stays intact.\n filterControlType(col) {\n if (!col || col.filterable === false || col.type === 'action') return 'none'\n if (col.type === 'checkbox') return 'checkbox'\n // Function-based options can't be resolved into a fixed list outside a\n // specific row, so those columns fall back to the text control.\n if ((col.type === 'select' || col.type === 'treeSelect') && Array.isArray(col.options)) return 'select'\n return 'text'\n },\n\n // Flat { value, label } list for a select-control filter cell. treeSelect\n // options are recursively flattened, each depth indented for readability.\n filterOptionsFor(col) {\n if (!col) return []\n if (col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, null, col._flatIndex)\n const out = []\n this._flattenTreeOptionsForFilter(raw, this.treeOptionKeys(col), 0, out)\n return out\n }\n return this.colOptions(col._flatIndex, null)\n },\n\n _flattenTreeOptionsForFilter(nodes, keys, depth, out) {\n for (const node of (nodes || [])) {\n const value = node[keys.valueKey]\n const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(value)\n out.push({ value: String(value), label: ' '.repeat(depth) + label })\n this._flattenTreeOptionsForFilter(node[keys.childrenKey], keys, depth + 1, out)\n }\n },\n\n getFilterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n return {\n position: 'absolute',\n left: left + 'px',\n top: (this._headerRowH * this.headerDepth) + 'px',\n width: w + 'px',\n height: this._filterRowH + 'px',\n boxSizing: 'border-box'\n }\n },\n\n setFilter(key, value) {\n if (!this.leafCols.some(c => c.key === key)) return\n const next = { ...this.internalFilters }\n if (value === '' || value === null || value === undefined) {\n delete next[key]\n } else {\n next[key] = value\n }\n this.internalFilters = next\n this.$emit('update:filters', { ...next })\n this.$emit('filter-change', { key, value, filters: { ...next } })\n },\n\n clearFilters() {\n if (!Object.keys(this.internalFilters).length) return\n this.internalFilters = {}\n // A deliberate, infrequent action (unlike typing) — recompute immediately\n // rather than waiting out the debounce, and drop any timer already pending.\n clearTimeout(this._filterRecomputeTimer)\n this._recomputeRowFilters()\n this.$emit('update:filters', {})\n this.$emit('filter-change', { key: null, value: null, filters: {} })\n },\n\n // Debounced — recomputing filteredRowIndices scans every row, so typing\n // in the text filter shouldn't trigger it on every keystroke.\n _scheduleFilterRecompute() {\n clearTimeout(this._filterRecomputeTimer)\n this._filterRecomputeTimer = setTimeout(() => { this._recomputeRowFilters() }, 150)\n },\n\n _activeFilterCols() {\n const cols = []\n for (const col of this.leafCols) {\n if (this.filterControlType(col) === 'none') continue\n const v = this.internalFilters[col.key]\n if (v === undefined || v === null || v === '') continue\n cols.push(col)\n }\n return cols\n },\n\n _cellMatchesFilter(row, ri, col) {\n const ci = col._flatIndex\n const filterValue = this.internalFilters[col.key]\n if (typeof col.filterFn === 'function') {\n return !!col.filterFn(this.getCellValue(ri, ci), filterValue, row, ri, col)\n }\n const kind = this.filterControlType(col)\n if (kind === 'checkbox') {\n const checked = this.isCheckboxChecked(ri, ci)\n return filterValue === 'true' ? checked : !checked\n }\n if (kind === 'select') {\n const raw = this.getCellValue(ri, ci)\n return String(raw == null ? '' : raw) === String(filterValue)\n }\n const display = this.getCellDisplayValue(ci, this.getCellValue(ri, ci), ri)\n return String(display == null ? '' : display).toLowerCase().indexOf(String(filterValue).toLowerCase()) !== -1\n },\n\n _recomputeRowFilters() {\n // Can be reached (via the `columns`/`filterRow` watchers) during initWatch(),\n // before created() has set up _dataRaw — getCellValue() depends on it. The\n // `data` watcher's own immediate handler sets _dataRaw first, then always\n // calls this again, so the initial mount still ends up correct.\n if (!this._dataRaw) return\n if (!this.filterRow) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const activeCols = this._activeFilterCols()\n if (!activeCols.length) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const rows = this.data\n const out = []\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n let matches = true\n for (const col of activeCols) {\n if (!this._cellMatchesFilter(row, ri, col)) { matches = false; break }\n }\n if (!matches) out.push(ri)\n }\n this.filteredRowIndices = out\n },\n\n // =========================================================\n // COLUMN SORTING\n // =========================================================\n\n isColSortable(col) {\n return !!(this.sortable && col && col.sortable !== false && col.type !== 'action')\n },\n\n sortIconClass(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return 'eg-icon-sort'\n return this.internalSort.direction === 'asc' ? 'eg-icon-sort-asc' : 'eg-icon-sort-desc'\n },\n\n sortIconTitle(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return this.t.sortNone\n return this.internalSort.direction === 'asc' ? this.t.sortAsc : this.t.sortDesc\n },\n\n // Cycles a column through none → asc → desc → none. Used by the header\n // sort icon's click handler; also usable from a custom #header-{key} slot.\n toggleSort(key) {\n if (this.internalLocked) return\n const current = this.internalSort\n if (!current || current.key !== key) { this.setSort(key, 'asc'); return }\n if (current.direction === 'asc') { this.setSort(key, 'desc'); return }\n this.clearSort()\n },\n\n setSort(key, direction) {\n if (direction !== 'asc' && direction !== 'desc') { this.clearSort(); return }\n this.sortByColumn(key, direction)\n },\n\n // Clicking a third time (or calling this directly) only clears the sort\n // indicator — it does not restore the pre-sort row order by itself,\n // since sorting already reordered `data` in place. Use undo() for that.\n clearSort() {\n if (!this.internalSort) return\n this.internalSort = null\n this.$emit('update:sort', null)\n this.$emit('sort-change', { key: null, direction: null })\n },\n\n sortByColumn(key, direction) {\n if (this.internalLocked) return\n // Merges and row groups are anchored to row *position* — a reorder\n // would silently invalidate them, so sorting is blocked while either\n // exists (same \"blocked while locked\" no-op convention as elsewhere).\n if (this.internalMerges.length || this.rowGroups.length) return\n const col = this.leafCols.find(c => c.key === key)\n if (!this.isColSortable(col)) return\n\n const oldData = this.data\n const pairs = oldData.map((row, ri) => ({ row, ri }))\n pairs.sort((a, b) => this._compareForSort(a.row, a.ri, b.row, b.ri, col, direction))\n const newData = pairs.map(p => p.row)\n\n this.internalSort = { key, direction }\n this._emitDataChange(oldData, newData)\n this.$emit('update:sort', { key, direction })\n this.$emit('sort-change', { key, direction })\n\n this.scrollTop = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) this.$refs.scrollBody.scrollTop = 0\n })\n },\n\n _sortValueFor(row, ri, col) {\n const ci = col._flatIndex\n if (col.type === 'checkbox') return this.isCheckboxChecked(ri, ci) ? 1 : 0\n if (col.type === 'date') {\n const d = parseDateValue(this.getCellValue(ri, ci))\n return d ? d.getTime() : null\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, this.getCellValue(ri, ci), ri)\n return this.getCellValue(ri, ci)\n },\n\n _compareForSort(rowA, riA, rowB, riB, col, direction) {\n if (typeof col.sortFn === 'function') {\n const result = col.sortFn(rowA, rowB, col)\n return direction === 'desc' ? -result : result\n }\n const a = this._sortValueFor(rowA, riA, col)\n const b = this._sortValueFor(rowB, riB, col)\n const aEmpty = a === null || a === undefined || a === ''\n const bEmpty = b === null || b === undefined || b === ''\n if (aEmpty && bEmpty) return 0\n if (aEmpty) return 1\n if (bEmpty) return -1\n const cmp = (typeof a === 'number' && typeof b === 'number')\n ? a - b\n : String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' })\n return direction === 'desc' ? -cmp : cmp\n },\n\n openDatePickerForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!colDef || colDef.type !== 'date') return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const val = this.getCellValue(row, col)\n this.datePickerOverlay = {\n row, col,\n format: colDef.format || 'YYYY-MM-DD',\n value: val == null ? '' : String(val),\n rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n }\n })\n },\n\n onDatePick(formattedVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n const colDef = this.leafCols[col]\n if (colDef) {\n if (formattedVal !== '' && !this.isCellValueValid(row, col, formattedVal)) {\n this._showInvalidCell(row, col, formattedVal)\n this.closeDatePicker()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: formattedVal }\n this._emitDataChange(oldData, newData)\n }\n this.closeDatePicker()\n },\n\n // See closeSelectDropdown() — same reasoning: every close path must return\n // focus to the grid wrapper or keyboard navigation goes dead afterward.\n closeDatePicker() {\n this.datePickerOverlay = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n _onWindowScrollForDropdown(e) {\n if (this.contextMenu.visible) this.hideContextMenu()\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n _onDocMouseDownForDropdown(e) {\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n onEditKeyDown(e) {\n if (e.key === 'Escape') {\n e.preventDefault()\n this.cancelEdit()\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(1, 0, e)\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n }\n },\n\n clearSelection() {\n if (this.internalLocked) return\n if (!this.selectionRanges.length) return\n const oldData = this.data\n const newData = oldData.slice()\n let hasChanges = false\n const skipped = []\n for (const range of this.selectionRanges) {\n const r = {\n startRow: Math.min(range.startRow, range.endRow),\n endRow: Math.max(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endCol: Math.max(range.startCol, range.endCol)\n }\n for (let ri = r.startRow; ri <= r.endRow && ri < newData.length; ri++) {\n let rowUpdated = null\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, '')) { skipped.push({ row: ri, col: ci, value: '' }); continue }\n if (!rowUpdated) rowUpdated = { ...newData[ri] }\n rowUpdated[col.key] = ''\n hasChanges = true\n }\n }\n if (rowUpdated) newData[ri] = rowUpdated\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n if (hasChanges) this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // FILL HANDLE (3.3)\n // =========================================================\n startFillDrag(e) {\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n this.fillDrag.active = true\n this.fillDrag.sourceRange = normalizeRange(pr)\n this.fillDrag.currentRow = pr.endRow\n this.fillDrag.currentCol = pr.endCol\n e.preventDefault()\n },\n\n onGlobalMouseMove(e) {\n // Detect column drag threshold (5 px)\n if (this.colDrag.pending) {\n if (Math.abs(e.clientX - this.colDrag.startX) > 5) {\n this.colDrag.pending = false\n this.colDrag.active = true\n this.colDrag.dropIndex = this.colDrag.sourceFirstLeaf\n this.isMouseDown = false // cancel cell-selection drag\n }\n }\n if (this.colDrag.active) {\n this.handleColDragMove(e)\n return\n }\n\n if (this.resizing.active) {\n this.handleResizeMove(e)\n return\n }\n if (this.fillDrag.active) {\n this.handleFillDragMove(e)\n return\n }\n if (this.dragMove.active) {\n this.handleDragMoveMove(e)\n return\n }\n },\n\n onGlobalMouseUp(e) {\n if (this.colDrag.active) {\n this.commitColDrag()\n return\n }\n if (this.colDrag.pending) {\n this._resetColDrag()\n // fall through — normal mouseup\n }\n\n if (this.resizing.active) {\n this.commitResize()\n return\n }\n if (this.fillDrag.active) {\n this.commitFillDrag()\n return\n }\n if (this.dragMove.active) {\n this.commitDragMove(e)\n return\n }\n this.isMouseDown = false\n },\n\n handleFillDragMove(e) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rect = scrollBody.getBoundingClientRect()\n const relY = e.clientY - rect.top + scrollBody.scrollTop\n const relX = e.clientX - rect.left + scrollBody.scrollLeft\n\n // Binary search on cumulativeHeights — O(log n) instead of O(n)\n const cum = this.cumulativeHeights\n const total = this.totalRows\n let lo = 0, hi = total - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (cum[mid + 1] <= relY) lo = mid + 1\n else hi = mid\n }\n const row = Math.min(lo, total - 1)\n\n // Columns are few — O(columns) is fast\n let left = this.getFrozenWidth()\n let col = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColumnScrollable(i)) continue\n left += this.getColW(i)\n if (relX < left) { col = i; break }\n col = i\n }\n\n this.fillDrag.currentRow = row\n this.fillDrag.currentCol = col\n },\n\n async commitFillDrag() {\n if (this.internalLocked) { this.fillDrag.active = false; return }\n const src = this.fillDrag.sourceRange\n if (!src) { this.fillDrag.active = false; return }\n\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n\n const oldData = this.data\n const newData = oldData.slice() // only affected rows get new objects\n let targetRange\n const skipped = []\n\n if (targetRow > src.endRow) {\n // Fill down\n const count = targetRow - src.endRow\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n targetRange = { startRow: src.endRow + 1, endRow: targetRow, startCol: src.startCol, endCol: src.endCol }\n } else if (targetCol > src.endCol) {\n // Fill right\n const count = targetCol - src.endCol\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n const srcValues = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n srcValues.push(col && oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ci = src.endCol + 1 + k\n if (ci < this.leafCols.length) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n }\n targetRange = { startRow: src.startRow, endRow: src.endRow, startCol: src.endCol + 1, endCol: targetCol }\n }\n\n let fireOnSelect = false\n if (targetRange) {\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n } else {\n this._emitDataChange(oldData, newData)\n this.$emit('fill', { sourceRange: src, targetRange, values: newData })\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n fireOnSelect = true\n }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\n\n if (fireOnSelect) {\n // See pasteAt's _fireOnSelectForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n doubleFillDown() {\n const pr = this.primaryRange\n if (!pr) return\n const src = normalizeRange(pr)\n // Find adjacent column filled length\n const adjCol = src.startCol > 0 ? src.startCol - 1 : src.endCol + 1\n if (adjCol < 0 || adjCol >= this.leafCols.length) return\n const adjColDef = this.leafCols[adjCol]\n if (!adjColDef) return\n let lastFilledRow = src.endRow\n for (let ri = src.endRow + 1; ri < this.totalRows; ri++) {\n if (this.data[ri] && this.data[ri][adjColDef.key] !== '' && this.data[ri][adjColDef.key] !== undefined) {\n lastFilledRow = ri\n } else break\n }\n if (lastFilledRow <= src.endRow) return\n\n const count = lastFilledRow - src.endRow\n const oldData = this.data\n const newData = oldData.slice()\n const skipped = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // COPY / PASTE (3.4)\n // =========================================================\n copySelection() {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n this.clipboardMatrix = extractMatrix(this.data, this.leafCols, r)\n this.clipboardRange = r\n this.clipboardMode = 'copy'\n\n // Write to system clipboard as TSV\n const tsv = matrixToTSV(this.clipboardMatrix)\n if (navigator.clipboard) {\n navigator.clipboard.writeText(tsv).catch(() => {})\n }\n },\n\n cutSelection() {\n this.copySelection()\n this.clipboardMode = 'cut'\n },\n\n // Bulk cell writes (paste, fill handle, drag-move) set row values directly\n // and skip the single-cell dropdown path in onComponentSelect — so\n // col.onSelect never fires for them unless we replay it here for every\n // select/treeSelect cell whose value actually changed in the range.\n _fireOnSelectForRange(oldData, newData, startRow, endRow, startCol, endCol) {\n for (let ri = startRow; ri <= endRow; ri++) {\n const oldRow = oldData[ri]\n const newRow = newData[ri]\n if (!newRow || newRow === oldRow) continue\n for (let ci = startCol; ci <= endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col || !this.isSelectLikeCol(col) || typeof col.onSelect !== 'function') continue\n const oldVal = oldRow ? oldRow[col.key] : undefined\n const newVal = newRow[col.key]\n if (newVal === oldVal) continue\n col.onSelect({ value: newVal, label: this.getSelectLabel(ci, newVal, ri) }, newRow, ri, col)\n }\n }\n },\n\n async pasteAt(targetRow, targetCol, options = {}) {\n if (this.internalLocked) return\n const { mode = 'all', transpose = false } = options\n\n let matrix = null\n\n // Try system clipboard first\n if (navigator.clipboard) {\n try {\n const text = await navigator.clipboard.readText()\n if (text) matrix = tsvToMatrix(text)\n } catch (_) {}\n }\n\n // Fall back to internal clipboard\n if (!matrix && this.clipboardMatrix) {\n matrix = this.clipboardMatrix\n }\n\n if (!matrix) return\n\n if (transpose) matrix = transposeMatrix(matrix)\n\n // ── Excel-style selection broadcast / tiling ──────────────────────────\n // When pasting over a multi-cell selection and the clipboard matrix is\n // smaller than the selection, tile the matrix to fill the selection —\n // provided the selection dimensions are exact multiples of the matrix\n // dimensions (matches Excel's tiling rule exactly).\n // Cut-paste is never tiled (source must be cleared exactly once).\n const isCopyPaste = !this.clipboardMode || this.clipboardMode === 'copy'\n if (isCopyPaste) {\n const pr = this.primaryRange\n if (pr) {\n const sel = normalizeRange(pr)\n const selRows = sel.endRow - sel.startRow + 1\n const selCols = sel.endCol - sel.startCol + 1\n const matRows = matrix.length\n const matCols = matrix[0] ? matrix[0].length : 0\n\n // Only tile when selection is strictly larger than clipboard\n if ((selRows > matRows || selCols > matCols) &&\n selRows % matRows === 0 && selCols % matCols === 0) {\n matrix = tileMatrix(matrix, selRows, selCols)\n // Paste at selection origin, not just at the active cell\n targetRow = sel.startRow\n targetCol = sel.startCol\n }\n }\n }\n\n const oldData = this.data\n const { newData, affectedRange, skipped } = applyMatrix(\n oldData, this.leafCols, targetRow, targetCol, matrix, mode,\n (ri, ci) => this.isCellEditable(ri, ci),\n (ri, ci, v) => this.isCellValueValid(ri, ci, v)\n )\n\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n\n if (this.clipboardMode === 'cut' && this.clipboardRange) {\n // Clear source — ensure each source row is a new copy before mutation\n const src = this.clipboardRange\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci) && this.isCellValueValid(ri, ci, '')) {\n newData[ri][col.key] = ''\n }\n }\n }\n this.clipboardRange = null\n this.clipboardMode = null\n }\n\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n // Wait for Vue's reactivity queue to flush before firing onSelect — hosts\n // commonly react to the emitted 'input' with their own watcher (e.g. to\n // mirror the row into an external store); firing onSelect synchronously\n // would run it against that store's pre-paste, stale state.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, affectedRange.startRow, affectedRange.endRow, affectedRange.startCol, affectedRange.endCol)\n },\n\n ctxCopy() { this.hideContextMenu(); this.copySelection() },\n ctxCut() { this.hideContextMenu(); this.cutSelection() },\n ctxPaste() { this.hideContextMenu(); this.pasteAt(this.activeCell.row, this.activeCell.col) },\n ctxPasteSpecial(mode, transpose = false) {\n this.hideContextMenu()\n this.pasteAt(this.activeCell.row, this.activeCell.col, { mode, transpose })\n },\n\n // =========================================================\n // COLUMN DRAG-AND-DROP\n // =========================================================\n\n _computeDragOffsets(dropIndex) {\n const { sourceFirstLeaf, sourceLeafCount } = this.colDrag\n if (dropIndex < 0 || (dropIndex >= sourceFirstLeaf && dropIndex <= sourceFirstLeaf + sourceLeafCount)) return {}\n\n const previewCols = moveColumnNodeInTree(this.columns, sourceFirstLeaf, sourceLeafCount, dropIndex)\n const previewLeaves = flattenColumns(previewCols)\n\n const byKey = {}\n this.leafCols.forEach(c => { byKey[c.key] = c })\n\n const offsets = {}\n for (const zone of ['left', 'scroll', 'right']) {\n const inZone = zone === 'left'\n ? ci => this.isColumnPinnedLeft(ci)\n : zone === 'right'\n ? ci => this.isColumnPinnedRight(ci)\n : ci => this.isColumnScrollable(ci)\n\n const origPos = {}\n let acc = 0\n this.leafCols.forEach(col => {\n const ci = col._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) return\n origPos[col.key] = acc\n acc += this.getColW(ci)\n })\n\n const previewPos = {}\n acc = 0\n for (const col of previewLeaves) {\n const origCol = byKey[col.key]\n if (!origCol) continue\n const ci = origCol._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) continue\n previewPos[col.key] = acc\n acc += this.getColW(ci)\n }\n\n for (const key of Object.keys(origPos)) {\n offsets[key] = (previewPos[key] !== undefined ? previewPos[key] : origPos[key]) - origPos[key]\n }\n }\n return offsets\n },\n\n _applyDragTransforms(offsets) {\n if (!this.$el) return\n const targets = []\n const dxMap = new Map()\n\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n targets.push(el)\n dxMap.set(el, dx)\n })\n this.$el.querySelectorAll('[data-start]').forEach(el => {\n const start = parseInt(el.dataset.start)\n const end = parseInt(el.dataset.end)\n if (isNaN(start)) return\n let key = null\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n targets.push(el)\n dxMap.set(el, key ? (offsets[key] || 0) : 0)\n })\n if (!targets.length) return\n\n // anime.remove cancels any in-progress animation before starting the new one.\n // This prevents chaining/overlap when dropIndex changes rapidly (no flickering).\n anime.remove(targets)\n anime({\n targets,\n translateX: el => dxMap.get(el) || 0,\n duration: 80,\n easing: 'easeOutQuart'\n })\n },\n\n _clearDragTransforms() {\n if (!this.$el) return\n const els = Array.from(this.$el.querySelectorAll('[data-fi], [data-start]'))\n // Kill all anime animations first, then instantly clear inline styles\n anime.remove(els)\n els.forEach(el => {\n el.style.transform = ''\n el.style.transition = ''\n })\n },\n\n handleColDragMove(e) {\n // Update ghost position via direct DOM — avoids Vue re-render on every mousemove\n const ghost = this.$refs.colDragGhost\n if (ghost) {\n ghost.style.left = (e.clientX + 16) + 'px'\n ghost.style.top = (e.clientY - 14) + 'px'\n }\n\n const prevDrop = this.colDrag.dropIndex\n this._updateColDragDropIndex(e.clientX)\n const newDrop = this.colDrag.dropIndex\n\n if (newDrop !== prevDrop) {\n this._dragOffsets = this._computeDragOffsets(newDrop)\n this._applyDragTransforms(this._dragOffsets)\n }\n },\n\n _updateColDragDropIndex(clientX) {\n if (!this.$el) return\n const n = this.leafCols.length\n if (n === 0) { this.colDrag.dropIndex = 0; return }\n\n // Use DOM cell rects but subtract CSS transforms so we get ORIGINAL positions.\n // This avoids circular dependency: live-preview transforms don't affect drop calc.\n const cells = Array.from(this.$el.querySelectorAll('.eg-header-cell:not(.eg-header-group)'))\n .map(el => {\n const fi = parseInt(el.dataset.fi)\n if (isNaN(fi)) return null\n const rect = el.getBoundingClientRect()\n const m = (el.style.transform || '').match(/translateX\\((-?[\\d.]+)px\\)/)\n const dx = m ? parseFloat(m[1]) : 0\n return { fi, left: rect.left - dx, right: rect.right - dx }\n })\n .filter(Boolean)\n .sort((a, b) => a.left - b.left)\n\n if (!cells.length) { this.colDrag.dropIndex = n; return }\n\n let dropIndex = n\n for (const { fi, left, right } of cells) {\n if (clientX < right) {\n dropIndex = clientX < left + (right - left) / 2 ? fi : fi + 1\n break\n }\n }\n this.colDrag.dropIndex = dropIndex\n },\n\n commitColDrag() {\n const from = this.colDrag.sourceFirstLeaf\n const leafCount = this.colDrag.sourceLeafCount\n const to = this.colDrag.dropIndex\n\n if (from < 0 || to < 0 || leafCount < 1) { this._resetColDrag(); return }\n if (to >= from && to <= from + leafCount) { this._resetColDrag(); return }\n\n // Kill anime animations but KEEP transform values in DOM while Vue re-renders\n // with the new column order. After Vue updates left positions ($nextTick),\n // we instantly clear transforms — both steps run as microtasks before browser paints,\n // so there is never a visible intermediate state.\n if (this.$el) {\n anime.remove(Array.from(this.$el.querySelectorAll('[data-fi], [data-start]')))\n }\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n\n const newCols = moveColumnNodeInTree(this.columns, from, leafCount, to)\n this.$emit('update:columns', newCols)\n this.$emit('column-reorder', { fromIndex: from, toIndex: to, columns: newCols })\n\n // Clear transforms after Vue re-renders new column left positions\n this.$nextTick(() => { this._clearDragTransforms() })\n },\n\n _resetColDrag() {\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n this._clearDragTransforms()\n },\n\n // =========================================================\n // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'col',\n index: ci,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getColW(ci),\n currentPos: e.clientX,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'row',\n index: ri,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getRowH(ri),\n currentPos: e.clientY,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n handleResizeMove(e) {\n if (this.resizing.type === 'col') {\n this.resizing.currentPos = e.clientX\n const newWidth = Math.max(20, this.resizing.startSize + (e.clientX - this.resizing.startX))\n this.$set(this.colWidths, this.resizing.index, newWidth)\n this._cellStyleCache = Object.create(null)\n } else {\n this.resizing.currentPos = e.clientY\n this._resizePreviewHeight = Math.max(10, this.resizing.startSize + (e.clientY - this.resizing.startY))\n // Batch $set + updateVirtualScroll to rAF — prevents O(n) cumulativeHeights rebuild\n // on every mousemove (200+ Hz). At most 60 rebuilds/sec instead of 200+.\n if (!this._resizeRAF) {\n this._resizeRAF = requestAnimationFrame(() => {\n this._resizeRAF = null\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n this.updateVirtualScroll()\n })\n }\n }\n },\n\n commitResize() {\n if (this._resizeRAF) {\n cancelAnimationFrame(this._resizeRAF)\n this._resizeRAF = null\n // Apply the last pending preview height immediately on commit\n if (this.resizing.type === 'row' && this._resizePreviewHeight !== undefined) {\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n }\n }\n if (this.resizing.type === 'col') {\n this._cellStyleCache = Object.create(null)\n const newSize = this.colWidths[this.resizing.index]\n this.$emit('column-resize', { index: this.resizing.index, newSize })\n } else {\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n const newSize = this.rowHeights[this.resizing.index]\n this.$emit('row-resize', { index: this.resizing.index, newSize })\n this.updateVirtualScroll()\n }\n this.resizing.active = false\n },\n\n autoFitColumn(ci) {\n const col = this.leafCols[ci]\n if (!col) return\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n const newWidth = Math.max(50, maxLen * 8 + 16)\n this.$set(this.colWidths, ci, newWidth)\n this._cellStyleCache = Object.create(null)\n this.$emit('column-resize', { index: ci, newSize: newWidth })\n },\n\n _applyAutoWidth() {\n if (!this.autoWidth || !this.leafCols.length) return\n this.leafCols.forEach((col, ci) => {\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n this.$set(this.colWidths, ci, Math.max(50, maxLen * 8 + 16))\n })\n this._cellStyleCache = Object.create(null)\n },\n\n ctxHideRow() {\n const targetRow = this.contextMenu.row\n // Row header: row >= 0 AND col = -1 (no column context)\n const fromRowHeader = targetRow >= 0 && this.contextMenu.col < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenRows]\n if (fromRowHeader) {\n if (!newHidden.includes(targetRow)) newHidden.push(targetRow)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n const targetCol = this.contextMenu.col\n // Column header: col >= 0 AND row = -1 (no row context)\n const fromColHeader = targetCol >= 0 && this.contextMenu.row < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenColumns]\n if (fromColHeader) {\n if (!newHidden.includes(targetCol)) newHidden.push(targetCol)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\n }\n }\n this.$emit('update:hiddenColumns', newHidden)\n },\n\n ctxUnhideRows() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenColumns', [])\n },\n\n ctxPinLeft() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n },\n\n isColPinned(ci) {\n const col = this.leafCols[ci]\n return !!(col && this.pinnedColumns[col.key])\n },\n\n // =========================================================\n // DEV MODE — column/row config info (context menu → modal)\n // =========================================================\n _stringifyForInfo(value) {\n const seen = new WeakSet()\n return JSON.stringify(value, (key, val) => {\n if (typeof val === 'function') return 'ƒ ' + (val.name || 'anonymous') + '()'\n if (val && typeof val === 'object') {\n if (seen.has(val)) return '[Circular]'\n seen.add(val)\n }\n return val\n }, 2)\n },\n\n ctxShowColumnInfo() {\n const ci = this.contextMenu.col\n this.hideContextMenu()\n const col = this.leafCols[ci]\n if (!col) return\n const { _flatIndex, ...rest } = col\n this.infoModal = { title: `${this.t.columnInfo}: ${col.title || col.key}`, json: this._stringifyForInfo(rest) }\n },\n\n ctxShowRowInfo() {\n const ri = this.contextMenu.row\n this.hideContextMenu()\n const row = this._dataRaw[ri]\n if (!row) return\n this.infoModal = { title: `${this.t.rowInfo} #${ri + 1}`, json: this._stringifyForInfo(row) }\n },\n\n closeInfoModal() {\n this.infoModal = null\n },\n\n // =========================================================\n // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\n if (this.internalLocked) return false\n const oldMerges = this.internalMerges.slice()\n const { merges: newMerges, conflict } = addMerge(range, this.internalMerges)\n if (conflict) {\n this.$emit('merge-error', { range, reason: 'Overlaps existing merge' })\n return false\n }\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n return true\n },\n\n unmergeCells(row, col) {\n if (this.internalLocked) return\n const oldMerges = this.internalMerges.slice()\n const newMerges = removeMergeAt(row, col, this.internalMerges)\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n },\n\n ctxMerge() {\n this.hideContextMenu()\n const pr = this.primaryRange\n if (!pr) return\n this.mergeCells(normalizeRange(pr))\n },\n\n ctxUnmerge() {\n this.hideContextMenu()\n this.unmergeCells(this.activeCell.row, this.activeCell.col)\n },\n\n // =========================================================\n // GROUPING (3.7)\n // =========================================================\n groupRows(startIndex, endIndex) {\n this.internalGroups = addGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n groupColumns(startIndex, endIndex) {\n this.internalGroups = addGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupRows(startIndex, endIndex) {\n this.internalGroups = removeGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupColumns(startIndex, endIndex) {\n this.internalGroups = removeGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n toggleGroup(group) {\n this.internalCollapsedGroups = toggleGroupCollapse(group, this.internalCollapsedGroups)\n this.$emit('update:collapsedGroups', [...this.internalCollapsedGroups])\n this.$emit('group-toggle', { group, collapsed: _isGroupCollapsed(group, this.internalCollapsedGroups) })\n this.updateVirtualScroll()\n },\n\n isGroupCollapsed(group) {\n return _isGroupCollapsed(group, this.internalCollapsedGroups)\n },\n\n expandAll() {\n this.internalCollapsedGroups = []\n this.$emit('update:collapsedGroups', [])\n this.updateVirtualScroll()\n },\n\n collapseAll() {\n this.internalCollapsedGroups = [...this.internalGroups]\n this.$emit('update:collapsedGroups', [...this.internalGroups])\n this.updateVirtualScroll()\n },\n\n // =========================================================\n // DRAG AND DROP (3.9)\n // =========================================================\n startDragMove(e, ri, ci) {\n const pr = this.primaryRange\n if (!pr) return\n this.dragMove = {\n active: true,\n sourceRange: normalizeRange(pr),\n startX: e.clientX,\n startY: e.clientY,\n targetRow: ri,\n targetCol: ci,\n copyMode: e.ctrlKey || e.metaKey\n }\n this.$emit('cell-drag-start', { sourceRange: this.dragMove.sourceRange })\n e.preventDefault()\n },\n\n handleDragMoveMove(e) {\n this.dragMove.copyMode = e.ctrlKey || e.metaKey\n this.$emit('cell-drag-move', {\n sourceRange: this.dragMove.sourceRange,\n targetRow: this.dragMove.targetRow,\n targetCol: this.dragMove.targetCol\n })\n },\n\n async commitDragMove(e) {\n if (!this.dragMove.active) return\n const { sourceRange, targetRow, targetCol, copyMode } = this.dragMove\n\n const targetRange = {\n startRow: targetRow,\n startCol: targetCol,\n endRow: targetRow + (sourceRange.endRow - sourceRange.startRow),\n endCol: targetCol + (sourceRange.endCol - sourceRange.startCol)\n }\n\n let proceed = true\n if (this.beforeDrop) {\n proceed = await Promise.resolve(this.beforeDrop(sourceRange, targetRange))\n }\n\n let oldData = null\n let newData = null\n if (proceed) {\n oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n newData = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci)).newData\n\n if (!copyMode) {\n // Clear source — ensure each source row is a new copy before mutation\n for (let ri = sourceRange.startRow; ri <= sourceRange.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = sourceRange.startCol; ci <= sourceRange.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) newData[ri][col.key] = ''\n }\n }\n }\n this._emitDataChange(oldData, newData)\n this.$emit('cell-drop', { sourceRange, targetRange })\n }\n\n this.dragMove = { active: false, sourceRange: null, startX: 0, startY: 0, targetRow: -1, targetCol: -1, copyMode: false }\n this.isMouseDown = false\n\n if (proceed) {\n // See pasteAt's _fireOnSelectForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireOnSelectForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n // =========================================================\n // EXCEL EXPORT (3.11)\n // =========================================================\n async exportToExcel(options = {}) {\n const {\n filename = 'export.xlsx',\n sheetName = 'Sheet1',\n scope = 'all',\n includeHidden = false\n } = options\n\n this.$emit('export-start')\n this.exportState = { active: true }\n\n const workerPayload = {\n data: this.data,\n columns: this.columns,\n leafCols: this.leafCols,\n merges: this.internalMerges,\n hiddenCols: [...this.effectiveHiddenColSet],\n hiddenRows: [...this.hiddenRowSet],\n range: scope === 'selection' ? this.primaryRange : null,\n filename,\n sheetName,\n scope,\n includeHidden\n }\n\n try {\n const worker = createExcelExportWorker()\n if (worker) {\n await this._exportViaWorker(worker, workerPayload)\n } else {\n // Fallback: synchronous export on main thread\n _exportToExcel({\n ...workerPayload,\n hiddenColSet: this.effectiveHiddenColSet,\n hiddenRowSet: this.hiddenRowSet\n })\n }\n this.$emit('export-success')\n } catch (err) {\n this.$emit('export-error', err)\n } finally {\n this.exportState = { active: false }\n }\n },\n\n _exportViaWorker(worker, payload) {\n return new Promise((resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, buffer, filename, message } = e.data\n if (type === 'result') {\n // Trigger browser download from the main thread (DOM required)\n const blob = new Blob([new Uint8Array(buffer)], {\n type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n finish(resolve)\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Export worker xatosi'))\n\n worker.postMessage(payload)\n })\n },\n\n // =========================================================\n // EXCEL IMPORT (3.12)\n // =========================================================\n async importFromFile(file, options = {}) {\n this.importState = { active: true, percent: 5, message: \"Fayl o'qilmoqda...\" }\n\n try {\n let result\n const worker = createExcelImportWorker()\n\n if (worker) {\n result = await this._importViaWorker(worker, file, options)\n } else {\n // Fallback: chunked async on main thread\n result = await _importFromFile(\n file,\n this.columns,\n options,\n (percent, message) => { this.importState = { active: true, percent, message } }\n )\n }\n\n const { columns, merges } = result\n // Freeze each row so Vue 2 skips deep Object.defineProperty observation.\n // Vue 2 checks Object.isExtensible() — frozen objects are skipped entirely.\n // All edit paths already use spread ({ ...row }) so frozen rows are safe.\n const data = result.data.map(row => Object.freeze(row))\n this.$emit('input', data)\n this.$emit('update:columns', columns)\n\n const hasMerges = merges && merges.length > 0\n this.internalMerges = hasMerges ? merges : []\n this.$emit('update:merges', hasMerges ? merges : [])\n this.$emit('update:collapsedGroups', [])\n this.$emit('update:hiddenRows', [])\n this.$emit('import-success', { data, columns, merges })\n\n // Reset scroll state and re-derive virtual scroll after DOM settles.\n // Without this, stale scrollTop + old rowHeights cause blank bands on scroll.\n this.rowHeights = {}\n this.scrollTop = 0\n this.scrollLeft = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) {\n this.$refs.scrollBody.scrollTop = 0\n this.$refs.scrollBody.scrollLeft = 0\n }\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = 0\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = 0\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = 0\n this.updateVirtualScroll()\n })\n } catch (err) {\n this.$emit('import-error', err)\n } finally {\n this.importState = { active: false, percent: 0, message: '' }\n }\n },\n\n _importViaWorker(worker, file, options) {\n return new Promise(async (resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, percent, message, data, columns, merges } = e.data\n if (type === 'progress') {\n this.importState = { active: true, percent, message }\n } else if (type === 'result') {\n finish(resolve, { data, columns, merges })\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Import worker xatosi'))\n\n try {\n // Transfer the ArrayBuffer zero-copy into the worker\n const buffer = await file.arrayBuffer()\n worker.postMessage(\n { buffer, existingColumns: this.columns, options },\n [buffer]\n )\n } catch (err) {\n finish(reject, err)\n }\n })\n },\n\n // =========================================================\n // UNDO / REDO (3.13)\n // =========================================================\n\n // Push one entry onto the non-reactive history stack.\n // Truncates the redo tail, caps at 100 entries.\n _pushHistory(entry) {\n if (this._historyIndex < this._history.length - 1) {\n this._history.splice(this._historyIndex + 1)\n }\n this._history.push(entry)\n if (this._history.length > 100) {\n this._history.shift()\n // historyIndex stays the same — it now points to the new last entry\n } else {\n this._historyIndex++\n }\n this.historyVersion++\n },\n\n // Emit 'input' and record a data diff entry (only changed row indices).\n // Use slice() + per-row spread in callers so unchanged rows keep the\n // same reference — then the identity check here catches only real changes.\n _emitDataChange(oldData, newData) {\n if (!this._historySkip) {\n const indices = []\n const beforeRows = []\n const afterRows = []\n const len = Math.max(oldData.length, newData.length)\n for (let i = 0; i < len; i++) {\n if (oldData[i] !== newData[i]) {\n indices.push(i)\n beforeRows.push(oldData[i])\n afterRows.push(newData[i])\n }\n }\n if (indices.length > 0) {\n this._pushHistory({\n type: 'data',\n before: { indices, rows: beforeRows },\n after: { indices, rows: afterRows }\n })\n }\n }\n this.$emit('input', newData)\n },\n\n // Emit 'update:merges' and record a merges diff entry.\n _emitMergeChange(oldMerges, newMerges) {\n if (!this._historySkip) {\n this._pushHistory({\n type: 'merges',\n before: oldMerges,\n after: newMerges\n })\n }\n this.$emit('update:merges', newMerges)\n },\n\n undo() {\n if (this.internalLocked) return\n if (this._historyIndex < 0) return\n const entry = this._history[this._historyIndex--]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.before.indices.length; i++) {\n newData[entry.before.indices[i]] = entry.before.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.before.slice()\n this.$emit('update:merges', entry.before.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n redo() {\n if (this.internalLocked) return\n if (this._historyIndex >= this._history.length - 1) return\n const entry = this._history[++this._historyIndex]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.after.indices.length; i++) {\n newData[entry.after.indices[i]] = entry.after.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.after.slice()\n this.$emit('update:merges', entry.after.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n // =========================================================\n // TOOLBAR (fullscreen / lock / export / import / custom buttons)\n // =========================================================\n\n // Public, ref-callable: returns keyboard focus to the grid. Meant for\n // hosts whose action-cell `handler` opens a modal/popup outside the\n // grid's control — call this once that modal closes, otherwise the\n // browser leaves focus on document.body (its subtree was removed) and\n // arrow-key navigation goes dead until a cell is clicked again.\n focus() {\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n toggleLock() {\n this.internalLocked = !this.internalLocked\n this.$emit('update:locked', this.internalLocked)\n this.$emit('lock-change', { locked: this.internalLocked })\n },\n\n // Pops a big lock/unlock icon in the center of the grid and fades it\n // out — pure visual feedback, mirrors the video-player OSD pattern.\n // Triggered by the internalLocked watcher, so it covers every path that\n // can change the lock state (toolbar click, toggleLock(), :locked.sync).\n _flashLock(locked) {\n this.lockFlashLocked = locked\n this.lockFlashVisible = true\n this.$nextTick(() => {\n const el = this.$refs.lockFlashIconEl\n if (!el) return\n anime.remove(el)\n anime.set(el, { opacity: 0, scale: 0.5 })\n anime.timeline()\n .add({ targets: el, opacity: 1, scale: 1.12, duration: 180, easing: 'easeOutBack' })\n .add({ targets: el, scale: 1, duration: 120, easing: 'easeOutQuad' })\n .add({ targets: el, opacity: 0, duration: 350, easing: 'easeInQuad' }, '+=250')\n .finished.then(() => { this.lockFlashVisible = false })\n })\n },\n\n toolbarHas(key) {\n return this._toolbarList.indexOf(key) !== -1\n },\n\n setSize(newSize) {\n if (!SIZE_CONFIG[newSize] || newSize === this.internalSize) return\n this.internalSize = newSize\n this.$emit('update:size', newSize)\n this.$emit('size-change', { size: newSize })\n },\n\n setTheme(newTheme) {\n if (THEME_LIST.indexOf(newTheme) === -1 || newTheme === this.internalTheme) return\n this.internalTheme = newTheme\n this.$emit('update:theme', newTheme)\n this.$emit('theme-change', { theme: newTheme })\n },\n\n sizeTitle(key) {\n return { small: this.t.sizeSmall, middle: this.t.sizeMiddle, large: this.t.sizeLarge }[key] || key\n },\n\n toggleFullscreen() {\n this.isFullscreen = !this.isFullscreen\n if (this.isFullscreen) {\n this._prevBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n document.addEventListener('keydown', this._onFullscreenKeyDown)\n } else {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n this.$emit('fullscreen-change', { fullscreen: this.isFullscreen })\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n _onFullscreenKeyDown(e) {\n if (e.key === 'Escape' && this.isFullscreen) this.toggleFullscreen()\n },\n\n toolbarExportClick() {\n this.exportToExcel()\n },\n\n toolbarImportClick() {\n if (this.internalLocked) return\n const input = this.$refs.toolbarImportInput\n if (input) input.click()\n },\n\n onToolbarImportFile(e) {\n const file = e.target.files && e.target.files[0]\n e.target.value = ''\n if (!file || this.internalLocked) return\n this.importFromFile(file)\n },\n\n onToolbarButtonClick(btn, event) {\n if (typeof btn.onClick === 'function') btn.onClick(event, this)\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-shell {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n\n.eg-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n\n.eg-toolbar-group + .eg-toolbar-group {\n border-left: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-spacer {\n flex: 1 1 auto;\n}\n\n.eg-toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n\n.eg-toolbar-btn:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n\n.eg-toolbar-btn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.eg-toolbar-btn-active {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n\n.eg-toolbar-select {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n\n.eg-toolbar-select:hover {\n border-color: var(--eg-color-accent);\n}\n\n.eg-toolbar-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n\n.eg-toolbar-segmented {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-toolbar-segment {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon { width: 12px; height: 12px; }\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon { width: 15px; height: 15px; }\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon { width: 18px; height: 18px; }\n\n.eg-toolbar-icon {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n\n.eg-icon-undo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-redo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-export { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); }\n.eg-icon-import { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); }\n.eg-icon-lock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); }\n.eg-icon-unlock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); }\n.eg-icon-expand { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); }\n.eg-icon-compress{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); }\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-asc { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-desc{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); }\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-middle { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-large { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); }\n\n.eg-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n\n.eg-header-area {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n\n.eg-header-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n\n.eg-header-cell:hover { background: var(--eg-color-border-cell); }\n.eg-header-group { background: var(--eg-color-header-group-bg); }\n\n.eg-sort-icon {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon { opacity: 0.65; }\n.eg-sort-icon:hover { opacity: 1; }\n.eg-sort-icon.eg-icon-sort-asc,\n.eg-sort-icon.eg-icon-sort-desc {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty { background: var(--eg-color-header-bg); }\n\n.eg-filter-input,\n.eg-filter-select {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input:focus,\n.eg-filter-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n\n.eg-col-resize-handle {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n\n.eg-body-area {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area {\n border-top: 1px solid var(--eg-color-border);\n}\n\n.eg-frozen-col-footer,\n.eg-scroll-col-footer,\n.eg-pinned-right-footer {\n position: relative;\n}\n\n.eg-footer-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.eg-row {\n position: relative;\n}\n\n.eg-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n\n.eg-locked .eg-cell { cursor: not-allowed; }\n\n.eg-cell-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right { justify-content: flex-end; }\n.eg-cell-align-center { justify-content: center; }\n.eg-cell--has-suffix.eg-cell-select::after { display: none; }\n.eg-cell-select-trigger-wrap {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: var(--eg-color-cell-selected) !important;\n}\n\n.eg-cell-fill-preview {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n\n.eg-cell-merged-hidden {\n display: none !important;\n}\n\n.eg-cell-invalid {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n\n.eg-cell-input {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-row-resize-handle {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n\n.eg-fill-handle {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n\n.eg-context-item:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n\n.eg-context-separator {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n\n.eg-context-submenu-parent::after { content: ' ▶'; float: right; }\n\n.eg-context-submenu {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n\n.eg-context-submenu-parent:hover .eg-context-submenu { display: block; }\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-info-modal {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n\n.eg-info-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n\n.eg-info-modal-title {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n\n.eg-info-modal-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n\n.eg-info-modal-close:hover { color: var(--eg-color-text); }\n\n.eg-info-modal-body {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.eg-group-btn {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: var(--eg-color-group-bg-collapsed); }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source {\n opacity: 0.4;\n}\n.eg-col-drag-ghost {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n\n.eg-resize-line {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n\n.eg-pinned-right-body::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n\n.eg-lock-flash {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n\n.eg-import-dialog {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin 0.8s linear infinite;\n}\n@keyframes eg-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select {\n cursor: pointer;\n}\n.eg-cell-select::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap { justify-content: flex-end; }\n.eg-cell-align-center .eg-cell-action-wrap { justify-content: center; }\n.eg-validation-tooltip {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu,\n.eg-size-small .eg-context-item {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item {\n padding: 4px 14px;\n}\n.eg-size-small .eg-toolbar {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input,\n.eg-size-small .eg-filter-select {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu,\n.eg-size-large .eg-context-item {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item {\n padding: 8px 18px;\n}\n.eg-size-large .eg-toolbar {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input,\n.eg-size-large .eg-filter-select {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon {\n width: 13px;\n height: 13px;\n}\n</style>\n\n<style>\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent); }\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted); }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: var(--eg-color-text-muted); }\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px; }\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg); }\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent); }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg); }\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden; }\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent); }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell); }\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked); }\n</style>\n"]}, media: undefined });
|
|
13254
|
+
inject("data-v-ada16cee_0", { source: "\n.eg-shell[data-v-ada16cee] {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n.eg-shell-fullscreen[data-v-ada16cee] {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material[data-v-ada16cee] {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd[data-v-ada16cee] {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent[data-v-ada16cee] {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon[data-v-ada16cee] {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n.eg-wrapper[data-v-ada16cee] {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n.eg-toolbar[data-v-ada16cee] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n.eg-toolbar-group[data-v-ada16cee] {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n.eg-toolbar-group + .eg-toolbar-group[data-v-ada16cee] {\n border-left: 1px solid var(--eg-color-border);\n}\n.eg-toolbar-spacer[data-v-ada16cee] {\n flex: 1 1 auto;\n}\n.eg-toolbar-btn[data-v-ada16cee] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n.eg-toolbar-btn[data-v-ada16cee]:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n.eg-toolbar-btn[data-v-ada16cee]:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.eg-toolbar-btn-active[data-v-ada16cee] {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n.eg-toolbar-select[data-v-ada16cee] {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n.eg-toolbar-select[data-v-ada16cee]:hover {\n border-color: var(--eg-color-accent);\n}\n.eg-toolbar-select[data-v-ada16cee]:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-toolbar-segmented[data-v-ada16cee] {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n.eg-toolbar-segment[data-v-ada16cee] {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon[data-v-ada16cee] { width: 12px; height: 12px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon[data-v-ada16cee] { width: 15px; height: 15px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon[data-v-ada16cee] { width: 18px; height: 18px;\n}\n.eg-toolbar-icon[data-v-ada16cee] {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n.eg-icon-undo[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\");\n}\n.eg-icon-redo[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\");\n}\n.eg-icon-export[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\");\n}\n.eg-icon-import[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\");\n}\n.eg-icon-lock[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\");\n}\n.eg-icon-unlock[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\");\n}\n.eg-icon-expand[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\");\n}\n.eg-icon-compress[data-v-ada16cee]{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\");\n}\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\");\n}\n.eg-icon-sort-asc[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\");\n}\n.eg-icon-sort-desc[data-v-ada16cee]{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\");\n}\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\");\n}\n.eg-icon-size-middle[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\");\n}\n.eg-icon-size-large[data-v-ada16cee] { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\");\n}\n.eg-container[data-v-ada16cee] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-ada16cee] {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n.eg-header-cell[data-v-ada16cee] {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n.eg-header-cell[data-v-ada16cee]:hover { background: var(--eg-color-border-cell);\n}\n.eg-header-group[data-v-ada16cee] { background: var(--eg-color-header-group-bg);\n}\n.eg-sort-icon[data-v-ada16cee] {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon[data-v-ada16cee] { opacity: 0.65;\n}\n.eg-sort-icon[data-v-ada16cee]:hover { opacity: 1;\n}\n.eg-sort-icon.eg-icon-sort-asc[data-v-ada16cee],\n.eg-sort-icon.eg-icon-sort-desc[data-v-ada16cee] {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell[data-v-ada16cee] {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty[data-v-ada16cee] { background: var(--eg-color-header-bg);\n}\n.eg-filter-input[data-v-ada16cee],\n.eg-filter-select[data-v-ada16cee] {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input[data-v-ada16cee]:focus,\n.eg-filter-select[data-v-ada16cee]:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select[data-v-ada16cee] {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n.eg-col-resize-handle[data-v-ada16cee] {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n.eg-body-area[data-v-ada16cee] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-ada16cee] {\n border-top: 1px solid var(--eg-color-border);\n}\n.eg-frozen-col-footer[data-v-ada16cee],\n.eg-scroll-col-footer[data-v-ada16cee],\n.eg-pinned-right-footer[data-v-ada16cee] {\n position: relative;\n}\n.eg-footer-cell[data-v-ada16cee] {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n/* Cells that actually show a computed total (col.showTotal / totalConfig.visible)\n stand out from the rest of the (otherwise mostly-empty) footer row. */\n.eg-footer-cell-total[data-v-ada16cee] {\n font-weight: 700;\n}\n.eg-row[data-v-ada16cee] {\n position: relative;\n}\n.eg-cell[data-v-ada16cee] {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n.eg-locked .eg-cell[data-v-ada16cee] { cursor: not-allowed;\n}\n.eg-cell-value[data-v-ada16cee] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right[data-v-ada16cee] { justify-content: flex-end;\n}\n.eg-cell-align-center[data-v-ada16cee] { justify-content: center;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-ada16cee]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-ada16cee] {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline[data-v-ada16cee] {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap[data-v-ada16cee] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-ada16cee] {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-ada16cee] {\n background: var(--eg-color-cell-selected) !important;\n}\n.eg-cell-fill-preview[data-v-ada16cee] {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n.eg-cell-copy[data-v-ada16cee] {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-ada16cee] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-ada16cee] {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-ada16cee] {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n.eg-row-header-cell[data-v-ada16cee] {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.eg-row-resize-handle[data-v-ada16cee] {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n.eg-fill-handle[data-v-ada16cee] {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n.eg-context-menu[data-v-ada16cee] {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n\n/* Only 3 short items (\"A → Z\" / \"Z → A\" / \"Clear sort\") — the generic\n 160px context-menu min-width looks oversized next to it, especially\n at size=\"small\" where rows/text are much smaller. */\n.eg-sort-menu[data-v-ada16cee] {\n min-width: 110px;\n}\n.eg-context-item[data-v-ada16cee] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n.eg-context-item[data-v-ada16cee]:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n.eg-context-separator[data-v-ada16cee] {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-ada16cee]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-ada16cee] {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n.eg-context-submenu-parent:hover .eg-context-submenu[data-v-ada16cee] { display: block;\n}\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop[data-v-ada16cee] {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.eg-info-modal[data-v-ada16cee] {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n.eg-info-modal-header[data-v-ada16cee] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n.eg-info-modal-title[data-v-ada16cee] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n.eg-info-modal-close[data-v-ada16cee] {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n.eg-info-modal-close[data-v-ada16cee]:hover { color: var(--eg-color-text);\n}\n.eg-info-modal-body[data-v-ada16cee] {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n.eg-group-btn[data-v-ada16cee] {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n.eg-group-btn.collapsed[data-v-ada16cee] { background: var(--eg-color-group-bg-collapsed);\n}\n.eg-drag-ghost[data-v-ada16cee] {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source[data-v-ada16cee] {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source[data-v-ada16cee] {\n opacity: 0.4;\n}\n.eg-col-drag-ghost[data-v-ada16cee] {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n.eg-resize-line[data-v-ada16cee] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-ada16cee]::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-body[data-v-ada16cee] {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n.eg-pinned-right-body[data-v-ada16cee]::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-header[data-v-ada16cee] {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n.eg-import-overlay[data-v-ada16cee] {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n.eg-lock-flash[data-v-ada16cee] {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon[data-v-ada16cee] {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n.eg-import-dialog[data-v-ada16cee] {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n.eg-import-title[data-v-ada16cee] {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n.eg-import-bar-wrap[data-v-ada16cee] {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n.eg-import-bar[data-v-ada16cee] {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n.eg-import-msg[data-v-ada16cee] {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-ada16cee] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner[data-v-ada16cee] {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin-data-v-ada16cee 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-ada16cee {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-ada16cee] {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-ada16cee] {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active[data-v-ada16cee] {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-ada16cee] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-ada16cee]::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox[data-v-ada16cee] {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap[data-v-ada16cee] {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap[data-v-ada16cee] { justify-content: flex-end;\n}\n.eg-cell-align-center .eg-cell-action-wrap[data-v-ada16cee] { justify-content: center;\n}\n.eg-validation-tooltip[data-v-ada16cee] {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell[data-v-ada16cee]:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell[data-v-ada16cee]:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell[data-v-ada16cee] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-ada16cee] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-ada16cee] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-ada16cee] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-ada16cee] {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu[data-v-ada16cee],\n.eg-size-small .eg-context-item[data-v-ada16cee] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-ada16cee] {\n padding: 4px 14px;\n}\n.eg-size-small .eg-sort-menu[data-v-ada16cee] {\n min-width: 90px;\n}\n.eg-size-small .eg-toolbar[data-v-ada16cee] {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn[data-v-ada16cee] {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon[data-v-ada16cee] {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon[data-v-ada16cee] {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input[data-v-ada16cee],\n.eg-size-small .eg-filter-select[data-v-ada16cee] {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon[data-v-ada16cee] {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-ada16cee] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-ada16cee] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-ada16cee] {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell[data-v-ada16cee] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-ada16cee] {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu[data-v-ada16cee],\n.eg-size-large .eg-context-item[data-v-ada16cee] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-ada16cee] {\n padding: 8px 18px;\n}\n.eg-size-large .eg-sort-menu[data-v-ada16cee] {\n min-width: 130px;\n}\n.eg-size-large .eg-toolbar[data-v-ada16cee] {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn[data-v-ada16cee] {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon[data-v-ada16cee] {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon[data-v-ada16cee] {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input[data-v-ada16cee],\n.eg-size-large .eg-filter-select[data-v-ada16cee] {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon[data-v-ada16cee] {\n width: 13px;\n height: 13px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AAmmKA;EACA;;GAEA;EACA,mEAAA;;EAEA,uBAAA;EACA,sEAAA;EACA,kCAAA;EACA,kCAAA;;EAEA,kBAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,iCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qDAAA;;EAEA,+BAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,mBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,sCAAA;;EAEA,wBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,0CAAA;EACA,qCAAA;;EAEA,iBAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oBAAA;EACA,sDAAA;EACA,qDAAA;EACA,oDAAA;EACA,6CAAA;;EAEA,uCAAA;EACA,4DAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qBAAA;EACA,6CAAA;EACA,6CAAA;;EAEA,yCAAA;EACA,kCAAA;EACA,kCAAA;;EAEA,sBAAA;EACA,kCAAA;EACA,wCAAA;EACA,+BAAA;EACA,gBAAA;AACA;AAEA;EACA,eAAA;EACA,QAAA;EACA,aAAA;EACA,mCAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;EACA,gBAAA;AACA;;AAEA,sEAAA;AACA;EACA,wEAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,8DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,+FAAA;EACA,8DAAA;EACA,6DAAA;EACA,mDAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,yBAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,oEAAA;AACA;EACA,wHAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,+DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,8CAAA;EACA,8CAAA;EACA,8CAAA;;EAEA,sHAAA;EACA,yDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,qEAAA;AACA;EACA,oEAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,iCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,oDAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,8FAAA;EACA,yDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,uBAAA;EACA,qCAAA;EACA,qCAAA;AACA;;AAEA,qEAAA;AACA;EACA,kFAAA;EACA,kCAAA;EACA,kCAAA;EACA,iCAAA;EACA,kCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,yDAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0DAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,qCAAA;EACA,qCAAA;EACA,qCAAA;;EAEA,wDAAA;EACA,wDAAA;EACA,wDAAA;EACA,8CAAA;;EAEA,yDAAA;EACA,0CAAA;EACA,0CAAA;;EAEA,6CAAA;EACA,6CAAA;;EAEA,4CAAA;EACA,qCAAA;EACA,qCAAA;AACA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,gBAAA;EACA,sBAAA;EACA,kCAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,cAAA;EACA,YAAA;EACA,cAAA;EACA,sBAAA;EACA,mCAAA;EACA,+CAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,cAAA;AACA;AAEA;EACA,6CAAA;AACA;AAEA;EACA,cAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,UAAA;EACA,6BAAA;EACA,kBAAA;EACA,uBAAA;EACA,eAAA;EACA,qCAAA;AACA;AAEA;EACA,2CAAA;EACA,oCAAA;AACA;AAEA;EACA,YAAA;EACA,eAAA;AACA;AAEA;EACA,uCAAA;EACA,oCAAA;EACA,kCAAA;AACA;AAEA;EACA,YAAA;EACA,qBAAA;EACA,wCAAA;EACA,kBAAA;EACA,yCAAA;EACA,sLAAA;EACA,4BAAA;EACA,qCAAA;EACA,wBAAA;EACA,qCAAA;EACA,eAAA;EACA,kCAAA;EACA,eAAA;EACA,gBAAA;EACA,wBAAA;EACA,aAAA;AACA;AAEA;EACA,oCAAA;AACA;AAEA;EACA,oCAAA;EACA,iDAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,wCAAA;EACA,kBAAA;EACA,gBAAA;EACA,YAAA;EACA,QAAA;EACA,mCAAA;AACA;AAEA;EACA,kBAAA;AACA;;AAEA;gFACA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AACA,qFAAA,WAAA,EAAA,YAAA;AAAA;AAEA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,8BAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;AACA;AAEA,oCAAA,gRAAA,EAAA,wRAAA;AAAA;AACA,oCAAA,mRAAA,EAAA,2RAAA;AAAA;AACA,oCAAA,2SAAA,EAAA,mTAAA;AAAA;AACA,oCAAA,4SAAA,EAAA,oTAAA;AAAA;AACA,oCAAA,2SAAA,EAAA,mTAAA;AAAA;AACA,oCAAA,0SAAA,EAAA,kTAAA;AAAA;AACA,oCAAA,kTAAA,EAAA,0TAAA;AAAA;AACA,oCAAA,kTAAA,EAAA,0TAAA;AAAA;;AAEA,2HAAA;AACA,qCAAA,+KAAA,EAAA,uLAAA;AAAA;AACA,qCAAA,4IAAA,EAAA,oJAAA;AAAA;AACA,qCAAA,+IAAA,EAAA,uJAAA;AAAA;;AAEA,2GAAA;AACA,wCAAA,8QAAA,EAAA,sRAAA;AAAA;AACA,wCAAA,gRAAA,EAAA,wRAAA;AAAA;AACA,wCAAA,oRAAA,EAAA,4RAAA;AAAA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,OAAA;EACA,cAAA;EACA,kBAAA;AACA;AAEA;EACA,aAAA;EACA,cAAA;EACA,iDAAA;EACA,kBAAA;EACA,WAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,sBAAA;EACA,8CAAA;EACA,+CAAA;EACA,qCAAA;EACA,qCAAA;EACA,gBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;EACA,iBAAA;EACA,eAAA;AACA;AAEA,yCAAA,uCAAA;AAAA;AACA,oCAAA,2CAAA;AAAA;AAEA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;EACA,sBAAA;EACA,cAAA;EACA,8BAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;EACA,eAAA;EACA,YAAA;AACA;AACA,uDAAA,aAAA;AAAA;AACA,uCAAA,UAAA;AAAA;AACA;;EAEA,UAAA;EACA,6BAAA;AACA;;AAEA,2BAAA;AACA;EACA,sBAAA;EACA,gBAAA;EACA,mDAAA;EACA,+CAAA;EACA,qCAAA;EACA,kBAAA;EACA,aAAA;EACA,mBAAA;AACA;AACA,yCAAA,qCAAA;AAAA;AAEA;;EAEA,WAAA;EACA,YAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,mCAAA;EACA,2BAAA;EACA,qCAAA;EACA,kCAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;AACA;AACA;;EAEA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,mBAAA;EACA,sLAAA;EACA,4BAAA;EACA,qCAAA;EACA,wBAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA;EACA,kBAAA;EACA,UAAA;AACA;AAEA;EACA,aAAA;EACA,OAAA;EACA,gBAAA;AACA;;AAEA,4CAAA;AACA;EACA,4CAAA;AACA;AAEA;;;EAGA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,mDAAA;EACA,gBAAA;EACA,mBAAA;EACA,8BAAA;EACA,gBAAA;EACA,mCAAA;EACA,kBAAA;EACA,MAAA;EACA,qCAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;AACA;;AAEA;wEACA;AACA;EACA,gBAAA;AACA;AAEA;EACA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,mDAAA;EACA,oDAAA;EACA,gBAAA;EACA,mBAAA;EACA,8BAAA;EACA,mCAAA;EACA,kBAAA;EACA,MAAA;EACA,mCAAA;EACA,eAAA;EACA,aAAA;EACA,mBAAA;AACA;AAEA,uCAAA,mBAAA;AAAA;AAEA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,YAAA;AACA;AACA,yCAAA,yBAAA;AAAA;AACA,yCAAA,uBAAA;AAAA;AACA,8DAAA,aAAA;AAAA;AACA;EACA,OAAA;EACA,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,YAAA;AACA;AACA;EACA,cAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;AACA;AACA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;AACA;AACA;EACA,oDAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,oDAAA;AACA;AAEA;EACA,wDAAA;EACA,+CAAA;EACA,oBAAA;AACA;AAEA;EACA,0CAAA;EACA,oBAAA;AACA;AAEA;EACA,wBAAA;AACA;AAEA;EACA,mDAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,WAAA;EACA,YAAA;EACA,YAAA;EACA,aAAA;EACA,mCAAA;EACA,8BAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,sBAAA;EACA,qCAAA;EACA,oDAAA;EACA,qDAAA;EACA,kBAAA;EACA,eAAA;EACA,qCAAA;EACA,eAAA;EACA,iBAAA;EACA,kBAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;AACA;AAEA;EACA,kBAAA;EACA,SAAA;EACA,OAAA;EACA,QAAA;EACA,WAAA;EACA,kBAAA;EACA,UAAA;AACA;AAEA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,kCAAA;EACA,uBAAA;EACA,iBAAA;EACA,WAAA;AACA;AAEA;EACA,eAAA;EACA,4DAAA;EACA,wCAAA;EACA,4CAAA;EACA,+BAAA;EACA,qCAAA;EACA,aAAA;EACA,gBAAA;EACA,8BAAA;EACA,cAAA;EACA,2BAAA;AACA;;AAEA;;sDAEA;AACA;EACA,gBAAA;AACA;AAEA;EACA,iBAAA;EACA,eAAA;EACA,kBAAA;EACA,mBAAA;AACA;AAEA;EACA,2CAAA;EACA,2BAAA;AACA;AAEA;EACA,WAAA;EACA,uCAAA;EACA,aAAA;AACA;AAEA,qDAAA,aAAA,EAAA,YAAA;AAAA;AAEA;EACA,aAAA;EACA,kBAAA;EACA,UAAA;EACA,SAAA;EACA,4DAAA;EACA,wCAAA;EACA,4CAAA;EACA,+BAAA;EACA,qCAAA;EACA,gBAAA;EACA,cAAA;EACA,cAAA;EACA,2BAAA;AACA;AAEA,wEAAA,cAAA;AAAA;;AAEA,oCAAA;AACA;EACA,eAAA;EACA,QAAA;EACA,cAAA;EACA,+BAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;AACA;AAEA;EACA,mCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,UAAA;EACA,gBAAA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,kCAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,8BAAA;EACA,SAAA;EACA,kBAAA;EACA,+CAAA;EACA,cAAA;AACA;AAEA;EACA,8BAAA;EACA,gBAAA;EACA,2BAAA;AACA;AAEA;EACA,gBAAA;EACA,YAAA;EACA,eAAA;EACA,eAAA;EACA,cAAA;EACA,qCAAA;EACA,gBAAA;AACA;AAEA,8CAAA,2BAAA;AAAA;AAEA;EACA,SAAA;EACA,aAAA;EACA,cAAA;EACA,OAAA;EACA,4EAAA;EACA,eAAA;EACA,2BAAA;EACA,qBAAA;EACA,sBAAA;AACA;AAEA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,2CAAA;EACA,8CAAA;EACA,kBAAA;EACA,UAAA;EACA,iBAAA;AACA;AAEA,2CAAA,8CAAA;AAAA;AAEA;EACA,kBAAA;EACA,yCAAA;EACA,oBAAA;EACA,WAAA;AACA;;AAEA,yBAAA;AACA;EACA,YAAA;EACA,yCAAA;EACA,oBAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,eAAA;EACA,iBAAA;EACA,qCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,qCAAA;EACA,gBAAA;EACA,2BAAA;EACA,mBAAA;EACA,oBAAA;EACA,aAAA;EACA,aAAA;EACA,2BAAA;AACA;AAEA;EACA,oBAAA;EACA,aAAA;AACA;;AAEA,8BAAA;AACA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,iFAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,QAAA;EACA,WAAA;EACA,mCAAA;EACA,cAAA;AACA;AAEA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,UAAA;EACA,SAAA;EACA,UAAA;EACA,gFAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,cAAA;EACA,oDAAA;EACA,qCAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,sCAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,YAAA;EACA,+BAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,oBAAA;EACA,WAAA;AACA;AACA;EACA,qBAAA;EACA,WAAA;EACA,YAAA;EACA,sCAAA;EACA,kBAAA;EACA,sBAAA;EACA,qBAAA;EACA,0BAAA;EACA,8BAAA;EACA,6BAAA;EACA,mDAAA;AACA;AAEA;EACA,mCAAA;EACA,6CAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,mCAAA;EACA,kBAAA;AACA;AAEA;EACA,eAAA;EACA,gBAAA;EACA,2BAAA;EACA,mBAAA;AACA;AAEA;EACA,WAAA;EACA,uCAAA;EACA,+BAAA;EACA,gBAAA;EACA,mBAAA;AACA;AAEA;EACA,YAAA;EACA,sEAAA;EACA,+BAAA;EACA,4BAAA;AACA;AAEA;EACA,eAAA;EACA,qCAAA;EACA,kBAAA;AACA;AAEA;EACA,8BAAA;EACA,gBAAA;EACA,6BAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;EACA,mBAAA;EACA,6CAAA;EACA,wCAAA;EACA,kBAAA;EACA,uDAAA;AACA;AACA;AACA,KAAA,yBAAA;AAAA;AACA;;AAEA,8BAAA;AACA;EACA,yCAAA;EACA,eAAA;AACA;AACA;EACA,kDAAA;AACA;AACA;EACA,yCAAA;AACA;;AAEA,gCAAA;AACA;EACA,eAAA;AACA;AACA;EACA,YAAA;EACA,kBAAA;EACA,UAAA;EACA,QAAA;EACA,2BAAA;EACA,iCAAA;EACA,eAAA;EACA,oBAAA;EACA,cAAA;AACA;;AAEA,8BAAA;AACA;EACA,uBAAA;EACA,cAAA;AACA;;AAEA,4BAAA;AACA;EACA,aAAA;EACA,mBAAA;EACA,YAAA;EACA,WAAA;EACA,eAAA;AACA;AACA;;6EAEA;AACA,6DAAA,yBAAA;AAAA;AACA,8DAAA,uBAAA;AAAA;AACA;EACA,sCAAA;EACA,uCAAA;EACA,iCAAA;EACA,eAAA;EACA,gBAAA;EACA,+BAAA;EACA,oCAAA;EACA,mBAAA;EACA,oBAAA;AACA;;AAEA,mCAAA;AACA;EACA,sCAAA;AACA;AACA;EACA,qCAAA;AACA;;AAEA,8BAAA;;AAEA,UAAA;AACA;EACA,eAAA;EACA,cAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,iBAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,UAAA;EACA,WAAA;AACA;;AAEA,UAAA;AACA;EACA,eAAA;EACA,cAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,8BAAA;AACA;AACA;EACA,eAAA;AACA;AACA;EACA,eAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,iBAAA;AACA;AACA;EACA,gBAAA;AACA;AACA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA;AACA;EACA,YAAA;EACA,aAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;AACA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + internalTheme, { 'eg-shell-fullscreen': isFullscreen, 'eg-locked': internalLocked }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"_toolbarVisible\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canUndo\"\n :title=\"t.undo\"\n @click=\"undo\"\n ><span class=\"eg-toolbar-icon eg-icon-undo\"></span></button>\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canRedo\"\n :title=\"t.redo\"\n @click=\"redo\"\n ><span class=\"eg-toolbar-icon eg-icon-redo\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('export')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"t.export\"\n @click=\"toolbarExportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-export\"></span></button>\n <button\n v-if=\"toolbarHas('import')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked\"\n :title=\"t.import\"\n @click=\"toolbarImportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-import\"></span></button>\n <input\n v-if=\"toolbarHas('import')\"\n ref=\"toolbarImportInput\"\n type=\"file\"\n accept=\".xlsx,.xls,.csv\"\n style=\"display: none\"\n @change=\"onToolbarImportFile\"\n >\n </div>\n\n <slot name=\"toolbar-extra\" :locked=\"internalLocked\" :fullscreen=\"isFullscreen\"></slot>\n\n <div v-if=\"toolbarButtons.length\" class=\"eg-toolbar-group\">\n <button\n v-for=\"btn in toolbarButtons\"\n :key=\"btn.key\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"btn.title\"\n @click=\"onToolbarButtonClick(btn, $event)\"\n ><span class=\"eg-toolbar-icon\" :style=\"btn.icon ? { maskImage: 'url(' + btn.icon + ')', webkitMaskImage: 'url(' + btn.icon + ')' } : null\">{{ btn.icon ? '' : (btn.label || '') }}</span></button>\n </div>\n\n <div class=\"eg-toolbar-spacer\"></div>\n\n <div v-if=\"toolbarHas('theme')\" class=\"eg-toolbar-group\">\n <select\n class=\"eg-toolbar-select\"\n :value=\"internalTheme\"\n :title=\"t.theme\"\n @change=\"setTheme($event.target.value)\"\n >\n <option value=\"excel\">Excel</option>\n <option value=\"material\">Material</option>\n <option value=\"antd\">Ant Design</option>\n <option value=\"fluent\">Fluent</option>\n <option value=\"carbon\">Carbon</option>\n </select>\n </div>\n\n <div v-if=\"toolbarHas('size')\" class=\"eg-toolbar-segmented\" role=\"radiogroup\" :aria-label=\"t.size\">\n <button\n v-for=\"key in sizeOptions\"\n :key=\"key\"\n type=\"button\"\n role=\"radio\"\n class=\"eg-toolbar-btn eg-toolbar-segment\"\n :class=\"['eg-toolbar-segment-' + key, { 'eg-toolbar-btn-active': internalSize === key }]\"\n :aria-checked=\"internalSize === key\"\n :title=\"sizeTitle(key)\"\n @click=\"setSize(key)\"\n ><span class=\"eg-toolbar-icon\" :class=\"'eg-icon-size-' + key\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('lock')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': internalLocked }\"\n :title=\"internalLocked ? t.unlock : t.lock\"\n @click=\"toggleLock\"\n ><span class=\"eg-toolbar-icon\" :class=\"internalLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"></span></button>\n <button\n v-if=\"toolbarHas('fullscreen')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': isFullscreen }\"\n :title=\"isFullscreen ? t.exitFullscreen : t.fullscreen\"\n @click=\"toggleFullscreen\"\n ><span class=\"eg-toolbar-icon\" :class=\"isFullscreen ? 'eg-icon-compress' : 'eg-icon-expand'\"></span></button>\n </div>\n </div>\n\n <div\n class=\"eg-wrapper\"\n :class=\"'eg-size-' + internalSize\"\n :style=\"wrapperStyle\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n @keypress=\"onKeyPress\"\n ref=\"wrapper\"\n >\n <!-- Context menu -->\n <div\n v-if=\"contextMenu.visible\"\n key=\"eg-context-menu\"\n ref=\"contextMenuEl\"\n class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: (contextMenuPositioned ? contextMenu.x : -9999) + 'px', visibility: contextMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"ctxCopy\">{{ t.copy }}</div>\n <div class=\"eg-context-item\" @click=\"ctxCut\">{{ t.cut }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPaste\">{{ t.paste }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item eg-context-submenu-parent\">\n {{ t.pasteSpecial }}\n <div class=\"eg-context-submenu\">\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all')\">{{ t.pasteAll }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('values')\">{{ t.pasteValues }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('formats')\">{{ t.pasteFormats }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all', true)\">{{ t.transpose }}</div>\n </div>\n </div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxMerge\">{{ t.mergeCells }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnmerge\">{{ t.unmergeCells }}</div>\n <div class=\"eg-context-separator\"></div>\n <div v-if=\"contextMenu.row >= 0\" class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div v-if=\"contextMenu.col >= 0\" class=\"eg-context-item\" @click=\"ctxHideColumn\">{{ t.hideColumns }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideRows\">{{ t.unhideRows }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideColumns\">{{ t.unhideColumns }}</div>\n <template v-if=\"contextMenu.row < 0 && contextMenu.col >= 0\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxPinLeft\">{{ t.pinLeft }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPinRight\">{{ t.pinRight }}</div>\n <div class=\"eg-context-item\" v-if=\"isColPinned(contextMenu.col)\" @click=\"ctxUnpin\">{{ t.unpin }}</div>\n </template>\n <template v-if=\"devMode && (contextMenu.col >= 0 || contextMenu.row >= 0)\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.col >= 0\" @click=\"ctxShowColumnInfo\">{{ t.columnInfo }}</div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.row >= 0\" @click=\"ctxShowRowInfo\">{{ t.rowInfo }}</div>\n </template>\n </div>\n\n <!-- Sort popup menu (header sort icon) -->\n <div\n v-if=\"sortMenu.visible\"\n key=\"eg-sort-menu\"\n ref=\"sortMenuEl\"\n class=\"eg-context-menu eg-sort-menu\"\n :style=\"{ top: sortMenu.y + 'px', left: (sortMenuPositioned ? sortMenu.x : -9999) + 'px', visibility: sortMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"sortMenuPickAsc\">{{ t.sortMenuAsc }}</div>\n <div class=\"eg-context-item\" @click=\"sortMenuPickDesc\">{{ t.sortMenuDesc }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"sortMenuPickCancel\">{{ t.sortMenuCancel }}</div>\n </div>\n\n <!-- devMode: column/row info modal -->\n <div v-if=\"infoModal\" key=\"eg-info-modal-backdrop\" class=\"eg-info-modal-backdrop\" @click=\"closeInfoModal\">\n <div class=\"eg-info-modal\" @click.stop>\n <div class=\"eg-info-modal-header\">\n <span class=\"eg-info-modal-title\">{{ infoModal.title }}</span>\n <button type=\"button\" class=\"eg-info-modal-close\" @click=\"closeInfoModal\">×</button>\n </div>\n <pre class=\"eg-info-modal-body\">{{ infoModal.json }}</pre>\n </div>\n </div>\n\n <!-- Scroll container -->\n <div class=\"eg-container\" ref=\"container\" @scroll=\"onScroll\" @click=\"hideFloatingMenus\">\n\n <!-- ===== HEADER AREA ===== -->\n <div class=\"eg-header-area\" :style=\"headerAreaStyle\">\n\n <!-- Corner cell -->\n <div\n class=\"eg-corner\"\n :style=\"cornerStyle\"\n @click=\"selectAll\"\n ></div>\n\n <!-- Row group outline gutter (left) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-gutter\" :style=\"rowOutlineGutterStyle\"></div>\n\n <!-- Frozen columns header -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-header\" :style=\"frozenColHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, true)\">\n <div\n :key=\"'fh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, true)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'ffilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable columns header -->\n <div class=\"eg-scroll-col-header\" ref=\"scrollColHeader\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-header-inner\" :style=\"{ width: scrollContentWidth + 'px', height: (headerRowHeight * headerDepth + _filterRowH) + 'px', position: 'relative' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, false)\">\n <div\n :key=\"'sh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, false)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'sfilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns header -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-header\" :style=\"pinnedRightHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellInZone(cell, rowIdx, 'right')\">\n <div\n :key=\"'prh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyleForZone(cell, rowIdx, 'right')\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prfilter-' + col._flatIndex\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(col._flatIndex)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- ===== BODY AREA ===== -->\n <div class=\"eg-body-area\" :style=\"bodyAreaStyle\">\n\n <!-- Row number header column -->\n <div class=\"eg-row-header-col\" :style=\"rowHeaderColStyle\">\n <!-- Scrollable row headers -->\n <div class=\"eg-scroll-row-headers\" :style=\"scrollRowHeadersStyle\" ref=\"scrollRowHeaders\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'srh-' + ri\"\n class=\"eg-row-header-cell\"\n :style=\"getRowHeaderStyle(ri)\"\n @mousedown=\"onRowHeaderMouseDown($event, ri)\"\n @contextmenu.prevent=\"onRowContextMenu($event, ri)\"\n >\n {{ ri + 1 }}\n <div class=\"eg-row-resize-handle\" @mousedown.stop=\"startRowResize($event, ri)\"></div>\n </div>\n </template>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Row group outline gutter (left side of rows) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-col\" :style=\"rowOutlineColStyle\">\n <template v-for=\"group in rowGroups\">\n <div\n :key=\"'rg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getRowGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n <!-- Main scroll body -->\n <div class=\"eg-scroll-body\" ref=\"scrollBody\" :style=\"scrollBodyStyle\" @scroll=\"onBodyScroll\">\n <div class=\"eg-scroll-body-inner\" :style=\"{ width: scrollContentWidth + 'px', height: totalVirtualHeight + 'px', position: 'relative' }\">\n\n <!-- Pinned left column body -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-body\" :style=\"frozenColBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'fc-cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @input=\"onEditInput\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Main data cells -->\n <div class=\"eg-main-cells\" :style=\"mainCellsStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative', width: scrollableContentWidth + 'px' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\n :class=\"{ 'eg-row--even': ri % 2 === 0 }\"\n :style=\"getRowStyle(ri)\"\n :data-row=\"ri\"\n >\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input\n v-else\n ref=\"editInput\"\n class=\"eg-cell-input\"\n v-model=\"editValue\"\n @input=\"onEditInput\"\n @blur=\"commitEdit\"\n @keydown.stop=\"onEditKeyDown\"\n />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Pinned Right column body -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-body\" :style=\"pinnedRightBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\n :data-fi=\"col._flatIndex\"\n :class=\"getCellClasses(ri, col._flatIndex)\"\n :style=\"getCellStyle(ri, col._flatIndex)\"\n @mousedown=\"onCellMouseDown($event, ri, col._flatIndex)\"\n @mouseover=\"onCellMouseOver($event, ri, col._flatIndex)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, col._flatIndex)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, col._flatIndex)\"\n >\n <div v-if=\"isMergedAway(ri, col._flatIndex)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, col._flatIndex)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, col._flatIndex)\"\n :disabled=\"!isCellEditable(ri, col._flatIndex)\"\n @change=\"(v) => setCheckboxValue(ri, col._flatIndex, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, col._flatIndex, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span></template><template v-else>{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Fill handle (shown on primary range) -->\n <div\n v-if=\"fillHandleVisible\"\n class=\"eg-fill-handle\"\n :style=\"fillHandleStyle\"\n @mousedown.stop=\"startFillDrag\"\n ></div>\n\n <!-- Drag move ghost border -->\n <div\n v-if=\"dragMove.active\"\n class=\"eg-drag-ghost\"\n :style=\"dragGhostStyle\"\n ></div>\n </div>\n </div>\n\n <!-- Column group outline row (top, above body) -->\n <div v-if=\"colGroups.length\" class=\"eg-col-outline-row\" :style=\"colOutlineRowStyle\">\n <template v-for=\"group in colGroups\">\n <div\n :key=\"'cg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getColGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n </div><!-- end body-area -->\n\n <!-- ===== FOOTER AREA (column totals) ===== -->\n <div v-if=\"hasFooter\" class=\"eg-footer-area\" :style=\"footerAreaStyle\">\n\n <!-- \"Jami\" label cell (mirrors corner cell) -->\n <div class=\"eg-footer-label\" :style=\"footerLabelStyle\">{{ t.total }}</div>\n\n <!-- Frozen columns footer -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-footer\" :style=\"frozenColFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div :key=\"'ff-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable footer (horizontally synced with body via scrollFooter ref) -->\n <div class=\"eg-scroll-col-footer\" ref=\"scrollFooter\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-footer-inner\" :style=\"{ width: scrollContentWidth + 'px', height: footerRowHeight + 'px', position: 'relative' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div :key=\"'sf-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns footer -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-footer\" :style=\"pinnedRightFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prf-' + col._flatIndex\"\n class=\"eg-footer-cell\"\n :class=\"getFooterCellClass(col._flatIndex)\"\n :style=\"getFooterCellStyle(col._flatIndex)\"\n >{{ getColumnTotalDisplay(col._flatIndex) }}</div>\n </div>\n </div>\n </div><!-- end footer-area -->\n\n <!-- Resize indicator line -->\n <div v-if=\"resizing.active\" class=\"eg-resize-line\" :style=\"resizeLineStyle\"></div>\n </div><!-- end container -->\n\n <!-- Import progress overlay -->\n <div v-if=\"importState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl import qilinmoqda</div>\n <div class=\"eg-import-bar-wrap\">\n <div class=\"eg-import-bar\" :style=\"{ width: importState.percent + '%' }\"></div>\n </div>\n <div class=\"eg-import-msg\">{{ importState.message }}</div>\n <div class=\"eg-import-pct\">{{ importState.percent }}%</div>\n </div>\n </div>\n\n <!-- Export progress overlay -->\n <div v-if=\"exportState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl export qilinmoqda...</div>\n <div class=\"eg-export-spinner\"></div>\n </div>\n </div>\n\n <!-- Lock/unlock state feedback — brief centered icon flash -->\n <div v-if=\"lockFlashVisible\" class=\"eg-lock-flash\" aria-hidden=\"true\">\n <span\n ref=\"lockFlashIconEl\"\n class=\"eg-lock-flash-icon\"\n :class=\"lockFlashLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"\n ></span>\n </div>\n\n <!-- Select dropdown overlay (flat select) -->\n <eg-select\n v-if=\"selectDropdown && !selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n <!-- Tree select dropdown overlay -->\n <eg-tree-select\n v-if=\"selectDropdown && selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :value-key=\"selectDropdown.keys.valueKey\"\n :label-key=\"selectDropdown.keys.labelKey\"\n :children-key=\"selectDropdown.keys.childrenKey\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n\n <!-- Date picker overlay -->\n <eg-date-picker\n v-if=\"datePickerOverlay\"\n ref=\"datePickerComp\"\n :value=\"datePickerOverlay.value\"\n :format=\"datePickerOverlay.format\"\n :locale=\"locale\"\n :trigger-rect=\"datePickerOverlay.rect\"\n @pick=\"onDatePick\"\n @typing=\"onDateTyping\"\n @close=\"closeDatePicker\"\n />\n\n <!-- Validation error tooltip (cellValidator) -->\n <div\n v-if=\"invalidCell && invalidCell.rect\"\n class=\"eg-validation-tooltip\"\n :style=\"invalidTooltipStyle\"\n >{{ invalidCell.message }}</div>\n\n <!-- Column drag ghost (follows mouse — position updated via direct DOM in handleColDragMove) -->\n <div\n v-if=\"colDrag.active\"\n ref=\"colDragGhost\"\n class=\"eg-col-drag-ghost\"\n :style=\"{ left: (colDrag.startX + 16) + 'px', top: (colDrag.startY - 14) + 'px' }\"\n >{{ colDrag.ghostLabel }}</div>\n\n </div>\n </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth, moveColumnNodeInTree } from '../utils/headerTree.js'\nimport { calcVisibleRows, buildCumulativeHeights, findVisibleRange, getRowTop, getRowHeight } from '../utils/virtualScroll.js'\nimport { generateFillValues } from '../utils/fillHandle.js'\nimport {\n matrixToTSV, tsvToMatrix, extractMatrix, transposeMatrix, applyMatrix, tileMatrix\n} from '../utils/clipboard.js'\nimport {\n mergeHasConflict, findMergeAtCell, isMergeAnchor, addMerge,\n removeMergeAt, resolveMergeAnchor\n} from '../utils/merge.js'\nimport {\n addGroup, removeGroup, toggleGroupCollapse, isGroupCollapsed as _isGroupCollapsed,\n getCollapsedRowIndices, getCollapsedColIndices\n} from '../utils/outline.js'\nimport { exportToExcel as _exportToExcel } from '../utils/excelExport.js'\nimport { importFromFile as _importFromFile } from '../utils/excelImport.js'\nimport { createExcelImportWorker, createExcelExportWorker } from '../utils/workerFactory'\nimport { getMessages } from '../utils/i18n.js'\nimport EgCheckbox from './EgCheckbox.vue'\nimport anime from 'animejs'\nimport EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\nimport EgDatePicker from './EgDatePicker.vue'\nimport { parseDateValue, formatDate } from '../utils/dateUtils.js'\n\nconst SIZE_CONFIG = {\n small: { row: 22, header: 24, footer: 24, filter: 26 },\n middle: { row: 28, header: 28, footer: 28, filter: 32 },\n large: { row: 36, header: 36, footer: 36, filter: 40 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\n// Default toolbar tool list when `toolbar` is `true` or an object without\n// its own `list` — every group except 'theme', which stays opt-in since it\n// needs :theme.sync on the parent to do anything.\nconst DEFAULT_TOOLBAR_LIST = ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen']\nconst THEME_LIST = ['excel', 'material', 'antd', 'fluent', 'carbon']\n\nexport default {\n name: 'ExcelGrid',\n\n components: { EgCheckbox, EgSelect, EgTreeSelect, EgDatePicker },\n\n // v-model → :data + @input\n model: { prop: 'data', event: 'input' },\n\n props: {\n /** Array of row objects keyed by columns[].key */\n data: { type: Array, default: () => [] },\n\n /** Column definitions (may be nested via children[]) */\n columns: { type: Array, default: () => [] },\n\n /** Indices of hidden leaf columns */\n hiddenColumns: { type: Array, default: () => [] },\n\n /** Indices of hidden rows */\n hiddenRows: { type: Array, default: () => [] },\n\n /** Initial merge descriptors [{ startRow, startCol, endRow, endCol }] */\n merges: { type: Array, default: () => [] },\n\n /** Collapsed group descriptors [{ type, startIndex, endIndex }] */\n collapsedGroups: { type: Array, default: () => [] },\n\n /**\n * Height of the grid container in px.\n *\n * Omit it (or pass `null`) for auto-height: the grid sizes itself to fit\n * the header + filter row + all rows + footer for the current `data`\n * length, capped at 70% of the viewport height (`window.innerHeight`) —\n * beyond that it stops growing and scrolls internally, same as a fixed\n * `height` would. Recomputes reactively as `data` grows/shrinks and on\n * window resize. Does not apply in fullscreen mode (already sized to the\n * viewport there) and does not include the toolbar's own height.\n *\n * Pass an explicit `Number` to opt back into the old fixed-height behavior.\n */\n height: { type: Number, default: null },\n\n /** Width of the grid container (px or 'auto') */\n width: { type: [Number, String], default: '100%' },\n\n /**\n * Called before drag-drop: (sourceRange, targetRange) => bool\n * Return false to cancel the drop.\n */\n beforeDrop: { type: Function, default: null },\n\n /**\n * UI locale for the context menu.\n * Supported: 'en' | 'ru' | 'uz' | 'uz-Cyrl'\n */\n locale: { type: String, default: 'en' },\n\n /**\n * Pinned columns map: { [colKey]: 'left' | 'right' }\n * Pinned-left columns render as a fixed zone on the left.\n * Pinned-right columns render as a fixed zone on the right.\n * Emit 'update:pinnedColumns' to support .sync modifier.\n */\n pinnedColumns: { type: Object, default: () => ({}) },\n\n /**\n * Dynamic cell editability: (row, rowIndex, col, colIndex) => boolean\n * Overrides column-level editable when provided.\n * row = plain data object, col = column definition.\n */\n cellEditable: { type: Function, default: null },\n\n /**\n * Cell value validation: (value, row, rowIndex, col, colIndex) => boolean\n * Return false to reject the write — the cell keeps its old value and\n * an error tooltip is shown anchored to that cell.\n */\n cellValidator: { type: Function, default: null },\n\n /**\n * Error tooltip text for a rejected value.\n * Function: (value, row, rowIndex, col, colIndex) => string\n * String: same message for every rejected cell.\n * Default: locale's \"invalidValue\" message.\n */\n cellValidationMessage: { type: [Function, String], default: null },\n\n /**\n * Bulk-write strictness (paste / fill handle / Delete).\n * false (default): invalid cells in the affected range are skipped,\n * the rest of the operation still applies.\n * true: if ANY cell in the affected range fails validation, the whole\n * bulk operation is rejected — nothing is written.\n */\n strictBulkValidation: { type: Boolean, default: false },\n\n /** Visual size of the grid. Affects row/header height and font sizes. */\n size: { type: String, default: 'middle' },\n\n /** Auto-fit all column widths to content (same as double-clicking each resize handle). Re-applies on data/columns change. */\n autoWidth: { type: Boolean, default: false },\n\n /**\n * Allow columns to be reordered by dragging their headers.\n * On drop, emits 'update:columns' (compatible with .sync) and 'column-reorder'.\n */\n columnDraggable: { type: Boolean, default: true },\n\n /**\n * Controls the built-in toolbar above the grid — visibility as a whole,\n * and (optionally) which individual tool groups appear. Controlled\n * entirely by the parent — no in-component toggle for the bar itself.\n *\n * @type {Boolean|{visible?: Boolean, list?: Array<('undoRedo'|'export'|'import'|'size'|'lock'|'fullscreen'|'theme')>}}\n * @default false\n *\n * Two forms:\n * - `Boolean` — `false` (default) hides the toolbar entirely; `true`\n * shows it with the default tool list (every group except `'theme'`,\n * since that one needs `:theme.sync` on the parent to be useful).\n * - `Object { visible, list }` — fine-grained control:\n * - `visible` {Boolean} — defaults to `true` when an object is given\n * (passing an object is itself an opt-in to show the toolbar);\n * set explicitly to `false` to hide it while still describing a\n * `list` you intend to reuse later.\n * - `list` {Array<String>} — which tool groups render, using the\n * identifiers above. Unknown/misspelled strings are silently\n * ignored. Omitting `list` falls back to the same default set as\n * the `true` shorthand. **Order in `list` does not change the\n * toolbar's layout** — groups always render in a fixed position\n * (undo/redo → export/import → #toolbar-extra slot → toolbarButtons\n * → spacer → theme → size → lock/fullscreen); `list` is purely an\n * inclusion filter.\n *\n * `toolbarButtons` and the `#toolbar-extra` slot are independent of\n * this prop — they render whenever content is provided, regardless of\n * `list`.\n *\n * @example\n * // Whole default set\n * <excel-grid toolbar />\n * @example\n * // Only undo/redo and fullscreen\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'fullscreen'] }\" />\n * @example\n * // Toggle visibility from a data property while keeping a fixed list\n * <excel-grid :toolbar=\"{ visible: showToolbar, list: ['export', 'import'] }\" />\n * @example\n * // Opt into the theme picker (needs :theme.sync)\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen', 'theme'] }\" :theme.sync=\"myTheme\" />\n */\n toolbar: { type: [Boolean, Object], default: false },\n\n /** Extra custom toolbar buttons: [{ key, icon, title, onClick(event, gridApi) }] */\n toolbarButtons: { type: Array, default: () => [] },\n\n /**\n * Locks the entire grid: blocks all edits (typing, paste, fill, delete,\n * checkbox, select/treeSelect) and structural changes (resize, column\n * reorder, merge/unmerge, undo/redo). Independent from cellEditable/\n * col.editable, which remain per-cell rules. Emit 'update:locked' to\n * support the .sync modifier.\n */\n locked: { type: Boolean, default: false },\n\n /** Show \"Column info\" / \"Row info\" items in the context menu (dev/debug aid). */\n devMode: { type: Boolean, default: false },\n\n /**\n * Visual theme of the grid.\n * Built-in: 'excel' | 'material' | 'antd' | 'fluent' | 'carbon'\n * Default is the classic Excel 365 look. Use 'material' for Material Design 3.\n * Mirrors the `size` prop's convention: an internal `internalTheme` state\n * mirrors this prop and is what the toolbar's theme picker actually reads\n * from/writes to (via `setTheme()`), so the picker works standalone even\n * without `:theme.sync` — `.sync` is only needed to reflect the current\n * theme back into the host's own state.\n */\n theme: { type: String, default: 'excel' },\n\n /**\n * Shows a filter row directly beneath the column headers (inside the\n * sticky header area). Each leaf column gets a filter control matched to\n * its `type`:\n * - default (no type / 'date' / 'progress'): text search input,\n * matched case-insensitively against the column's *displayed* value\n * (so 'date' matches the formatted string, not the raw stored value).\n * - 'select' / 'treeSelect' *with a static `col.options` array*: a\n * native <select> populated from those options (tree options are\n * flattened, indented by depth), matched by exact raw value.\n * - 'select' / 'treeSelect' with function-based `col.options` (options\n * computed per-row): falls back to the text control, since a single\n * fixed option list can't be resolved outside a specific row.\n * - 'checkbox': a tri-state <select> — All / Checked / Unchecked.\n * - 'action': no filter control (not a data-bound column).\n *\n * Rows that don't match every active filter are hidden using the same\n * mechanism as `hiddenRows` — virtual scroll, keyboard navigation, and\n * `exportToExcel()` all skip them automatically, with no separate\n * filtering code path to keep in sync.\n *\n * Set `col.filterable: false` to omit the filter control for one column\n * (its filter cell renders empty). Set\n * `col.filterFn(cellValue, filterValue, row, rowIndex, column)` to\n * override the default match logic for that column entirely.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row />\n */\n filterRow: { type: Boolean, default: false },\n\n /**\n * Current filter values, keyed by column `key`. Optional — the filter\n * row works standalone via internal state (`internalFilters`) even\n * without `.sync`, mirroring the `size`/`theme` prop convention. Bind\n * `:filters.sync` only if the host needs to read or restore the current\n * filter state.\n * @type {Object}\n * @default {}\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row :filters.sync=\"myFilters\" />\n */\n filters: { type: Object, default: () => ({}) },\n\n /**\n * Shows a sort icon in every leaf column header. Clicking it cycles\n * none → ascending → descending → none for that column (clicking a\n * different column's icon replaces the active sort — single-column\n * sort only).\n *\n * Sorting **physically reorders** the `data` array (like a real\n * spreadsheet sort) and emits it through the normal v-model/`input`\n * event — it is recorded in the undo/redo history exactly like any\n * other data edit, so `undo()` restores the pre-sort row order.\n * Clicking a third time (back to \"none\") only clears the sort\n * indicator — it does **not** restore the previous order by itself;\n * use `undo()` for that.\n *\n * Comparison is matched to the column's `type`: plain/`'progress'`\n * columns compare the raw value (numeric-aware); `'date'` compares the\n * parsed date chronologically, not the formatted string; `'select'` /\n * `'treeSelect'` compare the displayed option label; `'checkbox'`\n * compares the boolean state. Empty/`null` values always sort last,\n * regardless of direction. `'action'` columns are never sortable.\n *\n * Set `col.sortable: false` to omit the icon for one column. Set\n * `col.sortFn(rowA, rowB, column) => number` to fully override the\n * comparison for that column.\n *\n * Blocked (no-op) while the grid is `locked`, or while any merged\n * cells or row outline groups exist — both are anchored to row\n * *position*, which a reorder would silently invalidate.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable />\n */\n sortable: { type: Boolean, default: false },\n\n /**\n * Current sort state — `{ key, direction }` (`direction` is `'asc'` or\n * `'desc'`) or `null` when unsorted. Optional — sorting works standalone\n * via internal state (`internalSort`) even without `.sync`, mirroring\n * the `size`/`theme`/`filters` prop convention. Bind `:sort.sync` only\n * if the host needs to read or restore the current sort state.\n * @type {Object}\n * @default null\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable :sort.sync=\"mySort\" />\n */\n sort: { type: Object, default: null }\n },\n\n data() {\n return {\n // Selection\n activeCell: { row: 0, col: 0 },\n selectionRanges: [], // [{ startRow, startCol, endRow, endCol }]\n isMouseDown: false,\n mouseDownRange: null,\n isCtrlDrag: false,\n\n // Edit\n editingCell: null, // { row, col }\n editValue: '',\n editOriginalValue: '',\n\n // Scroll\n scrollTop: 0,\n scrollLeft: 0,\n headerViewportWidth: null,\n\n // Auto-height (height prop omitted) — 70vh cap, kept in sync on resize\n windowInnerH: typeof window !== 'undefined' ? window.innerHeight : 800,\n\n // Virtual scroll\n virtualStartIndex: 0,\n virtualEndIndex: 0,\n virtualOffsetTop: 0,\n totalVirtualHeight: 0,\n\n // Column widths (flat index → px)\n colWidths: {},\n\n // Row heights (row index → px)\n rowHeights: {},\n\n // Import/export progress\n importState: { active: false, percent: 0, message: '' },\n exportState: { active: false },\n\n resizing: {\n active: false,\n type: null, // 'col' | 'row'\n index: -1,\n startX: 0,\n startY: 0,\n startSize: 0,\n currentPos: 0\n },\n\n // Fill handle\n fillDrag: {\n active: false,\n sourceRange: null,\n currentRow: -1,\n currentCol: -1\n },\n\n // Drag-move\n dragMove: {\n active: false,\n sourceRange: null,\n startX: 0,\n startY: 0,\n targetRow: -1,\n targetCol: -1,\n copyMode: false\n },\n\n // Clipboard (internal)\n clipboardMatrix: null,\n clipboardRange: null,\n clipboardMode: null, // 'copy' | 'cut'\n\n // Context menu\n contextMenu: { visible: false, x: 0, y: 0, row: -1, col: -1 },\n // false while the menu's real size hasn't been measured yet — kept hidden\n // (not painted) so the flip-to-fit repositioning in _positionContextMenu()\n // doesn't cause a visible jump.\n contextMenuPositioned: false,\n\n // Sort popup menu (header sort icon click) — { visible, x, y, key }\n sortMenu: { visible: false, x: 0, y: 0, key: null },\n sortMenuPositioned: false,\n\n // devMode — column/row config info modal ({ title, json } | null)\n infoModal: null,\n\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, rect }\n\n // Date picker overlay\n datePickerOverlay: null, // { row, col, format, value, rect }\n\n // Cell value validation (cellValidator)\n invalidCell: null, // { row, col, message, rect }\n\n // Merges (controlled via prop + update:merges)\n internalMerges: [],\n\n // Toolbar (controlled via prop + update:locked / update:size / update:theme)\n internalLocked: false,\n internalSize: 'middle',\n internalTheme: 'excel',\n // Brief centered icon feedback whenever the lock state actually changes\n // (toolbar click, toggleLock(), or a host-driven :locked.sync change).\n lockFlashVisible: false,\n lockFlashLocked: false,\n isFullscreen: false,\n // Bumped whenever _history/_historyIndex change so canUndo/canRedo stay reactive.\n historyVersion: 0,\n\n // Groups\n internalGroups: [], // [{ type, startIndex, endIndex }]\n internalCollapsedGroups: [],\n\n // Header\n headerDepth: 1,\n headerRows: [],\n\n // Leaf columns cache\n leafCols: [],\n\n // Footer column totals (flat col index → number | null)\n columnTotals: {},\n\n // Filter row (controlled via prop + update:filters)\n internalFilters: {},\n // Row indices currently hidden by an active filter — merged into\n // hiddenRowSet, so virtual scroll/export/navigation need no separate path.\n filteredRowIndices: [],\n\n // Sort (controlled via prop + update:sort) — { key, direction } | null\n internalSort: null,\n\n // Column drag-and-drop state\n colDrag: {\n pending: false, // mousedown received, waiting for movement threshold\n active: false, // drag in progress\n sourceFirstLeaf: -1, // first leaf flat-index of the dragged node\n sourceLeafCount: 1, // number of leaves in the dragged node (1=leaf, N=group)\n dropIndex: -1, // insert-before flat index (0..N)\n startX: 0,\n startY: 0,\n ghostLabel: ''\n },\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n pinnedLeftSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'left') s.add(col._flatIndex) })\n return s\n },\n\n pinnedRightSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'right') s.add(col._flatIndex) })\n return s\n },\n\n pinnedLeftCols() { return this.leafCols.filter(col => this.pinnedLeftSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n\n hasPinnedLeft() { return this.pinnedLeftSet.size > 0 },\n hasPinnedRight() { return this.pinnedRightSet.size > 0 },\n\n hasFooter() { return this.leafCols.some(col => this.isColumnTotalVisible(col)) },\n\n pinnedLeftWidth() {\n let w = 0\n for (const col of this.pinnedLeftCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n pinnedRightWidth() {\n let w = 0\n for (const col of this.pinnedRightCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n totalRows() { return this.data.length },\n\n // See the `toolbar` prop's JSDoc for the full Boolean|Object contract.\n _toolbarVisible() {\n if (typeof this.toolbar === 'boolean') return this.toolbar\n return !!this.toolbar && this.toolbar.visible !== false\n },\n _toolbarList() {\n if (this.toolbar && Array.isArray(this.toolbar.list)) return this.toolbar.list\n return DEFAULT_TOOLBAR_LIST\n },\n\n _sizeConf() { return SIZE_CONFIG[this.internalSize] || SIZE_CONFIG.middle },\n\n sizeOptions() { return Object.keys(SIZE_CONFIG) },\n _rowH() { return this._sizeConf.row },\n _headerRowH() { return this._sizeConf.header },\n _footerRowH() { return this._sizeConf.footer },\n _filterRowH() { return this.filterRow ? this._sizeConf.filter : 0 },\n\n headerRowHeight() { return this._headerRowH },\n footerRowHeight() { return this._footerRowH },\n\n // Pre-built cumulative height array — O(n) once, then cached by Vue.\n // findVisibleRange() uses binary search against this cached array → O(log n) per scroll.\n // Invalidated only when row count, custom heights, or hidden rows change.\n cumulativeHeights() {\n return buildCumulativeHeights(this.totalRows, this._rowH, this.rowHeights, this.hiddenRowSet)\n },\n\n // height prop omitted → fit header + filter row + all (visible) rows + footer exactly.\n _autoContentHeight() {\n const headerH = this._headerRowH * this.headerDepth + this._filterRowH\n const bodyH = this.cumulativeHeights[this.totalRows] || 0\n const footerH = this.hasFooter ? this._footerRowH : 0\n return headerH + bodyH + footerH\n },\n\n // Resolved grid-area height: explicit `height` prop wins as-is (old\n // behavior); omitted → auto-fit content, capped at 70% of the viewport.\n effectiveHeight() {\n if (this.height != null) return this.height\n return Math.min(this._autoContentHeight, this.windowInnerH * 0.7)\n },\n\n hiddenColSet() { return new Set(this.hiddenColumns) },\n\n hiddenRowSet() {\n const s = new Set(this.hiddenRows)\n // Also add rows hidden by collapsed row groups\n const collapsed = getCollapsedRowIndices(this.internalGroups, this.internalCollapsedGroups)\n collapsed.forEach(i => s.add(i))\n // Also add rows hidden by the filter row (see filteredRowIndices/_recomputeRowFilters)\n this.filteredRowIndices.forEach(i => s.add(i))\n return s\n },\n\n hiddenColFromGroupSet() {\n return getCollapsedColIndices(this.internalGroups, this.internalCollapsedGroups)\n },\n\n effectiveHiddenColSet() {\n const s = new Set(this.hiddenColSet)\n this.hiddenColFromGroupSet.forEach(i => s.add(i))\n // col.visible === false — a static schema-level hide, independent of the\n // toggleable hiddenColumns list. \"Show All Columns\" never reveals it.\n this.leafCols.forEach(col => { if (col.visible === false) s.add(col._flatIndex) })\n return s\n },\n\n rowGroups() { return this.internalGroups.filter(g => g.type === 'row') },\n colGroups() { return this.internalGroups.filter(g => g.type === 'column') },\n\n visibleRowRange() {\n const rows = []\n for (let i = this.virtualStartIndex; i <= this.virtualEndIndex; i++) rows.push(i)\n return rows\n },\n\n primaryRange() {\n return getPrimaryRange(this.selectionRanges, this.activeCell)\n },\n\n fillHandleVisible() {\n return this.primaryRange && !this.editingCell\n },\n\n fillHandleStyle() {\n if (!this.primaryRange) return {}\n const r = normalizeRange(this.primaryRange)\n // O(1) lookup via cumulativeHeights instead of O(endRow) getRowTop loop\n const bottom = this.cumulativeHeights[r.endRow + 1] || 0\n const right = this.getColLeft(r.endCol + 1)\n return {\n position: 'absolute',\n width: '6px',\n height: '6px',\n background: 'var(--eg-color-accent)',\n border: '1px solid white',\n bottom: (this.totalVirtualHeight - bottom + 1) + 'px',\n right: (this.scrollContentWidth - right + 1) + 'px',\n cursor: 'crosshair',\n zIndex: 10\n }\n },\n\n\n invalidTooltipStyle() {\n if (!this.invalidCell || !this.invalidCell.rect) return {}\n const { rect } = this.invalidCell\n return {\n position: 'fixed',\n top: (rect.bottom + 4) + 'px',\n left: rect.left + 'px',\n zIndex: 9999\n }\n },\n\n dragGhostStyle() {\n if (!this.dragMove.active || this.dragMove.targetRow < 0) return {}\n const r = normalizeRange(this.dragMove.sourceRange)\n const rows = r.endRow - r.startRow\n const cols = r.endCol - r.startCol\n // O(1) lookup via cumulativeHeights instead of O(targetRow) getRowTop loop\n const top = this.cumulativeHeights[this.dragMove.targetRow] || 0\n const left = this.getColLeft(this.dragMove.targetCol)\n const width = this.getRangeWidth(this.dragMove.targetCol, this.dragMove.targetCol + cols)\n const height = this.getRangeHeight(this.dragMove.targetRow, this.dragMove.targetRow + rows)\n return {\n position: 'absolute',\n top: top + 'px',\n left: left + 'px',\n width: width + 'px',\n height: height + 'px',\n border: '2px dashed #1a73e8',\n pointerEvents: 'none',\n zIndex: 20\n }\n },\n\n wrapperStyle() {\n if (this.isFullscreen) {\n return {\n width: '100%',\n height: 'auto',\n flex: '1 1 auto',\n minHeight: 0,\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n }\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n height: this.effectiveHeight + 'px',\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n },\n\n shellStyle() {\n if (this.isFullscreen) return {}\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n display: 'flex',\n flexDirection: 'column'\n }\n },\n\n canUndo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex >= 0\n },\n\n canRedo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex < this._history.length - 1\n },\n\n headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n cornerStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: (this._headerRowH * this.headerDepth + this._filterRowH) + 'px',\n background: 'var(--eg-color-header-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderBottom: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0\n }\n },\n\n rowOutlineGutterStyle() {\n return { width: OUTLINE_GUTTER + 'px', flexShrink: 0 }\n },\n\n frozenColHeaderStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n scrollColHeaderStyle() {\n // Once measured, lock the header to the body's exact clientWidth (flex: 0 0 <px>)\n // instead of growing independently via flex:1 — this guarantees identical\n // scrollWidth-clientWidth (max scrollLeft) on both, so syncing scrollLeft never clamps\n // one before the other. (padding-right can't do this: clientWidth always includes\n // padding, so padding never actually shrinks it.)\n if (this.headerViewportWidth != null) {\n return { flex: '0 0 ' + this.headerViewportWidth + 'px', overflow: 'hidden' }\n }\n return { flex: 1, overflow: 'hidden' }\n },\n\n bodyAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flex: 1,\n position: 'relative'\n }\n },\n\n rowHeaderColStyle() {\n return {\n width: ROW_HEADER_WIDTH + 'px',\n minWidth: ROW_HEADER_WIDTH + 'px',\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n position: 'sticky',\n left: 0,\n zIndex: 20,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n rowOutlineColStyle() {\n return {\n width: OUTLINE_GUTTER + 'px',\n minWidth: OUTLINE_GUTTER + 'px',\n flexShrink: 0,\n position: 'relative',\n background: 'var(--eg-color-cell-bg)'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative',\n overflowAnchor: 'none' /* prevent Chrome scroll anchoring from fighting virtual scroll translateY */\n }\n },\n\n frozenColBodyStyle() {\n const w = this.getFrozenWidth()\n return {\n position: 'sticky',\n left: 0,\n width: w + 'px',\n zIndex: 10,\n background: 'white',\n overflow: 'hidden'\n }\n },\n\n mainCellsStyle() {\n const left = this.getFrozenWidth()\n return {\n position: 'absolute',\n left: left + 'px',\n top: 0,\n bottom: 0\n }\n },\n\n scrollContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && !this.pinnedRightSet.has(i)) w += this.getColW(i)\n }\n // Extra right padding so pinned-right zone doesn't cover the last scrollable column\n if (this.hasPinnedRight) w += this.pinnedRightWidth\n return w\n },\n\n scrollableContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && this.isColumnScrollable(i)) w += this.getColW(i)\n }\n return w\n },\n\n pinnedRightBodyStyle() {\n return {\n position: 'sticky',\n right: 0,\n width: this.pinnedRightWidth + 'px',\n zIndex: 10,\n background: 'white',\n flexShrink: 0\n }\n },\n\n pinnedRightHeaderStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative', overflowAnchor: 'none' }\n },\n\n footerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flexShrink: 0,\n position: 'sticky',\n bottom: 0,\n zIndex: 30,\n background: 'var(--eg-color-footer-bg)',\n boxShadow: '0 -2px 0 0 var(--eg-color-border-strong)'\n }\n },\n\n footerLabelStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: this._footerRowH + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--eg-color-footer-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderTop: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: 'var(--eg-font-size-header)',\n fontWeight: 600\n }\n },\n\n frozenColFooterStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n pinnedRightFooterStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n colOutlineRowStyle() {\n return {\n position: 'absolute',\n top: -OUTLINE_GUTTER + 'px',\n left: 0,\n right: 0,\n height: OUTLINE_GUTTER + 'px'\n }\n },\n\n resizeLineStyle() {\n if (!this.resizing.active) return {}\n if (this.resizing.type === 'col') {\n return {\n position: 'absolute',\n top: 0,\n left: (this.resizing.currentPos - (this.resizing.containerLeft || 0)) + 'px',\n width: '2px',\n height: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'absolute',\n left: 0,\n top: (this.resizing.currentPos - (this.resizing.containerTop || 0)) + 'px',\n height: '2px',\n width: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size: {\n immediate: true,\n handler(v) { this.internalSize = v || 'middle' }\n },\n\n theme: {\n immediate: true,\n handler(v) { this.internalTheme = v || 'excel' }\n },\n\n // Runs before the `columns`/`data` watchers below (both call\n // _recomputeRowFilters()) so internalFilters already reflects the prop\n // by the time either of them fires during the initial immediate pass.\n filters: {\n immediate: true,\n handler(v) { this.internalFilters = v ? { ...v } : {} }\n },\n\n internalFilters() { this._scheduleFilterRecompute() },\n\n filterRow() {\n this._recomputeRowFilters()\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n sort: {\n immediate: true,\n handler(v) { this.internalSort = v ? { key: v.key, direction: v.direction } : null }\n },\n\n internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n // Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:\n // true` prop doesn't flash the icon before the grid has even appeared.\n internalLocked(v) {\n if (!this._lockFlashArmed) return\n this._flashLock(v)\n },\n\n autoWidth(v) {\n if (v) this._applyAutoWidth()\n },\n\n columns: {\n immediate: true,\n handler(cols) {\n // Build key→width map from current state before rebuilding leafCols.\n // This preserves user-resized widths across column reorders: when columns\n // are moved, _flatIndex values shift but col.key stays the same.\n const keyToWidth = {}\n ;(this.leafCols || []).forEach(col => {\n if (this.colWidths[col._flatIndex] !== undefined) {\n keyToWidth[col.key] = this.colWidths[col._flatIndex]\n }\n })\n\n this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n\n // Rebuild colWidths by new flat index, using col.key to look up saved widths\n const newColWidths = {}\n this.leafCols.forEach(col => {\n newColWidths[col._flatIndex] = keyToWidth[col.key] !== undefined\n ? keyToWidth[col.key]\n : (col.width || DEFAULT_COL_WIDTH)\n })\n this.colWidths = newColWidths\n\n this.computeColumnTotals()\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this._applyAutoWidth()\n }\n },\n merges: {\n immediate: true,\n handler(v) { this.internalMerges = v ? [...v] : [] }\n },\n collapsedGroups: {\n immediate: true,\n handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }\n },\n locked: {\n immediate: true,\n handler(v) { this.internalLocked = !!v }\n },\n data: {\n immediate: true,\n handler(newData) {\n // Keep _dataRaw in sync so template uses fresh data without triggering dependArray\n this._dataRaw = newData || []\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this.computeColumnTotals()\n // Force re-render so cell values refresh even when virtualScroll indices don't change\n // (e.g. editing a cell: same row count, same scroll position, only content changed)\n this.$forceUpdate()\n this._applyAutoWidth()\n }\n },\n totalRows() { this.updateVirtualScroll() },\n hiddenRowSet() { this.updateVirtualScroll() },\n totalVirtualHeight() { this.$nextTick(this.syncHeaderViewportWidth) }\n },\n\n created() {\n // Non-reactive row data reference — avoids Vue 2 dependArray(data) O(n) loop.\n // Vue 2 calls dependArray() for EVERY `data[ri]` access in the template,\n // iterating all 50,000 rows each time. With 1200 cells per render that's\n // 60 million iterations per render (~600ms). _dataRaw bypasses this.\n this._dataRaw = this.data || []\n\n // Non-reactive caches: same (w,h) → same style object → Vue 2 skips DOM style writes\n this._cellStyleCache = Object.create(null)\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n // rAF handles for scroll/resize throttle\n this._scrollRAF = null\n this._resizeRAF = null\n\n // Undo/redo history — non-reactive to avoid Vue overhead.\n // Each entry: { type: 'data'|'merges', before: {...}, after: {...} }\n // 'data' stores only diff rows: { indices: number[], rows: object[] }\n this._history = []\n this._historyIndex = -1\n this._historySkip = false // true while undo/redo is in progress\n\n // Column drag preview offsets — non-reactive so direct DOM updates bypass Vue re-renders.\n // Keys are column keys, values are pixel offsets.\n this._dragOffsets = {}\n\n },\n\n mounted() {\n this.updateVirtualScroll()\n this.$nextTick(this.syncHeaderViewportWidth)\n document.addEventListener('mousemove', this.onGlobalMouseMove)\n document.addEventListener('mouseup', this.onGlobalMouseUp)\n document.addEventListener('keydown', this.onGlobalKeyDown)\n document.addEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.addEventListener('resize', this.syncHeaderViewportWidth)\n window.addEventListener('scroll', this._onWindowScrollForDropdown, true)\n this._lockFlashArmed = true\n },\n\n updated() {\n // After Vue re-renders (e.g., virtual scroll adds new rows during drag),\n // apply missing transforms to newly rendered cells that don't have one yet.\n if (!this.colDrag.active || !this.$el) return\n const offsets = this._dragOffsets\n if (!offsets || !Object.keys(offsets).length) return\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n if (el.style.transform) return\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n if (dx) el.style.transform = `translateX(${dx}px)`\n })\n },\n\n beforeDestroy() {\n clearTimeout(this._filterRecomputeTimer)\n clearTimeout(this._onInputTimer)\n document.removeEventListener('mousemove', this.onGlobalMouseMove)\n document.removeEventListener('mouseup', this.onGlobalMouseUp)\n document.removeEventListener('keydown', this.onGlobalKeyDown)\n document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.removeEventListener('resize', this.syncHeaderViewportWidth)\n window.removeEventListener('scroll', this._onWindowScrollForDropdown, true)\n if (this.isFullscreen) {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n },\n\n methods: {\n // =========================================================\n // VIRTUAL SCROLL\n // =========================================================\n // In fullscreen, .eg-scroll-body's real height comes from flexbox filling\n // the viewport (which can be far taller than the `height` prop the host\n // originally passed for the fixed-size layout) — measure it directly\n // instead of trusting `height`, otherwise virtual scroll only ever\n // renders enough rows for the old fixed size, leaving the rest of the\n // (visually stretched) container blank.\n _containerH() {\n if (this.isFullscreen && this.$refs.scrollBody) return this.$refs.scrollBody.clientHeight\n const footerH = this.hasFooter ? this._footerRowH : 0\n return this.effectiveHeight - (this._headerRowH * this.headerDepth) - this._filterRowH - footerH\n },\n\n // Same fullscreen-aware measurement as _containerH(), but mirrors PageUp/\n // PageDown's pre-existing (footer-agnostic) row-span formula exactly for\n // the non-fullscreen case, so normal-mode behavior is unchanged.\n _pageRowSpan() {\n const h = this.isFullscreen && this.$refs.scrollBody\n ? this.$refs.scrollBody.clientHeight\n : (this.effectiveHeight - this._headerRowH * this.headerDepth - this._filterRowH)\n return Math.floor(h / this._rowH)\n },\n\n updateVirtualScroll() {\n // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll.\n // overscan=8 (≈224px at default row height) gives enough buffer for fast scroll\n // without blank-band artifacts between rAF updates.\n const result = findVisibleRange(\n this.scrollTop, Math.max(this._containerH(), 100), this.totalRows, this.cumulativeHeights, 8\n )\n this.virtualStartIndex = result.startIndex\n this.virtualEndIndex = result.endIndex\n this.virtualOffsetTop = result.offsetTop\n this.totalVirtualHeight = result.totalHeight\n },\n\n onBodyScroll(e) {\n const target = e.target\n // Sync sibling scroll containers immediately (visual alignment, no layout trigger)\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = target.scrollLeft\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = target.scrollLeft\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = target.scrollTop\n\n // Close dropdown on scroll — fixed-position dropdown drifts away from the moving cell\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame.\n // Read scrollTop from the DOM ref at rAF time (not from the stale event closure)\n // so that multiple scroll events arriving between frames still resolve to the\n // final position — eliminates the flickering caused by render/scroll mismatch.\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n const sb = this.$refs.scrollBody\n if (!sb) return\n this.scrollTop = sb.scrollTop\n this.scrollLeft = sb.scrollLeft\n this.updateVirtualScroll()\n })\n }\n },\n\n onScroll(e) {},\n\n // .eg-scroll-col-header (overflow:hidden, synced via JS) has no native scrollbar, but\n // .eg-scroll-body (overflow:auto) does — its real vertical scrollbar eats into its\n // clientWidth. If both containers just grow via flex:1, the header ends up wider than\n // the body, so its max scrollLeft is smaller and syncing clamps it before the body\n // reaches its rightmost scroll position — column borders desync right at the scroll\n // end. Locking the header to the body's exact measured clientWidth (scrollColHeaderStyle)\n // makes both max scrollLeft values identical.\n syncHeaderViewportWidth() {\n // Auto-height's 70vh cap tracks the real viewport — keep it current on resize.\n if (typeof window !== 'undefined' && this.windowInnerH !== window.innerHeight) {\n this.windowInnerH = window.innerHeight\n }\n const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\n // Window resize while fullscreen changes the real available row-viewport\n // height too (see _containerH()) — recompute which rows are rendered.\n // Same for auto-height (height prop omitted): the 70vh cap just moved.\n if (this.isFullscreen || this.height == null) this.updateVirtualScroll()\n },\n\n // =========================================================\n // HELPERS\n // =========================================================\n isRowHidden(ri) { return this.hiddenRowSet.has(ri) },\n isColHidden(ci) { return this.effectiveHiddenColSet.has(ci) },\n\n getColW(ci) {\n return this.colWidths[ci] !== undefined ? this.colWidths[ci] : (this.leafCols[ci] ? this.leafCols[ci].width || DEFAULT_COL_WIDTH : DEFAULT_COL_WIDTH)\n },\n\n getRowH(ri) {\n if (this.isRowHidden(ri)) return 0\n return this.rowHeights[ri] || this._rowH\n },\n\n getColLeft(ci) {\n let left = 0\n for (let i = 0; i < ci && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) left += this.getColW(i)\n }\n return left\n },\n\n getFrozenWidth() {\n return this.hasPinnedLeft ? this.pinnedLeftWidth : 0\n },\n\n isColumnPinnedLeft(ci) {\n return this.pinnedLeftSet.has(ci)\n },\n\n isColumnPinnedRight(ci) {\n return this.pinnedRightSet.has(ci)\n },\n\n isColumnScrollable(ci) {\n return !this.isColumnPinnedLeft(ci) && !this.isColumnPinnedRight(ci)\n },\n\n getZoneColLeft(ci, zone) {\n let left = 0\n if (zone === 'left') {\n for (const col of this.pinnedLeftCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else if (zone === 'right') {\n for (const col of this.pinnedRightCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else {\n for (let i = 0; i < this.leafCols.length; i++) {\n const col = this.leafCols[i]\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex) && this.isColumnScrollable(col._flatIndex)) {\n left += this.getColW(col._flatIndex)\n }\n }\n }\n return left\n },\n\n isCellInZone(cell, rowIdx, zone) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n const z = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n return z === zone\n },\n\n getHeaderCellStyleForZone(cell, rowIndex, zone) {\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z !== zone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getRangeWidth(startCol, endCol) {\n let w = 0\n for (let i = startCol; i <= endCol && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) w += this.getColW(i)\n }\n return w\n },\n\n getRangeHeight(startRow, endRow) {\n let h = 0\n for (let i = startRow; i <= endRow && i < this.totalRows; i++) {\n if (!this.isRowHidden(i)) h += this.getRowH(i)\n }\n return h\n },\n\n getCellValue(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return ''\n const row = this._dataRaw[ri]\n if (!row) return ''\n const v = row[col.key]\n return v !== undefined ? v : ''\n },\n\n isEditing(ri, ci) {\n return this.editingCell && this.editingCell.row === ri && this.editingCell.col === ci\n },\n\n isMergedAway(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return false\n const nm = normalizeRange(merge)\n return !(nm.startRow === ri && nm.startCol === ci)\n },\n\n getMergeSpan(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return { rowspan: 1, colspan: 1 }\n const nm = normalizeRange(merge)\n return {\n rowspan: nm.endRow - nm.startRow + 1,\n colspan: nm.endCol - nm.startCol + 1\n }\n },\n\n isSelected(ri, ci) {\n return isCellInAnyRange(ri, ci, this.selectionRanges)\n },\n\n isActiveCell(ri, ci) {\n return this.activeCell.row === ri && this.activeCell.col === ci\n },\n\n isCopyHighlighted(ri, ci) {\n if (!this.clipboardRange || this.clipboardMode !== 'copy') return false\n return isCellInRange(ri, ci, this.clipboardRange)\n },\n\n // =========================================================\n // STYLES\n // =========================================================\n getRowStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowStyleCache[key]) return this._rowStyleCache[key]\n const style = { height: h + 'px', minHeight: h + 'px' }\n this._rowStyleCache[key] = style\n return style\n },\n\n getRowHeaderStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowHeaderStyleCache[key]) return this._rowHeaderStyleCache[key]\n const style = { height: h + 'px' }\n this._rowHeaderStyleCache[key] = style\n return style\n },\n\n getCellStyle(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n const nm = merge ? normalizeRange(merge) : null\n let w = this.getColW(ci)\n let h = this.getRowH(ri)\n\n if (nm && nm.startRow === ri && nm.startCol === ci) {\n w = this.getRangeWidth(nm.startCol, nm.endCol)\n h = this.getRangeHeight(nm.startRow, nm.endRow)\n }\n\n // Position cells with the same absolute-left arithmetic the header uses\n // (getZoneColLeft), instead of relying on flex layout to accumulate widths —\n // flex and a JS integer sum can drift apart by a few px over many columns,\n // desyncing the body's column borders from the header's.\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n\n // Cache by dimensions+position — same (w,h,left) → same object ref → Vue 2 skips DOM style writes\n const key = w + '_' + h + '_' + left\n if (this._cellStyleCache[key]) return this._cellStyleCache[key]\n const style = { position: 'absolute', left: left + 'px', top: '0px', width: w + 'px', minWidth: w + 'px', height: h + 'px' }\n this._cellStyleCache[key] = style\n return style\n },\n\n getCellClasses(ri, ci) {\n const col = this.leafCols[ci]\n return {\n 'eg-cell-active': this.isActiveCell(ri, ci),\n 'eg-cell-selected': this.isSelected(ri, ci) && !this.isActiveCell(ri, ci),\n 'eg-cell-fill-preview': this.isInFillPreview(ri, ci),\n 'eg-cell-copy': this.isCopyHighlighted(ri, ci),\n 'eg-cell-merged-hidden': this.isMergedAway(ri, ci),\n 'eg-cell-select': !!(col && this.isSelectLikeCol(col)),\n 'eg-cell-date': !!(col && col.type === 'date'),\n 'eg-cell-checkbox': !!(col && col.type === 'checkbox'),\n 'eg-cell-action': !!(col && col.type === 'action'),\n 'eg-cell-readonly': !this.isCellEditable(ri, ci),\n 'eg-cell-invalid': !!(this.invalidCell && this.invalidCell.row === ri && this.invalidCell.col === ci),\n 'eg-cell--has-suffix': !!(col && this.hasSuffixSlot(col)),\n 'eg-cell--drag-source': this.colDrag.active && ci >= this.colDrag.sourceFirstLeaf && ci < this.colDrag.sourceFirstLeaf + this.colDrag.sourceLeafCount,\n 'eg-cell-align-right': !!(col && col.align === 'right'),\n 'eg-cell-align-center': !!(col && col.align === 'center')\n }\n },\n\n isInFillPreview(ri, ci) {\n if (!this.fillDrag.active || !this.fillDrag.sourceRange) return false\n const src = this.fillDrag.sourceRange\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n if (targetRow > src.endRow) {\n return ri > src.endRow && ri <= targetRow && ci >= src.startCol && ci <= src.endCol\n }\n if (targetCol > src.endCol) {\n return ci > src.endCol && ci <= targetCol && ri >= src.startRow && ri <= src.endRow\n }\n return false\n },\n\n getHeaderCellStyle(cell, rowIndex, isFrozen) {\n const zone = isFrozen ? 'left' : 'scroll'\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n if (isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (!inZone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getHeaderDragTransform(cell) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking, no re-render on offset change\n let key\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n } else {\n key = cell.column.key\n }\n if (!key) return null\n const dx = this._dragOffsets[key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n getCellDragTransform(ci) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking\n const col = this.leafCols[ci]\n if (!col) return null\n const dx = this._dragOffsets[col.key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n\n // col.totalConfig.visible (boolean) takes priority over col.showTotal when present.\n isColumnTotalVisible(col) {\n if (col.totalConfig && typeof col.totalConfig.visible === 'boolean') return col.totalConfig.visible\n return !!col.showTotal\n },\n\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!this.isColumnTotalVisible(col)) return\n let sum = 0\n let hasNumber = false\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n const v = row ? row[col.key] : undefined\n if (typeof v === 'number' && isFinite(v)) {\n sum += v\n hasNumber = true\n }\n }\n totals[col._flatIndex] = hasNumber ? Math.round(sum * 1e6) / 1e6 : null\n })\n this.columnTotals = totals\n },\n\n getColumnTotalDisplay(ci) {\n const col = this.leafCols[ci]\n if (!col || !this.isColumnTotalVisible(col)) return ''\n const raw = this.columnTotals[ci]\n const cfg = col.totalConfig\n if (cfg && typeof cfg.formatter === 'function') {\n return cfg.formatter(raw, this._dataRaw, col, ci)\n }\n return raw === null || raw === undefined ? '-' : raw\n },\n\n getFooterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n const style = {\n position: 'absolute',\n left: left + 'px',\n top: '0px',\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px'\n }\n const col = this.leafCols[ci]\n const extra = col && col.totalConfig && col.totalConfig.style\n return extra ? { ...style, ...extra } : style\n },\n\n getFooterCellClass(ci) {\n const col = this.leafCols[ci]\n if (!col) return null\n const classList = col.totalConfig && col.totalConfig.classList\n const base = this.isColumnTotalVisible(col) ? 'eg-footer-cell-total' : null\n if (!base) return classList || null\n return classList ? [base, classList] : base\n },\n\n isCellVisible(cell, rowIndex, isFrozen) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (inZone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n return isFrozen ? this.isColumnPinnedLeft(ci) : this.isColumnScrollable(ci)\n },\n\n getRowGroupBtnStyle(group) {\n const top = getRowTop(group.startIndex, this._rowH, this.rowHeights) - this.scrollTop\n return {\n position: 'absolute',\n top: top + 'px',\n left: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px',\n zIndex: 5\n }\n },\n\n getColGroupBtnStyle(group) {\n const left = this.getColLeft(group.startIndex) - this.scrollLeft\n return {\n position: 'absolute',\n left: left + 'px',\n top: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px'\n }\n },\n\n // =========================================================\n // CELL SELECTION (3.1)\n // =========================================================\n selectCell(row, col, addToSelection = false) {\n if (row < 0 || row >= this.totalRows || col < 0 || col >= this.leafCols.length) return\n this.activeCell = { row, col }\n if (addToSelection) {\n this.selectionRanges.push(singleCellRange(row, col))\n } else {\n this.selectionRanges = [singleCellRange(row, col)]\n }\n this.emitSelectionChange()\n },\n\n selectRange(startRow, startCol, endRow, endCol, replace = true) {\n const range = { startRow, startCol, endRow, endCol }\n if (replace) {\n this.selectionRanges = [range]\n } else {\n this.selectionRanges.push(range)\n }\n this.emitSelectionChange()\n },\n\n selectRow(ri) {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [{ startRow: ri, endRow: ri, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n selectColumn(ci) {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n this.emitSelectionChange()\n },\n\n selectAll() {\n this.activeCell = { row: 0, col: 0 }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n emitSelectionChange() {\n this.$emit('selection-change', {\n activeCell: { ...this.activeCell },\n ranges: this.selectionRanges.map(r => normalizeRange(r))\n })\n },\n\n onCellMouseDown(e, ri, ci) {\n this.invalidCell = null\n if (this.editingCell) {\n const editCol = this.leafCols[this.editingCell.col]\n if (editCol && this._hasCustomEdit(editCol)) {\n this.cancelEdit()\n } else {\n this.commitEdit()\n }\n }\n this.hideContextMenu()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n\n if (e.ctrlKey || e.metaKey) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Ctrl+click: start new independent range\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges.push(this.mouseDownRange)\n this.isCtrlDrag = true\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Shift+click: extend primary range to this cell\n this.selectionRanges = [buildRange(this.activeCell, { row: ri, col: ci })]\n } else {\n const wasActive = this.activeCell.row === ri && this.activeCell.col === ci\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges = [this.mouseDownRange]\n this.isCtrlDrag = false\n\n // Open select/date dropdown on click if cell was already active (2nd click)\n const col = this.leafCols[ci]\n if (wasActive && col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openSelectDropdown(ri, ci, e)\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (wasActive && col && col.type === 'date' && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openDatePickerForActive()\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n }\n\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onCellMouseOver(e, ri, ci) {\n if (!this.isMouseDown) return\n if (this.fillDrag.active) return\n if (this.dragMove.active) {\n this.dragMove.targetRow = ri\n this.dragMove.targetCol = ci\n return\n }\n\n // Extend current drag range\n if (this.mouseDownRange) {\n this.mouseDownRange.endRow = ri\n this.mouseDownRange.endCol = ci\n this.emitSelectionChange()\n }\n },\n\n onCellMouseUp() {\n this.isMouseDown = false\n },\n\n onCellDblClick(ri, ci) {\n const col = this.leafCols[ci]\n if (!this.isCellEditable(ri, ci)) return\n if (col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col)) {\n this.openSelectDropdownForActive()\n return\n }\n if (col && col.type === 'date' && !this._hasCustomEdit(col)) {\n this.openDatePickerForActive()\n return\n }\n if (col && col.type === 'checkbox') return\n if (col && col.type === 'action') return\n this.startEditing(ri, ci, null, true)\n },\n\n onRowHeaderMouseDown(e, ri) {\n const endCol = this.leafCols.length - 1\n const fullRow = { startRow: ri, endRow: ri, startCol: 0, endCol }\n\n if (e.ctrlKey || e.metaKey) {\n if (e.shiftKey && this.selectionRanges.length > 0) {\n // Ctrl+Shift: active qatordan ri gacha yangi range qo'sh\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [...this.selectionRanges, { startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n // Ctrl+click: qatorni toggle qil\n const idx = this.selectionRanges.findIndex(r =>\n r.startRow === ri && r.endRow === ri && r.startCol === 0 && r.endCol === endCol\n )\n if (idx >= 0) {\n const next = this.selectionRanges.filter((_, i) => i !== idx)\n this.selectionRanges = next.length ? next : [fullRow]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [...this.selectionRanges, fullRow]\n }\n }\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [{ startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [fullRow]\n }\n\n this.mouseDownRange = this.selectionRanges[this.selectionRanges.length - 1]\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderMouseDown(e, cell) {\n // Start pending drag (activates after 5 px horizontal movement)\n // Works for both leaf and group headers\n if (this.columnDraggable && !this.internalLocked && !e.shiftKey && !e.ctrlKey && !e.metaKey) {\n this.colDrag.pending = true\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = cell._firstLeafFlatIndex\n this.colDrag.sourceLeafCount = cell.isGroup ? cell.colspan : 1\n this.colDrag.startX = e.clientX\n this.colDrag.startY = e.clientY\n this.colDrag.ghostLabel = cell.column.title || ''\n }\n\n if (cell.isGroup) {\n // Group header click: select all leaf columns within the group\n const startCol = cell._firstLeafFlatIndex\n const endCol = startCol + cell.colspan - 1\n this.activeCell = { row: 0, col: startCol }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol, endCol }]\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n return\n }\n\n const ci = cell.column._flatIndex\n if (e.shiftKey && this.selectionRanges.length > 0) {\n const sc = Math.min(this.activeCell.col, ci)\n const ec = Math.max(this.activeCell.col, ci)\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: sc, endCol: ec }]\n } else {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n }\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderDblClick(e, cell) {\n if (cell.isGroup) return\n this.autoFitColumn(cell.column._flatIndex)\n },\n\n onColHeaderContextMenu(e, cell) {\n if (!cell.isGroup) {\n this._openContextMenu(e, -1, cell.column._flatIndex)\n }\n },\n\n onRowContextMenu(e, ri) {\n this._openContextMenu(e, ri, -1)\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this._openContextMenu(e, ri, ci)\n },\n\n _openContextMenu(e, row, col) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row, col }\n this.contextMenuPositioned = false\n this.$nextTick(this._positionContextMenu)\n },\n\n // Flips the menu up/left if it would overflow the viewport at the raw\n // click coordinates — measured against the actual rendered size (item\n // count varies with devMode/pin/row-vs-col context, so an estimate isn't\n // reliable). Rendered off-screen (left:-9999px, see template) while\n // unpositioned so this first measurement reflects the menu's true\n // natural (single-line) width — measuring it in place near a viewport\n // edge would let the browser's own shrink-to-fit sizing wrap the text\n // to fit the cramped space, which then \"sticks\" even after flipping to\n // a roomier position since we only move it, we don't force a width.\n // Stays hidden (visibility, not v-if) until repositioned so there's no\n // visible jump.\n _positionContextMenu() {\n const el = this.$refs.contextMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.contextMenu.x\n let y = this.contextMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.contextMenu.x = x\n this.contextMenu.y = y\n this.contextMenuPositioned = true\n },\n\n hideContextMenu() {\n this.contextMenu.visible = false\n },\n\n // General click-anywhere-in-the-grid dismissal for both floating menus —\n // each menu item stops propagation (@click.stop) so a click *inside*\n // either menu never reaches this handler.\n hideFloatingMenus() {\n this.hideContextMenu()\n this.hideSortMenu()\n },\n\n // =========================================================\n // KEYBOARD NAVIGATION (3.2)\n // =========================================================\n onKeyDown(e) {\n // Delegate keyboard navigation to the active dropdown component\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n if (comp && comp.handleKey) comp.handleKey(e)\n return\n }\n\n if (this.infoModal) {\n if (e.key === 'Escape') this.closeInfoModal()\n return\n }\n\n if (this.editingCell) {\n // Normally handled by onEditKeyDown on the built-in <input> (which stops\n // propagation, so this branch never runs for it). Custom #cell-{key} edit\n // slots own their own input and focus it themselves — if that focus didn't\n // land (e.g. the host relies on the unreliable HTML `autofocus` attribute\n // on a freshly-inserted element), Escape falls through to here instead.\n // Without this, editingCell stays stuck and even arrow-key navigation is\n // swallowed by this same guard on every subsequent keydown.\n if (e.key === 'Escape') this.cancelEdit()\n return\n }\n const key = e.key\n\n if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {\n e.preventDefault()\n this.undo()\n return\n }\n if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {\n e.preventDefault()\n this.redo()\n return\n }\n\n if (key === 'a' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.selectAll()\n return\n }\n\n if (key === 'c' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.copySelection()\n return\n }\n if (key === 'x' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.cutSelection()\n return\n }\n if (key === 'v' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.pasteAt(this.activeCell.row, this.activeCell.col)\n return\n }\n\n if (key === 'Delete' || key === 'Backspace') {\n e.preventDefault()\n this.clearSelection()\n return\n }\n\n if (key === 'F2') {\n e.preventDefault()\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openDatePickerForActive()\n } else if (activeCol && activeCol.type === 'checkbox' && activeEditable) {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else if (activeCol && activeCol.type === 'action' && activeEditable) {\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n } else if (activeEditable) {\n this.startEditing(this.activeCell.row, this.activeCell.col, null, true)\n }\n return\n }\n\n if (key === ' ') {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n return\n }\n }\n\n if (key === 'Escape') {\n this.clipboardRange = null\n this.clipboardMatrix = null\n this.clipboardMode = null\n return\n }\n\n if (key === 'Tab') {\n e.preventDefault()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n return\n }\n if (key === 'Enter') {\n e.preventDefault()\n this.moveActive(e.shiftKey ? -1 : 1, 0, e)\n return\n }\n\n const arrows = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] }\n if (arrows[key]) {\n e.preventDefault()\n const [dr, dc] = arrows[key]\n if (e.shiftKey) {\n // Extend selection\n const pr = this.primaryRange || singleCellRange(this.activeCell.row, this.activeCell.col)\n const nm = normalizeRange(pr)\n // Determine which edge to extend\n if (dr === -1) pr.startRow = Math.max(0, nm.startRow - 1)\n if (dr === 1) pr.endRow = Math.min(this.totalRows - 1, nm.endRow + 1)\n if (dc === -1) pr.startCol = Math.max(0, nm.startCol - 1)\n if (dc === 1) pr.endCol = Math.min(this.leafCols.length - 1, nm.endCol + 1)\n this.selectionRanges = [pr]\n this.emitSelectionChange()\n } else {\n this.moveActive(dr, dc, e)\n }\n return\n }\n\n if (key === 'Home') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(0, 0)\n } else {\n this.selectCell(this.activeCell.row, 0)\n }\n return\n }\n if (key === 'End') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(this.totalRows - 1, this.leafCols.length - 1)\n } else {\n this.selectCell(this.activeCell.row, this.leafCols.length - 1)\n }\n return\n }\n if (key === 'PageUp') {\n e.preventDefault()\n this.moveActive(-this._pageRowSpan(), 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n this.moveActive(this._pageRowSpan(), 0, e)\n return\n }\n },\n\n onKeyPress(e) {\n if (this.editingCell) return\n if (this.selectDropdown) return\n if (this.datePickerOverlay) return\n if (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openDatePickerForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n return\n }\n this.startEditing(this.activeCell.row, this.activeCell.col, e.key, false)\n }\n },\n\n onGlobalKeyDown(e) {\n if (e.key === 'Escape' && (this.colDrag.active || this.colDrag.pending)) {\n this._resetColDrag()\n e.preventDefault()\n }\n },\n\n moveActive(dr, dc, e) {\n let row = this.activeCell.row + dr\n let col = this.activeCell.col + dc\n\n // Tab: wrap around rows\n if (e && e.key === 'Tab') {\n if (col >= this.leafCols.length) { col = 0; row++ }\n if (col < 0) { col = this.leafCols.length - 1; row-- }\n }\n\n row = Math.max(0, Math.min(this.totalRows - 1, row))\n col = Math.max(0, Math.min(this.leafCols.length - 1, col))\n\n // Skip hidden cols\n while (this.isColHidden(col) && col > 0 && col < this.leafCols.length - 1) {\n col += dc >= 0 ? 1 : -1\n }\n // Skip hidden rows\n while (this.isRowHidden(row) && row > 0 && row < this.totalRows - 1) {\n row += dr >= 0 ? 1 : -1\n }\n\n // Handle merge navigation\n const resolved = resolveMergeAnchor(row, col, this.internalMerges)\n row = resolved.row\n col = resolved.col\n\n this.activeCell = { row, col }\n this.selectionRanges = [singleCellRange(row, col)]\n this.emitSelectionChange()\n this.scrollToCell(row, col)\n },\n\n scrollToCell(ri, ci) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rowTop = getRowTop(ri, this._rowH, this.rowHeights)\n const rowBottom = rowTop + this.getRowH(ri)\n const colLeft = this.getColLeft(ci) - this.getFrozenWidth()\n const colRight = colLeft + this.getColW(ci)\n\n if (rowTop < scrollBody.scrollTop) scrollBody.scrollTop = rowTop\n if (rowBottom > scrollBody.scrollTop + scrollBody.clientHeight) {\n scrollBody.scrollTop = rowBottom - scrollBody.clientHeight\n }\n if (colLeft < scrollBody.scrollLeft) scrollBody.scrollLeft = colLeft\n if (colRight > scrollBody.scrollLeft + scrollBody.clientWidth) {\n scrollBody.scrollLeft = colRight - scrollBody.clientWidth\n }\n },\n\n // =========================================================\n // EDITING (used by 3.1 + 3.2)\n // =========================================================\n startEditing(ri, ci, initialChar = null, keepValue = false) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (ri < 0 || ri >= this.totalRows) return\n if (!this._hasCustomEdit(col)) {\n if (this.isSelectLikeCol(col)) { this.openSelectDropdownForActive(); return }\n if (col.type === 'date') { this.openDatePickerForActive(); return }\n if (col.type === 'checkbox') return\n if (col.type === 'action') return\n }\n\n this.invalidCell = null\n this.editingCell = { row: ri, col: ci }\n if (initialChar !== null) {\n this.editValue = initialChar\n } else if (keepValue) {\n this.editValue = String(this.getCellValue(ri, ci) || '')\n } else {\n this.editValue = ''\n }\n this.editOriginalValue = String(this.getCellValue(ri, ci) || '')\n\n this.$nextTick(() => {\n const inputs = this.$refs.editInput\n if (inputs) {\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) {\n input.focus()\n if (keepValue) {\n input.setSelectionRange(input.value.length, input.value.length)\n }\n }\n }\n })\n },\n\n commitEdit() {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n\n if (!this.isCellValueValid(row, col, this.editValue)) {\n this._showInvalidCell(row, col, this.editValue)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: this.editValue }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(row, col, this.editValue, newData[row])\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n cancelEdit() {\n clearTimeout(this._onInputTimer)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // Fires while the user is actively typing in the plain-text edit input —\n // debounced (col.debounceMs, default 400ms) so col.onInput isn't called\n // on every keystroke. `row` is the pre-edit row (not yet committed);\n // `value` is the live in-progress text.\n onEditInput(e) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n this._scheduleOnInput(row, col, e.target.value)\n },\n\n _scheduleOnInput(ri, ci, value) {\n const col = this.leafCols[ci]\n if (!col || typeof col.onInput !== 'function') return\n clearTimeout(this._onInputTimer)\n this._onInputPending = true\n const delay = typeof col.debounceMs === 'number' ? col.debounceMs : 400\n this._onInputTimer = setTimeout(() => {\n this._onInputPending = false\n const row = this._dataRaw[ri]\n if (row) {\n this._lastFiredOnInput = { ri, ci, value }\n col.onInput(value, row, ri, col)\n }\n }, delay)\n },\n\n // Cancels any pending debounced call and fires col.onInput immediately —\n // used at commit points (Enter/blur, date pick, checkbox toggle) so a\n // still-in-flight debounce timer never drops the final value. If nothing\n // is pending (the debounce already fired naturally with this exact\n // value), skip re-firing — commitEdit() calls this unconditionally, so\n // without this guard a slow typist would get onInput called twice for\n // the same settled value.\n _flushOnInput(ri, ci, value, row) {\n const col = this.leafCols[ci]\n if (!col || typeof col.onInput !== 'function') return\n const wasPending = this._onInputPending\n clearTimeout(this._onInputTimer)\n this._onInputPending = false\n const last = this._lastFiredOnInput\n if (!wasPending && last && last.ri === ri && last.ci === ci && last.value === value) return\n this._lastFiredOnInput = { ri, ci, value }\n col.onInput(value, row || this._dataRaw[ri], ri, col)\n },\n\n hasCellSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])\n },\n hasLegacyEditSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])\n },\n hasPrefixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['prefix-' + col.key])\n },\n hasSuffixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['suffix-' + col.key])\n },\n _hasCustomEdit(col) {\n // For select/treeSelect/date: cell-{key} slot only customizes display; picker still opens normally\n if (col.type === 'select' || col.type === 'treeSelect' || col.type === 'date') {\n return this.hasLegacyEditSlot(col)\n }\n return this.hasCellSlot(col) || this.hasLegacyEditSlot(col)\n },\n\n commitEditSlot(value) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // CHECKBOX CELLS\n // =========================================================\n\n isCheckboxChecked(ri, ci) {\n const v = this.getCellValue(ri, ci)\n if (v === true || v === 1) return true\n if (typeof v === 'string') return ['true', '1'].includes(v.trim().toLowerCase())\n return false\n },\n\n setCheckboxValue(ri, ci, value) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (!this.isCellValueValid(ri, ci, value)) {\n this._showInvalidCell(ri, ci, value)\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[ri] = { ...oldData[ri], [col.key]: value }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(ri, ci, value, newData[ri])\n },\n\n toggleCheckbox(ri, ci) {\n this.setCheckboxValue(ri, ci, !this.isCheckboxChecked(ri, ci))\n },\n\n // =========================================================\n // ACTION CELLS (col.type === 'action')\n // =========================================================\n\n // Dynamic v-on argument expressions can't contain quotes/spaces in Vue 2.6\n // templates, so the 'click' fallback is resolved here instead of inline.\n actionHandlerEvent(col) {\n return (col && col.handlerType) || 'click'\n },\n\n triggerCellAction(ri, ci, e) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || col.type !== 'action' || typeof col.handler !== 'function') return\n if (!this.isCellEditable(ri, ci)) return\n col.handler(this._dataRaw[ri], ri, col, ci, e)\n },\n\n // =========================================================\n // CELL EDITABILITY (3.17)\n // =========================================================\n\n isCellEditable(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return false\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellEditable === 'function') return !!this.cellEditable(row, ri, col, ci)\n if (typeof col.editable === 'function') return !!col.editable(row, ri)\n if (col.editable === false) return false\n return true\n },\n\n // =========================================================\n // CELL VALUE VALIDATION (cellValidator)\n // =========================================================\n\n isCellValueValid(ri, ci, value) {\n if (typeof this.cellValidator !== 'function') return true\n const col = this.leafCols[ci]\n if (!col) return true\n const row = this._dataRaw[ri] || {}\n return !!this.cellValidator(value, row, ri, col, ci)\n },\n\n _resolveValidationMessage(ri, ci, value) {\n const col = this.leafCols[ci]\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellValidationMessage === 'function') return this.cellValidationMessage(value, row, ri, col, ci)\n if (typeof this.cellValidationMessage === 'string' && this.cellValidationMessage) return this.cellValidationMessage\n return this.t.invalidValue\n },\n\n // Find the on-screen rect of the cell currently being committed — the edit\n // input if still mounted (commitEdit, before editingCell is cleared), else\n // the active cell element (checkbox/select commits, which never enter edit mode).\n _invalidCellRect() {\n const inputs = this.$refs.editInput\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) return input.getBoundingClientRect()\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n return el ? el.getBoundingClientRect() : null\n },\n\n _showInvalidCell(ri, ci, value) {\n const message = this._resolveValidationMessage(ri, ci, value)\n this.invalidCell = { row: ri, col: ci, message, rect: this._invalidCellRect() }\n this.$emit('cell-validation-error', { row: ri, col: ci, value, message })\n },\n\n // Resolve+emit messages for a batch of rejected { row, col, value } entries\n // from a bulk write (paste / fill / clear). blocked=true means the whole\n // operation was aborted (strictBulkValidation); false means these cells\n // were skipped but the rest of the write still applied.\n _emitBulkValidationError(skipped, blocked) {\n const cells = skipped.map(c => ({\n row: c.row, col: c.col, value: c.value,\n message: this._resolveValidationMessage(c.row, c.col, c.value)\n }))\n this.$emit('bulk-validation-error', { blocked, cells })\n },\n\n // =========================================================\n // SELECT DROPDOWN (3.16) + TREE SELECT (3.22)\n // =========================================================\n\n isSelectLikeCol(col) {\n return !!(col && (col.type === 'select' || col.type === 'treeSelect'))\n },\n\n treeOptionKeys(col) {\n return {\n valueKey: col.optionValueKey || 'value',\n labelKey: col.optionLabelKey || 'label',\n childrenKey: col.optionChildrenKey || 'children'\n }\n },\n\n _treeFindLabel(nodes, keys, value) {\n for (const node of (nodes || [])) {\n if (String(node[keys.valueKey]) === value) {\n return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])\n }\n const found = this._treeFindLabel(node[keys.childrenKey], keys, value)\n if (found != null) return found\n }\n return null\n },\n\n // col.options may be a static array OR a function (row, rowIndex, column, colIndex) => array,\n // letting hosts return a different option list per cell.\n hasOptionsSource(col) {\n return !!(col && (Array.isArray(col.options) || typeof col.options === 'function'))\n },\n\n resolveOptionsArray(col, ri, ci) {\n if (!col) return []\n if (typeof col.options === 'function') {\n const row = ri != null ? (this._dataRaw[ri] || {}) : {}\n const result = col.options(row, ri, col, ci != null ? ci : col._flatIndex)\n return Array.isArray(result) ? result : []\n }\n return Array.isArray(col.options) ? col.options : []\n },\n\n colOptions(ci, ri) {\n const col = this.leafCols[ci]\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return []\n return raw.map(o =>\n typeof o === 'string' ? { value: o, label: o } : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n\n getSelectLabel(ci, value, ri) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return v\n const found = this._treeFindLabel(raw, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci, ri)\n if (!opts.length) return v\n const found = opts.find(o => String(o.value) === v)\n return found ? found.label : v\n },\n\n _buildSelectDropdownState(ri, ci, col, currentValue, rect) {\n const rectObj = { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n return {\n row: ri, col: ci,\n tree: col.type === 'treeSelect',\n keys: col.type === 'treeSelect' ? this.treeOptionKeys(col) : null,\n rawOptions: this.resolveOptionsArray(col, ri, ci),\n currentValue,\n rect: rectObj,\n enableSearch: !!col.enableSearch\n }\n },\n\n openSelectDropdown(ri, ci, e) {\n const col = this.leafCols[ci]\n if (!this.hasOptionsSource(col)) return\n const el = e.currentTarget\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(ri, ci) != null ? this.getCellValue(ri, ci) : '')\n this.selectDropdown = this._buildSelectDropdownState(ri, ci, col, currentValue, rect)\n },\n\n openSelectDropdownForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!this.hasOptionsSource(colDef)) return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(row, col) != null ? this.getCellValue(row, col) : '')\n this.selectDropdown = this._buildSelectDropdownState(row, col, colDef, currentValue, rect)\n })\n },\n\n onComponentSelect({ value, label }) {\n if (this.internalLocked) { this.closeSelectDropdown(); return }\n if (!this.selectDropdown) return\n const { row, col } = this.selectDropdown\n const colDef = this.leafCols[col]\n if (colDef) {\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.closeSelectDropdown()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n if (typeof colDef.onSelect === 'function') {\n // Deferred the same way as _fireCellCallbacksForRange (paste/fill/drag-move) —\n // a host watching the emitted data (e.g. mirroring it into its own\n // store) needs that watcher to run first, or onSelect reads stale state.\n this.$nextTick(() => colDef.onSelect({ value, label }, newData[row], row, colDef))\n }\n }\n this.closeSelectDropdown()\n },\n\n // Always returns focus to the grid wrapper — the dropdown (especially with\n // enableSearch) can hold real DOM focus on its own search input, and every\n // close path (Escape, outside click, picking a value) must hand focus back\n // or keyboard navigation on the grid goes dead until the user clicks a cell.\n closeSelectDropdown() {\n this.selectDropdown = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // DATE PICKER\n // =========================================================\n\n getCellDisplayValue(ci, val, ri) {\n const col = this.leafCols[ci]\n if (!col) return val == null ? '' : val\n if (typeof col.formatter === 'function') {\n const out = col.formatter(val, this._dataRaw[ri], ri, col, ci)\n return out == null ? '' : out\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)\n if (col.type === 'date') {\n const d = parseDateValue(val)\n return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)\n }\n return val == null ? '' : val\n },\n\n // =========================================================\n // FILTER ROW\n // =========================================================\n\n // Which control the filter cell renders for this column.\n // 'none' cells still render (empty) so column alignment stays intact.\n filterControlType(col) {\n if (!col || col.filterable === false || col.type === 'action') return 'none'\n if (col.type === 'checkbox') return 'checkbox'\n // Function-based options can't be resolved into a fixed list outside a\n // specific row, so those columns fall back to the text control.\n if ((col.type === 'select' || col.type === 'treeSelect') && Array.isArray(col.options)) return 'select'\n return 'text'\n },\n\n // Flat { value, label } list for a select-control filter cell. treeSelect\n // options are recursively flattened, each depth indented for readability.\n filterOptionsFor(col) {\n if (!col) return []\n if (col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, null, col._flatIndex)\n const out = []\n this._flattenTreeOptionsForFilter(raw, this.treeOptionKeys(col), 0, out)\n return out\n }\n return this.colOptions(col._flatIndex, null)\n },\n\n _flattenTreeOptionsForFilter(nodes, keys, depth, out) {\n for (const node of (nodes || [])) {\n const value = node[keys.valueKey]\n const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(value)\n out.push({ value: String(value), label: ' '.repeat(depth) + label })\n this._flattenTreeOptionsForFilter(node[keys.childrenKey], keys, depth + 1, out)\n }\n },\n\n getFilterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n return {\n position: 'absolute',\n left: left + 'px',\n top: (this._headerRowH * this.headerDepth) + 'px',\n width: w + 'px',\n height: this._filterRowH + 'px',\n boxSizing: 'border-box'\n }\n },\n\n setFilter(key, value) {\n if (!this.leafCols.some(c => c.key === key)) return\n const next = { ...this.internalFilters }\n if (value === '' || value === null || value === undefined) {\n delete next[key]\n } else {\n next[key] = value\n }\n this.internalFilters = next\n this.$emit('update:filters', { ...next })\n this.$emit('filter-change', { key, value, filters: { ...next } })\n },\n\n clearFilters() {\n if (!Object.keys(this.internalFilters).length) return\n this.internalFilters = {}\n // A deliberate, infrequent action (unlike typing) — recompute immediately\n // rather than waiting out the debounce, and drop any timer already pending.\n clearTimeout(this._filterRecomputeTimer)\n this._recomputeRowFilters()\n this.$emit('update:filters', {})\n this.$emit('filter-change', { key: null, value: null, filters: {} })\n },\n\n // Debounced — recomputing filteredRowIndices scans every row, so typing\n // in the text filter shouldn't trigger it on every keystroke.\n _scheduleFilterRecompute() {\n clearTimeout(this._filterRecomputeTimer)\n this._filterRecomputeTimer = setTimeout(() => { this._recomputeRowFilters() }, 150)\n },\n\n _activeFilterCols() {\n const cols = []\n for (const col of this.leafCols) {\n if (this.filterControlType(col) === 'none') continue\n const v = this.internalFilters[col.key]\n if (v === undefined || v === null || v === '') continue\n cols.push(col)\n }\n return cols\n },\n\n _cellMatchesFilter(row, ri, col) {\n const ci = col._flatIndex\n const filterValue = this.internalFilters[col.key]\n if (typeof col.filterFn === 'function') {\n return !!col.filterFn(this.getCellValue(ri, ci), filterValue, row, ri, col)\n }\n const kind = this.filterControlType(col)\n if (kind === 'checkbox') {\n const checked = this.isCheckboxChecked(ri, ci)\n return filterValue === 'true' ? checked : !checked\n }\n if (kind === 'select') {\n const raw = this.getCellValue(ri, ci)\n return String(raw == null ? '' : raw) === String(filterValue)\n }\n const display = this.getCellDisplayValue(ci, this.getCellValue(ri, ci), ri)\n return String(display == null ? '' : display).toLowerCase().indexOf(String(filterValue).toLowerCase()) !== -1\n },\n\n _recomputeRowFilters() {\n // Can be reached (via the `columns`/`filterRow` watchers) during initWatch(),\n // before created() has set up _dataRaw — getCellValue() depends on it. The\n // `data` watcher's own immediate handler sets _dataRaw first, then always\n // calls this again, so the initial mount still ends up correct.\n if (!this._dataRaw) return\n if (!this.filterRow) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const activeCols = this._activeFilterCols()\n if (!activeCols.length) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const rows = this.data\n const out = []\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n let matches = true\n for (const col of activeCols) {\n if (!this._cellMatchesFilter(row, ri, col)) { matches = false; break }\n }\n if (!matches) out.push(ri)\n }\n this.filteredRowIndices = out\n },\n\n // =========================================================\n // COLUMN SORTING\n // =========================================================\n\n isColSortable(col) {\n return !!(this.sortable && col && col.sortable !== false && col.type !== 'action')\n },\n\n sortIconClass(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return 'eg-icon-sort'\n return this.internalSort.direction === 'asc' ? 'eg-icon-sort-asc' : 'eg-icon-sort-desc'\n },\n\n sortIconTitle() {\n return this.t.sortMenuTooltip\n },\n\n // Opens the sort popup menu (A → Z / Z → A / Cancel) anchored to the\n // clicked header icon. Replaces the old direct cycle-on-click behavior.\n openSortMenu(e, col) {\n if (this.internalLocked) return\n this.sortMenu = { visible: true, x: e.clientX, y: e.clientY, key: col.key }\n this.sortMenuPositioned = false\n this.$nextTick(this._positionSortMenu)\n },\n\n // Same flip-to-fit reasoning as _positionContextMenu().\n _positionSortMenu() {\n const el = this.$refs.sortMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.sortMenu.x\n let y = this.sortMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.sortMenu.x = x\n this.sortMenu.y = y\n this.sortMenuPositioned = true\n },\n\n hideSortMenu() {\n this.sortMenu.visible = false\n },\n\n sortMenuPickAsc() {\n this.setSort(this.sortMenu.key, 'asc')\n this.hideSortMenu()\n },\n\n sortMenuPickDesc() {\n this.setSort(this.sortMenu.key, 'desc')\n this.hideSortMenu()\n },\n\n sortMenuPickCancel() {\n this.clearSort()\n this.hideSortMenu()\n },\n\n // Cycles a column through none → asc → desc → none. Kept as a public\n // method for programmatic/custom #header-{key} slot use — the built-in\n // header icon now opens the popup menu (openSortMenu) instead.\n toggleSort(key) {\n if (this.internalLocked) return\n const current = this.internalSort\n if (!current || current.key !== key) { this.setSort(key, 'asc'); return }\n if (current.direction === 'asc') { this.setSort(key, 'desc'); return }\n this.clearSort()\n },\n\n setSort(key, direction) {\n if (direction !== 'asc' && direction !== 'desc') { this.clearSort(); return }\n this.sortByColumn(key, direction)\n },\n\n // Clicking a third time (or calling this directly) only clears the sort\n // indicator — it does not restore the pre-sort row order by itself,\n // since sorting already reordered `data` in place. Use undo() for that.\n clearSort() {\n if (!this.internalSort) return\n this.internalSort = null\n this.$emit('update:sort', null)\n this.$emit('sort-change', { key: null, direction: null })\n },\n\n sortByColumn(key, direction) {\n if (this.internalLocked) return\n // Merges and row groups are anchored to row *position* — a reorder\n // would silently invalidate them, so sorting is blocked while either\n // exists (same \"blocked while locked\" no-op convention as elsewhere).\n if (this.internalMerges.length || this.rowGroups.length) return\n const col = this.leafCols.find(c => c.key === key)\n if (!this.isColSortable(col)) return\n\n const oldData = this.data\n const pairs = oldData.map((row, ri) => ({ row, ri }))\n pairs.sort((a, b) => this._compareForSort(a.row, a.ri, b.row, b.ri, col, direction))\n const newData = pairs.map(p => p.row)\n\n this.internalSort = { key, direction }\n this._emitDataChange(oldData, newData)\n this.$emit('update:sort', { key, direction })\n this.$emit('sort-change', { key, direction })\n\n this.scrollTop = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) this.$refs.scrollBody.scrollTop = 0\n })\n },\n\n _sortValueFor(row, ri, col) {\n const ci = col._flatIndex\n if (col.type === 'checkbox') return this.isCheckboxChecked(ri, ci) ? 1 : 0\n if (col.type === 'date') {\n const d = parseDateValue(this.getCellValue(ri, ci))\n return d ? d.getTime() : null\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, this.getCellValue(ri, ci), ri)\n return this.getCellValue(ri, ci)\n },\n\n _compareForSort(rowA, riA, rowB, riB, col, direction) {\n if (typeof col.sortFn === 'function') {\n const result = col.sortFn(rowA, rowB, col)\n return direction === 'desc' ? -result : result\n }\n const a = this._sortValueFor(rowA, riA, col)\n const b = this._sortValueFor(rowB, riB, col)\n const aEmpty = a === null || a === undefined || a === ''\n const bEmpty = b === null || b === undefined || b === ''\n if (aEmpty && bEmpty) return 0\n if (aEmpty) return 1\n if (bEmpty) return -1\n const cmp = (typeof a === 'number' && typeof b === 'number')\n ? a - b\n : String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' })\n return direction === 'desc' ? -cmp : cmp\n },\n\n openDatePickerForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!colDef || colDef.type !== 'date') return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const val = this.getCellValue(row, col)\n this.datePickerOverlay = {\n row, col,\n format: colDef.format || 'YYYY-MM-DD',\n value: val == null ? '' : String(val),\n rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n }\n })\n },\n\n onDatePick(formattedVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n const colDef = this.leafCols[col]\n if (colDef) {\n if (formattedVal !== '' && !this.isCellValueValid(row, col, formattedVal)) {\n this._showInvalidCell(row, col, formattedVal)\n this.closeDatePicker()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: formattedVal }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(row, col, formattedVal, newData[row])\n }\n this.closeDatePicker()\n },\n\n // Raw keystrokes while typing in the date picker's manual text field —\n // debounced, same as onEditInput(). onDatePick() above flushes the final,\n // validated value immediately when the date actually commits.\n onDateTyping(rawVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n this._scheduleOnInput(row, col, rawVal)\n },\n\n // See closeSelectDropdown() — same reasoning: every close path must return\n // focus to the grid wrapper or keyboard navigation goes dead afterward.\n closeDatePicker() {\n clearTimeout(this._onInputTimer)\n this.datePickerOverlay = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n _onWindowScrollForDropdown(e) {\n if (this.contextMenu.visible) this.hideContextMenu()\n if (this.sortMenu.visible) this.hideSortMenu()\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n _onDocMouseDownForDropdown(e) {\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n onEditKeyDown(e) {\n if (e.key === 'Escape') {\n e.preventDefault()\n this.cancelEdit()\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(1, 0, e)\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n }\n },\n\n clearSelection() {\n if (this.internalLocked) return\n if (!this.selectionRanges.length) return\n const oldData = this.data\n const newData = oldData.slice()\n let hasChanges = false\n const skipped = []\n for (const range of this.selectionRanges) {\n const r = {\n startRow: Math.min(range.startRow, range.endRow),\n endRow: Math.max(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endCol: Math.max(range.startCol, range.endCol)\n }\n for (let ri = r.startRow; ri <= r.endRow && ri < newData.length; ri++) {\n let rowUpdated = null\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, '')) { skipped.push({ row: ri, col: ci, value: '' }); continue }\n if (!rowUpdated) rowUpdated = { ...newData[ri] }\n rowUpdated[col.key] = ''\n hasChanges = true\n }\n }\n if (rowUpdated) newData[ri] = rowUpdated\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n if (hasChanges) this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // FILL HANDLE (3.3)\n // =========================================================\n startFillDrag(e) {\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n this.fillDrag.active = true\n this.fillDrag.sourceRange = normalizeRange(pr)\n this.fillDrag.currentRow = pr.endRow\n this.fillDrag.currentCol = pr.endCol\n e.preventDefault()\n },\n\n onGlobalMouseMove(e) {\n // Detect column drag threshold (5 px)\n if (this.colDrag.pending) {\n if (Math.abs(e.clientX - this.colDrag.startX) > 5) {\n this.colDrag.pending = false\n this.colDrag.active = true\n this.colDrag.dropIndex = this.colDrag.sourceFirstLeaf\n this.isMouseDown = false // cancel cell-selection drag\n }\n }\n if (this.colDrag.active) {\n this.handleColDragMove(e)\n return\n }\n\n if (this.resizing.active) {\n this.handleResizeMove(e)\n return\n }\n if (this.fillDrag.active) {\n this.handleFillDragMove(e)\n return\n }\n if (this.dragMove.active) {\n this.handleDragMoveMove(e)\n return\n }\n },\n\n onGlobalMouseUp(e) {\n if (this.colDrag.active) {\n this.commitColDrag()\n return\n }\n if (this.colDrag.pending) {\n this._resetColDrag()\n // fall through — normal mouseup\n }\n\n if (this.resizing.active) {\n this.commitResize()\n return\n }\n if (this.fillDrag.active) {\n this.commitFillDrag()\n return\n }\n if (this.dragMove.active) {\n this.commitDragMove(e)\n return\n }\n this.isMouseDown = false\n },\n\n handleFillDragMove(e) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rect = scrollBody.getBoundingClientRect()\n const relY = e.clientY - rect.top + scrollBody.scrollTop\n const relX = e.clientX - rect.left + scrollBody.scrollLeft\n\n // Binary search on cumulativeHeights — O(log n) instead of O(n)\n const cum = this.cumulativeHeights\n const total = this.totalRows\n let lo = 0, hi = total - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (cum[mid + 1] <= relY) lo = mid + 1\n else hi = mid\n }\n const row = Math.min(lo, total - 1)\n\n // Columns are few — O(columns) is fast\n let left = this.getFrozenWidth()\n let col = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColumnScrollable(i)) continue\n left += this.getColW(i)\n if (relX < left) { col = i; break }\n col = i\n }\n\n this.fillDrag.currentRow = row\n this.fillDrag.currentCol = col\n },\n\n async commitFillDrag() {\n if (this.internalLocked) { this.fillDrag.active = false; return }\n const src = this.fillDrag.sourceRange\n if (!src) { this.fillDrag.active = false; return }\n\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n\n const oldData = this.data\n const newData = oldData.slice() // only affected rows get new objects\n let targetRange\n const skipped = []\n\n if (targetRow > src.endRow) {\n // Fill down\n const count = targetRow - src.endRow\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n targetRange = { startRow: src.endRow + 1, endRow: targetRow, startCol: src.startCol, endCol: src.endCol }\n } else if (targetCol > src.endCol) {\n // Fill right\n const count = targetCol - src.endCol\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n const srcValues = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n srcValues.push(col && oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ci = src.endCol + 1 + k\n if (ci < this.leafCols.length) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n }\n targetRange = { startRow: src.startRow, endRow: src.endRow, startCol: src.endCol + 1, endCol: targetCol }\n }\n\n let fireOnSelect = false\n if (targetRange) {\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n } else {\n this._emitDataChange(oldData, newData)\n this.$emit('fill', { sourceRange: src, targetRange, values: newData })\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n fireOnSelect = true\n }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\n\n if (fireOnSelect) {\n // See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n doubleFillDown() {\n const pr = this.primaryRange\n if (!pr) return\n const src = normalizeRange(pr)\n // Find adjacent column filled length\n const adjCol = src.startCol > 0 ? src.startCol - 1 : src.endCol + 1\n if (adjCol < 0 || adjCol >= this.leafCols.length) return\n const adjColDef = this.leafCols[adjCol]\n if (!adjColDef) return\n let lastFilledRow = src.endRow\n for (let ri = src.endRow + 1; ri < this.totalRows; ri++) {\n if (this.data[ri] && this.data[ri][adjColDef.key] !== '' && this.data[ri][adjColDef.key] !== undefined) {\n lastFilledRow = ri\n } else break\n }\n if (lastFilledRow <= src.endRow) return\n\n const count = lastFilledRow - src.endRow\n const oldData = this.data\n const newData = oldData.slice()\n const skipped = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // COPY / PASTE (3.4)\n // =========================================================\n copySelection() {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n this.clipboardMatrix = extractMatrix(this.data, this.leafCols, r)\n this.clipboardRange = r\n this.clipboardMode = 'copy'\n\n // Write to system clipboard as TSV\n const tsv = matrixToTSV(this.clipboardMatrix)\n if (navigator.clipboard) {\n navigator.clipboard.writeText(tsv).catch(() => {})\n }\n },\n\n cutSelection() {\n this.copySelection()\n this.clipboardMode = 'cut'\n },\n\n // Bulk cell writes (paste, fill handle, drag-move) set row values directly\n // and skip the single-cell paths (onComponentSelect / commitEdit /\n // setCheckboxValue / onDatePick) — so col.onSelect / col.onInput never\n // fire for them unless we replay it here for every cell whose value\n // actually changed in the range. select/treeSelect columns get onSelect\n // (matching the single-pick shape); everything else gets onInput —\n // fired immediately, no debounce (this is already a single bulk commit,\n // not live keystrokes).\n _fireCellCallbacksForRange(oldData, newData, startRow, endRow, startCol, endCol) {\n for (let ri = startRow; ri <= endRow; ri++) {\n const oldRow = oldData[ri]\n const newRow = newData[ri]\n if (!newRow || newRow === oldRow) continue\n for (let ci = startCol; ci <= endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const oldVal = oldRow ? oldRow[col.key] : undefined\n const newVal = newRow[col.key]\n if (newVal === oldVal) continue\n if (this.isSelectLikeCol(col)) {\n if (typeof col.onSelect === 'function') {\n col.onSelect({ value: newVal, label: this.getSelectLabel(ci, newVal, ri) }, newRow, ri, col)\n }\n } else if (typeof col.onInput === 'function') {\n col.onInput(newVal, newRow, ri, col)\n }\n }\n }\n },\n\n async pasteAt(targetRow, targetCol, options = {}) {\n if (this.internalLocked) return\n const { mode = 'all', transpose = false } = options\n\n let matrix = null\n\n // Try system clipboard first\n if (navigator.clipboard) {\n try {\n const text = await navigator.clipboard.readText()\n if (text) matrix = tsvToMatrix(text)\n } catch (_) {}\n }\n\n // Fall back to internal clipboard\n if (!matrix && this.clipboardMatrix) {\n matrix = this.clipboardMatrix\n }\n\n if (!matrix) return\n\n if (transpose) matrix = transposeMatrix(matrix)\n\n // ── Excel-style selection broadcast / tiling ──────────────────────────\n // When pasting over a multi-cell selection and the clipboard matrix is\n // smaller than the selection, tile the matrix to fill the selection —\n // provided the selection dimensions are exact multiples of the matrix\n // dimensions (matches Excel's tiling rule exactly).\n // Cut-paste is never tiled (source must be cleared exactly once).\n const isCopyPaste = !this.clipboardMode || this.clipboardMode === 'copy'\n if (isCopyPaste) {\n const pr = this.primaryRange\n if (pr) {\n const sel = normalizeRange(pr)\n const selRows = sel.endRow - sel.startRow + 1\n const selCols = sel.endCol - sel.startCol + 1\n const matRows = matrix.length\n const matCols = matrix[0] ? matrix[0].length : 0\n\n // Only tile when selection is strictly larger than clipboard\n if ((selRows > matRows || selCols > matCols) &&\n selRows % matRows === 0 && selCols % matCols === 0) {\n matrix = tileMatrix(matrix, selRows, selCols)\n // Paste at selection origin, not just at the active cell\n targetRow = sel.startRow\n targetCol = sel.startCol\n }\n }\n }\n\n const oldData = this.data\n const { newData, affectedRange, skipped } = applyMatrix(\n oldData, this.leafCols, targetRow, targetCol, matrix, mode,\n (ri, ci) => this.isCellEditable(ri, ci),\n (ri, ci, v) => this.isCellValueValid(ri, ci, v)\n )\n\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n\n if (this.clipboardMode === 'cut' && this.clipboardRange) {\n // Clear source — ensure each source row is a new copy before mutation\n const src = this.clipboardRange\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci) && this.isCellValueValid(ri, ci, '')) {\n newData[ri][col.key] = ''\n }\n }\n }\n this.clipboardRange = null\n this.clipboardMode = null\n }\n\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n // Wait for Vue's reactivity queue to flush before firing onSelect — hosts\n // commonly react to the emitted 'input' with their own watcher (e.g. to\n // mirror the row into an external store); firing onSelect synchronously\n // would run it against that store's pre-paste, stale state.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, affectedRange.startRow, affectedRange.endRow, affectedRange.startCol, affectedRange.endCol)\n },\n\n ctxCopy() { this.hideContextMenu(); this.copySelection() },\n ctxCut() { this.hideContextMenu(); this.cutSelection() },\n ctxPaste() { this.hideContextMenu(); this.pasteAt(this.activeCell.row, this.activeCell.col) },\n ctxPasteSpecial(mode, transpose = false) {\n this.hideContextMenu()\n this.pasteAt(this.activeCell.row, this.activeCell.col, { mode, transpose })\n },\n\n // =========================================================\n // COLUMN DRAG-AND-DROP\n // =========================================================\n\n _computeDragOffsets(dropIndex) {\n const { sourceFirstLeaf, sourceLeafCount } = this.colDrag\n if (dropIndex < 0 || (dropIndex >= sourceFirstLeaf && dropIndex <= sourceFirstLeaf + sourceLeafCount)) return {}\n\n const previewCols = moveColumnNodeInTree(this.columns, sourceFirstLeaf, sourceLeafCount, dropIndex)\n const previewLeaves = flattenColumns(previewCols)\n\n const byKey = {}\n this.leafCols.forEach(c => { byKey[c.key] = c })\n\n const offsets = {}\n for (const zone of ['left', 'scroll', 'right']) {\n const inZone = zone === 'left'\n ? ci => this.isColumnPinnedLeft(ci)\n : zone === 'right'\n ? ci => this.isColumnPinnedRight(ci)\n : ci => this.isColumnScrollable(ci)\n\n const origPos = {}\n let acc = 0\n this.leafCols.forEach(col => {\n const ci = col._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) return\n origPos[col.key] = acc\n acc += this.getColW(ci)\n })\n\n const previewPos = {}\n acc = 0\n for (const col of previewLeaves) {\n const origCol = byKey[col.key]\n if (!origCol) continue\n const ci = origCol._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) continue\n previewPos[col.key] = acc\n acc += this.getColW(ci)\n }\n\n for (const key of Object.keys(origPos)) {\n offsets[key] = (previewPos[key] !== undefined ? previewPos[key] : origPos[key]) - origPos[key]\n }\n }\n return offsets\n },\n\n _applyDragTransforms(offsets) {\n if (!this.$el) return\n const targets = []\n const dxMap = new Map()\n\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n targets.push(el)\n dxMap.set(el, dx)\n })\n this.$el.querySelectorAll('[data-start]').forEach(el => {\n const start = parseInt(el.dataset.start)\n const end = parseInt(el.dataset.end)\n if (isNaN(start)) return\n let key = null\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n targets.push(el)\n dxMap.set(el, key ? (offsets[key] || 0) : 0)\n })\n if (!targets.length) return\n\n // anime.remove cancels any in-progress animation before starting the new one.\n // This prevents chaining/overlap when dropIndex changes rapidly (no flickering).\n anime.remove(targets)\n anime({\n targets,\n translateX: el => dxMap.get(el) || 0,\n duration: 80,\n easing: 'easeOutQuart'\n })\n },\n\n _clearDragTransforms() {\n if (!this.$el) return\n const els = Array.from(this.$el.querySelectorAll('[data-fi], [data-start]'))\n // Kill all anime animations first, then instantly clear inline styles\n anime.remove(els)\n els.forEach(el => {\n el.style.transform = ''\n el.style.transition = ''\n })\n },\n\n handleColDragMove(e) {\n // Update ghost position via direct DOM — avoids Vue re-render on every mousemove\n const ghost = this.$refs.colDragGhost\n if (ghost) {\n ghost.style.left = (e.clientX + 16) + 'px'\n ghost.style.top = (e.clientY - 14) + 'px'\n }\n\n const prevDrop = this.colDrag.dropIndex\n this._updateColDragDropIndex(e.clientX)\n const newDrop = this.colDrag.dropIndex\n\n if (newDrop !== prevDrop) {\n this._dragOffsets = this._computeDragOffsets(newDrop)\n this._applyDragTransforms(this._dragOffsets)\n }\n },\n\n _updateColDragDropIndex(clientX) {\n if (!this.$el) return\n const n = this.leafCols.length\n if (n === 0) { this.colDrag.dropIndex = 0; return }\n\n // Use DOM cell rects but subtract CSS transforms so we get ORIGINAL positions.\n // This avoids circular dependency: live-preview transforms don't affect drop calc.\n const cells = Array.from(this.$el.querySelectorAll('.eg-header-cell:not(.eg-header-group)'))\n .map(el => {\n const fi = parseInt(el.dataset.fi)\n if (isNaN(fi)) return null\n const rect = el.getBoundingClientRect()\n const m = (el.style.transform || '').match(/translateX\\((-?[\\d.]+)px\\)/)\n const dx = m ? parseFloat(m[1]) : 0\n return { fi, left: rect.left - dx, right: rect.right - dx }\n })\n .filter(Boolean)\n .sort((a, b) => a.left - b.left)\n\n if (!cells.length) { this.colDrag.dropIndex = n; return }\n\n let dropIndex = n\n for (const { fi, left, right } of cells) {\n if (clientX < right) {\n dropIndex = clientX < left + (right - left) / 2 ? fi : fi + 1\n break\n }\n }\n this.colDrag.dropIndex = dropIndex\n },\n\n commitColDrag() {\n const from = this.colDrag.sourceFirstLeaf\n const leafCount = this.colDrag.sourceLeafCount\n const to = this.colDrag.dropIndex\n\n if (from < 0 || to < 0 || leafCount < 1) { this._resetColDrag(); return }\n if (to >= from && to <= from + leafCount) { this._resetColDrag(); return }\n\n // Kill anime animations but KEEP transform values in DOM while Vue re-renders\n // with the new column order. After Vue updates left positions ($nextTick),\n // we instantly clear transforms — both steps run as microtasks before browser paints,\n // so there is never a visible intermediate state.\n if (this.$el) {\n anime.remove(Array.from(this.$el.querySelectorAll('[data-fi], [data-start]')))\n }\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n\n const newCols = moveColumnNodeInTree(this.columns, from, leafCount, to)\n this.$emit('update:columns', newCols)\n this.$emit('column-reorder', { fromIndex: from, toIndex: to, columns: newCols })\n\n // Clear transforms after Vue re-renders new column left positions\n this.$nextTick(() => { this._clearDragTransforms() })\n },\n\n _resetColDrag() {\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n this._clearDragTransforms()\n },\n\n // =========================================================\n // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'col',\n index: ci,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getColW(ci),\n currentPos: e.clientX,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'row',\n index: ri,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getRowH(ri),\n currentPos: e.clientY,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n handleResizeMove(e) {\n if (this.resizing.type === 'col') {\n this.resizing.currentPos = e.clientX\n const newWidth = Math.max(20, this.resizing.startSize + (e.clientX - this.resizing.startX))\n this.$set(this.colWidths, this.resizing.index, newWidth)\n this._cellStyleCache = Object.create(null)\n } else {\n this.resizing.currentPos = e.clientY\n this._resizePreviewHeight = Math.max(10, this.resizing.startSize + (e.clientY - this.resizing.startY))\n // Batch $set + updateVirtualScroll to rAF — prevents O(n) cumulativeHeights rebuild\n // on every mousemove (200+ Hz). At most 60 rebuilds/sec instead of 200+.\n if (!this._resizeRAF) {\n this._resizeRAF = requestAnimationFrame(() => {\n this._resizeRAF = null\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n this.updateVirtualScroll()\n })\n }\n }\n },\n\n commitResize() {\n if (this._resizeRAF) {\n cancelAnimationFrame(this._resizeRAF)\n this._resizeRAF = null\n // Apply the last pending preview height immediately on commit\n if (this.resizing.type === 'row' && this._resizePreviewHeight !== undefined) {\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n }\n }\n if (this.resizing.type === 'col') {\n this._cellStyleCache = Object.create(null)\n const newSize = this.colWidths[this.resizing.index]\n this.$emit('column-resize', { index: this.resizing.index, newSize })\n } else {\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n const newSize = this.rowHeights[this.resizing.index]\n this.$emit('row-resize', { index: this.resizing.index, newSize })\n this.updateVirtualScroll()\n }\n this.resizing.active = false\n },\n\n autoFitColumn(ci) {\n const col = this.leafCols[ci]\n if (!col) return\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n const newWidth = Math.max(50, maxLen * 8 + 16)\n this.$set(this.colWidths, ci, newWidth)\n this._cellStyleCache = Object.create(null)\n this.$emit('column-resize', { index: ci, newSize: newWidth })\n },\n\n _applyAutoWidth() {\n if (!this.autoWidth || !this.leafCols.length) return\n this.leafCols.forEach((col, ci) => {\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n this.$set(this.colWidths, ci, Math.max(50, maxLen * 8 + 16))\n })\n this._cellStyleCache = Object.create(null)\n },\n\n ctxHideRow() {\n const targetRow = this.contextMenu.row\n // Row header: row >= 0 AND col = -1 (no column context)\n const fromRowHeader = targetRow >= 0 && this.contextMenu.col < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenRows]\n if (fromRowHeader) {\n if (!newHidden.includes(targetRow)) newHidden.push(targetRow)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n const targetCol = this.contextMenu.col\n // Column header: col >= 0 AND row = -1 (no row context)\n const fromColHeader = targetCol >= 0 && this.contextMenu.row < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenColumns]\n if (fromColHeader) {\n if (!newHidden.includes(targetCol)) newHidden.push(targetCol)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\n }\n }\n this.$emit('update:hiddenColumns', newHidden)\n },\n\n ctxUnhideRows() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenColumns', [])\n },\n\n ctxPinLeft() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n },\n\n isColPinned(ci) {\n const col = this.leafCols[ci]\n return !!(col && this.pinnedColumns[col.key])\n },\n\n // =========================================================\n // DEV MODE — column/row config info (context menu → modal)\n // =========================================================\n _stringifyForInfo(value) {\n const seen = new WeakSet()\n return JSON.stringify(value, (key, val) => {\n if (typeof val === 'function') return 'ƒ ' + (val.name || 'anonymous') + '()'\n if (val && typeof val === 'object') {\n if (seen.has(val)) return '[Circular]'\n seen.add(val)\n }\n return val\n }, 2)\n },\n\n ctxShowColumnInfo() {\n const ci = this.contextMenu.col\n this.hideContextMenu()\n const col = this.leafCols[ci]\n if (!col) return\n const { _flatIndex, ...rest } = col\n this.infoModal = { title: `${this.t.columnInfo}: ${col.title || col.key}`, json: this._stringifyForInfo(rest) }\n },\n\n ctxShowRowInfo() {\n const ri = this.contextMenu.row\n this.hideContextMenu()\n const row = this._dataRaw[ri]\n if (!row) return\n this.infoModal = { title: `${this.t.rowInfo} #${ri + 1}`, json: this._stringifyForInfo(row) }\n },\n\n closeInfoModal() {\n this.infoModal = null\n },\n\n // =========================================================\n // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\n if (this.internalLocked) return false\n const oldMerges = this.internalMerges.slice()\n const { merges: newMerges, conflict } = addMerge(range, this.internalMerges)\n if (conflict) {\n this.$emit('merge-error', { range, reason: 'Overlaps existing merge' })\n return false\n }\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n return true\n },\n\n unmergeCells(row, col) {\n if (this.internalLocked) return\n const oldMerges = this.internalMerges.slice()\n const newMerges = removeMergeAt(row, col, this.internalMerges)\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n },\n\n ctxMerge() {\n this.hideContextMenu()\n const pr = this.primaryRange\n if (!pr) return\n this.mergeCells(normalizeRange(pr))\n },\n\n ctxUnmerge() {\n this.hideContextMenu()\n this.unmergeCells(this.activeCell.row, this.activeCell.col)\n },\n\n // =========================================================\n // GROUPING (3.7)\n // =========================================================\n groupRows(startIndex, endIndex) {\n this.internalGroups = addGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n groupColumns(startIndex, endIndex) {\n this.internalGroups = addGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupRows(startIndex, endIndex) {\n this.internalGroups = removeGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupColumns(startIndex, endIndex) {\n this.internalGroups = removeGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n toggleGroup(group) {\n this.internalCollapsedGroups = toggleGroupCollapse(group, this.internalCollapsedGroups)\n this.$emit('update:collapsedGroups', [...this.internalCollapsedGroups])\n this.$emit('group-toggle', { group, collapsed: _isGroupCollapsed(group, this.internalCollapsedGroups) })\n this.updateVirtualScroll()\n },\n\n isGroupCollapsed(group) {\n return _isGroupCollapsed(group, this.internalCollapsedGroups)\n },\n\n expandAll() {\n this.internalCollapsedGroups = []\n this.$emit('update:collapsedGroups', [])\n this.updateVirtualScroll()\n },\n\n collapseAll() {\n this.internalCollapsedGroups = [...this.internalGroups]\n this.$emit('update:collapsedGroups', [...this.internalGroups])\n this.updateVirtualScroll()\n },\n\n // =========================================================\n // DRAG AND DROP (3.9)\n // =========================================================\n startDragMove(e, ri, ci) {\n const pr = this.primaryRange\n if (!pr) return\n this.dragMove = {\n active: true,\n sourceRange: normalizeRange(pr),\n startX: e.clientX,\n startY: e.clientY,\n targetRow: ri,\n targetCol: ci,\n copyMode: e.ctrlKey || e.metaKey\n }\n this.$emit('cell-drag-start', { sourceRange: this.dragMove.sourceRange })\n e.preventDefault()\n },\n\n handleDragMoveMove(e) {\n this.dragMove.copyMode = e.ctrlKey || e.metaKey\n this.$emit('cell-drag-move', {\n sourceRange: this.dragMove.sourceRange,\n targetRow: this.dragMove.targetRow,\n targetCol: this.dragMove.targetCol\n })\n },\n\n async commitDragMove(e) {\n if (!this.dragMove.active) return\n const { sourceRange, targetRow, targetCol, copyMode } = this.dragMove\n\n const targetRange = {\n startRow: targetRow,\n startCol: targetCol,\n endRow: targetRow + (sourceRange.endRow - sourceRange.startRow),\n endCol: targetCol + (sourceRange.endCol - sourceRange.startCol)\n }\n\n let proceed = true\n if (this.beforeDrop) {\n proceed = await Promise.resolve(this.beforeDrop(sourceRange, targetRange))\n }\n\n let oldData = null\n let newData = null\n if (proceed) {\n oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n newData = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci)).newData\n\n if (!copyMode) {\n // Clear source — ensure each source row is a new copy before mutation\n for (let ri = sourceRange.startRow; ri <= sourceRange.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = sourceRange.startCol; ci <= sourceRange.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) newData[ri][col.key] = ''\n }\n }\n }\n this._emitDataChange(oldData, newData)\n this.$emit('cell-drop', { sourceRange, targetRange })\n }\n\n this.dragMove = { active: false, sourceRange: null, startX: 0, startY: 0, targetRow: -1, targetCol: -1, copyMode: false }\n this.isMouseDown = false\n\n if (proceed) {\n // See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n // =========================================================\n // EXCEL EXPORT (3.11)\n // =========================================================\n async exportToExcel(options = {}) {\n const {\n filename = 'export.xlsx',\n sheetName = 'Sheet1',\n scope = 'all',\n includeHidden = false\n } = options\n\n this.$emit('export-start')\n this.exportState = { active: true }\n\n const workerPayload = {\n data: this.data,\n columns: this.columns,\n leafCols: this.leafCols,\n merges: this.internalMerges,\n hiddenCols: [...this.effectiveHiddenColSet],\n hiddenRows: [...this.hiddenRowSet],\n range: scope === 'selection' ? this.primaryRange : null,\n filename,\n sheetName,\n scope,\n includeHidden\n }\n\n try {\n const worker = createExcelExportWorker()\n if (worker) {\n await this._exportViaWorker(worker, workerPayload)\n } else {\n // Fallback: synchronous export on main thread\n _exportToExcel({\n ...workerPayload,\n hiddenColSet: this.effectiveHiddenColSet,\n hiddenRowSet: this.hiddenRowSet\n })\n }\n this.$emit('export-success')\n } catch (err) {\n this.$emit('export-error', err)\n } finally {\n this.exportState = { active: false }\n }\n },\n\n _exportViaWorker(worker, payload) {\n return new Promise((resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, buffer, filename, message } = e.data\n if (type === 'result') {\n // Trigger browser download from the main thread (DOM required)\n const blob = new Blob([new Uint8Array(buffer)], {\n type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n finish(resolve)\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Export worker xatosi'))\n\n worker.postMessage(payload)\n })\n },\n\n // =========================================================\n // EXCEL IMPORT (3.12)\n // =========================================================\n async importFromFile(file, options = {}) {\n this.importState = { active: true, percent: 5, message: \"Fayl o'qilmoqda...\" }\n\n try {\n let result\n const worker = createExcelImportWorker()\n\n if (worker) {\n result = await this._importViaWorker(worker, file, options)\n } else {\n // Fallback: chunked async on main thread\n result = await _importFromFile(\n file,\n this.columns,\n options,\n (percent, message) => { this.importState = { active: true, percent, message } }\n )\n }\n\n const { columns, merges } = result\n // Freeze each row so Vue 2 skips deep Object.defineProperty observation.\n // Vue 2 checks Object.isExtensible() — frozen objects are skipped entirely.\n // All edit paths already use spread ({ ...row }) so frozen rows are safe.\n const data = result.data.map(row => Object.freeze(row))\n this.$emit('input', data)\n this.$emit('update:columns', columns)\n\n const hasMerges = merges && merges.length > 0\n this.internalMerges = hasMerges ? merges : []\n this.$emit('update:merges', hasMerges ? merges : [])\n this.$emit('update:collapsedGroups', [])\n this.$emit('update:hiddenRows', [])\n this.$emit('import-success', { data, columns, merges })\n\n // Reset scroll state and re-derive virtual scroll after DOM settles.\n // Without this, stale scrollTop + old rowHeights cause blank bands on scroll.\n this.rowHeights = {}\n this.scrollTop = 0\n this.scrollLeft = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) {\n this.$refs.scrollBody.scrollTop = 0\n this.$refs.scrollBody.scrollLeft = 0\n }\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = 0\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = 0\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = 0\n this.updateVirtualScroll()\n })\n } catch (err) {\n this.$emit('import-error', err)\n } finally {\n this.importState = { active: false, percent: 0, message: '' }\n }\n },\n\n _importViaWorker(worker, file, options) {\n return new Promise(async (resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, percent, message, data, columns, merges } = e.data\n if (type === 'progress') {\n this.importState = { active: true, percent, message }\n } else if (type === 'result') {\n finish(resolve, { data, columns, merges })\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Import worker xatosi'))\n\n try {\n // Transfer the ArrayBuffer zero-copy into the worker\n const buffer = await file.arrayBuffer()\n worker.postMessage(\n { buffer, existingColumns: this.columns, options },\n [buffer]\n )\n } catch (err) {\n finish(reject, err)\n }\n })\n },\n\n // =========================================================\n // UNDO / REDO (3.13)\n // =========================================================\n\n // Push one entry onto the non-reactive history stack.\n // Truncates the redo tail, caps at 100 entries.\n _pushHistory(entry) {\n if (this._historyIndex < this._history.length - 1) {\n this._history.splice(this._historyIndex + 1)\n }\n this._history.push(entry)\n if (this._history.length > 100) {\n this._history.shift()\n // historyIndex stays the same — it now points to the new last entry\n } else {\n this._historyIndex++\n }\n this.historyVersion++\n },\n\n // Emit 'input' and record a data diff entry (only changed row indices).\n // Use slice() + per-row spread in callers so unchanged rows keep the\n // same reference — then the identity check here catches only real changes.\n _emitDataChange(oldData, newData) {\n if (!this._historySkip) {\n const indices = []\n const beforeRows = []\n const afterRows = []\n const len = Math.max(oldData.length, newData.length)\n for (let i = 0; i < len; i++) {\n if (oldData[i] !== newData[i]) {\n indices.push(i)\n beforeRows.push(oldData[i])\n afterRows.push(newData[i])\n }\n }\n if (indices.length > 0) {\n this._pushHistory({\n type: 'data',\n before: { indices, rows: beforeRows },\n after: { indices, rows: afterRows }\n })\n }\n }\n this.$emit('input', newData)\n },\n\n // Emit 'update:merges' and record a merges diff entry.\n _emitMergeChange(oldMerges, newMerges) {\n if (!this._historySkip) {\n this._pushHistory({\n type: 'merges',\n before: oldMerges,\n after: newMerges\n })\n }\n this.$emit('update:merges', newMerges)\n },\n\n undo() {\n if (this.internalLocked) return\n if (this._historyIndex < 0) return\n const entry = this._history[this._historyIndex--]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.before.indices.length; i++) {\n newData[entry.before.indices[i]] = entry.before.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.before.slice()\n this.$emit('update:merges', entry.before.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n redo() {\n if (this.internalLocked) return\n if (this._historyIndex >= this._history.length - 1) return\n const entry = this._history[++this._historyIndex]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.after.indices.length; i++) {\n newData[entry.after.indices[i]] = entry.after.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.after.slice()\n this.$emit('update:merges', entry.after.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n // =========================================================\n // TOOLBAR (fullscreen / lock / export / import / custom buttons)\n // =========================================================\n\n // Public, ref-callable: returns keyboard focus to the grid. Meant for\n // hosts whose action-cell `handler` opens a modal/popup outside the\n // grid's control — call this once that modal closes, otherwise the\n // browser leaves focus on document.body (its subtree was removed) and\n // arrow-key navigation goes dead until a cell is clicked again.\n focus() {\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n toggleLock() {\n this.internalLocked = !this.internalLocked\n this.$emit('update:locked', this.internalLocked)\n this.$emit('lock-change', { locked: this.internalLocked })\n },\n\n // Pops a big lock/unlock icon in the center of the grid and fades it\n // out — pure visual feedback, mirrors the video-player OSD pattern.\n // Triggered by the internalLocked watcher, so it covers every path that\n // can change the lock state (toolbar click, toggleLock(), :locked.sync).\n _flashLock(locked) {\n this.lockFlashLocked = locked\n this.lockFlashVisible = true\n this.$nextTick(() => {\n const el = this.$refs.lockFlashIconEl\n if (!el) return\n anime.remove(el)\n anime.set(el, { opacity: 0, scale: 0.5 })\n anime.timeline()\n .add({ targets: el, opacity: 1, scale: 1.12, duration: 180, easing: 'easeOutBack' })\n .add({ targets: el, scale: 1, duration: 120, easing: 'easeOutQuad' })\n .add({ targets: el, opacity: 0, duration: 350, easing: 'easeInQuad' }, '+=250')\n .finished.then(() => { this.lockFlashVisible = false })\n })\n },\n\n toolbarHas(key) {\n return this._toolbarList.indexOf(key) !== -1\n },\n\n setSize(newSize) {\n if (!SIZE_CONFIG[newSize] || newSize === this.internalSize) return\n this.internalSize = newSize\n this.$emit('update:size', newSize)\n this.$emit('size-change', { size: newSize })\n },\n\n setTheme(newTheme) {\n if (THEME_LIST.indexOf(newTheme) === -1 || newTheme === this.internalTheme) return\n this.internalTheme = newTheme\n this.$emit('update:theme', newTheme)\n this.$emit('theme-change', { theme: newTheme })\n },\n\n sizeTitle(key) {\n return { small: this.t.sizeSmall, middle: this.t.sizeMiddle, large: this.t.sizeLarge }[key] || key\n },\n\n toggleFullscreen() {\n this.isFullscreen = !this.isFullscreen\n if (this.isFullscreen) {\n this._prevBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n document.addEventListener('keydown', this._onFullscreenKeyDown)\n } else {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n this.$emit('fullscreen-change', { fullscreen: this.isFullscreen })\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n _onFullscreenKeyDown(e) {\n if (e.key === 'Escape' && this.isFullscreen) this.toggleFullscreen()\n },\n\n toolbarExportClick() {\n this.exportToExcel()\n },\n\n toolbarImportClick() {\n if (this.internalLocked) return\n const input = this.$refs.toolbarImportInput\n if (input) input.click()\n },\n\n onToolbarImportFile(e) {\n const file = e.target.files && e.target.files[0]\n e.target.value = ''\n if (!file || this.internalLocked) return\n this.importFromFile(file)\n },\n\n onToolbarButtonClick(btn, event) {\n if (typeof btn.onClick === 'function') btn.onClick(event, this)\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-shell {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n\n.eg-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n\n.eg-toolbar-group + .eg-toolbar-group {\n border-left: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-spacer {\n flex: 1 1 auto;\n}\n\n.eg-toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n\n.eg-toolbar-btn:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n\n.eg-toolbar-btn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.eg-toolbar-btn-active {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n\n.eg-toolbar-select {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n\n.eg-toolbar-select:hover {\n border-color: var(--eg-color-accent);\n}\n\n.eg-toolbar-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n\n.eg-toolbar-segmented {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-toolbar-segment {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon { width: 12px; height: 12px; }\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon { width: 15px; height: 15px; }\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon { width: 18px; height: 18px; }\n\n.eg-toolbar-icon {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n\n.eg-icon-undo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-redo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-export { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); }\n.eg-icon-import { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); }\n.eg-icon-lock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); }\n.eg-icon-unlock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); }\n.eg-icon-expand { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); }\n.eg-icon-compress{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); }\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-asc { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-desc{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); }\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-middle { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-large { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); }\n\n.eg-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n\n.eg-header-area {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n\n.eg-header-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n\n.eg-header-cell:hover { background: var(--eg-color-border-cell); }\n.eg-header-group { background: var(--eg-color-header-group-bg); }\n\n.eg-sort-icon {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon { opacity: 0.65; }\n.eg-sort-icon:hover { opacity: 1; }\n.eg-sort-icon.eg-icon-sort-asc,\n.eg-sort-icon.eg-icon-sort-desc {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty { background: var(--eg-color-header-bg); }\n\n.eg-filter-input,\n.eg-filter-select {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input:focus,\n.eg-filter-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n\n.eg-col-resize-handle {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n\n.eg-body-area {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area {\n border-top: 1px solid var(--eg-color-border);\n}\n\n.eg-frozen-col-footer,\n.eg-scroll-col-footer,\n.eg-pinned-right-footer {\n position: relative;\n}\n\n.eg-footer-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n/* Cells that actually show a computed total (col.showTotal / totalConfig.visible)\n stand out from the rest of the (otherwise mostly-empty) footer row. */\n.eg-footer-cell-total {\n font-weight: 700;\n}\n\n.eg-row {\n position: relative;\n}\n\n.eg-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n\n.eg-locked .eg-cell { cursor: not-allowed; }\n\n.eg-cell-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right { justify-content: flex-end; }\n.eg-cell-align-center { justify-content: center; }\n.eg-cell--has-suffix.eg-cell-select::after { display: none; }\n.eg-cell-select-trigger-wrap {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: var(--eg-color-cell-selected) !important;\n}\n\n.eg-cell-fill-preview {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n\n.eg-cell-merged-hidden {\n display: none !important;\n}\n\n.eg-cell-invalid {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n\n.eg-cell-input {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-row-resize-handle {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n\n.eg-fill-handle {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n\n/* Only 3 short items (\"A → Z\" / \"Z → A\" / \"Clear sort\") — the generic\n 160px context-menu min-width looks oversized next to it, especially\n at size=\"small\" where rows/text are much smaller. */\n.eg-sort-menu {\n min-width: 110px;\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n\n.eg-context-item:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n\n.eg-context-separator {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n\n.eg-context-submenu-parent::after { content: ' ▶'; float: right; }\n\n.eg-context-submenu {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n\n.eg-context-submenu-parent:hover .eg-context-submenu { display: block; }\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-info-modal {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n\n.eg-info-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n\n.eg-info-modal-title {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n\n.eg-info-modal-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n\n.eg-info-modal-close:hover { color: var(--eg-color-text); }\n\n.eg-info-modal-body {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.eg-group-btn {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: var(--eg-color-group-bg-collapsed); }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source {\n opacity: 0.4;\n}\n.eg-col-drag-ghost {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n\n.eg-resize-line {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n\n.eg-pinned-right-body::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n\n.eg-lock-flash {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n\n.eg-import-dialog {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin 0.8s linear infinite;\n}\n@keyframes eg-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select {\n cursor: pointer;\n}\n.eg-cell-select::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap { justify-content: flex-end; }\n.eg-cell-align-center .eg-cell-action-wrap { justify-content: center; }\n.eg-validation-tooltip {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu,\n.eg-size-small .eg-context-item {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item {\n padding: 4px 14px;\n}\n.eg-size-small .eg-sort-menu {\n min-width: 90px;\n}\n.eg-size-small .eg-toolbar {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input,\n.eg-size-small .eg-filter-select {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu,\n.eg-size-large .eg-context-item {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item {\n padding: 8px 18px;\n}\n.eg-size-large .eg-sort-menu {\n min-width: 130px;\n}\n.eg-size-large .eg-toolbar {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input,\n.eg-size-large .eg-filter-select {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon {\n width: 13px;\n height: 13px;\n}\n</style>\n\n<style>\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent); }\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted); }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: var(--eg-color-text-muted); }\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px; }\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg); }\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent); }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg); }\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden; }\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent); }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell); }\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked); }\n</style>\n"]}, media: undefined })
|
|
13255
|
+
,inject("data-v-ada16cee_1", { source: "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent);\n}\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.eg-select-placeholder { color: var(--eg-color-text-muted);\n}\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px;\n}\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg);\n}\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden;\n}\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft);\n}\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell);\n}\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked);\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiyMA,wFAAA;AACA;EACA,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,8BAAA;EACA,oBAAA;AACA;AACA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,gBAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,mCAAA;EACA,eAAA;EACA,iBAAA;EACA,2BAAA;EACA,wDAAA;AACA;AACA,2BAAA,oCAAA;AAAA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA,+BAAA,yCAAA,EAAA,eAAA,EAAA,iCAAA;AAAA;AACA,2BAAA,OAAA,EAAA,gBAAA,EAAA,uBAAA,EAAA,mBAAA;AAAA;AACA,yBAAA,iCAAA;AAAA;AACA,mBAAA,cAAA,EAAA,iCAAA,EAAA,eAAA;AAAA;AACA;EACA,mCAAA;EACA,6CAAA;EACA,+BAAA;EACA,qCAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;AACA;AACA;EACA,kBAAA;EACA,SAAA;EACA,OAAA;EACA,eAAA;EACA,YAAA;EACA,eAAA;AACA;AACA;EACA,gBAAA;EACA,oDAAA;EACA,gBAAA;EACA,MAAA;EACA,mCAAA;EACA,UAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,gBAAA;EACA,8BAAA;EACA,aAAA;EACA,2BAAA;EACA,mCAAA;EACA,oBAAA;AACA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,8BAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,2BAAA;AACA;AACA;iCACA,2CAAA;AAAA;AACA,4BAAA,gBAAA,EAAA,6BAAA;AAAA;AACA;kCACA,kDAAA;AAAA;AACA;;;;wBAIA;AACA;EACA,eAAA;EACA,iBAAA;AACA;AACA;EACA,eAAA;EACA,iBAAA;AACA;AACA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,eAAA;AACA;AACA,gBAAA;AACA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;AACA;AACA,wBAAA,kBAAA;AAAA;AACA,eAAA;AACA;EACA,kBAAA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;EACA,cAAA;EACA,WAAA;EACA,YAAA;EACA,sBAAA;EACA,yCAAA;EACA,4BAAA;EACA,2BAAA;EACA,oBAAA;EACA,mDAAA;EACA,+BAAA;EACA,yFAAA;AACA;AACA,sCAAA,oCAAA;AAAA;AACA;EACA,wCAAA;EACA,+SAAA;EACA,wBAAA;EACA,oCAAA;AACA;AACA,sDAAA,iDAAA;AAAA;AACA;4CACA,eAAA;AAAA;AACA,iDAAA,+CAAA,EAAA,yCAAA;AAAA;AACA;gCACA,mBAAA;AAAA;AACA,yDAAA,2DAAA,EAAA,uDAAA;AAAA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + internalTheme, { 'eg-shell-fullscreen': isFullscreen, 'eg-locked': internalLocked }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"_toolbarVisible\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canUndo\"\n :title=\"t.undo\"\n @click=\"undo\"\n ><span class=\"eg-toolbar-icon eg-icon-undo\"></span></button>\n <button\n v-if=\"toolbarHas('undoRedo')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked || !canRedo\"\n :title=\"t.redo\"\n @click=\"redo\"\n ><span class=\"eg-toolbar-icon eg-icon-redo\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('export')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"t.export\"\n @click=\"toolbarExportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-export\"></span></button>\n <button\n v-if=\"toolbarHas('import')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :disabled=\"internalLocked\"\n :title=\"t.import\"\n @click=\"toolbarImportClick\"\n ><span class=\"eg-toolbar-icon eg-icon-import\"></span></button>\n <input\n v-if=\"toolbarHas('import')\"\n ref=\"toolbarImportInput\"\n type=\"file\"\n accept=\".xlsx,.xls,.csv\"\n style=\"display: none\"\n @change=\"onToolbarImportFile\"\n >\n </div>\n\n <slot name=\"toolbar-extra\" :locked=\"internalLocked\" :fullscreen=\"isFullscreen\"></slot>\n\n <div v-if=\"toolbarButtons.length\" class=\"eg-toolbar-group\">\n <button\n v-for=\"btn in toolbarButtons\"\n :key=\"btn.key\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :title=\"btn.title\"\n @click=\"onToolbarButtonClick(btn, $event)\"\n ><span class=\"eg-toolbar-icon\" :style=\"btn.icon ? { maskImage: 'url(' + btn.icon + ')', webkitMaskImage: 'url(' + btn.icon + ')' } : null\">{{ btn.icon ? '' : (btn.label || '') }}</span></button>\n </div>\n\n <div class=\"eg-toolbar-spacer\"></div>\n\n <div v-if=\"toolbarHas('theme')\" class=\"eg-toolbar-group\">\n <select\n class=\"eg-toolbar-select\"\n :value=\"internalTheme\"\n :title=\"t.theme\"\n @change=\"setTheme($event.target.value)\"\n >\n <option value=\"excel\">Excel</option>\n <option value=\"material\">Material</option>\n <option value=\"antd\">Ant Design</option>\n <option value=\"fluent\">Fluent</option>\n <option value=\"carbon\">Carbon</option>\n </select>\n </div>\n\n <div v-if=\"toolbarHas('size')\" class=\"eg-toolbar-segmented\" role=\"radiogroup\" :aria-label=\"t.size\">\n <button\n v-for=\"key in sizeOptions\"\n :key=\"key\"\n type=\"button\"\n role=\"radio\"\n class=\"eg-toolbar-btn eg-toolbar-segment\"\n :class=\"['eg-toolbar-segment-' + key, { 'eg-toolbar-btn-active': internalSize === key }]\"\n :aria-checked=\"internalSize === key\"\n :title=\"sizeTitle(key)\"\n @click=\"setSize(key)\"\n ><span class=\"eg-toolbar-icon\" :class=\"'eg-icon-size-' + key\"></span></button>\n </div>\n\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarHas('lock')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': internalLocked }\"\n :title=\"internalLocked ? t.unlock : t.lock\"\n @click=\"toggleLock\"\n ><span class=\"eg-toolbar-icon\" :class=\"internalLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"></span></button>\n <button\n v-if=\"toolbarHas('fullscreen')\"\n type=\"button\"\n class=\"eg-toolbar-btn\"\n :class=\"{ 'eg-toolbar-btn-active': isFullscreen }\"\n :title=\"isFullscreen ? t.exitFullscreen : t.fullscreen\"\n @click=\"toggleFullscreen\"\n ><span class=\"eg-toolbar-icon\" :class=\"isFullscreen ? 'eg-icon-compress' : 'eg-icon-expand'\"></span></button>\n </div>\n </div>\n\n <div\n class=\"eg-wrapper\"\n :class=\"'eg-size-' + internalSize\"\n :style=\"wrapperStyle\"\n tabindex=\"0\"\n @keydown=\"onKeyDown\"\n @keypress=\"onKeyPress\"\n ref=\"wrapper\"\n >\n <!-- Context menu -->\n <div\n v-if=\"contextMenu.visible\"\n key=\"eg-context-menu\"\n ref=\"contextMenuEl\"\n class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: (contextMenuPositioned ? contextMenu.x : -9999) + 'px', visibility: contextMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"ctxCopy\">{{ t.copy }}</div>\n <div class=\"eg-context-item\" @click=\"ctxCut\">{{ t.cut }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPaste\">{{ t.paste }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item eg-context-submenu-parent\">\n {{ t.pasteSpecial }}\n <div class=\"eg-context-submenu\">\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all')\">{{ t.pasteAll }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('values')\">{{ t.pasteValues }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('formats')\">{{ t.pasteFormats }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPasteSpecial('all', true)\">{{ t.transpose }}</div>\n </div>\n </div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxMerge\">{{ t.mergeCells }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnmerge\">{{ t.unmergeCells }}</div>\n <div class=\"eg-context-separator\"></div>\n <div v-if=\"contextMenu.row >= 0\" class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div v-if=\"contextMenu.col >= 0\" class=\"eg-context-item\" @click=\"ctxHideColumn\">{{ t.hideColumns }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideRows\">{{ t.unhideRows }}</div>\n <div class=\"eg-context-item\" @click=\"ctxUnhideColumns\">{{ t.unhideColumns }}</div>\n <template v-if=\"contextMenu.row < 0 && contextMenu.col >= 0\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"ctxPinLeft\">{{ t.pinLeft }}</div>\n <div class=\"eg-context-item\" @click=\"ctxPinRight\">{{ t.pinRight }}</div>\n <div class=\"eg-context-item\" v-if=\"isColPinned(contextMenu.col)\" @click=\"ctxUnpin\">{{ t.unpin }}</div>\n </template>\n <template v-if=\"devMode && (contextMenu.col >= 0 || contextMenu.row >= 0)\">\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.col >= 0\" @click=\"ctxShowColumnInfo\">{{ t.columnInfo }}</div>\n <div class=\"eg-context-item\" v-if=\"contextMenu.row >= 0\" @click=\"ctxShowRowInfo\">{{ t.rowInfo }}</div>\n </template>\n </div>\n\n <!-- Sort popup menu (header sort icon) -->\n <div\n v-if=\"sortMenu.visible\"\n key=\"eg-sort-menu\"\n ref=\"sortMenuEl\"\n class=\"eg-context-menu eg-sort-menu\"\n :style=\"{ top: sortMenu.y + 'px', left: (sortMenuPositioned ? sortMenu.x : -9999) + 'px', visibility: sortMenuPositioned ? 'visible' : 'hidden' }\"\n @click.stop\n >\n <div class=\"eg-context-item\" @click=\"sortMenuPickAsc\">{{ t.sortMenuAsc }}</div>\n <div class=\"eg-context-item\" @click=\"sortMenuPickDesc\">{{ t.sortMenuDesc }}</div>\n <div class=\"eg-context-separator\"></div>\n <div class=\"eg-context-item\" @click=\"sortMenuPickCancel\">{{ t.sortMenuCancel }}</div>\n </div>\n\n <!-- devMode: column/row info modal -->\n <div v-if=\"infoModal\" key=\"eg-info-modal-backdrop\" class=\"eg-info-modal-backdrop\" @click=\"closeInfoModal\">\n <div class=\"eg-info-modal\" @click.stop>\n <div class=\"eg-info-modal-header\">\n <span class=\"eg-info-modal-title\">{{ infoModal.title }}</span>\n <button type=\"button\" class=\"eg-info-modal-close\" @click=\"closeInfoModal\">×</button>\n </div>\n <pre class=\"eg-info-modal-body\">{{ infoModal.json }}</pre>\n </div>\n </div>\n\n <!-- Scroll container -->\n <div class=\"eg-container\" ref=\"container\" @scroll=\"onScroll\" @click=\"hideFloatingMenus\">\n\n <!-- ===== HEADER AREA ===== -->\n <div class=\"eg-header-area\" :style=\"headerAreaStyle\">\n\n <!-- Corner cell -->\n <div\n class=\"eg-corner\"\n :style=\"cornerStyle\"\n @click=\"selectAll\"\n ></div>\n\n <!-- Row group outline gutter (left) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-gutter\" :style=\"rowOutlineGutterStyle\"></div>\n\n <!-- Frozen columns header -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-header\" :style=\"frozenColHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, true)\">\n <div\n :key=\"'fh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, true)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'ffilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable columns header -->\n <div class=\"eg-scroll-col-header\" ref=\"scrollColHeader\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-header-inner\" :style=\"{ width: scrollContentWidth + 'px', height: (headerRowHeight * headerDepth + _filterRowH) + 'px', position: 'relative' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellVisible(cell, rowIdx, false)\">\n <div\n :key=\"'sh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyle(cell, rowIdx, false)\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'sfilter-' + ci\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(ci)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns header -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-header\" :style=\"pinnedRightHeaderStyle\">\n <div :style=\"{ position: 'relative', height: (headerRowHeight * headerDepth + _filterRowH) + 'px' }\">\n <template v-for=\"(row, rowIdx) in headerRows\">\n <template v-for=\"cell in row\">\n <template v-if=\"isCellInZone(cell, rowIdx, 'right')\">\n <div\n :key=\"'prh-' + rowIdx + '-' + (cell.column.key || cell.column.title)\"\n class=\"eg-header-cell\"\n :class=\"{ 'eg-header-group': cell.isGroup, 'eg-header-cell--drag-source': colDrag.active && cell._firstLeafFlatIndex >= colDrag.sourceFirstLeaf && cell._firstLeafFlatIndex < colDrag.sourceFirstLeaf + colDrag.sourceLeafCount }\"\n :data-fi=\"!cell.isGroup ? cell.column._flatIndex : undefined\"\n :data-start=\"cell.isGroup ? cell._firstLeafFlatIndex : undefined\"\n :data-end=\"cell.isGroup ? cell._firstLeafFlatIndex + cell.colspan - 1 : undefined\"\n :style=\"getHeaderCellStyleForZone(cell, rowIdx, 'right')\"\n @mousedown=\"onColHeaderMouseDown($event, cell)\"\n @dblclick=\"onColHeaderDblClick($event, cell)\"\n @contextmenu.prevent=\"onColHeaderContextMenu($event, cell)\"\n >\n <slot v-if=\"!cell.isGroup\" :name=\"'header-' + cell.column.key\" :column=\"cell.column\">\n {{ cell.column.title }}\n </slot>\n <span v-else>{{ cell.column.title }}</span>\n <span\n v-if=\"!cell.isGroup && isColSortable(cell.column)\"\n class=\"eg-sort-icon\"\n :class=\"sortIconClass(cell.column)\"\n :title=\"sortIconTitle()\"\n @mousedown.stop\n @click.stop=\"openSortMenu($event, cell.column)\"\n ></span>\n <div\n v-if=\"!cell.isGroup\"\n class=\"eg-col-resize-handle\"\n @mousedown.stop=\"startColResize($event, cell.column._flatIndex)\"\n ></div>\n </div>\n </template>\n </template>\n </template>\n <template v-if=\"filterRow\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prfilter-' + col._flatIndex\"\n class=\"eg-filter-cell\"\n :class=\"{ 'eg-filter-cell-empty': filterControlType(col) === 'none' }\"\n :style=\"getFilterCellStyle(col._flatIndex)\"\n @mousedown.stop\n >\n <input\n v-if=\"filterControlType(col) === 'text'\"\n type=\"text\"\n class=\"eg-filter-input\"\n :value=\"internalFilters[col.key] || ''\"\n :placeholder=\"t.filterPlaceholder\"\n @input=\"setFilter(col.key, $event.target.value)\"\n @keydown.stop\n @click.stop\n />\n <select\n v-else-if=\"filterControlType(col) === 'select'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option v-for=\"opt in filterOptionsFor(col)\" :key=\"opt.value\" :value=\"opt.value\">{{ opt.label }}</option>\n </select>\n <select\n v-else-if=\"filterControlType(col) === 'checkbox'\"\n class=\"eg-filter-select\"\n :value=\"internalFilters[col.key] || ''\"\n @change=\"setFilter(col.key, $event.target.value)\"\n @click.stop\n >\n <option value=\"\">{{ t.filterAll }}</option>\n <option value=\"true\">{{ t.filterChecked }}</option>\n <option value=\"false\">{{ t.filterUnchecked }}</option>\n </select>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- ===== BODY AREA ===== -->\n <div class=\"eg-body-area\" :style=\"bodyAreaStyle\">\n\n <!-- Row number header column -->\n <div class=\"eg-row-header-col\" :style=\"rowHeaderColStyle\">\n <!-- Scrollable row headers -->\n <div class=\"eg-scroll-row-headers\" :style=\"scrollRowHeadersStyle\" ref=\"scrollRowHeaders\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'srh-' + ri\"\n class=\"eg-row-header-cell\"\n :style=\"getRowHeaderStyle(ri)\"\n @mousedown=\"onRowHeaderMouseDown($event, ri)\"\n @contextmenu.prevent=\"onRowContextMenu($event, ri)\"\n >\n {{ ri + 1 }}\n <div class=\"eg-row-resize-handle\" @mousedown.stop=\"startRowResize($event, ri)\"></div>\n </div>\n </template>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Row group outline gutter (left side of rows) -->\n <div v-if=\"rowGroups.length\" class=\"eg-row-outline-col\" :style=\"rowOutlineColStyle\">\n <template v-for=\"group in rowGroups\">\n <div\n :key=\"'rg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getRowGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n <!-- Main scroll body -->\n <div class=\"eg-scroll-body\" ref=\"scrollBody\" :style=\"scrollBodyStyle\" @scroll=\"onBodyScroll\">\n <div class=\"eg-scroll-body-inner\" :style=\"{ width: scrollContentWidth + 'px', height: totalVirtualHeight + 'px', position: 'relative' }\">\n\n <!-- Pinned left column body -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-body\" :style=\"frozenColBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div\n :key=\"'fc-cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @input=\"onEditInput\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Main data cells -->\n <div class=\"eg-main-cells\" :style=\"mainCellsStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative', width: scrollableContentWidth + 'px' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\n :class=\"{ 'eg-row--even': ri % 2 === 0 }\"\n :style=\"getRowStyle(ri)\"\n :data-row=\"ri\"\n >\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div\n :key=\"'cell-' + ri + '-' + ci\"\n class=\"eg-cell\"\n :data-fi=\"ci\"\n :class=\"getCellClasses(ri, ci)\"\n :style=\"getCellStyle(ri, ci)\"\n @mousedown=\"onCellMouseDown($event, ri, ci)\"\n @mouseover=\"onCellMouseOver($event, ri, ci)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, ci)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, ci)\"\n >\n <div v-if=\"isMergedAway(ri, ci)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, ci)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input\n v-else\n ref=\"editInput\"\n class=\"eg-cell-input\"\n v-model=\"editValue\"\n @input=\"onEditInput\"\n @blur=\"commitEdit\"\n @keydown.stop=\"onEditKeyDown\"\n />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"(v) => setCheckboxValue(ri, ci, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, ci, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</span></template><template v-else>{{ getCellDisplayValue(ci, getCellValue(ri, ci), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Pinned Right column body -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-body\" :style=\"pinnedRightBodyStyle\">\n <div :style=\"{ height: totalVirtualHeight + 'px', position: 'relative' }\">\n <div :style=\"{ transform: 'translateY(' + virtualOffsetTop + 'px)', willChange: 'transform', contain: 'layout style' }\">\n <template v-for=\"ri in visibleRowRange\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :class=\"{ 'eg-row--even': ri % 2 === 0 }\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\n :data-fi=\"col._flatIndex\"\n :class=\"getCellClasses(ri, col._flatIndex)\"\n :style=\"getCellStyle(ri, col._flatIndex)\"\n @mousedown=\"onCellMouseDown($event, ri, col._flatIndex)\"\n @mouseover=\"onCellMouseOver($event, ri, col._flatIndex)\"\n @mouseup=\"onCellMouseUp\"\n @dblclick=\"onCellDblClick(ri, col._flatIndex)\"\n @contextmenu.prevent=\"onCellContextMenu($event, ri, col._flatIndex)\"\n >\n <div v-if=\"isMergedAway(ri, col._flatIndex)\" style=\"display:none\"></div>\n <template v-else-if=\"isEditing(ri, col._flatIndex)\">\n <slot v-if=\"hasCellSlot(col)\" :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"true\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <slot v-else-if=\"hasLegacyEditSlot(col)\" :name=\"'edit-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :done=\"(v) => commitEditSlot(v)\" :cancel=\"cancelEdit\" />\n <input v-else ref=\"editInput\" class=\"eg-cell-input\" v-model=\"editValue\" @blur=\"commitEdit\" @keydown.stop=\"onEditKeyDown\" />\n </template>\n <template v-else-if=\"col.type === 'checkbox'\">\n <eg-checkbox\n :value=\"isCheckboxChecked(ri, col._flatIndex)\"\n :disabled=\"!isCellEditable(ri, col._flatIndex)\"\n @change=\"(v) => setCheckboxValue(ri, col._flatIndex, v)\"\n />\n </template>\n <template v-else-if=\"col.type === 'action'\">\n <span class=\"eg-cell-action-wrap\" @[actionHandlerEvent(col)]=\"triggerCellAction(ri, col._flatIndex, $event)\">\n <slot :name=\"'action-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" />\n </span>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" :isEditing=\"false\" :done=\"null\" :cancel=\"null\">\n <slot v-if=\"hasPrefixSlot(col)\" :name=\"'prefix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /><template v-if=\"hasSuffixSlot(col) && isSelectLikeCol(col)\"><span class=\"eg-cell-select-trigger-wrap\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</span></template><template v-else>{{ getCellDisplayValue(col._flatIndex, getCellValue(ri, col._flatIndex), ri) }}</template><span v-if=\"hasSuffixSlot(col)\" class=\"eg-cell-suffix-wrap\"><slot :name=\"'suffix-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\" /></span>\n </slot>\n </template>\n </div>\n </template>\n </div>\n </template>\n </div>\n </div>\n </div>\n\n <!-- Fill handle (shown on primary range) -->\n <div\n v-if=\"fillHandleVisible\"\n class=\"eg-fill-handle\"\n :style=\"fillHandleStyle\"\n @mousedown.stop=\"startFillDrag\"\n ></div>\n\n <!-- Drag move ghost border -->\n <div\n v-if=\"dragMove.active\"\n class=\"eg-drag-ghost\"\n :style=\"dragGhostStyle\"\n ></div>\n </div>\n </div>\n\n <!-- Column group outline row (top, above body) -->\n <div v-if=\"colGroups.length\" class=\"eg-col-outline-row\" :style=\"colOutlineRowStyle\">\n <template v-for=\"group in colGroups\">\n <div\n :key=\"'cg-' + group.startIndex + '-' + group.endIndex\"\n class=\"eg-group-btn\"\n :class=\"{ collapsed: isGroupCollapsed(group) }\"\n :style=\"getColGroupBtnStyle(group)\"\n @click=\"toggleGroup(group)\"\n >{{ isGroupCollapsed(group) ? '+' : '−' }}</div>\n </template>\n </div>\n\n </div><!-- end body-area -->\n\n <!-- ===== FOOTER AREA (column totals) ===== -->\n <div v-if=\"hasFooter\" class=\"eg-footer-area\" :style=\"footerAreaStyle\">\n\n <!-- \"Jami\" label cell (mirrors corner cell) -->\n <div class=\"eg-footer-label\" :style=\"footerLabelStyle\">{{ t.total }}</div>\n\n <!-- Frozen columns footer -->\n <div v-if=\"getFrozenWidth() > 0\" class=\"eg-frozen-col-footer\" :style=\"frozenColFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnPinnedLeft(ci) && !isColHidden(ci)\">\n <div :key=\"'ff-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Scrollable footer (horizontally synced with body via scrollFooter ref) -->\n <div class=\"eg-scroll-col-footer\" ref=\"scrollFooter\" :style=\"scrollColHeaderStyle\">\n <div class=\"eg-scroll-col-footer-inner\" :style=\"{ width: scrollContentWidth + 'px', height: footerRowHeight + 'px', position: 'relative' }\">\n <template v-for=\"(col, ci) in leafCols\">\n <template v-if=\"isColumnScrollable(ci) && !isColHidden(ci)\">\n <div :key=\"'sf-' + ci\" class=\"eg-footer-cell\" :class=\"getFooterCellClass(ci)\" :style=\"getFooterCellStyle(ci)\">\n {{ getColumnTotalDisplay(ci) }}\n </div>\n </template>\n </template>\n </div>\n </div>\n\n <!-- Pinned Right columns footer -->\n <div v-if=\"hasPinnedRight\" class=\"eg-pinned-right-footer\" :style=\"pinnedRightFooterStyle\">\n <div :style=\"{ position: 'relative', height: footerRowHeight + 'px' }\">\n <div\n v-for=\"col in pinnedRightCols\"\n :key=\"'prf-' + col._flatIndex\"\n class=\"eg-footer-cell\"\n :class=\"getFooterCellClass(col._flatIndex)\"\n :style=\"getFooterCellStyle(col._flatIndex)\"\n >{{ getColumnTotalDisplay(col._flatIndex) }}</div>\n </div>\n </div>\n </div><!-- end footer-area -->\n\n <!-- Resize indicator line -->\n <div v-if=\"resizing.active\" class=\"eg-resize-line\" :style=\"resizeLineStyle\"></div>\n </div><!-- end container -->\n\n <!-- Import progress overlay -->\n <div v-if=\"importState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl import qilinmoqda</div>\n <div class=\"eg-import-bar-wrap\">\n <div class=\"eg-import-bar\" :style=\"{ width: importState.percent + '%' }\"></div>\n </div>\n <div class=\"eg-import-msg\">{{ importState.message }}</div>\n <div class=\"eg-import-pct\">{{ importState.percent }}%</div>\n </div>\n </div>\n\n <!-- Export progress overlay -->\n <div v-if=\"exportState.active\" class=\"eg-import-overlay\">\n <div class=\"eg-import-dialog\">\n <div class=\"eg-import-title\">Fayl export qilinmoqda...</div>\n <div class=\"eg-export-spinner\"></div>\n </div>\n </div>\n\n <!-- Lock/unlock state feedback — brief centered icon flash -->\n <div v-if=\"lockFlashVisible\" class=\"eg-lock-flash\" aria-hidden=\"true\">\n <span\n ref=\"lockFlashIconEl\"\n class=\"eg-lock-flash-icon\"\n :class=\"lockFlashLocked ? 'eg-icon-lock' : 'eg-icon-unlock'\"\n ></span>\n </div>\n\n <!-- Select dropdown overlay (flat select) -->\n <eg-select\n v-if=\"selectDropdown && !selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n <!-- Tree select dropdown overlay -->\n <eg-tree-select\n v-if=\"selectDropdown && selectDropdown.tree\"\n ref=\"selectDropdownComp\"\n :value=\"selectDropdown.currentValue\"\n :options=\"selectDropdown.rawOptions\"\n :value-key=\"selectDropdown.keys.valueKey\"\n :label-key=\"selectDropdown.keys.labelKey\"\n :children-key=\"selectDropdown.keys.childrenKey\"\n :enable-search=\"selectDropdown.enableSearch\"\n :trigger-rect=\"selectDropdown.rect\"\n :dropdown-only=\"true\"\n :size=\"internalSize\"\n @select=\"onComponentSelect\"\n @close=\"closeSelectDropdown\"\n />\n\n <!-- Date picker overlay -->\n <eg-date-picker\n v-if=\"datePickerOverlay\"\n ref=\"datePickerComp\"\n :value=\"datePickerOverlay.value\"\n :format=\"datePickerOverlay.format\"\n :locale=\"locale\"\n :trigger-rect=\"datePickerOverlay.rect\"\n @pick=\"onDatePick\"\n @typing=\"onDateTyping\"\n @close=\"closeDatePicker\"\n />\n\n <!-- Validation error tooltip (cellValidator) -->\n <div\n v-if=\"invalidCell && invalidCell.rect\"\n class=\"eg-validation-tooltip\"\n :style=\"invalidTooltipStyle\"\n >{{ invalidCell.message }}</div>\n\n <!-- Column drag ghost (follows mouse — position updated via direct DOM in handleColDragMove) -->\n <div\n v-if=\"colDrag.active\"\n ref=\"colDragGhost\"\n class=\"eg-col-drag-ghost\"\n :style=\"{ left: (colDrag.startX + 16) + 'px', top: (colDrag.startY - 14) + 'px' }\"\n >{{ colDrag.ghostLabel }}</div>\n\n </div>\n </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth, moveColumnNodeInTree } from '../utils/headerTree.js'\nimport { calcVisibleRows, buildCumulativeHeights, findVisibleRange, getRowTop, getRowHeight } from '../utils/virtualScroll.js'\nimport { generateFillValues } from '../utils/fillHandle.js'\nimport {\n matrixToTSV, tsvToMatrix, extractMatrix, transposeMatrix, applyMatrix, tileMatrix\n} from '../utils/clipboard.js'\nimport {\n mergeHasConflict, findMergeAtCell, isMergeAnchor, addMerge,\n removeMergeAt, resolveMergeAnchor\n} from '../utils/merge.js'\nimport {\n addGroup, removeGroup, toggleGroupCollapse, isGroupCollapsed as _isGroupCollapsed,\n getCollapsedRowIndices, getCollapsedColIndices\n} from '../utils/outline.js'\nimport { exportToExcel as _exportToExcel } from '../utils/excelExport.js'\nimport { importFromFile as _importFromFile } from '../utils/excelImport.js'\nimport { createExcelImportWorker, createExcelExportWorker } from '../utils/workerFactory'\nimport { getMessages } from '../utils/i18n.js'\nimport EgCheckbox from './EgCheckbox.vue'\nimport anime from 'animejs'\nimport EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\nimport EgDatePicker from './EgDatePicker.vue'\nimport { parseDateValue, formatDate } from '../utils/dateUtils.js'\n\nconst SIZE_CONFIG = {\n small: { row: 22, header: 24, footer: 24, filter: 26 },\n middle: { row: 28, header: 28, footer: 28, filter: 32 },\n large: { row: 36, header: 36, footer: 36, filter: 40 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\n// Default toolbar tool list when `toolbar` is `true` or an object without\n// its own `list` — every group except 'theme', which stays opt-in since it\n// needs :theme.sync on the parent to do anything.\nconst DEFAULT_TOOLBAR_LIST = ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen']\nconst THEME_LIST = ['excel', 'material', 'antd', 'fluent', 'carbon']\n\nexport default {\n name: 'ExcelGrid',\n\n components: { EgCheckbox, EgSelect, EgTreeSelect, EgDatePicker },\n\n // v-model → :data + @input\n model: { prop: 'data', event: 'input' },\n\n props: {\n /** Array of row objects keyed by columns[].key */\n data: { type: Array, default: () => [] },\n\n /** Column definitions (may be nested via children[]) */\n columns: { type: Array, default: () => [] },\n\n /** Indices of hidden leaf columns */\n hiddenColumns: { type: Array, default: () => [] },\n\n /** Indices of hidden rows */\n hiddenRows: { type: Array, default: () => [] },\n\n /** Initial merge descriptors [{ startRow, startCol, endRow, endCol }] */\n merges: { type: Array, default: () => [] },\n\n /** Collapsed group descriptors [{ type, startIndex, endIndex }] */\n collapsedGroups: { type: Array, default: () => [] },\n\n /**\n * Height of the grid container in px.\n *\n * Omit it (or pass `null`) for auto-height: the grid sizes itself to fit\n * the header + filter row + all rows + footer for the current `data`\n * length, capped at 70% of the viewport height (`window.innerHeight`) —\n * beyond that it stops growing and scrolls internally, same as a fixed\n * `height` would. Recomputes reactively as `data` grows/shrinks and on\n * window resize. Does not apply in fullscreen mode (already sized to the\n * viewport there) and does not include the toolbar's own height.\n *\n * Pass an explicit `Number` to opt back into the old fixed-height behavior.\n */\n height: { type: Number, default: null },\n\n /** Width of the grid container (px or 'auto') */\n width: { type: [Number, String], default: '100%' },\n\n /**\n * Called before drag-drop: (sourceRange, targetRange) => bool\n * Return false to cancel the drop.\n */\n beforeDrop: { type: Function, default: null },\n\n /**\n * UI locale for the context menu.\n * Supported: 'en' | 'ru' | 'uz' | 'uz-Cyrl'\n */\n locale: { type: String, default: 'en' },\n\n /**\n * Pinned columns map: { [colKey]: 'left' | 'right' }\n * Pinned-left columns render as a fixed zone on the left.\n * Pinned-right columns render as a fixed zone on the right.\n * Emit 'update:pinnedColumns' to support .sync modifier.\n */\n pinnedColumns: { type: Object, default: () => ({}) },\n\n /**\n * Dynamic cell editability: (row, rowIndex, col, colIndex) => boolean\n * Overrides column-level editable when provided.\n * row = plain data object, col = column definition.\n */\n cellEditable: { type: Function, default: null },\n\n /**\n * Cell value validation: (value, row, rowIndex, col, colIndex) => boolean\n * Return false to reject the write — the cell keeps its old value and\n * an error tooltip is shown anchored to that cell.\n */\n cellValidator: { type: Function, default: null },\n\n /**\n * Error tooltip text for a rejected value.\n * Function: (value, row, rowIndex, col, colIndex) => string\n * String: same message for every rejected cell.\n * Default: locale's \"invalidValue\" message.\n */\n cellValidationMessage: { type: [Function, String], default: null },\n\n /**\n * Bulk-write strictness (paste / fill handle / Delete).\n * false (default): invalid cells in the affected range are skipped,\n * the rest of the operation still applies.\n * true: if ANY cell in the affected range fails validation, the whole\n * bulk operation is rejected — nothing is written.\n */\n strictBulkValidation: { type: Boolean, default: false },\n\n /** Visual size of the grid. Affects row/header height and font sizes. */\n size: { type: String, default: 'middle' },\n\n /** Auto-fit all column widths to content (same as double-clicking each resize handle). Re-applies on data/columns change. */\n autoWidth: { type: Boolean, default: false },\n\n /**\n * Allow columns to be reordered by dragging their headers.\n * On drop, emits 'update:columns' (compatible with .sync) and 'column-reorder'.\n */\n columnDraggable: { type: Boolean, default: true },\n\n /**\n * Controls the built-in toolbar above the grid — visibility as a whole,\n * and (optionally) which individual tool groups appear. Controlled\n * entirely by the parent — no in-component toggle for the bar itself.\n *\n * @type {Boolean|{visible?: Boolean, list?: Array<('undoRedo'|'export'|'import'|'size'|'lock'|'fullscreen'|'theme')>}}\n * @default false\n *\n * Two forms:\n * - `Boolean` — `false` (default) hides the toolbar entirely; `true`\n * shows it with the default tool list (every group except `'theme'`,\n * since that one needs `:theme.sync` on the parent to be useful).\n * - `Object { visible, list }` — fine-grained control:\n * - `visible` {Boolean} — defaults to `true` when an object is given\n * (passing an object is itself an opt-in to show the toolbar);\n * set explicitly to `false` to hide it while still describing a\n * `list` you intend to reuse later.\n * - `list` {Array<String>} — which tool groups render, using the\n * identifiers above. Unknown/misspelled strings are silently\n * ignored. Omitting `list` falls back to the same default set as\n * the `true` shorthand. **Order in `list` does not change the\n * toolbar's layout** — groups always render in a fixed position\n * (undo/redo → export/import → #toolbar-extra slot → toolbarButtons\n * → spacer → theme → size → lock/fullscreen); `list` is purely an\n * inclusion filter.\n *\n * `toolbarButtons` and the `#toolbar-extra` slot are independent of\n * this prop — they render whenever content is provided, regardless of\n * `list`.\n *\n * @example\n * // Whole default set\n * <excel-grid toolbar />\n * @example\n * // Only undo/redo and fullscreen\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'fullscreen'] }\" />\n * @example\n * // Toggle visibility from a data property while keeping a fixed list\n * <excel-grid :toolbar=\"{ visible: showToolbar, list: ['export', 'import'] }\" />\n * @example\n * // Opt into the theme picker (needs :theme.sync)\n * <excel-grid :toolbar=\"{ list: ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen', 'theme'] }\" :theme.sync=\"myTheme\" />\n */\n toolbar: { type: [Boolean, Object], default: false },\n\n /** Extra custom toolbar buttons: [{ key, icon, title, onClick(event, gridApi) }] */\n toolbarButtons: { type: Array, default: () => [] },\n\n /**\n * Locks the entire grid: blocks all edits (typing, paste, fill, delete,\n * checkbox, select/treeSelect) and structural changes (resize, column\n * reorder, merge/unmerge, undo/redo). Independent from cellEditable/\n * col.editable, which remain per-cell rules. Emit 'update:locked' to\n * support the .sync modifier.\n */\n locked: { type: Boolean, default: false },\n\n /** Show \"Column info\" / \"Row info\" items in the context menu (dev/debug aid). */\n devMode: { type: Boolean, default: false },\n\n /**\n * Visual theme of the grid.\n * Built-in: 'excel' | 'material' | 'antd' | 'fluent' | 'carbon'\n * Default is the classic Excel 365 look. Use 'material' for Material Design 3.\n * Mirrors the `size` prop's convention: an internal `internalTheme` state\n * mirrors this prop and is what the toolbar's theme picker actually reads\n * from/writes to (via `setTheme()`), so the picker works standalone even\n * without `:theme.sync` — `.sync` is only needed to reflect the current\n * theme back into the host's own state.\n */\n theme: { type: String, default: 'excel' },\n\n /**\n * Shows a filter row directly beneath the column headers (inside the\n * sticky header area). Each leaf column gets a filter control matched to\n * its `type`:\n * - default (no type / 'date' / 'progress'): text search input,\n * matched case-insensitively against the column's *displayed* value\n * (so 'date' matches the formatted string, not the raw stored value).\n * - 'select' / 'treeSelect' *with a static `col.options` array*: a\n * native <select> populated from those options (tree options are\n * flattened, indented by depth), matched by exact raw value.\n * - 'select' / 'treeSelect' with function-based `col.options` (options\n * computed per-row): falls back to the text control, since a single\n * fixed option list can't be resolved outside a specific row.\n * - 'checkbox': a tri-state <select> — All / Checked / Unchecked.\n * - 'action': no filter control (not a data-bound column).\n *\n * Rows that don't match every active filter are hidden using the same\n * mechanism as `hiddenRows` — virtual scroll, keyboard navigation, and\n * `exportToExcel()` all skip them automatically, with no separate\n * filtering code path to keep in sync.\n *\n * Set `col.filterable: false` to omit the filter control for one column\n * (its filter cell renders empty). Set\n * `col.filterFn(cellValue, filterValue, row, rowIndex, column)` to\n * override the default match logic for that column entirely.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row />\n */\n filterRow: { type: Boolean, default: false },\n\n /**\n * Current filter values, keyed by column `key`. Optional — the filter\n * row works standalone via internal state (`internalFilters`) even\n * without `.sync`, mirroring the `size`/`theme` prop convention. Bind\n * `:filters.sync` only if the host needs to read or restore the current\n * filter state.\n * @type {Object}\n * @default {}\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" filter-row :filters.sync=\"myFilters\" />\n */\n filters: { type: Object, default: () => ({}) },\n\n /**\n * Shows a sort icon in every leaf column header. Clicking it cycles\n * none → ascending → descending → none for that column (clicking a\n * different column's icon replaces the active sort — single-column\n * sort only).\n *\n * Sorting **physically reorders** the `data` array (like a real\n * spreadsheet sort) and emits it through the normal v-model/`input`\n * event — it is recorded in the undo/redo history exactly like any\n * other data edit, so `undo()` restores the pre-sort row order.\n * Clicking a third time (back to \"none\") only clears the sort\n * indicator — it does **not** restore the previous order by itself;\n * use `undo()` for that.\n *\n * Comparison is matched to the column's `type`: plain/`'progress'`\n * columns compare the raw value (numeric-aware); `'date'` compares the\n * parsed date chronologically, not the formatted string; `'select'` /\n * `'treeSelect'` compare the displayed option label; `'checkbox'`\n * compares the boolean state. Empty/`null` values always sort last,\n * regardless of direction. `'action'` columns are never sortable.\n *\n * Set `col.sortable: false` to omit the icon for one column. Set\n * `col.sortFn(rowA, rowB, column) => number` to fully override the\n * comparison for that column.\n *\n * Blocked (no-op) while the grid is `locked`, or while any merged\n * cells or row outline groups exist — both are anchored to row\n * *position*, which a reorder would silently invalidate.\n *\n * @type {Boolean}\n * @default false\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable />\n */\n sortable: { type: Boolean, default: false },\n\n /**\n * Current sort state — `{ key, direction }` (`direction` is `'asc'` or\n * `'desc'`) or `null` when unsorted. Optional — sorting works standalone\n * via internal state (`internalSort`) even without `.sync`, mirroring\n * the `size`/`theme`/`filters` prop convention. Bind `:sort.sync` only\n * if the host needs to read or restore the current sort state.\n * @type {Object}\n * @default null\n * @example\n * <excel-grid :columns=\"columns\" v-model=\"data\" sortable :sort.sync=\"mySort\" />\n */\n sort: { type: Object, default: null }\n },\n\n data() {\n return {\n // Selection\n activeCell: { row: 0, col: 0 },\n selectionRanges: [], // [{ startRow, startCol, endRow, endCol }]\n isMouseDown: false,\n mouseDownRange: null,\n isCtrlDrag: false,\n\n // Edit\n editingCell: null, // { row, col }\n editValue: '',\n editOriginalValue: '',\n\n // Scroll\n scrollTop: 0,\n scrollLeft: 0,\n headerViewportWidth: null,\n\n // Auto-height (height prop omitted) — 70vh cap, kept in sync on resize\n windowInnerH: typeof window !== 'undefined' ? window.innerHeight : 800,\n\n // Virtual scroll\n virtualStartIndex: 0,\n virtualEndIndex: 0,\n virtualOffsetTop: 0,\n totalVirtualHeight: 0,\n\n // Column widths (flat index → px)\n colWidths: {},\n\n // Row heights (row index → px)\n rowHeights: {},\n\n // Import/export progress\n importState: { active: false, percent: 0, message: '' },\n exportState: { active: false },\n\n resizing: {\n active: false,\n type: null, // 'col' | 'row'\n index: -1,\n startX: 0,\n startY: 0,\n startSize: 0,\n currentPos: 0\n },\n\n // Fill handle\n fillDrag: {\n active: false,\n sourceRange: null,\n currentRow: -1,\n currentCol: -1\n },\n\n // Drag-move\n dragMove: {\n active: false,\n sourceRange: null,\n startX: 0,\n startY: 0,\n targetRow: -1,\n targetCol: -1,\n copyMode: false\n },\n\n // Clipboard (internal)\n clipboardMatrix: null,\n clipboardRange: null,\n clipboardMode: null, // 'copy' | 'cut'\n\n // Context menu\n contextMenu: { visible: false, x: 0, y: 0, row: -1, col: -1 },\n // false while the menu's real size hasn't been measured yet — kept hidden\n // (not painted) so the flip-to-fit repositioning in _positionContextMenu()\n // doesn't cause a visible jump.\n contextMenuPositioned: false,\n\n // Sort popup menu (header sort icon click) — { visible, x, y, key }\n sortMenu: { visible: false, x: 0, y: 0, key: null },\n sortMenuPositioned: false,\n\n // devMode — column/row config info modal ({ title, json } | null)\n infoModal: null,\n\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, rect }\n\n // Date picker overlay\n datePickerOverlay: null, // { row, col, format, value, rect }\n\n // Cell value validation (cellValidator)\n invalidCell: null, // { row, col, message, rect }\n\n // Merges (controlled via prop + update:merges)\n internalMerges: [],\n\n // Toolbar (controlled via prop + update:locked / update:size / update:theme)\n internalLocked: false,\n internalSize: 'middle',\n internalTheme: 'excel',\n // Brief centered icon feedback whenever the lock state actually changes\n // (toolbar click, toggleLock(), or a host-driven :locked.sync change).\n lockFlashVisible: false,\n lockFlashLocked: false,\n isFullscreen: false,\n // Bumped whenever _history/_historyIndex change so canUndo/canRedo stay reactive.\n historyVersion: 0,\n\n // Groups\n internalGroups: [], // [{ type, startIndex, endIndex }]\n internalCollapsedGroups: [],\n\n // Header\n headerDepth: 1,\n headerRows: [],\n\n // Leaf columns cache\n leafCols: [],\n\n // Footer column totals (flat col index → number | null)\n columnTotals: {},\n\n // Filter row (controlled via prop + update:filters)\n internalFilters: {},\n // Row indices currently hidden by an active filter — merged into\n // hiddenRowSet, so virtual scroll/export/navigation need no separate path.\n filteredRowIndices: [],\n\n // Sort (controlled via prop + update:sort) — { key, direction } | null\n internalSort: null,\n\n // Column drag-and-drop state\n colDrag: {\n pending: false, // mousedown received, waiting for movement threshold\n active: false, // drag in progress\n sourceFirstLeaf: -1, // first leaf flat-index of the dragged node\n sourceLeafCount: 1, // number of leaves in the dragged node (1=leaf, N=group)\n dropIndex: -1, // insert-before flat index (0..N)\n startX: 0,\n startY: 0,\n ghostLabel: ''\n },\n }\n },\n\n computed: {\n t() { return getMessages(this.locale) },\n\n pinnedLeftSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'left') s.add(col._flatIndex) })\n return s\n },\n\n pinnedRightSet() {\n const s = new Set()\n this.leafCols.forEach(col => { if (this.pinnedColumns[col.key] === 'right') s.add(col._flatIndex) })\n return s\n },\n\n pinnedLeftCols() { return this.leafCols.filter(col => this.pinnedLeftSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(col._flatIndex) && !this.isColHidden(col._flatIndex)) },\n\n hasPinnedLeft() { return this.pinnedLeftSet.size > 0 },\n hasPinnedRight() { return this.pinnedRightSet.size > 0 },\n\n hasFooter() { return this.leafCols.some(col => this.isColumnTotalVisible(col)) },\n\n pinnedLeftWidth() {\n let w = 0\n for (const col of this.pinnedLeftCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n pinnedRightWidth() {\n let w = 0\n for (const col of this.pinnedRightCols) {\n if (!this.isColHidden(col._flatIndex)) w += this.getColW(col._flatIndex)\n }\n return w\n },\n\n totalRows() { return this.data.length },\n\n // See the `toolbar` prop's JSDoc for the full Boolean|Object contract.\n _toolbarVisible() {\n if (typeof this.toolbar === 'boolean') return this.toolbar\n return !!this.toolbar && this.toolbar.visible !== false\n },\n _toolbarList() {\n if (this.toolbar && Array.isArray(this.toolbar.list)) return this.toolbar.list\n return DEFAULT_TOOLBAR_LIST\n },\n\n _sizeConf() { return SIZE_CONFIG[this.internalSize] || SIZE_CONFIG.middle },\n\n sizeOptions() { return Object.keys(SIZE_CONFIG) },\n _rowH() { return this._sizeConf.row },\n _headerRowH() { return this._sizeConf.header },\n _footerRowH() { return this._sizeConf.footer },\n _filterRowH() { return this.filterRow ? this._sizeConf.filter : 0 },\n\n headerRowHeight() { return this._headerRowH },\n footerRowHeight() { return this._footerRowH },\n\n // Pre-built cumulative height array — O(n) once, then cached by Vue.\n // findVisibleRange() uses binary search against this cached array → O(log n) per scroll.\n // Invalidated only when row count, custom heights, or hidden rows change.\n cumulativeHeights() {\n return buildCumulativeHeights(this.totalRows, this._rowH, this.rowHeights, this.hiddenRowSet)\n },\n\n // height prop omitted → fit header + filter row + all (visible) rows + footer exactly.\n _autoContentHeight() {\n const headerH = this._headerRowH * this.headerDepth + this._filterRowH\n const bodyH = this.cumulativeHeights[this.totalRows] || 0\n const footerH = this.hasFooter ? this._footerRowH : 0\n return headerH + bodyH + footerH\n },\n\n // Resolved grid-area height: explicit `height` prop wins as-is (old\n // behavior); omitted → auto-fit content, capped at 70% of the viewport.\n effectiveHeight() {\n if (this.height != null) return this.height\n return Math.min(this._autoContentHeight, this.windowInnerH * 0.7)\n },\n\n hiddenColSet() { return new Set(this.hiddenColumns) },\n\n hiddenRowSet() {\n const s = new Set(this.hiddenRows)\n // Also add rows hidden by collapsed row groups\n const collapsed = getCollapsedRowIndices(this.internalGroups, this.internalCollapsedGroups)\n collapsed.forEach(i => s.add(i))\n // Also add rows hidden by the filter row (see filteredRowIndices/_recomputeRowFilters)\n this.filteredRowIndices.forEach(i => s.add(i))\n return s\n },\n\n hiddenColFromGroupSet() {\n return getCollapsedColIndices(this.internalGroups, this.internalCollapsedGroups)\n },\n\n effectiveHiddenColSet() {\n const s = new Set(this.hiddenColSet)\n this.hiddenColFromGroupSet.forEach(i => s.add(i))\n // col.visible === false — a static schema-level hide, independent of the\n // toggleable hiddenColumns list. \"Show All Columns\" never reveals it.\n this.leafCols.forEach(col => { if (col.visible === false) s.add(col._flatIndex) })\n return s\n },\n\n rowGroups() { return this.internalGroups.filter(g => g.type === 'row') },\n colGroups() { return this.internalGroups.filter(g => g.type === 'column') },\n\n visibleRowRange() {\n const rows = []\n for (let i = this.virtualStartIndex; i <= this.virtualEndIndex; i++) rows.push(i)\n return rows\n },\n\n primaryRange() {\n return getPrimaryRange(this.selectionRanges, this.activeCell)\n },\n\n fillHandleVisible() {\n return this.primaryRange && !this.editingCell\n },\n\n fillHandleStyle() {\n if (!this.primaryRange) return {}\n const r = normalizeRange(this.primaryRange)\n // O(1) lookup via cumulativeHeights instead of O(endRow) getRowTop loop\n const bottom = this.cumulativeHeights[r.endRow + 1] || 0\n const right = this.getColLeft(r.endCol + 1)\n return {\n position: 'absolute',\n width: '6px',\n height: '6px',\n background: 'var(--eg-color-accent)',\n border: '1px solid white',\n bottom: (this.totalVirtualHeight - bottom + 1) + 'px',\n right: (this.scrollContentWidth - right + 1) + 'px',\n cursor: 'crosshair',\n zIndex: 10\n }\n },\n\n\n invalidTooltipStyle() {\n if (!this.invalidCell || !this.invalidCell.rect) return {}\n const { rect } = this.invalidCell\n return {\n position: 'fixed',\n top: (rect.bottom + 4) + 'px',\n left: rect.left + 'px',\n zIndex: 9999\n }\n },\n\n dragGhostStyle() {\n if (!this.dragMove.active || this.dragMove.targetRow < 0) return {}\n const r = normalizeRange(this.dragMove.sourceRange)\n const rows = r.endRow - r.startRow\n const cols = r.endCol - r.startCol\n // O(1) lookup via cumulativeHeights instead of O(targetRow) getRowTop loop\n const top = this.cumulativeHeights[this.dragMove.targetRow] || 0\n const left = this.getColLeft(this.dragMove.targetCol)\n const width = this.getRangeWidth(this.dragMove.targetCol, this.dragMove.targetCol + cols)\n const height = this.getRangeHeight(this.dragMove.targetRow, this.dragMove.targetRow + rows)\n return {\n position: 'absolute',\n top: top + 'px',\n left: left + 'px',\n width: width + 'px',\n height: height + 'px',\n border: '2px dashed #1a73e8',\n pointerEvents: 'none',\n zIndex: 20\n }\n },\n\n wrapperStyle() {\n if (this.isFullscreen) {\n return {\n width: '100%',\n height: 'auto',\n flex: '1 1 auto',\n minHeight: 0,\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n }\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n height: this.effectiveHeight + 'px',\n position: 'relative',\n outline: 'none',\n userSelect: 'none'\n }\n },\n\n shellStyle() {\n if (this.isFullscreen) return {}\n return {\n width: typeof this.width === 'number' ? this.width + 'px' : this.width,\n display: 'flex',\n flexDirection: 'column'\n }\n },\n\n canUndo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex >= 0\n },\n\n canRedo() {\n // eslint-disable-next-line no-unused-expressions\n this.historyVersion\n return this._historyIndex < this._history.length - 1\n },\n\n headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n cornerStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: (this._headerRowH * this.headerDepth + this._filterRowH) + 'px',\n background: 'var(--eg-color-header-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderBottom: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0\n }\n },\n\n rowOutlineGutterStyle() {\n return { width: OUTLINE_GUTTER + 'px', flexShrink: 0 }\n },\n\n frozenColHeaderStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n scrollColHeaderStyle() {\n // Once measured, lock the header to the body's exact clientWidth (flex: 0 0 <px>)\n // instead of growing independently via flex:1 — this guarantees identical\n // scrollWidth-clientWidth (max scrollLeft) on both, so syncing scrollLeft never clamps\n // one before the other. (padding-right can't do this: clientWidth always includes\n // padding, so padding never actually shrinks it.)\n if (this.headerViewportWidth != null) {\n return { flex: '0 0 ' + this.headerViewportWidth + 'px', overflow: 'hidden' }\n }\n return { flex: 1, overflow: 'hidden' }\n },\n\n bodyAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flex: 1,\n position: 'relative'\n }\n },\n\n rowHeaderColStyle() {\n return {\n width: ROW_HEADER_WIDTH + 'px',\n minWidth: ROW_HEADER_WIDTH + 'px',\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n position: 'sticky',\n left: 0,\n zIndex: 20,\n background: 'var(--eg-color-header-bg)'\n }\n },\n\n rowOutlineColStyle() {\n return {\n width: OUTLINE_GUTTER + 'px',\n minWidth: OUTLINE_GUTTER + 'px',\n flexShrink: 0,\n position: 'relative',\n background: 'var(--eg-color-cell-bg)'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative',\n overflowAnchor: 'none' /* prevent Chrome scroll anchoring from fighting virtual scroll translateY */\n }\n },\n\n frozenColBodyStyle() {\n const w = this.getFrozenWidth()\n return {\n position: 'sticky',\n left: 0,\n width: w + 'px',\n zIndex: 10,\n background: 'white',\n overflow: 'hidden'\n }\n },\n\n mainCellsStyle() {\n const left = this.getFrozenWidth()\n return {\n position: 'absolute',\n left: left + 'px',\n top: 0,\n bottom: 0\n }\n },\n\n scrollContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && !this.pinnedRightSet.has(i)) w += this.getColW(i)\n }\n // Extra right padding so pinned-right zone doesn't cover the last scrollable column\n if (this.hasPinnedRight) w += this.pinnedRightWidth\n return w\n },\n\n scrollableContentWidth() {\n let w = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColHidden(i) && this.isColumnScrollable(i)) w += this.getColW(i)\n }\n return w\n },\n\n pinnedRightBodyStyle() {\n return {\n position: 'sticky',\n right: 0,\n width: this.pinnedRightWidth + 'px',\n zIndex: 10,\n background: 'white',\n flexShrink: 0\n }\n },\n\n pinnedRightHeaderStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative', overflowAnchor: 'none' }\n },\n\n footerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n flexShrink: 0,\n position: 'sticky',\n bottom: 0,\n zIndex: 30,\n background: 'var(--eg-color-footer-bg)',\n boxShadow: '0 -2px 0 0 var(--eg-color-border-strong)'\n }\n },\n\n footerLabelStyle() {\n const outlineW = this.rowGroups.length ? OUTLINE_GUTTER : 0\n return {\n width: (ROW_HEADER_WIDTH + outlineW) + 'px',\n minWidth: (ROW_HEADER_WIDTH + outlineW) + 'px',\n height: this._footerRowH + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n background: 'var(--eg-color-footer-bg)',\n borderRight: '1px solid var(--eg-color-border)',\n borderTop: '1px solid var(--eg-color-border)',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: 'var(--eg-font-size-header)',\n fontWeight: 600\n }\n },\n\n frozenColFooterStyle() {\n const w = this.getFrozenWidth()\n return {\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0\n }\n },\n\n pinnedRightFooterStyle() {\n return {\n width: this.pinnedRightWidth + 'px',\n minWidth: this.pinnedRightWidth + 'px',\n height: this._footerRowH + 'px',\n overflow: 'hidden',\n flexShrink: 0,\n borderLeft: '1px solid var(--eg-color-border)'\n }\n },\n\n colOutlineRowStyle() {\n return {\n position: 'absolute',\n top: -OUTLINE_GUTTER + 'px',\n left: 0,\n right: 0,\n height: OUTLINE_GUTTER + 'px'\n }\n },\n\n resizeLineStyle() {\n if (!this.resizing.active) return {}\n if (this.resizing.type === 'col') {\n return {\n position: 'absolute',\n top: 0,\n left: (this.resizing.currentPos - (this.resizing.containerLeft || 0)) + 'px',\n width: '2px',\n height: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'absolute',\n left: 0,\n top: (this.resizing.currentPos - (this.resizing.containerTop || 0)) + 'px',\n height: '2px',\n width: '100%',\n background: 'var(--eg-color-accent)',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size: {\n immediate: true,\n handler(v) { this.internalSize = v || 'middle' }\n },\n\n theme: {\n immediate: true,\n handler(v) { this.internalTheme = v || 'excel' }\n },\n\n // Runs before the `columns`/`data` watchers below (both call\n // _recomputeRowFilters()) so internalFilters already reflects the prop\n // by the time either of them fires during the initial immediate pass.\n filters: {\n immediate: true,\n handler(v) { this.internalFilters = v ? { ...v } : {} }\n },\n\n internalFilters() { this._scheduleFilterRecompute() },\n\n filterRow() {\n this._recomputeRowFilters()\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n sort: {\n immediate: true,\n handler(v) { this.internalSort = v ? { key: v.key, direction: v.direction } : null }\n },\n\n internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n // Guarded by _lockFlashArmed (set once mounted()) so an initial `locked:\n // true` prop doesn't flash the icon before the grid has even appeared.\n internalLocked(v) {\n if (!this._lockFlashArmed) return\n this._flashLock(v)\n },\n\n autoWidth(v) {\n if (v) this._applyAutoWidth()\n },\n\n columns: {\n immediate: true,\n handler(cols) {\n // Build key→width map from current state before rebuilding leafCols.\n // This preserves user-resized widths across column reorders: when columns\n // are moved, _flatIndex values shift but col.key stays the same.\n const keyToWidth = {}\n ;(this.leafCols || []).forEach(col => {\n if (this.colWidths[col._flatIndex] !== undefined) {\n keyToWidth[col.key] = this.colWidths[col._flatIndex]\n }\n })\n\n this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n\n // Rebuild colWidths by new flat index, using col.key to look up saved widths\n const newColWidths = {}\n this.leafCols.forEach(col => {\n newColWidths[col._flatIndex] = keyToWidth[col.key] !== undefined\n ? keyToWidth[col.key]\n : (col.width || DEFAULT_COL_WIDTH)\n })\n this.colWidths = newColWidths\n\n this.computeColumnTotals()\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this._applyAutoWidth()\n }\n },\n merges: {\n immediate: true,\n handler(v) { this.internalMerges = v ? [...v] : [] }\n },\n collapsedGroups: {\n immediate: true,\n handler(v) { this.internalCollapsedGroups = v ? [...v] : [] }\n },\n locked: {\n immediate: true,\n handler(v) { this.internalLocked = !!v }\n },\n data: {\n immediate: true,\n handler(newData) {\n // Keep _dataRaw in sync so template uses fresh data without triggering dependArray\n this._dataRaw = newData || []\n this._recomputeRowFilters()\n this.updateVirtualScroll()\n this.computeColumnTotals()\n // Force re-render so cell values refresh even when virtualScroll indices don't change\n // (e.g. editing a cell: same row count, same scroll position, only content changed)\n this.$forceUpdate()\n this._applyAutoWidth()\n }\n },\n totalRows() { this.updateVirtualScroll() },\n hiddenRowSet() { this.updateVirtualScroll() },\n totalVirtualHeight() { this.$nextTick(this.syncHeaderViewportWidth) }\n },\n\n created() {\n // Non-reactive row data reference — avoids Vue 2 dependArray(data) O(n) loop.\n // Vue 2 calls dependArray() for EVERY `data[ri]` access in the template,\n // iterating all 50,000 rows each time. With 1200 cells per render that's\n // 60 million iterations per render (~600ms). _dataRaw bypasses this.\n this._dataRaw = this.data || []\n\n // Non-reactive caches: same (w,h) → same style object → Vue 2 skips DOM style writes\n this._cellStyleCache = Object.create(null)\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n // rAF handles for scroll/resize throttle\n this._scrollRAF = null\n this._resizeRAF = null\n\n // Undo/redo history — non-reactive to avoid Vue overhead.\n // Each entry: { type: 'data'|'merges', before: {...}, after: {...} }\n // 'data' stores only diff rows: { indices: number[], rows: object[] }\n this._history = []\n this._historyIndex = -1\n this._historySkip = false // true while undo/redo is in progress\n\n // Column drag preview offsets — non-reactive so direct DOM updates bypass Vue re-renders.\n // Keys are column keys, values are pixel offsets.\n this._dragOffsets = {}\n\n },\n\n mounted() {\n this.updateVirtualScroll()\n this.$nextTick(this.syncHeaderViewportWidth)\n document.addEventListener('mousemove', this.onGlobalMouseMove)\n document.addEventListener('mouseup', this.onGlobalMouseUp)\n document.addEventListener('keydown', this.onGlobalKeyDown)\n document.addEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.addEventListener('resize', this.syncHeaderViewportWidth)\n window.addEventListener('scroll', this._onWindowScrollForDropdown, true)\n this._lockFlashArmed = true\n },\n\n updated() {\n // After Vue re-renders (e.g., virtual scroll adds new rows during drag),\n // apply missing transforms to newly rendered cells that don't have one yet.\n if (!this.colDrag.active || !this.$el) return\n const offsets = this._dragOffsets\n if (!offsets || !Object.keys(offsets).length) return\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n if (el.style.transform) return\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n if (dx) el.style.transform = `translateX(${dx}px)`\n })\n },\n\n beforeDestroy() {\n clearTimeout(this._filterRecomputeTimer)\n clearTimeout(this._onInputTimer)\n document.removeEventListener('mousemove', this.onGlobalMouseMove)\n document.removeEventListener('mouseup', this.onGlobalMouseUp)\n document.removeEventListener('keydown', this.onGlobalKeyDown)\n document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true)\n window.removeEventListener('resize', this.syncHeaderViewportWidth)\n window.removeEventListener('scroll', this._onWindowScrollForDropdown, true)\n if (this.isFullscreen) {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n },\n\n methods: {\n // =========================================================\n // VIRTUAL SCROLL\n // =========================================================\n // In fullscreen, .eg-scroll-body's real height comes from flexbox filling\n // the viewport (which can be far taller than the `height` prop the host\n // originally passed for the fixed-size layout) — measure it directly\n // instead of trusting `height`, otherwise virtual scroll only ever\n // renders enough rows for the old fixed size, leaving the rest of the\n // (visually stretched) container blank.\n _containerH() {\n if (this.isFullscreen && this.$refs.scrollBody) return this.$refs.scrollBody.clientHeight\n const footerH = this.hasFooter ? this._footerRowH : 0\n return this.effectiveHeight - (this._headerRowH * this.headerDepth) - this._filterRowH - footerH\n },\n\n // Same fullscreen-aware measurement as _containerH(), but mirrors PageUp/\n // PageDown's pre-existing (footer-agnostic) row-span formula exactly for\n // the non-fullscreen case, so normal-mode behavior is unchanged.\n _pageRowSpan() {\n const h = this.isFullscreen && this.$refs.scrollBody\n ? this.$refs.scrollBody.clientHeight\n : (this.effectiveHeight - this._headerRowH * this.headerDepth - this._filterRowH)\n return Math.floor(h / this._rowH)\n },\n\n updateVirtualScroll() {\n // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll.\n // overscan=8 (≈224px at default row height) gives enough buffer for fast scroll\n // without blank-band artifacts between rAF updates.\n const result = findVisibleRange(\n this.scrollTop, Math.max(this._containerH(), 100), this.totalRows, this.cumulativeHeights, 8\n )\n this.virtualStartIndex = result.startIndex\n this.virtualEndIndex = result.endIndex\n this.virtualOffsetTop = result.offsetTop\n this.totalVirtualHeight = result.totalHeight\n },\n\n onBodyScroll(e) {\n const target = e.target\n // Sync sibling scroll containers immediately (visual alignment, no layout trigger)\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = target.scrollLeft\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = target.scrollLeft\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = target.scrollTop\n\n // Close dropdown on scroll — fixed-position dropdown drifts away from the moving cell\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame.\n // Read scrollTop from the DOM ref at rAF time (not from the stale event closure)\n // so that multiple scroll events arriving between frames still resolve to the\n // final position — eliminates the flickering caused by render/scroll mismatch.\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n const sb = this.$refs.scrollBody\n if (!sb) return\n this.scrollTop = sb.scrollTop\n this.scrollLeft = sb.scrollLeft\n this.updateVirtualScroll()\n })\n }\n },\n\n onScroll(e) {},\n\n // .eg-scroll-col-header (overflow:hidden, synced via JS) has no native scrollbar, but\n // .eg-scroll-body (overflow:auto) does — its real vertical scrollbar eats into its\n // clientWidth. If both containers just grow via flex:1, the header ends up wider than\n // the body, so its max scrollLeft is smaller and syncing clamps it before the body\n // reaches its rightmost scroll position — column borders desync right at the scroll\n // end. Locking the header to the body's exact measured clientWidth (scrollColHeaderStyle)\n // makes both max scrollLeft values identical.\n syncHeaderViewportWidth() {\n // Auto-height's 70vh cap tracks the real viewport — keep it current on resize.\n if (typeof window !== 'undefined' && this.windowInnerH !== window.innerHeight) {\n this.windowInnerH = window.innerHeight\n }\n const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\n // Window resize while fullscreen changes the real available row-viewport\n // height too (see _containerH()) — recompute which rows are rendered.\n // Same for auto-height (height prop omitted): the 70vh cap just moved.\n if (this.isFullscreen || this.height == null) this.updateVirtualScroll()\n },\n\n // =========================================================\n // HELPERS\n // =========================================================\n isRowHidden(ri) { return this.hiddenRowSet.has(ri) },\n isColHidden(ci) { return this.effectiveHiddenColSet.has(ci) },\n\n getColW(ci) {\n return this.colWidths[ci] !== undefined ? this.colWidths[ci] : (this.leafCols[ci] ? this.leafCols[ci].width || DEFAULT_COL_WIDTH : DEFAULT_COL_WIDTH)\n },\n\n getRowH(ri) {\n if (this.isRowHidden(ri)) return 0\n return this.rowHeights[ri] || this._rowH\n },\n\n getColLeft(ci) {\n let left = 0\n for (let i = 0; i < ci && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) left += this.getColW(i)\n }\n return left\n },\n\n getFrozenWidth() {\n return this.hasPinnedLeft ? this.pinnedLeftWidth : 0\n },\n\n isColumnPinnedLeft(ci) {\n return this.pinnedLeftSet.has(ci)\n },\n\n isColumnPinnedRight(ci) {\n return this.pinnedRightSet.has(ci)\n },\n\n isColumnScrollable(ci) {\n return !this.isColumnPinnedLeft(ci) && !this.isColumnPinnedRight(ci)\n },\n\n getZoneColLeft(ci, zone) {\n let left = 0\n if (zone === 'left') {\n for (const col of this.pinnedLeftCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else if (zone === 'right') {\n for (const col of this.pinnedRightCols) {\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex)) left += this.getColW(col._flatIndex)\n }\n } else {\n for (let i = 0; i < this.leafCols.length; i++) {\n const col = this.leafCols[i]\n if (col._flatIndex === ci) break\n if (!this.isColHidden(col._flatIndex) && this.isColumnScrollable(col._flatIndex)) {\n left += this.getColW(col._flatIndex)\n }\n }\n }\n return left\n },\n\n isCellInZone(cell, rowIdx, zone) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n const z = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n return z === zone\n },\n\n getHeaderCellStyleForZone(cell, rowIndex, zone) {\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z === zone) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const z = this.isColumnPinnedLeft(i) ? 'left' : (this.isColumnPinnedRight(i) ? 'right' : 'scroll')\n if (z !== zone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getRangeWidth(startCol, endCol) {\n let w = 0\n for (let i = startCol; i <= endCol && i < this.leafCols.length; i++) {\n if (!this.isColHidden(i)) w += this.getColW(i)\n }\n return w\n },\n\n getRangeHeight(startRow, endRow) {\n let h = 0\n for (let i = startRow; i <= endRow && i < this.totalRows; i++) {\n if (!this.isRowHidden(i)) h += this.getRowH(i)\n }\n return h\n },\n\n getCellValue(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return ''\n const row = this._dataRaw[ri]\n if (!row) return ''\n const v = row[col.key]\n return v !== undefined ? v : ''\n },\n\n isEditing(ri, ci) {\n return this.editingCell && this.editingCell.row === ri && this.editingCell.col === ci\n },\n\n isMergedAway(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return false\n const nm = normalizeRange(merge)\n return !(nm.startRow === ri && nm.startCol === ci)\n },\n\n getMergeSpan(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n if (!merge) return { rowspan: 1, colspan: 1 }\n const nm = normalizeRange(merge)\n return {\n rowspan: nm.endRow - nm.startRow + 1,\n colspan: nm.endCol - nm.startCol + 1\n }\n },\n\n isSelected(ri, ci) {\n return isCellInAnyRange(ri, ci, this.selectionRanges)\n },\n\n isActiveCell(ri, ci) {\n return this.activeCell.row === ri && this.activeCell.col === ci\n },\n\n isCopyHighlighted(ri, ci) {\n if (!this.clipboardRange || this.clipboardMode !== 'copy') return false\n return isCellInRange(ri, ci, this.clipboardRange)\n },\n\n // =========================================================\n // STYLES\n // =========================================================\n getRowStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowStyleCache[key]) return this._rowStyleCache[key]\n const style = { height: h + 'px', minHeight: h + 'px' }\n this._rowStyleCache[key] = style\n return style\n },\n\n getRowHeaderStyle(ri) {\n const h = this.getRowH(ri)\n const key = h\n if (this._rowHeaderStyleCache[key]) return this._rowHeaderStyleCache[key]\n const style = { height: h + 'px' }\n this._rowHeaderStyleCache[key] = style\n return style\n },\n\n getCellStyle(ri, ci) {\n const merge = findMergeAtCell(ri, ci, this.internalMerges)\n const nm = merge ? normalizeRange(merge) : null\n let w = this.getColW(ci)\n let h = this.getRowH(ri)\n\n if (nm && nm.startRow === ri && nm.startCol === ci) {\n w = this.getRangeWidth(nm.startCol, nm.endCol)\n h = this.getRangeHeight(nm.startRow, nm.endRow)\n }\n\n // Position cells with the same absolute-left arithmetic the header uses\n // (getZoneColLeft), instead of relying on flex layout to accumulate widths —\n // flex and a JS integer sum can drift apart by a few px over many columns,\n // desyncing the body's column borders from the header's.\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n\n // Cache by dimensions+position — same (w,h,left) → same object ref → Vue 2 skips DOM style writes\n const key = w + '_' + h + '_' + left\n if (this._cellStyleCache[key]) return this._cellStyleCache[key]\n const style = { position: 'absolute', left: left + 'px', top: '0px', width: w + 'px', minWidth: w + 'px', height: h + 'px' }\n this._cellStyleCache[key] = style\n return style\n },\n\n getCellClasses(ri, ci) {\n const col = this.leafCols[ci]\n return {\n 'eg-cell-active': this.isActiveCell(ri, ci),\n 'eg-cell-selected': this.isSelected(ri, ci) && !this.isActiveCell(ri, ci),\n 'eg-cell-fill-preview': this.isInFillPreview(ri, ci),\n 'eg-cell-copy': this.isCopyHighlighted(ri, ci),\n 'eg-cell-merged-hidden': this.isMergedAway(ri, ci),\n 'eg-cell-select': !!(col && this.isSelectLikeCol(col)),\n 'eg-cell-date': !!(col && col.type === 'date'),\n 'eg-cell-checkbox': !!(col && col.type === 'checkbox'),\n 'eg-cell-action': !!(col && col.type === 'action'),\n 'eg-cell-readonly': !this.isCellEditable(ri, ci),\n 'eg-cell-invalid': !!(this.invalidCell && this.invalidCell.row === ri && this.invalidCell.col === ci),\n 'eg-cell--has-suffix': !!(col && this.hasSuffixSlot(col)),\n 'eg-cell--drag-source': this.colDrag.active && ci >= this.colDrag.sourceFirstLeaf && ci < this.colDrag.sourceFirstLeaf + this.colDrag.sourceLeafCount,\n 'eg-cell-align-right': !!(col && col.align === 'right'),\n 'eg-cell-align-center': !!(col && col.align === 'center')\n }\n },\n\n isInFillPreview(ri, ci) {\n if (!this.fillDrag.active || !this.fillDrag.sourceRange) return false\n const src = this.fillDrag.sourceRange\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n if (targetRow > src.endRow) {\n return ri > src.endRow && ri <= targetRow && ci >= src.startCol && ci <= src.endCol\n }\n if (targetCol > src.endCol) {\n return ci > src.endCol && ci <= targetCol && ri >= src.startRow && ri <= src.endRow\n }\n return false\n },\n\n getHeaderCellStyle(cell, rowIndex, isFrozen) {\n const zone = isFrozen ? 'left' : 'scroll'\n let leftLeafIdx = cell._firstLeafFlatIndex\n if (cell.isGroup) {\n const end = leftLeafIdx + cell.colspan - 1\n for (let i = leftLeafIdx; i <= end; i++) {\n if (this.isColHidden(i)) continue\n if (isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)) { leftLeafIdx = i; break }\n }\n }\n const left = this.getZoneColLeft(leftLeafIdx, zone)\n\n let width = 0\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (!inZone) continue\n width += this.getColW(i)\n }\n } else {\n width = this.getColW(cell.column._flatIndex)\n }\n\n return {\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px'\n }\n },\n\n getHeaderDragTransform(cell) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking, no re-render on offset change\n let key\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n } else {\n key = cell.column.key\n }\n if (!key) return null\n const dx = this._dragOffsets[key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n getCellDragTransform(ci) {\n if (!this.colDrag.active) return null\n // Read from non-reactive _dragOffsets — no Vue dependency tracking\n const col = this.leafCols[ci]\n if (!col) return null\n const dx = this._dragOffsets[col.key]\n if (!dx) return null\n return { transform: `translateX(${dx}px)` }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n\n // col.totalConfig.visible (boolean) takes priority over col.showTotal when present.\n isColumnTotalVisible(col) {\n if (col.totalConfig && typeof col.totalConfig.visible === 'boolean') return col.totalConfig.visible\n return !!col.showTotal\n },\n\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!this.isColumnTotalVisible(col)) return\n let sum = 0\n let hasNumber = false\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n const v = row ? row[col.key] : undefined\n if (typeof v === 'number' && isFinite(v)) {\n sum += v\n hasNumber = true\n }\n }\n totals[col._flatIndex] = hasNumber ? Math.round(sum * 1e6) / 1e6 : null\n })\n this.columnTotals = totals\n },\n\n getColumnTotalDisplay(ci) {\n const col = this.leafCols[ci]\n if (!col || !this.isColumnTotalVisible(col)) return ''\n const raw = this.columnTotals[ci]\n const cfg = col.totalConfig\n if (cfg && typeof cfg.formatter === 'function') {\n return cfg.formatter(raw, this._dataRaw, col, ci)\n }\n return raw === null || raw === undefined ? '-' : raw\n },\n\n getFooterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n const style = {\n position: 'absolute',\n left: left + 'px',\n top: '0px',\n width: w + 'px',\n minWidth: w + 'px',\n height: this._footerRowH + 'px'\n }\n const col = this.leafCols[ci]\n const extra = col && col.totalConfig && col.totalConfig.style\n return extra ? { ...style, ...extra } : style\n },\n\n getFooterCellClass(ci) {\n const col = this.leafCols[ci]\n if (!col) return null\n const classList = col.totalConfig && col.totalConfig.classList\n const base = this.isColumnTotalVisible(col) ? 'eg-footer-cell-total' : null\n if (!base) return classList || null\n return classList ? [base, classList] : base\n },\n\n isCellVisible(cell, rowIndex, isFrozen) {\n if (cell.isGroup) {\n const start = cell._firstLeafFlatIndex\n const end = start + cell.colspan - 1\n for (let i = start; i <= end; i++) {\n if (this.isColHidden(i)) continue\n const inZone = isFrozen ? this.isColumnPinnedLeft(i) : this.isColumnScrollable(i)\n if (inZone) return true\n }\n return false\n }\n const ci = cell.column._flatIndex\n if (this.isColHidden(ci)) return false\n return isFrozen ? this.isColumnPinnedLeft(ci) : this.isColumnScrollable(ci)\n },\n\n getRowGroupBtnStyle(group) {\n const top = getRowTop(group.startIndex, this._rowH, this.rowHeights) - this.scrollTop\n return {\n position: 'absolute',\n top: top + 'px',\n left: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px',\n zIndex: 5\n }\n },\n\n getColGroupBtnStyle(group) {\n const left = this.getColLeft(group.startIndex) - this.scrollLeft\n return {\n position: 'absolute',\n left: left + 'px',\n top: '2px',\n width: '16px',\n height: '16px',\n lineHeight: '16px',\n textAlign: 'center',\n fontSize: '10px',\n cursor: 'pointer',\n background: 'var(--eg-color-accent-soft)',\n border: '1px solid var(--eg-color-group-border)',\n borderRadius: '2px'\n }\n },\n\n // =========================================================\n // CELL SELECTION (3.1)\n // =========================================================\n selectCell(row, col, addToSelection = false) {\n if (row < 0 || row >= this.totalRows || col < 0 || col >= this.leafCols.length) return\n this.activeCell = { row, col }\n if (addToSelection) {\n this.selectionRanges.push(singleCellRange(row, col))\n } else {\n this.selectionRanges = [singleCellRange(row, col)]\n }\n this.emitSelectionChange()\n },\n\n selectRange(startRow, startCol, endRow, endCol, replace = true) {\n const range = { startRow, startCol, endRow, endCol }\n if (replace) {\n this.selectionRanges = [range]\n } else {\n this.selectionRanges.push(range)\n }\n this.emitSelectionChange()\n },\n\n selectRow(ri) {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [{ startRow: ri, endRow: ri, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n selectColumn(ci) {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n this.emitSelectionChange()\n },\n\n selectAll() {\n this.activeCell = { row: 0, col: 0 }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: 0, endCol: this.leafCols.length - 1 }]\n this.emitSelectionChange()\n },\n\n emitSelectionChange() {\n this.$emit('selection-change', {\n activeCell: { ...this.activeCell },\n ranges: this.selectionRanges.map(r => normalizeRange(r))\n })\n },\n\n onCellMouseDown(e, ri, ci) {\n this.invalidCell = null\n if (this.editingCell) {\n const editCol = this.leafCols[this.editingCell.col]\n if (editCol && this._hasCustomEdit(editCol)) {\n this.cancelEdit()\n } else {\n this.commitEdit()\n }\n }\n this.hideContextMenu()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n\n if (e.ctrlKey || e.metaKey) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Ctrl+click: start new independent range\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges.push(this.mouseDownRange)\n this.isCtrlDrag = true\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n // Shift+click: extend primary range to this cell\n this.selectionRanges = [buildRange(this.activeCell, { row: ri, col: ci })]\n } else {\n const wasActive = this.activeCell.row === ri && this.activeCell.col === ci\n this.activeCell = { row: ri, col: ci }\n this.mouseDownRange = { startRow: ri, startCol: ci, endRow: ri, endCol: ci }\n this.selectionRanges = [this.mouseDownRange]\n this.isCtrlDrag = false\n\n // Open select/date dropdown on click if cell was already active (2nd click)\n const col = this.leafCols[ci]\n if (wasActive && col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openSelectDropdown(ri, ci, e)\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (wasActive && col && col.type === 'date' && !this._hasCustomEdit(col) && this.isCellEditable(ri, ci)) {\n this.openDatePickerForActive()\n e.preventDefault()\n this.isMouseDown = true\n this.emitSelectionChange()\n return\n }\n if (this.selectDropdown) this.closeSelectDropdown()\n if (this.datePickerOverlay) this.closeDatePicker()\n }\n\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onCellMouseOver(e, ri, ci) {\n if (!this.isMouseDown) return\n if (this.fillDrag.active) return\n if (this.dragMove.active) {\n this.dragMove.targetRow = ri\n this.dragMove.targetCol = ci\n return\n }\n\n // Extend current drag range\n if (this.mouseDownRange) {\n this.mouseDownRange.endRow = ri\n this.mouseDownRange.endCol = ci\n this.emitSelectionChange()\n }\n },\n\n onCellMouseUp() {\n this.isMouseDown = false\n },\n\n onCellDblClick(ri, ci) {\n const col = this.leafCols[ci]\n if (!this.isCellEditable(ri, ci)) return\n if (col && this.isSelectLikeCol(col) && !this._hasCustomEdit(col)) {\n this.openSelectDropdownForActive()\n return\n }\n if (col && col.type === 'date' && !this._hasCustomEdit(col)) {\n this.openDatePickerForActive()\n return\n }\n if (col && col.type === 'checkbox') return\n if (col && col.type === 'action') return\n this.startEditing(ri, ci, null, true)\n },\n\n onRowHeaderMouseDown(e, ri) {\n const endCol = this.leafCols.length - 1\n const fullRow = { startRow: ri, endRow: ri, startCol: 0, endCol }\n\n if (e.ctrlKey || e.metaKey) {\n if (e.shiftKey && this.selectionRanges.length > 0) {\n // Ctrl+Shift: active qatordan ri gacha yangi range qo'sh\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [...this.selectionRanges, { startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n // Ctrl+click: qatorni toggle qil\n const idx = this.selectionRanges.findIndex(r =>\n r.startRow === ri && r.endRow === ri && r.startCol === 0 && r.endCol === endCol\n )\n if (idx >= 0) {\n const next = this.selectionRanges.filter((_, i) => i !== idx)\n this.selectionRanges = next.length ? next : [fullRow]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [...this.selectionRanges, fullRow]\n }\n }\n } else if (e.shiftKey && this.selectionRanges.length > 0) {\n const sr = Math.min(this.activeCell.row, ri)\n const er = Math.max(this.activeCell.row, ri)\n this.selectionRanges = [{ startRow: sr, endRow: er, startCol: 0, endCol }]\n } else {\n this.activeCell = { row: ri, col: 0 }\n this.selectionRanges = [fullRow]\n }\n\n this.mouseDownRange = this.selectionRanges[this.selectionRanges.length - 1]\n this.isMouseDown = true\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderMouseDown(e, cell) {\n // Start pending drag (activates after 5 px horizontal movement)\n // Works for both leaf and group headers\n if (this.columnDraggable && !this.internalLocked && !e.shiftKey && !e.ctrlKey && !e.metaKey) {\n this.colDrag.pending = true\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = cell._firstLeafFlatIndex\n this.colDrag.sourceLeafCount = cell.isGroup ? cell.colspan : 1\n this.colDrag.startX = e.clientX\n this.colDrag.startY = e.clientY\n this.colDrag.ghostLabel = cell.column.title || ''\n }\n\n if (cell.isGroup) {\n // Group header click: select all leaf columns within the group\n const startCol = cell._firstLeafFlatIndex\n const endCol = startCol + cell.colspan - 1\n this.activeCell = { row: 0, col: startCol }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol, endCol }]\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n return\n }\n\n const ci = cell.column._flatIndex\n if (e.shiftKey && this.selectionRanges.length > 0) {\n const sc = Math.min(this.activeCell.col, ci)\n const ec = Math.max(this.activeCell.col, ci)\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: sc, endCol: ec }]\n } else {\n this.activeCell = { row: 0, col: ci }\n this.selectionRanges = [{ startRow: 0, endRow: this.totalRows - 1, startCol: ci, endCol: ci }]\n }\n this.isMouseDown = true\n this.mouseDownRange = this.selectionRanges[0]\n this.emitSelectionChange()\n e.preventDefault()\n },\n\n onColHeaderDblClick(e, cell) {\n if (cell.isGroup) return\n this.autoFitColumn(cell.column._flatIndex)\n },\n\n onColHeaderContextMenu(e, cell) {\n if (!cell.isGroup) {\n this._openContextMenu(e, -1, cell.column._flatIndex)\n }\n },\n\n onRowContextMenu(e, ri) {\n this._openContextMenu(e, ri, -1)\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this._openContextMenu(e, ri, ci)\n },\n\n _openContextMenu(e, row, col) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row, col }\n this.contextMenuPositioned = false\n this.$nextTick(this._positionContextMenu)\n },\n\n // Flips the menu up/left if it would overflow the viewport at the raw\n // click coordinates — measured against the actual rendered size (item\n // count varies with devMode/pin/row-vs-col context, so an estimate isn't\n // reliable). Rendered off-screen (left:-9999px, see template) while\n // unpositioned so this first measurement reflects the menu's true\n // natural (single-line) width — measuring it in place near a viewport\n // edge would let the browser's own shrink-to-fit sizing wrap the text\n // to fit the cramped space, which then \"sticks\" even after flipping to\n // a roomier position since we only move it, we don't force a width.\n // Stays hidden (visibility, not v-if) until repositioned so there's no\n // visible jump.\n _positionContextMenu() {\n const el = this.$refs.contextMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.contextMenu.x\n let y = this.contextMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.contextMenu.x = x\n this.contextMenu.y = y\n this.contextMenuPositioned = true\n },\n\n hideContextMenu() {\n this.contextMenu.visible = false\n },\n\n // General click-anywhere-in-the-grid dismissal for both floating menus —\n // each menu item stops propagation (@click.stop) so a click *inside*\n // either menu never reaches this handler.\n hideFloatingMenus() {\n this.hideContextMenu()\n this.hideSortMenu()\n },\n\n // =========================================================\n // KEYBOARD NAVIGATION (3.2)\n // =========================================================\n onKeyDown(e) {\n // Delegate keyboard navigation to the active dropdown component\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n if (comp && comp.handleKey) comp.handleKey(e)\n return\n }\n\n if (this.infoModal) {\n if (e.key === 'Escape') this.closeInfoModal()\n return\n }\n\n if (this.editingCell) {\n // Normally handled by onEditKeyDown on the built-in <input> (which stops\n // propagation, so this branch never runs for it). Custom #cell-{key} edit\n // slots own their own input and focus it themselves — if that focus didn't\n // land (e.g. the host relies on the unreliable HTML `autofocus` attribute\n // on a freshly-inserted element), Escape falls through to here instead.\n // Without this, editingCell stays stuck and even arrow-key navigation is\n // swallowed by this same guard on every subsequent keydown.\n if (e.key === 'Escape') this.cancelEdit()\n return\n }\n const key = e.key\n\n if ((e.ctrlKey || e.metaKey) && key === 'z' && !e.shiftKey) {\n e.preventDefault()\n this.undo()\n return\n }\n if ((e.ctrlKey || e.metaKey) && (key === 'y' || (key === 'z' && e.shiftKey))) {\n e.preventDefault()\n this.redo()\n return\n }\n\n if (key === 'a' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.selectAll()\n return\n }\n\n if (key === 'c' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.copySelection()\n return\n }\n if (key === 'x' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.cutSelection()\n return\n }\n if (key === 'v' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault()\n this.pasteAt(this.activeCell.row, this.activeCell.col)\n return\n }\n\n if (key === 'Delete' || key === 'Backspace') {\n e.preventDefault()\n this.clearSelection()\n return\n }\n\n if (key === 'F2') {\n e.preventDefault()\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol) && activeEditable) {\n this.openDatePickerForActive()\n } else if (activeCol && activeCol.type === 'checkbox' && activeEditable) {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else if (activeCol && activeCol.type === 'action' && activeEditable) {\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n } else if (activeEditable) {\n this.startEditing(this.activeCell.row, this.activeCell.col, null, true)\n }\n return\n }\n\n if (key === ' ') {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n this.triggerCellAction(this.activeCell.row, this.activeCell.col, e)\n return\n }\n }\n\n if (key === 'Escape') {\n this.clipboardRange = null\n this.clipboardMatrix = null\n this.clipboardMode = null\n return\n }\n\n if (key === 'Tab') {\n e.preventDefault()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n return\n }\n if (key === 'Enter') {\n e.preventDefault()\n this.moveActive(e.shiftKey ? -1 : 1, 0, e)\n return\n }\n\n const arrows = { ArrowUp: [-1,0], ArrowDown: [1,0], ArrowLeft: [0,-1], ArrowRight: [0,1] }\n if (arrows[key]) {\n e.preventDefault()\n const [dr, dc] = arrows[key]\n if (e.shiftKey) {\n // Extend selection\n const pr = this.primaryRange || singleCellRange(this.activeCell.row, this.activeCell.col)\n const nm = normalizeRange(pr)\n // Determine which edge to extend\n if (dr === -1) pr.startRow = Math.max(0, nm.startRow - 1)\n if (dr === 1) pr.endRow = Math.min(this.totalRows - 1, nm.endRow + 1)\n if (dc === -1) pr.startCol = Math.max(0, nm.startCol - 1)\n if (dc === 1) pr.endCol = Math.min(this.leafCols.length - 1, nm.endCol + 1)\n this.selectionRanges = [pr]\n this.emitSelectionChange()\n } else {\n this.moveActive(dr, dc, e)\n }\n return\n }\n\n if (key === 'Home') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(0, 0)\n } else {\n this.selectCell(this.activeCell.row, 0)\n }\n return\n }\n if (key === 'End') {\n e.preventDefault()\n if (e.ctrlKey) {\n this.selectCell(this.totalRows - 1, this.leafCols.length - 1)\n } else {\n this.selectCell(this.activeCell.row, this.leafCols.length - 1)\n }\n return\n }\n if (key === 'PageUp') {\n e.preventDefault()\n this.moveActive(-this._pageRowSpan(), 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n this.moveActive(this._pageRowSpan(), 0, e)\n return\n }\n },\n\n onKeyPress(e) {\n if (this.editingCell) return\n if (this.selectDropdown) return\n if (this.datePickerOverlay) return\n if (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n const activeEditable = this.isCellEditable(this.activeCell.row, this.activeCell.col)\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'date' && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n if (activeEditable) this.openDatePickerForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\n e.preventDefault()\n return\n }\n if (activeCol && activeCol.type === 'action') {\n e.preventDefault()\n return\n }\n this.startEditing(this.activeCell.row, this.activeCell.col, e.key, false)\n }\n },\n\n onGlobalKeyDown(e) {\n if (e.key === 'Escape' && (this.colDrag.active || this.colDrag.pending)) {\n this._resetColDrag()\n e.preventDefault()\n }\n },\n\n moveActive(dr, dc, e) {\n let row = this.activeCell.row + dr\n let col = this.activeCell.col + dc\n\n // Tab: wrap around rows\n if (e && e.key === 'Tab') {\n if (col >= this.leafCols.length) { col = 0; row++ }\n if (col < 0) { col = this.leafCols.length - 1; row-- }\n }\n\n row = Math.max(0, Math.min(this.totalRows - 1, row))\n col = Math.max(0, Math.min(this.leafCols.length - 1, col))\n\n // Skip hidden cols\n while (this.isColHidden(col) && col > 0 && col < this.leafCols.length - 1) {\n col += dc >= 0 ? 1 : -1\n }\n // Skip hidden rows\n while (this.isRowHidden(row) && row > 0 && row < this.totalRows - 1) {\n row += dr >= 0 ? 1 : -1\n }\n\n // Handle merge navigation\n const resolved = resolveMergeAnchor(row, col, this.internalMerges)\n row = resolved.row\n col = resolved.col\n\n this.activeCell = { row, col }\n this.selectionRanges = [singleCellRange(row, col)]\n this.emitSelectionChange()\n this.scrollToCell(row, col)\n },\n\n scrollToCell(ri, ci) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rowTop = getRowTop(ri, this._rowH, this.rowHeights)\n const rowBottom = rowTop + this.getRowH(ri)\n const colLeft = this.getColLeft(ci) - this.getFrozenWidth()\n const colRight = colLeft + this.getColW(ci)\n\n if (rowTop < scrollBody.scrollTop) scrollBody.scrollTop = rowTop\n if (rowBottom > scrollBody.scrollTop + scrollBody.clientHeight) {\n scrollBody.scrollTop = rowBottom - scrollBody.clientHeight\n }\n if (colLeft < scrollBody.scrollLeft) scrollBody.scrollLeft = colLeft\n if (colRight > scrollBody.scrollLeft + scrollBody.clientWidth) {\n scrollBody.scrollLeft = colRight - scrollBody.clientWidth\n }\n },\n\n // =========================================================\n // EDITING (used by 3.1 + 3.2)\n // =========================================================\n startEditing(ri, ci, initialChar = null, keepValue = false) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (ri < 0 || ri >= this.totalRows) return\n if (!this._hasCustomEdit(col)) {\n if (this.isSelectLikeCol(col)) { this.openSelectDropdownForActive(); return }\n if (col.type === 'date') { this.openDatePickerForActive(); return }\n if (col.type === 'checkbox') return\n if (col.type === 'action') return\n }\n\n this.invalidCell = null\n this.editingCell = { row: ri, col: ci }\n if (initialChar !== null) {\n this.editValue = initialChar\n } else if (keepValue) {\n this.editValue = String(this.getCellValue(ri, ci) || '')\n } else {\n this.editValue = ''\n }\n this.editOriginalValue = String(this.getCellValue(ri, ci) || '')\n\n this.$nextTick(() => {\n const inputs = this.$refs.editInput\n if (inputs) {\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) {\n input.focus()\n if (keepValue) {\n input.setSelectionRange(input.value.length, input.value.length)\n }\n }\n }\n })\n },\n\n commitEdit() {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n\n if (!this.isCellValueValid(row, col, this.editValue)) {\n this._showInvalidCell(row, col, this.editValue)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: this.editValue }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(row, col, this.editValue, newData[row])\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n cancelEdit() {\n clearTimeout(this._onInputTimer)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // Fires while the user is actively typing in the plain-text edit input —\n // debounced (col.debounceMs, default 400ms) so col.onInput isn't called\n // on every keystroke. `row` is the pre-edit row (not yet committed);\n // `value` is the live in-progress text.\n onEditInput(e) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n this._scheduleOnInput(row, col, e.target.value)\n },\n\n _scheduleOnInput(ri, ci, value) {\n const col = this.leafCols[ci]\n if (!col || typeof col.onInput !== 'function') return\n clearTimeout(this._onInputTimer)\n this._onInputPending = true\n const delay = typeof col.debounceMs === 'number' ? col.debounceMs : 400\n this._onInputTimer = setTimeout(() => {\n this._onInputPending = false\n const row = this._dataRaw[ri]\n if (row) {\n this._lastFiredOnInput = { ri, ci, value }\n col.onInput(value, row, ri, col)\n }\n }, delay)\n },\n\n // Cancels any pending debounced call and fires col.onInput immediately —\n // used at commit points (Enter/blur, date pick, checkbox toggle) so a\n // still-in-flight debounce timer never drops the final value. If nothing\n // is pending (the debounce already fired naturally with this exact\n // value), skip re-firing — commitEdit() calls this unconditionally, so\n // without this guard a slow typist would get onInput called twice for\n // the same settled value.\n _flushOnInput(ri, ci, value, row) {\n const col = this.leafCols[ci]\n if (!col || typeof col.onInput !== 'function') return\n const wasPending = this._onInputPending\n clearTimeout(this._onInputTimer)\n this._onInputPending = false\n const last = this._lastFiredOnInput\n if (!wasPending && last && last.ri === ri && last.ci === ci && last.value === value) return\n this._lastFiredOnInput = { ri, ci, value }\n col.onInput(value, row || this._dataRaw[ri], ri, col)\n },\n\n hasCellSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])\n },\n hasLegacyEditSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])\n },\n hasPrefixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['prefix-' + col.key])\n },\n hasSuffixSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['suffix-' + col.key])\n },\n _hasCustomEdit(col) {\n // For select/treeSelect/date: cell-{key} slot only customizes display; picker still opens normally\n if (col.type === 'select' || col.type === 'treeSelect' || col.type === 'date') {\n return this.hasLegacyEditSlot(col)\n }\n return this.hasCellSlot(col) || this.hasLegacyEditSlot(col)\n },\n\n commitEditSlot(value) {\n if (!this.editingCell) return\n const { row, col } = this.editingCell\n const colDef = this.leafCols[col]\n if (!colDef) { this.editingCell = null; return }\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n this.editingCell = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // CHECKBOX CELLS\n // =========================================================\n\n isCheckboxChecked(ri, ci) {\n const v = this.getCellValue(ri, ci)\n if (v === true || v === 1) return true\n if (typeof v === 'string') return ['true', '1'].includes(v.trim().toLowerCase())\n return false\n },\n\n setCheckboxValue(ri, ci, value) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || !this.isCellEditable(ri, ci)) return\n if (!this.isCellValueValid(ri, ci, value)) {\n this._showInvalidCell(ri, ci, value)\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[ri] = { ...oldData[ri], [col.key]: value }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(ri, ci, value, newData[ri])\n },\n\n toggleCheckbox(ri, ci) {\n this.setCheckboxValue(ri, ci, !this.isCheckboxChecked(ri, ci))\n },\n\n // =========================================================\n // ACTION CELLS (col.type === 'action')\n // =========================================================\n\n // Dynamic v-on argument expressions can't contain quotes/spaces in Vue 2.6\n // templates, so the 'click' fallback is resolved here instead of inline.\n actionHandlerEvent(col) {\n return (col && col.handlerType) || 'click'\n },\n\n triggerCellAction(ri, ci, e) {\n if (this.internalLocked) return\n const col = this.leafCols[ci]\n if (!col || col.type !== 'action' || typeof col.handler !== 'function') return\n if (!this.isCellEditable(ri, ci)) return\n col.handler(this._dataRaw[ri], ri, col, ci, e)\n },\n\n // =========================================================\n // CELL EDITABILITY (3.17)\n // =========================================================\n\n isCellEditable(ri, ci) {\n const col = this.leafCols[ci]\n if (!col) return false\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellEditable === 'function') return !!this.cellEditable(row, ri, col, ci)\n if (typeof col.editable === 'function') return !!col.editable(row, ri)\n if (col.editable === false) return false\n return true\n },\n\n // =========================================================\n // CELL VALUE VALIDATION (cellValidator)\n // =========================================================\n\n isCellValueValid(ri, ci, value) {\n if (typeof this.cellValidator !== 'function') return true\n const col = this.leafCols[ci]\n if (!col) return true\n const row = this._dataRaw[ri] || {}\n return !!this.cellValidator(value, row, ri, col, ci)\n },\n\n _resolveValidationMessage(ri, ci, value) {\n const col = this.leafCols[ci]\n const row = this._dataRaw[ri] || {}\n if (typeof this.cellValidationMessage === 'function') return this.cellValidationMessage(value, row, ri, col, ci)\n if (typeof this.cellValidationMessage === 'string' && this.cellValidationMessage) return this.cellValidationMessage\n return this.t.invalidValue\n },\n\n // Find the on-screen rect of the cell currently being committed — the edit\n // input if still mounted (commitEdit, before editingCell is cleared), else\n // the active cell element (checkbox/select commits, which never enter edit mode).\n _invalidCellRect() {\n const inputs = this.$refs.editInput\n const input = Array.isArray(inputs) ? inputs[0] : inputs\n if (input) return input.getBoundingClientRect()\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n return el ? el.getBoundingClientRect() : null\n },\n\n _showInvalidCell(ri, ci, value) {\n const message = this._resolveValidationMessage(ri, ci, value)\n this.invalidCell = { row: ri, col: ci, message, rect: this._invalidCellRect() }\n this.$emit('cell-validation-error', { row: ri, col: ci, value, message })\n },\n\n // Resolve+emit messages for a batch of rejected { row, col, value } entries\n // from a bulk write (paste / fill / clear). blocked=true means the whole\n // operation was aborted (strictBulkValidation); false means these cells\n // were skipped but the rest of the write still applied.\n _emitBulkValidationError(skipped, blocked) {\n const cells = skipped.map(c => ({\n row: c.row, col: c.col, value: c.value,\n message: this._resolveValidationMessage(c.row, c.col, c.value)\n }))\n this.$emit('bulk-validation-error', { blocked, cells })\n },\n\n // =========================================================\n // SELECT DROPDOWN (3.16) + TREE SELECT (3.22)\n // =========================================================\n\n isSelectLikeCol(col) {\n return !!(col && (col.type === 'select' || col.type === 'treeSelect'))\n },\n\n treeOptionKeys(col) {\n return {\n valueKey: col.optionValueKey || 'value',\n labelKey: col.optionLabelKey || 'label',\n childrenKey: col.optionChildrenKey || 'children'\n }\n },\n\n _treeFindLabel(nodes, keys, value) {\n for (const node of (nodes || [])) {\n if (String(node[keys.valueKey]) === value) {\n return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])\n }\n const found = this._treeFindLabel(node[keys.childrenKey], keys, value)\n if (found != null) return found\n }\n return null\n },\n\n // col.options may be a static array OR a function (row, rowIndex, column, colIndex) => array,\n // letting hosts return a different option list per cell.\n hasOptionsSource(col) {\n return !!(col && (Array.isArray(col.options) || typeof col.options === 'function'))\n },\n\n resolveOptionsArray(col, ri, ci) {\n if (!col) return []\n if (typeof col.options === 'function') {\n const row = ri != null ? (this._dataRaw[ri] || {}) : {}\n const result = col.options(row, ri, col, ci != null ? ci : col._flatIndex)\n return Array.isArray(result) ? result : []\n }\n return Array.isArray(col.options) ? col.options : []\n },\n\n colOptions(ci, ri) {\n const col = this.leafCols[ci]\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return []\n return raw.map(o =>\n typeof o === 'string' ? { value: o, label: o } : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n\n getSelectLabel(ci, value, ri) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, ri, ci)\n if (!raw.length) return v\n const found = this._treeFindLabel(raw, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci, ri)\n if (!opts.length) return v\n const found = opts.find(o => String(o.value) === v)\n return found ? found.label : v\n },\n\n _buildSelectDropdownState(ri, ci, col, currentValue, rect) {\n const rectObj = { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n return {\n row: ri, col: ci,\n tree: col.type === 'treeSelect',\n keys: col.type === 'treeSelect' ? this.treeOptionKeys(col) : null,\n rawOptions: this.resolveOptionsArray(col, ri, ci),\n currentValue,\n rect: rectObj,\n enableSearch: !!col.enableSearch\n }\n },\n\n openSelectDropdown(ri, ci, e) {\n const col = this.leafCols[ci]\n if (!this.hasOptionsSource(col)) return\n const el = e.currentTarget\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(ri, ci) != null ? this.getCellValue(ri, ci) : '')\n this.selectDropdown = this._buildSelectDropdownState(ri, ci, col, currentValue, rect)\n },\n\n openSelectDropdownForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!this.hasOptionsSource(colDef)) return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const currentValue = String(this.getCellValue(row, col) != null ? this.getCellValue(row, col) : '')\n this.selectDropdown = this._buildSelectDropdownState(row, col, colDef, currentValue, rect)\n })\n },\n\n onComponentSelect({ value, label }) {\n if (this.internalLocked) { this.closeSelectDropdown(); return }\n if (!this.selectDropdown) return\n const { row, col } = this.selectDropdown\n const colDef = this.leafCols[col]\n if (colDef) {\n if (!this.isCellValueValid(row, col, value)) {\n this._showInvalidCell(row, col, value)\n this.closeSelectDropdown()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: value }\n this._emitDataChange(oldData, newData)\n if (typeof colDef.onSelect === 'function') {\n // Deferred the same way as _fireCellCallbacksForRange (paste/fill/drag-move) —\n // a host watching the emitted data (e.g. mirroring it into its own\n // store) needs that watcher to run first, or onSelect reads stale state.\n this.$nextTick(() => colDef.onSelect({ value, label }, newData[row], row, colDef))\n }\n }\n this.closeSelectDropdown()\n },\n\n // Always returns focus to the grid wrapper — the dropdown (especially with\n // enableSearch) can hold real DOM focus on its own search input, and every\n // close path (Escape, outside click, picking a value) must hand focus back\n // or keyboard navigation on the grid goes dead until the user clicks a cell.\n closeSelectDropdown() {\n this.selectDropdown = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n // =========================================================\n // DATE PICKER\n // =========================================================\n\n getCellDisplayValue(ci, val, ri) {\n const col = this.leafCols[ci]\n if (!col) return val == null ? '' : val\n if (typeof col.formatter === 'function') {\n const out = col.formatter(val, this._dataRaw[ri], ri, col, ci)\n return out == null ? '' : out\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, val, ri)\n if (col.type === 'date') {\n const d = parseDateValue(val)\n return d ? formatDate(d, col.format || 'YYYY-MM-DD') : (val == null ? '' : val)\n }\n return val == null ? '' : val\n },\n\n // =========================================================\n // FILTER ROW\n // =========================================================\n\n // Which control the filter cell renders for this column.\n // 'none' cells still render (empty) so column alignment stays intact.\n filterControlType(col) {\n if (!col || col.filterable === false || col.type === 'action') return 'none'\n if (col.type === 'checkbox') return 'checkbox'\n // Function-based options can't be resolved into a fixed list outside a\n // specific row, so those columns fall back to the text control.\n if ((col.type === 'select' || col.type === 'treeSelect') && Array.isArray(col.options)) return 'select'\n return 'text'\n },\n\n // Flat { value, label } list for a select-control filter cell. treeSelect\n // options are recursively flattened, each depth indented for readability.\n filterOptionsFor(col) {\n if (!col) return []\n if (col.type === 'treeSelect') {\n const raw = this.resolveOptionsArray(col, null, col._flatIndex)\n const out = []\n this._flattenTreeOptionsForFilter(raw, this.treeOptionKeys(col), 0, out)\n return out\n }\n return this.colOptions(col._flatIndex, null)\n },\n\n _flattenTreeOptionsForFilter(nodes, keys, depth, out) {\n for (const node of (nodes || [])) {\n const value = node[keys.valueKey]\n const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(value)\n out.push({ value: String(value), label: ' '.repeat(depth) + label })\n this._flattenTreeOptionsForFilter(node[keys.childrenKey], keys, depth + 1, out)\n }\n },\n\n getFilterCellStyle(ci) {\n const zone = this.isColumnPinnedLeft(ci) ? 'left' : (this.isColumnPinnedRight(ci) ? 'right' : 'scroll')\n const left = this.getZoneColLeft(ci, zone)\n const w = this.getColW(ci)\n return {\n position: 'absolute',\n left: left + 'px',\n top: (this._headerRowH * this.headerDepth) + 'px',\n width: w + 'px',\n height: this._filterRowH + 'px',\n boxSizing: 'border-box'\n }\n },\n\n setFilter(key, value) {\n if (!this.leafCols.some(c => c.key === key)) return\n const next = { ...this.internalFilters }\n if (value === '' || value === null || value === undefined) {\n delete next[key]\n } else {\n next[key] = value\n }\n this.internalFilters = next\n this.$emit('update:filters', { ...next })\n this.$emit('filter-change', { key, value, filters: { ...next } })\n },\n\n clearFilters() {\n if (!Object.keys(this.internalFilters).length) return\n this.internalFilters = {}\n // A deliberate, infrequent action (unlike typing) — recompute immediately\n // rather than waiting out the debounce, and drop any timer already pending.\n clearTimeout(this._filterRecomputeTimer)\n this._recomputeRowFilters()\n this.$emit('update:filters', {})\n this.$emit('filter-change', { key: null, value: null, filters: {} })\n },\n\n // Debounced — recomputing filteredRowIndices scans every row, so typing\n // in the text filter shouldn't trigger it on every keystroke.\n _scheduleFilterRecompute() {\n clearTimeout(this._filterRecomputeTimer)\n this._filterRecomputeTimer = setTimeout(() => { this._recomputeRowFilters() }, 150)\n },\n\n _activeFilterCols() {\n const cols = []\n for (const col of this.leafCols) {\n if (this.filterControlType(col) === 'none') continue\n const v = this.internalFilters[col.key]\n if (v === undefined || v === null || v === '') continue\n cols.push(col)\n }\n return cols\n },\n\n _cellMatchesFilter(row, ri, col) {\n const ci = col._flatIndex\n const filterValue = this.internalFilters[col.key]\n if (typeof col.filterFn === 'function') {\n return !!col.filterFn(this.getCellValue(ri, ci), filterValue, row, ri, col)\n }\n const kind = this.filterControlType(col)\n if (kind === 'checkbox') {\n const checked = this.isCheckboxChecked(ri, ci)\n return filterValue === 'true' ? checked : !checked\n }\n if (kind === 'select') {\n const raw = this.getCellValue(ri, ci)\n return String(raw == null ? '' : raw) === String(filterValue)\n }\n const display = this.getCellDisplayValue(ci, this.getCellValue(ri, ci), ri)\n return String(display == null ? '' : display).toLowerCase().indexOf(String(filterValue).toLowerCase()) !== -1\n },\n\n _recomputeRowFilters() {\n // Can be reached (via the `columns`/`filterRow` watchers) during initWatch(),\n // before created() has set up _dataRaw — getCellValue() depends on it. The\n // `data` watcher's own immediate handler sets _dataRaw first, then always\n // calls this again, so the initial mount still ends up correct.\n if (!this._dataRaw) return\n if (!this.filterRow) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const activeCols = this._activeFilterCols()\n if (!activeCols.length) {\n if (this.filteredRowIndices.length) this.filteredRowIndices = []\n return\n }\n const rows = this.data\n const out = []\n for (let ri = 0; ri < rows.length; ri++) {\n const row = rows[ri]\n let matches = true\n for (const col of activeCols) {\n if (!this._cellMatchesFilter(row, ri, col)) { matches = false; break }\n }\n if (!matches) out.push(ri)\n }\n this.filteredRowIndices = out\n },\n\n // =========================================================\n // COLUMN SORTING\n // =========================================================\n\n isColSortable(col) {\n return !!(this.sortable && col && col.sortable !== false && col.type !== 'action')\n },\n\n sortIconClass(col) {\n if (!this.internalSort || this.internalSort.key !== col.key) return 'eg-icon-sort'\n return this.internalSort.direction === 'asc' ? 'eg-icon-sort-asc' : 'eg-icon-sort-desc'\n },\n\n sortIconTitle() {\n return this.t.sortMenuTooltip\n },\n\n // Opens the sort popup menu (A → Z / Z → A / Cancel) anchored to the\n // clicked header icon. Replaces the old direct cycle-on-click behavior.\n openSortMenu(e, col) {\n if (this.internalLocked) return\n this.sortMenu = { visible: true, x: e.clientX, y: e.clientY, key: col.key }\n this.sortMenuPositioned = false\n this.$nextTick(this._positionSortMenu)\n },\n\n // Same flip-to-fit reasoning as _positionContextMenu().\n _positionSortMenu() {\n const el = this.$refs.sortMenuEl\n if (!el) return\n const rect = el.getBoundingClientRect()\n const margin = 4\n let x = this.sortMenu.x\n let y = this.sortMenu.y\n if (x + rect.width > window.innerWidth - margin) {\n x = Math.max(margin, window.innerWidth - rect.width - margin)\n }\n if (y + rect.height > window.innerHeight - margin) {\n y = Math.max(margin, window.innerHeight - rect.height - margin)\n }\n this.sortMenu.x = x\n this.sortMenu.y = y\n this.sortMenuPositioned = true\n },\n\n hideSortMenu() {\n this.sortMenu.visible = false\n },\n\n sortMenuPickAsc() {\n this.setSort(this.sortMenu.key, 'asc')\n this.hideSortMenu()\n },\n\n sortMenuPickDesc() {\n this.setSort(this.sortMenu.key, 'desc')\n this.hideSortMenu()\n },\n\n sortMenuPickCancel() {\n this.clearSort()\n this.hideSortMenu()\n },\n\n // Cycles a column through none → asc → desc → none. Kept as a public\n // method for programmatic/custom #header-{key} slot use — the built-in\n // header icon now opens the popup menu (openSortMenu) instead.\n toggleSort(key) {\n if (this.internalLocked) return\n const current = this.internalSort\n if (!current || current.key !== key) { this.setSort(key, 'asc'); return }\n if (current.direction === 'asc') { this.setSort(key, 'desc'); return }\n this.clearSort()\n },\n\n setSort(key, direction) {\n if (direction !== 'asc' && direction !== 'desc') { this.clearSort(); return }\n this.sortByColumn(key, direction)\n },\n\n // Clicking a third time (or calling this directly) only clears the sort\n // indicator — it does not restore the pre-sort row order by itself,\n // since sorting already reordered `data` in place. Use undo() for that.\n clearSort() {\n if (!this.internalSort) return\n this.internalSort = null\n this.$emit('update:sort', null)\n this.$emit('sort-change', { key: null, direction: null })\n },\n\n sortByColumn(key, direction) {\n if (this.internalLocked) return\n // Merges and row groups are anchored to row *position* — a reorder\n // would silently invalidate them, so sorting is blocked while either\n // exists (same \"blocked while locked\" no-op convention as elsewhere).\n if (this.internalMerges.length || this.rowGroups.length) return\n const col = this.leafCols.find(c => c.key === key)\n if (!this.isColSortable(col)) return\n\n const oldData = this.data\n const pairs = oldData.map((row, ri) => ({ row, ri }))\n pairs.sort((a, b) => this._compareForSort(a.row, a.ri, b.row, b.ri, col, direction))\n const newData = pairs.map(p => p.row)\n\n this.internalSort = { key, direction }\n this._emitDataChange(oldData, newData)\n this.$emit('update:sort', { key, direction })\n this.$emit('sort-change', { key, direction })\n\n this.scrollTop = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) this.$refs.scrollBody.scrollTop = 0\n })\n },\n\n _sortValueFor(row, ri, col) {\n const ci = col._flatIndex\n if (col.type === 'checkbox') return this.isCheckboxChecked(ri, ci) ? 1 : 0\n if (col.type === 'date') {\n const d = parseDateValue(this.getCellValue(ri, ci))\n return d ? d.getTime() : null\n }\n if (this.isSelectLikeCol(col)) return this.getSelectLabel(ci, this.getCellValue(ri, ci), ri)\n return this.getCellValue(ri, ci)\n },\n\n _compareForSort(rowA, riA, rowB, riB, col, direction) {\n if (typeof col.sortFn === 'function') {\n const result = col.sortFn(rowA, rowB, col)\n return direction === 'desc' ? -result : result\n }\n const a = this._sortValueFor(rowA, riA, col)\n const b = this._sortValueFor(rowB, riB, col)\n const aEmpty = a === null || a === undefined || a === ''\n const bEmpty = b === null || b === undefined || b === ''\n if (aEmpty && bEmpty) return 0\n if (aEmpty) return 1\n if (bEmpty) return -1\n const cmp = (typeof a === 'number' && typeof b === 'number')\n ? a - b\n : String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' })\n return direction === 'desc' ? -cmp : cmp\n },\n\n openDatePickerForActive() {\n const { row, col } = this.activeCell\n const colDef = this.leafCols[col]\n if (!colDef || colDef.type !== 'date') return\n this.$nextTick(() => {\n const el = this.$el ? this.$el.querySelector('.eg-cell-active') : null\n if (!el) return\n const rect = el.getBoundingClientRect()\n const val = this.getCellValue(row, col)\n this.datePickerOverlay = {\n row, col,\n format: colDef.format || 'YYYY-MM-DD',\n value: val == null ? '' : String(val),\n rect: { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width }\n }\n })\n },\n\n onDatePick(formattedVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n const colDef = this.leafCols[col]\n if (colDef) {\n if (formattedVal !== '' && !this.isCellValueValid(row, col, formattedVal)) {\n this._showInvalidCell(row, col, formattedVal)\n this.closeDatePicker()\n return\n }\n this.invalidCell = null\n const oldData = this.data\n const newData = oldData.slice()\n newData[row] = { ...oldData[row], [colDef.key]: formattedVal }\n this._emitDataChange(oldData, newData)\n this._flushOnInput(row, col, formattedVal, newData[row])\n }\n this.closeDatePicker()\n },\n\n // Raw keystrokes while typing in the date picker's manual text field —\n // debounced, same as onEditInput(). onDatePick() above flushes the final,\n // validated value immediately when the date actually commits.\n onDateTyping(rawVal) {\n if (!this.datePickerOverlay) return\n const { row, col } = this.datePickerOverlay\n this._scheduleOnInput(row, col, rawVal)\n },\n\n // See closeSelectDropdown() — same reasoning: every close path must return\n // focus to the grid wrapper or keyboard navigation goes dead afterward.\n closeDatePicker() {\n clearTimeout(this._onInputTimer)\n this.datePickerOverlay = null\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n _onWindowScrollForDropdown(e) {\n if (this.contextMenu.visible) this.hideContextMenu()\n if (this.sortMenu.visible) this.hideSortMenu()\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n _onDocMouseDownForDropdown(e) {\n if (this.selectDropdown) {\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (!(dd && dd.contains(e.target))) this.closeSelectDropdown()\n }\n if (this.datePickerOverlay) {\n const dp = this.$refs.datePickerComp\n const el = dp && dp.$el ? dp.$el : null\n if (!(el && el.contains(e.target))) this.closeDatePicker()\n }\n },\n\n onEditKeyDown(e) {\n if (e.key === 'Escape') {\n e.preventDefault()\n this.cancelEdit()\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(1, 0, e)\n } else if (e.key === 'Tab') {\n e.preventDefault()\n this.commitEdit()\n this.moveActive(0, e.shiftKey ? -1 : 1, e)\n }\n },\n\n clearSelection() {\n if (this.internalLocked) return\n if (!this.selectionRanges.length) return\n const oldData = this.data\n const newData = oldData.slice()\n let hasChanges = false\n const skipped = []\n for (const range of this.selectionRanges) {\n const r = {\n startRow: Math.min(range.startRow, range.endRow),\n endRow: Math.max(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endCol: Math.max(range.startCol, range.endCol)\n }\n for (let ri = r.startRow; ri <= r.endRow && ri < newData.length; ri++) {\n let rowUpdated = null\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, '')) { skipped.push({ row: ri, col: ci, value: '' }); continue }\n if (!rowUpdated) rowUpdated = { ...newData[ri] }\n rowUpdated[col.key] = ''\n hasChanges = true\n }\n }\n if (rowUpdated) newData[ri] = rowUpdated\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n if (hasChanges) this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // FILL HANDLE (3.3)\n // =========================================================\n startFillDrag(e) {\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n this.fillDrag.active = true\n this.fillDrag.sourceRange = normalizeRange(pr)\n this.fillDrag.currentRow = pr.endRow\n this.fillDrag.currentCol = pr.endCol\n e.preventDefault()\n },\n\n onGlobalMouseMove(e) {\n // Detect column drag threshold (5 px)\n if (this.colDrag.pending) {\n if (Math.abs(e.clientX - this.colDrag.startX) > 5) {\n this.colDrag.pending = false\n this.colDrag.active = true\n this.colDrag.dropIndex = this.colDrag.sourceFirstLeaf\n this.isMouseDown = false // cancel cell-selection drag\n }\n }\n if (this.colDrag.active) {\n this.handleColDragMove(e)\n return\n }\n\n if (this.resizing.active) {\n this.handleResizeMove(e)\n return\n }\n if (this.fillDrag.active) {\n this.handleFillDragMove(e)\n return\n }\n if (this.dragMove.active) {\n this.handleDragMoveMove(e)\n return\n }\n },\n\n onGlobalMouseUp(e) {\n if (this.colDrag.active) {\n this.commitColDrag()\n return\n }\n if (this.colDrag.pending) {\n this._resetColDrag()\n // fall through — normal mouseup\n }\n\n if (this.resizing.active) {\n this.commitResize()\n return\n }\n if (this.fillDrag.active) {\n this.commitFillDrag()\n return\n }\n if (this.dragMove.active) {\n this.commitDragMove(e)\n return\n }\n this.isMouseDown = false\n },\n\n handleFillDragMove(e) {\n const scrollBody = this.$refs.scrollBody\n if (!scrollBody) return\n const rect = scrollBody.getBoundingClientRect()\n const relY = e.clientY - rect.top + scrollBody.scrollTop\n const relX = e.clientX - rect.left + scrollBody.scrollLeft\n\n // Binary search on cumulativeHeights — O(log n) instead of O(n)\n const cum = this.cumulativeHeights\n const total = this.totalRows\n let lo = 0, hi = total - 1\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (cum[mid + 1] <= relY) lo = mid + 1\n else hi = mid\n }\n const row = Math.min(lo, total - 1)\n\n // Columns are few — O(columns) is fast\n let left = this.getFrozenWidth()\n let col = 0\n for (let i = 0; i < this.leafCols.length; i++) {\n if (!this.isColumnScrollable(i)) continue\n left += this.getColW(i)\n if (relX < left) { col = i; break }\n col = i\n }\n\n this.fillDrag.currentRow = row\n this.fillDrag.currentCol = col\n },\n\n async commitFillDrag() {\n if (this.internalLocked) { this.fillDrag.active = false; return }\n const src = this.fillDrag.sourceRange\n if (!src) { this.fillDrag.active = false; return }\n\n const targetRow = this.fillDrag.currentRow\n const targetCol = this.fillDrag.currentCol\n\n const oldData = this.data\n const newData = oldData.slice() // only affected rows get new objects\n let targetRange\n const skipped = []\n\n if (targetRow > src.endRow) {\n // Fill down\n const count = targetRow - src.endRow\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n targetRange = { startRow: src.endRow + 1, endRow: targetRow, startCol: src.startCol, endCol: src.endCol }\n } else if (targetCol > src.endCol) {\n // Fill right\n const count = targetCol - src.endCol\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n const srcValues = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n srcValues.push(col && oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ci = src.endCol + 1 + k\n if (ci < this.leafCols.length) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n }\n targetRange = { startRow: src.startRow, endRow: src.endRow, startCol: src.endCol + 1, endCol: targetCol }\n }\n\n let fireOnSelect = false\n if (targetRange) {\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n } else {\n this._emitDataChange(oldData, newData)\n this.$emit('fill', { sourceRange: src, targetRange, values: newData })\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n fireOnSelect = true\n }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\n\n if (fireOnSelect) {\n // See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n doubleFillDown() {\n const pr = this.primaryRange\n if (!pr) return\n const src = normalizeRange(pr)\n // Find adjacent column filled length\n const adjCol = src.startCol > 0 ? src.startCol - 1 : src.endCol + 1\n if (adjCol < 0 || adjCol >= this.leafCols.length) return\n const adjColDef = this.leafCols[adjCol]\n if (!adjColDef) return\n let lastFilledRow = src.endRow\n for (let ri = src.endRow + 1; ri < this.totalRows; ri++) {\n if (this.data[ri] && this.data[ri][adjColDef.key] !== '' && this.data[ri][adjColDef.key] !== undefined) {\n lastFilledRow = ri\n } else break\n }\n if (lastFilledRow <= src.endRow) return\n\n const count = lastFilledRow - src.endRow\n const oldData = this.data\n const newData = oldData.slice()\n const skipped = []\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const srcValues = []\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n srcValues.push(oldData[ri] ? oldData[ri][col.key] : '')\n }\n const filled = generateFillValues(srcValues, count)\n for (let k = 0; k < count; k++) {\n const ri = src.endRow + 1 + k\n if (ri < this.totalRows && this.isCellEditable(ri, ci)) {\n if (!this.isCellValueValid(ri, ci, filled[k])) { skipped.push({ row: ri, col: ci, value: filled[k] }); continue }\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n newData[ri][col.key] = filled[k]\n }\n }\n }\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n },\n\n // =========================================================\n // COPY / PASTE (3.4)\n // =========================================================\n copySelection() {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n this.clipboardMatrix = extractMatrix(this.data, this.leafCols, r)\n this.clipboardRange = r\n this.clipboardMode = 'copy'\n\n // Write to system clipboard as TSV\n const tsv = matrixToTSV(this.clipboardMatrix)\n if (navigator.clipboard) {\n navigator.clipboard.writeText(tsv).catch(() => {})\n }\n },\n\n cutSelection() {\n this.copySelection()\n this.clipboardMode = 'cut'\n },\n\n // Bulk cell writes (paste, fill handle, drag-move) set row values directly\n // and skip the single-cell paths (onComponentSelect / commitEdit /\n // setCheckboxValue / onDatePick) — so col.onSelect / col.onInput never\n // fire for them unless we replay it here for every cell whose value\n // actually changed in the range. select/treeSelect columns get onSelect\n // (matching the single-pick shape); everything else gets onInput —\n // fired immediately, no debounce (this is already a single bulk commit,\n // not live keystrokes).\n _fireCellCallbacksForRange(oldData, newData, startRow, endRow, startCol, endCol) {\n for (let ri = startRow; ri <= endRow; ri++) {\n const oldRow = oldData[ri]\n const newRow = newData[ri]\n if (!newRow || newRow === oldRow) continue\n for (let ci = startCol; ci <= endCol; ci++) {\n const col = this.leafCols[ci]\n if (!col) continue\n const oldVal = oldRow ? oldRow[col.key] : undefined\n const newVal = newRow[col.key]\n if (newVal === oldVal) continue\n if (this.isSelectLikeCol(col)) {\n if (typeof col.onSelect === 'function') {\n col.onSelect({ value: newVal, label: this.getSelectLabel(ci, newVal, ri) }, newRow, ri, col)\n }\n } else if (typeof col.onInput === 'function') {\n col.onInput(newVal, newRow, ri, col)\n }\n }\n }\n },\n\n async pasteAt(targetRow, targetCol, options = {}) {\n if (this.internalLocked) return\n const { mode = 'all', transpose = false } = options\n\n let matrix = null\n\n // Try system clipboard first\n if (navigator.clipboard) {\n try {\n const text = await navigator.clipboard.readText()\n if (text) matrix = tsvToMatrix(text)\n } catch (_) {}\n }\n\n // Fall back to internal clipboard\n if (!matrix && this.clipboardMatrix) {\n matrix = this.clipboardMatrix\n }\n\n if (!matrix) return\n\n if (transpose) matrix = transposeMatrix(matrix)\n\n // ── Excel-style selection broadcast / tiling ──────────────────────────\n // When pasting over a multi-cell selection and the clipboard matrix is\n // smaller than the selection, tile the matrix to fill the selection —\n // provided the selection dimensions are exact multiples of the matrix\n // dimensions (matches Excel's tiling rule exactly).\n // Cut-paste is never tiled (source must be cleared exactly once).\n const isCopyPaste = !this.clipboardMode || this.clipboardMode === 'copy'\n if (isCopyPaste) {\n const pr = this.primaryRange\n if (pr) {\n const sel = normalizeRange(pr)\n const selRows = sel.endRow - sel.startRow + 1\n const selCols = sel.endCol - sel.startCol + 1\n const matRows = matrix.length\n const matCols = matrix[0] ? matrix[0].length : 0\n\n // Only tile when selection is strictly larger than clipboard\n if ((selRows > matRows || selCols > matCols) &&\n selRows % matRows === 0 && selCols % matCols === 0) {\n matrix = tileMatrix(matrix, selRows, selCols)\n // Paste at selection origin, not just at the active cell\n targetRow = sel.startRow\n targetCol = sel.startCol\n }\n }\n }\n\n const oldData = this.data\n const { newData, affectedRange, skipped } = applyMatrix(\n oldData, this.leafCols, targetRow, targetCol, matrix, mode,\n (ri, ci) => this.isCellEditable(ri, ci),\n (ri, ci, v) => this.isCellValueValid(ri, ci, v)\n )\n\n if (this.strictBulkValidation && skipped.length > 0) {\n this._emitBulkValidationError(skipped, true)\n return\n }\n\n if (this.clipboardMode === 'cut' && this.clipboardRange) {\n // Clear source — ensure each source row is a new copy before mutation\n const src = this.clipboardRange\n for (let ri = src.startRow; ri <= src.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = src.startCol; ci <= src.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci) && this.isCellValueValid(ri, ci, '')) {\n newData[ri][col.key] = ''\n }\n }\n }\n this.clipboardRange = null\n this.clipboardMode = null\n }\n\n this._emitDataChange(oldData, newData)\n if (skipped.length > 0) this._emitBulkValidationError(skipped, false)\n // Wait for Vue's reactivity queue to flush before firing onSelect — hosts\n // commonly react to the emitted 'input' with their own watcher (e.g. to\n // mirror the row into an external store); firing onSelect synchronously\n // would run it against that store's pre-paste, stale state.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, affectedRange.startRow, affectedRange.endRow, affectedRange.startCol, affectedRange.endCol)\n },\n\n ctxCopy() { this.hideContextMenu(); this.copySelection() },\n ctxCut() { this.hideContextMenu(); this.cutSelection() },\n ctxPaste() { this.hideContextMenu(); this.pasteAt(this.activeCell.row, this.activeCell.col) },\n ctxPasteSpecial(mode, transpose = false) {\n this.hideContextMenu()\n this.pasteAt(this.activeCell.row, this.activeCell.col, { mode, transpose })\n },\n\n // =========================================================\n // COLUMN DRAG-AND-DROP\n // =========================================================\n\n _computeDragOffsets(dropIndex) {\n const { sourceFirstLeaf, sourceLeafCount } = this.colDrag\n if (dropIndex < 0 || (dropIndex >= sourceFirstLeaf && dropIndex <= sourceFirstLeaf + sourceLeafCount)) return {}\n\n const previewCols = moveColumnNodeInTree(this.columns, sourceFirstLeaf, sourceLeafCount, dropIndex)\n const previewLeaves = flattenColumns(previewCols)\n\n const byKey = {}\n this.leafCols.forEach(c => { byKey[c.key] = c })\n\n const offsets = {}\n for (const zone of ['left', 'scroll', 'right']) {\n const inZone = zone === 'left'\n ? ci => this.isColumnPinnedLeft(ci)\n : zone === 'right'\n ? ci => this.isColumnPinnedRight(ci)\n : ci => this.isColumnScrollable(ci)\n\n const origPos = {}\n let acc = 0\n this.leafCols.forEach(col => {\n const ci = col._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) return\n origPos[col.key] = acc\n acc += this.getColW(ci)\n })\n\n const previewPos = {}\n acc = 0\n for (const col of previewLeaves) {\n const origCol = byKey[col.key]\n if (!origCol) continue\n const ci = origCol._flatIndex\n if (this.isColHidden(ci) || !inZone(ci)) continue\n previewPos[col.key] = acc\n acc += this.getColW(ci)\n }\n\n for (const key of Object.keys(origPos)) {\n offsets[key] = (previewPos[key] !== undefined ? previewPos[key] : origPos[key]) - origPos[key]\n }\n }\n return offsets\n },\n\n _applyDragTransforms(offsets) {\n if (!this.$el) return\n const targets = []\n const dxMap = new Map()\n\n this.$el.querySelectorAll('[data-fi]').forEach(el => {\n const fi = parseInt(el.dataset.fi)\n const col = this.leafCols[fi]\n const dx = col ? (offsets[col.key] || 0) : 0\n targets.push(el)\n dxMap.set(el, dx)\n })\n this.$el.querySelectorAll('[data-start]').forEach(el => {\n const start = parseInt(el.dataset.start)\n const end = parseInt(el.dataset.end)\n if (isNaN(start)) return\n let key = null\n for (let i = start; i <= end; i++) {\n if (!this.isColHidden(i) && this.leafCols[i]) { key = this.leafCols[i].key; break }\n }\n targets.push(el)\n dxMap.set(el, key ? (offsets[key] || 0) : 0)\n })\n if (!targets.length) return\n\n // anime.remove cancels any in-progress animation before starting the new one.\n // This prevents chaining/overlap when dropIndex changes rapidly (no flickering).\n anime.remove(targets)\n anime({\n targets,\n translateX: el => dxMap.get(el) || 0,\n duration: 80,\n easing: 'easeOutQuart'\n })\n },\n\n _clearDragTransforms() {\n if (!this.$el) return\n const els = Array.from(this.$el.querySelectorAll('[data-fi], [data-start]'))\n // Kill all anime animations first, then instantly clear inline styles\n anime.remove(els)\n els.forEach(el => {\n el.style.transform = ''\n el.style.transition = ''\n })\n },\n\n handleColDragMove(e) {\n // Update ghost position via direct DOM — avoids Vue re-render on every mousemove\n const ghost = this.$refs.colDragGhost\n if (ghost) {\n ghost.style.left = (e.clientX + 16) + 'px'\n ghost.style.top = (e.clientY - 14) + 'px'\n }\n\n const prevDrop = this.colDrag.dropIndex\n this._updateColDragDropIndex(e.clientX)\n const newDrop = this.colDrag.dropIndex\n\n if (newDrop !== prevDrop) {\n this._dragOffsets = this._computeDragOffsets(newDrop)\n this._applyDragTransforms(this._dragOffsets)\n }\n },\n\n _updateColDragDropIndex(clientX) {\n if (!this.$el) return\n const n = this.leafCols.length\n if (n === 0) { this.colDrag.dropIndex = 0; return }\n\n // Use DOM cell rects but subtract CSS transforms so we get ORIGINAL positions.\n // This avoids circular dependency: live-preview transforms don't affect drop calc.\n const cells = Array.from(this.$el.querySelectorAll('.eg-header-cell:not(.eg-header-group)'))\n .map(el => {\n const fi = parseInt(el.dataset.fi)\n if (isNaN(fi)) return null\n const rect = el.getBoundingClientRect()\n const m = (el.style.transform || '').match(/translateX\\((-?[\\d.]+)px\\)/)\n const dx = m ? parseFloat(m[1]) : 0\n return { fi, left: rect.left - dx, right: rect.right - dx }\n })\n .filter(Boolean)\n .sort((a, b) => a.left - b.left)\n\n if (!cells.length) { this.colDrag.dropIndex = n; return }\n\n let dropIndex = n\n for (const { fi, left, right } of cells) {\n if (clientX < right) {\n dropIndex = clientX < left + (right - left) / 2 ? fi : fi + 1\n break\n }\n }\n this.colDrag.dropIndex = dropIndex\n },\n\n commitColDrag() {\n const from = this.colDrag.sourceFirstLeaf\n const leafCount = this.colDrag.sourceLeafCount\n const to = this.colDrag.dropIndex\n\n if (from < 0 || to < 0 || leafCount < 1) { this._resetColDrag(); return }\n if (to >= from && to <= from + leafCount) { this._resetColDrag(); return }\n\n // Kill anime animations but KEEP transform values in DOM while Vue re-renders\n // with the new column order. After Vue updates left positions ($nextTick),\n // we instantly clear transforms — both steps run as microtasks before browser paints,\n // so there is never a visible intermediate state.\n if (this.$el) {\n anime.remove(Array.from(this.$el.querySelectorAll('[data-fi], [data-start]')))\n }\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n\n const newCols = moveColumnNodeInTree(this.columns, from, leafCount, to)\n this.$emit('update:columns', newCols)\n this.$emit('column-reorder', { fromIndex: from, toIndex: to, columns: newCols })\n\n // Clear transforms after Vue re-renders new column left positions\n this.$nextTick(() => { this._clearDragTransforms() })\n },\n\n _resetColDrag() {\n this._dragOffsets = {}\n this.colDrag.pending = false\n this.colDrag.active = false\n this.colDrag.sourceFirstLeaf = -1\n this.colDrag.sourceLeafCount = 1\n this.colDrag.dropIndex = -1\n this.colDrag.ghostLabel = ''\n this._clearDragTransforms()\n },\n\n // =========================================================\n // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'col',\n index: ci,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getColW(ci),\n currentPos: e.clientX,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\n if (this.internalLocked) return\n const rect = this.$refs.container.getBoundingClientRect()\n this.resizing = {\n active: true,\n type: 'row',\n index: ri,\n startX: e.clientX,\n startY: e.clientY,\n startSize: this.getRowH(ri),\n currentPos: e.clientY,\n containerLeft: rect.left,\n containerTop: rect.top\n }\n e.preventDefault()\n },\n\n handleResizeMove(e) {\n if (this.resizing.type === 'col') {\n this.resizing.currentPos = e.clientX\n const newWidth = Math.max(20, this.resizing.startSize + (e.clientX - this.resizing.startX))\n this.$set(this.colWidths, this.resizing.index, newWidth)\n this._cellStyleCache = Object.create(null)\n } else {\n this.resizing.currentPos = e.clientY\n this._resizePreviewHeight = Math.max(10, this.resizing.startSize + (e.clientY - this.resizing.startY))\n // Batch $set + updateVirtualScroll to rAF — prevents O(n) cumulativeHeights rebuild\n // on every mousemove (200+ Hz). At most 60 rebuilds/sec instead of 200+.\n if (!this._resizeRAF) {\n this._resizeRAF = requestAnimationFrame(() => {\n this._resizeRAF = null\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n this.updateVirtualScroll()\n })\n }\n }\n },\n\n commitResize() {\n if (this._resizeRAF) {\n cancelAnimationFrame(this._resizeRAF)\n this._resizeRAF = null\n // Apply the last pending preview height immediately on commit\n if (this.resizing.type === 'row' && this._resizePreviewHeight !== undefined) {\n this.$set(this.rowHeights, this.resizing.index, this._resizePreviewHeight)\n }\n }\n if (this.resizing.type === 'col') {\n this._cellStyleCache = Object.create(null)\n const newSize = this.colWidths[this.resizing.index]\n this.$emit('column-resize', { index: this.resizing.index, newSize })\n } else {\n this._rowStyleCache = Object.create(null)\n this._rowHeaderStyleCache = Object.create(null)\n this._cellStyleCache = Object.create(null)\n const newSize = this.rowHeights[this.resizing.index]\n this.$emit('row-resize', { index: this.resizing.index, newSize })\n this.updateVirtualScroll()\n }\n this.resizing.active = false\n },\n\n autoFitColumn(ci) {\n const col = this.leafCols[ci]\n if (!col) return\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n const newWidth = Math.max(50, maxLen * 8 + 16)\n this.$set(this.colWidths, ci, newWidth)\n this._cellStyleCache = Object.create(null)\n this.$emit('column-resize', { index: ci, newSize: newWidth })\n },\n\n _applyAutoWidth() {\n if (!this.autoWidth || !this.leafCols.length) return\n this.leafCols.forEach((col, ci) => {\n let maxLen = (col.title || '').length\n for (let ri = 0; ri < Math.min(this.totalRows, 500); ri++) {\n const v = this.data[ri] ? String(this.data[ri][col.key] || '') : ''\n if (v.length > maxLen) maxLen = v.length\n }\n this.$set(this.colWidths, ci, Math.max(50, maxLen * 8 + 16))\n })\n this._cellStyleCache = Object.create(null)\n },\n\n ctxHideRow() {\n const targetRow = this.contextMenu.row\n // Row header: row >= 0 AND col = -1 (no column context)\n const fromRowHeader = targetRow >= 0 && this.contextMenu.col < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenRows]\n if (fromRowHeader) {\n if (!newHidden.includes(targetRow)) newHidden.push(targetRow)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n const targetCol = this.contextMenu.col\n // Column header: col >= 0 AND row = -1 (no row context)\n const fromColHeader = targetCol >= 0 && this.contextMenu.row < 0\n this.hideContextMenu()\n if (this.internalLocked) return\n const newHidden = [...this.hiddenColumns]\n if (fromColHeader) {\n if (!newHidden.includes(targetCol)) newHidden.push(targetCol)\n } else {\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\n }\n }\n this.$emit('update:hiddenColumns', newHidden)\n },\n\n ctxUnhideRows() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\n if (this.internalLocked) return\n this.$emit('update:hiddenColumns', [])\n },\n\n ctxPinLeft() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n this.hideContextMenu()\n if (!col) return\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n },\n\n isColPinned(ci) {\n const col = this.leafCols[ci]\n return !!(col && this.pinnedColumns[col.key])\n },\n\n // =========================================================\n // DEV MODE — column/row config info (context menu → modal)\n // =========================================================\n _stringifyForInfo(value) {\n const seen = new WeakSet()\n return JSON.stringify(value, (key, val) => {\n if (typeof val === 'function') return 'ƒ ' + (val.name || 'anonymous') + '()'\n if (val && typeof val === 'object') {\n if (seen.has(val)) return '[Circular]'\n seen.add(val)\n }\n return val\n }, 2)\n },\n\n ctxShowColumnInfo() {\n const ci = this.contextMenu.col\n this.hideContextMenu()\n const col = this.leafCols[ci]\n if (!col) return\n const { _flatIndex, ...rest } = col\n this.infoModal = { title: `${this.t.columnInfo}: ${col.title || col.key}`, json: this._stringifyForInfo(rest) }\n },\n\n ctxShowRowInfo() {\n const ri = this.contextMenu.row\n this.hideContextMenu()\n const row = this._dataRaw[ri]\n if (!row) return\n this.infoModal = { title: `${this.t.rowInfo} #${ri + 1}`, json: this._stringifyForInfo(row) }\n },\n\n closeInfoModal() {\n this.infoModal = null\n },\n\n // =========================================================\n // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\n if (this.internalLocked) return false\n const oldMerges = this.internalMerges.slice()\n const { merges: newMerges, conflict } = addMerge(range, this.internalMerges)\n if (conflict) {\n this.$emit('merge-error', { range, reason: 'Overlaps existing merge' })\n return false\n }\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n return true\n },\n\n unmergeCells(row, col) {\n if (this.internalLocked) return\n const oldMerges = this.internalMerges.slice()\n const newMerges = removeMergeAt(row, col, this.internalMerges)\n this.internalMerges = newMerges\n this._emitMergeChange(oldMerges, [...newMerges])\n },\n\n ctxMerge() {\n this.hideContextMenu()\n const pr = this.primaryRange\n if (!pr) return\n this.mergeCells(normalizeRange(pr))\n },\n\n ctxUnmerge() {\n this.hideContextMenu()\n this.unmergeCells(this.activeCell.row, this.activeCell.col)\n },\n\n // =========================================================\n // GROUPING (3.7)\n // =========================================================\n groupRows(startIndex, endIndex) {\n this.internalGroups = addGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n groupColumns(startIndex, endIndex) {\n this.internalGroups = addGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupRows(startIndex, endIndex) {\n this.internalGroups = removeGroup('row', startIndex, endIndex, this.internalGroups)\n },\n\n ungroupColumns(startIndex, endIndex) {\n this.internalGroups = removeGroup('column', startIndex, endIndex, this.internalGroups)\n },\n\n toggleGroup(group) {\n this.internalCollapsedGroups = toggleGroupCollapse(group, this.internalCollapsedGroups)\n this.$emit('update:collapsedGroups', [...this.internalCollapsedGroups])\n this.$emit('group-toggle', { group, collapsed: _isGroupCollapsed(group, this.internalCollapsedGroups) })\n this.updateVirtualScroll()\n },\n\n isGroupCollapsed(group) {\n return _isGroupCollapsed(group, this.internalCollapsedGroups)\n },\n\n expandAll() {\n this.internalCollapsedGroups = []\n this.$emit('update:collapsedGroups', [])\n this.updateVirtualScroll()\n },\n\n collapseAll() {\n this.internalCollapsedGroups = [...this.internalGroups]\n this.$emit('update:collapsedGroups', [...this.internalGroups])\n this.updateVirtualScroll()\n },\n\n // =========================================================\n // DRAG AND DROP (3.9)\n // =========================================================\n startDragMove(e, ri, ci) {\n const pr = this.primaryRange\n if (!pr) return\n this.dragMove = {\n active: true,\n sourceRange: normalizeRange(pr),\n startX: e.clientX,\n startY: e.clientY,\n targetRow: ri,\n targetCol: ci,\n copyMode: e.ctrlKey || e.metaKey\n }\n this.$emit('cell-drag-start', { sourceRange: this.dragMove.sourceRange })\n e.preventDefault()\n },\n\n handleDragMoveMove(e) {\n this.dragMove.copyMode = e.ctrlKey || e.metaKey\n this.$emit('cell-drag-move', {\n sourceRange: this.dragMove.sourceRange,\n targetRow: this.dragMove.targetRow,\n targetCol: this.dragMove.targetCol\n })\n },\n\n async commitDragMove(e) {\n if (!this.dragMove.active) return\n const { sourceRange, targetRow, targetCol, copyMode } = this.dragMove\n\n const targetRange = {\n startRow: targetRow,\n startCol: targetCol,\n endRow: targetRow + (sourceRange.endRow - sourceRange.startRow),\n endCol: targetCol + (sourceRange.endCol - sourceRange.startCol)\n }\n\n let proceed = true\n if (this.beforeDrop) {\n proceed = await Promise.resolve(this.beforeDrop(sourceRange, targetRange))\n }\n\n let oldData = null\n let newData = null\n if (proceed) {\n oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n newData = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci)).newData\n\n if (!copyMode) {\n // Clear source — ensure each source row is a new copy before mutation\n for (let ri = sourceRange.startRow; ri <= sourceRange.endRow; ri++) {\n if (newData[ri] === oldData[ri]) newData[ri] = { ...oldData[ri] }\n for (let ci = sourceRange.startCol; ci <= sourceRange.endCol; ci++) {\n const col = this.leafCols[ci]\n if (col && this.isCellEditable(ri, ci)) newData[ri][col.key] = ''\n }\n }\n }\n this._emitDataChange(oldData, newData)\n this.$emit('cell-drop', { sourceRange, targetRange })\n }\n\n this.dragMove = { active: false, sourceRange: null, startX: 0, startY: 0, targetRow: -1, targetCol: -1, copyMode: false }\n this.isMouseDown = false\n\n if (proceed) {\n // See pasteAt's _fireCellCallbacksForRange call — same nextTick deferral reasoning.\n await this.$nextTick()\n this._fireCellCallbacksForRange(oldData, newData, targetRange.startRow, targetRange.endRow, targetRange.startCol, targetRange.endCol)\n }\n },\n\n // =========================================================\n // EXCEL EXPORT (3.11)\n // =========================================================\n async exportToExcel(options = {}) {\n const {\n filename = 'export.xlsx',\n sheetName = 'Sheet1',\n scope = 'all',\n includeHidden = false\n } = options\n\n this.$emit('export-start')\n this.exportState = { active: true }\n\n const workerPayload = {\n data: this.data,\n columns: this.columns,\n leafCols: this.leafCols,\n merges: this.internalMerges,\n hiddenCols: [...this.effectiveHiddenColSet],\n hiddenRows: [...this.hiddenRowSet],\n range: scope === 'selection' ? this.primaryRange : null,\n filename,\n sheetName,\n scope,\n includeHidden\n }\n\n try {\n const worker = createExcelExportWorker()\n if (worker) {\n await this._exportViaWorker(worker, workerPayload)\n } else {\n // Fallback: synchronous export on main thread\n _exportToExcel({\n ...workerPayload,\n hiddenColSet: this.effectiveHiddenColSet,\n hiddenRowSet: this.hiddenRowSet\n })\n }\n this.$emit('export-success')\n } catch (err) {\n this.$emit('export-error', err)\n } finally {\n this.exportState = { active: false }\n }\n },\n\n _exportViaWorker(worker, payload) {\n return new Promise((resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, buffer, filename, message } = e.data\n if (type === 'result') {\n // Trigger browser download from the main thread (DOM required)\n const blob = new Blob([new Uint8Array(buffer)], {\n type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = filename\n document.body.appendChild(a)\n a.click()\n document.body.removeChild(a)\n URL.revokeObjectURL(url)\n finish(resolve)\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Export worker xatosi'))\n\n worker.postMessage(payload)\n })\n },\n\n // =========================================================\n // EXCEL IMPORT (3.12)\n // =========================================================\n async importFromFile(file, options = {}) {\n this.importState = { active: true, percent: 5, message: \"Fayl o'qilmoqda...\" }\n\n try {\n let result\n const worker = createExcelImportWorker()\n\n if (worker) {\n result = await this._importViaWorker(worker, file, options)\n } else {\n // Fallback: chunked async on main thread\n result = await _importFromFile(\n file,\n this.columns,\n options,\n (percent, message) => { this.importState = { active: true, percent, message } }\n )\n }\n\n const { columns, merges } = result\n // Freeze each row so Vue 2 skips deep Object.defineProperty observation.\n // Vue 2 checks Object.isExtensible() — frozen objects are skipped entirely.\n // All edit paths already use spread ({ ...row }) so frozen rows are safe.\n const data = result.data.map(row => Object.freeze(row))\n this.$emit('input', data)\n this.$emit('update:columns', columns)\n\n const hasMerges = merges && merges.length > 0\n this.internalMerges = hasMerges ? merges : []\n this.$emit('update:merges', hasMerges ? merges : [])\n this.$emit('update:collapsedGroups', [])\n this.$emit('update:hiddenRows', [])\n this.$emit('import-success', { data, columns, merges })\n\n // Reset scroll state and re-derive virtual scroll after DOM settles.\n // Without this, stale scrollTop + old rowHeights cause blank bands on scroll.\n this.rowHeights = {}\n this.scrollTop = 0\n this.scrollLeft = 0\n this.$nextTick(() => {\n if (this.$refs.scrollBody) {\n this.$refs.scrollBody.scrollTop = 0\n this.$refs.scrollBody.scrollLeft = 0\n }\n if (this.$refs.scrollColHeader) this.$refs.scrollColHeader.scrollLeft = 0\n if (this.$refs.scrollFooter) this.$refs.scrollFooter.scrollLeft = 0\n if (this.$refs.scrollRowHeaders) this.$refs.scrollRowHeaders.scrollTop = 0\n this.updateVirtualScroll()\n })\n } catch (err) {\n this.$emit('import-error', err)\n } finally {\n this.importState = { active: false, percent: 0, message: '' }\n }\n },\n\n _importViaWorker(worker, file, options) {\n return new Promise(async (resolve, reject) => {\n let done = false\n const finish = (fn, val) => {\n if (done) return\n done = true\n worker.terminate()\n fn(val)\n }\n\n worker.onmessage = (e) => {\n const { type, percent, message, data, columns, merges } = e.data\n if (type === 'progress') {\n this.importState = { active: true, percent, message }\n } else if (type === 'result') {\n finish(resolve, { data, columns, merges })\n } else if (type === 'error') {\n finish(reject, new Error(message))\n }\n }\n\n worker.onerror = (err) => finish(reject, new Error(err.message || 'Import worker xatosi'))\n\n try {\n // Transfer the ArrayBuffer zero-copy into the worker\n const buffer = await file.arrayBuffer()\n worker.postMessage(\n { buffer, existingColumns: this.columns, options },\n [buffer]\n )\n } catch (err) {\n finish(reject, err)\n }\n })\n },\n\n // =========================================================\n // UNDO / REDO (3.13)\n // =========================================================\n\n // Push one entry onto the non-reactive history stack.\n // Truncates the redo tail, caps at 100 entries.\n _pushHistory(entry) {\n if (this._historyIndex < this._history.length - 1) {\n this._history.splice(this._historyIndex + 1)\n }\n this._history.push(entry)\n if (this._history.length > 100) {\n this._history.shift()\n // historyIndex stays the same — it now points to the new last entry\n } else {\n this._historyIndex++\n }\n this.historyVersion++\n },\n\n // Emit 'input' and record a data diff entry (only changed row indices).\n // Use slice() + per-row spread in callers so unchanged rows keep the\n // same reference — then the identity check here catches only real changes.\n _emitDataChange(oldData, newData) {\n if (!this._historySkip) {\n const indices = []\n const beforeRows = []\n const afterRows = []\n const len = Math.max(oldData.length, newData.length)\n for (let i = 0; i < len; i++) {\n if (oldData[i] !== newData[i]) {\n indices.push(i)\n beforeRows.push(oldData[i])\n afterRows.push(newData[i])\n }\n }\n if (indices.length > 0) {\n this._pushHistory({\n type: 'data',\n before: { indices, rows: beforeRows },\n after: { indices, rows: afterRows }\n })\n }\n }\n this.$emit('input', newData)\n },\n\n // Emit 'update:merges' and record a merges diff entry.\n _emitMergeChange(oldMerges, newMerges) {\n if (!this._historySkip) {\n this._pushHistory({\n type: 'merges',\n before: oldMerges,\n after: newMerges\n })\n }\n this.$emit('update:merges', newMerges)\n },\n\n undo() {\n if (this.internalLocked) return\n if (this._historyIndex < 0) return\n const entry = this._history[this._historyIndex--]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.before.indices.length; i++) {\n newData[entry.before.indices[i]] = entry.before.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.before.slice()\n this.$emit('update:merges', entry.before.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n redo() {\n if (this.internalLocked) return\n if (this._historyIndex >= this._history.length - 1) return\n const entry = this._history[++this._historyIndex]\n this._historySkip = true\n try {\n if (entry.type === 'data') {\n const newData = this.data.slice()\n for (let i = 0; i < entry.after.indices.length; i++) {\n newData[entry.after.indices[i]] = entry.after.rows[i]\n }\n this.$emit('input', newData)\n } else if (entry.type === 'merges') {\n this.internalMerges = entry.after.slice()\n this.$emit('update:merges', entry.after.slice())\n }\n } finally {\n this._historySkip = false\n this.historyVersion++\n }\n },\n\n // =========================================================\n // TOOLBAR (fullscreen / lock / export / import / custom buttons)\n // =========================================================\n\n // Public, ref-callable: returns keyboard focus to the grid. Meant for\n // hosts whose action-cell `handler` opens a modal/popup outside the\n // grid's control — call this once that modal closes, otherwise the\n // browser leaves focus on document.body (its subtree was removed) and\n // arrow-key navigation goes dead until a cell is clicked again.\n focus() {\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n toggleLock() {\n this.internalLocked = !this.internalLocked\n this.$emit('update:locked', this.internalLocked)\n this.$emit('lock-change', { locked: this.internalLocked })\n },\n\n // Pops a big lock/unlock icon in the center of the grid and fades it\n // out — pure visual feedback, mirrors the video-player OSD pattern.\n // Triggered by the internalLocked watcher, so it covers every path that\n // can change the lock state (toolbar click, toggleLock(), :locked.sync).\n _flashLock(locked) {\n this.lockFlashLocked = locked\n this.lockFlashVisible = true\n this.$nextTick(() => {\n const el = this.$refs.lockFlashIconEl\n if (!el) return\n anime.remove(el)\n anime.set(el, { opacity: 0, scale: 0.5 })\n anime.timeline()\n .add({ targets: el, opacity: 1, scale: 1.12, duration: 180, easing: 'easeOutBack' })\n .add({ targets: el, scale: 1, duration: 120, easing: 'easeOutQuad' })\n .add({ targets: el, opacity: 0, duration: 350, easing: 'easeInQuad' }, '+=250')\n .finished.then(() => { this.lockFlashVisible = false })\n })\n },\n\n toolbarHas(key) {\n return this._toolbarList.indexOf(key) !== -1\n },\n\n setSize(newSize) {\n if (!SIZE_CONFIG[newSize] || newSize === this.internalSize) return\n this.internalSize = newSize\n this.$emit('update:size', newSize)\n this.$emit('size-change', { size: newSize })\n },\n\n setTheme(newTheme) {\n if (THEME_LIST.indexOf(newTheme) === -1 || newTheme === this.internalTheme) return\n this.internalTheme = newTheme\n this.$emit('update:theme', newTheme)\n this.$emit('theme-change', { theme: newTheme })\n },\n\n sizeTitle(key) {\n return { small: this.t.sizeSmall, middle: this.t.sizeMiddle, large: this.t.sizeLarge }[key] || key\n },\n\n toggleFullscreen() {\n this.isFullscreen = !this.isFullscreen\n if (this.isFullscreen) {\n this._prevBodyOverflow = document.body.style.overflow\n document.body.style.overflow = 'hidden'\n document.addEventListener('keydown', this._onFullscreenKeyDown)\n } else {\n document.body.style.overflow = this._prevBodyOverflow || ''\n document.removeEventListener('keydown', this._onFullscreenKeyDown)\n }\n this.$emit('fullscreen-change', { fullscreen: this.isFullscreen })\n this.$nextTick(() => this.updateVirtualScroll())\n },\n\n _onFullscreenKeyDown(e) {\n if (e.key === 'Escape' && this.isFullscreen) this.toggleFullscreen()\n },\n\n toolbarExportClick() {\n this.exportToExcel()\n },\n\n toolbarImportClick() {\n if (this.internalLocked) return\n const input = this.$refs.toolbarImportInput\n if (input) input.click()\n },\n\n onToolbarImportFile(e) {\n const file = e.target.files && e.target.files[0]\n e.target.value = ''\n if (!file || this.internalLocked) return\n this.importFromFile(file)\n },\n\n onToolbarButtonClick(btn, event) {\n if (typeof btn.onClick === 'function') btn.onClick(event, this)\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-shell {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\n /* Tokens are defined on this element and cascade to all children */\n\n /* --- Typography --- */\n --eg-font-family: 'Calibri', 'Segoe UI', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 11px;\n\n /* --- Shape --- */\n --eg-radius: 0px;\n --eg-cell-padding-x: 3px;\n\n /* --- Accent (Excel green) --- */\n --eg-color-accent: #217346;\n --eg-color-accent-dark: #185a30;\n --eg-color-accent-soft: rgba(33, 115, 70, 0.15);\n\n /* --- Error / Validation --- */\n --eg-color-error: #c00000;\n --eg-color-error-soft: #fff2f2;\n --eg-color-error-dark: #9b0000;\n\n /* --- Borders --- */\n --eg-color-border: #d9d9d9;\n --eg-color-border-cell: #d9d9d9;\n --eg-color-border-strong: #bfbfbf;\n\n /* --- Header --- */\n --eg-color-header-bg: #f2f2f2;\n --eg-color-header-group-bg: #f2f2f2;\n --eg-color-group-border: #92d050;\n --eg-color-group-bg-collapsed: #70ad47;\n\n /* --- Cell states --- */\n --eg-color-cell-bg: #ffffff;\n --eg-color-cell-selected: #bdd7ee;\n --eg-color-cell-fill-preview: #9bc2e6;\n --eg-color-cell-readonly: #f2f2f2;\n --eg-color-cell-readonly-selected: #a9c9e4;\n --eg-color-footer-bg: #f2f2f2;\n\n /* --- Text --- */\n --eg-color-text: #000000;\n --eg-color-text-secondary: #595959;\n --eg-color-text-muted: #767676;\n\n /* --- Shadows --- */\n --eg-shadow-dropdown: 1px 2px 8px rgba(0, 0, 0, 0.18);\n --eg-shadow-dialog: 0 2px 12px rgba(0, 0, 0, 0.22);\n --eg-shadow-tooltip: 0 1px 4px rgba(0, 0, 0, 0.18);\n --eg-color-freeze-shadow: rgba(0, 0, 0, 0.12);\n\n /* --- Overlay & dropdown options --- */\n --eg-color-overlay-bg: rgba(255, 255, 255, 0.88);\n --eg-color-option-hover-bg: #e2efda;\n --eg-color-option-active-hover-bg: #c6e0b4;\n\n /* --- Checkbox --- */\n --eg-color-checkbox-border: #767676;\n --eg-color-checkbox-disabled-checked: #bfbfbf;\n\n /* --- Row effects (off by default) --- */\n --eg-color-row-hover: transparent;\n --eg-color-row-stripe: transparent;\n\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n overflow: hidden;\n}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n display: flex;\n flex-direction: column;\n border: none;\n border-radius: 0;\n overflow: hidden;\n}\n\n/* ── Material Design 3 — ko'k-ranglantirilgan header, 8px radius ── */\n.eg-shell.eg-theme-material {\n --eg-font-family: 'Roboto', 'Google Sans', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 8px;\n --eg-cell-padding-x: 6px;\n\n --eg-color-accent: #1565C0;\n --eg-color-accent-dark: #0D47A1;\n --eg-color-accent-soft: #DBEAFE;\n\n --eg-color-error: #D32F2F;\n --eg-color-error-soft: #FFEBEE;\n --eg-color-error-dark: #B71C1C;\n\n /* Ko'k chegaralar — Excel'dan aniq farqli */\n --eg-color-border: #90CAF9;\n --eg-color-border-cell: #BBDEFB;\n --eg-color-border-strong: #64B5F6;\n\n /* Ko'k-ranglantirilgan header — eng ko'zga ko'rinuvchi farq */\n --eg-color-header-bg: #DBEAFE;\n --eg-color-header-group-bg: #BFDBFE;\n --eg-color-group-border: #1565C0;\n --eg-color-group-bg-collapsed:#0D47A1;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #BFDBFE;\n --eg-color-cell-fill-preview: #93C5FD;\n --eg-color-cell-readonly: #EFF6FF;\n --eg-color-cell-readonly-selected: #DBEAFE;\n --eg-color-footer-bg: #EFF6FF;\n\n --eg-color-text: #1E3A5F;\n --eg-color-text-secondary: #4B7BA8;\n --eg-color-text-muted: #93C5FD;\n\n --eg-shadow-dropdown: 0 4px 12px rgba(21,101,192,0.20), 0 1px 4px rgba(21,101,192,0.12);\n --eg-shadow-dialog: 0 6px 24px rgba(21,101,192,0.18);\n --eg-shadow-tooltip: 0 2px 6px rgba(21,101,192,0.20);\n --eg-color-freeze-shadow: rgba(21,101,192,0.20);\n\n --eg-color-overlay-bg: rgba(239,246,255,0.94);\n --eg-color-option-hover-bg: #EFF6FF;\n --eg-color-option-active-hover-bg: #DBEAFE;\n\n --eg-color-checkbox-border: #64B5F6;\n --eg-color-checkbox-disabled-checked: #BBDEFB;\n\n /* Ko'k hover va stripe */\n --eg-color-row-hover: #EFF6FF;\n --eg-color-row-stripe: #F0F7FF;\n}\n\n/* ── Ant Design 5 — minimal, toza, 6px radius ─────────────────── */\n.eg-shell.eg-theme-antd {\n --eg-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 12px;\n --eg-radius: 6px;\n --eg-cell-padding-x: 8px;\n\n --eg-color-accent: #1677FF;\n --eg-color-accent-dark: #0958D9;\n --eg-color-accent-soft: #E6F4FF;\n\n --eg-color-error: #FF4D4F;\n --eg-color-error-soft: #FFF2F0;\n --eg-color-error-dark: #CF1322;\n\n /* Ant: ingichka kulrang chegaralar */\n --eg-color-border: #D9D9D9;\n --eg-color-border-cell: #F0F0F0;\n --eg-color-border-strong: #BFBFBF;\n\n /* Ant header: mo'tadil ko'k-kulrang, zamonaviy jadval uslubi */\n --eg-color-header-bg: #F0F5FF;\n --eg-color-header-group-bg: #E6EEFF;\n --eg-color-group-border: #1677FF;\n --eg-color-group-bg-collapsed:#0958D9;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #E6F4FF;\n --eg-color-cell-fill-preview: #BAE0FF;\n --eg-color-cell-readonly: #FAFAFA;\n --eg-color-cell-readonly-selected: #D6EEFF;\n --eg-color-footer-bg: #F0F5FF;\n\n --eg-color-text: rgba(0,0,0,0.88);\n --eg-color-text-secondary: rgba(0,0,0,0.45);\n --eg-color-text-muted: rgba(0,0,0,0.25);\n\n --eg-shadow-dropdown: 0 6px 16px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.12), 0 9px 28px 8px rgba(0,0,0,0.05);\n --eg-shadow-dialog: 0 6px 16px rgba(0,0,0,0.08);\n --eg-shadow-tooltip: 0 2px 8px rgba(0,0,0,0.15);\n --eg-color-freeze-shadow: rgba(0,0,0,0.10);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #E6F4FF;\n --eg-color-option-active-hover-bg: #BAE0FF;\n\n --eg-color-checkbox-border: #D9D9D9;\n --eg-color-checkbox-disabled-checked: #BFBFBF;\n\n /* Ant hover: ko'zga ko'rinuvchi ko'k tint */\n --eg-color-row-hover: #E6F4FF;\n --eg-color-row-stripe: #FAFBFF;\n}\n\n/* ── Fluent UI / Office 365 — iliq kulrang, 2px radius ─────────── */\n.eg-shell.eg-theme-fluent {\n --eg-font-family: 'Segoe UI', Calibri, Arial, sans-serif;\n --eg-font-size: 13px;\n --eg-font-size-header: 11px;\n --eg-radius: 2px;\n --eg-cell-padding-x: 5px;\n\n --eg-color-accent: #0078D4;\n --eg-color-accent-dark: #005EA6;\n --eg-color-accent-soft: #EBF3FB;\n\n --eg-color-error: #C50F1F;\n --eg-color-error-soft: #FDE7E9;\n --eg-color-error-dark: #A80000;\n\n /* Fluent: iliq kulrang chegaralar */\n --eg-color-border: #D2D0CE;\n --eg-color-border-cell: #EDEBE9;\n --eg-color-border-strong: #B3B0AD;\n\n /* Fluent header: iliq kulrang (Office 365 uslubi) */\n --eg-color-header-bg: #E9E7E4;\n --eg-color-header-group-bg: #DDD9D4;\n --eg-color-group-border: #0078D4;\n --eg-color-group-bg-collapsed:#005EA6;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #DEECF9;\n --eg-color-cell-fill-preview: #C7E0F4;\n --eg-color-cell-readonly: #F3F2F1;\n --eg-color-cell-readonly-selected: #D0E8F8;\n --eg-color-footer-bg: #E9E7E4;\n\n --eg-color-text: #323130;\n --eg-color-text-secondary: #605E5C;\n --eg-color-text-muted: #A19F9D;\n\n --eg-shadow-dropdown: 0 3.2px 7.2px rgba(0,0,0,0.132), 0 0.6px 1.8px rgba(0,0,0,0.108);\n --eg-shadow-dialog: 0 8px 16px rgba(0,0,0,0.14);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.12);\n --eg-color-freeze-shadow: rgba(0,0,0,0.14);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.92);\n --eg-color-option-hover-bg: #F3F2F1;\n --eg-color-option-active-hover-bg: #DEECF9;\n\n --eg-color-checkbox-border: #8A8886;\n --eg-color-checkbox-disabled-checked: #C8C6C4;\n\n /* Fluent: iliq hover */\n --eg-color-row-hover: #F3F2F1;\n --eg-color-row-stripe: #FAF9F8;\n}\n\n/* ── IBM Carbon — to'q kulrang header, zebra stripe, 0px radius ── */\n.eg-shell.eg-theme-carbon {\n --eg-font-family: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;\n --eg-font-size: 14px;\n --eg-font-size-header: 12px;\n --eg-radius: 0px;\n --eg-cell-padding-x: 16px;\n\n --eg-color-accent: #0F62FE;\n --eg-color-accent-dark: #0043CE;\n --eg-color-accent-soft: #D0E2FF;\n\n --eg-color-error: #DA1E28;\n --eg-color-error-soft: #FFF1F1;\n --eg-color-error-dark: #A2191F;\n\n /* Carbon: og'ir kulrang chegaralar (Gray-20 / Gray-50) */\n --eg-color-border: #C6C6C6;\n --eg-color-border-cell: #E0E0E0;\n --eg-color-border-strong: #8D8D8D;\n\n /* Carbon: to'q kulrang header — Excel'dan eng aniq farq */\n --eg-color-header-bg: #C1C7CD;\n --eg-color-header-group-bg: #A2A9B0;\n --eg-color-group-border: #0F62FE;\n --eg-color-group-bg-collapsed:#0043CE;\n\n --eg-color-cell-bg: #FFFFFF;\n --eg-color-cell-selected: #D0E2FF;\n --eg-color-cell-fill-preview: #A6C8FF;\n --eg-color-cell-readonly: #F4F4F4;\n --eg-color-cell-readonly-selected: #BDD6FF;\n --eg-color-footer-bg: #C1C7CD;\n\n --eg-color-text: #161616;\n --eg-color-text-secondary: #525252;\n --eg-color-text-muted: #8D8D8D;\n\n --eg-shadow-dropdown: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-dialog: 0 2px 6px rgba(0,0,0,0.30);\n --eg-shadow-tooltip: 0 2px 6px rgba(0,0,0,0.30);\n --eg-color-freeze-shadow: rgba(0,0,0,0.25);\n\n --eg-color-overlay-bg: rgba(255,255,255,0.88);\n --eg-color-option-hover-bg: #E8E8E8;\n --eg-color-option-active-hover-bg: #D0E2FF;\n\n --eg-color-checkbox-border: #8D8D8D;\n --eg-color-checkbox-disabled-checked: #C6C6C6;\n\n /* Carbon: ko'zga ko'rinuvchi zebra stripe */\n --eg-color-row-hover: #E8E8E8;\n --eg-color-row-stripe: #F4F4F4;\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n font-family: var(--eg-font-family);\n}\n\n.eg-toolbar {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 36px;\n padding: 0 8px;\n box-sizing: border-box;\n background: var(--eg-color-cell-bg);\n border-bottom: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n\n.eg-toolbar-group + .eg-toolbar-group {\n border-left: 1px solid var(--eg-color-border);\n}\n\n.eg-toolbar-spacer {\n flex: 1 1 auto;\n}\n\n.eg-toolbar-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 3px;\n background: transparent;\n cursor: pointer;\n color: var(--eg-color-text-secondary);\n}\n\n.eg-toolbar-btn:hover:not(:disabled) {\n background: var(--eg-color-option-hover-bg);\n border-color: var(--eg-color-border);\n}\n\n.eg-toolbar-btn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.eg-toolbar-btn-active {\n background: var(--eg-color-accent-soft);\n border-color: var(--eg-color-accent);\n color: var(--eg-color-accent-dark);\n}\n\n.eg-toolbar-select {\n height: 26px;\n padding: 0 22px 0 8px;\n border: 1px solid var(--eg-color-border);\n border-radius: 3px;\n background-color: var(--eg-color-cell-bg);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 8px 5px;\n color: var(--eg-color-text-secondary);\n font-size: 12px;\n font-family: var(--eg-font-family);\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n outline: none;\n}\n\n.eg-toolbar-select:hover {\n border-color: var(--eg-color-accent);\n}\n\n.eg-toolbar-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n\n.eg-toolbar-segmented {\n display: flex;\n align-items: center;\n border: 1px solid var(--eg-color-border);\n border-radius: 4px;\n overflow: hidden;\n padding: 1px;\n gap: 1px;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-toolbar-segment {\n border-radius: 2px;\n}\n\n/* Higher specificity than .eg-size-{x} .eg-toolbar-icon so each segment keeps its\n own progressively-sized icon regardless of the grid's current size variant. */\n.eg-toolbar-segmented .eg-toolbar-segment-small .eg-toolbar-icon { width: 12px; height: 12px; }\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon { width: 15px; height: 15px; }\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon { width: 18px; height: 18px; }\n\n.eg-toolbar-icon {\n display: inline-block;\n width: 15px;\n height: 15px;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n}\n\n.eg-icon-undo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 15 4 10 9 5'/%3E%3Cpath d='M4 10h11a5 5 0 0 1 5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-redo { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 15 20 10 15 5'/%3E%3Cpath d='M20 10H9a5 5 0 0 0-5 5'/%3E%3C/svg%3E\"); }\n.eg-icon-export { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 10v8m-3-3 3 3 3-3'/%3E%3C/svg%3E\"); }\n.eg-icon-import { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 3h10l6 6v12H4z'/%3E%3Cpath d='M14 3v6h6'/%3E%3Cpath d='M12 18v-8m-3 3 3-3 3 3'/%3E%3C/svg%3E\"); }\n.eg-icon-lock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E\"); }\n.eg-icon-unlock { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='11' width='16' height='9' rx='1.5'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 9.5-2'/%3E%3C/svg%3E\"); }\n.eg-icon-expand { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 3H3v6'/%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M9 21H3v-6'/%3E%3Cpath d='M15 21h6v-6'/%3E%3C/svg%3E\"); }\n.eg-icon-compress{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h6V3'/%3E%3Cpath d='M20 9h-6V3'/%3E%3Cpath d='M4 15h6v6'/%3E%3Cpath d='M20 15h-6v6'/%3E%3C/svg%3E\"); }\n\n/* Sort icon (header) — neutral shows both arrows faint on hover; asc/desc show one arrow, accent-colored, always visible */\n.eg-icon-sort { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3l4.5 5h-9z'/%3E%3Cpath d='M12 21l-4.5-5h9z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-asc { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5l6 7H6z'/%3E%3C/svg%3E\"); }\n.eg-icon-sort-desc{ mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19l-6-7h12z'/%3E%3C/svg%3E\"); }\n\n/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='6' width='16' height='2' rx='1'/%3E%3Crect x='4' y='11' width='16' height='2' rx='1'/%3E%3Crect x='4' y='16' width='16' height='2' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-middle { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='4' width='16' height='3' rx='1'/%3E%3Crect x='4' y='10.5' width='16' height='3' rx='1'/%3E%3Crect x='4' y='17' width='16' height='3' rx='1'/%3E%3C/svg%3E\"); }\n.eg-icon-size-large { mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); -webkit-mask-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='4' y='2' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='10' width='16' height='4' rx='1.5'/%3E%3Crect x='4' y='18' width='16' height='4' rx='1.5'/%3E%3C/svg%3E\"); }\n\n.eg-container {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n\n.eg-header-area {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 var(--eg-color-border-cell);\n position: relative;\n z-index: 31;\n}\n\n.eg-header-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n position: absolute;\n user-select: none;\n cursor: pointer;\n}\n\n.eg-header-cell:hover { background: var(--eg-color-border-cell); }\n.eg-header-group { background: var(--eg-color-header-group-bg); }\n\n.eg-sort-icon {\n display: inline-block;\n width: 11px;\n height: 11px;\n margin-left: 4px;\n vertical-align: middle;\n flex-shrink: 0;\n background-color: currentColor;\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n cursor: pointer;\n opacity: 0.3;\n}\n.eg-header-cell:hover .eg-sort-icon { opacity: 0.65; }\n.eg-sort-icon:hover { opacity: 1; }\n.eg-sort-icon.eg-icon-sort-asc,\n.eg-sort-icon.eg-icon-sort-desc {\n opacity: 1;\n color: var(--eg-color-accent);\n}\n\n/* ===== FILTER ROW ===== */\n.eg-filter-cell {\n box-sizing: border-box;\n padding: 3px 4px;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border);\n background: var(--eg-color-header-bg);\n position: absolute;\n display: flex;\n align-items: center;\n}\n.eg-filter-cell-empty { background: var(--eg-color-header-bg); }\n\n.eg-filter-input,\n.eg-filter-select {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n color: var(--eg-color-text);\n font-size: var(--eg-font-size-header);\n font-family: var(--eg-font-family);\n font-weight: 400;\n padding: 0 6px;\n outline: none;\n}\n.eg-filter-input:focus,\n.eg-filter-select:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-filter-select {\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n padding-right: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 8px 5px;\n}\n\n.eg-col-resize-handle {\n position: absolute;\n right: 0;\n top: 0;\n bottom: 0;\n width: 5px;\n cursor: col-resize;\n z-index: 5;\n}\n\n.eg-body-area {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area {\n border-top: 1px solid var(--eg-color-border);\n}\n\n.eg-frozen-col-footer,\n.eg-scroll-col-footer,\n.eg-pinned-right-footer {\n position: relative;\n}\n\n.eg-footer-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n font-weight: 600;\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-footer-bg);\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n/* Cells that actually show a computed total (col.showTotal / totalConfig.visible)\n stand out from the rest of the (otherwise mostly-empty) footer row. */\n.eg-footer-cell-total {\n font-weight: 700;\n}\n\n.eg-row {\n position: relative;\n}\n\n.eg-cell {\n box-sizing: border-box;\n border-right: 1px solid var(--eg-color-border-cell);\n border-bottom: 1px solid var(--eg-color-border-cell);\n overflow: hidden;\n white-space: nowrap;\n font-size: var(--eg-font-size);\n padding: 0 var(--eg-cell-padding-x);\n position: absolute;\n top: 0;\n background: var(--eg-color-cell-bg);\n cursor: default;\n display: flex;\n align-items: center;\n}\n\n.eg-locked .eg-cell { cursor: not-allowed; }\n\n.eg-cell-value {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell-align-right { justify-content: flex-end; }\n.eg-cell-align-center { justify-content: center; }\n.eg-cell--has-suffix.eg-cell-select::after { display: none; }\n.eg-cell-select-trigger-wrap {\n flex: 1;\n display: flex;\n align-items: center;\n overflow: hidden;\n min-width: 0;\n}\n.eg-cell-select-caret-inline {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: var(--eg-color-cell-selected) !important;\n}\n\n.eg-cell-fill-preview {\n background: var(--eg-color-cell-fill-preview) !important;\n outline: 1px dashed var(--eg-color-accent-dark);\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n\n.eg-cell-merged-hidden {\n display: none !important;\n}\n\n.eg-cell-invalid {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n\n.eg-cell-input {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: var(--eg-color-cell-bg);\n font-size: var(--eg-font-size);\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border-bottom: 1px solid var(--eg-color-border-cell);\n border-right: 2px solid var(--eg-color-border-strong);\n text-align: center;\n font-size: 11px;\n color: var(--eg-color-text-secondary);\n cursor: pointer;\n user-select: none;\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-row-resize-handle {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 4px;\n cursor: row-resize;\n z-index: 5;\n}\n\n.eg-fill-handle {\n position: absolute;\n width: 6px;\n height: 6px;\n background: var(--eg-color-accent);\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n color: var(--eg-color-text);\n}\n\n/* Only 3 short items (\"A → Z\" / \"Z → A\" / \"Clear sort\") — the generic\n 160px context-menu min-width looks oversized next to it, especially\n at size=\"small\" where rows/text are much smaller. */\n.eg-sort-menu {\n min-width: 110px;\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n\n.eg-context-item:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n\n.eg-context-separator {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n\n.eg-context-submenu-parent::after { content: ' ▶'; float: right; }\n\n.eg-context-submenu {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: var(--eg-color-menu-bg, var(--eg-color-cell-bg));\n border: 1px solid var(--eg-color-border);\n border-top: 3px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n color: var(--eg-color-text);\n}\n\n.eg-context-submenu-parent:hover .eg-context-submenu { display: block; }\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop {\n position: fixed;\n inset: 0;\n z-index: 10001;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eg-info-modal {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n width: 90%;\n max-width: 640px;\n max-height: 80vh;\n display: flex;\n flex-direction: column;\n font-family: var(--eg-font-family);\n}\n\n.eg-info-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--eg-color-border);\n flex-shrink: 0;\n}\n\n.eg-info-modal-title {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-text);\n}\n\n.eg-info-modal-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 18px;\n line-height: 1;\n color: var(--eg-color-text-secondary);\n padding: 2px 6px;\n}\n\n.eg-info-modal-close:hover { color: var(--eg-color-text); }\n\n.eg-info-modal-body {\n margin: 0;\n padding: 14px;\n overflow: auto;\n flex: 1;\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n color: var(--eg-color-text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.eg-group-btn {\n position: absolute;\n width: 16px;\n height: 16px;\n line-height: 14px;\n text-align: center;\n font-size: 10px;\n cursor: pointer;\n background: var(--eg-color-header-group-bg);\n border: 1px solid var(--eg-color-group-border);\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: var(--eg-color-group-bg-collapsed); }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n\n/* Column drag-and-drop */\n.eg-header-cell--drag-source {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source {\n opacity: 0.4;\n}\n.eg-col-drag-ghost {\n position: fixed;\n padding: 3px 10px;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-accent);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n font-size: var(--eg-font-size-header);\n font-weight: 600;\n color: var(--eg-color-text);\n white-space: nowrap;\n pointer-events: none;\n z-index: 9999;\n opacity: 0.92;\n transform: translateY(-2px);\n}\n\n.eg-resize-line {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: var(--eg-color-cell-bg);\n flex-shrink: 0;\n}\n\n.eg-pinned-right-body::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, var(--eg-color-freeze-shadow), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid var(--eg-color-border-strong);\n background: var(--eg-color-header-bg);\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: var(--eg-color-overlay-bg);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: var(--eg-radius);\n}\n\n.eg-lock-flash {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n z-index: 60;\n}\n.eg-lock-flash-icon {\n display: inline-block;\n width: 96px;\n height: 96px;\n background-color: var(--eg-color-text);\n mask-size: contain;\n mask-repeat: no-repeat;\n mask-position: center;\n -webkit-mask-size: contain;\n -webkit-mask-repeat: no-repeat;\n -webkit-mask-position: center;\n filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.28));\n}\n\n.eg-import-dialog {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: var(--eg-shadow-dialog);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: var(--eg-color-text);\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, var(--eg-color-accent), #4caf50);\n border-radius: var(--eg-radius);\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid var(--eg-color-border-cell);\n border-top-color: var(--eg-color-accent);\n border-radius: 50%;\n animation: eg-spin 0.8s linear infinite;\n}\n@keyframes eg-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select {\n cursor: pointer;\n}\n.eg-cell-select::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--eg-color-text-muted);\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap {\n display: flex;\n align-items: center;\n height: 100%;\n width: 100%;\n cursor: pointer;\n}\n/* The wrap fills the cell (100% width) so the whole cell area stays clickable —\n .eg-cell's own justify-content (from col.align) has no leftover space to act\n on, so col.align is re-applied here to position the slot content itself. */\n.eg-cell-align-right .eg-cell-action-wrap { justify-content: flex-end; }\n.eg-cell-align-center .eg-cell-action-wrap { justify-content: center; }\n.eg-validation-tooltip {\n background: var(--eg-color-error-soft);\n border: 1px solid var(--eg-color-error);\n color: var(--eg-color-error-dark);\n font-size: 12px;\n padding: 4px 8px;\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-tooltip);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW STRIPE & HOVER ===== */\n.eg-row--even .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-stripe);\n}\n.eg-row:hover .eg-cell:not(.eg-cell-selected):not(.eg-cell-active):not(.eg-cell-fill-preview) {\n background: var(--eg-color-row-hover);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu,\n.eg-size-small .eg-context-item {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item {\n padding: 4px 14px;\n}\n.eg-size-small .eg-sort-menu {\n min-width: 90px;\n}\n.eg-size-small .eg-toolbar {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon {\n width: 13px;\n height: 13px;\n}\n.eg-size-small .eg-lock-flash-icon {\n width: 72px;\n height: 72px;\n}\n.eg-size-small .eg-filter-input,\n.eg-size-small .eg-filter-select {\n font-size: 11px;\n}\n.eg-size-small .eg-sort-icon {\n width: 9px;\n height: 9px;\n}\n\n/* large */\n.eg-size-large .eg-cell {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu,\n.eg-size-large .eg-context-item {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item {\n padding: 8px 18px;\n}\n.eg-size-large .eg-sort-menu {\n min-width: 130px;\n}\n.eg-size-large .eg-toolbar {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon {\n width: 18px;\n height: 18px;\n}\n.eg-size-large .eg-lock-flash-icon {\n width: 120px;\n height: 120px;\n}\n.eg-size-large .eg-filter-input,\n.eg-size-large .eg-filter-select {\n font-size: 13px;\n}\n.eg-size-large .eg-sort-icon {\n width: 13px;\n height: 13px;\n}\n</style>\n\n<style>\n/* ── EgSelect / EgTreeSelect dropdown (non-scoped: injected by ExcelGrid on mount) ── */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: var(--eg-font-size);\n font-family: inherit;\n}\n.eg-select-trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 30px;\n padding: 4px 8px;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n background: var(--eg-color-cell-bg);\n cursor: pointer;\n user-select: none;\n color: var(--eg-color-text);\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: var(--eg-color-accent); }\n.eg-select-trigger--open {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-trigger--disabled { background: var(--eg-color-cell-readonly); cursor: default; color: var(--eg-color-text-muted); }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: var(--eg-color-text-muted); }\n.eg-select-caret { flex-shrink: 0; color: var(--eg-color-text-muted); font-size: 12px; }\n.eg-select-dropdown {\n background: var(--eg-color-cell-bg);\n border: 1px solid var(--eg-color-border-cell);\n border-radius: var(--eg-radius);\n box-shadow: var(--eg-shadow-dropdown);\n overflow-y: auto;\n max-height: 320px;\n padding: 4px 0;\n}\n.eg-select-dropdown--floating {\n position: absolute;\n top: 100%;\n left: 0;\n min-width: 100%;\n z-index: 999;\n margin-top: 2px;\n}\n.eg-select-search {\n padding: 6px 8px;\n border-bottom: 1px solid var(--eg-color-border-cell);\n position: sticky;\n top: 0;\n background: var(--eg-color-cell-bg);\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n padding: 4px 8px;\n font-size: var(--eg-font-size);\n outline: none;\n color: var(--eg-color-text);\n background: var(--eg-color-cell-bg);\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: var(--eg-color-accent);\n box-shadow: 0 0 0 2px var(--eg-color-accent-soft);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: var(--eg-font-size);\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: var(--eg-color-text);\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: var(--eg-color-option-hover-bg); }\n.eg-select-option--active { font-weight: 600; color: var(--eg-color-accent); }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: var(--eg-color-option-active-hover-bg); }\n/* Grid `size` overrides — must live in this *unscoped* style block, not the\n scoped one above: .eg-select-option is rendered by the separate EgSelect /\n EgTreeSelect components, so Vue's scoped-CSS attribute (added only to this\n SFC's own template elements) never lands on it — a scoped rule targeting\n it can never match. */\n.eg-size-small .eg-select-option {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-large .eg-select-option {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n margin-top: 3px;\n}\n/* Tree toggle */\n.eg-tree-toggle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n flex-shrink: 0;\n font-size: 10px;\n color: var(--eg-color-text-muted);\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf { visibility: hidden; }\n/* EgCheckbox */\n.eg-checkbox {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n cursor: pointer;\n}\n.eg-checkbox-input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n opacity: 0;\n cursor: pointer;\n}\n.eg-checkbox-box {\n position: relative;\n display: block;\n width: 19px;\n height: 19px;\n box-sizing: border-box;\n background-color: var(--eg-color-cell-bg);\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid var(--eg-color-checkbox-border);\n border-radius: var(--eg-radius);\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: var(--eg-color-accent); }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: var(--eg-color-accent);\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: var(--eg-color-accent);\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px var(--eg-color-accent-soft); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: var(--eg-color-cell-readonly); border-color: var(--eg-color-border-cell); }\n.eg-locked .eg-checkbox,\n.eg-locked .eg-checkbox-input { cursor: not-allowed; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: var(--eg-color-checkbox-disabled-checked); border-color: var(--eg-color-checkbox-disabled-checked); }\n</style>\n"]}, media: undefined });
|
|
13056
13256
|
|
|
13057
13257
|
};
|
|
13058
13258
|
/* scoped */
|
|
13059
|
-
const __vue_scope_id__ = "data-v-
|
|
13259
|
+
const __vue_scope_id__ = "data-v-ada16cee";
|
|
13060
13260
|
/* module identifier */
|
|
13061
13261
|
const __vue_module_identifier__ = undefined;
|
|
13062
13262
|
/* functional template */
|