react-arborist 3.10.1 → 3.10.3

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/README.md CHANGED
@@ -800,6 +800,22 @@ function Node({ node, style }) {
800
800
 
801
801
  Pass a partial `chars` object to override any of the default characters (e.g. for an ASCII-only style).
802
802
 
803
+ ## Contributing
804
+
805
+ The package ships with Jest tests under `modules/react-arborist/src/**/*.test.ts?(x)`.
806
+ From the package directory you can run:
807
+
808
+ ```bash
809
+ cd modules/react-arborist
810
+ yarn test
811
+ ```
812
+
813
+ Good starting points:
814
+
815
+ - `src/interfaces/tree-api.test.ts` for pure API behavior
816
+ - `src/components/provider.test.tsx` for rendered tree behavior
817
+ - `src/dnd/drag-hook.test.ts` for drag-and-drop behavior
818
+
803
819
  ## Author
804
820
 
805
821
  [James Kerr](https://twitter.com/specialCaseDev) at [Brim Data](https://brimdata.io) for the [Zui desktop app](https://www.youtube.com/watch?v=I2y663n8d2A).
@@ -2,4 +2,5 @@ import { ConnectDragSource } from "react-dnd";
2
2
  import { NodeApi } from "../interfaces/node-api";
3
3
  import { TreeProps } from "../types/tree-props";
4
4
  export declare function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string;
5
+ export declare function canDragNode<T>(node: NodeApi<T>): boolean;
5
6
  export declare function useDragHook<T>(node: NodeApi<T>): ConnectDragSource;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dragTypeForNode = dragTypeForNode;
4
+ exports.canDragNode = canDragNode;
4
5
  exports.useDragHook = useDragHook;
5
6
  const react_1 = require("react");
6
7
  const react_dnd_1 = require("react-dnd");
@@ -14,11 +15,17 @@ function dragTypeForNode(dragType, node) {
14
15
  return dragType(node);
15
16
  return dragType !== null && dragType !== void 0 ? dragType : "NODE";
16
17
  }
18
+ /* A node can start a drag only when it's draggable and not currently being
19
+ renamed. Without the editing guard, dragging inside the rename input would
20
+ pick the row up and move it (issue #195). */
21
+ function canDragNode(node) {
22
+ return node.isDraggable && !node.isEditing;
23
+ }
17
24
  function useDragHook(node) {
18
25
  const tree = (0, context_1.useTreeApi)();
19
26
  const ids = tree.selectedIds;
20
27
  const [_, ref, preview] = (0, react_dnd_1.useDrag)(() => ({
21
- canDrag: () => node.isDraggable,
28
+ canDrag: () => canDragNode(node),
22
29
  type: dragTypeForNode(tree.props.dragType, node),
23
30
  item: () => {
24
31
  // This is fired once at the beginning of a drag operation
@@ -17,3 +17,17 @@ test("resolves a per-node dragType function against the node", () => {
17
17
  expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
18
18
  expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
19
19
  });
20
+ /* canDragNode only reads the isDraggable/isEditing flags, so a minimal stub
21
+ stands in for a real NodeApi. */
22
+ function draggableNode(flags) {
23
+ return flags;
24
+ }
25
+ test("a draggable node that isn't being edited can drag", () => {
26
+ expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: true, isEditing: false }))).toBe(true);
27
+ });
28
+ test("a non-draggable node can't drag", () => {
29
+ expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: false, isEditing: false }))).toBe(false);
30
+ });
31
+ test("a node being renamed can't drag, even when draggable (#195)", () => {
32
+ expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: true, isEditing: true }))).toBe(false);
33
+ });
@@ -5,8 +5,6 @@ const react_dnd_1 = require("react-dnd");
5
5
  const context_1 = require("../context");
6
6
  const compute_drop_1 = require("./compute-drop");
7
7
  const dnd_slice_1 = require("../state/dnd-slice");
8
- const utils_1 = require("../utils");
9
- const create_root_1 = require("../data/create-root");
10
8
  function useDropHook(el, node) {
11
9
  const tree = (0, context_1.useTreeApi)();
12
10
  const [_, dropRef] = (0, react_dnd_1.useDrop)(() => ({
@@ -37,15 +35,7 @@ function useDropHook(el, node) {
37
35
  drop: (_, m) => {
38
36
  if (!m.canDrop())
39
37
  return null;
40
- let { parentId, index, dragIds } = tree.state.dnd;
41
- (0, utils_1.safeRun)(tree.props.onMove, {
42
- dragIds,
43
- parentId: parentId === create_root_1.ROOT_ID ? null : parentId,
44
- index: index === null ? 0 : index, // When it's null it was dropped over a folder
45
- dragNodes: tree.dragNodes,
46
- parentNode: tree.get(parentId),
47
- });
48
- tree.open(parentId);
38
+ tree.drop();
49
39
  },
50
40
  }), [node, el.current, tree.props]);
51
41
  return dropRef;
@@ -39,6 +39,13 @@ function useOuterDrop() {
39
39
  tree.hideCursor();
40
40
  }
41
41
  },
42
+ drop: (_item, m) => {
43
+ if (!m.isOver({ shallow: true }))
44
+ return null;
45
+ if (!m.canDrop())
46
+ return null;
47
+ tree.drop();
48
+ },
42
49
  }), [tree]);
43
50
  drop(tree.listEl);
44
51
  }
@@ -205,6 +205,7 @@ export declare class TreeApi<T> {
205
205
  get dragDestinationParent(): NodeApi<T> | null;
206
206
  get dragDestinationIndex(): number | null;
207
207
  canDrop(): boolean;
208
+ drop(): void;
208
209
  hideCursor(): void;
209
210
  showCursor(cursor: Cursor): void;
210
211
  open(identity: Identity, redraw?: boolean): void;
@@ -551,6 +551,17 @@ class TreeApi {
551
551
  return true;
552
552
  }
553
553
  }
554
+ drop() {
555
+ const { parentId, index, dragIds } = this.state.dnd;
556
+ safeRun(this.props.onMove, {
557
+ dragIds,
558
+ parentId: parentId === create_root_1.ROOT_ID ? null : parentId,
559
+ index: index === null ? 0 : index, // When it's null it was dropped over a folder
560
+ dragNodes: this.dragNodes,
561
+ parentNode: this.get(parentId),
562
+ });
563
+ this.open(parentId);
564
+ }
554
565
  hideCursor() {
555
566
  this.dispatch(dnd_slice_1.actions.cursor({ type: "none" }));
556
567
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const redux_1 = require("redux");
4
4
  const root_reducer_1 = require("../state/root-reducer");
5
+ const dnd_slice_1 = require("../state/dnd-slice");
5
6
  const tree_api_1 = require("./tree-api");
6
7
  function setupApi(props) {
7
8
  const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
@@ -13,6 +14,30 @@ test("tree.canDrop()", () => {
13
14
  expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
14
15
  });
15
16
  const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
17
+ describe("tree.drop() fires onMove (#313)", () => {
18
+ test("reports the hovered parent and index, mapping the root id to null", () => {
19
+ const onMove = jest.fn();
20
+ const api = setupApi({ data: rowData, onMove });
21
+ // The bottom drop zone hovers the root with an index past the end, just like
22
+ // computeDrop() reports it. tree.drop() should map the root id back to null.
23
+ api.dispatch(dnd_slice_1.actions.dragStart("a", ["a"]));
24
+ api.dispatch(dnd_slice_1.actions.hovering(api.root.id, 3));
25
+ api.drop();
26
+ expect(onMove).toHaveBeenCalledTimes(1);
27
+ expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ dragIds: ["a"], parentId: null, index: 3 }));
28
+ });
29
+ test("coerces a null index (dropped onto a folder) to 0", () => {
30
+ const onMove = jest.fn();
31
+ const folderData = [{ id: "folder", children: [{ id: "child" }] }];
32
+ const api = setupApi({ data: folderData, onMove });
33
+ // Dropping onto a folder (rather than between rows) reports the folder as the
34
+ // parent with a null index, which tree.drop() should coerce to 0.
35
+ api.dispatch(dnd_slice_1.actions.dragStart("child", ["child"]));
36
+ api.dispatch(dnd_slice_1.actions.hovering("folder", null));
37
+ api.drop();
38
+ expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
39
+ });
40
+ });
16
41
  test("rowHeight defaults to 24", () => {
17
42
  const api = setupApi({});
18
43
  expect(api.rowHeight).toBe(24);
@@ -2,4 +2,5 @@ import { ConnectDragSource } from "react-dnd";
2
2
  import { NodeApi } from "../interfaces/node-api";
3
3
  import { TreeProps } from "../types/tree-props";
4
4
  export declare function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string;
5
+ export declare function canDragNode<T>(node: NodeApi<T>): boolean;
5
6
  export declare function useDragHook<T>(node: NodeApi<T>): ConnectDragSource;
@@ -10,11 +10,17 @@ export function dragTypeForNode(dragType, node) {
10
10
  return dragType(node);
11
11
  return dragType !== null && dragType !== void 0 ? dragType : "NODE";
12
12
  }
13
+ /* A node can start a drag only when it's draggable and not currently being
14
+ renamed. Without the editing guard, dragging inside the rename input would
15
+ pick the row up and move it (issue #195). */
16
+ export function canDragNode(node) {
17
+ return node.isDraggable && !node.isEditing;
18
+ }
13
19
  export function useDragHook(node) {
14
20
  const tree = useTreeApi();
15
21
  const ids = tree.selectedIds;
16
22
  const [_, ref, preview] = useDrag(() => ({
17
- canDrag: () => node.isDraggable,
23
+ canDrag: () => canDragNode(node),
18
24
  type: dragTypeForNode(tree.props.dragType, node),
19
25
  item: () => {
20
26
  // This is fired once at the beginning of a drag operation
@@ -1,4 +1,4 @@
1
- import { dragTypeForNode } from "./drag-hook";
1
+ import { canDragNode, dragTypeForNode } from "./drag-hook";
2
2
  /* dragTypeForNode only reads node.data when dragType is a function, so a
3
3
  minimal stub stands in for a real NodeApi. */
4
4
  function nodeWith(data) {
@@ -15,3 +15,17 @@ test("resolves a per-node dragType function against the node", () => {
15
15
  expect(dragTypeForNode(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
16
16
  expect(dragTypeForNode(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
17
17
  });
18
+ /* canDragNode only reads the isDraggable/isEditing flags, so a minimal stub
19
+ stands in for a real NodeApi. */
20
+ function draggableNode(flags) {
21
+ return flags;
22
+ }
23
+ test("a draggable node that isn't being edited can drag", () => {
24
+ expect(canDragNode(draggableNode({ isDraggable: true, isEditing: false }))).toBe(true);
25
+ });
26
+ test("a non-draggable node can't drag", () => {
27
+ expect(canDragNode(draggableNode({ isDraggable: false, isEditing: false }))).toBe(false);
28
+ });
29
+ test("a node being renamed can't drag, even when draggable (#195)", () => {
30
+ expect(canDragNode(draggableNode({ isDraggable: true, isEditing: true }))).toBe(false);
31
+ });
@@ -2,8 +2,6 @@ import { useDrop } from "react-dnd";
2
2
  import { useTreeApi } from "../context";
3
3
  import { computeDrop } from "./compute-drop";
4
4
  import { actions as dnd } from "../state/dnd-slice";
5
- import { safeRun } from "../utils";
6
- import { ROOT_ID } from "../data/create-root";
7
5
  export function useDropHook(el, node) {
8
6
  const tree = useTreeApi();
9
7
  const [_, dropRef] = useDrop(() => ({
@@ -34,15 +32,7 @@ export function useDropHook(el, node) {
34
32
  drop: (_, m) => {
35
33
  if (!m.canDrop())
36
34
  return null;
37
- let { parentId, index, dragIds } = tree.state.dnd;
38
- safeRun(tree.props.onMove, {
39
- dragIds,
40
- parentId: parentId === ROOT_ID ? null : parentId,
41
- index: index === null ? 0 : index, // When it's null it was dropped over a folder
42
- dragNodes: tree.dragNodes,
43
- parentNode: tree.get(parentId),
44
- });
45
- tree.open(parentId);
35
+ tree.drop();
46
36
  },
47
37
  }), [node, el.current, tree.props]);
48
38
  return dropRef;
@@ -36,6 +36,13 @@ export function useOuterDrop() {
36
36
  tree.hideCursor();
37
37
  }
38
38
  },
39
+ drop: (_item, m) => {
40
+ if (!m.isOver({ shallow: true }))
41
+ return null;
42
+ if (!m.canDrop())
43
+ return null;
44
+ tree.drop();
45
+ },
39
46
  }), [tree]);
40
47
  drop(tree.listEl);
41
48
  }
@@ -205,6 +205,7 @@ export declare class TreeApi<T> {
205
205
  get dragDestinationParent(): NodeApi<T> | null;
206
206
  get dragDestinationIndex(): number | null;
207
207
  canDrop(): boolean;
208
+ drop(): void;
208
209
  hideCursor(): void;
209
210
  showCursor(cursor: Cursor): void;
210
211
  open(identity: Identity, redraw?: boolean): void;
@@ -525,6 +525,17 @@ export class TreeApi {
525
525
  return true;
526
526
  }
527
527
  }
528
+ drop() {
529
+ const { parentId, index, dragIds } = this.state.dnd;
530
+ safeRun(this.props.onMove, {
531
+ dragIds,
532
+ parentId: parentId === ROOT_ID ? null : parentId,
533
+ index: index === null ? 0 : index, // When it's null it was dropped over a folder
534
+ dragNodes: this.dragNodes,
535
+ parentNode: this.get(parentId),
536
+ });
537
+ this.open(parentId);
538
+ }
528
539
  hideCursor() {
529
540
  this.dispatch(dnd.cursor({ type: "none" }));
530
541
  }
@@ -1,5 +1,6 @@
1
1
  import { createStore } from "redux";
2
2
  import { rootReducer } from "../state/root-reducer";
3
+ import { actions as dnd } from "../state/dnd-slice";
3
4
  import { TreeApi } from "./tree-api";
4
5
  function setupApi(props) {
5
6
  const store = createStore(rootReducer);
@@ -11,6 +12,30 @@ test("tree.canDrop()", () => {
11
12
  expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
12
13
  });
13
14
  const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
15
+ describe("tree.drop() fires onMove (#313)", () => {
16
+ test("reports the hovered parent and index, mapping the root id to null", () => {
17
+ const onMove = jest.fn();
18
+ const api = setupApi({ data: rowData, onMove });
19
+ // The bottom drop zone hovers the root with an index past the end, just like
20
+ // computeDrop() reports it. tree.drop() should map the root id back to null.
21
+ api.dispatch(dnd.dragStart("a", ["a"]));
22
+ api.dispatch(dnd.hovering(api.root.id, 3));
23
+ api.drop();
24
+ expect(onMove).toHaveBeenCalledTimes(1);
25
+ expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ dragIds: ["a"], parentId: null, index: 3 }));
26
+ });
27
+ test("coerces a null index (dropped onto a folder) to 0", () => {
28
+ const onMove = jest.fn();
29
+ const folderData = [{ id: "folder", children: [{ id: "child" }] }];
30
+ const api = setupApi({ data: folderData, onMove });
31
+ // Dropping onto a folder (rather than between rows) reports the folder as the
32
+ // parent with a null index, which tree.drop() should coerce to 0.
33
+ api.dispatch(dnd.dragStart("child", ["child"]));
34
+ api.dispatch(dnd.hovering("folder", null));
35
+ api.drop();
36
+ expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
37
+ });
38
+ });
14
39
  test("rowHeight defaults to 24", () => {
15
40
  const api = setupApi({});
16
41
  expect(api.rowHeight).toBe(24);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-arborist",
3
- "version": "3.10.1",
3
+ "version": "3.10.3",
4
4
  "keywords": [
5
5
  "arborist",
6
6
  "dnd",
@@ -1,5 +1,5 @@
1
1
  import { NodeApi } from "../interfaces/node-api";
2
- import { dragTypeForNode } from "./drag-hook";
2
+ import { canDragNode, dragTypeForNode } from "./drag-hook";
3
3
 
4
4
  /* dragTypeForNode only reads node.data when dragType is a function, so a
5
5
  minimal stub stands in for a real NodeApi. */
@@ -20,3 +20,21 @@ test("resolves a per-node dragType function against the node", () => {
20
20
  expect(dragTypeForNode(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
21
21
  expect(dragTypeForNode(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
22
22
  });
23
+
24
+ /* canDragNode only reads the isDraggable/isEditing flags, so a minimal stub
25
+ stands in for a real NodeApi. */
26
+ function draggableNode(flags: { isDraggable: boolean; isEditing: boolean }): NodeApi {
27
+ return flags as NodeApi;
28
+ }
29
+
30
+ test("a draggable node that isn't being edited can drag", () => {
31
+ expect(canDragNode(draggableNode({ isDraggable: true, isEditing: false }))).toBe(true);
32
+ });
33
+
34
+ test("a non-draggable node can't drag", () => {
35
+ expect(canDragNode(draggableNode({ isDraggable: false, isEditing: false }))).toBe(false);
36
+ });
37
+
38
+ test("a node being renamed can't drag, even when draggable (#195)", () => {
39
+ expect(canDragNode(draggableNode({ isDraggable: true, isEditing: true }))).toBe(false);
40
+ });
@@ -15,12 +15,19 @@ export function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: Nod
15
15
  return dragType ?? "NODE";
16
16
  }
17
17
 
18
+ /* A node can start a drag only when it's draggable and not currently being
19
+ renamed. Without the editing guard, dragging inside the rename input would
20
+ pick the row up and move it (issue #195). */
21
+ export function canDragNode<T>(node: NodeApi<T>): boolean {
22
+ return node.isDraggable && !node.isEditing;
23
+ }
24
+
18
25
  export function useDragHook<T>(node: NodeApi<T>): ConnectDragSource {
19
26
  const tree = useTreeApi<T>();
20
27
  const ids = tree.selectedIds;
21
28
  const [_, ref, preview] = useDrag<DragItem<T>, DropResult, void>(
22
29
  () => ({
23
- canDrag: () => node.isDraggable,
30
+ canDrag: () => canDragNode(node),
24
31
  type: dragTypeForNode(tree.props.dragType, node),
25
32
  item: () => {
26
33
  // This is fired once at the beginning of a drag operation
@@ -5,8 +5,6 @@ import { NodeApi } from "../interfaces/node-api";
5
5
  import { DragItem } from "../types/dnd";
6
6
  import { computeDrop } from "./compute-drop";
7
7
  import { actions as dnd } from "../state/dnd-slice";
8
- import { safeRun } from "../utils";
9
- import { ROOT_ID } from "../data/create-root";
10
8
 
11
9
  export type DropResult = {
12
10
  parentId: string | null;
@@ -43,15 +41,7 @@ export function useDropHook(
43
41
  },
44
42
  drop: (_, m) => {
45
43
  if (!m.canDrop()) return null;
46
- let { parentId, index, dragIds } = tree.state.dnd;
47
- safeRun(tree.props.onMove, {
48
- dragIds,
49
- parentId: parentId === ROOT_ID ? null : parentId,
50
- index: index === null ? 0 : index, // When it's null it was dropped over a folder
51
- dragNodes: tree.dragNodes,
52
- parentNode: tree.get(parentId),
53
- });
54
- tree.open(parentId);
44
+ tree.drop();
55
45
  },
56
46
  }),
57
47
  [node, el.current, tree.props],
@@ -36,6 +36,11 @@ export function useOuterDrop() {
36
36
  tree.hideCursor();
37
37
  }
38
38
  },
39
+ drop: (_item, m) => {
40
+ if (!m.isOver({ shallow: true })) return null;
41
+ if (!m.canDrop()) return null;
42
+ tree.drop();
43
+ },
39
44
  }),
40
45
  [tree],
41
46
  );
@@ -1,5 +1,6 @@
1
1
  import { createStore } from "redux";
2
2
  import { rootReducer } from "../state/root-reducer";
3
+ import { actions as dnd } from "../state/dnd-slice";
3
4
  import { TreeProps } from "../types/tree-props";
4
5
  import { TreeApi } from "./tree-api";
5
6
 
@@ -16,6 +17,36 @@ test("tree.canDrop()", () => {
16
17
 
17
18
  const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
18
19
 
20
+ describe("tree.drop() fires onMove (#313)", () => {
21
+ test("reports the hovered parent and index, mapping the root id to null", () => {
22
+ const onMove = jest.fn();
23
+ const api = setupApi({ data: rowData, onMove });
24
+ // The bottom drop zone hovers the root with an index past the end, just like
25
+ // computeDrop() reports it. tree.drop() should map the root id back to null.
26
+ api.dispatch(dnd.dragStart("a", ["a"]));
27
+ api.dispatch(dnd.hovering(api.root.id, 3));
28
+ api.drop();
29
+ expect(onMove).toHaveBeenCalledTimes(1);
30
+ expect(onMove).toHaveBeenCalledWith(
31
+ expect.objectContaining({ dragIds: ["a"], parentId: null, index: 3 }),
32
+ );
33
+ });
34
+
35
+ test("coerces a null index (dropped onto a folder) to 0", () => {
36
+ const onMove = jest.fn();
37
+ const folderData = [{ id: "folder", children: [{ id: "child" }] }];
38
+ const api = setupApi({ data: folderData, onMove });
39
+ // Dropping onto a folder (rather than between rows) reports the folder as the
40
+ // parent with a null index, which tree.drop() should coerce to 0.
41
+ api.dispatch(dnd.dragStart("child", ["child"]));
42
+ api.dispatch(dnd.hovering("folder", null));
43
+ api.drop();
44
+ expect(onMove).toHaveBeenCalledWith(
45
+ expect.objectContaining({ parentId: "folder", index: 0 }),
46
+ );
47
+ });
48
+ });
49
+
19
50
  test("rowHeight defaults to 24", () => {
20
51
  const api = setupApi({});
21
52
  expect(api.rowHeight).toBe(24);
@@ -546,6 +546,18 @@ export class TreeApi<T> {
546
546
  }
547
547
  }
548
548
 
549
+ drop() {
550
+ const { parentId, index, dragIds } = this.state.dnd;
551
+ safeRun(this.props.onMove, {
552
+ dragIds,
553
+ parentId: parentId === ROOT_ID ? null : parentId,
554
+ index: index === null ? 0 : index, // When it's null it was dropped over a folder
555
+ dragNodes: this.dragNodes,
556
+ parentNode: this.get(parentId),
557
+ });
558
+ this.open(parentId);
559
+ }
560
+
549
561
  hideCursor() {
550
562
  this.dispatch(dnd.cursor({ type: "none" }));
551
563
  }