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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2-excel-grid",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "description": "Excel-style grid component for Vue 2.6.14 with virtual scrolling",
5
5
  "license": "MIT",
6
6
  "author": "Fozilbek Raimberdiyev",
@@ -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
- * Maximum number of local versions kept per `localSyncKey`. Once a new
1464
- * version pushes the count past this, the oldest version(s) (by
1465
- * `createdAt`) are deleted automatically (FIFO). Has no effect without
1466
- * `localSyncKey`.
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 _loadLocalVersionsOnMount()
2423
- // once, at mount time. Without this watcher, a key that starts empty (a
2424
- // host conditionally passing `:local-sync-key="ready ? id : null"`, or
2425
- // one only known after an async load) and later becomes truthy would
2426
- // never load its version list, never promote a leftover buffer, and
2427
- // never show the toolbar badge — the feature would look "broken" for the
2428
- // rest of that page's lifetime even though everything else works.
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.localSyncAvailable = false
2432
- if (newKey) this._loadLocalVersionsOnMount()
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 being left (not per-edit).
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.localSyncKey)
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.localSyncKey // captured now, not re-read 800ms later —
6468
- // a host that swaps localSyncKey mid-edit (rare, but now reactive, see
6469
- // the localSyncKey watcher above) must not have this land under 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.localSyncKey)
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.localSyncKey, buffered.data, hash, this.localSyncMaxVersions
6594
+ this.effectiveSyncKey, buffered.data, hash, this.localSyncMaxVersions
6497
6595
  )
6498
- await clearBuffer(this.localSyncKey)
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.localSyncKey, data)
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.localSyncKey, data, hash, this.localSyncMaxVersions
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.localSyncKey).catch(() => {})
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.localSyncKey)
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