vue2-excel-grid 1.1.16 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vue2-excel-grid.esm.js +637 -1626
- package/dist/vue2-excel-grid.umd.js +1 -1
- package/package.json +1 -1
- package/src/components/ExcelGrid.vue +437 -88
- package/src/index.js +1 -7
- package/src/components/EgCheckbox.vue +0 -96
- package/src/components/EgSelect.vue +0 -377
- package/src/components/EgTreeSelect.vue +0 -357
|
@@ -1283,1376 +1283,6 @@ function getMessages(locale) {
|
|
|
1283
1283
|
return messages[locale] || messages.en
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
|
|
1300
|
-
var script$3 = {
|
|
1301
|
-
name: 'EgCheckbox',
|
|
1302
|
-
model: { prop: 'value', event: 'input' },
|
|
1303
|
-
props: {
|
|
1304
|
-
value: { default: false },
|
|
1305
|
-
trueValue: { default: true },
|
|
1306
|
-
falseValue: { default: false },
|
|
1307
|
-
disabled: { type: Boolean, default: false }
|
|
1308
|
-
},
|
|
1309
|
-
computed: {
|
|
1310
|
-
isChecked() {
|
|
1311
|
-
return this.value === this.trueValue
|
|
1312
|
-
}
|
|
1313
|
-
},
|
|
1314
|
-
methods: {
|
|
1315
|
-
onChange(e) {
|
|
1316
|
-
const val = e.target.checked ? this.trueValue : this.falseValue;
|
|
1317
|
-
this.$emit('input', val);
|
|
1318
|
-
this.$emit('change', val);
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
|
|
1323
|
-
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
1324
|
-
if (typeof shadowMode !== 'boolean') {
|
|
1325
|
-
createInjectorSSR = createInjector;
|
|
1326
|
-
createInjector = shadowMode;
|
|
1327
|
-
shadowMode = false;
|
|
1328
|
-
}
|
|
1329
|
-
// Vue.extend constructor export interop.
|
|
1330
|
-
const options = typeof script === 'function' ? script.options : script;
|
|
1331
|
-
// render functions
|
|
1332
|
-
if (template && template.render) {
|
|
1333
|
-
options.render = template.render;
|
|
1334
|
-
options.staticRenderFns = template.staticRenderFns;
|
|
1335
|
-
options._compiled = true;
|
|
1336
|
-
// functional template
|
|
1337
|
-
if (isFunctionalTemplate) {
|
|
1338
|
-
options.functional = true;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
// scopedId
|
|
1342
|
-
if (scopeId) {
|
|
1343
|
-
options._scopeId = scopeId;
|
|
1344
|
-
}
|
|
1345
|
-
let hook;
|
|
1346
|
-
if (moduleIdentifier) {
|
|
1347
|
-
// server build
|
|
1348
|
-
hook = function (context) {
|
|
1349
|
-
// 2.3 injection
|
|
1350
|
-
context =
|
|
1351
|
-
context || // cached call
|
|
1352
|
-
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|
1353
|
-
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
|
|
1354
|
-
// 2.2 with runInNewContext: true
|
|
1355
|
-
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
|
1356
|
-
context = __VUE_SSR_CONTEXT__;
|
|
1357
|
-
}
|
|
1358
|
-
// inject component styles
|
|
1359
|
-
if (style) {
|
|
1360
|
-
style.call(this, createInjectorSSR(context));
|
|
1361
|
-
}
|
|
1362
|
-
// register component module identifier for async chunk inference
|
|
1363
|
-
if (context && context._registeredComponents) {
|
|
1364
|
-
context._registeredComponents.add(moduleIdentifier);
|
|
1365
|
-
}
|
|
1366
|
-
};
|
|
1367
|
-
// used by ssr in case component is cached and beforeCreate
|
|
1368
|
-
// never gets called
|
|
1369
|
-
options._ssrRegister = hook;
|
|
1370
|
-
}
|
|
1371
|
-
else if (style) {
|
|
1372
|
-
hook = shadowMode
|
|
1373
|
-
? function (context) {
|
|
1374
|
-
style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
|
|
1375
|
-
}
|
|
1376
|
-
: function (context) {
|
|
1377
|
-
style.call(this, createInjector(context));
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
if (hook) {
|
|
1381
|
-
if (options.functional) {
|
|
1382
|
-
// register for functional component in vue file
|
|
1383
|
-
const originalRender = options.render;
|
|
1384
|
-
options.render = function renderWithStyleInjection(h, context) {
|
|
1385
|
-
hook.call(context);
|
|
1386
|
-
return originalRender(h, context);
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
else {
|
|
1390
|
-
// inject component registration as beforeCreate hook
|
|
1391
|
-
const existing = options.beforeCreate;
|
|
1392
|
-
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
return script;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const isOldIE = typeof navigator !== 'undefined' &&
|
|
1399
|
-
/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
|
|
1400
|
-
function createInjector(context) {
|
|
1401
|
-
return (id, style) => addStyle(id, style);
|
|
1402
|
-
}
|
|
1403
|
-
let HEAD;
|
|
1404
|
-
const styles = {};
|
|
1405
|
-
function addStyle(id, css) {
|
|
1406
|
-
const group = isOldIE ? css.media || 'default' : id;
|
|
1407
|
-
const style = styles[group] || (styles[group] = { ids: new Set(), styles: [] });
|
|
1408
|
-
if (!style.ids.has(id)) {
|
|
1409
|
-
style.ids.add(id);
|
|
1410
|
-
let code = css.source;
|
|
1411
|
-
if (css.map) {
|
|
1412
|
-
// https://developer.chrome.com/devtools/docs/javascript-debugging
|
|
1413
|
-
// this makes source maps inside style tags work properly in Chrome
|
|
1414
|
-
code += '\n/*# sourceURL=' + css.map.sources[0] + ' */';
|
|
1415
|
-
// http://stackoverflow.com/a/26603875
|
|
1416
|
-
code +=
|
|
1417
|
-
'\n/*# sourceMappingURL=data:application/json;base64,' +
|
|
1418
|
-
btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) +
|
|
1419
|
-
' */';
|
|
1420
|
-
}
|
|
1421
|
-
if (!style.element) {
|
|
1422
|
-
style.element = document.createElement('style');
|
|
1423
|
-
style.element.type = 'text/css';
|
|
1424
|
-
if (css.media)
|
|
1425
|
-
style.element.setAttribute('media', css.media);
|
|
1426
|
-
if (HEAD === undefined) {
|
|
1427
|
-
HEAD = document.head || document.getElementsByTagName('head')[0];
|
|
1428
|
-
}
|
|
1429
|
-
HEAD.appendChild(style.element);
|
|
1430
|
-
}
|
|
1431
|
-
if ('styleSheet' in style.element) {
|
|
1432
|
-
style.styles.push(code);
|
|
1433
|
-
style.element.styleSheet.cssText = style.styles
|
|
1434
|
-
.filter(Boolean)
|
|
1435
|
-
.join('\n');
|
|
1436
|
-
}
|
|
1437
|
-
else {
|
|
1438
|
-
const index = style.ids.size - 1;
|
|
1439
|
-
const textNode = document.createTextNode(code);
|
|
1440
|
-
const nodes = style.element.childNodes;
|
|
1441
|
-
if (nodes[index])
|
|
1442
|
-
style.element.removeChild(nodes[index]);
|
|
1443
|
-
if (nodes.length)
|
|
1444
|
-
style.element.insertBefore(textNode, nodes[index]);
|
|
1445
|
-
else
|
|
1446
|
-
style.element.appendChild(textNode);
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
/* script */
|
|
1452
|
-
const __vue_script__$3 = script$3;
|
|
1453
|
-
|
|
1454
|
-
/* template */
|
|
1455
|
-
var __vue_render__$3 = function () {
|
|
1456
|
-
var _vm = this;
|
|
1457
|
-
var _h = _vm.$createElement;
|
|
1458
|
-
var _c = _vm._self._c || _h;
|
|
1459
|
-
return _c(
|
|
1460
|
-
"label",
|
|
1461
|
-
{
|
|
1462
|
-
staticClass: "eg-checkbox",
|
|
1463
|
-
class: { "eg-checkbox--disabled": _vm.disabled },
|
|
1464
|
-
},
|
|
1465
|
-
[
|
|
1466
|
-
_c("input", {
|
|
1467
|
-
staticClass: "eg-checkbox-input",
|
|
1468
|
-
attrs: { type: "checkbox", disabled: _vm.disabled },
|
|
1469
|
-
domProps: { checked: _vm.isChecked },
|
|
1470
|
-
on: { change: _vm.onChange },
|
|
1471
|
-
}),
|
|
1472
|
-
_vm._v(" "),
|
|
1473
|
-
_c("span", { staticClass: "eg-checkbox-box" }),
|
|
1474
|
-
]
|
|
1475
|
-
)
|
|
1476
|
-
};
|
|
1477
|
-
var __vue_staticRenderFns__$3 = [];
|
|
1478
|
-
__vue_render__$3._withStripped = true;
|
|
1479
|
-
|
|
1480
|
-
/* style */
|
|
1481
|
-
const __vue_inject_styles__$3 = function (inject) {
|
|
1482
|
-
if (!inject) return
|
|
1483
|
-
inject("data-v-48d91ddc_0", { source: "\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 {\n 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 {\n box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);\n}\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input {\n cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box {\n background-color: #f0f1f3;\n border-color: #dadde2;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box {\n background-color: #b9c2ce;\n border-color: #b9c2ce;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/EgCheckbox.vue"],"names":[],"mappings":";AAuCA;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;EACA,qBAAA;AACA;AACA;EACA,yBAAA;EACA,+SAAA;EACA,wBAAA;EACA,qBAAA;AACA;AACA;EACA,6CAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,yBAAA;EACA,qBAAA;AACA;AACA;EACA,yBAAA;EACA,qBAAA;AACA","file":"EgCheckbox.vue","sourcesContent":["<template>\n <label class=\"eg-checkbox\" :class=\"{ 'eg-checkbox--disabled': disabled }\">\n <input\n type=\"checkbox\"\n class=\"eg-checkbox-input\"\n :checked=\"isChecked\"\n :disabled=\"disabled\"\n @change=\"onChange\"\n />\n <span class=\"eg-checkbox-box\"></span>\n </label>\n</template>\n\n<script>\nexport default {\n name: 'EgCheckbox',\n model: { prop: 'value', event: 'input' },\n props: {\n value: { default: false },\n trueValue: { default: true },\n falseValue: { default: false },\n disabled: { type: Boolean, default: false }\n },\n computed: {\n isChecked() {\n return this.value === this.trueValue\n }\n },\n methods: {\n onChange(e) {\n const val = e.target.checked ? this.trueValue : this.falseValue\n this.$emit('input', val)\n this.$emit('change', val)\n }\n }\n}\n</script>\n\n<style>\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 {\n 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 {\n box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);\n}\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input {\n cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box {\n background-color: #f0f1f3;\n border-color: #dadde2;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box {\n background-color: #b9c2ce;\n border-color: #b9c2ce;\n}\n</style>\n"]}, media: undefined });
|
|
1484
|
-
|
|
1485
|
-
};
|
|
1486
|
-
/* scoped */
|
|
1487
|
-
const __vue_scope_id__$3 = undefined;
|
|
1488
|
-
/* module identifier */
|
|
1489
|
-
const __vue_module_identifier__$3 = undefined;
|
|
1490
|
-
/* functional template */
|
|
1491
|
-
const __vue_is_functional_template__$3 = false;
|
|
1492
|
-
/* style inject SSR */
|
|
1493
|
-
|
|
1494
|
-
/* style inject shadow dom */
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const __vue_component__$3 = /*#__PURE__*/normalizeComponent(
|
|
1499
|
-
{ render: __vue_render__$3, staticRenderFns: __vue_staticRenderFns__$3 },
|
|
1500
|
-
__vue_inject_styles__$3,
|
|
1501
|
-
__vue_script__$3,
|
|
1502
|
-
__vue_scope_id__$3,
|
|
1503
|
-
__vue_is_functional_template__$3,
|
|
1504
|
-
__vue_module_identifier__$3,
|
|
1505
|
-
false,
|
|
1506
|
-
createInjector,
|
|
1507
|
-
undefined,
|
|
1508
|
-
undefined
|
|
1509
|
-
);
|
|
1510
|
-
|
|
1511
|
-
//
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
//
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1571
|
-
//
|
|
1572
|
-
//
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
|
|
1602
|
-
var script$2 = {
|
|
1603
|
-
name: 'EgSelect',
|
|
1604
|
-
model: { prop: 'value', event: 'input' },
|
|
1605
|
-
props: {
|
|
1606
|
-
value: { default: null },
|
|
1607
|
-
options: { type: Array, default: () => [] },
|
|
1608
|
-
enableSearch: { type: Boolean, default: false },
|
|
1609
|
-
placeholder: { type: String, default: '' },
|
|
1610
|
-
disabled: { type: Boolean, default: false },
|
|
1611
|
-
dropdownOnly: { type: Boolean, default: false },
|
|
1612
|
-
triggerRect: { type: Object, default: null }
|
|
1613
|
-
},
|
|
1614
|
-
data() {
|
|
1615
|
-
return {
|
|
1616
|
-
open: false,
|
|
1617
|
-
searchQuery: '',
|
|
1618
|
-
highlight: -1
|
|
1619
|
-
}
|
|
1620
|
-
},
|
|
1621
|
-
computed: {
|
|
1622
|
-
normalizedOptions() {
|
|
1623
|
-
return (this.options || []).map(o =>
|
|
1624
|
-
typeof o === 'string'
|
|
1625
|
-
? { value: o, label: o }
|
|
1626
|
-
: { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }
|
|
1627
|
-
)
|
|
1628
|
-
},
|
|
1629
|
-
filteredOptions() {
|
|
1630
|
-
if (!this.enableSearch || !this.searchQuery.trim()) return this.normalizedOptions
|
|
1631
|
-
const q = this.searchQuery.trim().toLowerCase();
|
|
1632
|
-
return this.normalizedOptions.filter(o => String(o.label || '').toLowerCase().includes(q))
|
|
1633
|
-
},
|
|
1634
|
-
selectedOpt() {
|
|
1635
|
-
const v = String(this.value == null ? '' : this.value);
|
|
1636
|
-
return this.normalizedOptions.find(o => String(o.value) === v) || null
|
|
1637
|
-
},
|
|
1638
|
-
overlayStyle() {
|
|
1639
|
-
if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
|
|
1640
|
-
const rect = this.triggerRect;
|
|
1641
|
-
const searchH = this.enableSearch ? 42 : 0;
|
|
1642
|
-
const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320);
|
|
1643
|
-
const spaceBelow = window.innerHeight - rect.bottom;
|
|
1644
|
-
const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH;
|
|
1645
|
-
return {
|
|
1646
|
-
position: 'fixed',
|
|
1647
|
-
top: top + 'px',
|
|
1648
|
-
left: rect.left + 'px',
|
|
1649
|
-
minWidth: Math.max(rect.width, 120) + 'px',
|
|
1650
|
-
maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
|
|
1651
|
-
zIndex: 9999
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
},
|
|
1655
|
-
watch: {
|
|
1656
|
-
open(val) {
|
|
1657
|
-
if (val && this.enableSearch) {
|
|
1658
|
-
this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus(); });
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
},
|
|
1662
|
-
mounted() {
|
|
1663
|
-
if (this.dropdownOnly) {
|
|
1664
|
-
// Portal: move to document.body so position:fixed is always relative to viewport,
|
|
1665
|
-
// regardless of any CSS transform on ancestor elements in the host project.
|
|
1666
|
-
// Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.
|
|
1667
|
-
if (this.$el && this.$el.parentNode) {
|
|
1668
|
-
document.body.appendChild(this.$el);
|
|
1669
|
-
}
|
|
1670
|
-
const v = String(this.value == null ? '' : this.value);
|
|
1671
|
-
this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v);
|
|
1672
|
-
if (this.enableSearch) {
|
|
1673
|
-
this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus(); });
|
|
1674
|
-
}
|
|
1675
|
-
} else {
|
|
1676
|
-
document.addEventListener('click', this._onDocClick);
|
|
1677
|
-
}
|
|
1678
|
-
},
|
|
1679
|
-
beforeDestroy() {
|
|
1680
|
-
if (!this.dropdownOnly) {
|
|
1681
|
-
document.removeEventListener('click', this._onDocClick);
|
|
1682
|
-
}
|
|
1683
|
-
},
|
|
1684
|
-
methods: {
|
|
1685
|
-
toggle() {
|
|
1686
|
-
if (this.disabled) return
|
|
1687
|
-
this.open = !this.open;
|
|
1688
|
-
if (this.open) {
|
|
1689
|
-
const v = String(this.value == null ? '' : this.value);
|
|
1690
|
-
this.highlight = this.normalizedOptions.findIndex(o => String(o.value) === v);
|
|
1691
|
-
this.searchQuery = '';
|
|
1692
|
-
}
|
|
1693
|
-
},
|
|
1694
|
-
pickOption(opt) {
|
|
1695
|
-
this.$emit('input', opt.value);
|
|
1696
|
-
this.$emit('select', { value: opt.value, label: opt.label });
|
|
1697
|
-
if (!this.dropdownOnly) {
|
|
1698
|
-
this.open = false;
|
|
1699
|
-
this.searchQuery = '';
|
|
1700
|
-
this.highlight = -1;
|
|
1701
|
-
}
|
|
1702
|
-
},
|
|
1703
|
-
onSearch(e) {
|
|
1704
|
-
this.searchQuery = e.target.value;
|
|
1705
|
-
this.highlight = 0;
|
|
1706
|
-
},
|
|
1707
|
-
onSearchKey(e) {
|
|
1708
|
-
const key = e.key;
|
|
1709
|
-
if (key === 'Escape') {
|
|
1710
|
-
e.preventDefault();
|
|
1711
|
-
if (this.dropdownOnly) this.$emit('close');
|
|
1712
|
-
else { this.open = false; this.searchQuery = ''; }
|
|
1713
|
-
} else if (key === 'ArrowDown') {
|
|
1714
|
-
e.preventDefault();
|
|
1715
|
-
this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1);
|
|
1716
|
-
} else if (key === 'ArrowUp') {
|
|
1717
|
-
e.preventDefault();
|
|
1718
|
-
this.highlight = Math.max((this.highlight || 0) - 1, 0);
|
|
1719
|
-
} else if (key === 'Enter') {
|
|
1720
|
-
e.preventDefault();
|
|
1721
|
-
const opt = this.filteredOptions[this.highlight];
|
|
1722
|
-
if (opt) this.pickOption(opt);
|
|
1723
|
-
}
|
|
1724
|
-
},
|
|
1725
|
-
handleKey(e) {
|
|
1726
|
-
const key = e.key;
|
|
1727
|
-
if (key === 'Escape') {
|
|
1728
|
-
e.preventDefault();
|
|
1729
|
-
this.$emit('close');
|
|
1730
|
-
} else if (key === 'Enter' || key === ' ') {
|
|
1731
|
-
e.preventDefault();
|
|
1732
|
-
const opt = this.filteredOptions[this.highlight];
|
|
1733
|
-
if (opt) this.pickOption(opt);
|
|
1734
|
-
else this.$emit('close');
|
|
1735
|
-
} else if (key === 'ArrowDown') {
|
|
1736
|
-
e.preventDefault();
|
|
1737
|
-
this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1);
|
|
1738
|
-
} else if (key === 'ArrowUp') {
|
|
1739
|
-
e.preventDefault();
|
|
1740
|
-
this.highlight = Math.max((this.highlight || 0) - 1, 0);
|
|
1741
|
-
}
|
|
1742
|
-
},
|
|
1743
|
-
onRootMouseDown(e) {
|
|
1744
|
-
if (this.dropdownOnly) e.preventDefault();
|
|
1745
|
-
},
|
|
1746
|
-
_onDocClick(e) {
|
|
1747
|
-
if (!this.open) return
|
|
1748
|
-
if (this.$el && this.$el.contains(e.target)) return
|
|
1749
|
-
this.open = false;
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
};
|
|
1753
|
-
|
|
1754
|
-
/* script */
|
|
1755
|
-
const __vue_script__$2 = script$2;
|
|
1756
|
-
|
|
1757
|
-
/* template */
|
|
1758
|
-
var __vue_render__$2 = function () {
|
|
1759
|
-
var _vm = this;
|
|
1760
|
-
var _h = _vm.$createElement;
|
|
1761
|
-
var _c = _vm._self._c || _h;
|
|
1762
|
-
return _c(
|
|
1763
|
-
"div",
|
|
1764
|
-
{
|
|
1765
|
-
class: _vm.dropdownOnly ? "eg-select-dropdown" : "eg-select-wrapper",
|
|
1766
|
-
style: _vm.dropdownOnly ? _vm.overlayStyle : null,
|
|
1767
|
-
on: { mousedown: _vm.onRootMouseDown },
|
|
1768
|
-
},
|
|
1769
|
-
[
|
|
1770
|
-
!_vm.dropdownOnly
|
|
1771
|
-
? _c(
|
|
1772
|
-
"div",
|
|
1773
|
-
{
|
|
1774
|
-
staticClass: "eg-select-trigger",
|
|
1775
|
-
class: {
|
|
1776
|
-
"eg-select-trigger--disabled": _vm.disabled,
|
|
1777
|
-
"eg-select-trigger--open": _vm.open,
|
|
1778
|
-
},
|
|
1779
|
-
on: {
|
|
1780
|
-
click: function ($event) {
|
|
1781
|
-
$event.stopPropagation();
|
|
1782
|
-
return _vm.toggle.apply(null, arguments)
|
|
1783
|
-
},
|
|
1784
|
-
},
|
|
1785
|
-
},
|
|
1786
|
-
[
|
|
1787
|
-
_vm.selectedOpt && _vm.selectedOpt.color
|
|
1788
|
-
? _c("span", {
|
|
1789
|
-
staticClass: "eg-select-dot",
|
|
1790
|
-
style: { background: _vm.selectedOpt.color },
|
|
1791
|
-
})
|
|
1792
|
-
: _vm._e(),
|
|
1793
|
-
_vm._v(" "),
|
|
1794
|
-
_c(
|
|
1795
|
-
"span",
|
|
1796
|
-
{
|
|
1797
|
-
staticClass: "eg-select-trigger-label",
|
|
1798
|
-
class: { "eg-select-placeholder": !_vm.selectedOpt },
|
|
1799
|
-
},
|
|
1800
|
-
[
|
|
1801
|
-
_vm._v(
|
|
1802
|
-
"\n " +
|
|
1803
|
-
_vm._s(
|
|
1804
|
-
_vm.selectedOpt
|
|
1805
|
-
? _vm.selectedOpt.label
|
|
1806
|
-
: _vm.placeholder
|
|
1807
|
-
) +
|
|
1808
|
-
"\n "
|
|
1809
|
-
),
|
|
1810
|
-
]
|
|
1811
|
-
),
|
|
1812
|
-
_vm._v(" "),
|
|
1813
|
-
_c("span", { staticClass: "eg-select-caret" }, [_vm._v("▾")]),
|
|
1814
|
-
]
|
|
1815
|
-
)
|
|
1816
|
-
: _vm._e(),
|
|
1817
|
-
_vm._v(" "),
|
|
1818
|
-
_vm.dropdownOnly
|
|
1819
|
-
? [
|
|
1820
|
-
_vm.enableSearch
|
|
1821
|
-
? _c(
|
|
1822
|
-
"div",
|
|
1823
|
-
{
|
|
1824
|
-
staticClass: "eg-select-search",
|
|
1825
|
-
on: {
|
|
1826
|
-
mousedown: function ($event) {
|
|
1827
|
-
$event.stopPropagation();
|
|
1828
|
-
},
|
|
1829
|
-
},
|
|
1830
|
-
},
|
|
1831
|
-
[
|
|
1832
|
-
_c("input", {
|
|
1833
|
-
ref: "searchInput",
|
|
1834
|
-
staticClass: "eg-select-search-input",
|
|
1835
|
-
attrs: {
|
|
1836
|
-
placeholder: "Qidirish...",
|
|
1837
|
-
autocomplete: "off",
|
|
1838
|
-
autocorrect: "off",
|
|
1839
|
-
autocapitalize: "off",
|
|
1840
|
-
spellcheck: "false",
|
|
1841
|
-
},
|
|
1842
|
-
domProps: { value: _vm.searchQuery },
|
|
1843
|
-
on: {
|
|
1844
|
-
input: _vm.onSearch,
|
|
1845
|
-
keydown: function ($event) {
|
|
1846
|
-
$event.stopPropagation();
|
|
1847
|
-
return _vm.onSearchKey.apply(null, arguments)
|
|
1848
|
-
},
|
|
1849
|
-
},
|
|
1850
|
-
}),
|
|
1851
|
-
]
|
|
1852
|
-
)
|
|
1853
|
-
: _vm._e(),
|
|
1854
|
-
_vm._v(" "),
|
|
1855
|
-
_vm._l(_vm.filteredOptions, function (opt, idx) {
|
|
1856
|
-
return _c(
|
|
1857
|
-
"div",
|
|
1858
|
-
{
|
|
1859
|
-
key: String(opt.value),
|
|
1860
|
-
staticClass: "eg-select-option",
|
|
1861
|
-
class: {
|
|
1862
|
-
"eg-select-option--active":
|
|
1863
|
-
String(opt.value) === String(_vm.value),
|
|
1864
|
-
"eg-select-option--highlighted": idx === _vm.highlight,
|
|
1865
|
-
},
|
|
1866
|
-
on: {
|
|
1867
|
-
mousedown: function ($event) {
|
|
1868
|
-
$event.preventDefault();
|
|
1869
|
-
return _vm.pickOption(opt)
|
|
1870
|
-
},
|
|
1871
|
-
mouseover: function ($event) {
|
|
1872
|
-
_vm.highlight = idx;
|
|
1873
|
-
},
|
|
1874
|
-
},
|
|
1875
|
-
},
|
|
1876
|
-
[
|
|
1877
|
-
opt.color
|
|
1878
|
-
? _c("span", {
|
|
1879
|
-
staticClass: "eg-select-dot",
|
|
1880
|
-
style: { background: opt.color },
|
|
1881
|
-
})
|
|
1882
|
-
: _vm._e(),
|
|
1883
|
-
_vm._v("\n " + _vm._s(opt.label) + "\n "),
|
|
1884
|
-
]
|
|
1885
|
-
)
|
|
1886
|
-
}),
|
|
1887
|
-
]
|
|
1888
|
-
: _vm.open
|
|
1889
|
-
? _c(
|
|
1890
|
-
"div",
|
|
1891
|
-
{
|
|
1892
|
-
staticClass: "eg-select-dropdown eg-select-dropdown--floating",
|
|
1893
|
-
on: {
|
|
1894
|
-
mousedown: function ($event) {
|
|
1895
|
-
$event.stopPropagation();
|
|
1896
|
-
$event.preventDefault();
|
|
1897
|
-
},
|
|
1898
|
-
},
|
|
1899
|
-
},
|
|
1900
|
-
[
|
|
1901
|
-
_vm.enableSearch
|
|
1902
|
-
? _c(
|
|
1903
|
-
"div",
|
|
1904
|
-
{
|
|
1905
|
-
staticClass: "eg-select-search",
|
|
1906
|
-
on: {
|
|
1907
|
-
mousedown: function ($event) {
|
|
1908
|
-
$event.stopPropagation();
|
|
1909
|
-
},
|
|
1910
|
-
},
|
|
1911
|
-
},
|
|
1912
|
-
[
|
|
1913
|
-
_c("input", {
|
|
1914
|
-
ref: "searchInput",
|
|
1915
|
-
staticClass: "eg-select-search-input",
|
|
1916
|
-
attrs: {
|
|
1917
|
-
placeholder: "Qidirish...",
|
|
1918
|
-
autocomplete: "off",
|
|
1919
|
-
autocorrect: "off",
|
|
1920
|
-
autocapitalize: "off",
|
|
1921
|
-
spellcheck: "false",
|
|
1922
|
-
},
|
|
1923
|
-
domProps: { value: _vm.searchQuery },
|
|
1924
|
-
on: {
|
|
1925
|
-
input: _vm.onSearch,
|
|
1926
|
-
keydown: function ($event) {
|
|
1927
|
-
$event.stopPropagation();
|
|
1928
|
-
return _vm.onSearchKey.apply(null, arguments)
|
|
1929
|
-
},
|
|
1930
|
-
},
|
|
1931
|
-
}),
|
|
1932
|
-
]
|
|
1933
|
-
)
|
|
1934
|
-
: _vm._e(),
|
|
1935
|
-
_vm._v(" "),
|
|
1936
|
-
_vm._l(_vm.filteredOptions, function (opt, idx) {
|
|
1937
|
-
return _c(
|
|
1938
|
-
"div",
|
|
1939
|
-
{
|
|
1940
|
-
key: String(opt.value),
|
|
1941
|
-
staticClass: "eg-select-option",
|
|
1942
|
-
class: {
|
|
1943
|
-
"eg-select-option--active":
|
|
1944
|
-
String(opt.value) === String(_vm.value),
|
|
1945
|
-
"eg-select-option--highlighted": idx === _vm.highlight,
|
|
1946
|
-
},
|
|
1947
|
-
on: {
|
|
1948
|
-
mousedown: function ($event) {
|
|
1949
|
-
$event.preventDefault();
|
|
1950
|
-
return _vm.pickOption(opt)
|
|
1951
|
-
},
|
|
1952
|
-
mouseover: function ($event) {
|
|
1953
|
-
_vm.highlight = idx;
|
|
1954
|
-
},
|
|
1955
|
-
},
|
|
1956
|
-
},
|
|
1957
|
-
[
|
|
1958
|
-
opt.color
|
|
1959
|
-
? _c("span", {
|
|
1960
|
-
staticClass: "eg-select-dot",
|
|
1961
|
-
style: { background: opt.color },
|
|
1962
|
-
})
|
|
1963
|
-
: _vm._e(),
|
|
1964
|
-
_vm._v("\n " + _vm._s(opt.label) + "\n "),
|
|
1965
|
-
]
|
|
1966
|
-
)
|
|
1967
|
-
}),
|
|
1968
|
-
],
|
|
1969
|
-
2
|
|
1970
|
-
)
|
|
1971
|
-
: _vm._e(),
|
|
1972
|
-
],
|
|
1973
|
-
2
|
|
1974
|
-
)
|
|
1975
|
-
};
|
|
1976
|
-
var __vue_staticRenderFns__$2 = [];
|
|
1977
|
-
__vue_render__$2._withStripped = true;
|
|
1978
|
-
|
|
1979
|
-
/* style */
|
|
1980
|
-
const __vue_inject_styles__$2 = function (inject) {
|
|
1981
|
-
if (!inject) return
|
|
1982
|
-
inject("data-v-524aca00_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/* 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqPA,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 // Portal: move to document.body so position:fixed is always relative to viewport,\n // regardless of any CSS transform on ancestor elements in the host project.\n // Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.\n if (this.$el && this.$el.parentNode) {\n document.body.appendChild(this.$el)\n }\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 });
|
|
1983
|
-
|
|
1984
|
-
};
|
|
1985
|
-
/* scoped */
|
|
1986
|
-
const __vue_scope_id__$2 = undefined;
|
|
1987
|
-
/* module identifier */
|
|
1988
|
-
const __vue_module_identifier__$2 = undefined;
|
|
1989
|
-
/* functional template */
|
|
1990
|
-
const __vue_is_functional_template__$2 = false;
|
|
1991
|
-
/* style inject SSR */
|
|
1992
|
-
|
|
1993
|
-
/* style inject shadow dom */
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
const __vue_component__$2 = /*#__PURE__*/normalizeComponent(
|
|
1998
|
-
{ render: __vue_render__$2, staticRenderFns: __vue_staticRenderFns__$2 },
|
|
1999
|
-
__vue_inject_styles__$2,
|
|
2000
|
-
__vue_script__$2,
|
|
2001
|
-
__vue_scope_id__$2,
|
|
2002
|
-
__vue_is_functional_template__$2,
|
|
2003
|
-
__vue_module_identifier__$2,
|
|
2004
|
-
false,
|
|
2005
|
-
createInjector,
|
|
2006
|
-
undefined,
|
|
2007
|
-
undefined
|
|
2008
|
-
);
|
|
2009
|
-
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
//
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2029
|
-
//
|
|
2030
|
-
//
|
|
2031
|
-
//
|
|
2032
|
-
//
|
|
2033
|
-
//
|
|
2034
|
-
//
|
|
2035
|
-
//
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
//
|
|
2039
|
-
//
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
2045
|
-
//
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
//
|
|
2051
|
-
//
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2055
|
-
//
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
//
|
|
2072
|
-
//
|
|
2073
|
-
//
|
|
2074
|
-
//
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
//
|
|
2085
|
-
//
|
|
2086
|
-
//
|
|
2087
|
-
//
|
|
2088
|
-
//
|
|
2089
|
-
//
|
|
2090
|
-
//
|
|
2091
|
-
//
|
|
2092
|
-
//
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2096
|
-
//
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2099
|
-
//
|
|
2100
|
-
//
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
//
|
|
2104
|
-
//
|
|
2105
|
-
//
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2108
|
-
//
|
|
2109
|
-
//
|
|
2110
|
-
//
|
|
2111
|
-
//
|
|
2112
|
-
//
|
|
2113
|
-
|
|
2114
|
-
var script$1 = {
|
|
2115
|
-
name: 'EgTreeSelect',
|
|
2116
|
-
model: { prop: 'value', event: 'input' },
|
|
2117
|
-
props: {
|
|
2118
|
-
value: { default: null },
|
|
2119
|
-
options: { type: Array, default: () => [] },
|
|
2120
|
-
valueKey: { type: String, default: 'value' },
|
|
2121
|
-
labelKey: { type: String, default: 'label' },
|
|
2122
|
-
childrenKey: { type: String, default: 'children' },
|
|
2123
|
-
enableSearch: { type: Boolean, default: false },
|
|
2124
|
-
placeholder: { type: String, default: '' },
|
|
2125
|
-
disabled: { type: Boolean, default: false },
|
|
2126
|
-
dropdownOnly: { type: Boolean, default: false },
|
|
2127
|
-
triggerRect: { type: Object, default: null }
|
|
2128
|
-
},
|
|
2129
|
-
data() {
|
|
2130
|
-
return {
|
|
2131
|
-
open: false,
|
|
2132
|
-
searchQuery: '',
|
|
2133
|
-
highlight: -1,
|
|
2134
|
-
expandedMap: {}
|
|
2135
|
-
}
|
|
2136
|
-
},
|
|
2137
|
-
computed: {
|
|
2138
|
-
keys() {
|
|
2139
|
-
return { valueKey: this.valueKey, labelKey: this.labelKey, childrenKey: this.childrenKey }
|
|
2140
|
-
},
|
|
2141
|
-
flatOptions() {
|
|
2142
|
-
return this._treeFlatten(this.options || [], this.keys, this.expandedMap, 0, [])
|
|
2143
|
-
},
|
|
2144
|
-
allFlatOptions() {
|
|
2145
|
-
if (!this.enableSearch) return null
|
|
2146
|
-
return this._treeFlattenAll(this.options || [], this.keys)
|
|
2147
|
-
},
|
|
2148
|
-
filteredOptions() {
|
|
2149
|
-
if (!this.enableSearch || !this.searchQuery.trim()) return this.flatOptions
|
|
2150
|
-
const q = this.searchQuery.trim().toLowerCase();
|
|
2151
|
-
const pool = this.allFlatOptions || this.flatOptions;
|
|
2152
|
-
return pool.filter(o => String(o.label || '').toLowerCase().includes(q))
|
|
2153
|
-
},
|
|
2154
|
-
selectedLabel() {
|
|
2155
|
-
const v = String(this.value == null ? '' : this.value);
|
|
2156
|
-
if (!v) return null
|
|
2157
|
-
const found = this._treeFindLabel(this.options || [], this.keys, v);
|
|
2158
|
-
return found != null ? found : null
|
|
2159
|
-
},
|
|
2160
|
-
overlayStyle() {
|
|
2161
|
-
if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
|
|
2162
|
-
const rect = this.triggerRect;
|
|
2163
|
-
const searchH = this.enableSearch ? 42 : 0;
|
|
2164
|
-
const approxH = Math.min(this.filteredOptions.length * 28 + 8 + searchH, 320);
|
|
2165
|
-
const spaceBelow = window.innerHeight - rect.bottom;
|
|
2166
|
-
const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH;
|
|
2167
|
-
return {
|
|
2168
|
-
position: 'fixed',
|
|
2169
|
-
top: top + 'px',
|
|
2170
|
-
left: rect.left + 'px',
|
|
2171
|
-
minWidth: Math.max(rect.width, 120) + 'px',
|
|
2172
|
-
maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
|
|
2173
|
-
zIndex: 9999
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
},
|
|
2177
|
-
watch: {
|
|
2178
|
-
open(val) {
|
|
2179
|
-
if (val && this.enableSearch) {
|
|
2180
|
-
this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus(); });
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
},
|
|
2184
|
-
mounted() {
|
|
2185
|
-
if (this.dropdownOnly) {
|
|
2186
|
-
// Portal: move to document.body so position:fixed is always relative to viewport,
|
|
2187
|
-
// regardless of any CSS transform on ancestor elements in the host project.
|
|
2188
|
-
// Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.
|
|
2189
|
-
if (this.$el && this.$el.parentNode) {
|
|
2190
|
-
document.body.appendChild(this.$el);
|
|
2191
|
-
}
|
|
2192
|
-
this._initExpanded();
|
|
2193
|
-
if (this.enableSearch) {
|
|
2194
|
-
this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus(); });
|
|
2195
|
-
}
|
|
2196
|
-
} else {
|
|
2197
|
-
document.addEventListener('click', this._onDocClick);
|
|
2198
|
-
}
|
|
2199
|
-
},
|
|
2200
|
-
beforeDestroy() {
|
|
2201
|
-
if (!this.dropdownOnly) {
|
|
2202
|
-
document.removeEventListener('click', this._onDocClick);
|
|
2203
|
-
}
|
|
2204
|
-
},
|
|
2205
|
-
methods: {
|
|
2206
|
-
_initExpanded() {
|
|
2207
|
-
const v = String(this.value == null ? '' : this.value);
|
|
2208
|
-
const path = this._treeFindPath(this.options || [], this.keys, v, []) || [];
|
|
2209
|
-
const expandedMap = {};
|
|
2210
|
-
path.forEach(id => { expandedMap[id] = true; });
|
|
2211
|
-
this.expandedMap = expandedMap;
|
|
2212
|
-
const flat = this.flatOptions;
|
|
2213
|
-
this.highlight = flat.findIndex(o => String(o.value) === v);
|
|
2214
|
-
},
|
|
2215
|
-
toggle() {
|
|
2216
|
-
if (this.disabled) return
|
|
2217
|
-
this.open = !this.open;
|
|
2218
|
-
if (this.open) {
|
|
2219
|
-
this.searchQuery = '';
|
|
2220
|
-
this._initExpanded();
|
|
2221
|
-
}
|
|
2222
|
-
},
|
|
2223
|
-
pickOption(opt) {
|
|
2224
|
-
this.$emit('input', opt.value);
|
|
2225
|
-
this.$emit('select', { value: opt.value, label: opt.label });
|
|
2226
|
-
if (!this.dropdownOnly) {
|
|
2227
|
-
this.open = false;
|
|
2228
|
-
this.searchQuery = '';
|
|
2229
|
-
this.highlight = -1;
|
|
2230
|
-
}
|
|
2231
|
-
},
|
|
2232
|
-
toggleNode(idx) {
|
|
2233
|
-
const opt = this.flatOptions[idx];
|
|
2234
|
-
if (!opt || !opt.hasChildren) return
|
|
2235
|
-
const next = { ...this.expandedMap };
|
|
2236
|
-
if (next[opt.id]) delete next[opt.id];
|
|
2237
|
-
else next[opt.id] = true;
|
|
2238
|
-
this.expandedMap = next;
|
|
2239
|
-
const newFlat = this.flatOptions;
|
|
2240
|
-
const newIdx = newFlat.findIndex(o => o.id === opt.id);
|
|
2241
|
-
this.highlight = newIdx >= 0 ? newIdx : this.highlight;
|
|
2242
|
-
},
|
|
2243
|
-
onSearch(e) {
|
|
2244
|
-
this.searchQuery = e.target.value;
|
|
2245
|
-
this.highlight = 0;
|
|
2246
|
-
},
|
|
2247
|
-
onSearchKey(e) {
|
|
2248
|
-
const key = e.key;
|
|
2249
|
-
if (key === 'Escape') {
|
|
2250
|
-
e.preventDefault();
|
|
2251
|
-
if (this.dropdownOnly) this.$emit('close');
|
|
2252
|
-
else { this.open = false; this.searchQuery = ''; }
|
|
2253
|
-
} else if (key === 'ArrowDown') {
|
|
2254
|
-
e.preventDefault();
|
|
2255
|
-
this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1);
|
|
2256
|
-
} else if (key === 'ArrowUp') {
|
|
2257
|
-
e.preventDefault();
|
|
2258
|
-
this.highlight = Math.max((this.highlight || 0) - 1, 0);
|
|
2259
|
-
} else if (key === 'Enter') {
|
|
2260
|
-
e.preventDefault();
|
|
2261
|
-
const opt = this.filteredOptions[this.highlight];
|
|
2262
|
-
if (opt) this.pickOption(opt);
|
|
2263
|
-
}
|
|
2264
|
-
},
|
|
2265
|
-
handleKey(e) {
|
|
2266
|
-
const key = e.key;
|
|
2267
|
-
if (key === 'Escape') {
|
|
2268
|
-
e.preventDefault();
|
|
2269
|
-
this.$emit('close');
|
|
2270
|
-
} else if (key === 'Enter' || key === ' ') {
|
|
2271
|
-
e.preventDefault();
|
|
2272
|
-
const opt = this.filteredOptions[this.highlight];
|
|
2273
|
-
if (opt) this.pickOption(opt);
|
|
2274
|
-
else this.$emit('close');
|
|
2275
|
-
} else if (key === 'ArrowDown') {
|
|
2276
|
-
e.preventDefault();
|
|
2277
|
-
this.highlight = Math.min((this.highlight || 0) + 1, this.filteredOptions.length - 1);
|
|
2278
|
-
} else if (key === 'ArrowUp') {
|
|
2279
|
-
e.preventDefault();
|
|
2280
|
-
this.highlight = Math.max((this.highlight || 0) - 1, 0);
|
|
2281
|
-
} else if (!this.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {
|
|
2282
|
-
e.preventDefault();
|
|
2283
|
-
const opt = this.flatOptions[this.highlight];
|
|
2284
|
-
if (opt && opt.hasChildren) {
|
|
2285
|
-
if (key === 'ArrowRight' && !opt.expanded) this.toggleNode(this.highlight);
|
|
2286
|
-
if (key === 'ArrowLeft' && opt.expanded) this.toggleNode(this.highlight);
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
},
|
|
2290
|
-
onRootMouseDown(e) {
|
|
2291
|
-
if (this.dropdownOnly) e.preventDefault();
|
|
2292
|
-
},
|
|
2293
|
-
_onDocClick(e) {
|
|
2294
|
-
if (!this.open) return
|
|
2295
|
-
if (this.$el && this.$el.contains(e.target)) return
|
|
2296
|
-
this.open = false;
|
|
2297
|
-
},
|
|
2298
|
-
_treeFlatten(nodes, keys, expandedMap, depth, pathArr) {
|
|
2299
|
-
let out = []
|
|
2300
|
-
;(nodes || []).forEach((node, idx) => {
|
|
2301
|
-
const path = pathArr.concat(idx);
|
|
2302
|
-
const id = path.join('-');
|
|
2303
|
-
const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : [];
|
|
2304
|
-
const hasChildren = children.length > 0;
|
|
2305
|
-
const expanded = hasChildren && !!expandedMap[id];
|
|
2306
|
-
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 });
|
|
2307
|
-
if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path));
|
|
2308
|
-
});
|
|
2309
|
-
return out
|
|
2310
|
-
},
|
|
2311
|
-
_treeFlattenAll(nodes, keys) {
|
|
2312
|
-
const result = [];
|
|
2313
|
-
const walk = (list) => {
|
|
2314
|
-
(list || []).forEach(node => {
|
|
2315
|
-
const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : [];
|
|
2316
|
-
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 });
|
|
2317
|
-
if (children.length) walk(children);
|
|
2318
|
-
});
|
|
2319
|
-
};
|
|
2320
|
-
walk(nodes);
|
|
2321
|
-
return result
|
|
2322
|
-
},
|
|
2323
|
-
_treeFindLabel(nodes, keys, value) {
|
|
2324
|
-
for (const node of (nodes || [])) {
|
|
2325
|
-
if (String(node[keys.valueKey]) === value) {
|
|
2326
|
-
return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
|
|
2327
|
-
}
|
|
2328
|
-
const found = this._treeFindLabel(node[keys.childrenKey], keys, value);
|
|
2329
|
-
if (found != null) return found
|
|
2330
|
-
}
|
|
2331
|
-
return null
|
|
2332
|
-
},
|
|
2333
|
-
_treeFindPath(nodes, keys, value, pathArr) {
|
|
2334
|
-
for (let i = 0; i < (nodes || []).length; i++) {
|
|
2335
|
-
const node = nodes[i];
|
|
2336
|
-
const path = pathArr.concat(i);
|
|
2337
|
-
if (String(node[keys.valueKey]) === value) return [path.join('-')]
|
|
2338
|
-
const children = node[keys.childrenKey];
|
|
2339
|
-
if (Array.isArray(children)) {
|
|
2340
|
-
const sub = this._treeFindPath(children, keys, value, path);
|
|
2341
|
-
if (sub) return [path.join('-')].concat(sub)
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
return null
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
};
|
|
2348
|
-
|
|
2349
|
-
/* script */
|
|
2350
|
-
const __vue_script__$1 = script$1;
|
|
2351
|
-
|
|
2352
|
-
/* template */
|
|
2353
|
-
var __vue_render__$1 = function () {
|
|
2354
|
-
var _vm = this;
|
|
2355
|
-
var _h = _vm.$createElement;
|
|
2356
|
-
var _c = _vm._self._c || _h;
|
|
2357
|
-
return _c(
|
|
2358
|
-
"div",
|
|
2359
|
-
{
|
|
2360
|
-
class: _vm.dropdownOnly ? "eg-select-dropdown" : "eg-select-wrapper",
|
|
2361
|
-
style: _vm.dropdownOnly ? _vm.overlayStyle : null,
|
|
2362
|
-
on: { mousedown: _vm.onRootMouseDown },
|
|
2363
|
-
},
|
|
2364
|
-
[
|
|
2365
|
-
!_vm.dropdownOnly
|
|
2366
|
-
? _c(
|
|
2367
|
-
"div",
|
|
2368
|
-
{
|
|
2369
|
-
staticClass: "eg-select-trigger",
|
|
2370
|
-
class: {
|
|
2371
|
-
"eg-select-trigger--disabled": _vm.disabled,
|
|
2372
|
-
"eg-select-trigger--open": _vm.open,
|
|
2373
|
-
},
|
|
2374
|
-
on: {
|
|
2375
|
-
click: function ($event) {
|
|
2376
|
-
$event.stopPropagation();
|
|
2377
|
-
return _vm.toggle.apply(null, arguments)
|
|
2378
|
-
},
|
|
2379
|
-
},
|
|
2380
|
-
},
|
|
2381
|
-
[
|
|
2382
|
-
_c(
|
|
2383
|
-
"span",
|
|
2384
|
-
{
|
|
2385
|
-
staticClass: "eg-select-trigger-label",
|
|
2386
|
-
class: { "eg-select-placeholder": _vm.selectedLabel == null },
|
|
2387
|
-
},
|
|
2388
|
-
[
|
|
2389
|
-
_vm._v(
|
|
2390
|
-
"\n " +
|
|
2391
|
-
_vm._s(
|
|
2392
|
-
_vm.selectedLabel != null
|
|
2393
|
-
? _vm.selectedLabel
|
|
2394
|
-
: _vm.placeholder
|
|
2395
|
-
) +
|
|
2396
|
-
"\n "
|
|
2397
|
-
),
|
|
2398
|
-
]
|
|
2399
|
-
),
|
|
2400
|
-
_vm._v(" "),
|
|
2401
|
-
_c("span", { staticClass: "eg-select-caret" }, [_vm._v("▾")]),
|
|
2402
|
-
]
|
|
2403
|
-
)
|
|
2404
|
-
: _vm._e(),
|
|
2405
|
-
_vm._v(" "),
|
|
2406
|
-
_vm.dropdownOnly
|
|
2407
|
-
? [
|
|
2408
|
-
_vm.enableSearch
|
|
2409
|
-
? _c(
|
|
2410
|
-
"div",
|
|
2411
|
-
{
|
|
2412
|
-
staticClass: "eg-select-search",
|
|
2413
|
-
on: {
|
|
2414
|
-
mousedown: function ($event) {
|
|
2415
|
-
$event.stopPropagation();
|
|
2416
|
-
},
|
|
2417
|
-
},
|
|
2418
|
-
},
|
|
2419
|
-
[
|
|
2420
|
-
_c("input", {
|
|
2421
|
-
ref: "searchInput",
|
|
2422
|
-
staticClass: "eg-select-search-input",
|
|
2423
|
-
attrs: {
|
|
2424
|
-
placeholder: "Qidirish...",
|
|
2425
|
-
autocomplete: "off",
|
|
2426
|
-
autocorrect: "off",
|
|
2427
|
-
autocapitalize: "off",
|
|
2428
|
-
spellcheck: "false",
|
|
2429
|
-
},
|
|
2430
|
-
domProps: { value: _vm.searchQuery },
|
|
2431
|
-
on: {
|
|
2432
|
-
input: _vm.onSearch,
|
|
2433
|
-
keydown: function ($event) {
|
|
2434
|
-
$event.stopPropagation();
|
|
2435
|
-
return _vm.onSearchKey.apply(null, arguments)
|
|
2436
|
-
},
|
|
2437
|
-
},
|
|
2438
|
-
}),
|
|
2439
|
-
]
|
|
2440
|
-
)
|
|
2441
|
-
: _vm._e(),
|
|
2442
|
-
_vm._v(" "),
|
|
2443
|
-
_vm._l(_vm.filteredOptions, function (opt, idx) {
|
|
2444
|
-
return _c(
|
|
2445
|
-
"div",
|
|
2446
|
-
{
|
|
2447
|
-
key: opt.id != null ? opt.id : String(opt.value),
|
|
2448
|
-
staticClass: "eg-select-option",
|
|
2449
|
-
class: {
|
|
2450
|
-
"eg-select-option--active":
|
|
2451
|
-
String(opt.value) === String(_vm.value),
|
|
2452
|
-
"eg-select-option--highlighted": idx === _vm.highlight,
|
|
2453
|
-
},
|
|
2454
|
-
style: !_vm.searchQuery
|
|
2455
|
-
? { paddingLeft: 12 + opt.depth * 16 + "px" }
|
|
2456
|
-
: null,
|
|
2457
|
-
on: {
|
|
2458
|
-
mousedown: function ($event) {
|
|
2459
|
-
$event.preventDefault();
|
|
2460
|
-
return _vm.pickOption(opt)
|
|
2461
|
-
},
|
|
2462
|
-
mouseover: function ($event) {
|
|
2463
|
-
_vm.highlight = idx;
|
|
2464
|
-
},
|
|
2465
|
-
},
|
|
2466
|
-
},
|
|
2467
|
-
[
|
|
2468
|
-
!_vm.searchQuery
|
|
2469
|
-
? _c(
|
|
2470
|
-
"span",
|
|
2471
|
-
{
|
|
2472
|
-
staticClass: "eg-tree-toggle",
|
|
2473
|
-
class: { "eg-tree-toggle--leaf": !opt.hasChildren },
|
|
2474
|
-
on: {
|
|
2475
|
-
mousedown: function ($event) {
|
|
2476
|
-
$event.preventDefault();
|
|
2477
|
-
$event.stopPropagation();
|
|
2478
|
-
return _vm.toggleNode(idx)
|
|
2479
|
-
},
|
|
2480
|
-
},
|
|
2481
|
-
},
|
|
2482
|
-
[
|
|
2483
|
-
_vm._v(
|
|
2484
|
-
_vm._s(
|
|
2485
|
-
opt.hasChildren ? (opt.expanded ? "▾" : "▸") : ""
|
|
2486
|
-
)
|
|
2487
|
-
),
|
|
2488
|
-
]
|
|
2489
|
-
)
|
|
2490
|
-
: _vm._e(),
|
|
2491
|
-
_vm._v(" "),
|
|
2492
|
-
opt.color
|
|
2493
|
-
? _c("span", {
|
|
2494
|
-
staticClass: "eg-select-dot",
|
|
2495
|
-
style: { background: opt.color },
|
|
2496
|
-
})
|
|
2497
|
-
: _vm._e(),
|
|
2498
|
-
_vm._v("\n " + _vm._s(opt.label) + "\n "),
|
|
2499
|
-
]
|
|
2500
|
-
)
|
|
2501
|
-
}),
|
|
2502
|
-
]
|
|
2503
|
-
: _vm.open
|
|
2504
|
-
? _c(
|
|
2505
|
-
"div",
|
|
2506
|
-
{
|
|
2507
|
-
staticClass: "eg-select-dropdown eg-select-dropdown--floating",
|
|
2508
|
-
on: {
|
|
2509
|
-
mousedown: function ($event) {
|
|
2510
|
-
$event.stopPropagation();
|
|
2511
|
-
$event.preventDefault();
|
|
2512
|
-
},
|
|
2513
|
-
},
|
|
2514
|
-
},
|
|
2515
|
-
[
|
|
2516
|
-
_vm.enableSearch
|
|
2517
|
-
? _c(
|
|
2518
|
-
"div",
|
|
2519
|
-
{
|
|
2520
|
-
staticClass: "eg-select-search",
|
|
2521
|
-
on: {
|
|
2522
|
-
mousedown: function ($event) {
|
|
2523
|
-
$event.stopPropagation();
|
|
2524
|
-
},
|
|
2525
|
-
},
|
|
2526
|
-
},
|
|
2527
|
-
[
|
|
2528
|
-
_c("input", {
|
|
2529
|
-
ref: "searchInput",
|
|
2530
|
-
staticClass: "eg-select-search-input",
|
|
2531
|
-
attrs: {
|
|
2532
|
-
placeholder: "Qidirish...",
|
|
2533
|
-
autocomplete: "off",
|
|
2534
|
-
autocorrect: "off",
|
|
2535
|
-
autocapitalize: "off",
|
|
2536
|
-
spellcheck: "false",
|
|
2537
|
-
},
|
|
2538
|
-
domProps: { value: _vm.searchQuery },
|
|
2539
|
-
on: {
|
|
2540
|
-
input: _vm.onSearch,
|
|
2541
|
-
keydown: function ($event) {
|
|
2542
|
-
$event.stopPropagation();
|
|
2543
|
-
return _vm.onSearchKey.apply(null, arguments)
|
|
2544
|
-
},
|
|
2545
|
-
},
|
|
2546
|
-
}),
|
|
2547
|
-
]
|
|
2548
|
-
)
|
|
2549
|
-
: _vm._e(),
|
|
2550
|
-
_vm._v(" "),
|
|
2551
|
-
_vm._l(_vm.filteredOptions, function (opt, idx) {
|
|
2552
|
-
return _c(
|
|
2553
|
-
"div",
|
|
2554
|
-
{
|
|
2555
|
-
key: opt.id != null ? opt.id : String(opt.value),
|
|
2556
|
-
staticClass: "eg-select-option",
|
|
2557
|
-
class: {
|
|
2558
|
-
"eg-select-option--active":
|
|
2559
|
-
String(opt.value) === String(_vm.value),
|
|
2560
|
-
"eg-select-option--highlighted": idx === _vm.highlight,
|
|
2561
|
-
},
|
|
2562
|
-
style: !_vm.searchQuery
|
|
2563
|
-
? { paddingLeft: 12 + opt.depth * 16 + "px" }
|
|
2564
|
-
: null,
|
|
2565
|
-
on: {
|
|
2566
|
-
mousedown: function ($event) {
|
|
2567
|
-
$event.preventDefault();
|
|
2568
|
-
return _vm.pickOption(opt)
|
|
2569
|
-
},
|
|
2570
|
-
mouseover: function ($event) {
|
|
2571
|
-
_vm.highlight = idx;
|
|
2572
|
-
},
|
|
2573
|
-
},
|
|
2574
|
-
},
|
|
2575
|
-
[
|
|
2576
|
-
!_vm.searchQuery
|
|
2577
|
-
? _c(
|
|
2578
|
-
"span",
|
|
2579
|
-
{
|
|
2580
|
-
staticClass: "eg-tree-toggle",
|
|
2581
|
-
class: { "eg-tree-toggle--leaf": !opt.hasChildren },
|
|
2582
|
-
on: {
|
|
2583
|
-
mousedown: function ($event) {
|
|
2584
|
-
$event.preventDefault();
|
|
2585
|
-
$event.stopPropagation();
|
|
2586
|
-
return _vm.toggleNode(idx)
|
|
2587
|
-
},
|
|
2588
|
-
},
|
|
2589
|
-
},
|
|
2590
|
-
[
|
|
2591
|
-
_vm._v(
|
|
2592
|
-
_vm._s(
|
|
2593
|
-
opt.hasChildren
|
|
2594
|
-
? opt.expanded
|
|
2595
|
-
? "▾"
|
|
2596
|
-
: "▸"
|
|
2597
|
-
: ""
|
|
2598
|
-
)
|
|
2599
|
-
),
|
|
2600
|
-
]
|
|
2601
|
-
)
|
|
2602
|
-
: _vm._e(),
|
|
2603
|
-
_vm._v(" "),
|
|
2604
|
-
opt.color
|
|
2605
|
-
? _c("span", {
|
|
2606
|
-
staticClass: "eg-select-dot",
|
|
2607
|
-
style: { background: opt.color },
|
|
2608
|
-
})
|
|
2609
|
-
: _vm._e(),
|
|
2610
|
-
_vm._v("\n " + _vm._s(opt.label) + "\n "),
|
|
2611
|
-
]
|
|
2612
|
-
)
|
|
2613
|
-
}),
|
|
2614
|
-
],
|
|
2615
|
-
2
|
|
2616
|
-
)
|
|
2617
|
-
: _vm._e(),
|
|
2618
|
-
],
|
|
2619
|
-
2
|
|
2620
|
-
)
|
|
2621
|
-
};
|
|
2622
|
-
var __vue_staticRenderFns__$1 = [];
|
|
2623
|
-
__vue_render__$1._withStripped = true;
|
|
2624
|
-
|
|
2625
|
-
/* style */
|
|
2626
|
-
const __vue_inject_styles__$1 = function (inject) {
|
|
2627
|
-
if (!inject) return
|
|
2628
|
-
inject("data-v-7628cbd0_0", { source: "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqVA,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 // Portal: move to document.body so position:fixed is always relative to viewport,\n // regardless of any CSS transform on ancestor elements in the host project.\n // Vue's removeNode uses el.parentNode, so cleanup works correctly after the move.\n if (this.$el && this.$el.parentNode) {\n document.body.appendChild(this.$el)\n }\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 });
|
|
2629
|
-
|
|
2630
|
-
};
|
|
2631
|
-
/* scoped */
|
|
2632
|
-
const __vue_scope_id__$1 = undefined;
|
|
2633
|
-
/* module identifier */
|
|
2634
|
-
const __vue_module_identifier__$1 = undefined;
|
|
2635
|
-
/* functional template */
|
|
2636
|
-
const __vue_is_functional_template__$1 = false;
|
|
2637
|
-
/* style inject SSR */
|
|
2638
|
-
|
|
2639
|
-
/* style inject shadow dom */
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
const __vue_component__$1 = /*#__PURE__*/normalizeComponent(
|
|
2644
|
-
{ render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
|
|
2645
|
-
__vue_inject_styles__$1,
|
|
2646
|
-
__vue_script__$1,
|
|
2647
|
-
__vue_scope_id__$1,
|
|
2648
|
-
__vue_is_functional_template__$1,
|
|
2649
|
-
__vue_module_identifier__$1,
|
|
2650
|
-
false,
|
|
2651
|
-
createInjector,
|
|
2652
|
-
undefined,
|
|
2653
|
-
undefined
|
|
2654
|
-
);
|
|
2655
|
-
|
|
2656
1286
|
//
|
|
2657
1287
|
|
|
2658
1288
|
const SIZE_CONFIG = {
|
|
@@ -2667,8 +1297,6 @@ const OUTLINE_GUTTER = 20;
|
|
|
2667
1297
|
var script = {
|
|
2668
1298
|
name: 'ExcelGrid',
|
|
2669
1299
|
|
|
2670
|
-
components: { EgCheckbox: __vue_component__$3, EgSelect: __vue_component__$2, EgTreeSelect: __vue_component__$1 },
|
|
2671
|
-
|
|
2672
1300
|
// v-model → :data + @input
|
|
2673
1301
|
model: { prop: 'data', event: 'input' },
|
|
2674
1302
|
|
|
@@ -2974,6 +1602,32 @@ var script = {
|
|
|
2974
1602
|
}
|
|
2975
1603
|
},
|
|
2976
1604
|
|
|
1605
|
+
filteredSelectOptions() {
|
|
1606
|
+
if (!this.selectDropdown) return []
|
|
1607
|
+
const { options, searchQuery, enableSearch, tree, allFlatOptions } = this.selectDropdown;
|
|
1608
|
+
if (!enableSearch || !searchQuery || !searchQuery.trim()) return options
|
|
1609
|
+
const q = searchQuery.trim().toLowerCase();
|
|
1610
|
+
const pool = tree && allFlatOptions ? allFlatOptions : options;
|
|
1611
|
+
return pool.filter(o => String(o.label || '').toLowerCase().includes(q))
|
|
1612
|
+
},
|
|
1613
|
+
|
|
1614
|
+
dropdownStyle() {
|
|
1615
|
+
if (!this.selectDropdown) return {}
|
|
1616
|
+
const { rect } = this.selectDropdown;
|
|
1617
|
+
const optionH = this._rowH + 4;
|
|
1618
|
+
const searchH = this.selectDropdown.enableSearch ? 42 : 0;
|
|
1619
|
+
const approxHeight = Math.min(this.filteredSelectOptions.length * optionH + 8 + searchH, 320);
|
|
1620
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
1621
|
+
const top = spaceBelow >= approxHeight ? rect.bottom : rect.top - approxHeight;
|
|
1622
|
+
return {
|
|
1623
|
+
position: 'fixed',
|
|
1624
|
+
top: top + 'px',
|
|
1625
|
+
left: rect.left + 'px',
|
|
1626
|
+
minWidth: Math.max(rect.width, 120) + 'px',
|
|
1627
|
+
maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
|
|
1628
|
+
zIndex: 9999
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
2977
1631
|
|
|
2978
1632
|
invalidTooltipStyle() {
|
|
2979
1633
|
if (!this.invalidCell || !this.invalidCell.rect) return {}
|
|
@@ -3876,7 +2530,7 @@ var script = {
|
|
|
3876
2530
|
this.invalidCell = null;
|
|
3877
2531
|
if (this.editingCell) {
|
|
3878
2532
|
const editCol = this.leafCols[this.editingCell.col];
|
|
3879
|
-
if (editCol && this.
|
|
2533
|
+
if (editCol && this.hasEditSlot(editCol)) {
|
|
3880
2534
|
this.cancelEdit();
|
|
3881
2535
|
} else {
|
|
3882
2536
|
this.commitEdit();
|
|
@@ -3905,7 +2559,7 @@ var script = {
|
|
|
3905
2559
|
|
|
3906
2560
|
// Open select dropdown on click if cell was already active (2nd click)
|
|
3907
2561
|
const col = this.leafCols[ci];
|
|
3908
|
-
if (wasActive && col && this.isSelectLikeCol(col) && !this.
|
|
2562
|
+
if (wasActive && col && this.isSelectLikeCol(col) && !this.hasEditSlot(col) && this.isCellEditable(ri, ci)) {
|
|
3909
2563
|
this.openSelectDropdown(ri, ci, e);
|
|
3910
2564
|
e.preventDefault();
|
|
3911
2565
|
this.isMouseDown = true;
|
|
@@ -3944,7 +2598,7 @@ var script = {
|
|
|
3944
2598
|
onCellDblClick(ri, ci) {
|
|
3945
2599
|
const col = this.leafCols[ci];
|
|
3946
2600
|
if (!this.isCellEditable(ri, ci)) return
|
|
3947
|
-
if (col && this.isSelectLikeCol(col) && !this.
|
|
2601
|
+
if (col && this.isSelectLikeCol(col) && !this.hasEditSlot(col)) {
|
|
3948
2602
|
this.openSelectDropdownForActive();
|
|
3949
2603
|
return
|
|
3950
2604
|
}
|
|
@@ -4037,11 +2691,44 @@ var script = {
|
|
|
4037
2691
|
// KEYBOARD NAVIGATION (3.2)
|
|
4038
2692
|
// =========================================================
|
|
4039
2693
|
onKeyDown(e) {
|
|
4040
|
-
//
|
|
2694
|
+
// Handle open select dropdown keyboard navigation
|
|
4041
2695
|
if (this.selectDropdown) {
|
|
4042
|
-
const
|
|
4043
|
-
if (
|
|
4044
|
-
|
|
2696
|
+
const key = e.key;
|
|
2697
|
+
if (key === 'Escape') {
|
|
2698
|
+
e.preventDefault();
|
|
2699
|
+
this.closeSelectDropdown();
|
|
2700
|
+
return
|
|
2701
|
+
}
|
|
2702
|
+
if (key === 'Enter' || key === ' ') {
|
|
2703
|
+
e.preventDefault();
|
|
2704
|
+
const h = this.selectDropdown.highlight;
|
|
2705
|
+
if (h >= 0 && this.selectDropdown.options[h]) {
|
|
2706
|
+
this.commitSelectValue(this.selectDropdown.options[h].value);
|
|
2707
|
+
} else {
|
|
2708
|
+
this.closeSelectDropdown();
|
|
2709
|
+
}
|
|
2710
|
+
return
|
|
2711
|
+
}
|
|
2712
|
+
if (key === 'ArrowDown') {
|
|
2713
|
+
e.preventDefault();
|
|
2714
|
+
this.selectDropdown.highlight = Math.min(this.selectDropdown.highlight + 1, this.selectDropdown.options.length - 1);
|
|
2715
|
+
return
|
|
2716
|
+
}
|
|
2717
|
+
if (key === 'ArrowUp') {
|
|
2718
|
+
e.preventDefault();
|
|
2719
|
+
this.selectDropdown.highlight = Math.max(this.selectDropdown.highlight - 1, 0);
|
|
2720
|
+
return
|
|
2721
|
+
}
|
|
2722
|
+
if (this.selectDropdown.tree && !this.selectDropdown.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {
|
|
2723
|
+
e.preventDefault();
|
|
2724
|
+
const opt = this.selectDropdown.options[this.selectDropdown.highlight];
|
|
2725
|
+
if (opt && opt.hasChildren) {
|
|
2726
|
+
if (key === 'ArrowRight' && !opt.expanded) this.toggleTreeNode(this.selectDropdown.highlight);
|
|
2727
|
+
if (key === 'ArrowLeft' && opt.expanded) this.toggleTreeNode(this.selectDropdown.highlight);
|
|
2728
|
+
}
|
|
2729
|
+
return
|
|
2730
|
+
}
|
|
2731
|
+
return // block other keys while dropdown open
|
|
4045
2732
|
}
|
|
4046
2733
|
|
|
4047
2734
|
if (this.editingCell) return // handled by onEditKeyDown
|
|
@@ -4089,7 +2776,7 @@ var script = {
|
|
|
4089
2776
|
if (key === 'F2') {
|
|
4090
2777
|
e.preventDefault();
|
|
4091
2778
|
const activeCol = this.leafCols[this.activeCell.col];
|
|
4092
|
-
if (activeCol && this.isSelectLikeCol(activeCol) && !this.
|
|
2779
|
+
if (activeCol && this.isSelectLikeCol(activeCol) && !this.hasEditSlot(activeCol)) {
|
|
4093
2780
|
this.openSelectDropdownForActive();
|
|
4094
2781
|
} else if (activeCol && activeCol.type === 'checkbox') {
|
|
4095
2782
|
this.toggleCheckbox(this.activeCell.row, this.activeCell.col);
|
|
@@ -4185,7 +2872,7 @@ var script = {
|
|
|
4185
2872
|
if (e.ctrlKey || e.metaKey || e.altKey) return
|
|
4186
2873
|
if (e.key && e.key.length === 1) {
|
|
4187
2874
|
const activeCol = this.leafCols[this.activeCell.col];
|
|
4188
|
-
if (activeCol && this.isSelectLikeCol(activeCol) && !this.
|
|
2875
|
+
if (activeCol && this.isSelectLikeCol(activeCol) && !this.hasEditSlot(activeCol)) {
|
|
4189
2876
|
e.preventDefault();
|
|
4190
2877
|
this.openSelectDropdownForActive();
|
|
4191
2878
|
return
|
|
@@ -4260,7 +2947,7 @@ var script = {
|
|
|
4260
2947
|
const col = this.leafCols[ci];
|
|
4261
2948
|
if (!col || !this.isCellEditable(ri, ci)) return
|
|
4262
2949
|
if (ri < 0 || ri >= this.totalRows) return
|
|
4263
|
-
if (!this.
|
|
2950
|
+
if (!this.hasEditSlot(col)) {
|
|
4264
2951
|
if (this.isSelectLikeCol(col)) { this.openSelectDropdownForActive(); return }
|
|
4265
2952
|
if (col.type === 'checkbox') return
|
|
4266
2953
|
}
|
|
@@ -4317,19 +3004,9 @@ var script = {
|
|
|
4317
3004
|
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
4318
3005
|
},
|
|
4319
3006
|
|
|
4320
|
-
|
|
4321
|
-
return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['cell-' + col.key])
|
|
4322
|
-
},
|
|
4323
|
-
hasLegacyEditSlot(col) {
|
|
3007
|
+
hasEditSlot(col) {
|
|
4324
3008
|
return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])
|
|
4325
3009
|
},
|
|
4326
|
-
_hasCustomEdit(col) {
|
|
4327
|
-
// For select/treeSelect: cell-{key} slot only customizes display; dropdown still opens normally
|
|
4328
|
-
if (col.type === 'select' || col.type === 'treeSelect') {
|
|
4329
|
-
return this.hasLegacyEditSlot(col)
|
|
4330
|
-
}
|
|
4331
|
-
return this.hasCellSlot(col) || this.hasLegacyEditSlot(col)
|
|
4332
|
-
},
|
|
4333
3010
|
|
|
4334
3011
|
commitEditSlot(value) {
|
|
4335
3012
|
if (!this.editingCell) return
|
|
@@ -4380,6 +3057,10 @@ var script = {
|
|
|
4380
3057
|
this.setCheckboxValue(ri, ci, !this.isCheckboxChecked(ri, ci));
|
|
4381
3058
|
},
|
|
4382
3059
|
|
|
3060
|
+
onCheckboxChange(e, ri, ci) {
|
|
3061
|
+
this.setCheckboxValue(ri, ci, e.target.checked);
|
|
3062
|
+
},
|
|
3063
|
+
|
|
4383
3064
|
// =========================================================
|
|
4384
3065
|
// CELL EDITABILITY (3.17)
|
|
4385
3066
|
// =========================================================
|
|
@@ -4451,12 +3132,51 @@ var script = {
|
|
|
4451
3132
|
return !!(col && (col.type === 'select' || col.type === 'treeSelect'))
|
|
4452
3133
|
},
|
|
4453
3134
|
|
|
4454
|
-
treeOptionKeys(col) {
|
|
4455
|
-
return {
|
|
4456
|
-
valueKey: col.optionValueKey || 'value',
|
|
4457
|
-
labelKey: col.optionLabelKey || 'label',
|
|
4458
|
-
childrenKey: col.optionChildrenKey || 'children'
|
|
4459
|
-
}
|
|
3135
|
+
treeOptionKeys(col) {
|
|
3136
|
+
return {
|
|
3137
|
+
valueKey: col.optionValueKey || 'value',
|
|
3138
|
+
labelKey: col.optionLabelKey || 'label',
|
|
3139
|
+
childrenKey: col.optionChildrenKey || 'children'
|
|
3140
|
+
}
|
|
3141
|
+
},
|
|
3142
|
+
|
|
3143
|
+
// Flattens a tree of option nodes into a visible list, respecting which
|
|
3144
|
+
// node ids are currently expanded. Each returned entry carries enough
|
|
3145
|
+
// info (depth/hasChildren/expanded/id) to render indentation + a toggle.
|
|
3146
|
+
_treeFlatten(nodes, keys, expandedMap, depth, pathArr) {
|
|
3147
|
+
let out = []
|
|
3148
|
+
;(nodes || []).forEach((node, idx) => {
|
|
3149
|
+
const path = pathArr.concat(idx);
|
|
3150
|
+
const id = path.join('-');
|
|
3151
|
+
const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : [];
|
|
3152
|
+
const hasChildren = children.length > 0;
|
|
3153
|
+
const expanded = hasChildren && !!expandedMap[id];
|
|
3154
|
+
out.push({
|
|
3155
|
+
value: node[keys.valueKey],
|
|
3156
|
+
label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]),
|
|
3157
|
+
color: node.color,
|
|
3158
|
+
id, depth, hasChildren, expanded
|
|
3159
|
+
});
|
|
3160
|
+
if (expanded) {
|
|
3161
|
+
out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path));
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
return out
|
|
3165
|
+
},
|
|
3166
|
+
|
|
3167
|
+
_treeFlattenAll(nodes, keys, depth, result) {
|
|
3168
|
+
(nodes || []).forEach(node => {
|
|
3169
|
+
const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : [];
|
|
3170
|
+
result.push({
|
|
3171
|
+
value: node[keys.valueKey],
|
|
3172
|
+
label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]),
|
|
3173
|
+
color: node.color,
|
|
3174
|
+
id: String(node[keys.valueKey]),
|
|
3175
|
+
depth: 0, hasChildren: false, expanded: false
|
|
3176
|
+
});
|
|
3177
|
+
if (children.length) this._treeFlattenAll(children, keys, depth + 1, result);
|
|
3178
|
+
});
|
|
3179
|
+
return result
|
|
4460
3180
|
},
|
|
4461
3181
|
|
|
4462
3182
|
_treeFindLabel(nodes, keys, value) {
|
|
@@ -4470,9 +3190,28 @@ var script = {
|
|
|
4470
3190
|
return null
|
|
4471
3191
|
},
|
|
4472
3192
|
|
|
3193
|
+
// Returns the chain of ancestor node ids leading to `value` (inclusive),
|
|
3194
|
+
// so callers can pre-expand exactly that branch when opening the dropdown.
|
|
3195
|
+
_treeFindPath(nodes, keys, value, pathArr) {
|
|
3196
|
+
for (let idx = 0; idx < (nodes || []).length; idx++) {
|
|
3197
|
+
const node = nodes[idx];
|
|
3198
|
+
const path = pathArr.concat(idx);
|
|
3199
|
+
if (String(node[keys.valueKey]) === value) return [path.join('-')]
|
|
3200
|
+
const children = node[keys.childrenKey];
|
|
3201
|
+
if (Array.isArray(children)) {
|
|
3202
|
+
const sub = this._treeFindPath(children, keys, value, path);
|
|
3203
|
+
if (sub) return [path.join('-')].concat(sub)
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
return null
|
|
3207
|
+
},
|
|
3208
|
+
|
|
4473
3209
|
colOptions(ci) {
|
|
4474
3210
|
const col = this.leafCols[ci];
|
|
4475
3211
|
if (!col || !Array.isArray(col.options)) return []
|
|
3212
|
+
if (col.type === 'treeSelect') {
|
|
3213
|
+
return this._treeFlatten(col.options, this.treeOptionKeys(col), {}, 0, [])
|
|
3214
|
+
}
|
|
4476
3215
|
return col.options.map(o =>
|
|
4477
3216
|
typeof o === 'string' ? { value: o, label: o } : { value: o.value, label: o.label != null ? o.label : String(o.value), color: o.color }
|
|
4478
3217
|
)
|
|
@@ -4494,17 +3233,58 @@ var script = {
|
|
|
4494
3233
|
|
|
4495
3234
|
_buildSelectDropdownState(ri, ci, col, currentValue, rect) {
|
|
4496
3235
|
const rectObj = { top: rect.top, bottom: rect.bottom, left: rect.left, width: rect.width };
|
|
3236
|
+
const enableSearch = !!col.enableSearch;
|
|
3237
|
+
if (col.type === 'treeSelect') {
|
|
3238
|
+
const keys = this.treeOptionKeys(col);
|
|
3239
|
+
const path = (Array.isArray(col.options) && this._treeFindPath(col.options, keys, currentValue, [])) || [];
|
|
3240
|
+
const expandedMap = {};
|
|
3241
|
+
path.forEach(id => { expandedMap[id] = true; });
|
|
3242
|
+
const flat = this._treeFlatten(col.options, keys, expandedMap, 0, []);
|
|
3243
|
+
return {
|
|
3244
|
+
row: ri, col: ci, tree: true, keys, expandedMap,
|
|
3245
|
+
options: flat,
|
|
3246
|
+
allFlatOptions: enableSearch ? this._treeFlattenAll(col.options || [], keys, 0, []) : null,
|
|
3247
|
+
currentValue,
|
|
3248
|
+
highlight: flat.findIndex(o => String(o.value) === currentValue),
|
|
3249
|
+
rect: rectObj,
|
|
3250
|
+
enableSearch,
|
|
3251
|
+
searchQuery: ''
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
const opts = this.colOptions(ci);
|
|
4497
3255
|
return {
|
|
4498
|
-
row: ri, col: ci,
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
rawOptions: col.options || [],
|
|
3256
|
+
row: ri, col: ci, tree: false,
|
|
3257
|
+
options: opts,
|
|
3258
|
+
allFlatOptions: null,
|
|
4502
3259
|
currentValue,
|
|
3260
|
+
highlight: opts.findIndex(o => String(o.value) === currentValue),
|
|
4503
3261
|
rect: rectObj,
|
|
4504
|
-
enableSearch
|
|
3262
|
+
enableSearch,
|
|
3263
|
+
searchQuery: ''
|
|
4505
3264
|
}
|
|
4506
3265
|
},
|
|
4507
3266
|
|
|
3267
|
+
toggleTreeNode(idx) {
|
|
3268
|
+
if (!this.selectDropdown || !this.selectDropdown.tree) return
|
|
3269
|
+
const opt = this.selectDropdown.options[idx];
|
|
3270
|
+
if (!opt || !opt.hasChildren) return
|
|
3271
|
+
const { row, col, expandedMap, keys } = this.selectDropdown;
|
|
3272
|
+
const colDef = this.leafCols[col];
|
|
3273
|
+
if (!colDef) return
|
|
3274
|
+
const nextExpandedMap = { ...expandedMap };
|
|
3275
|
+
if (nextExpandedMap[opt.id]) delete nextExpandedMap[opt.id];
|
|
3276
|
+
else nextExpandedMap[opt.id] = true;
|
|
3277
|
+
const flat = this._treeFlatten(colDef.options, keys, nextExpandedMap, 0, []);
|
|
3278
|
+
const newHighlight = flat.findIndex(o => o.id === opt.id);
|
|
3279
|
+
this.selectDropdown = {
|
|
3280
|
+
...this.selectDropdown,
|
|
3281
|
+
row, col,
|
|
3282
|
+
expandedMap: nextExpandedMap,
|
|
3283
|
+
options: flat,
|
|
3284
|
+
highlight: newHighlight >= 0 ? newHighlight : this.selectDropdown.highlight
|
|
3285
|
+
};
|
|
3286
|
+
},
|
|
3287
|
+
|
|
4508
3288
|
openSelectDropdown(ri, ci, e) {
|
|
4509
3289
|
const col = this.leafCols[ci];
|
|
4510
3290
|
if (!col || !Array.isArray(col.options)) return
|
|
@@ -4512,6 +3292,9 @@ var script = {
|
|
|
4512
3292
|
const rect = el.getBoundingClientRect();
|
|
4513
3293
|
const currentValue = String(this.getCellValue(ri, ci) != null ? this.getCellValue(ri, ci) : '');
|
|
4514
3294
|
this.selectDropdown = this._buildSelectDropdownState(ri, ci, col, currentValue, rect);
|
|
3295
|
+
if (col.enableSearch) {
|
|
3296
|
+
this.$nextTick(() => { this.$refs.dropdownSearch && this.$refs.dropdownSearch.focus(); });
|
|
3297
|
+
}
|
|
4515
3298
|
},
|
|
4516
3299
|
|
|
4517
3300
|
openSelectDropdownForActive() {
|
|
@@ -4524,10 +3307,13 @@ var script = {
|
|
|
4524
3307
|
const rect = el.getBoundingClientRect();
|
|
4525
3308
|
const currentValue = String(this.getCellValue(row, col) != null ? this.getCellValue(row, col) : '');
|
|
4526
3309
|
this.selectDropdown = this._buildSelectDropdownState(row, col, colDef, currentValue, rect);
|
|
3310
|
+
if (colDef.enableSearch) {
|
|
3311
|
+
this.$nextTick(() => { this.$refs.dropdownSearch && this.$refs.dropdownSearch.focus(); });
|
|
3312
|
+
}
|
|
4527
3313
|
});
|
|
4528
3314
|
},
|
|
4529
3315
|
|
|
4530
|
-
|
|
3316
|
+
commitSelectValue(value) {
|
|
4531
3317
|
if (!this.selectDropdown) return
|
|
4532
3318
|
const { row, col } = this.selectDropdown;
|
|
4533
3319
|
const colDef = this.leafCols[col];
|
|
@@ -4544,30 +3330,59 @@ var script = {
|
|
|
4544
3330
|
newData[row] = { ...oldData[row], [colDef.key]: value };
|
|
4545
3331
|
this._emitDataChange(oldData, newData);
|
|
4546
3332
|
if (typeof colDef.onSelect === 'function') {
|
|
4547
|
-
|
|
3333
|
+
const _opt = this.filteredSelectOptions.find(o => o.value === value);
|
|
3334
|
+
colDef.onSelect({ value, label: _opt ? _opt.label : String(value) }, newData[row], row, colDef);
|
|
4548
3335
|
}
|
|
4549
3336
|
}
|
|
4550
3337
|
this.closeSelectDropdown();
|
|
4551
3338
|
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
4552
3339
|
},
|
|
4553
3340
|
|
|
3341
|
+
onDropdownSearch(e) {
|
|
3342
|
+
if (!this.selectDropdown) return
|
|
3343
|
+
this.selectDropdown.searchQuery = e.target.value;
|
|
3344
|
+
this.selectDropdown.highlight = 0;
|
|
3345
|
+
},
|
|
3346
|
+
|
|
3347
|
+
onDropdownSearchKeyDown(e) {
|
|
3348
|
+
if (!this.selectDropdown) return
|
|
3349
|
+
const { key } = e;
|
|
3350
|
+
if (key === 'Escape') {
|
|
3351
|
+
this.closeSelectDropdown();
|
|
3352
|
+
this.$refs.wrapper && this.$refs.wrapper.focus();
|
|
3353
|
+
} else if (key === 'ArrowDown') {
|
|
3354
|
+
e.preventDefault();
|
|
3355
|
+
this.selectDropdown.highlight = Math.min(
|
|
3356
|
+
(this.selectDropdown.highlight || 0) + 1,
|
|
3357
|
+
this.filteredSelectOptions.length - 1
|
|
3358
|
+
);
|
|
3359
|
+
} else if (key === 'ArrowUp') {
|
|
3360
|
+
e.preventDefault();
|
|
3361
|
+
this.selectDropdown.highlight = Math.max((this.selectDropdown.highlight || 0) - 1, 0);
|
|
3362
|
+
} else if (key === 'Enter') {
|
|
3363
|
+
e.preventDefault();
|
|
3364
|
+
const h = this.selectDropdown.highlight;
|
|
3365
|
+
const opts = this.filteredSelectOptions;
|
|
3366
|
+
if (h >= 0 && opts[h]) this.commitSelectValue(opts[h].value);
|
|
3367
|
+
}
|
|
3368
|
+
},
|
|
3369
|
+
|
|
4554
3370
|
closeSelectDropdown() {
|
|
4555
3371
|
this.selectDropdown = null;
|
|
4556
3372
|
},
|
|
4557
3373
|
|
|
4558
3374
|
_onWindowScrollForDropdown(e) {
|
|
4559
3375
|
if (!this.selectDropdown) return
|
|
4560
|
-
const
|
|
4561
|
-
const dd = comp && comp.$el ? comp.$el : null;
|
|
3376
|
+
const dd = this.$el ? this.$el.querySelector('.eg-select-dropdown') : null;
|
|
4562
3377
|
if (dd && dd.contains(e.target)) return
|
|
4563
3378
|
this.closeSelectDropdown();
|
|
4564
3379
|
},
|
|
4565
3380
|
|
|
4566
3381
|
_onDocMouseDownForDropdown(e) {
|
|
4567
3382
|
if (!this.selectDropdown) return
|
|
4568
|
-
const
|
|
4569
|
-
const dd = comp && comp.$el ? comp.$el : null;
|
|
3383
|
+
const dd = this.$el ? this.$el.querySelector('.eg-select-dropdown') : null;
|
|
4570
3384
|
if (dd && dd.contains(e.target)) return
|
|
3385
|
+
// Click outside dropdown — close it
|
|
4571
3386
|
this.closeSelectDropdown();
|
|
4572
3387
|
},
|
|
4573
3388
|
|
|
@@ -5513,6 +4328,134 @@ var script = {
|
|
|
5513
4328
|
}
|
|
5514
4329
|
};
|
|
5515
4330
|
|
|
4331
|
+
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
|
|
4332
|
+
if (typeof shadowMode !== 'boolean') {
|
|
4333
|
+
createInjectorSSR = createInjector;
|
|
4334
|
+
createInjector = shadowMode;
|
|
4335
|
+
shadowMode = false;
|
|
4336
|
+
}
|
|
4337
|
+
// Vue.extend constructor export interop.
|
|
4338
|
+
const options = typeof script === 'function' ? script.options : script;
|
|
4339
|
+
// render functions
|
|
4340
|
+
if (template && template.render) {
|
|
4341
|
+
options.render = template.render;
|
|
4342
|
+
options.staticRenderFns = template.staticRenderFns;
|
|
4343
|
+
options._compiled = true;
|
|
4344
|
+
// functional template
|
|
4345
|
+
if (isFunctionalTemplate) {
|
|
4346
|
+
options.functional = true;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
// scopedId
|
|
4350
|
+
if (scopeId) {
|
|
4351
|
+
options._scopeId = scopeId;
|
|
4352
|
+
}
|
|
4353
|
+
let hook;
|
|
4354
|
+
if (moduleIdentifier) {
|
|
4355
|
+
// server build
|
|
4356
|
+
hook = function (context) {
|
|
4357
|
+
// 2.3 injection
|
|
4358
|
+
context =
|
|
4359
|
+
context || // cached call
|
|
4360
|
+
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|
4361
|
+
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
|
|
4362
|
+
// 2.2 with runInNewContext: true
|
|
4363
|
+
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
|
4364
|
+
context = __VUE_SSR_CONTEXT__;
|
|
4365
|
+
}
|
|
4366
|
+
// inject component styles
|
|
4367
|
+
if (style) {
|
|
4368
|
+
style.call(this, createInjectorSSR(context));
|
|
4369
|
+
}
|
|
4370
|
+
// register component module identifier for async chunk inference
|
|
4371
|
+
if (context && context._registeredComponents) {
|
|
4372
|
+
context._registeredComponents.add(moduleIdentifier);
|
|
4373
|
+
}
|
|
4374
|
+
};
|
|
4375
|
+
// used by ssr in case component is cached and beforeCreate
|
|
4376
|
+
// never gets called
|
|
4377
|
+
options._ssrRegister = hook;
|
|
4378
|
+
}
|
|
4379
|
+
else if (style) {
|
|
4380
|
+
hook = shadowMode
|
|
4381
|
+
? function (context) {
|
|
4382
|
+
style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
|
|
4383
|
+
}
|
|
4384
|
+
: function (context) {
|
|
4385
|
+
style.call(this, createInjector(context));
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4388
|
+
if (hook) {
|
|
4389
|
+
if (options.functional) {
|
|
4390
|
+
// register for functional component in vue file
|
|
4391
|
+
const originalRender = options.render;
|
|
4392
|
+
options.render = function renderWithStyleInjection(h, context) {
|
|
4393
|
+
hook.call(context);
|
|
4394
|
+
return originalRender(h, context);
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
else {
|
|
4398
|
+
// inject component registration as beforeCreate hook
|
|
4399
|
+
const existing = options.beforeCreate;
|
|
4400
|
+
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return script;
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
const isOldIE = typeof navigator !== 'undefined' &&
|
|
4407
|
+
/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
|
|
4408
|
+
function createInjector(context) {
|
|
4409
|
+
return (id, style) => addStyle(id, style);
|
|
4410
|
+
}
|
|
4411
|
+
let HEAD;
|
|
4412
|
+
const styles = {};
|
|
4413
|
+
function addStyle(id, css) {
|
|
4414
|
+
const group = isOldIE ? css.media || 'default' : id;
|
|
4415
|
+
const style = styles[group] || (styles[group] = { ids: new Set(), styles: [] });
|
|
4416
|
+
if (!style.ids.has(id)) {
|
|
4417
|
+
style.ids.add(id);
|
|
4418
|
+
let code = css.source;
|
|
4419
|
+
if (css.map) {
|
|
4420
|
+
// https://developer.chrome.com/devtools/docs/javascript-debugging
|
|
4421
|
+
// this makes source maps inside style tags work properly in Chrome
|
|
4422
|
+
code += '\n/*# sourceURL=' + css.map.sources[0] + ' */';
|
|
4423
|
+
// http://stackoverflow.com/a/26603875
|
|
4424
|
+
code +=
|
|
4425
|
+
'\n/*# sourceMappingURL=data:application/json;base64,' +
|
|
4426
|
+
btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) +
|
|
4427
|
+
' */';
|
|
4428
|
+
}
|
|
4429
|
+
if (!style.element) {
|
|
4430
|
+
style.element = document.createElement('style');
|
|
4431
|
+
style.element.type = 'text/css';
|
|
4432
|
+
if (css.media)
|
|
4433
|
+
style.element.setAttribute('media', css.media);
|
|
4434
|
+
if (HEAD === undefined) {
|
|
4435
|
+
HEAD = document.head || document.getElementsByTagName('head')[0];
|
|
4436
|
+
}
|
|
4437
|
+
HEAD.appendChild(style.element);
|
|
4438
|
+
}
|
|
4439
|
+
if ('styleSheet' in style.element) {
|
|
4440
|
+
style.styles.push(code);
|
|
4441
|
+
style.element.styleSheet.cssText = style.styles
|
|
4442
|
+
.filter(Boolean)
|
|
4443
|
+
.join('\n');
|
|
4444
|
+
}
|
|
4445
|
+
else {
|
|
4446
|
+
const index = style.ids.size - 1;
|
|
4447
|
+
const textNode = document.createTextNode(code);
|
|
4448
|
+
const nodes = style.element.childNodes;
|
|
4449
|
+
if (nodes[index])
|
|
4450
|
+
style.element.removeChild(nodes[index]);
|
|
4451
|
+
if (nodes.length)
|
|
4452
|
+
style.element.insertBefore(textNode, nodes[index]);
|
|
4453
|
+
else
|
|
4454
|
+
style.element.appendChild(textNode);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
|
|
5516
4459
|
/* script */
|
|
5517
4460
|
const __vue_script__ = script;
|
|
5518
4461
|
|
|
@@ -6398,42 +5341,9 @@ var __vue_render__ = function () {
|
|
|
6398
5341
|
ci
|
|
6399
5342
|
)
|
|
6400
5343
|
? [
|
|
6401
|
-
_vm.
|
|
5344
|
+
_vm.hasEditSlot(
|
|
6402
5345
|
col
|
|
6403
5346
|
)
|
|
6404
|
-
? _vm._t(
|
|
6405
|
-
"cell-" +
|
|
6406
|
-
col.key,
|
|
6407
|
-
null,
|
|
6408
|
-
{
|
|
6409
|
-
row: _vm
|
|
6410
|
-
._dataRaw[
|
|
6411
|
-
ri
|
|
6412
|
-
],
|
|
6413
|
-
rowIndex:
|
|
6414
|
-
ri,
|
|
6415
|
-
column:
|
|
6416
|
-
col,
|
|
6417
|
-
value:
|
|
6418
|
-
_vm.getCellValue(
|
|
6419
|
-
ri,
|
|
6420
|
-
ci
|
|
6421
|
-
),
|
|
6422
|
-
isEditing: true,
|
|
6423
|
-
done: function (
|
|
6424
|
-
v
|
|
6425
|
-
) {
|
|
6426
|
-
return _vm.commitEditSlot(
|
|
6427
|
-
v
|
|
6428
|
-
)
|
|
6429
|
-
},
|
|
6430
|
-
cancel:
|
|
6431
|
-
_vm.cancelEdit,
|
|
6432
|
-
}
|
|
6433
|
-
)
|
|
6434
|
-
: _vm.hasLegacyEditSlot(
|
|
6435
|
-
col
|
|
6436
|
-
)
|
|
6437
5347
|
? _vm._t(
|
|
6438
5348
|
"edit-" +
|
|
6439
5349
|
col.key,
|
|
@@ -6521,34 +5431,67 @@ var __vue_render__ = function () {
|
|
|
6521
5431
|
"checkbox"
|
|
6522
5432
|
? [
|
|
6523
5433
|
_c(
|
|
6524
|
-
"
|
|
5434
|
+
"label",
|
|
6525
5435
|
{
|
|
6526
|
-
|
|
5436
|
+
staticClass:
|
|
5437
|
+
"eg-checkbox",
|
|
5438
|
+
class:
|
|
6527
5439
|
{
|
|
6528
|
-
|
|
6529
|
-
_vm.isCheckboxChecked(
|
|
6530
|
-
ri,
|
|
6531
|
-
ci
|
|
6532
|
-
),
|
|
6533
|
-
disabled:
|
|
5440
|
+
"eg-checkbox--disabled":
|
|
6534
5441
|
!_vm.isCellEditable(
|
|
6535
5442
|
ri,
|
|
6536
5443
|
ci
|
|
6537
5444
|
),
|
|
6538
5445
|
},
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
5446
|
+
},
|
|
5447
|
+
[
|
|
5448
|
+
_c(
|
|
5449
|
+
"input",
|
|
5450
|
+
{
|
|
5451
|
+
staticClass:
|
|
5452
|
+
"eg-checkbox-input",
|
|
5453
|
+
attrs:
|
|
5454
|
+
{
|
|
5455
|
+
type: "checkbox",
|
|
5456
|
+
disabled:
|
|
5457
|
+
!_vm.isCellEditable(
|
|
5458
|
+
ri,
|
|
5459
|
+
ci
|
|
5460
|
+
),
|
|
5461
|
+
},
|
|
5462
|
+
domProps:
|
|
5463
|
+
{
|
|
5464
|
+
checked:
|
|
5465
|
+
_vm.isCheckboxChecked(
|
|
5466
|
+
ri,
|
|
5467
|
+
ci
|
|
5468
|
+
),
|
|
5469
|
+
},
|
|
5470
|
+
on: {
|
|
5471
|
+
change:
|
|
5472
|
+
function (
|
|
5473
|
+
$event
|
|
5474
|
+
) {
|
|
5475
|
+
return _vm.onCheckboxChange(
|
|
5476
|
+
$event,
|
|
5477
|
+
ri,
|
|
5478
|
+
ci
|
|
5479
|
+
)
|
|
5480
|
+
},
|
|
6549
5481
|
},
|
|
6550
|
-
|
|
6551
|
-
|
|
5482
|
+
}
|
|
5483
|
+
),
|
|
5484
|
+
_vm._v(
|
|
5485
|
+
" "
|
|
5486
|
+
),
|
|
5487
|
+
_c(
|
|
5488
|
+
"span",
|
|
5489
|
+
{
|
|
5490
|
+
staticClass:
|
|
5491
|
+
"eg-checkbox-box",
|
|
5492
|
+
}
|
|
5493
|
+
),
|
|
5494
|
+
]
|
|
6552
5495
|
),
|
|
6553
5496
|
]
|
|
6554
5497
|
: [
|
|
@@ -6593,10 +5536,6 @@ var __vue_render__ = function () {
|
|
|
6593
5536
|
ri,
|
|
6594
5537
|
ci
|
|
6595
5538
|
),
|
|
6596
|
-
isEditing: false,
|
|
6597
|
-
done: null,
|
|
6598
|
-
cancel:
|
|
6599
|
-
null,
|
|
6600
5539
|
}
|
|
6601
5540
|
),
|
|
6602
5541
|
],
|
|
@@ -6762,42 +5701,9 @@ var __vue_render__ = function () {
|
|
|
6762
5701
|
ci
|
|
6763
5702
|
)
|
|
6764
5703
|
? [
|
|
6765
|
-
_vm.
|
|
5704
|
+
_vm.hasEditSlot(
|
|
6766
5705
|
col
|
|
6767
5706
|
)
|
|
6768
|
-
? _vm._t(
|
|
6769
|
-
"cell-" +
|
|
6770
|
-
col.key,
|
|
6771
|
-
null,
|
|
6772
|
-
{
|
|
6773
|
-
row: _vm
|
|
6774
|
-
._dataRaw[
|
|
6775
|
-
ri
|
|
6776
|
-
],
|
|
6777
|
-
rowIndex:
|
|
6778
|
-
ri,
|
|
6779
|
-
column:
|
|
6780
|
-
col,
|
|
6781
|
-
value:
|
|
6782
|
-
_vm.getCellValue(
|
|
6783
|
-
ri,
|
|
6784
|
-
ci
|
|
6785
|
-
),
|
|
6786
|
-
isEditing: true,
|
|
6787
|
-
done: function (
|
|
6788
|
-
v
|
|
6789
|
-
) {
|
|
6790
|
-
return _vm.commitEditSlot(
|
|
6791
|
-
v
|
|
6792
|
-
)
|
|
6793
|
-
},
|
|
6794
|
-
cancel:
|
|
6795
|
-
_vm.cancelEdit,
|
|
6796
|
-
}
|
|
6797
|
-
)
|
|
6798
|
-
: _vm.hasLegacyEditSlot(
|
|
6799
|
-
col
|
|
6800
|
-
)
|
|
6801
5707
|
? _vm._t(
|
|
6802
5708
|
"edit-" +
|
|
6803
5709
|
col.key,
|
|
@@ -6885,34 +5791,67 @@ var __vue_render__ = function () {
|
|
|
6885
5791
|
"checkbox"
|
|
6886
5792
|
? [
|
|
6887
5793
|
_c(
|
|
6888
|
-
"
|
|
5794
|
+
"label",
|
|
6889
5795
|
{
|
|
6890
|
-
|
|
5796
|
+
staticClass:
|
|
5797
|
+
"eg-checkbox",
|
|
5798
|
+
class:
|
|
6891
5799
|
{
|
|
6892
|
-
|
|
6893
|
-
_vm.isCheckboxChecked(
|
|
6894
|
-
ri,
|
|
6895
|
-
ci
|
|
6896
|
-
),
|
|
6897
|
-
disabled:
|
|
5800
|
+
"eg-checkbox--disabled":
|
|
6898
5801
|
!_vm.isCellEditable(
|
|
6899
5802
|
ri,
|
|
6900
5803
|
ci
|
|
6901
5804
|
),
|
|
6902
5805
|
},
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
5806
|
+
},
|
|
5807
|
+
[
|
|
5808
|
+
_c(
|
|
5809
|
+
"input",
|
|
5810
|
+
{
|
|
5811
|
+
staticClass:
|
|
5812
|
+
"eg-checkbox-input",
|
|
5813
|
+
attrs:
|
|
5814
|
+
{
|
|
5815
|
+
type: "checkbox",
|
|
5816
|
+
disabled:
|
|
5817
|
+
!_vm.isCellEditable(
|
|
5818
|
+
ri,
|
|
5819
|
+
ci
|
|
5820
|
+
),
|
|
5821
|
+
},
|
|
5822
|
+
domProps:
|
|
5823
|
+
{
|
|
5824
|
+
checked:
|
|
5825
|
+
_vm.isCheckboxChecked(
|
|
5826
|
+
ri,
|
|
5827
|
+
ci
|
|
5828
|
+
),
|
|
5829
|
+
},
|
|
5830
|
+
on: {
|
|
5831
|
+
change:
|
|
5832
|
+
function (
|
|
5833
|
+
$event
|
|
5834
|
+
) {
|
|
5835
|
+
return _vm.onCheckboxChange(
|
|
5836
|
+
$event,
|
|
5837
|
+
ri,
|
|
5838
|
+
ci
|
|
5839
|
+
)
|
|
5840
|
+
},
|
|
6913
5841
|
},
|
|
6914
|
-
|
|
6915
|
-
|
|
5842
|
+
}
|
|
5843
|
+
),
|
|
5844
|
+
_vm._v(
|
|
5845
|
+
" "
|
|
5846
|
+
),
|
|
5847
|
+
_c(
|
|
5848
|
+
"span",
|
|
5849
|
+
{
|
|
5850
|
+
staticClass:
|
|
5851
|
+
"eg-checkbox-box",
|
|
5852
|
+
}
|
|
5853
|
+
),
|
|
5854
|
+
]
|
|
6916
5855
|
),
|
|
6917
5856
|
]
|
|
6918
5857
|
: [
|
|
@@ -6957,10 +5896,6 @@ var __vue_render__ = function () {
|
|
|
6957
5896
|
ri,
|
|
6958
5897
|
ci
|
|
6959
5898
|
),
|
|
6960
|
-
isEditing: false,
|
|
6961
|
-
done: null,
|
|
6962
|
-
cancel:
|
|
6963
|
-
null,
|
|
6964
5899
|
}
|
|
6965
5900
|
),
|
|
6966
5901
|
],
|
|
@@ -7122,42 +6057,9 @@ var __vue_render__ = function () {
|
|
|
7122
6057
|
col._flatIndex
|
|
7123
6058
|
)
|
|
7124
6059
|
? [
|
|
7125
|
-
_vm.
|
|
6060
|
+
_vm.hasEditSlot(
|
|
7126
6061
|
col
|
|
7127
6062
|
)
|
|
7128
|
-
? _vm._t(
|
|
7129
|
-
"cell-" +
|
|
7130
|
-
col.key,
|
|
7131
|
-
null,
|
|
7132
|
-
{
|
|
7133
|
-
row: _vm
|
|
7134
|
-
._dataRaw[
|
|
7135
|
-
ri
|
|
7136
|
-
],
|
|
7137
|
-
rowIndex:
|
|
7138
|
-
ri,
|
|
7139
|
-
column:
|
|
7140
|
-
col,
|
|
7141
|
-
value:
|
|
7142
|
-
_vm.getCellValue(
|
|
7143
|
-
ri,
|
|
7144
|
-
col._flatIndex
|
|
7145
|
-
),
|
|
7146
|
-
isEditing: true,
|
|
7147
|
-
done: function (
|
|
7148
|
-
v
|
|
7149
|
-
) {
|
|
7150
|
-
return _vm.commitEditSlot(
|
|
7151
|
-
v
|
|
7152
|
-
)
|
|
7153
|
-
},
|
|
7154
|
-
cancel:
|
|
7155
|
-
_vm.cancelEdit,
|
|
7156
|
-
}
|
|
7157
|
-
)
|
|
7158
|
-
: _vm.hasLegacyEditSlot(
|
|
7159
|
-
col
|
|
7160
|
-
)
|
|
7161
6063
|
? _vm._t(
|
|
7162
6064
|
"edit-" +
|
|
7163
6065
|
col.key,
|
|
@@ -7245,33 +6147,66 @@ var __vue_render__ = function () {
|
|
|
7245
6147
|
"checkbox"
|
|
7246
6148
|
? [
|
|
7247
6149
|
_c(
|
|
7248
|
-
"
|
|
6150
|
+
"label",
|
|
7249
6151
|
{
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
col._flatIndex
|
|
7255
|
-
),
|
|
7256
|
-
disabled:
|
|
6152
|
+
staticClass:
|
|
6153
|
+
"eg-checkbox",
|
|
6154
|
+
class: {
|
|
6155
|
+
"eg-checkbox--disabled":
|
|
7257
6156
|
!_vm.isCellEditable(
|
|
7258
6157
|
ri,
|
|
7259
6158
|
col._flatIndex
|
|
7260
6159
|
),
|
|
7261
6160
|
},
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
6161
|
+
},
|
|
6162
|
+
[
|
|
6163
|
+
_c(
|
|
6164
|
+
"input",
|
|
6165
|
+
{
|
|
6166
|
+
staticClass:
|
|
6167
|
+
"eg-checkbox-input",
|
|
6168
|
+
attrs:
|
|
6169
|
+
{
|
|
6170
|
+
type: "checkbox",
|
|
6171
|
+
disabled:
|
|
6172
|
+
!_vm.isCellEditable(
|
|
6173
|
+
ri,
|
|
6174
|
+
col._flatIndex
|
|
6175
|
+
),
|
|
6176
|
+
},
|
|
6177
|
+
domProps:
|
|
6178
|
+
{
|
|
6179
|
+
checked:
|
|
6180
|
+
_vm.isCheckboxChecked(
|
|
6181
|
+
ri,
|
|
6182
|
+
col._flatIndex
|
|
6183
|
+
),
|
|
6184
|
+
},
|
|
6185
|
+
on: {
|
|
6186
|
+
change:
|
|
6187
|
+
function (
|
|
6188
|
+
$event
|
|
6189
|
+
) {
|
|
6190
|
+
return _vm.onCheckboxChange(
|
|
6191
|
+
$event,
|
|
6192
|
+
ri,
|
|
6193
|
+
col._flatIndex
|
|
6194
|
+
)
|
|
6195
|
+
},
|
|
7272
6196
|
},
|
|
7273
|
-
|
|
7274
|
-
|
|
6197
|
+
}
|
|
6198
|
+
),
|
|
6199
|
+
_vm._v(
|
|
6200
|
+
" "
|
|
6201
|
+
),
|
|
6202
|
+
_c(
|
|
6203
|
+
"span",
|
|
6204
|
+
{
|
|
6205
|
+
staticClass:
|
|
6206
|
+
"eg-checkbox-box",
|
|
6207
|
+
}
|
|
6208
|
+
),
|
|
6209
|
+
]
|
|
7275
6210
|
),
|
|
7276
6211
|
]
|
|
7277
6212
|
: [
|
|
@@ -7316,10 +6251,6 @@ var __vue_render__ = function () {
|
|
|
7316
6251
|
ri,
|
|
7317
6252
|
col._flatIndex
|
|
7318
6253
|
),
|
|
7319
|
-
isEditing: false,
|
|
7320
|
-
done: null,
|
|
7321
|
-
cancel:
|
|
7322
|
-
null,
|
|
7323
6254
|
}
|
|
7324
6255
|
),
|
|
7325
6256
|
],
|
|
@@ -7602,41 +6533,125 @@ var __vue_render__ = function () {
|
|
|
7602
6533
|
? _c("div", { staticClass: "eg-import-overlay" }, [_vm._m(0)])
|
|
7603
6534
|
: _vm._e(),
|
|
7604
6535
|
_vm._v(" "),
|
|
7605
|
-
_vm.selectDropdown
|
|
7606
|
-
? _c(
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
select: _vm.onComponentSelect,
|
|
7617
|
-
close: _vm.closeSelectDropdown,
|
|
7618
|
-
},
|
|
7619
|
-
})
|
|
7620
|
-
: _vm._e(),
|
|
7621
|
-
_vm._v(" "),
|
|
7622
|
-
_vm.selectDropdown && _vm.selectDropdown.tree
|
|
7623
|
-
? _c("eg-tree-select", {
|
|
7624
|
-
ref: "selectDropdownComp",
|
|
7625
|
-
attrs: {
|
|
7626
|
-
value: _vm.selectDropdown.currentValue,
|
|
7627
|
-
options: _vm.selectDropdown.rawOptions,
|
|
7628
|
-
"value-key": _vm.selectDropdown.keys.valueKey,
|
|
7629
|
-
"label-key": _vm.selectDropdown.keys.labelKey,
|
|
7630
|
-
"children-key": _vm.selectDropdown.keys.childrenKey,
|
|
7631
|
-
"enable-search": _vm.selectDropdown.enableSearch,
|
|
7632
|
-
"trigger-rect": _vm.selectDropdown.rect,
|
|
7633
|
-
"dropdown-only": true,
|
|
7634
|
-
},
|
|
7635
|
-
on: {
|
|
7636
|
-
select: _vm.onComponentSelect,
|
|
7637
|
-
close: _vm.closeSelectDropdown,
|
|
6536
|
+
_vm.selectDropdown
|
|
6537
|
+
? _c(
|
|
6538
|
+
"div",
|
|
6539
|
+
{
|
|
6540
|
+
staticClass: "eg-select-dropdown",
|
|
6541
|
+
style: _vm.dropdownStyle,
|
|
6542
|
+
on: {
|
|
6543
|
+
mousedown: function ($event) {
|
|
6544
|
+
$event.preventDefault();
|
|
6545
|
+
},
|
|
6546
|
+
},
|
|
7638
6547
|
},
|
|
7639
|
-
|
|
6548
|
+
[
|
|
6549
|
+
_vm.selectDropdown.enableSearch
|
|
6550
|
+
? _c(
|
|
6551
|
+
"div",
|
|
6552
|
+
{
|
|
6553
|
+
staticClass: "eg-select-search",
|
|
6554
|
+
on: {
|
|
6555
|
+
mousedown: function ($event) {
|
|
6556
|
+
$event.stopPropagation();
|
|
6557
|
+
},
|
|
6558
|
+
},
|
|
6559
|
+
},
|
|
6560
|
+
[
|
|
6561
|
+
_c("input", {
|
|
6562
|
+
ref: "dropdownSearch",
|
|
6563
|
+
staticClass: "eg-select-search-input",
|
|
6564
|
+
attrs: {
|
|
6565
|
+
placeholder: "Qidirish...",
|
|
6566
|
+
autocomplete: "off",
|
|
6567
|
+
autocorrect: "off",
|
|
6568
|
+
autocapitalize: "off",
|
|
6569
|
+
spellcheck: "false",
|
|
6570
|
+
},
|
|
6571
|
+
domProps: { value: _vm.selectDropdown.searchQuery },
|
|
6572
|
+
on: {
|
|
6573
|
+
input: _vm.onDropdownSearch,
|
|
6574
|
+
keydown: function ($event) {
|
|
6575
|
+
$event.stopPropagation();
|
|
6576
|
+
return _vm.onDropdownSearchKeyDown.apply(
|
|
6577
|
+
null,
|
|
6578
|
+
arguments
|
|
6579
|
+
)
|
|
6580
|
+
},
|
|
6581
|
+
},
|
|
6582
|
+
}),
|
|
6583
|
+
]
|
|
6584
|
+
)
|
|
6585
|
+
: _vm._e(),
|
|
6586
|
+
_vm._v(" "),
|
|
6587
|
+
_vm._l(_vm.filteredSelectOptions, function (opt, idx) {
|
|
6588
|
+
return _c(
|
|
6589
|
+
"div",
|
|
6590
|
+
{
|
|
6591
|
+
key: opt.id != null ? opt.id : opt.value,
|
|
6592
|
+
staticClass: "eg-select-option",
|
|
6593
|
+
class: {
|
|
6594
|
+
"eg-select-option--active":
|
|
6595
|
+
opt.value === _vm.selectDropdown.currentValue,
|
|
6596
|
+
"eg-select-option--highlighted":
|
|
6597
|
+
idx === _vm.selectDropdown.highlight,
|
|
6598
|
+
},
|
|
6599
|
+
style:
|
|
6600
|
+
_vm.selectDropdown.tree && !_vm.selectDropdown.searchQuery
|
|
6601
|
+
? { paddingLeft: 12 + opt.depth * 16 + "px" }
|
|
6602
|
+
: null,
|
|
6603
|
+
on: {
|
|
6604
|
+
mousedown: function ($event) {
|
|
6605
|
+
$event.preventDefault();
|
|
6606
|
+
return _vm.commitSelectValue(opt.value)
|
|
6607
|
+
},
|
|
6608
|
+
mouseover: function ($event) {
|
|
6609
|
+
_vm.selectDropdown.highlight = idx;
|
|
6610
|
+
},
|
|
6611
|
+
},
|
|
6612
|
+
},
|
|
6613
|
+
[
|
|
6614
|
+
_vm.selectDropdown.tree && !_vm.selectDropdown.searchQuery
|
|
6615
|
+
? _c(
|
|
6616
|
+
"span",
|
|
6617
|
+
{
|
|
6618
|
+
staticClass: "eg-tree-toggle",
|
|
6619
|
+
class: { "eg-tree-toggle--leaf": !opt.hasChildren },
|
|
6620
|
+
on: {
|
|
6621
|
+
mousedown: function ($event) {
|
|
6622
|
+
$event.preventDefault();
|
|
6623
|
+
$event.stopPropagation();
|
|
6624
|
+
return _vm.toggleTreeNode(idx)
|
|
6625
|
+
},
|
|
6626
|
+
},
|
|
6627
|
+
},
|
|
6628
|
+
[
|
|
6629
|
+
_vm._v(
|
|
6630
|
+
_vm._s(
|
|
6631
|
+
opt.hasChildren
|
|
6632
|
+
? opt.expanded
|
|
6633
|
+
? "▾"
|
|
6634
|
+
: "▸"
|
|
6635
|
+
: ""
|
|
6636
|
+
)
|
|
6637
|
+
),
|
|
6638
|
+
]
|
|
6639
|
+
)
|
|
6640
|
+
: _vm._e(),
|
|
6641
|
+
_vm._v(" "),
|
|
6642
|
+
opt.color
|
|
6643
|
+
? _c("span", {
|
|
6644
|
+
staticClass: "eg-select-dot",
|
|
6645
|
+
style: { background: opt.color },
|
|
6646
|
+
})
|
|
6647
|
+
: _vm._e(),
|
|
6648
|
+
_vm._v("\n " + _vm._s(opt.label) + "\n "),
|
|
6649
|
+
]
|
|
6650
|
+
)
|
|
6651
|
+
}),
|
|
6652
|
+
],
|
|
6653
|
+
2
|
|
6654
|
+
)
|
|
7640
6655
|
: _vm._e(),
|
|
7641
6656
|
_vm._v(" "),
|
|
7642
6657
|
_vm.invalidCell && _vm.invalidCell.rect
|
|
@@ -7649,8 +6664,7 @@ var __vue_render__ = function () {
|
|
|
7649
6664
|
[_vm._v(_vm._s(_vm.invalidCell.message))]
|
|
7650
6665
|
)
|
|
7651
6666
|
: _vm._e(),
|
|
7652
|
-
]
|
|
7653
|
-
1
|
|
6667
|
+
]
|
|
7654
6668
|
)
|
|
7655
6669
|
};
|
|
7656
6670
|
var __vue_staticRenderFns__ = [
|
|
@@ -7672,11 +6686,11 @@ __vue_render__._withStripped = true;
|
|
|
7672
6686
|
/* style */
|
|
7673
6687
|
const __vue_inject_styles__ = function (inject) {
|
|
7674
6688
|
if (!inject) return
|
|
7675
|
-
inject("data-v-5b531cb4_0", { source: "\n.eg-wrapper[data-v-5b531cb4] {\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-5b531cb4] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-5b531cb4] {\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-5b531cb4] {\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-5b531cb4]:hover { background: #e0e0e0;\n}\n.eg-header-group[data-v-5b531cb4] { background: #e8eaf6;\n}\n.eg-col-resize-handle[data-v-5b531cb4] {\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-5b531cb4] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-5b531cb4] {\n border-top: 1px solid #ccc;\n}\n.eg-frozen-col-footer[data-v-5b531cb4],\n.eg-scroll-col-footer[data-v-5b531cb4],\n.eg-pinned-right-footer[data-v-5b531cb4] {\n position: relative;\n}\n.eg-footer-cell[data-v-5b531cb4] {\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-5b531cb4] {\n position: relative;\n}\n.eg-cell[data-v-5b531cb4] {\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-active[data-v-5b531cb4] {\n outline: 2px solid #1a73e8 !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-5b531cb4] {\n background: #e3f2fd !important;\n}\n.eg-cell-fill-preview[data-v-5b531cb4] {\n background: #bbdefb !important;\n outline: 1px dashed #1565c0;\n outline-offset: -1px;\n}\n.eg-cell-copy[data-v-5b531cb4] {\n outline: 1px dashed #1a73e8;\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-5b531cb4] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-5b531cb4] {\n outline: 2px solid #e53935 !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-5b531cb4] {\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-5b531cb4] {\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-5b531cb4] {\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-5b531cb4] {\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-5b531cb4] {\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-5b531cb4] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n.eg-context-item[data-v-5b531cb4]:hover { background: #f0f0f0;\n}\n.eg-context-separator[data-v-5b531cb4] {\n height: 1px;\n background: #e0e0e0;\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-5b531cb4]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-5b531cb4] {\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-5b531cb4] { display: block;\n}\n.eg-group-btn[data-v-5b531cb4] {\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-5b531cb4] { background: #c5cae9;\n}\n.eg-drag-ghost[data-v-5b531cb4] {\n position: absolute;\n border: 2px dashed #1a73e8;\n pointer-events: none;\n z-index: 20;\n}\n.eg-resize-line[data-v-5b531cb4] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-5b531cb4]::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-5b531cb4] {\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-5b531cb4]::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-5b531cb4] {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid #bbb;\n background: #f0f0f0;\n}\n.eg-import-overlay[data-v-5b531cb4] {\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-5b531cb4] {\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-5b531cb4] {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 16px;\n}\n.eg-import-bar-wrap[data-v-5b531cb4] {\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 10px;\n}\n.eg-import-bar[data-v-5b531cb4] {\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-5b531cb4] {\n font-size: 12px;\n color: #666;\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-5b531cb4] {\n font-size: 13px;\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-export-spinner[data-v-5b531cb4] {\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-5b531cb4 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-5b531cb4 {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-5b531cb4] {\n background: #f8f8f8;\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-5b531cb4] {\n background: #dce8f5;\n}\n.eg-cell-readonly.eg-cell-active[data-v-5b531cb4] {\n outline-color: #aaa;\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-5b531cb4] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-5b531cb4]::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-5b531cb4] {\n justify-content: center;\n padding: 0 2px;\n}\n.eg-validation-tooltip[data-v-5b531cb4] {\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-5b531cb4],\n.eg-row-leave-active[data-v-5b531cb4] {\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.eg-row-enter[data-v-5b531cb4],\n.eg-row-leave-to[data-v-5b531cb4] {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell[data-v-5b531cb4] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-5b531cb4] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-5b531cb4] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-5b531cb4] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-5b531cb4] {\n font-size: 10px;\n}\n.eg-size-small .eg-select-option[data-v-5b531cb4] {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-small .eg-context-menu[data-v-5b531cb4],\n.eg-size-small .eg-context-item[data-v-5b531cb4] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-5b531cb4] {\n padding: 4px 14px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-5b531cb4] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-5b531cb4] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-5b531cb4] {\n font-size: 13px;\n}\n.eg-size-large .eg-footer-cell[data-v-5b531cb4] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-5b531cb4] {\n font-size: 12px;\n}\n.eg-size-large .eg-select-option[data-v-5b531cb4] {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-size-large .eg-context-menu[data-v-5b531cb4],\n.eg-size-large .eg-context-item[data-v-5b531cb4] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-5b531cb4] {\n padding: 8px 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AAwzGA;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,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 {{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}\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 {{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}\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 {{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}\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 }\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 _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-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"]}, media: undefined });
|
|
6689
|
+
inject("data-v-10cb7c27_0", { source: "\n.eg-wrapper[data-v-10cb7c27] {\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-10cb7c27] {\n display: flex;\n flex-direction: column;\n flex: 1;\n overflow: auto;\n position: relative;\n}\n.eg-header-area[data-v-10cb7c27] {\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-10cb7c27] {\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-10cb7c27]:hover { background: #e0e0e0;\n}\n.eg-header-group[data-v-10cb7c27] { background: #e8eaf6;\n}\n.eg-col-resize-handle[data-v-10cb7c27] {\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-10cb7c27] {\n display: flex;\n flex: 1;\n overflow: hidden;\n}\n\n/* ===== FOOTER AREA (column totals) ===== */\n.eg-footer-area[data-v-10cb7c27] {\n border-top: 1px solid #ccc;\n}\n.eg-frozen-col-footer[data-v-10cb7c27],\n.eg-scroll-col-footer[data-v-10cb7c27],\n.eg-pinned-right-footer[data-v-10cb7c27] {\n position: relative;\n}\n.eg-footer-cell[data-v-10cb7c27] {\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-10cb7c27] {\n position: relative;\n}\n.eg-cell[data-v-10cb7c27] {\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-active[data-v-10cb7c27] {\n outline: 2px solid #1a73e8 !important;\n outline-offset: -2px;\n z-index: 3;\n}\n.eg-cell-selected[data-v-10cb7c27] {\n background: #e3f2fd !important;\n}\n.eg-cell-fill-preview[data-v-10cb7c27] {\n background: #bbdefb !important;\n outline: 1px dashed #1565c0;\n outline-offset: -1px;\n}\n.eg-cell-copy[data-v-10cb7c27] {\n outline: 1px dashed #1a73e8;\n outline-offset: -1px;\n}\n.eg-cell-merged-hidden[data-v-10cb7c27] {\n display: none !important;\n}\n.eg-cell-invalid[data-v-10cb7c27] {\n outline: 2px solid #e53935 !important;\n outline-offset: -2px;\n z-index: 4;\n}\n.eg-cell-input[data-v-10cb7c27] {\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-10cb7c27] {\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-10cb7c27] {\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-10cb7c27] {\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-10cb7c27] {\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-10cb7c27] {\n padding: 6px 16px;\n cursor: pointer;\n position: relative;\n}\n.eg-context-item[data-v-10cb7c27]:hover { background: #f0f0f0;\n}\n.eg-context-separator[data-v-10cb7c27] {\n height: 1px;\n background: #e0e0e0;\n margin: 4px 0;\n}\n.eg-context-submenu-parent[data-v-10cb7c27]::after { content: ' ▶'; float: right;\n}\n.eg-context-submenu[data-v-10cb7c27] {\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-10cb7c27] { display: block;\n}\n.eg-group-btn[data-v-10cb7c27] {\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-10cb7c27] { background: #c5cae9;\n}\n.eg-drag-ghost[data-v-10cb7c27] {\n position: absolute;\n border: 2px dashed #1a73e8;\n pointer-events: none;\n z-index: 20;\n}\n.eg-resize-line[data-v-10cb7c27] {\n pointer-events: none;\n z-index: 9999;\n}\n\n/* Pin left separator shadow */\n.eg-frozen-col-body[data-v-10cb7c27]::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-10cb7c27] {\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-10cb7c27]::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-10cb7c27] {\n overflow: hidden;\n flex-shrink: 0;\n border-left: 2px solid #bbb;\n background: #f0f0f0;\n}\n.eg-import-overlay[data-v-10cb7c27] {\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-10cb7c27] {\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-10cb7c27] {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n margin-bottom: 16px;\n}\n.eg-import-bar-wrap[data-v-10cb7c27] {\n height: 8px;\n background: #e0e0e0;\n border-radius: 4px;\n overflow: hidden;\n margin-bottom: 10px;\n}\n.eg-import-bar[data-v-10cb7c27] {\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-10cb7c27] {\n font-size: 12px;\n color: #666;\n margin-bottom: 4px;\n}\n.eg-import-pct[data-v-10cb7c27] {\n font-size: 13px;\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-export-spinner[data-v-10cb7c27] {\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-10cb7c27 0.8s linear infinite;\n}\n@keyframes eg-spin-data-v-10cb7c27 {\nto { transform: rotate(360deg);\n}\n}\n\n/* ===== READONLY CELL ===== */\n.eg-cell-readonly[data-v-10cb7c27] {\n background: #f8f8f8;\n cursor: default;\n}\n.eg-cell-readonly.eg-cell-selected[data-v-10cb7c27] {\n background: #dce8f5;\n}\n.eg-cell-readonly.eg-cell-active[data-v-10cb7c27] {\n outline-color: #aaa;\n}\n\n/* ===== SELECT DROPDOWN ===== */\n.eg-cell-select[data-v-10cb7c27] {\n cursor: pointer;\n}\n.eg-cell-select[data-v-10cb7c27]::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-10cb7c27] {\n justify-content: center;\n padding: 0 2px;\n}\n.eg-checkbox[data-v-10cb7c27] {\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[data-v-10cb7c27] {\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[data-v-10cb7c27] {\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[data-v-10cb7c27] {\n border-color: #1a73e8;\n}\n.eg-checkbox-input:checked + .eg-checkbox-box[data-v-10cb7c27] {\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[data-v-10cb7c27] {\n box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);\n}\n.eg-checkbox--disabled[data-v-10cb7c27],\n.eg-checkbox--disabled .eg-checkbox-input[data-v-10cb7c27] {\n cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box[data-v-10cb7c27] {\n background-color: #f0f1f3;\n border-color: #dadde2;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box[data-v-10cb7c27] {\n background-color: #b9c2ce;\n border-color: #b9c2ce;\n}\n.eg-select-dropdown[data-v-10cb7c27] {\n background: white;\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-search[data-v-10cb7c27] {\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[data-v-10cb7c27] {\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[data-v-10cb7c27]:focus {\n border-color: #1a73e8;\n box-shadow: 0 0 0 2px rgba(26,115,232,0.15);\n}\n.eg-select-option[data-v-10cb7c27] {\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[data-v-10cb7c27]:hover,\n.eg-select-option--highlighted[data-v-10cb7c27] {\n background: #f0f4ff;\n}\n.eg-select-option--active[data-v-10cb7c27] {\n font-weight: 600;\n color: #1a73e8;\n}\n.eg-select-option--active.eg-select-option--highlighted[data-v-10cb7c27],\n.eg-select-option--active[data-v-10cb7c27]:hover {\n background: #e8f0fe;\n}\n.eg-validation-tooltip[data-v-10cb7c27] {\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.eg-select-dot[data-v-10cb7c27] {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n}\n.eg-tree-toggle[data-v-10cb7c27] {\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[data-v-10cb7c27] {\n visibility: hidden;\n}\n\n/* ===== ROW TRANSITION ===== */\n.eg-row-enter-active[data-v-10cb7c27],\n.eg-row-leave-active[data-v-10cb7c27] {\n transition: opacity 0.18s ease, transform 0.18s ease;\n}\n.eg-row-enter[data-v-10cb7c27],\n.eg-row-leave-to[data-v-10cb7c27] {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n/* ===== SIZE VARIANTS ===== */\n\n/* small */\n.eg-size-small .eg-cell[data-v-10cb7c27] {\n font-size: 12px;\n padding: 0 3px;\n}\n.eg-size-small .eg-cell-input[data-v-10cb7c27] {\n font-size: 12px;\n}\n.eg-size-small .eg-header-cell[data-v-10cb7c27] {\n font-size: 11px;\n}\n.eg-size-small .eg-footer-cell[data-v-10cb7c27] {\n font-size: 12px;\n}\n.eg-size-small .eg-row-header-cell[data-v-10cb7c27] {\n font-size: 10px;\n}\n.eg-size-small .eg-select-option[data-v-10cb7c27] {\n font-size: 12px;\n padding: 4px 10px;\n}\n.eg-size-small .eg-context-menu[data-v-10cb7c27],\n.eg-size-small .eg-context-item[data-v-10cb7c27] {\n font-size: 12px;\n}\n.eg-size-small .eg-context-item[data-v-10cb7c27] {\n padding: 4px 14px;\n}\n\n/* large */\n.eg-size-large .eg-cell[data-v-10cb7c27] {\n font-size: 14px;\n padding: 0 6px;\n}\n.eg-size-large .eg-cell-input[data-v-10cb7c27] {\n font-size: 14px;\n}\n.eg-size-large .eg-header-cell[data-v-10cb7c27] {\n font-size: 13px;\n}\n.eg-size-large .eg-footer-cell[data-v-10cb7c27] {\n font-size: 14px;\n}\n.eg-size-large .eg-row-header-cell[data-v-10cb7c27] {\n font-size: 12px;\n}\n.eg-size-large .eg-select-option[data-v-10cb7c27] {\n font-size: 14px;\n padding: 8px 14px;\n}\n.eg-size-large .eg-context-menu[data-v-10cb7c27],\n.eg-size-large .eg-context-item[data-v-10cb7c27] {\n font-size: 14px;\n}\n.eg-size-large .eg-context-item[data-v-10cb7c27] {\n padding: 8px 18px;\n}\n", map: {"version":3,"sources":["/Users/macmini/Projects/Realsoft/vue2-excel-grid/src/components/ExcelGrid.vue"],"names":[],"mappings":";AA0gHA;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,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,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;EACA,qBAAA;AACA;AACA;EACA,yBAAA;EACA,+SAAA;EACA,wBAAA;EACA,qBAAA;AACA;AACA;EACA,6CAAA;AACA;AACA;;EAEA,eAAA;AACA;AACA;EACA,yBAAA;EACA,qBAAA;AACA;AACA;EACA,yBAAA;EACA,qBAAA;AACA;AAEA;EACA,iBAAA;EACA,yBAAA;EACA,kBAAA;EACA,uCAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;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;AAEA;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;EACA,mBAAA;EACA,yBAAA;EACA,cAAA;EACA,eAAA;EACA,gBAAA;EACA,kBAAA;EACA,yCAAA;EACA,mBAAA;EACA,oBAAA;AACA;AAEA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,cAAA;EACA,qBAAA;AACA;AAEA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,eAAA;EACA,WAAA;EACA,eAAA;AACA;AACA;EACA,kBAAA;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=\"hasEditSlot(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 <label class=\"eg-checkbox\" :class=\"{ 'eg-checkbox--disabled': !isCellEditable(ri, ci) }\">\n <input\n type=\"checkbox\"\n class=\"eg-checkbox-input\"\n :checked=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"onCheckboxChange($event, ri, ci)\"\n />\n <span class=\"eg-checkbox-box\"></span>\n </label>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\">\n {{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}\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=\"hasEditSlot(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 <label class=\"eg-checkbox\" :class=\"{ 'eg-checkbox--disabled': !isCellEditable(ri, ci) }\">\n <input\n type=\"checkbox\"\n class=\"eg-checkbox-input\"\n :checked=\"isCheckboxChecked(ri, ci)\"\n :disabled=\"!isCellEditable(ri, ci)\"\n @change=\"onCheckboxChange($event, ri, ci)\"\n />\n <span class=\"eg-checkbox-box\"></span>\n </label>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, ci)\">\n {{ isSelectLikeCol(col) ? getSelectLabel(ci, getCellValue(ri, ci)) : getCellValue(ri, ci) }}\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=\"hasEditSlot(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 <label class=\"eg-checkbox\" :class=\"{ 'eg-checkbox--disabled': !isCellEditable(ri, col._flatIndex) }\">\n <input\n type=\"checkbox\"\n class=\"eg-checkbox-input\"\n :checked=\"isCheckboxChecked(ri, col._flatIndex)\"\n :disabled=\"!isCellEditable(ri, col._flatIndex)\"\n @change=\"onCheckboxChange($event, ri, col._flatIndex)\"\n />\n <span class=\"eg-checkbox-box\"></span>\n </label>\n </template>\n <template v-else>\n <slot :name=\"'cell-' + col.key\" :row=\"_dataRaw[ri]\" :rowIndex=\"ri\" :column=\"col\" :value=\"getCellValue(ri, col._flatIndex)\">\n {{ isSelectLikeCol(col) ? getSelectLabel(col._flatIndex, getCellValue(ri, col._flatIndex)) : getCellValue(ri, col._flatIndex) }}\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 -->\n <div\n v-if=\"selectDropdown\"\n class=\"eg-select-dropdown\"\n :style=\"dropdownStyle\"\n @mousedown.prevent\n >\n <div v-if=\"selectDropdown.enableSearch\" class=\"eg-select-search\" @mousedown.stop>\n <input\n ref=\"dropdownSearch\"\n class=\"eg-select-search-input\"\n :value=\"selectDropdown.searchQuery\"\n placeholder=\"Qidirish...\"\n autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n @input=\"onDropdownSearch\"\n @keydown.stop=\"onDropdownSearchKeyDown\"\n />\n </div>\n <div\n v-for=\"(opt, idx) in filteredSelectOptions\"\n :key=\"opt.id != null ? opt.id : opt.value\"\n class=\"eg-select-option\"\n :class=\"{\n 'eg-select-option--active': opt.value === selectDropdown.currentValue,\n 'eg-select-option--highlighted': idx === selectDropdown.highlight\n }\"\n :style=\"selectDropdown.tree && !selectDropdown.searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null\"\n @mousedown.prevent=\"commitSelectValue(opt.value)\"\n @mouseover=\"selectDropdown.highlight = idx\"\n >\n <span\n v-if=\"selectDropdown.tree && !selectDropdown.searchQuery\"\n class=\"eg-tree-toggle\"\n :class=\"{ 'eg-tree-toggle--leaf': !opt.hasChildren }\"\n @mousedown.prevent.stop=\"toggleTreeNode(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\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'\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 // 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 filteredSelectOptions() {\n if (!this.selectDropdown) return []\n const { options, searchQuery, enableSearch, tree, allFlatOptions } = this.selectDropdown\n if (!enableSearch || !searchQuery || !searchQuery.trim()) return options\n const q = searchQuery.trim().toLowerCase()\n const pool = tree && allFlatOptions ? allFlatOptions : options\n return pool.filter(o => String(o.label || '').toLowerCase().includes(q))\n },\n\n dropdownStyle() {\n if (!this.selectDropdown) return {}\n const { rect } = this.selectDropdown\n const optionH = this._rowH + 4\n const searchH = this.selectDropdown.enableSearch ? 42 : 0\n const approxHeight = Math.min(this.filteredSelectOptions.length * optionH + 8 + searchH, 320)\n const spaceBelow = window.innerHeight - rect.bottom\n const top = spaceBelow >= approxHeight ? rect.bottom : rect.top - approxHeight\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 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 }\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.hasEditSlot(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.hasEditSlot(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.hasEditSlot(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 // Handle open select dropdown keyboard navigation\n if (this.selectDropdown) {\n const key = e.key\n if (key === 'Escape') {\n e.preventDefault()\n this.closeSelectDropdown()\n return\n }\n if (key === 'Enter' || key === ' ') {\n e.preventDefault()\n const h = this.selectDropdown.highlight\n if (h >= 0 && this.selectDropdown.options[h]) {\n this.commitSelectValue(this.selectDropdown.options[h].value)\n } else {\n this.closeSelectDropdown()\n }\n return\n }\n if (key === 'ArrowDown') {\n e.preventDefault()\n this.selectDropdown.highlight = Math.min(this.selectDropdown.highlight + 1, this.selectDropdown.options.length - 1)\n return\n }\n if (key === 'ArrowUp') {\n e.preventDefault()\n this.selectDropdown.highlight = Math.max(this.selectDropdown.highlight - 1, 0)\n return\n }\n if (this.selectDropdown.tree && !this.selectDropdown.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {\n e.preventDefault()\n const opt = this.selectDropdown.options[this.selectDropdown.highlight]\n if (opt && opt.hasChildren) {\n if (key === 'ArrowRight' && !opt.expanded) this.toggleTreeNode(this.selectDropdown.highlight)\n if (key === 'ArrowLeft' && opt.expanded) this.toggleTreeNode(this.selectDropdown.highlight)\n }\n return\n }\n return // block other keys while dropdown open\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.hasEditSlot(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.hasEditSlot(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.hasEditSlot(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 hasEditSlot(col) {\n return !!(col && col.key && this.$scopedSlots && this.$scopedSlots['edit-' + col.key])\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 onCheckboxChange(e, ri, ci) {\n this.setCheckboxValue(ri, ci, e.target.checked)\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 // Flattens a tree of option nodes into a visible list, respecting which\n // node ids are currently expanded. Each returned entry carries enough\n // info (depth/hasChildren/expanded/id) to render indentation + a toggle.\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({\n value: node[keys.valueKey],\n label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]),\n color: node.color,\n id, depth, hasChildren, expanded\n })\n if (expanded) {\n out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))\n }\n })\n return out\n },\n\n _treeFlattenAll(nodes, keys, depth, result) {\n ;(nodes || []).forEach(node => {\n const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []\n result.push({\n value: node[keys.valueKey],\n label: node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey]),\n color: node.color,\n id: String(node[keys.valueKey]),\n depth: 0, hasChildren: false, expanded: false\n })\n if (children.length) this._treeFlattenAll(children, keys, depth + 1, result)\n })\n return result\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 // Returns the chain of ancestor node ids leading to `value` (inclusive),\n // so callers can pre-expand exactly that branch when opening the dropdown.\n _treeFindPath(nodes, keys, value, pathArr) {\n for (let idx = 0; idx < (nodes || []).length; idx++) {\n const node = nodes[idx]\n const path = pathArr.concat(idx)\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 colOptions(ci) {\n const col = this.leafCols[ci]\n if (!col || !Array.isArray(col.options)) return []\n if (col.type === 'treeSelect') {\n return this._treeFlatten(col.options, this.treeOptionKeys(col), {}, 0, [])\n }\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 const enableSearch = !!col.enableSearch\n if (col.type === 'treeSelect') {\n const keys = this.treeOptionKeys(col)\n const path = (Array.isArray(col.options) && this._treeFindPath(col.options, keys, currentValue, [])) || []\n const expandedMap = {}\n path.forEach(id => { expandedMap[id] = true })\n const flat = this._treeFlatten(col.options, keys, expandedMap, 0, [])\n return {\n row: ri, col: ci, tree: true, keys, expandedMap,\n options: flat,\n allFlatOptions: enableSearch ? this._treeFlattenAll(col.options || [], keys, 0, []) : null,\n currentValue,\n highlight: flat.findIndex(o => String(o.value) === currentValue),\n rect: rectObj,\n enableSearch,\n searchQuery: ''\n }\n }\n const opts = this.colOptions(ci)\n return {\n row: ri, col: ci, tree: false,\n options: opts,\n allFlatOptions: null,\n currentValue,\n highlight: opts.findIndex(o => String(o.value) === currentValue),\n rect: rectObj,\n enableSearch,\n searchQuery: ''\n }\n },\n\n toggleTreeNode(idx) {\n if (!this.selectDropdown || !this.selectDropdown.tree) return\n const opt = this.selectDropdown.options[idx]\n if (!opt || !opt.hasChildren) return\n const { row, col, expandedMap, keys } = this.selectDropdown\n const colDef = this.leafCols[col]\n if (!colDef) return\n const nextExpandedMap = { ...expandedMap }\n if (nextExpandedMap[opt.id]) delete nextExpandedMap[opt.id]\n else nextExpandedMap[opt.id] = true\n const flat = this._treeFlatten(colDef.options, keys, nextExpandedMap, 0, [])\n const newHighlight = flat.findIndex(o => o.id === opt.id)\n this.selectDropdown = {\n ...this.selectDropdown,\n row, col,\n expandedMap: nextExpandedMap,\n options: flat,\n highlight: newHighlight >= 0 ? newHighlight : this.selectDropdown.highlight\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 if (col.enableSearch) {\n this.$nextTick(() => { this.$refs.dropdownSearch && this.$refs.dropdownSearch.focus() })\n }\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 if (colDef.enableSearch) {\n this.$nextTick(() => { this.$refs.dropdownSearch && this.$refs.dropdownSearch.focus() })\n }\n })\n },\n\n commitSelectValue(value) {\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 const _opt = this.filteredSelectOptions.find(o => o.value === value)\n colDef.onSelect({ value, label: _opt ? _opt.label : String(value) }, newData[row], row, colDef)\n }\n }\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n },\n\n onDropdownSearch(e) {\n if (!this.selectDropdown) return\n this.selectDropdown.searchQuery = e.target.value\n this.selectDropdown.highlight = 0\n },\n\n onDropdownSearchKeyDown(e) {\n if (!this.selectDropdown) return\n const { key } = e\n if (key === 'Escape') {\n this.closeSelectDropdown()\n this.$refs.wrapper && this.$refs.wrapper.focus()\n } else if (key === 'ArrowDown') {\n e.preventDefault()\n this.selectDropdown.highlight = Math.min(\n (this.selectDropdown.highlight || 0) + 1,\n this.filteredSelectOptions.length - 1\n )\n } else if (key === 'ArrowUp') {\n e.preventDefault()\n this.selectDropdown.highlight = Math.max((this.selectDropdown.highlight || 0) - 1, 0)\n } else if (key === 'Enter') {\n e.preventDefault()\n const h = this.selectDropdown.highlight\n const opts = this.filteredSelectOptions\n if (h >= 0 && opts[h]) this.commitSelectValue(opts[h].value)\n }\n },\n\n closeSelectDropdown() {\n this.selectDropdown = null\n },\n\n _onWindowScrollForDropdown(e) {\n if (!this.selectDropdown) return\n const dd = this.$el ? this.$el.querySelector('.eg-select-dropdown') : null\n if (dd && dd.contains(e.target)) return\n this.closeSelectDropdown()\n },\n\n _onDocMouseDownForDropdown(e) {\n if (!this.selectDropdown) return\n const dd = this.$el ? this.$el.querySelector('.eg-select-dropdown') : null\n if (dd && dd.contains(e.target)) return\n // Click outside dropdown — close it\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-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-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 {\n 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 {\n box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2);\n}\n.eg-checkbox--disabled,\n.eg-checkbox--disabled .eg-checkbox-input {\n cursor: default;\n}\n.eg-checkbox-input:disabled + .eg-checkbox-box {\n background-color: #f0f1f3;\n border-color: #dadde2;\n}\n.eg-checkbox-input:disabled:checked + .eg-checkbox-box {\n background-color: #b9c2ce;\n border-color: #b9c2ce;\n}\n\n.eg-select-dropdown {\n background: white;\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-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.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.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.eg-select-dot {\n width: 9px;\n height: 9px;\n border-radius: 50%;\n flex-shrink: 0;\n display: inline-block;\n}\n\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\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"]}, media: undefined });
|
|
7676
6690
|
|
|
7677
6691
|
};
|
|
7678
6692
|
/* scoped */
|
|
7679
|
-
const __vue_scope_id__ = "data-v-
|
|
6693
|
+
const __vue_scope_id__ = "data-v-10cb7c27";
|
|
7680
6694
|
/* module identifier */
|
|
7681
6695
|
const __vue_module_identifier__ = undefined;
|
|
7682
6696
|
/* functional template */
|
|
@@ -7703,10 +6717,7 @@ __vue_render__._withStripped = true;
|
|
|
7703
6717
|
var index = {
|
|
7704
6718
|
install(Vue) {
|
|
7705
6719
|
Vue.component('ExcelGrid', __vue_component__);
|
|
7706
|
-
Vue.component('EgCheckbox', __vue_component__$3);
|
|
7707
|
-
Vue.component('EgSelect', __vue_component__$2);
|
|
7708
|
-
Vue.component('EgTreeSelect', __vue_component__$1);
|
|
7709
6720
|
}
|
|
7710
6721
|
};
|
|
7711
6722
|
|
|
7712
|
-
export { __vue_component__
|
|
6723
|
+
export { __vue_component__ as ExcelGrid, index as default };
|