vue2-excel-grid 1.1.61 → 1.1.64

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.
@@ -1308,7 +1308,8 @@ const messages = {
1308
1308
  size: 'Size',
1309
1309
  sizeSmall: 'Small',
1310
1310
  sizeMiddle: 'Middle',
1311
- sizeLarge: 'Large'
1311
+ sizeLarge: 'Large',
1312
+ theme: 'Theme'
1312
1313
  },
1313
1314
 
1314
1315
  ru: {
@@ -1350,7 +1351,8 @@ const messages = {
1350
1351
  size: 'Размер',
1351
1352
  sizeSmall: 'Маленький',
1352
1353
  sizeMiddle: 'Средний',
1353
- sizeLarge: 'Большой'
1354
+ sizeLarge: 'Большой',
1355
+ theme: 'Тема'
1354
1356
  },
1355
1357
 
1356
1358
  uz: {
@@ -1392,7 +1394,8 @@ const messages = {
1392
1394
  size: 'O\'lcham',
1393
1395
  sizeSmall: 'Kichik',
1394
1396
  sizeMiddle: 'O\'rta',
1395
- sizeLarge: 'Katta'
1397
+ sizeLarge: 'Katta',
1398
+ theme: 'Mavzu'
1396
1399
  },
1397
1400
 
1398
1401
  'uz-Cyrl': {
@@ -1434,7 +1437,8 @@ const messages = {
1434
1437
  size: 'Ўлчам',
1435
1438
  sizeSmall: 'Кичик',
1436
1439
  sizeMiddle: 'Ўрта',
1437
- sizeLarge: 'Катта'
1440
+ sizeLarge: 'Катта',
1441
+ theme: 'Мавзу'
1438
1442
  }
1439
1443
  };
1440
1444
 
@@ -3074,6 +3078,12 @@ anime.random = function (min, max) { return Math.floor(Math.random() * (max - mi
3074
3078
  //
3075
3079
  //
3076
3080
 
3081
+ // Approximate rendered height of one .eg-select-option row per grid `size`,
3082
+ // matching the .eg-size-{small,large}/.eg-size-middle (default) CSS overrides
3083
+ // below — used only to estimate whether the dropdown fits below the trigger
3084
+ // before it's actually rendered (see overlayStyle).
3085
+ const OPTION_ROW_H$1 = { small: 22, middle: 28, large: 33 };
3086
+
3077
3087
  var script$3 = {
3078
3088
  name: 'EgSelect',
3079
3089
  model: { prop: 'value', event: 'input' },
@@ -3084,7 +3094,8 @@ var script$3 = {
3084
3094
  placeholder: { type: String, default: '' },
3085
3095
  disabled: { type: Boolean, default: false },
3086
3096
  dropdownOnly: { type: Boolean, default: false },
3087
- triggerRect: { type: Object, default: null }
3097
+ triggerRect: { type: Object, default: null },
3098
+ size: { type: String, default: 'middle' }
3088
3099
  },
3089
3100
  data() {
3090
3101
  return {
@@ -3118,7 +3129,8 @@ var script$3 = {
3118
3129
  if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
3119
3130
  const rect = this.triggerRect;
3120
3131
  const searchH = this.enableSearch ? 42 : 0;
3121
- const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320);
3132
+ const rowH = OPTION_ROW_H$1[this.size] || OPTION_ROW_H$1.middle;
3133
+ const approxH = Math.min(this.filteredOptions.length * rowH + 8 + searchH, 320);
3122
3134
  const spaceBelow = window.innerHeight - rect.bottom;
3123
3135
  const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH;
3124
3136
  return {
@@ -3522,7 +3534,7 @@ __vue_render__$3._withStripped = true;
3522
3534
  /* style */
3523
3535
  const __vue_inject_styles__$3 = function (inject) {
3524
3536
  if (!inject) return
3525
- inject("data-v-991deba0_0", { 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/* Standalone wrapper */\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 {\n 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 {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eg-select-placeholder {\n color: var(--eg-color-text-muted);\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\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 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\n/* Search input inside dropdown */\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\n/* Option rows */\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 {\n background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active {\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: var(--eg-color-option-active-hover-bg);\n}\n\n/* Color dot */\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", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgSelect.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyTA,uBAAA;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;EACA,oCAAA;AACA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,yCAAA;EACA,eAAA;EACA,iCAAA;AACA;AACA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;AACA;AACA;EACA,iCAAA;AACA;AACA;EACA,cAAA;EACA,iCAAA;EACA,eAAA;AACA;;AAEA,mBAAA;AACA;EACA,mCAAA;EACA,wCAAA;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;;AAEA,iCAAA;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;;AAEA,gBAAA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,8BAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,2BAAA;AACA;AACA;;EAEA,2CAAA;AACA;AACA;EACA,gBAAA;EACA,6BAAA;AACA;AACA;;EAEA,kDAAA;AACA;;AAEA,cAAA;AACA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,eAAA;AACA","file":"EgSelect.vue","sourcesContent":["<template>\n <div\n :class=\"dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'\"\n :style=\"dropdownOnly ? overlayStyle : null\"\n @mousedown=\"onRootMouseDown\"\n >\n <!-- Standalone mode: trigger button -->\n <div\n v-if=\"!dropdownOnly\"\n class=\"eg-select-trigger\"\n :class=\"{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }\"\n @click.stop=\"toggle\"\n >\n <span v-if=\"selectedOpt && selectedOpt.color\" class=\"eg-select-dot\" :style=\"{ background: selectedOpt.color }\"></span>\n <span class=\"eg-select-trigger-label\" :class=\"{ 'eg-select-placeholder': !selectedOpt }\">\n {{ selectedOpt ? selectedOpt.label : placeholder }}\n </span>\n <span class=\"eg-select-caret\">▾</span>\n </div>\n\n <!-- Options panel in grid dropdown-only mode (root IS the dropdown panel) -->\n <template v-if=\"dropdownOnly\">\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </template>\n\n <!-- Options panel in standalone mode (floating dropdown below trigger) -->\n <div\n v-else-if=\"open\"\n class=\"eg-select-dropdown eg-select-dropdown--floating\"\n @mousedown.stop.prevent\n >\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </div>\n </div>\n</template>\n\n<script>\nexport default {\n name: 'EgSelect',\n model: { prop: 'value', event: 'input' },\n props: {\n value: { default: null },\n options: { type: Array, default: () => [] },\n enableSearch: { type: Boolean, default: false },\n placeholder: { type: String, default: '' },\n disabled: { type: Boolean, default: false },\n dropdownOnly: { type: Boolean, default: false },\n triggerRect: { type: Object, default: null }\n },\n data() {\n return {\n open: false,\n searchQuery: '',\n highlight: -1,\n // True right after Arrow/Page keyboard navigation — lets Space commit the\n // highlighted option instead of typing a literal space into the search box.\n // Reset on every text edit so normal multi-word search typing is unaffected.\n _navigatedByKeyboard: false\n }\n },\n computed: {\n normalizedOptions() {\n return (this.options || []).map(o =>\n typeof o === 'string'\n ? { value: o, label: o }\n : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n filteredOptions() {\n if (!this.enableSearch || !this.searchQuery.trim()) return this.normalizedOptions\n const q = this.searchQuery.trim().toLowerCase()\n return this.normalizedOptions.filter(o => String(o.label || '').toLowerCase().includes(q))\n },\n selectedOpt() {\n const v = String(this.value == null ? '' : this.value)\n return this.normalizedOptions.find(o => String(o.value) === v) || null\n },\n overlayStyle() {\n if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }\n const rect = this.triggerRect\n const searchH = this.enableSearch ? 42 : 0\n const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320)\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH\n return {\n position: 'fixed',\n top: top + 'px',\n left: rect.left + 'px',\n minWidth: Math.max(rect.width, 120) + 'px',\n maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',\n zIndex: 9999\n }\n }\n },\n watch: {\n open(val) {\n if (val && this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n }\n },\n mounted() {\n if (this.dropdownOnly) {\n const v = String(this.value == null ? '' : this.value)\n const selectedIndex = this.filteredOptions.findIndex(o => String(o.value) === v)\n this.highlight = selectedIndex\n this._scrollIndexIntoView(selectedIndex)\n if (this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n } else {\n document.addEventListener('click', this._onDocClick)\n }\n },\n beforeDestroy() {\n if (!this.dropdownOnly) {\n document.removeEventListener('click', this._onDocClick)\n }\n },\n methods: {\n toggle() {\n if (this.disabled) return\n this.open = !this.open\n if (this.open) {\n const v = String(this.value == null ? '' : this.value)\n this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)\n this.searchQuery = ''\n }\n },\n pickOption(opt) {\n this.$emit('input', opt.value)\n this.$emit('select', { value: opt.value, label: opt.label })\n if (!this.dropdownOnly) {\n this.open = false\n this.searchQuery = ''\n this.highlight = -1\n }\n },\n onSearch(e) {\n this.searchQuery = e.target.value\n this.highlight = 0\n this._navigatedByKeyboard = false\n },\n onSearchKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n if (this.dropdownOnly) this.$emit('close')\n else { this.open = false; this.searchQuery = '' }\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'Enter') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n } else if (key === ' ' && this._navigatedByKeyboard) {\n // Space commits the option reached via Arrow/Page navigation instead of\n // typing a literal space — a plain (non-navigated) space still types normally.\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n }\n },\n handleKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n else this.$emit('close')\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._scrollHighlightedIntoView()\n }\n },\n _scrollHighlightedIntoView() {\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[this.highlight]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n const elTop = el.offsetTop\n const elBottom = elTop + el.offsetHeight\n const visTop = container.scrollTop + stickyH\n const visBottom = container.scrollTop + container.clientHeight\n if (elTop < visTop) {\n container.scrollTop = elTop - stickyH\n } else if (elBottom > visBottom) {\n container.scrollTop = elBottom - container.clientHeight\n }\n })\n },\n // Aligns option `idx` to the top of the visible list on initial open.\n // Setting scrollTop past the container's max is clamped by the browser,\n // so an option near the end of the list naturally ends up scrolled to\n // the bottom instead of leaving blank space below it.\n _scrollIndexIntoView(idx) {\n if (idx < 0) return\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[idx]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n container.scrollTop = el.offsetTop - stickyH\n })\n },\n onRootMouseDown(e) {\n if (this.dropdownOnly) e.preventDefault()\n },\n _onDocClick(e) {\n if (!this.open) return\n if (this.$el && this.$el.contains(e.target)) return\n this.open = false\n }\n }\n}\n</script>\n\n<style>\n/* Standalone wrapper */\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 {\n 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 {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eg-select-placeholder {\n color: var(--eg-color-text-muted);\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\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 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\n/* Search input inside dropdown */\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\n/* Option rows */\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 {\n background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active {\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: var(--eg-color-option-active-hover-bg);\n}\n\n/* Color dot */\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</style>\n"]}, media: undefined });
3537
+ inject("data-v-fe40dc56_0", { 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/* Standalone wrapper */\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 {\n 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 {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eg-select-placeholder {\n color: var(--eg-color-text-muted);\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\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 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\n/* Search input inside dropdown */\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\n/* Option rows */\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 {\n background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active {\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: var(--eg-color-option-active-hover-bg);\n}\n\n/* Color dot */\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", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgSelect.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiUA,uBAAA;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;EACA,oCAAA;AACA;AACA;EACA,oCAAA;EACA,iDAAA;AACA;AACA;EACA,yCAAA;EACA,eAAA;EACA,iCAAA;AACA;AACA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;AACA;AACA;EACA,iCAAA;AACA;AACA;EACA,cAAA;EACA,iCAAA;EACA,eAAA;AACA;;AAEA,mBAAA;AACA;EACA,mCAAA;EACA,wCAAA;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;;AAEA,iCAAA;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;;AAEA,gBAAA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,8BAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,2BAAA;AACA;AACA;;EAEA,2CAAA;AACA;AACA;EACA,gBAAA;EACA,6BAAA;AACA;AACA;;EAEA,kDAAA;AACA;;AAEA,cAAA;AACA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,eAAA;AACA","file":"EgSelect.vue","sourcesContent":["<template>\n <div\n :class=\"dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'\"\n :style=\"dropdownOnly ? overlayStyle : null\"\n @mousedown=\"onRootMouseDown\"\n >\n <!-- Standalone mode: trigger button -->\n <div\n v-if=\"!dropdownOnly\"\n class=\"eg-select-trigger\"\n :class=\"{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }\"\n @click.stop=\"toggle\"\n >\n <span v-if=\"selectedOpt && selectedOpt.color\" class=\"eg-select-dot\" :style=\"{ background: selectedOpt.color }\"></span>\n <span class=\"eg-select-trigger-label\" :class=\"{ 'eg-select-placeholder': !selectedOpt }\">\n {{ selectedOpt ? selectedOpt.label : placeholder }}\n </span>\n <span class=\"eg-select-caret\">▾</span>\n </div>\n\n <!-- Options panel in grid dropdown-only mode (root IS the dropdown panel) -->\n <template v-if=\"dropdownOnly\">\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </template>\n\n <!-- Options panel in standalone mode (floating dropdown below trigger) -->\n <div\n v-else-if=\"open\"\n class=\"eg-select-dropdown eg-select-dropdown--floating\"\n @mousedown.stop.prevent\n >\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </div>\n </div>\n</template>\n\n<script>\n// Approximate rendered height of one .eg-select-option row per grid `size`,\n// matching the .eg-size-{small,large}/.eg-size-middle (default) CSS overrides\n// below — used only to estimate whether the dropdown fits below the trigger\n// before it's actually rendered (see overlayStyle).\nconst OPTION_ROW_H = { small: 22, middle: 28, large: 33 }\n\nexport default {\n name: 'EgSelect',\n model: { prop: 'value', event: 'input' },\n props: {\n value: { default: null },\n options: { type: Array, default: () => [] },\n enableSearch: { type: Boolean, default: false },\n placeholder: { type: String, default: '' },\n disabled: { type: Boolean, default: false },\n dropdownOnly: { type: Boolean, default: false },\n triggerRect: { type: Object, default: null },\n size: { type: String, default: 'middle' }\n },\n data() {\n return {\n open: false,\n searchQuery: '',\n highlight: -1,\n // True right after Arrow/Page keyboard navigation — lets Space commit the\n // highlighted option instead of typing a literal space into the search box.\n // Reset on every text edit so normal multi-word search typing is unaffected.\n _navigatedByKeyboard: false\n }\n },\n computed: {\n normalizedOptions() {\n return (this.options || []).map(o =>\n typeof o === 'string'\n ? { value: o, label: o }\n : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }\n )\n },\n filteredOptions() {\n if (!this.enableSearch || !this.searchQuery.trim()) return this.normalizedOptions\n const q = this.searchQuery.trim().toLowerCase()\n return this.normalizedOptions.filter(o => String(o.label || '').toLowerCase().includes(q))\n },\n selectedOpt() {\n const v = String(this.value == null ? '' : this.value)\n return this.normalizedOptions.find(o => String(o.value) === v) || null\n },\n overlayStyle() {\n if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }\n const rect = this.triggerRect\n const searchH = this.enableSearch ? 42 : 0\n const rowH = OPTION_ROW_H[this.size] || OPTION_ROW_H.middle\n const approxH = Math.min(this.filteredOptions.length * rowH + 8 + searchH, 320)\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH\n return {\n position: 'fixed',\n top: top + 'px',\n left: rect.left + 'px',\n minWidth: Math.max(rect.width, 120) + 'px',\n maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',\n zIndex: 9999\n }\n }\n },\n watch: {\n open(val) {\n if (val && this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n }\n },\n mounted() {\n if (this.dropdownOnly) {\n const v = String(this.value == null ? '' : this.value)\n const selectedIndex = this.filteredOptions.findIndex(o => String(o.value) === v)\n this.highlight = selectedIndex\n this._scrollIndexIntoView(selectedIndex)\n if (this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n } else {\n document.addEventListener('click', this._onDocClick)\n }\n },\n beforeDestroy() {\n if (!this.dropdownOnly) {\n document.removeEventListener('click', this._onDocClick)\n }\n },\n methods: {\n toggle() {\n if (this.disabled) return\n this.open = !this.open\n if (this.open) {\n const v = String(this.value == null ? '' : this.value)\n this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)\n this.searchQuery = ''\n }\n },\n pickOption(opt) {\n this.$emit('input', opt.value)\n this.$emit('select', { value: opt.value, label: opt.label })\n if (!this.dropdownOnly) {\n this.open = false\n this.searchQuery = ''\n this.highlight = -1\n }\n },\n onSearch(e) {\n this.searchQuery = e.target.value\n this.highlight = 0\n this._navigatedByKeyboard = false\n },\n onSearchKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n if (this.dropdownOnly) this.$emit('close')\n else { this.open = false; this.searchQuery = '' }\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'Enter') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n } else if (key === ' ' && this._navigatedByKeyboard) {\n // Space commits the option reached via Arrow/Page navigation instead of\n // typing a literal space — a plain (non-navigated) space still types normally.\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n }\n },\n handleKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n else this.$emit('close')\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._scrollHighlightedIntoView()\n }\n },\n _scrollHighlightedIntoView() {\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[this.highlight]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n const elTop = el.offsetTop\n const elBottom = elTop + el.offsetHeight\n const visTop = container.scrollTop + stickyH\n const visBottom = container.scrollTop + container.clientHeight\n if (elTop < visTop) {\n container.scrollTop = elTop - stickyH\n } else if (elBottom > visBottom) {\n container.scrollTop = elBottom - container.clientHeight\n }\n })\n },\n // Aligns option `idx` to the top of the visible list on initial open.\n // Setting scrollTop past the container's max is clamped by the browser,\n // so an option near the end of the list naturally ends up scrolled to\n // the bottom instead of leaving blank space below it.\n _scrollIndexIntoView(idx) {\n if (idx < 0) return\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[idx]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n container.scrollTop = el.offsetTop - stickyH\n })\n },\n onRootMouseDown(e) {\n if (this.dropdownOnly) e.preventDefault()\n },\n _onDocClick(e) {\n if (!this.open) return\n if (this.$el && this.$el.contains(e.target)) return\n this.open = false\n }\n }\n}\n</script>\n\n<style>\n/* Standalone wrapper */\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 {\n 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 {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n color: var(--eg-color-text-muted);\n}\n.eg-select-trigger-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.eg-select-placeholder {\n color: var(--eg-color-text-muted);\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: var(--eg-color-text-muted);\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\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 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\n/* Search input inside dropdown */\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\n/* Option rows */\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 {\n background: var(--eg-color-option-hover-bg);\n}\n.eg-select-option--active {\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: var(--eg-color-option-active-hover-bg);\n}\n\n/* Color dot */\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</style>\n"]}, media: undefined });
3526
3538
 
3527
3539
  };
3528
3540
  /* scoped */
@@ -3654,6 +3666,12 @@ __vue_render__$3._withStripped = true;
3654
3666
  //
3655
3667
  //
3656
3668
 
3669
+ // Approximate rendered height of one .eg-select-option row per grid `size`,
3670
+ // matching the .eg-size-{small,large}/.eg-size-middle (default) CSS overrides
3671
+ // (see ExcelGrid.vue's <style>) — used only to estimate whether the dropdown
3672
+ // fits below the trigger before it's actually rendered (see overlayStyle).
3673
+ const OPTION_ROW_H = { small: 22, middle: 28, large: 33 };
3674
+
3657
3675
  var script$2 = {
3658
3676
  name: 'EgTreeSelect',
3659
3677
  model: { prop: 'value', event: 'input' },
@@ -3667,7 +3685,8 @@ var script$2 = {
3667
3685
  placeholder: { type: String, default: '' },
3668
3686
  disabled: { type: Boolean, default: false },
3669
3687
  dropdownOnly: { type: Boolean, default: false },
3670
- triggerRect: { type: Object, default: null }
3688
+ triggerRect: { type: Object, default: null },
3689
+ size: { type: String, default: 'middle' }
3671
3690
  },
3672
3691
  data() {
3673
3692
  return {
@@ -3708,7 +3727,8 @@ var script$2 = {
3708
3727
  if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
3709
3728
  const rect = this.triggerRect;
3710
3729
  const searchH = this.enableSearch ? 42 : 0;
3711
- const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320);
3730
+ const rowH = OPTION_ROW_H[this.size] || OPTION_ROW_H.middle;
3731
+ const approxH = Math.min(this.filteredOptions.length * rowH + 8 + searchH, 320);
3712
3732
  const spaceBelow = window.innerHeight - rect.bottom;
3713
3733
  const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH;
3714
3734
  return {
@@ -4235,7 +4255,7 @@ __vue_render__$2._withStripped = true;
4235
4255
  /* style */
4236
4256
  const __vue_inject_styles__$2 = function (inject) {
4237
4257
  if (!inject) return
4238
- inject("data-v-6d3e0cc0_0", { 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/* 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 {\n visibility: hidden;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgTreeSelect.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwZA,gBAAA;AACA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;AACA","file":"EgTreeSelect.vue","sourcesContent":["<template>\n <div\n :class=\"dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'\"\n :style=\"dropdownOnly ? overlayStyle : null\"\n @mousedown=\"onRootMouseDown\"\n >\n <!-- Standalone mode: trigger button -->\n <div\n v-if=\"!dropdownOnly\"\n class=\"eg-select-trigger\"\n :class=\"{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }\"\n @click.stop=\"toggle\"\n >\n <span class=\"eg-select-trigger-label\" :class=\"{ 'eg-select-placeholder': selectedLabel == null }\">\n {{ selectedLabel != null ? selectedLabel : placeholder }}\n </span>\n <span class=\"eg-select-caret\">▾</span>\n </div>\n\n <!-- Options panel in grid dropdown-only mode -->\n <template v-if=\"dropdownOnly\">\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"opt.id != null ? opt.id : String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n :style=\"!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span\n v-if=\"!searchQuery\"\n class=\"eg-tree-toggle\"\n :class=\"{ 'eg-tree-toggle--leaf': !opt.hasChildren }\"\n @mousedown.prevent.stop=\"toggleNode(idx)\"\n >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </template>\n\n <!-- Options panel in standalone mode -->\n <div\n v-else-if=\"open\"\n class=\"eg-select-dropdown eg-select-dropdown--floating\"\n @mousedown.stop.prevent\n >\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"opt.id != null ? opt.id : String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n :style=\"!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span\n v-if=\"!searchQuery\"\n class=\"eg-tree-toggle\"\n :class=\"{ 'eg-tree-toggle--leaf': !opt.hasChildren }\"\n @mousedown.prevent.stop=\"toggleNode(idx)\"\n >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </div>\n </div>\n</template>\n\n<script>\nexport default {\n name: 'EgTreeSelect',\n model: { prop: 'value', event: 'input' },\n props: {\n value: { default: null },\n options: { type: Array, default: () => [] },\n valueKey: { type: String, default: 'value' },\n labelKey: { type: String, default: 'label' },\n childrenKey: { type: String, default: 'children' },\n enableSearch: { type: Boolean, default: false },\n placeholder: { type: String, default: '' },\n disabled: { type: Boolean, default: false },\n dropdownOnly: { type: Boolean, default: false },\n triggerRect: { type: Object, default: null }\n },\n data() {\n return {\n open: false,\n searchQuery: '',\n highlight: -1,\n expandedMap: {},\n // True right after Arrow/Page keyboard navigation — lets Space commit the\n // highlighted option instead of typing a literal space into the search box.\n // Reset on every text edit so normal multi-word search typing is unaffected.\n _navigatedByKeyboard: false\n }\n },\n computed: {\n keys() {\n return { valueKey: this.valueKey, labelKey: this.labelKey, childrenKey: this.childrenKey }\n },\n flatOptions() {\n return this._treeFlatten(this.options || [], this.keys, this.expandedMap, 0, [])\n },\n allFlatOptions() {\n if (!this.enableSearch) return null\n return this._treeFlattenAll(this.options || [], this.keys)\n },\n filteredOptions() {\n if (!this.enableSearch || !this.searchQuery.trim()) return this.flatOptions\n const q = this.searchQuery.trim().toLowerCase()\n const pool = this.allFlatOptions || this.flatOptions\n return pool.filter(o => String(o.label || '').toLowerCase().includes(q))\n },\n selectedLabel() {\n const v = String(this.value == null ? '' : this.value)\n if (!v) return null\n const found = this._treeFindLabel(this.options || [], this.keys, v)\n return found != null ? found : null\n },\n overlayStyle() {\n if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }\n const rect = this.triggerRect\n const searchH = this.enableSearch ? 42 : 0\n const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320)\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH\n return {\n position: 'fixed',\n top: top + 'px',\n left: rect.left + 'px',\n minWidth: Math.max(rect.width, 120) + 'px',\n maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',\n zIndex: 9999\n }\n }\n },\n watch: {\n open(val) {\n if (val && this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n }\n },\n mounted() {\n if (this.dropdownOnly) {\n this._initExpanded()\n this._scrollIndexIntoView(this.highlight)\n if (this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n } else {\n document.addEventListener('click', this._onDocClick)\n }\n },\n beforeDestroy() {\n if (!this.dropdownOnly) {\n document.removeEventListener('click', this._onDocClick)\n }\n },\n methods: {\n _initExpanded() {\n const v = String(this.value == null ? '' : this.value)\n const path = this._treeFindPath(this.options || [], this.keys, v, []) || []\n const expandedMap = {}\n path.forEach(id => { expandedMap[id] = true })\n this.expandedMap = expandedMap\n const flat = this.flatOptions\n this.highlight = flat.findIndex(o => String(o.value) === v)\n },\n toggle() {\n if (this.disabled) return\n this.open = !this.open\n if (this.open) {\n this.searchQuery = ''\n this._initExpanded()\n }\n },\n pickOption(opt) {\n this.$emit('input', opt.value)\n this.$emit('select', { value: opt.value, label: opt.label })\n if (!this.dropdownOnly) {\n this.open = false\n this.searchQuery = ''\n this.highlight = -1\n }\n },\n toggleNode(idx) {\n const opt = this.flatOptions[idx]\n if (!opt || !opt.hasChildren) return\n const next = { ...this.expandedMap }\n if (next[opt.id]) delete next[opt.id]\n else next[opt.id] = true\n this.expandedMap = next\n const newFlat = this.flatOptions\n const newIdx = newFlat.findIndex(o => o.id === opt.id)\n this.highlight = newIdx >= 0 ? newIdx : this.highlight\n },\n onSearch(e) {\n this.searchQuery = e.target.value\n this.highlight = 0\n this._navigatedByKeyboard = false\n },\n onSearchKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n if (this.dropdownOnly) this.$emit('close')\n else { this.open = false; this.searchQuery = '' }\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'Enter') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n } else if (key === ' ' && this._navigatedByKeyboard) {\n // Space commits the option reached via Arrow/Page navigation instead of\n // typing a literal space — a plain (non-navigated) space still types normally.\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n }\n },\n handleKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n else this.$emit('close')\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._scrollHighlightedIntoView()\n } else if (!this.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {\n e.preventDefault()\n const opt = this.flatOptions[this.highlight]\n if (opt && opt.hasChildren) {\n if (key === 'ArrowRight' && !opt.expanded) this.toggleNode(this.highlight)\n if (key === 'ArrowLeft' && opt.expanded) this.toggleNode(this.highlight)\n }\n }\n },\n _scrollHighlightedIntoView() {\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[this.highlight]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n const elTop = el.offsetTop\n const elBottom = elTop + el.offsetHeight\n const visTop = container.scrollTop + stickyH\n const visBottom = container.scrollTop + container.clientHeight\n if (elTop < visTop) {\n container.scrollTop = elTop - stickyH\n } else if (elBottom > visBottom) {\n container.scrollTop = elBottom - container.clientHeight\n }\n })\n },\n // Aligns option `idx` to the top of the visible list on initial open.\n // Setting scrollTop past the container's max is clamped by the browser,\n // so an option near the end of the list naturally ends up scrolled to\n // the bottom instead of leaving blank space below it.\n _scrollIndexIntoView(idx) {\n if (idx < 0) return\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[idx]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n container.scrollTop = el.offsetTop - stickyH\n })\n },\n onRootMouseDown(e) {\n if (this.dropdownOnly) e.preventDefault()\n },\n _onDocClick(e) {\n if (!this.open) return\n if (this.$el && this.$el.contains(e.target)) return\n this.open = false\n },\n _treeFlatten(nodes, keys, expandedMap, depth, pathArr) {\n let out = []\n ;(nodes || []).forEach((node, idx) => {\n const path = pathArr.concat(idx)\n const id = path.join('-')\n const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []\n const hasChildren = children.length > 0\n const expanded = hasChildren && !!expandedMap[id]\n out.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id, depth, hasChildren, expanded })\n if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))\n })\n return out\n },\n _treeFlattenAll(nodes, keys) {\n const result = []\n const walk = (list) => {\n ;(list || []).forEach(node => {\n const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []\n result.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id: String(node[keys.valueKey]), depth: 0, hasChildren: false, expanded: false })\n if (children.length) walk(children)\n })\n }\n walk(nodes)\n return result\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 _treeFindPath(nodes, keys, value, pathArr) {\n for (let i = 0; i < (nodes || []).length; i++) {\n const node = nodes[i]\n const path = pathArr.concat(i)\n if (String(node[keys.valueKey]) === value) return [path.join('-')]\n const children = node[keys.childrenKey]\n if (Array.isArray(children)) {\n const sub = this._treeFindPath(children, keys, value, path)\n if (sub) return [path.join('-')].concat(sub)\n }\n }\n return null\n }\n }\n}\n</script>\n\n<style>\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 {\n visibility: hidden;\n}\n</style>\n"]}, media: undefined });
4258
+ inject("data-v-bfde3fb4_0", { 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/* 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 {\n visibility: hidden;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgTreeSelect.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgaA,gBAAA;AACA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,iCAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;AACA","file":"EgTreeSelect.vue","sourcesContent":["<template>\n <div\n :class=\"dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'\"\n :style=\"dropdownOnly ? overlayStyle : null\"\n @mousedown=\"onRootMouseDown\"\n >\n <!-- Standalone mode: trigger button -->\n <div\n v-if=\"!dropdownOnly\"\n class=\"eg-select-trigger\"\n :class=\"{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }\"\n @click.stop=\"toggle\"\n >\n <span class=\"eg-select-trigger-label\" :class=\"{ 'eg-select-placeholder': selectedLabel == null }\">\n {{ selectedLabel != null ? selectedLabel : placeholder }}\n </span>\n <span class=\"eg-select-caret\">▾</span>\n </div>\n\n <!-- Options panel in grid dropdown-only mode -->\n <template v-if=\"dropdownOnly\">\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"opt.id != null ? opt.id : String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n :style=\"!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span\n v-if=\"!searchQuery\"\n class=\"eg-tree-toggle\"\n :class=\"{ 'eg-tree-toggle--leaf': !opt.hasChildren }\"\n @mousedown.prevent.stop=\"toggleNode(idx)\"\n >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </template>\n\n <!-- Options panel in standalone mode -->\n <div\n v-else-if=\"open\"\n class=\"eg-select-dropdown eg-select-dropdown--floating\"\n @mousedown.stop.prevent\n >\n <div v-if=\"enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"searchInput\"\n class=\"eg-select-search-input\"\n :value=\"searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onSearch\"\n @keydown.stop=\"onSearchKey\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredOptions\"\n :key=\"opt.id != null ? opt.id : String(opt.value)\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': String(opt.value) === String(value),\n 'eg-select-option--highlighted': idx === highlight\n }\"\n :style=\"!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null\"\n @mousedown.prevent=\"pickOption(opt)\"\n @mouseover=\"highlight = idx\"\n >\n <span\n v-if=\"!searchQuery\"\n class=\"eg-tree-toggle\"\n :class=\"{ 'eg-tree-toggle--leaf': !opt.hasChildren }\"\n @mousedown.prevent.stop=\"toggleNode(idx)\"\n >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>\n <span v-if=\"opt.color\" class=\"eg-select-dot\" :style=\"{ background: opt.color }\"></span>\n {{ opt.label }}\n </div>\n </div>\n </div>\n</template>\n\n<script>\n// Approximate rendered height of one .eg-select-option row per grid `size`,\n// matching the .eg-size-{small,large}/.eg-size-middle (default) CSS overrides\n// (see ExcelGrid.vue's <style>) — used only to estimate whether the dropdown\n// fits below the trigger before it's actually rendered (see overlayStyle).\nconst OPTION_ROW_H = { small: 22, middle: 28, large: 33 }\n\nexport default {\n name: 'EgTreeSelect',\n model: { prop: 'value', event: 'input' },\n props: {\n value: { default: null },\n options: { type: Array, default: () => [] },\n valueKey: { type: String, default: 'value' },\n labelKey: { type: String, default: 'label' },\n childrenKey: { type: String, default: 'children' },\n enableSearch: { type: Boolean, default: false },\n placeholder: { type: String, default: '' },\n disabled: { type: Boolean, default: false },\n dropdownOnly: { type: Boolean, default: false },\n triggerRect: { type: Object, default: null },\n size: { type: String, default: 'middle' }\n },\n data() {\n return {\n open: false,\n searchQuery: '',\n highlight: -1,\n expandedMap: {},\n // True right after Arrow/Page keyboard navigation — lets Space commit the\n // highlighted option instead of typing a literal space into the search box.\n // Reset on every text edit so normal multi-word search typing is unaffected.\n _navigatedByKeyboard: false\n }\n },\n computed: {\n keys() {\n return { valueKey: this.valueKey, labelKey: this.labelKey, childrenKey: this.childrenKey }\n },\n flatOptions() {\n return this._treeFlatten(this.options || [], this.keys, this.expandedMap, 0, [])\n },\n allFlatOptions() {\n if (!this.enableSearch) return null\n return this._treeFlattenAll(this.options || [], this.keys)\n },\n filteredOptions() {\n if (!this.enableSearch || !this.searchQuery.trim()) return this.flatOptions\n const q = this.searchQuery.trim().toLowerCase()\n const pool = this.allFlatOptions || this.flatOptions\n return pool.filter(o => String(o.label || '').toLowerCase().includes(q))\n },\n selectedLabel() {\n const v = String(this.value == null ? '' : this.value)\n if (!v) return null\n const found = this._treeFindLabel(this.options || [], this.keys, v)\n return found != null ? found : null\n },\n overlayStyle() {\n if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }\n const rect = this.triggerRect\n const searchH = this.enableSearch ? 42 : 0\n const rowH = OPTION_ROW_H[this.size] || OPTION_ROW_H.middle\n const approxH = Math.min(this.filteredOptions.length * rowH + 8 + searchH, 320)\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH\n return {\n position: 'fixed',\n top: top + 'px',\n left: rect.left + 'px',\n minWidth: Math.max(rect.width, 120) + 'px',\n maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',\n zIndex: 9999\n }\n }\n },\n watch: {\n open(val) {\n if (val && this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n }\n },\n mounted() {\n if (this.dropdownOnly) {\n this._initExpanded()\n this._scrollIndexIntoView(this.highlight)\n if (this.enableSearch) {\n this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })\n }\n } else {\n document.addEventListener('click', this._onDocClick)\n }\n },\n beforeDestroy() {\n if (!this.dropdownOnly) {\n document.removeEventListener('click', this._onDocClick)\n }\n },\n methods: {\n _initExpanded() {\n const v = String(this.value == null ? '' : this.value)\n const path = this._treeFindPath(this.options || [], this.keys, v, []) || []\n const expandedMap = {}\n path.forEach(id => { expandedMap[id] = true })\n this.expandedMap = expandedMap\n const flat = this.flatOptions\n this.highlight = flat.findIndex(o => String(o.value) === v)\n },\n toggle() {\n if (this.disabled) return\n this.open = !this.open\n if (this.open) {\n this.searchQuery = ''\n this._initExpanded()\n }\n },\n pickOption(opt) {\n this.$emit('input', opt.value)\n this.$emit('select', { value: opt.value, label: opt.label })\n if (!this.dropdownOnly) {\n this.open = false\n this.searchQuery = ''\n this.highlight = -1\n }\n },\n toggleNode(idx) {\n const opt = this.flatOptions[idx]\n if (!opt || !opt.hasChildren) return\n const next = { ...this.expandedMap }\n if (next[opt.id]) delete next[opt.id]\n else next[opt.id] = true\n this.expandedMap = next\n const newFlat = this.flatOptions\n const newIdx = newFlat.findIndex(o => o.id === opt.id)\n this.highlight = newIdx >= 0 ? newIdx : this.highlight\n },\n onSearch(e) {\n this.searchQuery = e.target.value\n this.highlight = 0\n this._navigatedByKeyboard = false\n },\n onSearchKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n if (this.dropdownOnly) this.$emit('close')\n else { this.open = false; this.searchQuery = '' }\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._navigatedByKeyboard = true\n this._scrollHighlightedIntoView()\n } else if (key === 'Enter') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n } else if (key === ' ' && this._navigatedByKeyboard) {\n // Space commits the option reached via Arrow/Page navigation instead of\n // typing a literal space — a plain (non-navigated) space still types normally.\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n }\n },\n handleKey(e) {\n const key = e.key\n const last = this.filteredOptions.length - 1\n if (key === 'Escape') {\n e.preventDefault()\n this.$emit('close')\n } else if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n const opt = this.filteredOptions[this.highlight]\n if (opt) this.pickOption(opt)\n else this.$emit('close')\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageDown') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)\n this._scrollHighlightedIntoView()\n } else if (key === 'PageUp') {\n e.preventDefault()\n this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)\n this._scrollHighlightedIntoView()\n } else if (!this.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {\n e.preventDefault()\n const opt = this.flatOptions[this.highlight]\n if (opt && opt.hasChildren) {\n if (key === 'ArrowRight' && !opt.expanded) this.toggleNode(this.highlight)\n if (key === 'ArrowLeft' && opt.expanded) this.toggleNode(this.highlight)\n }\n }\n },\n _scrollHighlightedIntoView() {\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[this.highlight]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n const elTop = el.offsetTop\n const elBottom = elTop + el.offsetHeight\n const visTop = container.scrollTop + stickyH\n const visBottom = container.scrollTop + container.clientHeight\n if (elTop < visTop) {\n container.scrollTop = elTop - stickyH\n } else if (elBottom > visBottom) {\n container.scrollTop = elBottom - container.clientHeight\n }\n })\n },\n // Aligns option `idx` to the top of the visible list on initial open.\n // Setting scrollTop past the container's max is clamped by the browser,\n // so an option near the end of the list naturally ends up scrolled to\n // the bottom instead of leaving blank space below it.\n _scrollIndexIntoView(idx) {\n if (idx < 0) return\n this.$nextTick(() => {\n const items = this.$el.querySelectorAll('.eg-select-option')\n const el = items[idx]\n if (!el) return\n const container = this.$el\n const searchEl = container.querySelector('.eg-select-search')\n const stickyH = searchEl ? searchEl.offsetHeight : 0\n container.scrollTop = el.offsetTop - stickyH\n })\n },\n onRootMouseDown(e) {\n if (this.dropdownOnly) e.preventDefault()\n },\n _onDocClick(e) {\n if (!this.open) return\n if (this.$el && this.$el.contains(e.target)) return\n this.open = false\n },\n _treeFlatten(nodes, keys, expandedMap, depth, pathArr) {\n let out = []\n ;(nodes || []).forEach((node, idx) => {\n const path = pathArr.concat(idx)\n const id = path.join('-')\n const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []\n const hasChildren = children.length > 0\n const expanded = hasChildren && !!expandedMap[id]\n out.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id, depth, hasChildren, expanded })\n if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))\n })\n return out\n },\n _treeFlattenAll(nodes, keys) {\n const result = []\n const walk = (list) => {\n ;(list || []).forEach(node => {\n const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []\n result.push({ value: node[keys.valueKey], label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]), color: node.color, id: String(node[keys.valueKey]), depth: 0, hasChildren: false, expanded: false })\n if (children.length) walk(children)\n })\n }\n walk(nodes)\n return result\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 _treeFindPath(nodes, keys, value, pathArr) {\n for (let i = 0; i < (nodes || []).length; i++) {\n const node = nodes[i]\n const path = pathArr.concat(i)\n if (String(node[keys.valueKey]) === value) return [path.join('-')]\n const children = node[keys.childrenKey]\n if (Array.isArray(children)) {\n const sub = this._treeFindPath(children, keys, value, path)\n if (sub) return [path.join('-')].concat(sub)\n }\n }\n return null\n }\n }\n}\n</script>\n\n<style>\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 {\n visibility: hidden;\n}\n</style>\n"]}, media: undefined });
4239
4259
 
4240
4260
  };
4241
4261
  /* scoped */
@@ -4426,8 +4446,20 @@ var script$1 = {
4426
4446
  methods: {
4427
4447
  // ── Text input ──────────────────────────────────────────────
4428
4448
  onTextInput(e) {
4429
- this.inputVal = e.target.value;
4449
+ const isDeleting = e.inputType
4450
+ ? e.inputType.startsWith('delete')
4451
+ : e.target.value.length < this.inputVal.length;
4452
+
4453
+ this.inputVal = isDeleting ? e.target.value : this._autoFillSep(e.target.value);
4430
4454
  this.inputError = false;
4455
+
4456
+ if (!isDeleting) {
4457
+ this.$nextTick(() => {
4458
+ const el = this.$refs.textInput;
4459
+ if (el) el.setSelectionRange(this.inputVal.length, this.inputVal.length);
4460
+ });
4461
+ }
4462
+
4431
4463
  const d = parseDateValue(this.inputVal);
4432
4464
  if (d) {
4433
4465
  this.viewYear = d.getFullYear();
@@ -4436,6 +4468,30 @@ var script$1 = {
4436
4468
  }
4437
4469
  },
4438
4470
 
4471
+ _autoFillSep(raw) {
4472
+ const fmt = this.format;
4473
+ let sep, segments;
4474
+ if (fmt === 'YYYY-MM-DD') { sep = '-'; segments = [4, 2, 2]; }
4475
+ else if (fmt === 'DD.MM.YYYY') { sep = '.'; segments = [2, 2, 4]; }
4476
+ else if (fmt === 'DD/MM/YYYY') { sep = '/'; segments = [2, 2, 4]; }
4477
+ else if (fmt === 'MM/DD/YYYY') { sep = '/'; segments = [2, 2, 4]; }
4478
+ else return raw
4479
+
4480
+ const maxDigits = segments.reduce((a, b) => a + b, 0);
4481
+ const digits = raw.replace(/\D/g, '').slice(0, maxDigits);
4482
+
4483
+ let result = '';
4484
+ let pos = 0;
4485
+ for (let i = 0; i < segments.length; i++) {
4486
+ const len = segments[i];
4487
+ const chunk = digits.slice(pos, pos + len);
4488
+ result += chunk;
4489
+ pos += len;
4490
+ if (chunk.length === len && i < segments.length - 1) result += sep;
4491
+ }
4492
+ return result
4493
+ },
4494
+
4439
4495
  onInputKeyDown(e) {
4440
4496
  if (e.key === 'Enter') {
4441
4497
  e.preventDefault();
@@ -4708,7 +4764,7 @@ __vue_render__$1._withStripped = true;
4708
4764
  /* style */
4709
4765
  const __vue_inject_styles__$1 = function (inject) {
4710
4766
  if (!inject) return
4711
- inject("data-v-5ae418cf_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":";AA6PA;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\">&#8249;</button>\n <span class=\"eg-dp-title\">{{ t.months[viewMonth] }} {{ viewYear }}</span>\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"nextMonth\">&#8250;</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 this.inputVal = e.target.value\n this.inputError = false\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 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 });
4767
+ 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\">&#8249;</button>\n <span class=\"eg-dp-title\">{{ t.months[viewMonth] }} {{ viewYear }}</span>\n <button class=\"eg-dp-nav\" tabindex=\"-1\" @mousedown.prevent=\"nextMonth\">&#8250;</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 });
4712
4768
 
4713
4769
  };
4714
4770
  /* scoped */
@@ -4746,6 +4802,10 @@ const SIZE_CONFIG = {
4746
4802
  const ROW_HEADER_WIDTH = 50;
4747
4803
  const DEFAULT_COL_WIDTH = 100;
4748
4804
  const OUTLINE_GUTTER = 20;
4805
+ // Default toolbar tool list when `toolbar` is `true` or an object without
4806
+ // its own `list` — every group except 'theme', which stays opt-in since it
4807
+ // needs :theme.sync on the parent to do anything.
4808
+ const DEFAULT_TOOLBAR_LIST = ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen'];
4749
4809
 
4750
4810
  var script = {
4751
4811
  name: 'ExcelGrid',
@@ -4843,26 +4903,50 @@ var script = {
4843
4903
  */
4844
4904
  columnDraggable: { type: Boolean, default: true },
4845
4905
 
4846
- /** Show the built-in toolbar above the grid. Controlled entirely by the parent — no in-component toggle. */
4847
- toolbar: { type: Boolean, default: false },
4848
-
4849
- /** Show the fullscreen toggle button in the toolbar. */
4850
- toolbarFullscreen: { type: Boolean, default: true },
4851
-
4852
- /** Show the size (small/middle/large) selector in the toolbar. */
4853
- toolbarSize: { type: Boolean, default: true },
4854
-
4855
- /** Show the lock toggle button in the toolbar. */
4856
- toolbarLock: { type: Boolean, default: true },
4857
-
4858
- /** Show the undo/redo buttons in the toolbar. */
4859
- toolbarUndoRedo: { type: Boolean, default: true },
4860
-
4861
- /** Show the export button in the toolbar (calls exportToExcel() with no options). */
4862
- toolbarExport: { type: Boolean, default: true },
4863
-
4864
- /** Show the import button in the toolbar (opens a file picker, calls importFromFile()). */
4865
- toolbarImport: { type: Boolean, default: true },
4906
+ /**
4907
+ * Controls the built-in toolbar above the grid — visibility as a whole,
4908
+ * and (optionally) which individual tool groups appear. Controlled
4909
+ * entirely by the parent — no in-component toggle for the bar itself.
4910
+ *
4911
+ * @type {Boolean|{visible?: Boolean, list?: Array<('undoRedo'|'export'|'import'|'size'|'lock'|'fullscreen'|'theme')>}}
4912
+ * @default false
4913
+ *
4914
+ * Two forms:
4915
+ * - `Boolean` — `false` (default) hides the toolbar entirely; `true`
4916
+ * shows it with the default tool list (every group except `'theme'`,
4917
+ * since that one needs `:theme.sync` on the parent to be useful).
4918
+ * - `Object { visible, list }` — fine-grained control:
4919
+ * - `visible` {Boolean} — defaults to `true` when an object is given
4920
+ * (passing an object is itself an opt-in to show the toolbar);
4921
+ * set explicitly to `false` to hide it while still describing a
4922
+ * `list` you intend to reuse later.
4923
+ * - `list` {Array<String>} — which tool groups render, using the
4924
+ * identifiers above. Unknown/misspelled strings are silently
4925
+ * ignored. Omitting `list` falls back to the same default set as
4926
+ * the `true` shorthand. **Order in `list` does not change the
4927
+ * toolbar's layout** — groups always render in a fixed position
4928
+ * (undo/redo → export/import → #toolbar-extra slot → toolbarButtons
4929
+ * → spacer → theme → size → lock/fullscreen); `list` is purely an
4930
+ * inclusion filter.
4931
+ *
4932
+ * `toolbarButtons` and the `#toolbar-extra` slot are independent of
4933
+ * this prop — they render whenever content is provided, regardless of
4934
+ * `list`.
4935
+ *
4936
+ * @example
4937
+ * // Whole default set
4938
+ * <excel-grid toolbar />
4939
+ * @example
4940
+ * // Only undo/redo and fullscreen
4941
+ * <excel-grid :toolbar="{ list: ['undoRedo', 'fullscreen'] }" />
4942
+ * @example
4943
+ * // Toggle visibility from a data property while keeping a fixed list
4944
+ * <excel-grid :toolbar="{ visible: showToolbar, list: ['export', 'import'] }" />
4945
+ * @example
4946
+ * // Opt into the theme picker (needs :theme.sync)
4947
+ * <excel-grid :toolbar="{ list: ['undoRedo', 'export', 'import', 'size', 'lock', 'fullscreen', 'theme'] }" :theme.sync="myTheme" />
4948
+ */
4949
+ toolbar: { type: [Boolean, Object], default: false },
4866
4950
 
4867
4951
  /** Extra custom toolbar buttons: [{ key, icon, title, onClick(event, gridApi) }] */
4868
4952
  toolbarButtons: { type: Array, default: () => [] },
@@ -4877,7 +4961,14 @@ var script = {
4877
4961
  locked: { type: Boolean, default: false },
4878
4962
 
4879
4963
  /** Show "Column info" / "Row info" items in the context menu (dev/debug aid). */
4880
- devMode: { type: Boolean, default: false }
4964
+ devMode: { type: Boolean, default: false },
4965
+
4966
+ /**
4967
+ * Visual theme of the grid.
4968
+ * Built-in: 'material' | 'antd' | 'fluent' | 'carbon' | 'excel'
4969
+ * Default is Material Design 3. Use 'excel' for the classic Excel 365 look.
4970
+ */
4971
+ theme: { type: String, default: 'material' }
4881
4972
  },
4882
4973
 
4883
4974
  data() {
@@ -5047,6 +5138,16 @@ var script = {
5047
5138
 
5048
5139
  totalRows() { return this.data.length },
5049
5140
 
5141
+ // See the `toolbar` prop's JSDoc for the full Boolean|Object contract.
5142
+ _toolbarVisible() {
5143
+ if (typeof this.toolbar === 'boolean') return this.toolbar
5144
+ return !!this.toolbar && this.toolbar.visible !== false
5145
+ },
5146
+ _toolbarList() {
5147
+ if (this.toolbar && Array.isArray(this.toolbar.list)) return this.toolbar.list
5148
+ return DEFAULT_TOOLBAR_LIST
5149
+ },
5150
+
5050
5151
  _sizeConf() { return SIZE_CONFIG[this.internalSize] || SIZE_CONFIG.middle },
5051
5152
 
5052
5153
  sizeOptions() { return Object.keys(SIZE_CONFIG) },
@@ -7042,6 +7143,7 @@ var script = {
7042
7143
  },
7043
7144
 
7044
7145
  _onWindowScrollForDropdown(e) {
7146
+ if (this.contextMenu.visible) this.hideContextMenu();
7045
7147
  if (this.selectDropdown) {
7046
7148
  const comp = this.$refs.selectDropdownComp;
7047
7149
  const dd = comp && comp.$el ? comp.$el : null;
@@ -7750,27 +7852,41 @@ var script = {
7750
7852
  },
7751
7853
 
7752
7854
  ctxHideRow() {
7855
+ const targetRow = this.contextMenu.row;
7856
+ // Row header: row >= 0 AND col = -1 (no column context)
7857
+ const fromRowHeader = targetRow >= 0 && this.contextMenu.col < 0;
7753
7858
  this.hideContextMenu();
7754
7859
  if (this.internalLocked) return
7755
- const pr = this.primaryRange;
7756
- if (!pr) return
7757
- const r = normalizeRange(pr);
7758
7860
  const newHidden = [...this.hiddenRows];
7759
- for (let ri = r.startRow; ri <= r.endRow; ri++) {
7760
- if (!newHidden.includes(ri)) newHidden.push(ri);
7861
+ if (fromRowHeader) {
7862
+ if (!newHidden.includes(targetRow)) newHidden.push(targetRow);
7863
+ } else {
7864
+ const pr = this.primaryRange;
7865
+ if (!pr) return
7866
+ const r = normalizeRange(pr);
7867
+ for (let ri = r.startRow; ri <= r.endRow; ri++) {
7868
+ if (!newHidden.includes(ri)) newHidden.push(ri);
7869
+ }
7761
7870
  }
7762
7871
  this.$emit('update:hiddenRows', newHidden);
7763
7872
  },
7764
7873
 
7765
7874
  ctxHideColumn() {
7875
+ const targetCol = this.contextMenu.col;
7876
+ // Column header: col >= 0 AND row = -1 (no row context)
7877
+ const fromColHeader = targetCol >= 0 && this.contextMenu.row < 0;
7766
7878
  this.hideContextMenu();
7767
7879
  if (this.internalLocked) return
7768
- const pr = this.primaryRange;
7769
- if (!pr) return
7770
- const r = normalizeRange(pr);
7771
7880
  const newHidden = [...this.hiddenColumns];
7772
- for (let ci = r.startCol; ci <= r.endCol; ci++) {
7773
- if (!newHidden.includes(ci)) newHidden.push(ci);
7881
+ if (fromColHeader) {
7882
+ if (!newHidden.includes(targetCol)) newHidden.push(targetCol);
7883
+ } else {
7884
+ const pr = this.primaryRange;
7885
+ if (!pr) return
7886
+ const r = normalizeRange(pr);
7887
+ for (let ci = r.startCol; ci <= r.endCol; ci++) {
7888
+ if (!newHidden.includes(ci)) newHidden.push(ci);
7889
+ }
7774
7890
  }
7775
7891
  this.$emit('update:hiddenColumns', newHidden);
7776
7892
  },
@@ -7789,25 +7905,25 @@ var script = {
7789
7905
 
7790
7906
  ctxPinLeft() {
7791
7907
  const col = this.leafCols[this.contextMenu.col];
7792
- if (!col) { this.hideContextMenu(); return }
7793
- this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' });
7794
7908
  this.hideContextMenu();
7909
+ if (!col) return
7910
+ this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' });
7795
7911
  },
7796
7912
 
7797
7913
  ctxPinRight() {
7798
7914
  const col = this.leafCols[this.contextMenu.col];
7799
- if (!col) { this.hideContextMenu(); return }
7800
- this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' });
7801
7915
  this.hideContextMenu();
7916
+ if (!col) return
7917
+ this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' });
7802
7918
  },
7803
7919
 
7804
7920
  ctxUnpin() {
7805
7921
  const col = this.leafCols[this.contextMenu.col];
7806
- if (!col) { this.hideContextMenu(); return }
7922
+ this.hideContextMenu();
7923
+ if (!col) return
7807
7924
  const next = { ...this.pinnedColumns };
7808
7925
  delete next[col.key];
7809
7926
  this.$emit('update:pinnedColumns', next);
7810
- this.hideContextMenu();
7811
7927
  },
7812
7928
 
7813
7929
  isColPinned(ci) {
@@ -7831,16 +7947,17 @@ var script = {
7831
7947
  },
7832
7948
 
7833
7949
  ctxShowColumnInfo() {
7950
+ const ci = this.contextMenu.col;
7834
7951
  this.hideContextMenu();
7835
- const col = this.leafCols[this.contextMenu.col];
7952
+ const col = this.leafCols[ci];
7836
7953
  if (!col) return
7837
7954
  const { _flatIndex, ...rest } = col;
7838
7955
  this.infoModal = { title: `${this.t.columnInfo}: ${col.title || col.key}`, json: this._stringifyForInfo(rest) };
7839
7956
  },
7840
7957
 
7841
7958
  ctxShowRowInfo() {
7842
- this.hideContextMenu();
7843
7959
  const ri = this.contextMenu.row;
7960
+ this.hideContextMenu();
7844
7961
  const row = this._dataRaw[ri];
7845
7962
  if (!row) return
7846
7963
  this.infoModal = { title: `${this.t.rowInfo} #${ri + 1}`, json: this._stringifyForInfo(row) };
@@ -8293,6 +8410,10 @@ var script = {
8293
8410
  this.$emit('lock-change', { locked: this.internalLocked });
8294
8411
  },
8295
8412
 
8413
+ toolbarHas(key) {
8414
+ return this._toolbarList.indexOf(key) !== -1
8415
+ },
8416
+
8296
8417
  setSize(newSize) {
8297
8418
  if (!SIZE_CONFIG[newSize] || newSize === this.internalSize) return
8298
8419
  this.internalSize = newSize;
@@ -8359,18 +8480,19 @@ var __vue_render__ = function () {
8359
8480
  staticClass: "eg-shell",
8360
8481
  class: [
8361
8482
  "eg-size-" + _vm.internalSize,
8483
+ "eg-theme-" + _vm.theme,
8362
8484
  { "eg-shell-fullscreen": _vm.isFullscreen },
8363
8485
  ],
8364
8486
  style: _vm.shellStyle,
8365
8487
  },
8366
8488
  [
8367
- _vm.toolbar
8489
+ _vm._toolbarVisible
8368
8490
  ? _c(
8369
8491
  "div",
8370
8492
  { staticClass: "eg-toolbar" },
8371
8493
  [
8372
8494
  _c("div", { staticClass: "eg-toolbar-group" }, [
8373
- _vm.toolbarUndoRedo
8495
+ _vm.toolbarHas("undoRedo")
8374
8496
  ? _c(
8375
8497
  "button",
8376
8498
  {
@@ -8390,7 +8512,7 @@ var __vue_render__ = function () {
8390
8512
  )
8391
8513
  : _vm._e(),
8392
8514
  _vm._v(" "),
8393
- _vm.toolbarUndoRedo
8515
+ _vm.toolbarHas("undoRedo")
8394
8516
  ? _c(
8395
8517
  "button",
8396
8518
  {
@@ -8412,7 +8534,7 @@ var __vue_render__ = function () {
8412
8534
  ]),
8413
8535
  _vm._v(" "),
8414
8536
  _c("div", { staticClass: "eg-toolbar-group" }, [
8415
- _vm.toolbarExport
8537
+ _vm.toolbarHas("export")
8416
8538
  ? _c(
8417
8539
  "button",
8418
8540
  {
@@ -8428,7 +8550,7 @@ var __vue_render__ = function () {
8428
8550
  )
8429
8551
  : _vm._e(),
8430
8552
  _vm._v(" "),
8431
- _vm.toolbarImport
8553
+ _vm.toolbarHas("import")
8432
8554
  ? _c(
8433
8555
  "button",
8434
8556
  {
@@ -8448,7 +8570,7 @@ var __vue_render__ = function () {
8448
8570
  )
8449
8571
  : _vm._e(),
8450
8572
  _vm._v(" "),
8451
- _vm.toolbarImport
8573
+ _vm.toolbarHas("import")
8452
8574
  ? _c("input", {
8453
8575
  ref: "toolbarImportInput",
8454
8576
  staticStyle: { display: "none" },
@@ -8503,7 +8625,49 @@ var __vue_render__ = function () {
8503
8625
  _vm._v(" "),
8504
8626
  _c("div", { staticClass: "eg-toolbar-spacer" }),
8505
8627
  _vm._v(" "),
8506
- _vm.toolbarSize
8628
+ _vm.toolbarHas("theme")
8629
+ ? _c("div", { staticClass: "eg-toolbar-group" }, [
8630
+ _c(
8631
+ "select",
8632
+ {
8633
+ staticClass: "eg-toolbar-select",
8634
+ attrs: { title: _vm.t.theme },
8635
+ domProps: { value: _vm.theme },
8636
+ on: {
8637
+ change: function ($event) {
8638
+ return _vm.$emit(
8639
+ "update:theme",
8640
+ $event.target.value
8641
+ )
8642
+ },
8643
+ },
8644
+ },
8645
+ [
8646
+ _c("option", { attrs: { value: "excel" } }, [
8647
+ _vm._v("Excel"),
8648
+ ]),
8649
+ _vm._v(" "),
8650
+ _c("option", { attrs: { value: "material" } }, [
8651
+ _vm._v("Material"),
8652
+ ]),
8653
+ _vm._v(" "),
8654
+ _c("option", { attrs: { value: "antd" } }, [
8655
+ _vm._v("Ant Design"),
8656
+ ]),
8657
+ _vm._v(" "),
8658
+ _c("option", { attrs: { value: "fluent" } }, [
8659
+ _vm._v("Fluent"),
8660
+ ]),
8661
+ _vm._v(" "),
8662
+ _c("option", { attrs: { value: "carbon" } }, [
8663
+ _vm._v("Carbon"),
8664
+ ]),
8665
+ ]
8666
+ ),
8667
+ ])
8668
+ : _vm._e(),
8669
+ _vm._v(" "),
8670
+ _vm.toolbarHas("size")
8507
8671
  ? _c(
8508
8672
  "div",
8509
8673
  {
@@ -8547,7 +8711,7 @@ var __vue_render__ = function () {
8547
8711
  : _vm._e(),
8548
8712
  _vm._v(" "),
8549
8713
  _c("div", { staticClass: "eg-toolbar-group" }, [
8550
- _vm.toolbarLock
8714
+ _vm.toolbarHas("lock")
8551
8715
  ? _c(
8552
8716
  "button",
8553
8717
  {
@@ -8570,7 +8734,7 @@ var __vue_render__ = function () {
8570
8734
  )
8571
8735
  : _vm._e(),
8572
8736
  _vm._v(" "),
8573
- _vm.toolbarFullscreen
8737
+ _vm.toolbarHas("fullscreen")
8574
8738
  ? _c(
8575
8739
  "button",
8576
8740
  {
@@ -8605,7 +8769,7 @@ var __vue_render__ = function () {
8605
8769
  {
8606
8770
  ref: "wrapper",
8607
8771
  staticClass: "eg-wrapper",
8608
- class: "eg-size-" + _vm.size,
8772
+ class: "eg-size-" + _vm.internalSize,
8609
8773
  style: _vm.wrapperStyle,
8610
8774
  attrs: { tabindex: "0" },
8611
8775
  on: { keydown: _vm.onKeyDown, keypress: _vm.onKeyPress },
@@ -8750,23 +8914,27 @@ var __vue_render__ = function () {
8750
8914
  _vm._v(" "),
8751
8915
  _c("div", { staticClass: "eg-context-separator" }),
8752
8916
  _vm._v(" "),
8753
- _c(
8754
- "div",
8755
- {
8756
- staticClass: "eg-context-item",
8757
- on: { click: _vm.ctxHideRow },
8758
- },
8759
- [_vm._v(_vm._s(_vm.t.hideRows))]
8760
- ),
8917
+ _vm.contextMenu.row >= 0
8918
+ ? _c(
8919
+ "div",
8920
+ {
8921
+ staticClass: "eg-context-item",
8922
+ on: { click: _vm.ctxHideRow },
8923
+ },
8924
+ [_vm._v(_vm._s(_vm.t.hideRows))]
8925
+ )
8926
+ : _vm._e(),
8761
8927
  _vm._v(" "),
8762
- _c(
8763
- "div",
8764
- {
8765
- staticClass: "eg-context-item",
8766
- on: { click: _vm.ctxHideColumn },
8767
- },
8768
- [_vm._v(_vm._s(_vm.t.hideColumns))]
8769
- ),
8928
+ _vm.contextMenu.col >= 0
8929
+ ? _c(
8930
+ "div",
8931
+ {
8932
+ staticClass: "eg-context-item",
8933
+ on: { click: _vm.ctxHideColumn },
8934
+ },
8935
+ [_vm._v(_vm._s(_vm.t.hideColumns))]
8936
+ )
8937
+ : _vm._e(),
8770
8938
  _vm._v(" "),
8771
8939
  _c(
8772
8940
  "div",
@@ -11483,6 +11651,7 @@ var __vue_render__ = function () {
11483
11651
  "enable-search": _vm.selectDropdown.enableSearch,
11484
11652
  "trigger-rect": _vm.selectDropdown.rect,
11485
11653
  "dropdown-only": true,
11654
+ size: _vm.internalSize,
11486
11655
  },
11487
11656
  on: {
11488
11657
  select: _vm.onComponentSelect,
@@ -11503,6 +11672,7 @@ var __vue_render__ = function () {
11503
11672
  "enable-search": _vm.selectDropdown.enableSearch,
11504
11673
  "trigger-rect": _vm.selectDropdown.rect,
11505
11674
  "dropdown-only": true,
11675
+ size: _vm.internalSize,
11506
11676
  },
11507
11677
  on: {
11508
11678
  select: _vm.onComponentSelect,
@@ -11574,12 +11744,12 @@ __vue_render__._withStripped = true;
11574
11744
  /* style */
11575
11745
  const __vue_inject_styles__ = function (inject) {
11576
11746
  if (!inject) return
11577
- inject("data-v-02cbfa7c_0", { source: "\n.eg-shell[data-v-02cbfa7c] {\n /* ── Design tokens (Excel 365 defaults) — override on :root or any ancestor of <excel-grid> ──\n Full reference: docs/theming.md\n */\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}\n.eg-shell-fullscreen[data-v-02cbfa7c] {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n}\n.eg-wrapper[data-v-02cbfa7c] {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\n font-family: var(--eg-font-family);\n}\n.eg-toolbar[data-v-02cbfa7c] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n height: 32px;\n padding: 0 6px;\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-border);\n border-bottom: none;\n}\n.eg-toolbar-group[data-v-02cbfa7c] {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n.eg-toolbar-spacer[data-v-02cbfa7c] {\n flex: 1 1 auto;\n}\n.eg-toolbar-btn[data-v-02cbfa7c] {\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-02cbfa7c]: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-02cbfa7c]:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.eg-toolbar-btn-active[data-v-02cbfa7c] {\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-segmented[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] { width: 12px; height: 12px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon[data-v-02cbfa7c] { width: 15px; height: 15px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon[data-v-02cbfa7c] { width: 18px; height: 18px;\n}\n.eg-toolbar-icon[data-v-02cbfa7c] {\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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c]{ 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/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small[data-v-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] { 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-02cbfa7c] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c]:hover { background: var(--eg-color-border-cell);\n}\n.eg-header-group[data-v-02cbfa7c] { background: var(--eg-color-header-group-bg);\n}\n.eg-col-resize-handle[data-v-02cbfa7c] {\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-02cbfa7c] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-02cbfa7c] {\n border-top: 1px solid var(--eg-color-border);\n}\n.eg-frozen-col-footer[data-v-02cbfa7c],\n.eg-scroll-col-footer[data-v-02cbfa7c],\n.eg-pinned-right-footer[data-v-02cbfa7c] {\n position: relative;\n}\n.eg-footer-cell[data-v-02cbfa7c] {\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-02cbfa7c] {\n position: relative;\n}\n.eg-cell[data-v-02cbfa7c] {\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-cell-value[data-v-02cbfa7c] {\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-02cbfa7c] { justify-content: flex-end;\n}\n.eg-cell-align-center[data-v-02cbfa7c] { justify-content: center;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-02cbfa7c]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-02cbfa7c] {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-02cbfa7c] {\n background: var(--eg-color-cell-selected) !important;\n}\n.eg-cell-fill-preview[data-v-02cbfa7c] {\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-02cbfa7c] {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-02cbfa7c] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-02cbfa7c] {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\n position: fixed;\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 z-index: 9999;\n min-width: 160px;\n font-size: var(--eg-font-size);\n padding: 4px 0;\n}\n.eg-context-item[data-v-02cbfa7c] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n.eg-context-item[data-v-02cbfa7c]:hover { background: var(--eg-color-header-bg);\n}\n.eg-context-separator[data-v-02cbfa7c] {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-02cbfa7c]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-02cbfa7c] {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\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 min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n}\n.eg-context-submenu-parent:hover .eg-context-submenu[data-v-02cbfa7c] { display: block;\n}\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c]:hover { color: var(--eg-color-text);\n}\n.eg-info-modal-body[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] { background: var(--eg-color-group-bg-collapsed);\n}\n.eg-drag-ghost[data-v-02cbfa7c] {\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-02cbfa7c] {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source[data-v-02cbfa7c] {\n opacity: 0.4;\n}\n.eg-col-drag-ghost[data-v-02cbfa7c] {\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-02cbfa7c] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-02cbfa7c]::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-02cbfa7c] {\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-02cbfa7c]::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-02cbfa7c] {\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-02cbfa7c] {\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-import-dialog[data-v-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\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-02cbfa7c] {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-02cbfa7c] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner[data-v-02cbfa7c] {\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-02cbfa7c 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-02cbfa7c {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-02cbfa7c] {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-02cbfa7c] {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active[data-v-02cbfa7c] {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-02cbfa7c] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-02cbfa7c]::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-02cbfa7c] {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap[data-v-02cbfa7c] {\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-02cbfa7c] { justify-content: flex-end;\n}\n.eg-cell-align-center .eg-cell-action-wrap[data-v-02cbfa7c] { justify-content: center;\n}\n.eg-validation-tooltip[data-v-02cbfa7c] {\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-02cbfa7c]: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-02cbfa7c]: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-02cbfa7c] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-02cbfa7c] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-02cbfa7c] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-02cbfa7c] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-02cbfa7c] {\n font-size: 10px;\n}\n.eg-size-small .eg-select-option[data-v-02cbfa7c] {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-small .eg-context-menu[data-v-02cbfa7c],\n.eg-size-small .eg-context-item[data-v-02cbfa7c] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-02cbfa7c] {\n padding: 4px 14px;\n}\n.eg-size-small .eg-toolbar[data-v-02cbfa7c] {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn[data-v-02cbfa7c] {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon[data-v-02cbfa7c] {\n width: 13px;\n height: 13px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-02cbfa7c] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-02cbfa7c] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-02cbfa7c] {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell[data-v-02cbfa7c] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-02cbfa7c] {\n font-size: 12px;\n}\n.eg-size-large .eg-select-option[data-v-02cbfa7c] {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-size-large .eg-context-menu[data-v-02cbfa7c],\n.eg-size-large .eg-context-item[data-v-02cbfa7c] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-02cbfa7c] {\n padding: 8px 18px;\n}\n.eg-size-large .eg-toolbar[data-v-02cbfa7c] {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn[data-v-02cbfa7c] {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon[data-v-02cbfa7c] {\n width: 18px;\n height: 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA4sIA;EACA;;GAEA;;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;AACA;AAEA;EACA,eAAA;EACA,QAAA;EACA,aAAA;EACA,mCAAA;AACA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,gBAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,kCAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,cAAA;EACA,YAAA;EACA,cAAA;EACA,sBAAA;EACA,qCAAA;EACA,wCAAA;EACA,mBAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;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,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,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,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;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,mCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,aAAA;EACA,gBAAA;EACA,8BAAA;EACA,cAAA;AACA;AAEA;EACA,iBAAA;EACA,eAAA;EACA,kBAAA;EACA,mBAAA;AACA;AAEA,0CAAA,qCAAA;AAAA;AAEA;EACA,WAAA;EACA,uCAAA;EACA,aAAA;AACA;AAEA,qDAAA,aAAA,EAAA,YAAA;AAAA;AAEA;EACA,aAAA;EACA,kBAAA;EACA,UAAA;EACA,SAAA;EACA,mCAAA;EACA,wCAAA;EACA,+BAAA;EACA,qCAAA;EACA,gBAAA;EACA,cAAA;EACA,cAAA;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,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;EACA,eAAA;EACA,iBAAA;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;;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;EACA,eAAA;EACA,iBAAA;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","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, { 'eg-shell-fullscreen': isFullscreen }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"toolbar\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarUndoRedo\"\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=\"toolbarUndoRedo\"\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=\"toolbarExport\"\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=\"toolbarImport\"\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=\"toolbarImport\"\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=\"toolbarSize\" 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=\"toolbarLock\"\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=\"toolbarFullscreen\"\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-' + size\"\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 class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div 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) + '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 <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 </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) + '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 <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 </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) + '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 <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 </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 <!-- 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 @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 @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 },\n middle: { row: 28, header: 28, footer: 28 },\n large: { row: 36, header: 36, footer: 36 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\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 /** Height of the grid container in px */\n height: { type: Number, default: 400 },\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 /** Show the built-in toolbar above the grid. Controlled entirely by the parent — no in-component toggle. */\n toolbar: { type: Boolean, default: false },\n\n /** Show the fullscreen toggle button in the toolbar. */\n toolbarFullscreen: { type: Boolean, default: true },\n\n /** Show the size (small/middle/large) selector in the toolbar. */\n toolbarSize: { type: Boolean, default: true },\n\n /** Show the lock toggle button in the toolbar. */\n toolbarLock: { type: Boolean, default: true },\n\n /** Show the undo/redo buttons in the toolbar. */\n toolbarUndoRedo: { type: Boolean, default: true },\n\n /** Show the export button in the toolbar (calls exportToExcel() with no options). */\n toolbarExport: { type: Boolean, default: true },\n\n /** Show the import button in the toolbar (opens a file picker, calls importFromFile()). */\n toolbarImport: { type: Boolean, default: true },\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 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 // 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)\n internalLocked: false,\n internalSize: 'middle',\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 // 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)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(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 _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\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 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 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 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.height + '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) + '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 internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\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.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.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 },\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 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 updateVirtualScroll() {\n const footerH = this.hasFooter ? this._footerRowH : 0\n const containerH = this.height - (this._headerRowH * this.headerDepth) - footerH\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(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 const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\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 const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(-visibleRows, 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(visibleRows, 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 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 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.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 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 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 }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\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 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 },\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 this.hideContextMenu()\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n const newHidden = [...this.hiddenRows]\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n this.hideContextMenu()\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n const newHidden = [...this.hiddenColumns]\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\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 if (!col) { this.hideContextMenu(); return }\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n this.hideContextMenu()\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n if (!col) { this.hideContextMenu(); return }\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n this.hideContextMenu()\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n if (!col) { this.hideContextMenu(); return }\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n this.hideContextMenu()\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 this.hideContextMenu()\n const col = this.leafCols[this.contextMenu.col]\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 this.hideContextMenu()\n const ri = this.contextMenu.row\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 if (proceed) {\n const oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n const { newData } = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci))\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\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 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 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\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}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\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: 32px;\n padding: 0 6px;\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-border);\n border-bottom: none;\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\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-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/* 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-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-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-cell-bg);\n border: 1px solid var(--eg-color-border);\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}\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 { background: var(--eg-color-header-bg); }\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-cell-bg);\n border: 1px solid var(--eg-color-border);\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}\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-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-select-option {\n font-size: 12px;\n padding: 4px 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\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-select-option {\n font-size: 14px;\n padding: 8px 14px;\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</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.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-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 })
11578
- ,inject("data-v-02cbfa7c_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/* ── 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.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-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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0/JA,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;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,yDAAA,2DAAA,EAAA,uDAAA;AAAA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, { 'eg-shell-fullscreen': isFullscreen }]\"\n :style=\"shellStyle\"\n >\n <!-- Toolbar -->\n <div v-if=\"toolbar\" class=\"eg-toolbar\">\n <div class=\"eg-toolbar-group\">\n <button\n v-if=\"toolbarUndoRedo\"\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=\"toolbarUndoRedo\"\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=\"toolbarExport\"\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=\"toolbarImport\"\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=\"toolbarImport\"\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=\"toolbarSize\" 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=\"toolbarLock\"\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=\"toolbarFullscreen\"\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-' + size\"\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 class=\"eg-context-item\" @click=\"ctxHideRow\">{{ t.hideRows }}</div>\n <div 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) + '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 <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 </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) + '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 <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 </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) + '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 <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 </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 <!-- 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 @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 @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 },\n middle: { row: 28, header: 28, footer: 28 },\n large: { row: 36, header: 36, footer: 36 }\n}\nconst ROW_HEADER_WIDTH = 50\nconst DEFAULT_COL_WIDTH = 100\nconst OUTLINE_GUTTER = 20\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 /** Height of the grid container in px */\n height: { type: Number, default: 400 },\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 /** Show the built-in toolbar above the grid. Controlled entirely by the parent — no in-component toggle. */\n toolbar: { type: Boolean, default: false },\n\n /** Show the fullscreen toggle button in the toolbar. */\n toolbarFullscreen: { type: Boolean, default: true },\n\n /** Show the size (small/middle/large) selector in the toolbar. */\n toolbarSize: { type: Boolean, default: true },\n\n /** Show the lock toggle button in the toolbar. */\n toolbarLock: { type: Boolean, default: true },\n\n /** Show the undo/redo buttons in the toolbar. */\n toolbarUndoRedo: { type: Boolean, default: true },\n\n /** Show the export button in the toolbar (calls exportToExcel() with no options). */\n toolbarExport: { type: Boolean, default: true },\n\n /** Show the import button in the toolbar (opens a file picker, calls importFromFile()). */\n toolbarImport: { type: Boolean, default: true },\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 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 // 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)\n internalLocked: false,\n internalSize: 'middle',\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 // 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)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(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 _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\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 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 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 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.height + '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) + '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 internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\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.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.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 },\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 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 updateVirtualScroll() {\n const footerH = this.hasFooter ? this._footerRowH : 0\n const containerH = this.height - (this._headerRowH * this.headerDepth) - footerH\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(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 const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\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 const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(-visibleRows, 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(visibleRows, 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 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 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.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 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 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 }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\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 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 },\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 this.hideContextMenu()\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n const newHidden = [...this.hiddenRows]\n for (let ri = r.startRow; ri <= r.endRow; ri++) {\n if (!newHidden.includes(ri)) newHidden.push(ri)\n }\n this.$emit('update:hiddenRows', newHidden)\n },\n\n ctxHideColumn() {\n this.hideContextMenu()\n if (this.internalLocked) return\n const pr = this.primaryRange\n if (!pr) return\n const r = normalizeRange(pr)\n const newHidden = [...this.hiddenColumns]\n for (let ci = r.startCol; ci <= r.endCol; ci++) {\n if (!newHidden.includes(ci)) newHidden.push(ci)\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 if (!col) { this.hideContextMenu(); return }\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'left' })\n this.hideContextMenu()\n },\n\n ctxPinRight() {\n const col = this.leafCols[this.contextMenu.col]\n if (!col) { this.hideContextMenu(); return }\n this.$emit('update:pinnedColumns', { ...this.pinnedColumns, [col.key]: 'right' })\n this.hideContextMenu()\n },\n\n ctxUnpin() {\n const col = this.leafCols[this.contextMenu.col]\n if (!col) { this.hideContextMenu(); return }\n const next = { ...this.pinnedColumns }\n delete next[col.key]\n this.$emit('update:pinnedColumns', next)\n this.hideContextMenu()\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 this.hideContextMenu()\n const col = this.leafCols[this.contextMenu.col]\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 this.hideContextMenu()\n const ri = this.contextMenu.row\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 if (proceed) {\n const oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n const { newData } = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci))\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\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 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 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\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}\n\n.eg-shell-fullscreen {\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: var(--eg-color-cell-bg);\n}\n\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid var(--eg-color-border);\n border-radius: var(--eg-radius);\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: 32px;\n padding: 0 6px;\n box-sizing: border-box;\n background: var(--eg-color-header-bg);\n border: 1px solid var(--eg-color-border);\n border-bottom: none;\n}\n\n.eg-toolbar-group {\n display: flex;\n align-items: center;\n gap: 2px;\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-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/* 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-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-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-cell-bg);\n border: 1px solid var(--eg-color-border);\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}\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 { background: var(--eg-color-header-bg); }\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-cell-bg);\n border: 1px solid var(--eg-color-border);\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}\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-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-select-option {\n font-size: 12px;\n padding: 4px 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\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-select-option {\n font-size: 14px;\n padding: 8px 14px;\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</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.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-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 });
11747
+ inject("data-v-73ec7494_0", { source: "\n.eg-shell[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n}\n.eg-toolbar-group + .eg-toolbar-group[data-v-73ec7494] {\n border-left: 1px solid var(--eg-color-border);\n}\n.eg-toolbar-spacer[data-v-73ec7494] {\n flex: 1 1 auto;\n}\n.eg-toolbar-btn[data-v-73ec7494] {\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-73ec7494]: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-73ec7494]:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.eg-toolbar-btn-active[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494]:hover {\n border-color: var(--eg-color-accent);\n}\n.eg-toolbar-select[data-v-73ec7494]: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-73ec7494] {\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-73ec7494] {\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-73ec7494] { width: 12px; height: 12px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-middle .eg-toolbar-icon[data-v-73ec7494] { width: 15px; height: 15px;\n}\n.eg-toolbar-segmented .eg-toolbar-segment-large .eg-toolbar-icon[data-v-73ec7494] { width: 18px; height: 18px;\n}\n.eg-toolbar-icon[data-v-73ec7494] {\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-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494]{ 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/* Size segmented control — each icon shows row density: tight/thin bars → small, wide/thick bars → large */\n.eg-icon-size-small[data-v-73ec7494] { 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-73ec7494] { 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-73ec7494] { 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-73ec7494] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494]:hover { background: var(--eg-color-border-cell);\n}\n.eg-header-group[data-v-73ec7494] { background: var(--eg-color-header-group-bg);\n}\n.eg-col-resize-handle[data-v-73ec7494] {\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-73ec7494] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-73ec7494] {\n border-top: 1px solid var(--eg-color-border);\n}\n.eg-frozen-col-footer[data-v-73ec7494],\n.eg-scroll-col-footer[data-v-73ec7494],\n.eg-pinned-right-footer[data-v-73ec7494] {\n position: relative;\n}\n.eg-footer-cell[data-v-73ec7494] {\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-73ec7494] {\n position: relative;\n}\n.eg-cell[data-v-73ec7494] {\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-cell-value[data-v-73ec7494] {\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-73ec7494] { justify-content: flex-end;\n}\n.eg-cell-align-center[data-v-73ec7494] { justify-content: center;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-73ec7494]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-73ec7494] {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-73ec7494] {\n background: var(--eg-color-cell-selected) !important;\n}\n.eg-cell-fill-preview[data-v-73ec7494] {\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-73ec7494] {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-73ec7494] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-73ec7494] {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n white-space: nowrap;\n}\n.eg-context-item[data-v-73ec7494]:hover {\n background: var(--eg-color-option-hover-bg);\n color: var(--eg-color-text);\n}\n.eg-context-separator[data-v-73ec7494] {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-73ec7494]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-73ec7494] {\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-73ec7494] { display: block;\n}\n\n/* devMode — column/row info modal */\n.eg-info-modal-backdrop[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494]:hover { color: var(--eg-color-text);\n}\n.eg-info-modal-body[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] { background: var(--eg-color-group-bg-collapsed);\n}\n.eg-drag-ghost[data-v-73ec7494] {\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-73ec7494] {\n opacity: 0.5;\n outline: 1px solid var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell--drag-source[data-v-73ec7494] {\n opacity: 0.4;\n}\n.eg-col-drag-ghost[data-v-73ec7494] {\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-73ec7494] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-73ec7494]::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-73ec7494] {\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-73ec7494]::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-73ec7494] {\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-73ec7494] {\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-import-dialog[data-v-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\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-73ec7494] {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-73ec7494] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner[data-v-73ec7494] {\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-73ec7494 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-73ec7494 {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-73ec7494] {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-73ec7494] {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active[data-v-73ec7494] {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-73ec7494] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-73ec7494]::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-73ec7494] {\n justify-content: center;\n padding: 0 2px;\n}\n\n/* ===== ACTION CELL ===== */\n.eg-cell-action-wrap[data-v-73ec7494] {\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-73ec7494] { justify-content: flex-end;\n}\n.eg-cell-align-center .eg-cell-action-wrap[data-v-73ec7494] { justify-content: center;\n}\n.eg-validation-tooltip[data-v-73ec7494] {\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-73ec7494]: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-73ec7494]: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-73ec7494] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-73ec7494] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-73ec7494] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-73ec7494] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-73ec7494] {\n font-size: 10px;\n}\n.eg-size-small .eg-context-menu[data-v-73ec7494],\n.eg-size-small .eg-context-item[data-v-73ec7494] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-73ec7494] {\n padding: 4px 14px;\n}\n.eg-size-small .eg-toolbar[data-v-73ec7494] {\n height: 28px;\n}\n.eg-size-small .eg-toolbar-btn[data-v-73ec7494] {\n width: 22px;\n height: 22px;\n}\n.eg-size-small .eg-toolbar-icon[data-v-73ec7494] {\n width: 13px;\n height: 13px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-73ec7494] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-73ec7494] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-73ec7494] {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell[data-v-73ec7494] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-73ec7494] {\n font-size: 12px;\n}\n.eg-size-large .eg-context-menu[data-v-73ec7494],\n.eg-size-large .eg-context-item[data-v-73ec7494] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-73ec7494] {\n padding: 8px 18px;\n}\n.eg-size-large .eg-toolbar[data-v-73ec7494] {\n height: 40px;\n}\n.eg-size-large .eg-toolbar-btn[data-v-73ec7494] {\n width: 32px;\n height: 32px;\n}\n.eg-size-large .eg-toolbar-icon[data-v-73ec7494] {\n width: 18px;\n height: 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA8xIA;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,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,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;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,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;;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","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + theme, { 'eg-shell-fullscreen': isFullscreen }]\"\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=\"theme\"\n :title=\"t.theme\"\n @change=\"$emit('update:theme', $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) + '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 <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 </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) + '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 <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 </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) + '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 <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 </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 <!-- 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 },\n middle: { row: 28, header: 28, footer: 28 },\n large: { row: 36, header: 36, footer: 36 }\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']\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 /** Height of the grid container in px */\n height: { type: Number, default: 400 },\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: 'material' | 'antd' | 'fluent' | 'carbon' | 'excel'\n * Default is Material Design 3. Use 'excel' for the classic Excel 365 look.\n */\n theme: { type: String, default: 'material' }\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 // 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)\n internalLocked: false,\n internalSize: 'middle',\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 // 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)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(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\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 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 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 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.height + '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) + '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 internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\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.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.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 },\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 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 updateVirtualScroll() {\n const footerH = this.hasFooter ? this._footerRowH : 0\n const containerH = this.height - (this._headerRowH * this.headerDepth) - footerH\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(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 const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\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 const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(-visibleRows, 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(visibleRows, 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 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 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 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 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 }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\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 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 },\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 if (proceed) {\n const oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n const { newData } = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci))\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\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 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 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/* 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-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-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-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\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</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-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 })
11748
+ ,inject("data-v-73ec7494_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/* ── 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-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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA80KA,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,yDAAA,2DAAA,EAAA,uDAAA;AAAA","file":"ExcelGrid.vue","sourcesContent":["<template>\n <div\n class=\"eg-shell\"\n :class=\"['eg-size-' + internalSize, 'eg-theme-' + theme, { 'eg-shell-fullscreen': isFullscreen }]\"\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=\"theme\"\n :title=\"t.theme\"\n @change=\"$emit('update:theme', $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) + '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 <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 </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) + '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 <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 </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) + '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 <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 </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 <!-- 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 },\n middle: { row: 28, header: 28, footer: 28 },\n large: { row: 36, header: 36, footer: 36 }\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']\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 /** Height of the grid container in px */\n height: { type: Number, default: 400 },\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: 'material' | 'antd' | 'fluent' | 'carbon' | 'excel'\n * Default is Material Design 3. Use 'excel' for the classic Excel 365 look.\n */\n theme: { type: String, default: 'material' }\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 // 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)\n internalLocked: false,\n internalSize: 'middle',\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 // 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)) },\n pinnedRightCols() { return this.leafCols.filter(col => this.pinnedRightSet.has(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\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 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 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 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.height + '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) + '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 internalSize() {\n this.rowHeights = {}\n this.$nextTick(() => this.updateVirtualScroll())\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.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.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 },\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 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 updateVirtualScroll() {\n const footerH = this.hasFooter ? this._footerRowH : 0\n const containerH = this.height - (this._headerRowH * this.headerDepth) - footerH\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(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 const el = this.$refs.scrollBody\n if (!el) return\n if (this.headerViewportWidth !== el.clientWidth) this.headerViewportWidth = el.clientWidth\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 const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(-visibleRows, 0, e)\n return\n }\n if (key === 'PageDown') {\n e.preventDefault()\n const visibleRows = Math.floor((this.height - this._headerRowH * this.headerDepth) / this._rowH)\n this.moveActive(visibleRows, 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 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 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 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 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 }\n }\n\n this.fillDrag.active = false\n this.fillDrag.sourceRange = null\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 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 },\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 if (proceed) {\n const oldData = this.data\n const matrix = extractMatrix(oldData, this.leafCols, sourceRange)\n const { newData } = applyMatrix(oldData, this.leafCols, targetRow, targetCol, matrix, 'all', (ri, ci) => this.isCellEditable(ri, ci))\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\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 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 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/* 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-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-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-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\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</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-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 });
11579
11749
 
11580
11750
  };
11581
11751
  /* scoped */
11582
- const __vue_scope_id__ = "data-v-02cbfa7c";
11752
+ const __vue_scope_id__ = "data-v-73ec7494";
11583
11753
  /* module identifier */
11584
11754
  const __vue_module_identifier__ = undefined;
11585
11755
  /* functional template */