vue2-excel-grid 1.7.2 → 1.8.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": "1.7.2",
3
+ "version": "1.8.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",
@@ -61,11 +61,12 @@
61
61
  <button
62
62
  v-if="localSyncKey && toolbarHas('localSync')"
63
63
  type="button"
64
- class="eg-toolbar-btn"
64
+ class="eg-toolbar-btn eg-toolbar-btn-badged"
65
65
  :disabled="toolbarDisabled('localSync')"
66
- :title="t.localSync"
66
+ :title="localSyncAvailable ? t.localSyncAvailable : t.localSync"
67
67
  @click="openLocalSyncModal"
68
- ><span class="eg-toolbar-icon eg-icon-local-sync"></span></button>
68
+ ><span class="eg-toolbar-icon eg-icon-local-sync"></span
69
+ ><span v-if="localSyncAvailable" class="eg-toolbar-badge" aria-hidden="true"></span></button>
69
70
  </div>
70
71
 
71
72
  <slot name="toolbar-extra" :locked="internalLocked" :fullscreen="isFullscreen"></slot>
@@ -1508,6 +1509,10 @@ const ExcelGridComponent = {
1508
1509
  // localSyncKey feature — modal state + pending debounce timer (the
1509
1510
  // timer itself lives outside `data()`'s reactive tracking, see created()).
1510
1511
  localSyncModal: { visible: false, snapshot: null },
1512
+ // True when a snapshot already existed at mount and the user hasn't
1513
+ // dealt with it yet — drives the toolbar button's "unseen draft" dot.
1514
+ // See _checkLocalSnapshotOnMount() for the full lifecycle.
1515
+ localSyncAvailable: false,
1511
1516
 
1512
1517
  resizing: {
1513
1518
  active: false,
@@ -2414,14 +2419,6 @@ const ExcelGridComponent = {
2414
2419
  this._dragScrollRAF = null
2415
2420
  this._dragCleanupTimer = null
2416
2421
 
2417
- // localSyncKey autosave — deliberately a plain (non-immediate) $watch,
2418
- // NOT an entry in the `watch:` option above: the existing `data` watcher
2419
- // there runs with `immediate: true` (fires once on mount with the
2420
- // initial value), which would autosave a host's still-loading/empty
2421
- // initial `data` and silently overwrite a previously-saved IndexedDB
2422
- // snapshot before the user ever gets a chance to see/restore it via the
2423
- // modal. A plain $watch only fires on later, real changes.
2424
- this.$watch('data', () => this._scheduleLocalAutosave())
2425
2422
  },
2426
2423
 
2427
2424
  mounted() {
@@ -2454,6 +2451,7 @@ const ExcelGridComponent = {
2454
2451
  if (this.$el.parentElement) this._scrollBodyResizeObserver.observe(this.$el.parentElement)
2455
2452
  }
2456
2453
  this._lockFlashArmed = true
2454
+ this._checkLocalSnapshotOnMount()
2457
2455
  },
2458
2456
 
2459
2457
  updated() {
@@ -6013,6 +6011,7 @@ const ExcelGridComponent = {
6013
6011
  // All edit paths already use spread ({ ...row }) so frozen rows are safe.
6014
6012
  const data = result.data.map(row => Object.freeze(row))
6015
6013
  this.$emit('input', data)
6014
+ this._scheduleLocalAutosave()
6016
6015
  this.$emit('update:columns', columns)
6017
6016
 
6018
6017
  const hasMerges = merges && merges.length > 0
@@ -6128,6 +6127,7 @@ const ExcelGridComponent = {
6128
6127
  }
6129
6128
  }
6130
6129
  this.$emit('input', newData)
6130
+ this._scheduleLocalAutosave()
6131
6131
  },
6132
6132
 
6133
6133
  // Emit 'update:merges' and record a merges diff entry.
@@ -6161,6 +6161,7 @@ const ExcelGridComponent = {
6161
6161
  newData.length = entry.before.rowCount
6162
6162
  }
6163
6163
  this.$emit('input', newData)
6164
+ this._scheduleLocalAutosave()
6164
6165
  } else if (entry.type === 'merges') {
6165
6166
  this.internalMerges = entry.before.slice()
6166
6167
  this.$emit('update:merges', entry.before.slice())
@@ -6183,6 +6184,7 @@ const ExcelGridComponent = {
6183
6184
  newData[entry.after.indices[i]] = entry.after.rows[i]
6184
6185
  }
6185
6186
  this.$emit('input', newData)
6187
+ this._scheduleLocalAutosave()
6186
6188
  } else if (entry.type === 'merges') {
6187
6189
  this.internalMerges = entry.after.slice()
6188
6190
  this.$emit('update:merges', entry.after.slice())
@@ -6219,9 +6221,20 @@ const ExcelGridComponent = {
6219
6221
  // LOCAL SYNC — autosave `data` to IndexedDB, keyed by localSyncKey
6220
6222
  // =========================================================
6221
6223
 
6222
- // Internal: debounced (800ms) autosave, scheduled by the plain $watch
6223
- // set up in created() every time `data` actually changes. No-op when
6224
- // localSyncKey isn't set — zero IndexedDB access at all in that case.
6224
+ // Internal: debounced (800ms) autosave. No-op when localSyncKey isn't
6225
+ // set — zero IndexedDB access at all in that case.
6226
+ //
6227
+ // Deliberately called from the grid's OWN data-change points only
6228
+ // (_emitDataChange, undo, redo, import) — NOT from a $watch on `data`.
6229
+ // A watcher can't tell "the user edited a cell" from "the host replaced
6230
+ // `data` wholesale", and the second one is the common case right after
6231
+ // mount: a host that loads rows from an API assigns them ~100-2000ms in,
6232
+ // which a watcher would treat as a real change and autosave over the
6233
+ // user's previously saved draft — destroying exactly the work this
6234
+ // feature exists to protect, before they can open the modal to restore
6235
+ // it. Autosave must mean "the user worked", not "the data object
6236
+ // changed". restoreLocalSnapshot() likewise does NOT call this: the
6237
+ // snapshot it just applied is already what's stored.
6225
6238
  _scheduleLocalAutosave() {
6226
6239
  if (!this.localSyncKey) return
6227
6240
  clearTimeout(this._localSyncTimer)
@@ -6230,6 +6243,23 @@ const ExcelGridComponent = {
6230
6243
  }, 800)
6231
6244
  },
6232
6245
 
6246
+ // Internal, called once from mounted(): a snapshot that already exists at
6247
+ // mount is a draft from an EARLIER session, which the user has no way of
6248
+ // knowing about unless they happen to click the toolbar button. Flags it
6249
+ // so the button shows a dot, and emits local-sync-available so a host can
6250
+ // surface its own prompt ("restore unsaved changes?") instead.
6251
+ async _checkLocalSnapshotOnMount() {
6252
+ if (!this.localSyncKey) return
6253
+ const snapshot = await this.getLocalSnapshot()
6254
+ if (!snapshot) return
6255
+ this.localSyncAvailable = true
6256
+ this.$emit('local-sync-available', {
6257
+ key: this.localSyncKey,
6258
+ rows: snapshot.data.length,
6259
+ updatedAt: snapshot.updatedAt
6260
+ })
6261
+ },
6262
+
6233
6263
  // Public, ref-callable: saves the current `data` to IndexedDB right now,
6234
6264
  // bypassing the debounce (e.g. a host wants to force a checkpoint before
6235
6265
  // navigating away). No-op (resolves to null) when localSyncKey isn't set.
@@ -6238,6 +6268,9 @@ const ExcelGridComponent = {
6238
6268
  clearTimeout(this._localSyncTimer)
6239
6269
  try {
6240
6270
  const { updatedAt } = await saveSnapshot(this.localSyncKey, this.data)
6271
+ // What's stored is now exactly what's on screen — there's no longer an
6272
+ // older draft to go back to, so the "unseen draft" dot would mislead.
6273
+ this.localSyncAvailable = false
6241
6274
  this.$emit('local-sync-save', { key: this.localSyncKey, rows: this.data.length, updatedAt })
6242
6275
  return updatedAt
6243
6276
  } catch (error) {
@@ -6268,6 +6301,7 @@ const ExcelGridComponent = {
6268
6301
  if (!snapshot) return
6269
6302
  this.$emit('input', snapshot.data)
6270
6303
  this.$emit('update:data', snapshot.data)
6304
+ this.localSyncAvailable = false
6271
6305
  this.$emit('local-sync-restore', { key: this.localSyncKey, rows: snapshot.data.length })
6272
6306
  this.closeLocalSyncModal()
6273
6307
  } catch (error) {
@@ -6282,6 +6316,7 @@ const ExcelGridComponent = {
6282
6316
  try {
6283
6317
  await deleteSnapshot(this.localSyncKey)
6284
6318
  if (this.localSyncModal.snapshot) this.localSyncModal.snapshot = null
6319
+ this.localSyncAvailable = false
6285
6320
  this.$emit('local-sync-clear', { key: this.localSyncKey })
6286
6321
  } catch (error) {
6287
6322
  this.$emit('local-sync-error', { action: 'clear', error })
@@ -6318,6 +6353,7 @@ const ExcelGridComponent = {
6318
6353
  async openLocalSyncModal() {
6319
6354
  if (!this.localSyncKey) return
6320
6355
  this.localSyncModal.visible = true
6356
+ this.localSyncAvailable = false // the draft has now been seen
6321
6357
  try {
6322
6358
  this.localSyncModal.snapshot = await loadSnapshot(this.localSyncKey)
6323
6359
  } catch (error) {
@@ -6789,6 +6825,22 @@ export default ExcelGridComponent
6789
6825
  color: var(--eg-color-text-secondary, var(--eg-color-text-secondary-default));
6790
6826
  }
6791
6827
 
6828
+ /* Anchors the "unseen local draft" dot (localSyncKey feature) — only the
6829
+ local-sync button carries it, but kept as its own class so any future
6830
+ badged tool gets the same positioning. */
6831
+ .eg-toolbar-btn-badged {
6832
+ position: relative;
6833
+ }
6834
+ .eg-toolbar-badge {
6835
+ position: absolute;
6836
+ top: 2px;
6837
+ right: 2px;
6838
+ width: 7px;
6839
+ height: 7px;
6840
+ border-radius: 50%;
6841
+ background: var(--eg-color-accent, var(--eg-color-accent-default));
6842
+ pointer-events: none;
6843
+ }
6792
6844
  .eg-toolbar-btn:hover:not(:disabled) {
6793
6845
  background: var(--eg-color-option-hover-bg, var(--eg-color-option-hover-bg-default));
6794
6846
  border-color: var(--eg-color-border, var(--eg-color-border-default));
package/src/utils/i18n.js CHANGED
@@ -47,6 +47,7 @@ const messages = {
47
47
  theme: 'Theme',
48
48
  // local sync
49
49
  localSync: 'Local sync',
50
+ localSyncAvailable: 'A locally saved copy is available',
50
51
  localSyncTitle: 'Local saved copy',
51
52
  localSyncEmpty: 'No local copy saved yet',
52
53
  localSyncSavedAt: 'Last saved',
@@ -118,6 +119,7 @@ const messages = {
118
119
  theme: 'Тема',
119
120
  // local sync
120
121
  localSync: 'Локальная синхронизация',
122
+ localSyncAvailable: 'Есть локально сохранённая копия',
121
123
  localSyncTitle: 'Локально сохранённая копия',
122
124
  localSyncEmpty: 'Локальная копия пока не сохранена',
123
125
  localSyncSavedAt: 'Последнее сохранение',
@@ -189,6 +191,7 @@ const messages = {
189
191
  theme: 'Mavzu',
190
192
  // local sync
191
193
  localSync: 'Lokal sinxronlash',
194
+ localSyncAvailable: 'Lokal saqlangan nusxa mavjud',
192
195
  localSyncTitle: 'Lokal saqlangan nusxa',
193
196
  localSyncEmpty: 'Hali lokal saqlangan nusxa yo\'q',
194
197
  localSyncSavedAt: 'Oxirgi saqlangan',
@@ -260,6 +263,7 @@ const messages = {
260
263
  theme: 'Мавзу',
261
264
  // local sync
262
265
  localSync: 'Лакал синхронлаш',
266
+ localSyncAvailable: 'Локал сақланган нусха мавжуд',
263
267
  localSyncTitle: 'Лакал сақланган нусха',
264
268
  localSyncEmpty: 'Ҳали лакал сақланган нусха йўқ',
265
269
  localSyncSavedAt: 'Охирги сақланган',