vue2-excel-grid 2.2.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vue2-excel-grid.esm.js +162 -30
- package/dist/vue2-excel-grid.umd.js +2 -2
- package/package.json +1 -1
- package/src/components/ExcelGrid.vue +177 -25
package/package.json
CHANGED
|
@@ -792,6 +792,38 @@
|
|
|
792
792
|
|
|
793
793
|
<!-- Resize indicator line -->
|
|
794
794
|
<div v-if="resizing.active" class="eg-resize-line" :style="resizeLineStyle"></div>
|
|
795
|
+
|
|
796
|
+
<!-- Loading overlay (host-driven, e.g. during an async data fetch) -->
|
|
797
|
+
<div v-if="loading" class="eg-loading-overlay">
|
|
798
|
+
<template v-if="columns && columns.length">
|
|
799
|
+
<div class="eg-skeleton">
|
|
800
|
+
<div class="eg-skeleton-row eg-skeleton-row-header" :style="{ height: headerRowHeight + 'px' }">
|
|
801
|
+
<div
|
|
802
|
+
v-for="col in leafCols"
|
|
803
|
+
:key="'sk-h-' + col._flatIndex"
|
|
804
|
+
class="eg-skeleton-block"
|
|
805
|
+
:style="{ width: getColW(col._flatIndex) + 'px' }"
|
|
806
|
+
></div>
|
|
807
|
+
</div>
|
|
808
|
+
<div
|
|
809
|
+
v-for="r in 20"
|
|
810
|
+
:key="'sk-r-' + r"
|
|
811
|
+
class="eg-skeleton-row"
|
|
812
|
+
:style="{ height: _rowH + 'px' }"
|
|
813
|
+
>
|
|
814
|
+
<div
|
|
815
|
+
v-for="col in leafCols"
|
|
816
|
+
:key="'sk-c-' + col._flatIndex"
|
|
817
|
+
class="eg-skeleton-block"
|
|
818
|
+
:style="{ width: getColW(col._flatIndex) + 'px' }"
|
|
819
|
+
></div>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
</template>
|
|
823
|
+
<template v-else>
|
|
824
|
+
<div class="eg-loading-spinner"></div>
|
|
825
|
+
</template>
|
|
826
|
+
</div>
|
|
795
827
|
</div><!-- end container -->
|
|
796
828
|
|
|
797
829
|
<!-- Import progress overlay -->
|
|
@@ -1314,6 +1346,21 @@ const ExcelGridComponent = {
|
|
|
1314
1346
|
*/
|
|
1315
1347
|
locked: { type: Boolean, default: false },
|
|
1316
1348
|
|
|
1349
|
+
/**
|
|
1350
|
+
* Host-driven loading state (e.g. while an async data fetch is in
|
|
1351
|
+
* flight). When `true`, only the grid body (`.eg-container`) is
|
|
1352
|
+
* covered — the toolbar stays visible and interactive. If `columns`
|
|
1353
|
+
* is non-empty, a shimmering skeleton matching each column's width is
|
|
1354
|
+
* shown; otherwise a plain centered spinner is shown. Purely visual —
|
|
1355
|
+
* doesn't block edits/interaction the way `locked` does (combine both
|
|
1356
|
+
* if that's also needed).
|
|
1357
|
+
* @type {Boolean}
|
|
1358
|
+
* @default false
|
|
1359
|
+
* @example
|
|
1360
|
+
* <excel-grid :columns="columns" v-model="data" :loading="isFetching" />
|
|
1361
|
+
*/
|
|
1362
|
+
loading: { type: Boolean, default: false },
|
|
1363
|
+
|
|
1317
1364
|
/** Show "Column info" / "Row info" items in the context menu (dev/debug aid). */
|
|
1318
1365
|
devMode: { type: Boolean, default: false },
|
|
1319
1366
|
|
|
@@ -1460,10 +1507,34 @@ const ExcelGridComponent = {
|
|
|
1460
1507
|
localSyncKey: { type: String, default: null },
|
|
1461
1508
|
|
|
1462
1509
|
/**
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
* `
|
|
1510
|
+
* Optional tenant/organization identifier for multi-tenant hosts (SaaS
|
|
1511
|
+
* apps where the same browser/device is used across different
|
|
1512
|
+
* organizations). When set, it's combined with `localSyncKey`
|
|
1513
|
+
* internally (`${tenant}::${localSyncKey}`) so each tenant gets a fully
|
|
1514
|
+
* isolated IndexedDB version history — switching tenants on the same
|
|
1515
|
+
* device never shows another tenant's saved drafts. Omit it (default)
|
|
1516
|
+
* for single-tenant hosts; behavior is then identical to before this
|
|
1517
|
+
* prop existed.
|
|
1518
|
+
*
|
|
1519
|
+
* Has no effect without `localSyncKey` — it only changes how that key
|
|
1520
|
+
* is namespaced internally, it doesn't opt into the feature by itself.
|
|
1521
|
+
* Reactive: changing `tenant` (e.g. after an async "current
|
|
1522
|
+
* organization" load) reloads that tenant's own version list/buffer,
|
|
1523
|
+
* the same way a `localSyncKey` that starts empty and becomes truthy
|
|
1524
|
+
* later does. Avoid `::` inside `tenant`/`localSyncKey` values
|
|
1525
|
+
* themselves — it's the internal separator between the two.
|
|
1526
|
+
* @type {String}
|
|
1527
|
+
* @default null
|
|
1528
|
+
* @example
|
|
1529
|
+
* <excel-grid :columns="columns" v-model="data" local-sync-key="invoice-42" :tenant="currentOrgId" toolbar />
|
|
1530
|
+
*/
|
|
1531
|
+
tenant: { type: String, default: null },
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Maximum number of local versions kept per `localSyncKey` (per
|
|
1535
|
+
* `tenant`, when set). Once a new version pushes the count past this,
|
|
1536
|
+
* the oldest version(s) (by `createdAt`) are deleted automatically
|
|
1537
|
+
* (FIFO). Has no effect without `localSyncKey`.
|
|
1467
1538
|
* @type {Number}
|
|
1468
1539
|
* @default 10
|
|
1469
1540
|
* @example
|
|
@@ -1666,6 +1737,17 @@ const ExcelGridComponent = {
|
|
|
1666
1737
|
computed: {
|
|
1667
1738
|
t() { return getMessages(this.locale) },
|
|
1668
1739
|
|
|
1740
|
+
// localSyncKey feature — the actual IndexedDB storage key. idbStore.js
|
|
1741
|
+
// itself knows nothing about "tenants"; this is the one place that
|
|
1742
|
+
// namespaces localSyncKey by tenant when one is given, so every
|
|
1743
|
+
// storage-layer call site below just uses this instead of
|
|
1744
|
+
// this.localSyncKey directly. null whenever localSyncKey itself is
|
|
1745
|
+
// unset — tenant alone doesn't opt into the feature.
|
|
1746
|
+
effectiveSyncKey() {
|
|
1747
|
+
if (!this.localSyncKey) return null
|
|
1748
|
+
return this.tenant ? `${this.tenant}::${this.localSyncKey}` : this.localSyncKey
|
|
1749
|
+
},
|
|
1750
|
+
|
|
1669
1751
|
pinnedLeftSet() {
|
|
1670
1752
|
const s = new Set()
|
|
1671
1753
|
this.leafCols.forEach(col => { if (this.internalPinnedColumns[col.key] === 'left') s.add(col._flatIndex) })
|
|
@@ -2419,17 +2501,23 @@ const ExcelGridComponent = {
|
|
|
2419
2501
|
},
|
|
2420
2502
|
widthMode() { this.$nextTick(this.syncHeaderViewportWidth) },
|
|
2421
2503
|
|
|
2422
|
-
// localSyncKey feature — mounted() only calls
|
|
2423
|
-
// once, at mount time. Without
|
|
2424
|
-
//
|
|
2425
|
-
//
|
|
2426
|
-
//
|
|
2427
|
-
//
|
|
2428
|
-
//
|
|
2504
|
+
// localSyncKey/tenant feature — mounted() only calls
|
|
2505
|
+
// _loadLocalVersionsOnMount() once, at mount time. Without these
|
|
2506
|
+
// watchers, a key/tenant that starts empty (a host conditionally
|
|
2507
|
+
// passing `:local-sync-key="ready ? id : null"`, or a `tenant` only
|
|
2508
|
+
// known after an async "current organization" load) and later becomes
|
|
2509
|
+
// truthy would never load its version list, never promote a leftover
|
|
2510
|
+
// buffer, and never show the toolbar badge — the feature would look
|
|
2511
|
+
// "broken" for the rest of that page's lifetime even though everything
|
|
2512
|
+
// else works. Both share _reloadLocalSyncForKeyChange() since either one
|
|
2513
|
+
// changing means the effective storage key changed.
|
|
2429
2514
|
localSyncKey(newKey, oldKey) {
|
|
2430
2515
|
if (newKey === oldKey) return
|
|
2431
|
-
this.
|
|
2432
|
-
|
|
2516
|
+
this._reloadLocalSyncForKeyChange()
|
|
2517
|
+
},
|
|
2518
|
+
tenant(newTenant, oldTenant) {
|
|
2519
|
+
if (newTenant === oldTenant) return
|
|
2520
|
+
this._reloadLocalSyncForKeyChange()
|
|
2433
2521
|
}
|
|
2434
2522
|
},
|
|
2435
2523
|
|
|
@@ -6423,9 +6511,19 @@ const ExcelGridComponent = {
|
|
|
6423
6511
|
|
|
6424
6512
|
// =========================================================
|
|
6425
6513
|
// LOCAL SYNC — save `data` as a local IndexedDB *version*, keyed by
|
|
6426
|
-
// localSyncKey, only when the page is
|
|
6514
|
+
// localSyncKey (optionally namespaced by tenant), only when the page is
|
|
6515
|
+
// being left (not per-edit).
|
|
6427
6516
|
// =========================================================
|
|
6428
6517
|
|
|
6518
|
+
// Shared by the localSyncKey and tenant watchers above — either one
|
|
6519
|
+
// changing means effectiveSyncKey now points at a (possibly) different
|
|
6520
|
+
// version history, so the badge/version-list state must be reloaded
|
|
6521
|
+
// from scratch rather than left stale.
|
|
6522
|
+
_reloadLocalSyncForKeyChange() {
|
|
6523
|
+
this.localSyncAvailable = false
|
|
6524
|
+
if (this.effectiveSyncKey) this._loadLocalVersionsOnMount()
|
|
6525
|
+
},
|
|
6526
|
+
|
|
6429
6527
|
// Internal, called once from mounted(): populates the toolbar "unseen
|
|
6430
6528
|
// local versions" dot and emits local-sync-available if any versions
|
|
6431
6529
|
// already exist for this key (e.g. saved in an earlier visit).
|
|
@@ -6433,7 +6531,7 @@ const ExcelGridComponent = {
|
|
|
6433
6531
|
if (!this.localSyncKey) return
|
|
6434
6532
|
await this._promoteBufferIfAny()
|
|
6435
6533
|
try {
|
|
6436
|
-
const versions = await listVersions(this.
|
|
6534
|
+
const versions = await listVersions(this.effectiveSyncKey)
|
|
6437
6535
|
if (!versions.length) return
|
|
6438
6536
|
this.localSyncAvailable = true
|
|
6439
6537
|
this.$emit('local-sync-available', {
|
|
@@ -6464,9 +6562,9 @@ const ExcelGridComponent = {
|
|
|
6464
6562
|
// turns it into a real version next time the grid mounts.
|
|
6465
6563
|
_scheduleBufferWrite(data) {
|
|
6466
6564
|
if (!this.localSyncKey) return
|
|
6467
|
-
const key = this.
|
|
6468
|
-
// a host that swaps localSyncKey mid-edit (rare, but now
|
|
6469
|
-
// the
|
|
6565
|
+
const key = this.effectiveSyncKey // captured now, not re-read 800ms later —
|
|
6566
|
+
// a host that swaps localSyncKey/tenant mid-edit (rare, but now
|
|
6567
|
+
// reactive, see the watchers below) must not have this land under the
|
|
6470
6568
|
// NEW key with the OLD key's data.
|
|
6471
6569
|
clearTimeout(this._bufferWriteTimer)
|
|
6472
6570
|
this._bufferWriteTimer = setTimeout(() => { this._writeBufferNow(key, data) }, 800)
|
|
@@ -6489,13 +6587,13 @@ const ExcelGridComponent = {
|
|
|
6489
6587
|
// like any other non-leave-signal path.
|
|
6490
6588
|
async _promoteBufferIfAny() {
|
|
6491
6589
|
try {
|
|
6492
|
-
const buffered = await getBuffer(this.
|
|
6590
|
+
const buffered = await getBuffer(this.effectiveSyncKey)
|
|
6493
6591
|
if (!buffered) return
|
|
6494
6592
|
const hash = await hashRowsAsync(buffered.data)
|
|
6495
6593
|
const { version, overwritten } = await saveOrTouchVersion(
|
|
6496
|
-
this.
|
|
6594
|
+
this.effectiveSyncKey, buffered.data, hash, this.localSyncMaxVersions
|
|
6497
6595
|
)
|
|
6498
|
-
await clearBuffer(this.
|
|
6596
|
+
await clearBuffer(this.effectiveSyncKey)
|
|
6499
6597
|
this.$emit('local-sync-save', {
|
|
6500
6598
|
key: this.localSyncKey,
|
|
6501
6599
|
id: version.id,
|
|
@@ -6565,12 +6663,12 @@ const ExcelGridComponent = {
|
|
|
6565
6663
|
// the version-store write below is best-effort on top of it, reliable
|
|
6566
6664
|
// for the common case (tab switch/background, page stays alive).
|
|
6567
6665
|
clearTimeout(this._bufferWriteTimer)
|
|
6568
|
-
this._writeBufferNow(this.
|
|
6666
|
+
this._writeBufferNow(this.effectiveSyncKey, data)
|
|
6569
6667
|
try {
|
|
6570
6668
|
const hash = sync ? hashRows(data) : await hashRowsAsync(data)
|
|
6571
6669
|
if (this.data !== data) return // data changed again mid-hash — a later flush will catch it
|
|
6572
6670
|
const { version, overwritten } = await saveOrTouchVersion(
|
|
6573
|
-
this.
|
|
6671
|
+
this.effectiveSyncKey, data, hash, this.localSyncMaxVersions
|
|
6574
6672
|
)
|
|
6575
6673
|
this._lastFlushedDataRef = data
|
|
6576
6674
|
// The version store now has this content durably — the buffer's job
|
|
@@ -6578,7 +6676,7 @@ const ExcelGridComponent = {
|
|
|
6578
6676
|
// doesn't complete before teardown, _promoteBufferIfAny() finds the
|
|
6579
6677
|
// (now-redundant but harmless, dedup makes it a no-op touch) buffer
|
|
6580
6678
|
// on next mount anyway.
|
|
6581
|
-
clearBuffer(this.
|
|
6679
|
+
clearBuffer(this.effectiveSyncKey).catch(() => {})
|
|
6582
6680
|
this.$emit('local-sync-save', {
|
|
6583
6681
|
key: this.localSyncKey,
|
|
6584
6682
|
id: version.id,
|
|
@@ -6608,7 +6706,7 @@ const ExcelGridComponent = {
|
|
|
6608
6706
|
async listLocalVersions() {
|
|
6609
6707
|
if (!this.localSyncKey) return []
|
|
6610
6708
|
try {
|
|
6611
|
-
const versions = await listVersions(this.
|
|
6709
|
+
const versions = await listVersions(this.effectiveSyncKey)
|
|
6612
6710
|
return versions.map(v => ({ id: v.id, createdAt: v.createdAt, updatedAt: v.updatedAt, rowCount: v.data.length }))
|
|
6613
6711
|
} catch (error) {
|
|
6614
6712
|
this.$emit('local-sync-error', { action: 'load', error })
|
|
@@ -7956,6 +8054,60 @@ export default ExcelGridComponent
|
|
|
7956
8054
|
to { transform: rotate(360deg); }
|
|
7957
8055
|
}
|
|
7958
8056
|
|
|
8057
|
+
/* ===== LOADING OVERLAY (loading prop) ===== */
|
|
8058
|
+
.eg-loading-overlay {
|
|
8059
|
+
position: absolute;
|
|
8060
|
+
inset: 0;
|
|
8061
|
+
background: var(--eg-color-overlay-bg, var(--eg-color-overlay-bg-default));
|
|
8062
|
+
display: flex;
|
|
8063
|
+
align-items: center;
|
|
8064
|
+
justify-content: center;
|
|
8065
|
+
z-index: 90;
|
|
8066
|
+
overflow: hidden;
|
|
8067
|
+
}
|
|
8068
|
+
.eg-loading-spinner {
|
|
8069
|
+
width: 32px;
|
|
8070
|
+
height: 32px;
|
|
8071
|
+
border: 3px solid var(--eg-color-border-cell, var(--eg-color-border-cell-default));
|
|
8072
|
+
border-top-color: var(--eg-color-accent, var(--eg-color-accent-default));
|
|
8073
|
+
border-radius: 50%;
|
|
8074
|
+
animation: eg-spin 0.8s linear infinite;
|
|
8075
|
+
}
|
|
8076
|
+
.eg-skeleton {
|
|
8077
|
+
width: 100%;
|
|
8078
|
+
height: 100%;
|
|
8079
|
+
overflow: hidden;
|
|
8080
|
+
}
|
|
8081
|
+
.eg-skeleton-row {
|
|
8082
|
+
display: flex;
|
|
8083
|
+
align-items: center;
|
|
8084
|
+
padding: 0 4px;
|
|
8085
|
+
box-sizing: border-box;
|
|
8086
|
+
}
|
|
8087
|
+
.eg-skeleton-row-header {
|
|
8088
|
+
background: var(--eg-color-header-bg, var(--eg-color-header-bg-default));
|
|
8089
|
+
}
|
|
8090
|
+
.eg-skeleton-block {
|
|
8091
|
+
height: 60%;
|
|
8092
|
+
margin: 0 2px;
|
|
8093
|
+
border-radius: var(--eg-radius, var(--eg-radius-default));
|
|
8094
|
+
background: linear-gradient(
|
|
8095
|
+
90deg,
|
|
8096
|
+
var(--eg-color-border-cell, var(--eg-color-border-cell-default)) 25%,
|
|
8097
|
+
var(--eg-color-cell-readonly, var(--eg-color-cell-readonly-default)) 37%,
|
|
8098
|
+
var(--eg-color-border-cell, var(--eg-color-border-cell-default)) 63%
|
|
8099
|
+
);
|
|
8100
|
+
background-size: 400% 100%;
|
|
8101
|
+
animation: eg-skeleton-shimmer 1.4s ease infinite;
|
|
8102
|
+
}
|
|
8103
|
+
@keyframes eg-skeleton-shimmer {
|
|
8104
|
+
0% { background-position: 100% 50%; }
|
|
8105
|
+
100% { background-position: 0 50%; }
|
|
8106
|
+
}
|
|
8107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8108
|
+
.eg-skeleton-block, .eg-loading-spinner { animation: none; }
|
|
8109
|
+
}
|
|
8110
|
+
|
|
7959
8111
|
/* ===== READONLY CELL ===== */
|
|
7960
8112
|
/* !important throughout: same-specificity as the base .eg-cell / .eg-cell-active /
|
|
7961
8113
|
.eg-cell-selected rules, so which one wins would otherwise depend on source
|