@stonecrop/atable 0.4.32 → 0.4.33

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": "@stonecrop/atable",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": {
@@ -42,8 +42,8 @@
42
42
  "@vueuse/core": "^13.6.0",
43
43
  "pinia": "^3.0.3",
44
44
  "vue": "^3.5.18",
45
- "@stonecrop/themes": "0.4.32",
46
- "@stonecrop/utilities": "0.4.32"
45
+ "@stonecrop/themes": "0.4.33",
46
+ "@stonecrop/utilities": "0.4.33"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@microsoft/api-documenter": "^7.26.31",
@@ -11,7 +11,7 @@
11
11
  @focus="onFocus"
12
12
  @paste="updateCellData"
13
13
  @input="debouncedUpdateCellData"
14
- @click="showModal"
14
+ @click="onCellClick"
15
15
  class="atable-cell"
16
16
  :class="cellClasses">
17
17
  <component
@@ -27,7 +27,7 @@
27
27
  <script setup lang="ts">
28
28
  import { KeypressHandlers, defaultKeypressHandlers, useKeyboardNav } from '@stonecrop/utilities'
29
29
  import { useDebounceFn, useElementBounding } from '@vueuse/core'
30
- import { computed, type CSSProperties, ref, useTemplateRef } from 'vue'
30
+ import { computed, type CSSProperties, ref, useTemplateRef, nextTick } from 'vue'
31
31
 
32
32
  import { createTableStore } from '../stores/table'
33
33
  import { isHtmlString } from '../utils'
@@ -85,6 +85,14 @@ const cellClasses = computed(() => {
85
85
  }
86
86
  })
87
87
 
88
+ const onCellClick = () => {
89
+ // First, select all text if the cell is editable
90
+ selectAllText()
91
+
92
+ // Then handle modal display (original showModal behavior)
93
+ showModal()
94
+ }
95
+
88
96
  const showModal = () => {
89
97
  const { left, bottom, width, height } = useElementBounding(cellRef)
90
98
 
@@ -153,9 +161,91 @@ if (addNavigation) {
153
161
  // }
154
162
  // }
155
163
 
164
+ const selectAllText = () => {
165
+ if (cellRef.value && column.edit) {
166
+ // Use the Selection API to select all text content in the contenteditable cell
167
+ const selection = window.getSelection()
168
+ if (selection) {
169
+ try {
170
+ const range = document.createRange()
171
+ if (range.selectNodeContents) {
172
+ range.selectNodeContents(cellRef.value)
173
+ selection.removeAllRanges()
174
+ selection.addRange(range)
175
+ }
176
+ } catch (error) {
177
+ // Fallback for environments where Range API is not fully supported
178
+ // This is expected in some test environments
179
+ }
180
+ }
181
+ }
182
+ }
183
+
156
184
  const onFocus = () => {
157
185
  if (cellRef.value) {
158
186
  currentData.value = cellRef.value.textContent!
187
+ // Select all text when the cell receives focus
188
+ selectAllText()
189
+ }
190
+ }
191
+
192
+ const saveCursorPosition = () => {
193
+ try {
194
+ const selection = window.getSelection()
195
+ if (selection && selection.rangeCount > 0 && cellRef.value) {
196
+ const range = selection.getRangeAt(0)
197
+ // Save the offset from the start of the cell
198
+ const preCaretRange = range.cloneRange()
199
+ if (preCaretRange.selectNodeContents && preCaretRange.setEnd) {
200
+ preCaretRange.selectNodeContents(cellRef.value)
201
+ preCaretRange.setEnd(range.endContainer, range.endOffset)
202
+ return preCaretRange.toString().length
203
+ }
204
+ }
205
+ } catch (error) {
206
+ // Fallback for environments where Selection API is not fully supported
207
+ }
208
+ return 0
209
+ }
210
+
211
+ const restoreCursorPosition = (position: number) => {
212
+ if (!cellRef.value) return
213
+
214
+ try {
215
+ const selection = window.getSelection()
216
+ if (!selection) return
217
+
218
+ let charIndex = 0
219
+ const walker = document.createTreeWalker
220
+ ? document.createTreeWalker(cellRef.value, NodeFilter.SHOW_TEXT, null)
221
+ : null
222
+
223
+ if (!walker) return
224
+
225
+ let node: Node | null
226
+ let range: Range | null = null
227
+
228
+ while ((node = walker.nextNode())) {
229
+ const textNode = node as Text
230
+ const nextCharIndex = charIndex + textNode.textContent!.length
231
+
232
+ if (position <= nextCharIndex) {
233
+ range = document.createRange()
234
+ if (range.setStart && range.setEnd) {
235
+ range.setStart(textNode, position - charIndex)
236
+ range.setEnd(textNode, position - charIndex)
237
+ break
238
+ }
239
+ }
240
+ charIndex = nextCharIndex
241
+ }
242
+
243
+ if (range && selection.removeAllRanges && selection.addRange) {
244
+ selection.removeAllRanges()
245
+ selection.addRange(range)
246
+ }
247
+ } catch (error) {
248
+ // Fallback for environments where DOM APIs are not fully supported
159
249
  }
160
250
  }
161
251
 
@@ -165,6 +255,9 @@ const updateCellData = (payload: Event) => {
165
255
  return
166
256
  }
167
257
 
258
+ // Save cursor position before updating
259
+ const cursorPosition = saveCursorPosition()
260
+
168
261
  currentData.value = target.textContent!
169
262
 
170
263
  // only apply changes if the cell value has changed after being mounted
@@ -176,6 +269,11 @@ const updateCellData = (payload: Event) => {
176
269
  cellModified.value = target.textContent !== originalData
177
270
  store.setCellData(colIndex, rowIndex, target.textContent)
178
271
  }
272
+
273
+ // Use nextTick to restore cursor position after Vue's reactive updates but before browser repaint
274
+ void nextTick().then(() => {
275
+ restoreCursorPosition(cursorPosition)
276
+ })
179
277
  }
180
278
 
181
279
  const debouncedUpdateCellData = useDebounceFn(updateCellData, debounce)