react-glide-table 2.3.0 → 2.3.1

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/core.cjs CHANGED
@@ -526,6 +526,90 @@ function withCellUpdate(context, commitValue) {
526
526
  };
527
527
  }
528
528
 
529
+ // src/components/ui/table/features/cell-selection/pasteData.ts
530
+ function countLeadingEmptyCells(cells) {
531
+ let depth = 0;
532
+ while (depth < cells.length && cells[depth] === "") {
533
+ depth += 1;
534
+ }
535
+ return depth;
536
+ }
537
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
538
+ if (leadingEmptyCounts.length === 0) return false;
539
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
540
+ if (firstDepth !== 0) return false;
541
+ return leadingEmptyCounts.some((depth) => depth > 0);
542
+ }
543
+ function parseClipboardTSV(text) {
544
+ return parseClipboardTSVWithDepths(text).values;
545
+ }
546
+ function parseClipboardTSVWithDepths(text) {
547
+ if (!text) return { values: [], depths: [] };
548
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
549
+ const withoutTrailing = normalized.replace(/\n+$/, "");
550
+ if (!withoutTrailing) return { values: [], depths: [] };
551
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
552
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
553
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
554
+ const values = [];
555
+ const depths = [];
556
+ for (let index = 0; index < rows.length; index += 1) {
557
+ const cells = rows[index] ?? [];
558
+ const depth = leadingEmptyCounts[index] ?? 0;
559
+ if (treatAsDepth) {
560
+ values.push(cells.slice(depth));
561
+ depths.push(depth);
562
+ } else {
563
+ values.push(cells);
564
+ depths.push(0);
565
+ }
566
+ }
567
+ return { values, depths };
568
+ }
569
+ function resolvePasteColumnIds(rows, startCol, width) {
570
+ if (width <= 0) return [];
571
+ const cells = rows[0]?.getVisibleCells() ?? [];
572
+ const columnIds = [];
573
+ for (let offset = 0; offset < width; offset += 1) {
574
+ const cell = cells[startCol + offset];
575
+ if (!cell) break;
576
+ columnIds.push(cell.column.id);
577
+ }
578
+ return columnIds;
579
+ }
580
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
581
+ const { values, depths } = parseClipboardTSVWithDepths(text);
582
+ if (values.length === 0) return null;
583
+ const width = Math.max(...values.map((row) => row.length), 0);
584
+ if (width === 0) return null;
585
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
586
+ if (columnIds.length === 0) return null;
587
+ const rowIds = [];
588
+ for (let offset = 0; offset < values.length; offset += 1) {
589
+ const row = rows[startRow + offset];
590
+ if (!row) break;
591
+ rowIds.push(row.id);
592
+ }
593
+ const anchorRow = rows[endRow] ?? rows[startRow];
594
+ return {
595
+ mode,
596
+ startRow,
597
+ startCol,
598
+ endRow,
599
+ rowIds,
600
+ anchorRowId: anchorRow?.id ?? "",
601
+ columnIds,
602
+ values,
603
+ depths
604
+ };
605
+ }
606
+ function isEditablePasteTarget(target) {
607
+ if (!(target instanceof HTMLElement)) return false;
608
+ const tag = target.tagName;
609
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
610
+ return Boolean(target.isContentEditable);
611
+ }
612
+
529
613
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
530
614
  var import_react3 = require("react");
531
615
 
@@ -1259,90 +1343,6 @@ function hasFillExtension(sourceBounds, fillBounds) {
1259
1343
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
1260
1344
  }
1261
1345
 
1262
- // src/components/ui/table/features/cell-selection/pasteData.ts
1263
- function countLeadingEmptyCells(cells) {
1264
- let depth = 0;
1265
- while (depth < cells.length && cells[depth] === "") {
1266
- depth += 1;
1267
- }
1268
- return depth;
1269
- }
1270
- function looksLikeSubtreeIndentation(leadingEmptyCounts) {
1271
- if (leadingEmptyCounts.length === 0) return false;
1272
- const firstDepth = leadingEmptyCounts[0] ?? 0;
1273
- if (firstDepth !== 0) return false;
1274
- return leadingEmptyCounts.some((depth) => depth > 0);
1275
- }
1276
- function parseClipboardTSV(text) {
1277
- return parseClipboardTSVWithDepths(text).values;
1278
- }
1279
- function parseClipboardTSVWithDepths(text) {
1280
- if (!text) return { values: [], depths: [] };
1281
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1282
- const withoutTrailing = normalized.replace(/\n+$/, "");
1283
- if (!withoutTrailing) return { values: [], depths: [] };
1284
- const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
1285
- const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
1286
- const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
1287
- const values = [];
1288
- const depths = [];
1289
- for (let index = 0; index < rows.length; index += 1) {
1290
- const cells = rows[index] ?? [];
1291
- const depth = leadingEmptyCounts[index] ?? 0;
1292
- if (treatAsDepth) {
1293
- values.push(cells.slice(depth));
1294
- depths.push(depth);
1295
- } else {
1296
- values.push(cells);
1297
- depths.push(0);
1298
- }
1299
- }
1300
- return { values, depths };
1301
- }
1302
- function resolvePasteColumnIds(rows, startCol, width) {
1303
- if (width <= 0) return [];
1304
- const cells = rows[0]?.getVisibleCells() ?? [];
1305
- const columnIds = [];
1306
- for (let offset = 0; offset < width; offset += 1) {
1307
- const cell = cells[startCol + offset];
1308
- if (!cell) break;
1309
- columnIds.push(cell.column.id);
1310
- }
1311
- return columnIds;
1312
- }
1313
- function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
1314
- const { values, depths } = parseClipboardTSVWithDepths(text);
1315
- if (values.length === 0) return null;
1316
- const width = Math.max(...values.map((row) => row.length), 0);
1317
- if (width === 0) return null;
1318
- const columnIds = resolvePasteColumnIds(rows, startCol, width);
1319
- if (columnIds.length === 0) return null;
1320
- const rowIds = [];
1321
- for (let offset = 0; offset < values.length; offset += 1) {
1322
- const row = rows[startRow + offset];
1323
- if (!row) break;
1324
- rowIds.push(row.id);
1325
- }
1326
- const anchorRow = rows[endRow] ?? rows[startRow];
1327
- return {
1328
- mode,
1329
- startRow,
1330
- startCol,
1331
- endRow,
1332
- rowIds,
1333
- anchorRowId: anchorRow?.id ?? "",
1334
- columnIds,
1335
- values,
1336
- depths
1337
- };
1338
- }
1339
- function isEditablePasteTarget(target) {
1340
- if (!(target instanceof HTMLElement)) return false;
1341
- const tag = target.tagName;
1342
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
1343
- return Boolean(target.isContentEditable);
1344
- }
1345
-
1346
1346
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1347
1347
  function useCellSelection({
1348
1348
  data,
@@ -1424,11 +1424,19 @@ function useCellSelection({
1424
1424
  },
1425
1425
  [enabled]
1426
1426
  );
1427
+ const clearSelection = (0, import_react3.useCallback)(() => {
1428
+ const prev = dragStateRef.current;
1429
+ if (prev.start === null && prev.end === null && !prev.isSelecting && !prev.isFillDragging) {
1430
+ return;
1431
+ }
1432
+ dragStateRef.current = INITIAL_DRAG_STATE;
1433
+ setDragState(INITIAL_DRAG_STATE);
1434
+ }, []);
1427
1435
  (0, import_react3.useEffect)(() => {
1428
1436
  if (!enabled) {
1429
- setDragState(INITIAL_DRAG_STATE);
1437
+ clearSelection();
1430
1438
  }
1431
- }, [enabled]);
1439
+ }, [clearSelection, enabled]);
1432
1440
  (0, import_react3.useEffect)(() => {
1433
1441
  if (!enabled) return;
1434
1442
  const handleKeyDown = (e) => {
@@ -1642,6 +1650,7 @@ function useCellSelection({
1642
1650
  handleCellMouseDown,
1643
1651
  handleCellMouseEnter,
1644
1652
  handleFillHandleMouseDown,
1653
+ clearSelection,
1645
1654
  copySelection
1646
1655
  };
1647
1656
  }
@@ -2919,6 +2928,7 @@ function useGlideTable(options) {
2919
2928
  handleCellMouseDown,
2920
2929
  handleCellMouseEnter,
2921
2930
  handleFillHandleMouseDown,
2931
+ clearSelection: clearCellSelection,
2922
2932
  copySelection
2923
2933
  } = useCellSelection({
2924
2934
  data: tableData,
@@ -2934,6 +2944,42 @@ function useGlideTable(options) {
2934
2944
  cellRendererRegistry,
2935
2945
  rootRef
2936
2946
  });
2947
+ const clearRowSelection = (0, import_react6.useCallback)(() => {
2948
+ if (rowSelectionMode === "none") return;
2949
+ const hasSelection = Object.values(rowSelection).some(Boolean);
2950
+ if (!hasSelection) return;
2951
+ if (onRowSelectionChange) {
2952
+ onRowSelectionChange(() => ({}));
2953
+ return;
2954
+ }
2955
+ setInternalRowSelection({});
2956
+ }, [onRowSelectionChange, rowSelection, rowSelectionMode]);
2957
+ (0, import_react6.useEffect)(() => {
2958
+ const clearAllSelections = () => {
2959
+ clearCellSelection();
2960
+ clearRowSelection();
2961
+ };
2962
+ const handleKeyDown = (event) => {
2963
+ if (event.key !== "Escape") return;
2964
+ if (event.defaultPrevented) return;
2965
+ if (isEditablePasteTarget(event.target) || isEditablePasteTarget(document.activeElement)) {
2966
+ return;
2967
+ }
2968
+ clearAllSelections();
2969
+ };
2970
+ const handleMouseDown = (event) => {
2971
+ const root = rootRef.current;
2972
+ if (!root) return;
2973
+ if (event.target instanceof Node && root.contains(event.target)) return;
2974
+ clearAllSelections();
2975
+ };
2976
+ window.addEventListener("keydown", handleKeyDown);
2977
+ document.addEventListener("mousedown", handleMouseDown);
2978
+ return () => {
2979
+ window.removeEventListener("keydown", handleKeyDown);
2980
+ document.removeEventListener("mousedown", handleMouseDown);
2981
+ };
2982
+ }, [clearCellSelection, clearRowSelection]);
2937
2983
  const {
2938
2984
  editingCell,
2939
2985
  draftValue,
package/dist/core.d.cts CHANGED
@@ -463,6 +463,7 @@ declare function useCellSelection<T extends Record<string, unknown>>({ data, row
463
463
  handleCellMouseDown: (rowIndex: number, colIndex: number, options?: CellMouseDownOptions) => void;
464
464
  handleCellMouseEnter: (rowIndex: number, colIndex: number) => void;
465
465
  handleFillHandleMouseDown: (rowIndex: number, colIndex: number) => void;
466
+ clearSelection: () => void;
466
467
  copySelection: (options?: CopySelectionOptions) => Promise<boolean>;
467
468
  };
468
469
 
package/dist/core.d.ts CHANGED
@@ -463,6 +463,7 @@ declare function useCellSelection<T extends Record<string, unknown>>({ data, row
463
463
  handleCellMouseDown: (rowIndex: number, colIndex: number, options?: CellMouseDownOptions) => void;
464
464
  handleCellMouseEnter: (rowIndex: number, colIndex: number) => void;
465
465
  handleFillHandleMouseDown: (rowIndex: number, colIndex: number) => void;
466
+ clearSelection: () => void;
466
467
  copySelection: (options?: CopySelectionOptions) => Promise<boolean>;
467
468
  };
468
469
 
package/dist/core.js CHANGED
@@ -432,6 +432,90 @@ function withCellUpdate(context, commitValue) {
432
432
  };
433
433
  }
434
434
 
435
+ // src/components/ui/table/features/cell-selection/pasteData.ts
436
+ function countLeadingEmptyCells(cells) {
437
+ let depth = 0;
438
+ while (depth < cells.length && cells[depth] === "") {
439
+ depth += 1;
440
+ }
441
+ return depth;
442
+ }
443
+ function looksLikeSubtreeIndentation(leadingEmptyCounts) {
444
+ if (leadingEmptyCounts.length === 0) return false;
445
+ const firstDepth = leadingEmptyCounts[0] ?? 0;
446
+ if (firstDepth !== 0) return false;
447
+ return leadingEmptyCounts.some((depth) => depth > 0);
448
+ }
449
+ function parseClipboardTSV(text) {
450
+ return parseClipboardTSVWithDepths(text).values;
451
+ }
452
+ function parseClipboardTSVWithDepths(text) {
453
+ if (!text) return { values: [], depths: [] };
454
+ const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
455
+ const withoutTrailing = normalized.replace(/\n+$/, "");
456
+ if (!withoutTrailing) return { values: [], depths: [] };
457
+ const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
458
+ const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
459
+ const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
460
+ const values = [];
461
+ const depths = [];
462
+ for (let index = 0; index < rows.length; index += 1) {
463
+ const cells = rows[index] ?? [];
464
+ const depth = leadingEmptyCounts[index] ?? 0;
465
+ if (treatAsDepth) {
466
+ values.push(cells.slice(depth));
467
+ depths.push(depth);
468
+ } else {
469
+ values.push(cells);
470
+ depths.push(0);
471
+ }
472
+ }
473
+ return { values, depths };
474
+ }
475
+ function resolvePasteColumnIds(rows, startCol, width) {
476
+ if (width <= 0) return [];
477
+ const cells = rows[0]?.getVisibleCells() ?? [];
478
+ const columnIds = [];
479
+ for (let offset = 0; offset < width; offset += 1) {
480
+ const cell = cells[startCol + offset];
481
+ if (!cell) break;
482
+ columnIds.push(cell.column.id);
483
+ }
484
+ return columnIds;
485
+ }
486
+ function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
487
+ const { values, depths } = parseClipboardTSVWithDepths(text);
488
+ if (values.length === 0) return null;
489
+ const width = Math.max(...values.map((row) => row.length), 0);
490
+ if (width === 0) return null;
491
+ const columnIds = resolvePasteColumnIds(rows, startCol, width);
492
+ if (columnIds.length === 0) return null;
493
+ const rowIds = [];
494
+ for (let offset = 0; offset < values.length; offset += 1) {
495
+ const row = rows[startRow + offset];
496
+ if (!row) break;
497
+ rowIds.push(row.id);
498
+ }
499
+ const anchorRow = rows[endRow] ?? rows[startRow];
500
+ return {
501
+ mode,
502
+ startRow,
503
+ startCol,
504
+ endRow,
505
+ rowIds,
506
+ anchorRowId: anchorRow?.id ?? "",
507
+ columnIds,
508
+ values,
509
+ depths
510
+ };
511
+ }
512
+ function isEditablePasteTarget(target) {
513
+ if (!(target instanceof HTMLElement)) return false;
514
+ const tag = target.tagName;
515
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
516
+ return Boolean(target.isContentEditable);
517
+ }
518
+
435
519
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
436
520
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
437
521
 
@@ -1165,90 +1249,6 @@ function hasFillExtension(sourceBounds, fillBounds) {
1165
1249
  return fillBounds.startRow < sourceBounds.startRow || fillBounds.endRow > sourceBounds.endRow || fillBounds.startCol < sourceBounds.startCol || fillBounds.endCol > sourceBounds.endCol;
1166
1250
  }
1167
1251
 
1168
- // src/components/ui/table/features/cell-selection/pasteData.ts
1169
- function countLeadingEmptyCells(cells) {
1170
- let depth = 0;
1171
- while (depth < cells.length && cells[depth] === "") {
1172
- depth += 1;
1173
- }
1174
- return depth;
1175
- }
1176
- function looksLikeSubtreeIndentation(leadingEmptyCounts) {
1177
- if (leadingEmptyCounts.length === 0) return false;
1178
- const firstDepth = leadingEmptyCounts[0] ?? 0;
1179
- if (firstDepth !== 0) return false;
1180
- return leadingEmptyCounts.some((depth) => depth > 0);
1181
- }
1182
- function parseClipboardTSV(text) {
1183
- return parseClipboardTSVWithDepths(text).values;
1184
- }
1185
- function parseClipboardTSVWithDepths(text) {
1186
- if (!text) return { values: [], depths: [] };
1187
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1188
- const withoutTrailing = normalized.replace(/\n+$/, "");
1189
- if (!withoutTrailing) return { values: [], depths: [] };
1190
- const rows = withoutTrailing.split("\n").map((line) => line.split(" "));
1191
- const leadingEmptyCounts = rows.map(countLeadingEmptyCells);
1192
- const treatAsDepth = looksLikeSubtreeIndentation(leadingEmptyCounts);
1193
- const values = [];
1194
- const depths = [];
1195
- for (let index = 0; index < rows.length; index += 1) {
1196
- const cells = rows[index] ?? [];
1197
- const depth = leadingEmptyCounts[index] ?? 0;
1198
- if (treatAsDepth) {
1199
- values.push(cells.slice(depth));
1200
- depths.push(depth);
1201
- } else {
1202
- values.push(cells);
1203
- depths.push(0);
1204
- }
1205
- }
1206
- return { values, depths };
1207
- }
1208
- function resolvePasteColumnIds(rows, startCol, width) {
1209
- if (width <= 0) return [];
1210
- const cells = rows[0]?.getVisibleCells() ?? [];
1211
- const columnIds = [];
1212
- for (let offset = 0; offset < width; offset += 1) {
1213
- const cell = cells[startCol + offset];
1214
- if (!cell) break;
1215
- columnIds.push(cell.column.id);
1216
- }
1217
- return columnIds;
1218
- }
1219
- function buildRowsPastePayload(rows, startRow, startCol, text, mode, endRow = startRow) {
1220
- const { values, depths } = parseClipboardTSVWithDepths(text);
1221
- if (values.length === 0) return null;
1222
- const width = Math.max(...values.map((row) => row.length), 0);
1223
- if (width === 0) return null;
1224
- const columnIds = resolvePasteColumnIds(rows, startCol, width);
1225
- if (columnIds.length === 0) return null;
1226
- const rowIds = [];
1227
- for (let offset = 0; offset < values.length; offset += 1) {
1228
- const row = rows[startRow + offset];
1229
- if (!row) break;
1230
- rowIds.push(row.id);
1231
- }
1232
- const anchorRow = rows[endRow] ?? rows[startRow];
1233
- return {
1234
- mode,
1235
- startRow,
1236
- startCol,
1237
- endRow,
1238
- rowIds,
1239
- anchorRowId: anchorRow?.id ?? "",
1240
- columnIds,
1241
- values,
1242
- depths
1243
- };
1244
- }
1245
- function isEditablePasteTarget(target) {
1246
- if (!(target instanceof HTMLElement)) return false;
1247
- const tag = target.tagName;
1248
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
1249
- return Boolean(target.isContentEditable);
1250
- }
1251
-
1252
1252
  // src/components/ui/table/features/cell-selection/useCellSelection.ts
1253
1253
  function useCellSelection({
1254
1254
  data,
@@ -1330,11 +1330,19 @@ function useCellSelection({
1330
1330
  },
1331
1331
  [enabled]
1332
1332
  );
1333
+ const clearSelection = useCallback2(() => {
1334
+ const prev = dragStateRef.current;
1335
+ if (prev.start === null && prev.end === null && !prev.isSelecting && !prev.isFillDragging) {
1336
+ return;
1337
+ }
1338
+ dragStateRef.current = INITIAL_DRAG_STATE;
1339
+ setDragState(INITIAL_DRAG_STATE);
1340
+ }, []);
1333
1341
  useEffect2(() => {
1334
1342
  if (!enabled) {
1335
- setDragState(INITIAL_DRAG_STATE);
1343
+ clearSelection();
1336
1344
  }
1337
- }, [enabled]);
1345
+ }, [clearSelection, enabled]);
1338
1346
  useEffect2(() => {
1339
1347
  if (!enabled) return;
1340
1348
  const handleKeyDown = (e) => {
@@ -1548,6 +1556,7 @@ function useCellSelection({
1548
1556
  handleCellMouseDown,
1549
1557
  handleCellMouseEnter,
1550
1558
  handleFillHandleMouseDown,
1559
+ clearSelection,
1551
1560
  copySelection
1552
1561
  };
1553
1562
  }
@@ -2832,6 +2841,7 @@ function useGlideTable(options) {
2832
2841
  handleCellMouseDown,
2833
2842
  handleCellMouseEnter,
2834
2843
  handleFillHandleMouseDown,
2844
+ clearSelection: clearCellSelection,
2835
2845
  copySelection
2836
2846
  } = useCellSelection({
2837
2847
  data: tableData,
@@ -2847,6 +2857,42 @@ function useGlideTable(options) {
2847
2857
  cellRendererRegistry,
2848
2858
  rootRef
2849
2859
  });
2860
+ const clearRowSelection = useCallback4(() => {
2861
+ if (rowSelectionMode === "none") return;
2862
+ const hasSelection = Object.values(rowSelection).some(Boolean);
2863
+ if (!hasSelection) return;
2864
+ if (onRowSelectionChange) {
2865
+ onRowSelectionChange(() => ({}));
2866
+ return;
2867
+ }
2868
+ setInternalRowSelection({});
2869
+ }, [onRowSelectionChange, rowSelection, rowSelectionMode]);
2870
+ useEffect5(() => {
2871
+ const clearAllSelections = () => {
2872
+ clearCellSelection();
2873
+ clearRowSelection();
2874
+ };
2875
+ const handleKeyDown = (event) => {
2876
+ if (event.key !== "Escape") return;
2877
+ if (event.defaultPrevented) return;
2878
+ if (isEditablePasteTarget(event.target) || isEditablePasteTarget(document.activeElement)) {
2879
+ return;
2880
+ }
2881
+ clearAllSelections();
2882
+ };
2883
+ const handleMouseDown = (event) => {
2884
+ const root = rootRef.current;
2885
+ if (!root) return;
2886
+ if (event.target instanceof Node && root.contains(event.target)) return;
2887
+ clearAllSelections();
2888
+ };
2889
+ window.addEventListener("keydown", handleKeyDown);
2890
+ document.addEventListener("mousedown", handleMouseDown);
2891
+ return () => {
2892
+ window.removeEventListener("keydown", handleKeyDown);
2893
+ document.removeEventListener("mousedown", handleMouseDown);
2894
+ };
2895
+ }, [clearCellSelection, clearRowSelection]);
2850
2896
  const {
2851
2897
  editingCell,
2852
2898
  draftValue,