vue2-excel-grid 1.1.24 → 1.1.26

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.
@@ -1973,7 +1973,7 @@ __vue_render__$2._withStripped = true;
1973
1973
  /* style */
1974
1974
  const __vue_inject_styles__$2 = function (inject) {
1975
1975
  if (!inject) return
1976
- inject("data-v-76c9c032_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/* Standalone wrapper */\n.eg-select-wrapper {\n position: relative;\n display: inline-block;\n min-width: 120px;\n font-size: 13px;\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 #d1d5db;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n user-select: none;\n color: #333;\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover {\n border-color: #1a73e8;\n}\n.eg-select-trigger--open {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-trigger--disabled {\n background: #f5f5f5;\n cursor: default;\n color: #aaa;\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: #aaa;\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: #888;\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\n background: #fff;\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\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 #ebebeb;\n position: sticky;\n top: 0;\n background: #fff;\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #d1d5db;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n outline: none;\n color: #333;\n background: #fff;\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\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: 13px;\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: #333;\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted {\n background: #f0f4ff;\n}\n.eg-select-option--active {\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: #e8f0fe;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+OA,uBAAA;AACA;EACA,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,eAAA;EACA,oBAAA;AACA;AACA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,gBAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,iBAAA;EACA,WAAA;EACA,wDAAA;AACA;AACA;EACA,qBAAA;AACA;AACA;EACA,qBAAA;EACA,2CAAA;AACA;AACA;EACA,mBAAA;EACA,eAAA;EACA,WAAA;AACA;AACA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;AACA;AACA;EACA,WAAA;AACA;AACA;EACA,cAAA;EACA,WAAA;EACA,eAAA;AACA;;AAEA,mBAAA;AACA;EACA,gBAAA;EACA,yBAAA;EACA,kBAAA;EACA,uCAAA;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,gCAAA;EACA,gBAAA;EACA,MAAA;EACA,gBAAA;EACA,UAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,aAAA;EACA,WAAA;EACA,gBAAA;EACA,oBAAA;AACA;AACA;EACA,qBAAA;EACA,2CAAA;AACA;;AAEA,gBAAA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,WAAA;AACA;AACA;;EAEA,mBAAA;AACA;AACA;EACA,gBAAA;EACA,cAAA;AACA;AACA;;EAEA,mBAAA;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 }\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 this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)\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 },\n onSearchKey(e) {\n const key = e.key\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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n } else if (key === 'Enter') {\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 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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\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: 13px;\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 #d1d5db;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n user-select: none;\n color: #333;\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover {\n border-color: #1a73e8;\n}\n.eg-select-trigger--open {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-trigger--disabled {\n background: #f5f5f5;\n cursor: default;\n color: #aaa;\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: #aaa;\n}\n.eg-select-caret {\n flex-shrink: 0;\n color: #888;\n font-size: 12px;\n}\n\n/* Dropdown panel */\n.eg-select-dropdown {\n background: #fff;\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\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 #ebebeb;\n position: sticky;\n top: 0;\n background: #fff;\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #d1d5db;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n outline: none;\n color: #333;\n background: #fff;\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\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: 13px;\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: #333;\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted {\n background: #f0f4ff;\n}\n.eg-select-option--active {\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover {\n background: #e8f0fe;\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 });
1976
+ inject("data-v-89ca98c6_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/* 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+OA,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 }\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 this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v)\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 },\n onSearchKey(e) {\n const key = e.key\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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n } else if (key === 'Enter') {\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 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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\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 });
1977
1977
 
1978
1978
  };
1979
1979
  /* scoped */
@@ -2613,7 +2613,7 @@ __vue_render__$1._withStripped = true;
2613
2613
  /* style */
2614
2614
  const __vue_inject_styles__$1 = function (inject) {
2615
2615
  if (!inject) return
2616
- inject("data-v-63651711_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/* 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: #888;\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+UA,gBAAA;AACA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,WAAA;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 }\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 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 },\n onSearchKey(e) {\n const key = e.key\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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n } else if (key === 'Enter') {\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 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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\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 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: #888;\n margin-top: 1px;\n}\n.eg-tree-toggle--leaf {\n visibility: hidden;\n}\n</style>\n"]}, media: undefined });
2616
+ inject("data-v-7844aec6_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/* 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+UA,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 }\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 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 },\n onSearchKey(e) {\n const key = e.key\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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\n } else if (key === 'Enter') {\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 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.filteredOptions.length - 1)\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.highlight = Math.max((this.highlight || 0) - 1, 0)\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 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 });
2617
2617
 
2618
2618
  };
2619
2619
  /* scoped */
@@ -2739,9 +2739,6 @@ var script = {
2739
2739
  /** Visual size of the grid. Affects row/header height and font sizes. */
2740
2740
  size: { type: String, default: 'middle' },
2741
2741
 
2742
- /** Enable CSS list transitions on row enter/leave (data changes). Disabled during scroll. */
2743
- transitionEnabled: { type: Boolean, default: false },
2744
-
2745
2742
  /** Auto-fit all column widths to content (same as double-clicking each resize handle). Re-applies on data/columns change. */
2746
2743
  autoWidth: { type: Boolean, default: false }
2747
2744
  },
@@ -2781,9 +2778,6 @@ var script = {
2781
2778
  importState: { active: false, percent: 0, message: '' },
2782
2779
  exportState: { active: false },
2783
2780
 
2784
- // Resize state
2785
- _isScrolling: false,
2786
-
2787
2781
  resizing: {
2788
2782
  active: false,
2789
2783
  type: null, // 'col' | 'row'
@@ -2887,10 +2881,6 @@ var script = {
2887
2881
 
2888
2882
  totalRows() { return this.data.length },
2889
2883
 
2890
- _rowTransitionName() {
2891
- return this.transitionEnabled && !this._isScrolling ? 'eg-row' : ''
2892
- },
2893
-
2894
2884
  _sizeConf() { return SIZE_CONFIG[this.size] || SIZE_CONFIG.middle },
2895
2885
  _rowH() { return this._sizeConf.row },
2896
2886
  _headerRowH() { return this._sizeConf.header },
@@ -3094,7 +3084,8 @@ var script = {
3094
3084
  return {
3095
3085
  flex: 1,
3096
3086
  overflow: 'auto',
3097
- position: 'relative'
3087
+ position: 'relative',
3088
+ overflowAnchor: 'none' /* prevent Chrome scroll anchoring from fighting virtual scroll translateY */
3098
3089
  }
3099
3090
  },
3100
3091
 
@@ -3160,7 +3151,7 @@ var script = {
3160
3151
  },
3161
3152
 
3162
3153
  scrollRowHeadersStyle() {
3163
- return { flex: 1, overflow: 'hidden', position: 'relative' }
3154
+ return { flex: 1, overflow: 'hidden', position: 'relative', overflowAnchor: 'none' }
3164
3155
  },
3165
3156
 
3166
3157
  footerAreaStyle() {
@@ -3350,7 +3341,6 @@ var script = {
3350
3341
  document.removeEventListener('mousedown', this._onDocMouseDownForDropdown, true);
3351
3342
  window.removeEventListener('resize', this.syncHeaderViewportWidth);
3352
3343
  window.removeEventListener('scroll', this._onWindowScrollForDropdown, true);
3353
- clearTimeout(this._scrollEndTimer);
3354
3344
  },
3355
3345
 
3356
3346
  methods: {
@@ -3360,9 +3350,11 @@ var script = {
3360
3350
  updateVirtualScroll() {
3361
3351
  const footerH = this.hasFooter ? this._footerRowH : 0;
3362
3352
  const containerH = this.height - (this._headerRowH * this.headerDepth) - footerH;
3363
- // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll
3353
+ // Use binary search against the Vue-cached cumulativeHeights → O(log n) per scroll.
3354
+ // overscan=8 (≈224px at default row height) gives enough buffer for fast scroll
3355
+ // without blank-band artifacts between rAF updates.
3364
3356
  const result = findVisibleRange(
3365
- this.scrollTop, Math.max(containerH, 100), this.totalRows, this.cumulativeHeights
3357
+ this.scrollTop, Math.max(containerH, 100), this.totalRows, this.cumulativeHeights, 8
3366
3358
  );
3367
3359
  this.virtualStartIndex = result.startIndex;
3368
3360
  this.virtualEndIndex = result.endIndex;
@@ -3380,19 +3372,17 @@ var script = {
3380
3372
  // Close dropdown on scroll — fixed-position dropdown drifts away from the moving cell
3381
3373
  if (this.selectDropdown) this.closeSelectDropdown();
3382
3374
 
3383
- // Suppress row transitions during scroll to avoid animation on every virtual-scroll paint
3384
- if (this.transitionEnabled) {
3385
- this._isScrolling = true;
3386
- clearTimeout(this._scrollEndTimer);
3387
- this._scrollEndTimer = setTimeout(() => { this._isScrolling = false; }, 150);
3388
- }
3389
-
3390
- // Throttle reactive state updates + virtual scroll recalc to one rAF per frame
3375
+ // Throttle reactive state updates + virtual scroll recalc to one rAF per frame.
3376
+ // Read scrollTop from the DOM ref at rAF time (not from the stale event closure)
3377
+ // so that multiple scroll events arriving between frames still resolve to the
3378
+ // final position — eliminates the flickering caused by render/scroll mismatch.
3391
3379
  if (!this._scrollRAF) {
3392
3380
  this._scrollRAF = requestAnimationFrame(() => {
3393
3381
  this._scrollRAF = null;
3394
- this.scrollTop = target.scrollTop;
3395
- this.scrollLeft = target.scrollLeft;
3382
+ const sb = this.$refs.scrollBody;
3383
+ if (!sb) return
3384
+ this.scrollTop = sb.scrollTop;
3385
+ this.scrollLeft = sb.scrollLeft;
3396
3386
  this.updateVirtualScroll();
3397
3387
  });
3398
3388
  }
@@ -3518,16 +3508,10 @@ var script = {
3518
3508
  width = this.getColW(cell.column._flatIndex);
3519
3509
  }
3520
3510
  return {
3521
- position: 'absolute',
3522
3511
  top: (this._headerRowH * rowIndex) + 'px',
3523
3512
  left: left + 'px',
3524
3513
  width: width + 'px',
3525
- height: (this._headerRowH * cell.rowspan) + 'px',
3526
- display: 'flex', alignItems: 'center', justifyContent: 'center',
3527
- boxSizing: 'border-box',
3528
- borderRight: '1px solid #ccc', borderBottom: '1px solid #ccc',
3529
- background: '#f0f0f0', fontSize: '12px', fontWeight: 600,
3530
- overflow: 'hidden', whiteSpace: 'nowrap', userSelect: 'none', cursor: 'pointer'
3514
+ height: (this._headerRowH * cell.rowspan) + 'px'
3531
3515
  }
3532
3516
  },
3533
3517
 
@@ -3694,24 +3678,10 @@ var script = {
3694
3678
  }
3695
3679
 
3696
3680
  return {
3697
- position: 'absolute',
3698
3681
  top: (this._headerRowH * rowIndex) + 'px',
3699
3682
  left: left + 'px',
3700
3683
  width: width + 'px',
3701
- height: (this._headerRowH * cell.rowspan) + 'px',
3702
- display: 'flex',
3703
- alignItems: 'center',
3704
- justifyContent: 'center',
3705
- boxSizing: 'border-box',
3706
- borderRight: '1px solid #ccc',
3707
- borderBottom: '1px solid #ccc',
3708
- background: '#f0f0f0',
3709
- fontSize: '12px',
3710
- fontWeight: 600,
3711
- overflow: 'hidden',
3712
- whiteSpace: 'nowrap',
3713
- userSelect: 'none',
3714
- cursor: 'pointer'
3684
+ height: (this._headerRowH * cell.rowspan) + 'px'
3715
3685
  }
3716
3686
  },
3717
3687
 
@@ -6133,6 +6103,8 @@ var __vue_render__ = function () {
6133
6103
  style: {
6134
6104
  transform:
6135
6105
  "translateY(" + _vm.virtualOffsetTop + "px)",
6106
+ willChange: "transform",
6107
+ contain: "layout style",
6136
6108
  },
6137
6109
  },
6138
6110
  [
@@ -6276,391 +6248,361 @@ var __vue_render__ = function () {
6276
6248
  "translateY(" +
6277
6249
  _vm.virtualOffsetTop +
6278
6250
  "px)",
6251
+ willChange: "transform",
6252
+ contain: "layout style",
6279
6253
  },
6280
6254
  },
6281
6255
  [
6282
6256
  _vm._l(_vm.visibleRowRange, function (ri) {
6283
6257
  return [
6284
- _c(
6285
- "transition",
6286
- {
6287
- key: "tr-fc-" + ri,
6288
- attrs: {
6289
- name: _vm._rowTransitionName,
6290
- },
6291
- },
6292
- [
6293
- !_vm.isRowHidden(ri)
6294
- ? _c(
6295
- "div",
6296
- {
6297
- key: "fc-row-" + ri,
6298
- staticClass: "eg-row",
6299
- style: _vm.getRowStyle(ri),
6300
- },
6301
- [
6302
- _vm._l(
6303
- _vm.leafCols,
6304
- function (col, ci) {
6305
- return [
6306
- _vm.isColumnPinnedLeft(
6307
- ci
6308
- ) &&
6309
- !_vm.isColHidden(ci)
6310
- ? [
6311
- _c(
6312
- "div",
6313
- {
6314
- key:
6315
- "fc-cell-" +
6316
- ri +
6317
- "-" +
6318
- ci,
6319
- staticClass:
6320
- "eg-cell",
6321
- class:
6322
- _vm.getCellClasses(
6258
+ !_vm.isRowHidden(ri)
6259
+ ? _c(
6260
+ "div",
6261
+ {
6262
+ key: "fc-row-" + ri,
6263
+ staticClass: "eg-row",
6264
+ class: {
6265
+ "eg-row--even": ri % 2 === 0,
6266
+ },
6267
+ style: _vm.getRowStyle(ri),
6268
+ },
6269
+ [
6270
+ _vm._l(
6271
+ _vm.leafCols,
6272
+ function (col, ci) {
6273
+ return [
6274
+ _vm.isColumnPinnedLeft(
6275
+ ci
6276
+ ) && !_vm.isColHidden(ci)
6277
+ ? [
6278
+ _c(
6279
+ "div",
6280
+ {
6281
+ key:
6282
+ "fc-cell-" +
6283
+ ri +
6284
+ "-" +
6285
+ ci,
6286
+ staticClass:
6287
+ "eg-cell",
6288
+ class:
6289
+ _vm.getCellClasses(
6290
+ ri,
6291
+ ci
6292
+ ),
6293
+ style:
6294
+ _vm.getCellStyle(
6295
+ ri,
6296
+ ci
6297
+ ),
6298
+ on: {
6299
+ mousedown:
6300
+ function (
6301
+ $event
6302
+ ) {
6303
+ return _vm.onCellMouseDown(
6304
+ $event,
6323
6305
  ri,
6324
6306
  ci
6325
- ),
6326
- style:
6327
- _vm.getCellStyle(
6307
+ )
6308
+ },
6309
+ mouseover:
6310
+ function (
6311
+ $event
6312
+ ) {
6313
+ return _vm.onCellMouseOver(
6314
+ $event,
6328
6315
  ri,
6329
6316
  ci
6330
- ),
6331
- on: {
6332
- mousedown:
6333
- function (
6334
- $event
6335
- ) {
6336
- return _vm.onCellMouseDown(
6337
- $event,
6338
- ri,
6339
- ci
6340
- )
6341
- },
6342
- mouseover:
6343
- function (
6344
- $event
6345
- ) {
6346
- return _vm.onCellMouseOver(
6347
- $event,
6348
- ri,
6349
- ci
6350
- )
6351
- },
6352
- mouseup:
6353
- _vm.onCellMouseUp,
6354
- dblclick:
6355
- function (
6356
- $event
6357
- ) {
6358
- return _vm.onCellDblClick(
6359
- ri,
6360
- ci
6361
- )
6362
- },
6363
- contextmenu:
6364
- function (
6365
- $event
6366
- ) {
6367
- $event.preventDefault();
6368
- return _vm.onCellContextMenu(
6369
- $event,
6370
- ri,
6371
- ci
6372
- )
6373
- },
6317
+ )
6374
6318
  },
6375
- },
6376
- [
6377
- _vm.isMergedAway(
6319
+ mouseup:
6320
+ _vm.onCellMouseUp,
6321
+ dblclick:
6322
+ function (
6323
+ $event
6324
+ ) {
6325
+ return _vm.onCellDblClick(
6326
+ ri,
6327
+ ci
6328
+ )
6329
+ },
6330
+ contextmenu:
6331
+ function (
6332
+ $event
6333
+ ) {
6334
+ $event.preventDefault();
6335
+ return _vm.onCellContextMenu(
6336
+ $event,
6337
+ ri,
6338
+ ci
6339
+ )
6340
+ },
6341
+ },
6342
+ },
6343
+ [
6344
+ _vm.isMergedAway(
6345
+ ri,
6346
+ ci
6347
+ )
6348
+ ? _c("div", {
6349
+ staticStyle:
6350
+ {
6351
+ display:
6352
+ "none",
6353
+ },
6354
+ })
6355
+ : _vm.isEditing(
6378
6356
  ri,
6379
6357
  ci
6380
6358
  )
6381
- ? _c(
6382
- "div",
6383
- {
6384
- staticStyle:
6385
- {
6386
- display:
6387
- "none",
6359
+ ? [
6360
+ _vm.hasCellSlot(
6361
+ col
6362
+ )
6363
+ ? _vm._t(
6364
+ "cell-" +
6365
+ col.key,
6366
+ null,
6367
+ {
6368
+ row: _vm
6369
+ ._dataRaw[
6370
+ ri
6371
+ ],
6372
+ rowIndex:
6373
+ ri,
6374
+ column:
6375
+ col,
6376
+ value:
6377
+ _vm.getCellValue(
6378
+ ri,
6379
+ ci
6380
+ ),
6381
+ isEditing: true,
6382
+ done: function (
6383
+ v
6384
+ ) {
6385
+ return _vm.commitEditSlot(
6386
+ v
6387
+ )
6388
6388
  },
6389
- }
6390
- )
6391
- : _vm.isEditing(
6392
- ri,
6393
- ci
6394
- )
6395
- ? [
6396
- _vm.hasCellSlot(
6389
+ cancel:
6390
+ _vm.cancelEdit,
6391
+ }
6392
+ )
6393
+ : _vm.hasLegacyEditSlot(
6397
6394
  col
6398
6395
  )
6399
- ? _vm._t(
6400
- "cell-" +
6401
- col.key,
6402
- null,
6396
+ ? _vm._t(
6397
+ "edit-" +
6398
+ col.key,
6399
+ null,
6400
+ {
6401
+ row: _vm
6402
+ ._dataRaw[
6403
+ ri
6404
+ ],
6405
+ rowIndex:
6406
+ ri,
6407
+ column:
6408
+ col,
6409
+ value:
6410
+ _vm.getCellValue(
6411
+ ri,
6412
+ ci
6413
+ ),
6414
+ done: function (
6415
+ v
6416
+ ) {
6417
+ return _vm.commitEditSlot(
6418
+ v
6419
+ )
6420
+ },
6421
+ cancel:
6422
+ _vm.cancelEdit,
6423
+ }
6424
+ )
6425
+ : _c(
6426
+ "input",
6427
+ {
6428
+ directives:
6429
+ [
6430
+ {
6431
+ name: "model",
6432
+ rawName:
6433
+ "v-model",
6434
+ value:
6435
+ _vm.editValue,
6436
+ expression:
6437
+ "editValue",
6438
+ },
6439
+ ],
6440
+ ref: "editInput",
6441
+ refInFor: true,
6442
+ staticClass:
6443
+ "eg-cell-input",
6444
+ domProps:
6403
6445
  {
6404
- row: _vm
6405
- ._dataRaw[
6406
- ri
6407
- ],
6408
- rowIndex:
6409
- ri,
6410
- column:
6411
- col,
6412
6446
  value:
6413
- _vm.getCellValue(
6414
- ri,
6415
- ci
6416
- ),
6417
- isEditing: true,
6418
- done: function (
6419
- v
6447
+ _vm.editValue,
6448
+ },
6449
+ on: {
6450
+ blur: _vm.commitEdit,
6451
+ keydown:
6452
+ function (
6453
+ $event
6420
6454
  ) {
6421
- return _vm.commitEditSlot(
6422
- v
6423
- )
6424
- },
6425
- cancel:
6426
- _vm.cancelEdit,
6427
- }
6428
- )
6429
- : _vm.hasLegacyEditSlot(
6430
- col
6431
- )
6432
- ? _vm._t(
6433
- "edit-" +
6434
- col.key,
6435
- null,
6436
- {
6437
- row: _vm
6438
- ._dataRaw[
6439
- ri
6440
- ],
6441
- rowIndex:
6442
- ri,
6443
- column:
6444
- col,
6445
- value:
6446
- _vm.getCellValue(
6447
- ri,
6448
- ci
6449
- ),
6450
- done: function (
6451
- v
6452
- ) {
6453
- return _vm.commitEditSlot(
6454
- v
6455
+ $event.stopPropagation();
6456
+ return _vm.onEditKeyDown.apply(
6457
+ null,
6458
+ arguments
6455
6459
  )
6456
6460
  },
6457
- cancel:
6458
- _vm.cancelEdit,
6459
- }
6460
- )
6461
- : _c(
6462
- "input",
6463
- {
6464
- directives:
6465
- [
6466
- {
6467
- name: "model",
6468
- rawName:
6469
- "v-model",
6470
- value:
6471
- _vm.editValue,
6472
- expression:
6473
- "editValue",
6474
- },
6475
- ],
6476
- ref: "editInput",
6477
- refInFor: true,
6478
- staticClass:
6479
- "eg-cell-input",
6480
- domProps:
6481
- {
6482
- value:
6483
- _vm.editValue,
6484
- },
6485
- on: {
6486
- blur: _vm.commitEdit,
6487
- keydown:
6488
- function (
6489
- $event
6490
- ) {
6491
- $event.stopPropagation();
6492
- return _vm.onEditKeyDown.apply(
6493
- null,
6494
- arguments
6495
- )
6496
- },
6497
- input:
6498
- function (
6499
- $event
6500
- ) {
6501
- if (
6502
- $event
6503
- .target
6504
- .composing
6505
- ) {
6506
- return
6507
- }
6508
- _vm.editValue =
6509
- $event.target.value;
6510
- },
6511
- },
6512
- }
6513
- ),
6514
- ]
6515
- : col.type ===
6516
- "checkbox"
6517
- ? [
6518
- _c(
6519
- "eg-checkbox",
6520
- {
6521
- attrs:
6522
- {
6523
- value:
6524
- _vm.isCheckboxChecked(
6525
- ri,
6526
- ci
6527
- ),
6528
- disabled:
6529
- !_vm.isCellEditable(
6530
- ri,
6531
- ci
6532
- ),
6533
- },
6534
- on: {
6535
- change:
6461
+ input:
6536
6462
  function (
6537
- v
6463
+ $event
6538
6464
  ) {
6539
- return _vm.setCheckboxValue(
6540
- ri,
6541
- ci,
6542
- v
6543
- )
6465
+ if (
6466
+ $event
6467
+ .target
6468
+ .composing
6469
+ ) {
6470
+ return
6471
+ }
6472
+ _vm.editValue =
6473
+ $event.target.value;
6544
6474
  },
6545
6475
  },
6546
6476
  }
6547
6477
  ),
6548
- ]
6549
- : [
6550
- _vm._t(
6551
- "cell-" +
6552
- col.key,
6553
- function () {
6554
- return [
6555
- _vm.hasPrefixSlot(
6556
- col
6478
+ ]
6479
+ : col.type ===
6480
+ "checkbox"
6481
+ ? [
6482
+ _c(
6483
+ "eg-checkbox",
6484
+ {
6485
+ attrs:
6486
+ {
6487
+ value:
6488
+ _vm.isCheckboxChecked(
6489
+ ri,
6490
+ ci
6491
+ ),
6492
+ disabled:
6493
+ !_vm.isCellEditable(
6494
+ ri,
6495
+ ci
6496
+ ),
6497
+ },
6498
+ on: {
6499
+ change:
6500
+ function (
6501
+ v
6502
+ ) {
6503
+ return _vm.setCheckboxValue(
6504
+ ri,
6505
+ ci,
6506
+ v
6557
6507
  )
6558
- ? _vm._t(
6559
- "prefix-" +
6560
- col.key,
6561
- null,
6562
- {
6563
- row: _vm
6564
- ._dataRaw[
6565
- ri
6566
- ],
6567
- rowIndex:
6568
- ri,
6569
- column:
6570
- col,
6571
- value:
6572
- _vm.getCellValue(
6573
- ri,
6574
- ci
6575
- ),
6576
- }
6577
- )
6578
- : _vm._e(),
6579
- _vm.hasSuffixSlot(
6580
- col
6581
- ) &&
6582
- _vm.isSelectLikeCol(
6583
- col
6508
+ },
6509
+ },
6510
+ }
6511
+ ),
6512
+ ]
6513
+ : [
6514
+ _vm._t(
6515
+ "cell-" +
6516
+ col.key,
6517
+ function () {
6518
+ return [
6519
+ _vm.hasPrefixSlot(
6520
+ col
6521
+ )
6522
+ ? _vm._t(
6523
+ "prefix-" +
6524
+ col.key,
6525
+ null,
6526
+ {
6527
+ row: _vm
6528
+ ._dataRaw[
6529
+ ri
6530
+ ],
6531
+ rowIndex:
6532
+ ri,
6533
+ column:
6534
+ col,
6535
+ value:
6536
+ _vm.getCellValue(
6537
+ ri,
6538
+ ci
6539
+ ),
6540
+ }
6584
6541
  )
6585
- ? [
6542
+ : _vm._e(),
6543
+ _vm.hasSuffixSlot(
6544
+ col
6545
+ ) &&
6546
+ _vm.isSelectLikeCol(
6547
+ col
6548
+ )
6549
+ ? [
6550
+ _c(
6551
+ "span",
6552
+ {
6553
+ staticClass:
6554
+ "eg-cell-select-trigger-wrap",
6555
+ },
6556
+ [
6586
6557
  _c(
6587
6558
  "span",
6588
6559
  {
6589
6560
  staticClass:
6590
- "eg-cell-select-trigger-wrap",
6561
+ "eg-cell-value",
6591
6562
  },
6592
6563
  [
6593
- _c(
6594
- "span",
6595
- {
6596
- staticClass:
6597
- "eg-cell-value",
6598
- },
6599
- [
6600
- _vm._v(
6601
- _vm._s(
6602
- _vm.getSelectLabel(
6603
- ci,
6604
- _vm.getCellValue(
6605
- ri,
6606
- ci
6607
- )
6608
- )
6564
+ _vm._v(
6565
+ _vm._s(
6566
+ _vm.getSelectLabel(
6567
+ ci,
6568
+ _vm.getCellValue(
6569
+ ri,
6570
+ ci
6609
6571
  )
6610
- ),
6611
- ]
6612
- ),
6613
- _c(
6614
- "span",
6615
- {
6616
- staticClass:
6617
- "eg-cell-select-caret-inline",
6618
- },
6619
- [
6620
- _vm._v(
6621
- "▾"
6622
- ),
6623
- ]
6572
+ )
6573
+ )
6624
6574
  ),
6625
6575
  ]
6626
6576
  ),
6627
- ]
6628
- : _vm.hasPrefixSlot(
6629
- col
6630
- ) ||
6631
- _vm.hasSuffixSlot(
6632
- col
6633
- )
6634
- ? [
6635
6577
  _c(
6636
6578
  "span",
6637
6579
  {
6638
6580
  staticClass:
6639
- "eg-cell-value",
6581
+ "eg-cell-select-caret-inline",
6640
6582
  },
6641
6583
  [
6642
6584
  _vm._v(
6643
- _vm._s(
6644
- _vm.isSelectLikeCol(
6645
- col
6646
- )
6647
- ? _vm.getSelectLabel(
6648
- ci,
6649
- _vm.getCellValue(
6650
- ri,
6651
- ci
6652
- )
6653
- )
6654
- : _vm.getCellValue(
6655
- ri,
6656
- ci
6657
- )
6658
- )
6585
+ "▾"
6659
6586
  ),
6660
6587
  ]
6661
6588
  ),
6662
6589
  ]
6663
- : [
6590
+ ),
6591
+ ]
6592
+ : _vm.hasPrefixSlot(
6593
+ col
6594
+ ) ||
6595
+ _vm.hasSuffixSlot(
6596
+ col
6597
+ )
6598
+ ? [
6599
+ _c(
6600
+ "span",
6601
+ {
6602
+ staticClass:
6603
+ "eg-cell-value",
6604
+ },
6605
+ [
6664
6606
  _vm._v(
6665
6607
  _vm._s(
6666
6608
  _vm.isSelectLikeCol(
@@ -6679,83 +6621,103 @@ var __vue_render__ = function () {
6679
6621
  )
6680
6622
  )
6681
6623
  ),
6682
- ],
6683
- _vm.hasSuffixSlot(
6684
- col
6685
- )
6686
- ? _c(
6687
- "span",
6688
- {
6689
- staticClass:
6690
- "eg-cell-suffix-wrap",
6691
- },
6692
- [
6693
- _vm._t(
6694
- "suffix-" +
6695
- col.key,
6696
- null,
6697
- {
6698
- row: _vm
6699
- ._dataRaw[
6700
- ri
6701
- ],
6702
- rowIndex:
6624
+ ]
6625
+ ),
6626
+ ]
6627
+ : [
6628
+ _vm._v(
6629
+ _vm._s(
6630
+ _vm.isSelectLikeCol(
6631
+ col
6632
+ )
6633
+ ? _vm.getSelectLabel(
6634
+ ci,
6635
+ _vm.getCellValue(
6703
6636
  ri,
6704
- column:
6705
- col,
6706
- value:
6707
- _vm.getCellValue(
6708
- ri,
6709
- ci
6710
- ),
6711
- }
6712
- ),
6713
- ],
6714
- 2
6637
+ ci
6638
+ )
6639
+ )
6640
+ : _vm.getCellValue(
6641
+ ri,
6642
+ ci
6643
+ )
6715
6644
  )
6716
- : _vm._e(),
6717
- ]
6718
- },
6719
- {
6720
- row: _vm
6721
- ._dataRaw[
6722
- ri
6723
- ],
6724
- rowIndex:
6725
- ri,
6726
- column:
6727
- col,
6728
- value:
6729
- _vm.getCellValue(
6730
- ri,
6731
- ci
6732
- ),
6733
- isEditing: false,
6734
- done: null,
6735
- cancel:
6736
- null,
6737
- }
6738
- ),
6739
- ],
6740
- ],
6741
- 2
6742
- ),
6743
- ]
6744
- : _vm._e(),
6745
- ]
6746
- }
6747
- ),
6748
- ],
6749
- 2
6750
- )
6751
- : _vm._e(),
6752
- ]
6753
- ),
6754
- ]
6755
- }),
6756
- ],
6757
- 2
6758
- ),
6645
+ ),
6646
+ ],
6647
+ _vm.hasSuffixSlot(
6648
+ col
6649
+ )
6650
+ ? _c(
6651
+ "span",
6652
+ {
6653
+ staticClass:
6654
+ "eg-cell-suffix-wrap",
6655
+ },
6656
+ [
6657
+ _vm._t(
6658
+ "suffix-" +
6659
+ col.key,
6660
+ null,
6661
+ {
6662
+ row: _vm
6663
+ ._dataRaw[
6664
+ ri
6665
+ ],
6666
+ rowIndex:
6667
+ ri,
6668
+ column:
6669
+ col,
6670
+ value:
6671
+ _vm.getCellValue(
6672
+ ri,
6673
+ ci
6674
+ ),
6675
+ }
6676
+ ),
6677
+ ],
6678
+ 2
6679
+ )
6680
+ : _vm._e(),
6681
+ ]
6682
+ },
6683
+ {
6684
+ row: _vm
6685
+ ._dataRaw[
6686
+ ri
6687
+ ],
6688
+ rowIndex:
6689
+ ri,
6690
+ column:
6691
+ col,
6692
+ value:
6693
+ _vm.getCellValue(
6694
+ ri,
6695
+ ci
6696
+ ),
6697
+ isEditing: false,
6698
+ done: null,
6699
+ cancel:
6700
+ null,
6701
+ }
6702
+ ),
6703
+ ],
6704
+ ],
6705
+ 2
6706
+ ),
6707
+ ]
6708
+ : _vm._e(),
6709
+ ]
6710
+ }
6711
+ ),
6712
+ ],
6713
+ 2
6714
+ )
6715
+ : _vm._e(),
6716
+ ]
6717
+ }),
6718
+ ],
6719
+ 2
6720
+ ),
6759
6721
  ]
6760
6722
  ),
6761
6723
  ]
@@ -6787,357 +6749,310 @@ var __vue_render__ = function () {
6787
6749
  "translateY(" +
6788
6750
  _vm.virtualOffsetTop +
6789
6751
  "px)",
6752
+ willChange: "transform",
6753
+ contain: "layout style",
6790
6754
  },
6791
6755
  },
6792
6756
  [
6793
6757
  _vm._l(_vm.visibleRowRange, function (ri) {
6794
6758
  return [
6795
- _c(
6796
- "transition",
6797
- {
6798
- key: "tr-m-" + ri,
6799
- attrs: { name: _vm._rowTransitionName },
6800
- },
6801
- [
6802
- !_vm.isRowHidden(ri)
6803
- ? _c(
6804
- "div",
6805
- {
6806
- key: "row-" + ri,
6807
- staticClass: "eg-row",
6808
- style: _vm.getRowStyle(ri),
6809
- attrs: { "data-row": ri },
6810
- },
6811
- [
6812
- _vm._l(
6813
- _vm.leafCols,
6814
- function (col, ci) {
6815
- return [
6816
- _vm.isColumnScrollable(
6817
- ci
6818
- ) && !_vm.isColHidden(ci)
6819
- ? [
6820
- _c(
6821
- "div",
6822
- {
6823
- key:
6824
- "cell-" +
6825
- ri +
6826
- "-" +
6827
- ci,
6828
- staticClass:
6829
- "eg-cell",
6830
- class:
6831
- _vm.getCellClasses(
6759
+ !_vm.isRowHidden(ri)
6760
+ ? _c(
6761
+ "div",
6762
+ {
6763
+ key: "row-" + ri,
6764
+ staticClass: "eg-row",
6765
+ class: {
6766
+ "eg-row--even": ri % 2 === 0,
6767
+ },
6768
+ style: _vm.getRowStyle(ri),
6769
+ attrs: { "data-row": ri },
6770
+ },
6771
+ [
6772
+ _vm._l(
6773
+ _vm.leafCols,
6774
+ function (col, ci) {
6775
+ return [
6776
+ _vm.isColumnScrollable(ci) &&
6777
+ !_vm.isColHidden(ci)
6778
+ ? [
6779
+ _c(
6780
+ "div",
6781
+ {
6782
+ key:
6783
+ "cell-" +
6784
+ ri +
6785
+ "-" +
6786
+ ci,
6787
+ staticClass:
6788
+ "eg-cell",
6789
+ class:
6790
+ _vm.getCellClasses(
6791
+ ri,
6792
+ ci
6793
+ ),
6794
+ style:
6795
+ _vm.getCellStyle(
6796
+ ri,
6797
+ ci
6798
+ ),
6799
+ on: {
6800
+ mousedown:
6801
+ function (
6802
+ $event
6803
+ ) {
6804
+ return _vm.onCellMouseDown(
6805
+ $event,
6832
6806
  ri,
6833
6807
  ci
6834
- ),
6835
- style:
6836
- _vm.getCellStyle(
6808
+ )
6809
+ },
6810
+ mouseover:
6811
+ function (
6812
+ $event
6813
+ ) {
6814
+ return _vm.onCellMouseOver(
6815
+ $event,
6837
6816
  ri,
6838
6817
  ci
6839
- ),
6840
- on: {
6841
- mousedown:
6842
- function (
6843
- $event
6844
- ) {
6845
- return _vm.onCellMouseDown(
6846
- $event,
6847
- ri,
6848
- ci
6849
- )
6850
- },
6851
- mouseover:
6852
- function (
6853
- $event
6854
- ) {
6855
- return _vm.onCellMouseOver(
6856
- $event,
6857
- ri,
6858
- ci
6859
- )
6860
- },
6861
- mouseup:
6862
- _vm.onCellMouseUp,
6863
- dblclick:
6864
- function (
6865
- $event
6866
- ) {
6867
- return _vm.onCellDblClick(
6868
- ri,
6869
- ci
6870
- )
6871
- },
6872
- contextmenu:
6873
- function (
6874
- $event
6875
- ) {
6876
- $event.preventDefault();
6877
- return _vm.onCellContextMenu(
6878
- $event,
6879
- ri,
6880
- ci
6881
- )
6882
- },
6818
+ )
6883
6819
  },
6884
- },
6885
- [
6886
- _vm.isMergedAway(
6820
+ mouseup:
6821
+ _vm.onCellMouseUp,
6822
+ dblclick:
6823
+ function (
6824
+ $event
6825
+ ) {
6826
+ return _vm.onCellDblClick(
6827
+ ri,
6828
+ ci
6829
+ )
6830
+ },
6831
+ contextmenu:
6832
+ function (
6833
+ $event
6834
+ ) {
6835
+ $event.preventDefault();
6836
+ return _vm.onCellContextMenu(
6837
+ $event,
6838
+ ri,
6839
+ ci
6840
+ )
6841
+ },
6842
+ },
6843
+ },
6844
+ [
6845
+ _vm.isMergedAway(
6846
+ ri,
6847
+ ci
6848
+ )
6849
+ ? _c("div", {
6850
+ staticStyle: {
6851
+ display:
6852
+ "none",
6853
+ },
6854
+ })
6855
+ : _vm.isEditing(
6887
6856
  ri,
6888
6857
  ci
6889
6858
  )
6890
- ? _c("div", {
6891
- staticStyle:
6859
+ ? [
6860
+ _vm.hasCellSlot(
6861
+ col
6862
+ )
6863
+ ? _vm._t(
6864
+ "cell-" +
6865
+ col.key,
6866
+ null,
6892
6867
  {
6893
- display:
6894
- "none",
6895
- },
6896
- })
6897
- : _vm.isEditing(
6898
- ri,
6899
- ci
6900
- )
6901
- ? [
6902
- _vm.hasCellSlot(
6868
+ row: _vm
6869
+ ._dataRaw[
6870
+ ri
6871
+ ],
6872
+ rowIndex:
6873
+ ri,
6874
+ column:
6875
+ col,
6876
+ value:
6877
+ _vm.getCellValue(
6878
+ ri,
6879
+ ci
6880
+ ),
6881
+ isEditing: true,
6882
+ done: function (
6883
+ v
6884
+ ) {
6885
+ return _vm.commitEditSlot(
6886
+ v
6887
+ )
6888
+ },
6889
+ cancel:
6890
+ _vm.cancelEdit,
6891
+ }
6892
+ )
6893
+ : _vm.hasLegacyEditSlot(
6903
6894
  col
6904
6895
  )
6905
- ? _vm._t(
6906
- "cell-" +
6907
- col.key,
6908
- null,
6909
- {
6910
- row: _vm
6911
- ._dataRaw[
6912
- ri
6913
- ],
6914
- rowIndex:
6915
- ri,
6916
- column:
6917
- col,
6918
- value:
6919
- _vm.getCellValue(
6920
- ri,
6921
- ci
6922
- ),
6923
- isEditing: true,
6924
- done: function (
6925
- v
6926
- ) {
6927
- return _vm.commitEditSlot(
6928
- v
6929
- )
6930
- },
6931
- cancel:
6932
- _vm.cancelEdit,
6933
- }
6934
- )
6935
- : _vm.hasLegacyEditSlot(
6936
- col
6937
- )
6938
- ? _vm._t(
6939
- "edit-" +
6940
- col.key,
6941
- null,
6942
- {
6943
- row: _vm
6944
- ._dataRaw[
6945
- ri
6946
- ],
6947
- rowIndex:
6948
- ri,
6949
- column:
6950
- col,
6951
- value:
6952
- _vm.getCellValue(
6953
- ri,
6954
- ci
6955
- ),
6956
- done: function (
6957
- v
6958
- ) {
6959
- return _vm.commitEditSlot(
6960
- v
6961
- )
6962
- },
6963
- cancel:
6964
- _vm.cancelEdit,
6965
- }
6966
- )
6967
- : _c(
6968
- "input",
6969
- {
6970
- directives:
6971
- [
6972
- {
6973
- name: "model",
6974
- rawName:
6975
- "v-model",
6976
- value:
6977
- _vm.editValue,
6978
- expression:
6979
- "editValue",
6980
- },
6981
- ],
6982
- ref: "editInput",
6983
- refInFor: true,
6984
- staticClass:
6985
- "eg-cell-input",
6986
- domProps:
6987
- {
6988
- value:
6989
- _vm.editValue,
6990
- },
6991
- on: {
6992
- blur: _vm.commitEdit,
6993
- keydown:
6994
- function (
6995
- $event
6996
- ) {
6997
- $event.stopPropagation();
6998
- return _vm.onEditKeyDown.apply(
6999
- null,
7000
- arguments
7001
- )
7002
- },
7003
- input:
7004
- function (
7005
- $event
7006
- ) {
7007
- if (
7008
- $event
7009
- .target
7010
- .composing
7011
- ) {
7012
- return
7013
- }
7014
- _vm.editValue =
7015
- $event.target.value;
7016
- },
7017
- },
7018
- }
7019
- ),
7020
- ]
7021
- : col.type ===
7022
- "checkbox"
7023
- ? [
7024
- _c(
7025
- "eg-checkbox",
6896
+ ? _vm._t(
6897
+ "edit-" +
6898
+ col.key,
6899
+ null,
7026
6900
  {
7027
- attrs:
6901
+ row: _vm
6902
+ ._dataRaw[
6903
+ ri
6904
+ ],
6905
+ rowIndex:
6906
+ ri,
6907
+ column:
6908
+ col,
6909
+ value:
6910
+ _vm.getCellValue(
6911
+ ri,
6912
+ ci
6913
+ ),
6914
+ done: function (
6915
+ v
6916
+ ) {
6917
+ return _vm.commitEditSlot(
6918
+ v
6919
+ )
6920
+ },
6921
+ cancel:
6922
+ _vm.cancelEdit,
6923
+ }
6924
+ )
6925
+ : _c(
6926
+ "input",
6927
+ {
6928
+ directives:
6929
+ [
6930
+ {
6931
+ name: "model",
6932
+ rawName:
6933
+ "v-model",
6934
+ value:
6935
+ _vm.editValue,
6936
+ expression:
6937
+ "editValue",
6938
+ },
6939
+ ],
6940
+ ref: "editInput",
6941
+ refInFor: true,
6942
+ staticClass:
6943
+ "eg-cell-input",
6944
+ domProps:
7028
6945
  {
7029
6946
  value:
7030
- _vm.isCheckboxChecked(
7031
- ri,
7032
- ci
7033
- ),
7034
- disabled:
7035
- !_vm.isCellEditable(
7036
- ri,
7037
- ci
7038
- ),
6947
+ _vm.editValue,
7039
6948
  },
7040
6949
  on: {
7041
- change:
6950
+ blur: _vm.commitEdit,
6951
+ keydown:
7042
6952
  function (
7043
- v
6953
+ $event
7044
6954
  ) {
7045
- return _vm.setCheckboxValue(
7046
- ri,
7047
- ci,
7048
- v
6955
+ $event.stopPropagation();
6956
+ return _vm.onEditKeyDown.apply(
6957
+ null,
6958
+ arguments
7049
6959
  )
7050
6960
  },
6961
+ input:
6962
+ function (
6963
+ $event
6964
+ ) {
6965
+ if (
6966
+ $event
6967
+ .target
6968
+ .composing
6969
+ ) {
6970
+ return
6971
+ }
6972
+ _vm.editValue =
6973
+ $event.target.value;
6974
+ },
7051
6975
  },
7052
6976
  }
7053
6977
  ),
7054
- ]
7055
- : [
7056
- _vm._t(
7057
- "cell-" +
7058
- col.key,
7059
- function () {
7060
- return [
7061
- _vm.hasPrefixSlot(
7062
- col
7063
- )
7064
- ? _vm._t(
7065
- "prefix-" +
7066
- col.key,
7067
- null,
7068
- {
7069
- row: _vm
7070
- ._dataRaw[
7071
- ri
7072
- ],
7073
- rowIndex:
7074
- ri,
7075
- column:
7076
- col,
7077
- value:
7078
- _vm.getCellValue(
7079
- ri,
7080
- ci
7081
- ),
7082
- }
7083
- )
7084
- : _vm._e(),
7085
- _vm.hasSuffixSlot(
7086
- col
7087
- ) &&
7088
- _vm.isSelectLikeCol(
7089
- col
6978
+ ]
6979
+ : col.type ===
6980
+ "checkbox"
6981
+ ? [
6982
+ _c(
6983
+ "eg-checkbox",
6984
+ {
6985
+ attrs: {
6986
+ value:
6987
+ _vm.isCheckboxChecked(
6988
+ ri,
6989
+ ci
6990
+ ),
6991
+ disabled:
6992
+ !_vm.isCellEditable(
6993
+ ri,
6994
+ ci
6995
+ ),
6996
+ },
6997
+ on: {
6998
+ change:
6999
+ function (
7000
+ v
7001
+ ) {
7002
+ return _vm.setCheckboxValue(
7003
+ ri,
7004
+ ci,
7005
+ v
7090
7006
  )
7091
- ? [
7092
- _c(
7093
- "span",
7094
- {
7095
- staticClass:
7096
- "eg-cell-select-trigger-wrap",
7097
- },
7098
- [
7099
- _c(
7100
- "span",
7101
- {
7102
- staticClass:
7103
- "eg-cell-value",
7104
- },
7105
- [
7106
- _vm._v(
7107
- _vm._s(
7108
- _vm.getSelectLabel(
7109
- ci,
7110
- _vm.getCellValue(
7111
- ri,
7112
- ci
7113
- )
7114
- )
7115
- )
7116
- ),
7117
- ]
7118
- ),
7119
- _c(
7120
- "span",
7121
- {
7122
- staticClass:
7123
- "eg-cell-select-caret-inline",
7124
- },
7125
- [
7126
- _vm._v(
7127
- "▾"
7128
- ),
7129
- ]
7130
- ),
7131
- ]
7007
+ },
7008
+ },
7009
+ }
7010
+ ),
7011
+ ]
7012
+ : [
7013
+ _vm._t(
7014
+ "cell-" +
7015
+ col.key,
7016
+ function () {
7017
+ return [
7018
+ _vm.hasPrefixSlot(
7019
+ col
7020
+ )
7021
+ ? _vm._t(
7022
+ "prefix-" +
7023
+ col.key,
7024
+ null,
7025
+ {
7026
+ row: _vm
7027
+ ._dataRaw[
7028
+ ri
7029
+ ],
7030
+ rowIndex:
7031
+ ri,
7032
+ column:
7033
+ col,
7034
+ value:
7035
+ _vm.getCellValue(
7036
+ ri,
7037
+ ci
7132
7038
  ),
7133
- ]
7134
- : _vm.hasPrefixSlot(
7135
- col
7136
- ) ||
7137
- _vm.hasSuffixSlot(
7138
- col
7139
- )
7140
- ? [
7039
+ }
7040
+ )
7041
+ : _vm._e(),
7042
+ _vm.hasSuffixSlot(
7043
+ col
7044
+ ) &&
7045
+ _vm.isSelectLikeCol(
7046
+ col
7047
+ )
7048
+ ? [
7049
+ _c(
7050
+ "span",
7051
+ {
7052
+ staticClass:
7053
+ "eg-cell-select-trigger-wrap",
7054
+ },
7055
+ [
7141
7056
  _c(
7142
7057
  "span",
7143
7058
  {
@@ -7147,26 +7062,46 @@ var __vue_render__ = function () {
7147
7062
  [
7148
7063
  _vm._v(
7149
7064
  _vm._s(
7150
- _vm.isSelectLikeCol(
7151
- col
7065
+ _vm.getSelectLabel(
7066
+ ci,
7067
+ _vm.getCellValue(
7068
+ ri,
7069
+ ci
7070
+ )
7152
7071
  )
7153
- ? _vm.getSelectLabel(
7154
- ci,
7155
- _vm.getCellValue(
7156
- ri,
7157
- ci
7158
- )
7159
- )
7160
- : _vm.getCellValue(
7161
- ri,
7162
- ci
7163
- )
7164
7072
  )
7165
7073
  ),
7166
7074
  ]
7167
7075
  ),
7076
+ _c(
7077
+ "span",
7078
+ {
7079
+ staticClass:
7080
+ "eg-cell-select-caret-inline",
7081
+ },
7082
+ [
7083
+ _vm._v(
7084
+ "▾"
7085
+ ),
7086
+ ]
7087
+ ),
7168
7088
  ]
7169
- : [
7089
+ ),
7090
+ ]
7091
+ : _vm.hasPrefixSlot(
7092
+ col
7093
+ ) ||
7094
+ _vm.hasSuffixSlot(
7095
+ col
7096
+ )
7097
+ ? [
7098
+ _c(
7099
+ "span",
7100
+ {
7101
+ staticClass:
7102
+ "eg-cell-value",
7103
+ },
7104
+ [
7170
7105
  _vm._v(
7171
7106
  _vm._s(
7172
7107
  _vm.isSelectLikeCol(
@@ -7185,78 +7120,98 @@ var __vue_render__ = function () {
7185
7120
  )
7186
7121
  )
7187
7122
  ),
7188
- ],
7189
- _vm.hasSuffixSlot(
7190
- col
7191
- )
7192
- ? _c(
7193
- "span",
7194
- {
7195
- staticClass:
7196
- "eg-cell-suffix-wrap",
7197
- },
7198
- [
7199
- _vm._t(
7200
- "suffix-" +
7201
- col.key,
7202
- null,
7203
- {
7204
- row: _vm
7205
- ._dataRaw[
7206
- ri
7207
- ],
7208
- rowIndex:
7123
+ ]
7124
+ ),
7125
+ ]
7126
+ : [
7127
+ _vm._v(
7128
+ _vm._s(
7129
+ _vm.isSelectLikeCol(
7130
+ col
7131
+ )
7132
+ ? _vm.getSelectLabel(
7133
+ ci,
7134
+ _vm.getCellValue(
7209
7135
  ri,
7210
- column:
7211
- col,
7212
- value:
7213
- _vm.getCellValue(
7214
- ri,
7215
- ci
7216
- ),
7217
- }
7218
- ),
7219
- ],
7220
- 2
7136
+ ci
7137
+ )
7138
+ )
7139
+ : _vm.getCellValue(
7140
+ ri,
7141
+ ci
7142
+ )
7221
7143
  )
7222
- : _vm._e(),
7223
- ]
7224
- },
7225
- {
7226
- row: _vm
7227
- ._dataRaw[
7228
- ri
7229
- ],
7230
- rowIndex:
7231
- ri,
7232
- column:
7233
- col,
7234
- value:
7235
- _vm.getCellValue(
7236
- ri,
7237
- ci
7238
- ),
7239
- isEditing: false,
7240
- done: null,
7241
- cancel:
7242
- null,
7243
- }
7244
- ),
7245
- ],
7246
- ],
7247
- 2
7248
- ),
7249
- ]
7250
- : _vm._e(),
7251
- ]
7252
- }
7253
- ),
7254
- ],
7255
- 2
7256
- )
7257
- : _vm._e(),
7258
- ]
7259
- ),
7144
+ ),
7145
+ ],
7146
+ _vm.hasSuffixSlot(
7147
+ col
7148
+ )
7149
+ ? _c(
7150
+ "span",
7151
+ {
7152
+ staticClass:
7153
+ "eg-cell-suffix-wrap",
7154
+ },
7155
+ [
7156
+ _vm._t(
7157
+ "suffix-" +
7158
+ col.key,
7159
+ null,
7160
+ {
7161
+ row: _vm
7162
+ ._dataRaw[
7163
+ ri
7164
+ ],
7165
+ rowIndex:
7166
+ ri,
7167
+ column:
7168
+ col,
7169
+ value:
7170
+ _vm.getCellValue(
7171
+ ri,
7172
+ ci
7173
+ ),
7174
+ }
7175
+ ),
7176
+ ],
7177
+ 2
7178
+ )
7179
+ : _vm._e(),
7180
+ ]
7181
+ },
7182
+ {
7183
+ row: _vm
7184
+ ._dataRaw[
7185
+ ri
7186
+ ],
7187
+ rowIndex:
7188
+ ri,
7189
+ column:
7190
+ col,
7191
+ value:
7192
+ _vm.getCellValue(
7193
+ ri,
7194
+ ci
7195
+ ),
7196
+ isEditing: false,
7197
+ done: null,
7198
+ cancel:
7199
+ null,
7200
+ }
7201
+ ),
7202
+ ],
7203
+ ],
7204
+ 2
7205
+ ),
7206
+ ]
7207
+ : _vm._e(),
7208
+ ]
7209
+ }
7210
+ ),
7211
+ ],
7212
+ 2
7213
+ )
7214
+ : _vm._e(),
7260
7215
  ]
7261
7216
  }),
7262
7217
  ],
@@ -7292,382 +7247,354 @@ var __vue_render__ = function () {
7292
7247
  "translateY(" +
7293
7248
  _vm.virtualOffsetTop +
7294
7249
  "px)",
7250
+ willChange: "transform",
7251
+ contain: "layout style",
7295
7252
  },
7296
7253
  },
7297
7254
  [
7298
7255
  _vm._l(_vm.visibleRowRange, function (ri) {
7299
7256
  return [
7300
- _c(
7301
- "transition",
7302
- {
7303
- key: "tr-pr-" + ri,
7304
- attrs: {
7305
- name: _vm._rowTransitionName,
7306
- },
7307
- },
7308
- [
7309
- !_vm.isRowHidden(ri)
7310
- ? _c(
7311
- "div",
7312
- {
7313
- key: "pr-row-" + ri,
7314
- staticClass: "eg-row",
7315
- style: _vm.getRowStyle(ri),
7316
- },
7317
- [
7318
- _vm._l(
7319
- _vm.pinnedRightCols,
7320
- function (col) {
7321
- return [
7322
- _c(
7323
- "div",
7324
- {
7325
- key:
7326
- "pr-cell-" +
7327
- ri +
7328
- "-" +
7329
- col._flatIndex,
7330
- staticClass:
7331
- "eg-cell",
7332
- class:
7333
- _vm.getCellClasses(
7257
+ !_vm.isRowHidden(ri)
7258
+ ? _c(
7259
+ "div",
7260
+ {
7261
+ key: "pr-row-" + ri,
7262
+ staticClass: "eg-row",
7263
+ class: {
7264
+ "eg-row--even": ri % 2 === 0,
7265
+ },
7266
+ style: _vm.getRowStyle(ri),
7267
+ },
7268
+ [
7269
+ _vm._l(
7270
+ _vm.pinnedRightCols,
7271
+ function (col) {
7272
+ return [
7273
+ _c(
7274
+ "div",
7275
+ {
7276
+ key:
7277
+ "pr-cell-" +
7278
+ ri +
7279
+ "-" +
7280
+ col._flatIndex,
7281
+ staticClass:
7282
+ "eg-cell",
7283
+ class:
7284
+ _vm.getCellClasses(
7285
+ ri,
7286
+ col._flatIndex
7287
+ ),
7288
+ style:
7289
+ _vm.getCellStyle(
7290
+ ri,
7291
+ col._flatIndex
7292
+ ),
7293
+ on: {
7294
+ mousedown:
7295
+ function (
7296
+ $event
7297
+ ) {
7298
+ return _vm.onCellMouseDown(
7299
+ $event,
7334
7300
  ri,
7335
7301
  col._flatIndex
7336
- ),
7337
- style:
7338
- _vm.getCellStyle(
7302
+ )
7303
+ },
7304
+ mouseover:
7305
+ function (
7306
+ $event
7307
+ ) {
7308
+ return _vm.onCellMouseOver(
7309
+ $event,
7339
7310
  ri,
7340
7311
  col._flatIndex
7341
- ),
7342
- on: {
7343
- mousedown:
7344
- function (
7345
- $event
7346
- ) {
7347
- return _vm.onCellMouseDown(
7348
- $event,
7349
- ri,
7350
- col._flatIndex
7351
- )
7352
- },
7353
- mouseover:
7354
- function (
7355
- $event
7356
- ) {
7357
- return _vm.onCellMouseOver(
7358
- $event,
7359
- ri,
7360
- col._flatIndex
7361
- )
7362
- },
7363
- mouseup:
7364
- _vm.onCellMouseUp,
7365
- dblclick:
7366
- function (
7367
- $event
7368
- ) {
7369
- return _vm.onCellDblClick(
7370
- ri,
7371
- col._flatIndex
7372
- )
7373
- },
7374
- contextmenu:
7375
- function (
7376
- $event
7377
- ) {
7378
- $event.preventDefault();
7379
- return _vm.onCellContextMenu(
7380
- $event,
7381
- ri,
7382
- col._flatIndex
7383
- )
7384
- },
7312
+ )
7385
7313
  },
7314
+ mouseup:
7315
+ _vm.onCellMouseUp,
7316
+ dblclick: function (
7317
+ $event
7318
+ ) {
7319
+ return _vm.onCellDblClick(
7320
+ ri,
7321
+ col._flatIndex
7322
+ )
7386
7323
  },
7387
- [
7388
- _vm.isMergedAway(
7324
+ contextmenu:
7325
+ function (
7326
+ $event
7327
+ ) {
7328
+ $event.preventDefault();
7329
+ return _vm.onCellContextMenu(
7330
+ $event,
7331
+ ri,
7332
+ col._flatIndex
7333
+ )
7334
+ },
7335
+ },
7336
+ },
7337
+ [
7338
+ _vm.isMergedAway(
7339
+ ri,
7340
+ col._flatIndex
7341
+ )
7342
+ ? _c("div", {
7343
+ staticStyle: {
7344
+ display:
7345
+ "none",
7346
+ },
7347
+ })
7348
+ : _vm.isEditing(
7389
7349
  ri,
7390
7350
  col._flatIndex
7391
7351
  )
7392
- ? _c("div", {
7393
- staticStyle:
7352
+ ? [
7353
+ _vm.hasCellSlot(
7354
+ col
7355
+ )
7356
+ ? _vm._t(
7357
+ "cell-" +
7358
+ col.key,
7359
+ null,
7394
7360
  {
7395
- display:
7396
- "none",
7397
- },
7398
- })
7399
- : _vm.isEditing(
7400
- ri,
7401
- col._flatIndex
7402
- )
7403
- ? [
7404
- _vm.hasCellSlot(
7361
+ row: _vm
7362
+ ._dataRaw[
7363
+ ri
7364
+ ],
7365
+ rowIndex:
7366
+ ri,
7367
+ column:
7368
+ col,
7369
+ value:
7370
+ _vm.getCellValue(
7371
+ ri,
7372
+ col._flatIndex
7373
+ ),
7374
+ isEditing: true,
7375
+ done: function (
7376
+ v
7377
+ ) {
7378
+ return _vm.commitEditSlot(
7379
+ v
7380
+ )
7381
+ },
7382
+ cancel:
7383
+ _vm.cancelEdit,
7384
+ }
7385
+ )
7386
+ : _vm.hasLegacyEditSlot(
7405
7387
  col
7406
7388
  )
7407
- ? _vm._t(
7408
- "cell-" +
7409
- col.key,
7410
- null,
7411
- {
7412
- row: _vm
7413
- ._dataRaw[
7414
- ri
7415
- ],
7416
- rowIndex:
7417
- ri,
7418
- column:
7419
- col,
7420
- value:
7421
- _vm.getCellValue(
7422
- ri,
7423
- col._flatIndex
7424
- ),
7425
- isEditing: true,
7426
- done: function (
7427
- v
7428
- ) {
7429
- return _vm.commitEditSlot(
7430
- v
7431
- )
7389
+ ? _vm._t(
7390
+ "edit-" +
7391
+ col.key,
7392
+ null,
7393
+ {
7394
+ row: _vm
7395
+ ._dataRaw[
7396
+ ri
7397
+ ],
7398
+ rowIndex:
7399
+ ri,
7400
+ column:
7401
+ col,
7402
+ value:
7403
+ _vm.getCellValue(
7404
+ ri,
7405
+ col._flatIndex
7406
+ ),
7407
+ done: function (
7408
+ v
7409
+ ) {
7410
+ return _vm.commitEditSlot(
7411
+ v
7412
+ )
7413
+ },
7414
+ cancel:
7415
+ _vm.cancelEdit,
7416
+ }
7417
+ )
7418
+ : _c(
7419
+ "input",
7420
+ {
7421
+ directives:
7422
+ [
7423
+ {
7424
+ name: "model",
7425
+ rawName:
7426
+ "v-model",
7427
+ value:
7428
+ _vm.editValue,
7429
+ expression:
7430
+ "editValue",
7432
7431
  },
7433
- cancel:
7434
- _vm.cancelEdit,
7435
- }
7436
- )
7437
- : _vm.hasLegacyEditSlot(
7438
- col
7439
- )
7440
- ? _vm._t(
7441
- "edit-" +
7442
- col.key,
7443
- null,
7432
+ ],
7433
+ ref: "editInput",
7434
+ refInFor: true,
7435
+ staticClass:
7436
+ "eg-cell-input",
7437
+ domProps:
7444
7438
  {
7445
- row: _vm
7446
- ._dataRaw[
7447
- ri
7448
- ],
7449
- rowIndex:
7450
- ri,
7451
- column:
7452
- col,
7453
7439
  value:
7454
- _vm.getCellValue(
7455
- ri,
7456
- col._flatIndex
7457
- ),
7458
- done: function (
7459
- v
7440
+ _vm.editValue,
7441
+ },
7442
+ on: {
7443
+ blur: _vm.commitEdit,
7444
+ keydown:
7445
+ function (
7446
+ $event
7460
7447
  ) {
7461
- return _vm.commitEditSlot(
7462
- v
7448
+ $event.stopPropagation();
7449
+ return _vm.onEditKeyDown.apply(
7450
+ null,
7451
+ arguments
7463
7452
  )
7464
7453
  },
7465
- cancel:
7466
- _vm.cancelEdit,
7467
- }
7468
- )
7469
- : _c(
7470
- "input",
7471
- {
7472
- directives:
7473
- [
7474
- {
7475
- name: "model",
7476
- rawName:
7477
- "v-model",
7478
- value:
7479
- _vm.editValue,
7480
- expression:
7481
- "editValue",
7482
- },
7483
- ],
7484
- ref: "editInput",
7485
- refInFor: true,
7486
- staticClass:
7487
- "eg-cell-input",
7488
- domProps:
7489
- {
7490
- value:
7491
- _vm.editValue,
7492
- },
7493
- on: {
7494
- blur: _vm.commitEdit,
7495
- keydown:
7496
- function (
7497
- $event
7498
- ) {
7499
- $event.stopPropagation();
7500
- return _vm.onEditKeyDown.apply(
7501
- null,
7502
- arguments
7503
- )
7504
- },
7505
- input:
7506
- function (
7507
- $event
7508
- ) {
7509
- if (
7510
- $event
7511
- .target
7512
- .composing
7513
- ) {
7514
- return
7515
- }
7516
- _vm.editValue =
7517
- $event.target.value;
7518
- },
7519
- },
7520
- }
7521
- ),
7522
- ]
7523
- : col.type ===
7524
- "checkbox"
7525
- ? [
7526
- _c(
7527
- "eg-checkbox",
7528
- {
7529
- attrs: {
7530
- value:
7531
- _vm.isCheckboxChecked(
7532
- ri,
7533
- col._flatIndex
7534
- ),
7535
- disabled:
7536
- !_vm.isCellEditable(
7537
- ri,
7538
- col._flatIndex
7539
- ),
7540
- },
7541
- on: {
7542
- change:
7454
+ input:
7543
7455
  function (
7544
- v
7456
+ $event
7545
7457
  ) {
7546
- return _vm.setCheckboxValue(
7547
- ri,
7548
- col._flatIndex,
7549
- v
7550
- )
7458
+ if (
7459
+ $event
7460
+ .target
7461
+ .composing
7462
+ ) {
7463
+ return
7464
+ }
7465
+ _vm.editValue =
7466
+ $event.target.value;
7551
7467
  },
7552
7468
  },
7553
7469
  }
7554
7470
  ),
7555
- ]
7556
- : [
7557
- _vm._t(
7558
- "cell-" +
7559
- col.key,
7560
- function () {
7561
- return [
7562
- _vm.hasPrefixSlot(
7563
- col
7471
+ ]
7472
+ : col.type ===
7473
+ "checkbox"
7474
+ ? [
7475
+ _c(
7476
+ "eg-checkbox",
7477
+ {
7478
+ attrs: {
7479
+ value:
7480
+ _vm.isCheckboxChecked(
7481
+ ri,
7482
+ col._flatIndex
7483
+ ),
7484
+ disabled:
7485
+ !_vm.isCellEditable(
7486
+ ri,
7487
+ col._flatIndex
7488
+ ),
7489
+ },
7490
+ on: {
7491
+ change:
7492
+ function (
7493
+ v
7494
+ ) {
7495
+ return _vm.setCheckboxValue(
7496
+ ri,
7497
+ col._flatIndex,
7498
+ v
7564
7499
  )
7565
- ? _vm._t(
7566
- "prefix-" +
7567
- col.key,
7568
- null,
7569
- {
7570
- row: _vm
7571
- ._dataRaw[
7572
- ri
7573
- ],
7574
- rowIndex:
7575
- ri,
7576
- column:
7577
- col,
7578
- value:
7579
- _vm.getCellValue(
7580
- ri,
7581
- col._flatIndex
7582
- ),
7583
- }
7584
- )
7585
- : _vm._e(),
7586
- _vm.hasSuffixSlot(
7587
- col
7588
- ) &&
7589
- _vm.isSelectLikeCol(
7590
- col
7500
+ },
7501
+ },
7502
+ }
7503
+ ),
7504
+ ]
7505
+ : [
7506
+ _vm._t(
7507
+ "cell-" +
7508
+ col.key,
7509
+ function () {
7510
+ return [
7511
+ _vm.hasPrefixSlot(
7512
+ col
7513
+ )
7514
+ ? _vm._t(
7515
+ "prefix-" +
7516
+ col.key,
7517
+ null,
7518
+ {
7519
+ row: _vm
7520
+ ._dataRaw[
7521
+ ri
7522
+ ],
7523
+ rowIndex:
7524
+ ri,
7525
+ column:
7526
+ col,
7527
+ value:
7528
+ _vm.getCellValue(
7529
+ ri,
7530
+ col._flatIndex
7531
+ ),
7532
+ }
7591
7533
  )
7592
- ? [
7534
+ : _vm._e(),
7535
+ _vm.hasSuffixSlot(
7536
+ col
7537
+ ) &&
7538
+ _vm.isSelectLikeCol(
7539
+ col
7540
+ )
7541
+ ? [
7542
+ _c(
7543
+ "span",
7544
+ {
7545
+ staticClass:
7546
+ "eg-cell-select-trigger-wrap",
7547
+ },
7548
+ [
7593
7549
  _c(
7594
7550
  "span",
7595
7551
  {
7596
7552
  staticClass:
7597
- "eg-cell-select-trigger-wrap",
7553
+ "eg-cell-value",
7598
7554
  },
7599
7555
  [
7600
- _c(
7601
- "span",
7602
- {
7603
- staticClass:
7604
- "eg-cell-value",
7605
- },
7606
- [
7607
- _vm._v(
7608
- _vm._s(
7609
- _vm.getSelectLabel(
7610
- col._flatIndex,
7611
- _vm.getCellValue(
7612
- ri,
7613
- col._flatIndex
7614
- )
7615
- )
7556
+ _vm._v(
7557
+ _vm._s(
7558
+ _vm.getSelectLabel(
7559
+ col._flatIndex,
7560
+ _vm.getCellValue(
7561
+ ri,
7562
+ col._flatIndex
7616
7563
  )
7617
- ),
7618
- ]
7619
- ),
7620
- _c(
7621
- "span",
7622
- {
7623
- staticClass:
7624
- "eg-cell-select-caret-inline",
7625
- },
7626
- [
7627
- _vm._v(
7628
- "▾"
7629
- ),
7630
- ]
7564
+ )
7565
+ )
7631
7566
  ),
7632
7567
  ]
7633
7568
  ),
7634
- ]
7635
- : _vm.hasPrefixSlot(
7636
- col
7637
- ) ||
7638
- _vm.hasSuffixSlot(
7639
- col
7640
- )
7641
- ? [
7642
7569
  _c(
7643
7570
  "span",
7644
7571
  {
7645
7572
  staticClass:
7646
- "eg-cell-value",
7573
+ "eg-cell-select-caret-inline",
7647
7574
  },
7648
7575
  [
7649
7576
  _vm._v(
7650
- _vm._s(
7651
- _vm.isSelectLikeCol(
7652
- col
7653
- )
7654
- ? _vm.getSelectLabel(
7655
- col._flatIndex,
7656
- _vm.getCellValue(
7657
- ri,
7658
- col._flatIndex
7659
- )
7660
- )
7661
- : _vm.getCellValue(
7662
- ri,
7663
- col._flatIndex
7664
- )
7665
- )
7577
+ "▾"
7666
7578
  ),
7667
7579
  ]
7668
7580
  ),
7669
7581
  ]
7670
- : [
7582
+ ),
7583
+ ]
7584
+ : _vm.hasPrefixSlot(
7585
+ col
7586
+ ) ||
7587
+ _vm.hasSuffixSlot(
7588
+ col
7589
+ )
7590
+ ? [
7591
+ _c(
7592
+ "span",
7593
+ {
7594
+ staticClass:
7595
+ "eg-cell-value",
7596
+ },
7597
+ [
7671
7598
  _vm._v(
7672
7599
  _vm._s(
7673
7600
  _vm.isSelectLikeCol(
@@ -7686,76 +7613,95 @@ var __vue_render__ = function () {
7686
7613
  )
7687
7614
  )
7688
7615
  ),
7689
- ],
7690
- _vm.hasSuffixSlot(
7691
- col
7692
- )
7693
- ? _c(
7694
- "span",
7695
- {
7696
- staticClass:
7697
- "eg-cell-suffix-wrap",
7698
- },
7699
- [
7700
- _vm._t(
7701
- "suffix-" +
7702
- col.key,
7703
- null,
7704
- {
7705
- row: _vm
7706
- ._dataRaw[
7707
- ri
7708
- ],
7709
- rowIndex:
7616
+ ]
7617
+ ),
7618
+ ]
7619
+ : [
7620
+ _vm._v(
7621
+ _vm._s(
7622
+ _vm.isSelectLikeCol(
7623
+ col
7624
+ )
7625
+ ? _vm.getSelectLabel(
7626
+ col._flatIndex,
7627
+ _vm.getCellValue(
7710
7628
  ri,
7711
- column:
7712
- col,
7713
- value:
7714
- _vm.getCellValue(
7715
- ri,
7716
- col._flatIndex
7717
- ),
7718
- }
7719
- ),
7720
- ],
7721
- 2
7629
+ col._flatIndex
7630
+ )
7631
+ )
7632
+ : _vm.getCellValue(
7633
+ ri,
7634
+ col._flatIndex
7635
+ )
7722
7636
  )
7723
- : _vm._e(),
7724
- ]
7725
- },
7726
- {
7727
- row: _vm
7728
- ._dataRaw[
7729
- ri
7730
- ],
7731
- rowIndex:
7732
- ri,
7733
- column:
7734
- col,
7735
- value:
7736
- _vm.getCellValue(
7737
- ri,
7738
- col._flatIndex
7739
- ),
7740
- isEditing: false,
7741
- done: null,
7742
- cancel:
7743
- null,
7744
- }
7745
- ),
7746
- ],
7747
- ],
7748
- 2
7749
- ),
7750
- ]
7751
- }
7752
- ),
7753
- ],
7754
- 2
7755
- )
7756
- : _vm._e(),
7757
- ]
7758
- ),
7637
+ ),
7638
+ ],
7639
+ _vm.hasSuffixSlot(
7640
+ col
7641
+ )
7642
+ ? _c(
7643
+ "span",
7644
+ {
7645
+ staticClass:
7646
+ "eg-cell-suffix-wrap",
7647
+ },
7648
+ [
7649
+ _vm._t(
7650
+ "suffix-" +
7651
+ col.key,
7652
+ null,
7653
+ {
7654
+ row: _vm
7655
+ ._dataRaw[
7656
+ ri
7657
+ ],
7658
+ rowIndex:
7659
+ ri,
7660
+ column:
7661
+ col,
7662
+ value:
7663
+ _vm.getCellValue(
7664
+ ri,
7665
+ col._flatIndex
7666
+ ),
7667
+ }
7668
+ ),
7669
+ ],
7670
+ 2
7671
+ )
7672
+ : _vm._e(),
7673
+ ]
7674
+ },
7675
+ {
7676
+ row: _vm
7677
+ ._dataRaw[
7678
+ ri
7679
+ ],
7680
+ rowIndex:
7681
+ ri,
7682
+ column: col,
7683
+ value:
7684
+ _vm.getCellValue(
7685
+ ri,
7686
+ col._flatIndex
7687
+ ),
7688
+ isEditing: false,
7689
+ done: null,
7690
+ cancel:
7691
+ null,
7692
+ }
7693
+ ),
7694
+ ],
7695
+ ],
7696
+ 2
7697
+ ),
7698
+ ]
7699
+ }
7700
+ ),
7701
+ ],
7702
+ 2
7703
+ )
7704
+ : _vm._e(),
7759
7705
  ]
7760
7706
  }),
7761
7707
  ],
@@ -8093,12 +8039,12 @@ __vue_render__._withStripped = true;
8093
8039
  /* style */
8094
8040
  const __vue_inject_styles__ = function (inject) {
8095
8041
  if (!inject) return
8096
- inject("data-v-348abe18_0", { source: "\n.eg-wrapper[data-v-348abe18] {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid #ccc;\n border-radius: 4px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;\n}\n.eg-container[data-v-348abe18] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-348abe18] {\n display: flex;\n flex-shrink: 0;\n box-shadow: 0 2px 0 0 #d0d0d0;\n position: relative;\n z-index: 31;\n}\n.eg-header-cell[data-v-348abe18] {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n border-right: 1px solid #ccc;\n border-bottom: 1px solid #ccc;\n background: #f0f0f0;\n font-size: 12px;\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-348abe18]:hover { background: #e0e0e0;\n}\n.eg-header-group[data-v-348abe18] { background: #e8eaf6;\n}\n.eg-col-resize-handle[data-v-348abe18] {\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-348abe18] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-348abe18] {\n border-top: 1px solid #ccc;\n}\n.eg-frozen-col-footer[data-v-348abe18],\n.eg-scroll-col-footer[data-v-348abe18],\n.eg-pinned-right-footer[data-v-348abe18] {\n position: relative;\n}\n.eg-footer-cell[data-v-348abe18] {\n box-sizing: border-box;\n border-right: 1px solid #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: #f8f9fa;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n.eg-row[data-v-348abe18] {\n position: relative;\n}\n.eg-cell[data-v-348abe18] {\n box-sizing: border-box;\n border-right: 1px solid #e0e0e0;\n border-bottom: 1px solid #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: white;\n cursor: default;\n display: flex;\n align-items: center;\n}\n.eg-cell-value[data-v-348abe18] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-348abe18]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-348abe18] {\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-348abe18] {\n flex-shrink: 0;\n color: #aaa;\n font-size: 11px;\n margin-left: 2px;\n}\n.eg-cell-suffix-wrap[data-v-348abe18] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-348abe18] {\n outline: 2px solid #1a73e8 !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-348abe18] {\n background: #e3f2fd !important;\n}\n.eg-cell-fill-preview[data-v-348abe18] {\n background: #bbdefb !important;\n outline: 1px dashed #1565c0;\n outline-offset: -1px;\n}\n.eg-cell-copy[data-v-348abe18] {\n outline: 1px dashed #1a73e8;\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-348abe18] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-348abe18] {\n outline: 2px solid #e53935 !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-348abe18] {\n width: 100%;\n height: 100%;\n border: none;\n outline: none;\n background: white;\n font-size: 13px;\n font-family: inherit;\n padding: 0;\n}\n.eg-row-header-cell[data-v-348abe18] {\n box-sizing: border-box;\n background: #f0f0f0;\n border-bottom: 1px solid #e0e0e0;\n border-right: 2px solid #bbb;\n text-align: center;\n font-size: 11px;\n color: #666;\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-348abe18] {\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-348abe18] {\n position: absolute;\n width: 6px;\n height: 6px;\n background: #1a73e8;\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n.eg-context-menu[data-v-348abe18] {\n position: fixed;\n background: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\n z-index: 9999;\n min-width: 160px;\n font-size: 13px;\n padding: 4px 0;\n}\n.eg-context-item[data-v-348abe18] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n.eg-context-item[data-v-348abe18]:hover { background: #f0f0f0;\n}\n.eg-context-separator[data-v-348abe18] {\n height: 1px;\n background: #e0e0e0;\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-348abe18]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-348abe18] {\n display: none;\n position: absolute;\n left: 100%;\n top: -4px;\n background: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\n min-width: 160px;\n padding: 4px 0;\n z-index: 10000;\n}\n.eg-context-submenu-parent:hover .eg-context-submenu[data-v-348abe18] { display: block;\n}\n.eg-group-btn[data-v-348abe18] {\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: #e8eaf6;\n border: 1px solid #9fa8da;\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n.eg-group-btn.collapsed[data-v-348abe18] { background: #c5cae9;\n}\n.eg-drag-ghost[data-v-348abe18] {\n position: absolute;\n border: 2px dashed #1a73e8;\n pointer-events: none;\n z-index: 20;\n}\n.eg-resize-line[data-v-348abe18] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-348abe18]::after {\n content: '';\n position: absolute;\n top: 0;\n right: -4px;\n width: 4px;\n height: 100%;\n background: linear-gradient(to right, rgba(0,0,0,0.1), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-body[data-v-348abe18] {\n position: sticky;\n right: 0;\n z-index: 10;\n background: white;\n flex-shrink: 0;\n}\n.eg-pinned-right-body[data-v-348abe18]::before {\n content: '';\n position: absolute;\n top: 0;\n left: -4px;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to left, rgba(0,0,0,0.10), transparent);\n pointer-events: none;\n}\n.eg-pinned-right-header[data-v-348abe18] {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid #bbb;\n background: #f0f0f0;\n}\n.eg-import-overlay[data-v-348abe18] {\n position: absolute;\n inset: 0;\n background: rgba(255, 255, 255, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: 4px;\n}\n.eg-import-dialog[data-v-348abe18] {\n background: white;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.15);\n text-align: center;\n}\n.eg-import-title[data-v-348abe18] {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 16px;\n}\n.eg-import-bar-wrap[data-v-348abe18] {\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 10px;\n}\n.eg-import-bar[data-v-348abe18] {\n height: 100%;\n background: linear-gradient(to right, #1a73e8, #4caf50);\n border-radius: 4px;\n transition: width 0.15s ease;\n}\n.eg-import-msg[data-v-348abe18] {\n font-size: 12px;\n color: #666;\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-348abe18] {\n font-size: 13px;\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-export-spinner[data-v-348abe18] {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid #e0e0e0;\n border-top-color: #1a73e8;\n border-radius: 50%;\n animation: eg-spin-data-v-348abe18 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-348abe18 {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-348abe18] {\n background: #f8f8f8;\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-348abe18] {\n background: #dce8f5;\n}\n.eg-cell-readonly.eg-cell-active[data-v-348abe18] {\n outline-color: #aaa;\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-348abe18] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-348abe18]::after {\n content: '▾';\n position: absolute;\n right: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: #aaa;\n font-size: 11px;\n pointer-events: none;\n line-height: 1;\n}\n\n/* ===== CHECKBOX CELL ===== */\n.eg-cell-checkbox[data-v-348abe18] {\n justify-content: center;\n padding: 0 2px;\n}\n.eg-validation-tooltip[data-v-348abe18] {\n background: #fff0f0;\n border: 1px solid #e53935;\n color: #c62828;\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 4px;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW TRANSITION ===== */\n.eg-row-enter-active[data-v-348abe18],\n.eg-row-leave-active[data-v-348abe18] {\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.eg-row-enter[data-v-348abe18],\n.eg-row-leave-to[data-v-348abe18] {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell[data-v-348abe18] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-348abe18] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-348abe18] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-348abe18] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-348abe18] {\n font-size: 10px;\n}\n.eg-size-small .eg-select-option[data-v-348abe18] {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-small .eg-context-menu[data-v-348abe18],\n.eg-size-small .eg-context-item[data-v-348abe18] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-348abe18] {\n padding: 4px 14px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-348abe18] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-348abe18] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-348abe18] {\n font-size: 13px;\n}\n.eg-size-large .eg-footer-cell[data-v-348abe18] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-348abe18] {\n font-size: 12px;\n}\n.eg-size-large .eg-select-option[data-v-348abe18] {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-size-large .eg-context-menu[data-v-348abe18],\n.eg-size-large .eg-context-item[data-v-348abe18] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-348abe18] {\n padding: 8px 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA+zGA;EACA,aAAA;EACA,sBAAA;EACA,gBAAA;EACA,sBAAA;EACA,sBAAA;EACA,kBAAA;EACA,6EAAA;AACA;AAEA;EACA,aAAA;EACA,sBAAA;EACA,OAAA;EACA,cAAA;EACA,kBAAA;AACA;AAEA;EACA,aAAA;EACA,cAAA;EACA,6BAAA;EACA,kBAAA;EACA,WAAA;AACA;AAEA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,sBAAA;EACA,4BAAA;EACA,6BAAA;EACA,mBAAA;EACA,eAAA;EACA,gBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;EACA,iBAAA;EACA,eAAA;AACA;AAEA,yCAAA,mBAAA;AAAA;AACA,oCAAA,mBAAA;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,0BAAA;AACA;AAEA;;;EAGA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,+BAAA;EACA,gBAAA;EACA,mBAAA;EACA,eAAA;EACA,gBAAA;EACA,cAAA;EACA,kBAAA;EACA,MAAA;EACA,mBAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;AACA;AAEA;EACA,kBAAA;AACA;AAEA;EACA,sBAAA;EACA,+BAAA;EACA,gCAAA;EACA,gBAAA;EACA,mBAAA;EACA,eAAA;EACA,cAAA;EACA,kBAAA;EACA,MAAA;EACA,iBAAA;EACA,eAAA;EACA,aAAA;EACA,mBAAA;AACA;AAEA;EACA,OAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;EACA,YAAA;AACA;AACA,8DAAA,aAAA;AAAA;AACA;EACA,OAAA;EACA,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,YAAA;AACA;AACA;EACA,cAAA;EACA,WAAA;EACA,eAAA;EACA,gBAAA;AACA;AACA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;AACA;AACA;EACA,qCAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,8BAAA;AACA;AAEA;EACA,8BAAA;EACA,2BAAA;EACA,oBAAA;AACA;AAEA;EACA,2BAAA;EACA,oBAAA;AACA;AAEA;EACA,wBAAA;AACA;AAEA;EACA,qCAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,WAAA;EACA,YAAA;EACA,YAAA;EACA,aAAA;EACA,iBAAA;EACA,eAAA;EACA,oBAAA;EACA,UAAA;AACA;AAEA;EACA,sBAAA;EACA,mBAAA;EACA,gCAAA;EACA,4BAAA;EACA,kBAAA;EACA,eAAA;EACA,WAAA;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,mBAAA;EACA,uBAAA;EACA,iBAAA;EACA,WAAA;AACA;AAEA;EACA,eAAA;EACA,iBAAA;EACA,sBAAA;EACA,kBAAA;EACA,yCAAA;EACA,aAAA;EACA,gBAAA;EACA,eAAA;EACA,cAAA;AACA;AAEA;EACA,iBAAA;EACA,eAAA;EACA,kBAAA;AACA;AAEA,0CAAA,mBAAA;AAAA;AAEA;EACA,WAAA;EACA,mBAAA;EACA,aAAA;AACA;AAEA,qDAAA,aAAA,EAAA,YAAA;AAAA;AAEA;EACA,aAAA;EACA,kBAAA;EACA,UAAA;EACA,SAAA;EACA,iBAAA;EACA,sBAAA;EACA,kBAAA;EACA,yCAAA;EACA,gBAAA;EACA,cAAA;EACA,cAAA;AACA;AAEA,wEAAA,cAAA;AAAA;AAEA;EACA,kBAAA;EACA,WAAA;EACA,YAAA;EACA,iBAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;EACA,kBAAA;EACA,UAAA;EACA,iBAAA;AACA;AAEA,2CAAA,mBAAA;AAAA;AAEA;EACA,kBAAA;EACA,0BAAA;EACA,oBAAA;EACA,WAAA;AACA;AAEA;EACA,oBAAA;EACA,aAAA;AACA;;AAEA,8BAAA;AACA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,mEAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,QAAA;EACA,WAAA;EACA,iBAAA;EACA,cAAA;AACA;AAEA;EACA,WAAA;EACA,kBAAA;EACA,MAAA;EACA,UAAA;EACA,SAAA;EACA,UAAA;EACA,mEAAA;EACA,oBAAA;AACA;AAEA;EACA,gBAAA;EACA,cAAA;EACA,2BAAA;EACA,mBAAA;AACA;AAEA;EACA,kBAAA;EACA,QAAA;EACA,qCAAA;EACA,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,YAAA;EACA,kBAAA;AACA;AAEA;EACA,iBAAA;EACA,yBAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,uCAAA;EACA,kBAAA;AACA;AAEA;EACA,eAAA;EACA,gBAAA;EACA,WAAA;EACA,mBAAA;AACA;AAEA;EACA,WAAA;EACA,mBAAA;EACA,kBAAA;EACA,gBAAA;EACA,mBAAA;AACA;AAEA;EACA,YAAA;EACA,uDAAA;EACA,kBAAA;EACA,4BAAA;AACA;AAEA;EACA,eAAA;EACA,WAAA;EACA,kBAAA;AACA;AAEA;EACA,eAAA;EACA,gBAAA;EACA,cAAA;AACA;AACA;EACA,WAAA;EACA,YAAA;EACA,mBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;EACA,uDAAA;AACA;AACA;AACA,KAAA,yBAAA;AAAA;AACA;;AAEA,8BAAA;AACA;EACA,mBAAA;EACA,eAAA;AACA;AACA;EACA,mBAAA;AACA;AACA;EACA,mBAAA;AACA;;AAEA,gCAAA;AACA;EACA,eAAA;AACA;AACA;EACA,YAAA;EACA,kBAAA;EACA,UAAA;EACA,QAAA;EACA,2BAAA;EACA,WAAA;EACA,eAAA;EACA,oBAAA;EACA,cAAA;AACA;;AAEA,8BAAA;AACA;EACA,uBAAA;EACA,cAAA;AACA;AACA;EACA,mBAAA;EACA,yBAAA;EACA,cAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;EACA,yCAAA;EACA,mBAAA;EACA,oBAAA;AACA;;AAEA,+BAAA;AACA;;EAEA,oDAAA;AACA;AACA;;EAEA,UAAA;EACA,2BAAA;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;;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","file":"ExcelGrid.vue","sourcesContent":["<template>\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 class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }\"\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 </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 }\"\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 }\"\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 }\"\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)' }\">\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-fc-' + ri\" :name=\"_rowTransitionName\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 </transition>\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-m-' + ri\" :name=\"_rowTransitionName\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 </transition>\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-pr-' + ri\" :name=\"_rowTransitionName\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\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>\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\">{{ getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</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 </transition>\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\" :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\" :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 :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 <!-- 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 </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth } 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 EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\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 },\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 /** Enable CSS list transitions on row enter/leave (data changes). Disabled during scroll. */\n transitionEnabled: { type: Boolean, default: false },\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 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 // Resize state\n _isScrolling: 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\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, 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 // 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 },\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 => col.showTotal) },\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 _rowTransitionName() {\n return this.transitionEnabled && !this._isScrolling ? 'eg-row' : ''\n },\n\n _sizeConf() { return SIZE_CONFIG[this.size] || SIZE_CONFIG.middle },\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: '#1a73e8',\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 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 headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: '#f8f9fa'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\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: '#f0f0f0'\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: '#fafafa'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative'\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 #ccc'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative' }\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: '#f8f9fa',\n boxShadow: '0 -2px 0 0 #d0d0d0'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderTop: '1px solid #ccc',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: '12px',\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 #ccc'\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: 'fixed',\n top: 0,\n left: this.resizing.currentPos + 'px',\n width: '2px',\n height: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'fixed',\n left: 0,\n top: this.resizing.currentPos + 'px',\n height: '2px',\n width: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size() {\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 this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n // Init colWidths\n this.leafCols.forEach(col => {\n if (this.colWidths[col._flatIndex] === undefined) {\n this.colWidths[col._flatIndex] = col.width || DEFAULT_COL_WIDTH\n }\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 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 },\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 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 clearTimeout(this._scrollEndTimer)\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 const result = findVisibleRange(\n this.scrollTop, Math.max(containerH, 100), this.totalRows, this.cumulativeHeights\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\n // Suppress row transitions during scroll to avoid animation on every virtual-scroll paint\n if (this.transitionEnabled) {\n this._isScrolling = true\n clearTimeout(this._scrollEndTimer)\n this._scrollEndTimer = setTimeout(() => { this._isScrolling = false }, 150)\n }\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n this.scrollTop = target.scrollTop\n this.scrollLeft = target.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 position: 'absolute',\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px',\n display: 'flex', alignItems: 'center', justifyContent: 'center',\n boxSizing: 'border-box',\n borderRight: '1px solid #ccc', borderBottom: '1px solid #ccc',\n background: '#f0f0f0', fontSize: '12px', fontWeight: 600,\n overflow: 'hidden', whiteSpace: 'nowrap', userSelect: 'none', cursor: 'pointer'\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-checkbox': !!(col && col.type === 'checkbox'),\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 }\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 position: 'absolute',\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n boxSizing: 'border-box',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\n background: '#f0f0f0',\n fontSize: '12px',\n fontWeight: 600,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n userSelect: 'none',\n cursor: 'pointer'\n }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!col.showTotal) 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 || !col.showTotal) return ''\n const v = this.columnTotals[ci]\n return v === null || v === undefined ? '-' : v\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 return {\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 },\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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 // 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 // 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 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 (this.selectDropdown) this.closeSelectDropdown()\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 === 'checkbox') 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 if (cell.isGroup) return\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.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: -1, col: cell.column._flatIndex }\n }\n },\n\n onRowContextMenu(e, ri) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: -1 }\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: ci }\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.editingCell) return // handled by onEditKeyDown\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 if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'checkbox') {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else {\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 }\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 (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\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 // Handled by focused wrapper\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 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 === 'checkbox') 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: cell-{key} slot only customizes display; dropdown still opens normally\n if (col.type === 'select' || col.type === 'treeSelect') {\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 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 // 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 colOptions(ci) {\n const col = this.leafCols[ci]\n if (!col || !Array.isArray(col.options)) return []\n return col.options.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) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n if (!Array.isArray(col.options)) return v\n const found = this._treeFindLabel(col.options, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci)\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: col.options || [],\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 (!col || !Array.isArray(col.options)) 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 (!colDef || !Array.isArray(colDef.options)) 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.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 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 if (typeof colDef.onSelect === 'function') {\n colDef.onSelect({ value, label }, newData[row], row, colDef)\n }\n }\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n closeSelectDropdown() {\n this.selectDropdown = null\n },\n\n _onWindowScrollForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\n },\n\n _onDocMouseDownForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\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.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 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 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.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 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 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 // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n const rect = this.$refs.wrapper.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 }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\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 }\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 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 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 this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\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 // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\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 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 },\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._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 }\n },\n\n redo() {\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 }\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid #ccc;\n border-radius: 4px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;\n}\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 #d0d0d0;\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 #ccc;\n border-bottom: 1px solid #ccc;\n background: #f0f0f0;\n font-size: 12px;\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: #e0e0e0; }\n.eg-header-group { background: #e8eaf6; }\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 #ccc;\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 #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: #f8f9fa;\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 #e0e0e0;\n border-bottom: 1px solid #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: white;\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--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: #aaa;\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 #1a73e8 !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: #e3f2fd !important;\n}\n\n.eg-cell-fill-preview {\n background: #bbdefb !important;\n outline: 1px dashed #1565c0;\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed #1a73e8;\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 #e53935 !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: white;\n font-size: 13px;\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: #f0f0f0;\n border-bottom: 1px solid #e0e0e0;\n border-right: 2px solid #bbb;\n text-align: center;\n font-size: 11px;\n color: #666;\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: #1a73e8;\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\n z-index: 9999;\n min-width: 160px;\n font-size: 13px;\n padding: 4px 0;\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n\n.eg-context-item:hover { background: #f0f0f0; }\n\n.eg-context-separator {\n height: 1px;\n background: #e0e0e0;\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: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\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.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: #e8eaf6;\n border: 1px solid #9fa8da;\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: #c5cae9; }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed #1a73e8;\n pointer-events: none;\n z-index: 20;\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, rgba(0,0,0,0.1), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: white;\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, rgba(0,0,0,0.10), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid #bbb;\n background: #f0f0f0;\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: rgba(255, 255, 255, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: 4px;\n}\n\n.eg-import-dialog {\n background: white;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.15);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, #1a73e8, #4caf50);\n border-radius: 4px;\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: #666;\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: 13px;\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid #e0e0e0;\n border-top-color: #1a73e8;\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: #f8f8f8;\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: #dce8f5;\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: #aaa;\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: #aaa;\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.eg-validation-tooltip {\n background: #fff0f0;\n border: 1px solid #e53935;\n color: #c62828;\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 4px;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW TRANSITION ===== */\n.eg-row-enter-active,\n.eg-row-leave-active {\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.eg-row-enter,\n.eg-row-leave-to {\n opacity: 0;\n transform: translateY(-4px);\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\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: 13px;\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</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: 13px;\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 #d1d5db;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n user-select: none;\n color: #333;\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: #1a73e8; }\n.eg-select-trigger--open {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-trigger--disabled { background: #f5f5f5; cursor: default; color: #aaa; }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: #aaa; }\n.eg-select-caret { flex-shrink: 0; color: #888; font-size: 12px; }\n.eg-select-dropdown {\n background: #fff;\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\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 #ebebeb;\n position: sticky;\n top: 0;\n background: #fff;\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #d1d5db;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n outline: none;\n color: #333;\n background: #fff;\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: 13px;\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: #333;\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: #f0f4ff; }\n.eg-select-option--active { font-weight: 600; color: #1a73e8; }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: #e8f0fe; }\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: #888;\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: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid #c2c8d1;\n border-radius: 4px;\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: #1a73e8; }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: #1a73e8;\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: #1a73e8;\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px rgba(26,115,232,0.2); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: #f0f1f3; border-color: #dadde2; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: #b9c2ce; border-color: #b9c2ce; }\n</style>\n"]}, media: undefined })
8097
- ,inject("data-v-348abe18_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/* ── 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: 13px;\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 #d1d5db;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n user-select: none;\n color: #333;\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: #1a73e8;\n}\n.eg-select-trigger--open {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-trigger--disabled { background: #f5f5f5; cursor: default; color: #aaa;\n}\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.eg-select-placeholder { color: #aaa;\n}\n.eg-select-caret { flex-shrink: 0; color: #888; font-size: 12px;\n}\n.eg-select-dropdown {\n background: #fff;\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\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 #ebebeb;\n position: sticky;\n top: 0;\n background: #fff;\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #d1d5db;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n outline: none;\n color: #333;\n background: #fff;\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: 13px;\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: #333;\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: #f0f4ff;\n}\n.eg-select-option--active { font-weight: 600; color: #1a73e8;\n}\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: #e8f0fe;\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: #888;\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: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid #c2c8d1;\n border-radius: 4px;\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: #1a73e8;\n}\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: #1a73e8;\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: #1a73e8;\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px rgba(26,115,232,0.2);\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: #f0f1f3; border-color: #dadde2;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: #b9c2ce; border-color: #b9c2ce;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4zHA,wFAAA;AACA;EACA,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,eAAA;EACA,oBAAA;AACA;AACA;EACA,aAAA;EACA,mBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;EACA,gBAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,iBAAA;EACA,WAAA;EACA,wDAAA;AACA;AACA,2BAAA,qBAAA;AAAA;AACA;EACA,qBAAA;EACA,2CAAA;AACA;AACA,+BAAA,mBAAA,EAAA,eAAA,EAAA,WAAA;AAAA;AACA,2BAAA,OAAA,EAAA,gBAAA,EAAA,uBAAA,EAAA,mBAAA;AAAA;AACA,yBAAA,WAAA;AAAA;AACA,mBAAA,cAAA,EAAA,WAAA,EAAA,eAAA;AAAA;AACA;EACA,gBAAA;EACA,yBAAA;EACA,kBAAA;EACA,uCAAA;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,gCAAA;EACA,gBAAA;EACA,MAAA;EACA,gBAAA;EACA,UAAA;AACA;AACA;EACA,WAAA;EACA,sBAAA;EACA,yBAAA;EACA,kBAAA;EACA,gBAAA;EACA,eAAA;EACA,aAAA;EACA,WAAA;EACA,gBAAA;EACA,oBAAA;AACA;AACA;EACA,qBAAA;EACA,2CAAA;AACA;AACA;EACA,aAAA;EACA,uBAAA;EACA,QAAA;EACA,iBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,sBAAA;EACA,iBAAA;EACA,WAAA;AACA;AACA;iCACA,mBAAA;AAAA;AACA,4BAAA,gBAAA,EAAA,cAAA;AAAA;AACA;kCACA,mBAAA;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,WAAA;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,sBAAA;EACA,4BAAA;EACA,2BAAA;EACA,oBAAA;EACA,2BAAA;EACA,kBAAA;EACA,yFAAA;AACA;AACA,sCAAA,qBAAA;AAAA;AACA;EACA,yBAAA;EACA,+SAAA;EACA,wBAAA;EACA,qBAAA;AACA;AACA,sDAAA,0CAAA;AAAA;AACA;4CACA,eAAA;AAAA;AACA,iDAAA,yBAAA,EAAA,qBAAA;AAAA;AACA,yDAAA,yBAAA,EAAA,qBAAA;AAAA","file":"ExcelGrid.vue","sourcesContent":["<template>\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 class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }\"\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 </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 }\"\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 }\"\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 }\"\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)' }\">\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-fc-' + ri\" :name=\"_rowTransitionName\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'fc-row-' + ri\" class=\"eg-row\" :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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 </transition>\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-m-' + ri\" :name=\"_rowTransitionName\">\n <div\n v-if=\"!isRowHidden(ri)\"\n :key=\"'row-' + ri\"\n class=\"eg-row\"\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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 </transition>\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)' }\">\n <template v-for=\"ri in visibleRowRange\">\n <transition :key=\"'tr-pr-' + ri\" :name=\"_rowTransitionName\">\n <div v-if=\"!isRowHidden(ri)\" :key=\"'pr-row-' + ri\" class=\"eg-row\" :style=\"getRowStyle(ri)\">\n <template v-for=\"col in pinnedRightCols\">\n <div\n :key=\"'pr-cell-' + ri + '-' + col._flatIndex\"\n class=\"eg-cell\"\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>\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\">{{ getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</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 </transition>\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\" :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\" :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 :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 <!-- 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 </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth } 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 EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\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 },\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 /** Enable CSS list transitions on row enter/leave (data changes). Disabled during scroll. */\n transitionEnabled: { type: Boolean, default: false },\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 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 // Resize state\n _isScrolling: 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\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, 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 // 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 },\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 => col.showTotal) },\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 _rowTransitionName() {\n return this.transitionEnabled && !this._isScrolling ? 'eg-row' : ''\n },\n\n _sizeConf() { return SIZE_CONFIG[this.size] || SIZE_CONFIG.middle },\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: '#1a73e8',\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 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 headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: '#f8f9fa'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\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: '#f0f0f0'\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: '#fafafa'\n }\n },\n\n scrollBodyStyle() {\n return {\n flex: 1,\n overflow: 'auto',\n position: 'relative'\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 #ccc'\n }\n },\n\n scrollRowHeadersStyle() {\n return { flex: 1, overflow: 'hidden', position: 'relative' }\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: '#f8f9fa',\n boxShadow: '0 -2px 0 0 #d0d0d0'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderTop: '1px solid #ccc',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: '12px',\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 #ccc'\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: 'fixed',\n top: 0,\n left: this.resizing.currentPos + 'px',\n width: '2px',\n height: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'fixed',\n left: 0,\n top: this.resizing.currentPos + 'px',\n height: '2px',\n width: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size() {\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 this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n // Init colWidths\n this.leafCols.forEach(col => {\n if (this.colWidths[col._flatIndex] === undefined) {\n this.colWidths[col._flatIndex] = col.width || DEFAULT_COL_WIDTH\n }\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 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 },\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 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 clearTimeout(this._scrollEndTimer)\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 const result = findVisibleRange(\n this.scrollTop, Math.max(containerH, 100), this.totalRows, this.cumulativeHeights\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\n // Suppress row transitions during scroll to avoid animation on every virtual-scroll paint\n if (this.transitionEnabled) {\n this._isScrolling = true\n clearTimeout(this._scrollEndTimer)\n this._scrollEndTimer = setTimeout(() => { this._isScrolling = false }, 150)\n }\n\n // Throttle reactive state updates + virtual scroll recalc to one rAF per frame\n if (!this._scrollRAF) {\n this._scrollRAF = requestAnimationFrame(() => {\n this._scrollRAF = null\n this.scrollTop = target.scrollTop\n this.scrollLeft = target.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 position: 'absolute',\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px',\n display: 'flex', alignItems: 'center', justifyContent: 'center',\n boxSizing: 'border-box',\n borderRight: '1px solid #ccc', borderBottom: '1px solid #ccc',\n background: '#f0f0f0', fontSize: '12px', fontWeight: 600,\n overflow: 'hidden', whiteSpace: 'nowrap', userSelect: 'none', cursor: 'pointer'\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-checkbox': !!(col && col.type === 'checkbox'),\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 }\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 position: 'absolute',\n top: (this._headerRowH * rowIndex) + 'px',\n left: left + 'px',\n width: width + 'px',\n height: (this._headerRowH * cell.rowspan) + 'px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n boxSizing: 'border-box',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\n background: '#f0f0f0',\n fontSize: '12px',\n fontWeight: 600,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n userSelect: 'none',\n cursor: 'pointer'\n }\n },\n\n // =========================================================\n // FOOTER (column totals)\n // =========================================================\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!col.showTotal) 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 || !col.showTotal) return ''\n const v = this.columnTotals[ci]\n return v === null || v === undefined ? '-' : v\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 return {\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 },\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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 // 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 // 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 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 (this.selectDropdown) this.closeSelectDropdown()\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 === 'checkbox') 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 if (cell.isGroup) return\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.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: -1, col: cell.column._flatIndex }\n }\n },\n\n onRowContextMenu(e, ri) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: -1 }\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: ci }\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.editingCell) return // handled by onEditKeyDown\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 if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'checkbox') {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else {\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 }\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 (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\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 // Handled by focused wrapper\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 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 === 'checkbox') 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: cell-{key} slot only customizes display; dropdown still opens normally\n if (col.type === 'select' || col.type === 'treeSelect') {\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 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 // 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 colOptions(ci) {\n const col = this.leafCols[ci]\n if (!col || !Array.isArray(col.options)) return []\n return col.options.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) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n if (!Array.isArray(col.options)) return v\n const found = this._treeFindLabel(col.options, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci)\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: col.options || [],\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 (!col || !Array.isArray(col.options)) 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 (!colDef || !Array.isArray(colDef.options)) 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.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 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 if (typeof colDef.onSelect === 'function') {\n colDef.onSelect({ value, label }, newData[row], row, colDef)\n }\n }\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n closeSelectDropdown() {\n this.selectDropdown = null\n },\n\n _onWindowScrollForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\n },\n\n _onDocMouseDownForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\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.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 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 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.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 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 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 // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n const rect = this.$refs.wrapper.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 }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\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 }\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 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 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 this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\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 // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\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 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 },\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._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 }\n },\n\n redo() {\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 }\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-wrapper {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-sizing: border-box;\n border: 1px solid #ccc;\n border-radius: 4px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;\n}\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 #d0d0d0;\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 #ccc;\n border-bottom: 1px solid #ccc;\n background: #f0f0f0;\n font-size: 12px;\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: #e0e0e0; }\n.eg-header-group { background: #e8eaf6; }\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 #ccc;\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 #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: #f8f9fa;\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 #e0e0e0;\n border-bottom: 1px solid #e0e0e0;\n overflow: hidden;\n white-space: nowrap;\n font-size: 13px;\n padding: 0 4px;\n position: absolute;\n top: 0;\n background: white;\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--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: #aaa;\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 #1a73e8 !important;\n outline-offset: -2px;\n z-index: 3;\n}\n\n.eg-cell-selected {\n background: #e3f2fd !important;\n}\n\n.eg-cell-fill-preview {\n background: #bbdefb !important;\n outline: 1px dashed #1565c0;\n outline-offset: -1px;\n}\n\n.eg-cell-copy {\n outline: 1px dashed #1a73e8;\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 #e53935 !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: white;\n font-size: 13px;\n font-family: inherit;\n padding: 0;\n}\n\n.eg-row-header-cell {\n box-sizing: border-box;\n background: #f0f0f0;\n border-bottom: 1px solid #e0e0e0;\n border-right: 2px solid #bbb;\n text-align: center;\n font-size: 11px;\n color: #666;\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: #1a73e8;\n border: 1px solid white;\n cursor: crosshair;\n z-index: 10;\n}\n\n.eg-context-menu {\n position: fixed;\n background: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\n z-index: 9999;\n min-width: 160px;\n font-size: 13px;\n padding: 4px 0;\n}\n\n.eg-context-item {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n\n.eg-context-item:hover { background: #f0f0f0; }\n\n.eg-context-separator {\n height: 1px;\n background: #e0e0e0;\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: white;\n border: 1px solid #ccc;\n border-radius: 4px;\n box-shadow: 2px 4px 12px rgba(0,0,0,0.15);\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.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: #e8eaf6;\n border: 1px solid #9fa8da;\n border-radius: 2px;\n z-index: 5;\n user-select: none;\n}\n\n.eg-group-btn.collapsed { background: #c5cae9; }\n\n.eg-drag-ghost {\n position: absolute;\n border: 2px dashed #1a73e8;\n pointer-events: none;\n z-index: 20;\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, rgba(0,0,0,0.1), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-body {\n position: sticky;\n right: 0;\n z-index: 10;\n background: white;\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, rgba(0,0,0,0.10), transparent);\n pointer-events: none;\n}\n\n.eg-pinned-right-header {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid #bbb;\n background: #f0f0f0;\n}\n\n.eg-import-overlay {\n position: absolute;\n inset: 0;\n background: rgba(255, 255, 255, 0.85);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n border-radius: 4px;\n}\n\n.eg-import-dialog {\n background: white;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n padding: 24px 32px;\n min-width: 280px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.15);\n text-align: center;\n}\n\n.eg-import-title {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 16px;\n}\n\n.eg-import-bar-wrap {\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 10px;\n}\n\n.eg-import-bar {\n height: 100%;\n background: linear-gradient(to right, #1a73e8, #4caf50);\n border-radius: 4px;\n transition: width 0.15s ease;\n}\n\n.eg-import-msg {\n font-size: 12px;\n color: #666;\n margin-bottom: 4px;\n}\n\n.eg-import-pct {\n font-size: 13px;\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-export-spinner {\n width: 32px;\n height: 32px;\n margin: 12px auto 0;\n border: 3px solid #e0e0e0;\n border-top-color: #1a73e8;\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: #f8f8f8;\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected {\n background: #dce8f5;\n}\n.eg-cell-readonly.eg-cell-active {\n outline-color: #aaa;\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: #aaa;\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.eg-validation-tooltip {\n background: #fff0f0;\n border: 1px solid #e53935;\n color: #c62828;\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 4px;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);\n white-space: nowrap;\n pointer-events: none;\n}\n\n/* ===== ROW TRANSITION ===== */\n.eg-row-enter-active,\n.eg-row-leave-active {\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.eg-row-enter,\n.eg-row-leave-to {\n opacity: 0;\n transform: translateY(-4px);\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\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: 13px;\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</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: 13px;\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 #d1d5db;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n user-select: none;\n color: #333;\n transition: border-color .15s ease, box-shadow .15s ease;\n}\n.eg-select-trigger:hover { border-color: #1a73e8; }\n.eg-select-trigger--open {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-trigger--disabled { background: #f5f5f5; cursor: default; color: #aaa; }\n.eg-select-trigger-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.eg-select-placeholder { color: #aaa; }\n.eg-select-caret { flex-shrink: 0; color: #888; font-size: 12px; }\n.eg-select-dropdown {\n background: #fff;\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.15);\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 #ebebeb;\n position: sticky;\n top: 0;\n background: #fff;\n z-index: 1;\n}\n.eg-select-search-input {\n width: 100%;\n box-sizing: border-box;\n border: 1px solid #d1d5db;\n border-radius: 4px;\n padding: 4px 8px;\n font-size: 13px;\n outline: none;\n color: #333;\n background: #fff;\n font-family: inherit;\n}\n.eg-select-search-input:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 6px 12px;\n font-size: 13px;\n cursor: pointer;\n white-space: normal;\n word-break: break-word;\n user-select: none;\n color: #333;\n}\n.eg-select-option:hover,\n.eg-select-option--highlighted { background: #f0f4ff; }\n.eg-select-option--active { font-weight: 600; color: #1a73e8; }\n.eg-select-option--active.eg-select-option--highlighted,\n.eg-select-option--active:hover { background: #e8f0fe; }\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: #888;\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: #fff;\n background-repeat: no-repeat;\n background-position: center;\n background-size: 0 0;\n border: 1.5px solid #c2c8d1;\n border-radius: 4px;\n transition: background-color .12s ease, border-color .12s ease, background-size .12s ease;\n}\n.eg-checkbox:hover .eg-checkbox-box { border-color: #1a73e8; }\n.eg-checkbox-input:checked + .eg-checkbox-box {\n background-color: #1a73e8;\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTMuNSA4LjNsMyAzIDYtNi42IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==\");\n background-size: 70% 70%;\n border-color: #1a73e8;\n}\n.eg-checkbox-input:focus-visible + .eg-checkbox-box { box-shadow: 0 0 0 3px rgba(26,115,232,0.2); }\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input { cursor: default; }\n.eg-checkbox-input:disabled + .eg-checkbox-box { background-color: #f0f1f3; border-color: #dadde2; }\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box { background-color: #b9c2ce; border-color: #b9c2ce; }\n</style>\n"]}, media: undefined });
8042
+ inject("data-v-3d10518e_0", { source: "\n.eg-wrapper[data-v-3d10518e] {\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: #e2efda;\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 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-container[data-v-3d10518e] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-3d10518e] {\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-3d10518e] {\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-3d10518e]:hover { background: var(--eg-color-border-cell);\n}\n.eg-header-group[data-v-3d10518e] { background: var(--eg-color-header-group-bg);\n}\n.eg-col-resize-handle[data-v-3d10518e] {\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-3d10518e] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-3d10518e] {\n border-top: 1px solid var(--eg-color-border);\n}\n.eg-frozen-col-footer[data-v-3d10518e],\n.eg-scroll-col-footer[data-v-3d10518e],\n.eg-pinned-right-footer[data-v-3d10518e] {\n position: relative;\n}\n.eg-footer-cell[data-v-3d10518e] {\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-3d10518e] {\n position: relative;\n}\n.eg-cell[data-v-3d10518e] {\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-3d10518e] {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n.eg-cell--has-suffix.eg-cell-select[data-v-3d10518e]::after { display: none;\n}\n.eg-cell-select-trigger-wrap[data-v-3d10518e] {\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-3d10518e] {\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-3d10518e] {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n}\n.eg-cell-active[data-v-3d10518e] {\n outline: 2px solid var(--eg-color-accent) !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-3d10518e] {\n background: var(--eg-color-cell-selected) !important;\n}\n.eg-cell-fill-preview[data-v-3d10518e] {\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-3d10518e] {\n outline: 1px dashed var(--eg-color-accent);\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-3d10518e] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-3d10518e] {\n outline: 2px solid var(--eg-color-error) !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n.eg-context-item[data-v-3d10518e]:hover { background: var(--eg-color-header-bg);\n}\n.eg-context-separator[data-v-3d10518e] {\n height: 1px;\n background: var(--eg-color-border-cell);\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-3d10518e]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-3d10518e] {\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-3d10518e] { display: block;\n}\n.eg-group-btn[data-v-3d10518e] {\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-3d10518e] { background: var(--eg-color-group-bg-collapsed);\n}\n.eg-drag-ghost[data-v-3d10518e] {\n position: absolute;\n border: 2px dashed var(--eg-color-accent);\n pointer-events: none;\n z-index: 20;\n}\n.eg-resize-line[data-v-3d10518e] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-3d10518e]::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-3d10518e] {\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-3d10518e]::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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\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-3d10518e] {\n font-size: 12px;\n color: var(--eg-color-text-secondary);\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-3d10518e] {\n font-size: var(--eg-font-size);\n font-weight: 600;\n color: var(--eg-color-accent);\n}\n.eg-export-spinner[data-v-3d10518e] {\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-3d10518e 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-3d10518e {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-3d10518e] {\n background: var(--eg-color-cell-readonly);\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-3d10518e] {\n background: var(--eg-color-cell-readonly-selected);\n}\n.eg-cell-readonly.eg-cell-active[data-v-3d10518e] {\n outline-color: var(--eg-color-text-muted);\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-3d10518e] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-3d10518e]::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-3d10518e] {\n justify-content: center;\n padding: 0 2px;\n}\n.eg-validation-tooltip[data-v-3d10518e] {\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-3d10518e]: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-3d10518e]: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-3d10518e] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-3d10518e] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-3d10518e] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-3d10518e] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-3d10518e] {\n font-size: 10px;\n}\n.eg-size-small .eg-select-option[data-v-3d10518e] {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-small .eg-context-menu[data-v-3d10518e],\n.eg-size-small .eg-context-item[data-v-3d10518e] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-3d10518e] {\n padding: 4px 14px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-3d10518e] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-3d10518e] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-3d10518e] {\n font-size: var(--eg-font-size);\n}\n.eg-size-large .eg-footer-cell[data-v-3d10518e] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-3d10518e] {\n font-size: 12px;\n}\n.eg-size-large .eg-select-option[data-v-3d10518e] {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-size-large .eg-context-menu[data-v-3d10518e],\n.eg-size-large .eg-context-item[data-v-3d10518e] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-3d10518e] {\n padding: 8px 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA4xGA;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,aAAA;EACA,sBAAA;EACA,gBAAA;EACA,sBAAA;EACA,wCAAA;EACA,+BAAA;EACA,kCAAA;AACA;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,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;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;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;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;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;;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","file":"ExcelGrid.vue","sourcesContent":["<template>\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 class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }\"\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 </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 }\"\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 }\"\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 }\"\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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 :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>\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\">{{ getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</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\" :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\" :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 :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 <!-- 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 </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth } 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 EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\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 },\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 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\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, 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 // 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 },\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 => col.showTotal) },\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.size] || SIZE_CONFIG.middle },\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: '#1a73e8',\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 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 headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: '#f8f9fa'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\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: '#f0f0f0'\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: '#fafafa'\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 #ccc'\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: '#f8f9fa',\n boxShadow: '0 -2px 0 0 #d0d0d0'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderTop: '1px solid #ccc',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: '12px',\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 #ccc'\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: 'fixed',\n top: 0,\n left: this.resizing.currentPos + 'px',\n width: '2px',\n height: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'fixed',\n left: 0,\n top: this.resizing.currentPos + 'px',\n height: '2px',\n width: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size() {\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 this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n // Init colWidths\n this.leafCols.forEach(col => {\n if (this.colWidths[col._flatIndex] === undefined) {\n this.colWidths[col._flatIndex] = col.width || DEFAULT_COL_WIDTH\n }\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 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 },\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 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 },\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\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-checkbox': !!(col && col.type === 'checkbox'),\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 }\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 // =========================================================\n // FOOTER (column totals)\n // =========================================================\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!col.showTotal) 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 || !col.showTotal) return ''\n const v = this.columnTotals[ci]\n return v === null || v === undefined ? '-' : v\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 return {\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 },\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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 // 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 // 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 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 (this.selectDropdown) this.closeSelectDropdown()\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 === 'checkbox') 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 if (cell.isGroup) return\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.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: -1, col: cell.column._flatIndex }\n }\n },\n\n onRowContextMenu(e, ri) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: -1 }\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: ci }\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.editingCell) return // handled by onEditKeyDown\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 if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'checkbox') {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else {\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 }\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 (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\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 // Handled by focused wrapper\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 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 === 'checkbox') 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: cell-{key} slot only customizes display; dropdown still opens normally\n if (col.type === 'select' || col.type === 'treeSelect') {\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 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 // 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 colOptions(ci) {\n const col = this.leafCols[ci]\n if (!col || !Array.isArray(col.options)) return []\n return col.options.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) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n if (!Array.isArray(col.options)) return v\n const found = this._treeFindLabel(col.options, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci)\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: col.options || [],\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 (!col || !Array.isArray(col.options)) 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 (!colDef || !Array.isArray(colDef.options)) 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.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 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 if (typeof colDef.onSelect === 'function') {\n colDef.onSelect({ value, label }, newData[row], row, colDef)\n }\n }\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n closeSelectDropdown() {\n this.selectDropdown = null\n },\n\n _onWindowScrollForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\n },\n\n _onDocMouseDownForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\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.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 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 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.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 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 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 // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n const rect = this.$refs.wrapper.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 }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\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 }\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 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 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 this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\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 // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\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 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 },\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._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 }\n },\n\n redo() {\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 }\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-wrapper {\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: #e2efda;\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 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-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--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}\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.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.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.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\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</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 })
8043
+ ,inject("data-v-3d10518e_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/* ── 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAw1HA,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-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 class=\"eg-context-menu\"\n :style=\"{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }\"\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 </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 }\"\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 }\"\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 }\"\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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 :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>\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\">{{ getSelectLabel(ci, getCellValue(ri, ci)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}</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 :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>\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\">{{ getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) }}</span><span class=\"eg-cell-select-caret-inline\">▾</span></span></template><template v-else-if=\"hasPrefixSlot(col) || hasSuffixSlot(col)\"><span class=\"eg-cell-value\">{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</span></template><template v-else>{{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}</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\" :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\" :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 :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 <!-- 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 </div>\n</template>\n\n<script>\nimport {\n normalizeRange, isCellInRange, isCellInAnyRange,\n buildRange, singleCellRange, getPrimaryRange\n} from '../utils/selection.js'\nimport { flattenColumns, buildHeaderRows, columnTreeDepth } 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 EgSelect from './EgSelect.vue'\nimport EgTreeSelect from './EgTreeSelect.vue'\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 },\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 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\n // Select dropdown\n selectDropdown: null, // { row, col, options, currentValue, 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 // 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 },\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 => col.showTotal) },\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.size] || SIZE_CONFIG.middle },\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: '#1a73e8',\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 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 headerAreaStyle() {\n return {\n display: 'flex',\n flexDirection: 'row',\n position: 'sticky',\n top: 0,\n zIndex: 30,\n background: '#f8f9fa'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderBottom: '1px solid #ccc',\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: '#f0f0f0'\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: '#fafafa'\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 #ccc'\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: '#f8f9fa',\n boxShadow: '0 -2px 0 0 #d0d0d0'\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: '#f0f0f0',\n borderRight: '1px solid #ccc',\n borderTop: '1px solid #ccc',\n boxSizing: 'border-box',\n flexShrink: 0,\n fontSize: '12px',\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 #ccc'\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: 'fixed',\n top: 0,\n left: this.resizing.currentPos + 'px',\n width: '2px',\n height: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n } else {\n return {\n position: 'fixed',\n left: 0,\n top: this.resizing.currentPos + 'px',\n height: '2px',\n width: '100%',\n background: '#1a73e8',\n pointerEvents: 'none',\n zIndex: 9999\n }\n }\n }\n },\n\n watch: {\n size() {\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 this.leafCols = flattenColumns(cols)\n this.headerDepth = columnTreeDepth(cols)\n this.headerRows = buildHeaderRows(cols, this.headerDepth)\n // Init colWidths\n this.leafCols.forEach(col => {\n if (this.colWidths[col._flatIndex] === undefined) {\n this.colWidths[col._flatIndex] = col.width || DEFAULT_COL_WIDTH\n }\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 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 },\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 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 },\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\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-checkbox': !!(col && col.type === 'checkbox'),\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 }\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 // =========================================================\n // FOOTER (column totals)\n // =========================================================\n computeColumnTotals() {\n const totals = {}\n const rows = this._dataRaw || []\n this.leafCols.forEach(col => {\n if (!col.showTotal) 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 || !col.showTotal) return ''\n const v = this.columnTotals[ci]\n return v === null || v === undefined ? '-' : v\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 return {\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 },\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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: '#e8eaf6',\n border: '1px solid #9fa8da',\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 // 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 // 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 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 (this.selectDropdown) this.closeSelectDropdown()\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 === 'checkbox') 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 if (cell.isGroup) return\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.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: -1, col: cell.column._flatIndex }\n }\n },\n\n onRowContextMenu(e, ri) {\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: -1 }\n },\n\n onCellContextMenu(e, ri, ci) {\n if (!this.isSelected(ri, ci)) {\n this.selectCell(ri, ci)\n }\n this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, row: ri, col: ci }\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.editingCell) return // handled by onEditKeyDown\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 if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n this.openSelectDropdownForActive()\n } else if (activeCol && activeCol.type === 'checkbox') {\n this.toggleCheckbox(this.activeCell.row, this.activeCell.col)\n } else {\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 }\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 (e.ctrlKey || e.metaKey || e.altKey) return\n if (e.key && e.key.length === 1) {\n const activeCol = this.leafCols[this.activeCell.col]\n if (activeCol && this.isSelectLikeCol(activeCol) && !this._hasCustomEdit(activeCol)) {\n e.preventDefault()\n this.openSelectDropdownForActive()\n return\n }\n if (activeCol && activeCol.type === 'checkbox') {\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 // Handled by focused wrapper\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 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 === 'checkbox') 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: cell-{key} slot only customizes display; dropdown still opens normally\n if (col.type === 'select' || col.type === 'treeSelect') {\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 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 // 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 colOptions(ci) {\n const col = this.leafCols[ci]\n if (!col || !Array.isArray(col.options)) return []\n return col.options.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) {\n const col = this.leafCols[ci]\n const v = value == null ? '' : String(value)\n if (col && col.type === 'treeSelect') {\n if (!Array.isArray(col.options)) return v\n const found = this._treeFindLabel(col.options, this.treeOptionKeys(col), v)\n return found != null ? found : v\n }\n const opts = this.colOptions(ci)\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: col.options || [],\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 (!col || !Array.isArray(col.options)) 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 (!colDef || !Array.isArray(colDef.options)) 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.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 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 if (typeof colDef.onSelect === 'function') {\n colDef.onSelect({ value, label }, newData[row], row, colDef)\n }\n }\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n closeSelectDropdown() {\n this.selectDropdown = null\n },\n\n _onWindowScrollForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\n },\n\n _onDocMouseDownForDropdown(e) {\n if (!this.selectDropdown) return\n const comp = this.$refs.selectDropdownComp\n const dd = comp && comp.$el ? comp.$el : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\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.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 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 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.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 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 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 // RESIZE (3.5)\n // =========================================================\n startColResize(e, ci) {\n const rect = this.$refs.wrapper.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 }\n e.preventDefault()\n },\n\n startRowResize(e, ri) {\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 }\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 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 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 this.$emit('update:hiddenRows', [])\n },\n\n ctxUnhideColumns() {\n this.hideContextMenu()\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 // MERGE CELLS (3.8)\n // =========================================================\n mergeCells(range) {\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 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 },\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._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 }\n },\n\n redo() {\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 }\n }\n }\n}\n</script>\n\n<style scoped>\n.eg-wrapper {\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: #e2efda;\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 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-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--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}\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.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.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.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\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</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 });
8098
8044
 
8099
8045
  };
8100
8046
  /* scoped */
8101
- const __vue_scope_id__ = "data-v-348abe18";
8047
+ const __vue_scope_id__ = "data-v-3d10518e";
8102
8048
  /* module identifier */
8103
8049
  const __vue_module_identifier__ = undefined;
8104
8050
  /* functional template */