Skip to content

Commit 98a05df

Browse files
committed
select adjacent basic cases #3
1 parent 40d4fe4 commit 98a05df

File tree

7 files changed

+284
-47
lines changed

7 files changed

+284
-47
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
},
1717
"devDependencies": {
1818
"@types/jest": "^27.0.0",
19+
"@types/ramda": "types/npm-ramda#dist",
1920
"fast-check": "^2.17.0",
2021
"jest": "^27.0.6",
2122
"ts-jest": "^27.0.4",
2223
"typescript": "^4.3.5"
2324
},
2425
"dependencies": {
25-
"fp-ts": "^2.11.1"
26+
"ramda": "^0.27.1"
2627
}
2728
}

src/index.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,106 @@
1+
import { head, take } from "ramda";
2+
13
export type Context = {
24
list: string[],
35
selected: string[],
6+
adjacentPivot: undefined,
7+
lastSelected: undefined,
8+
} | {
9+
list: string[],
10+
selected: string[],
11+
adjacentPivotstring,
12+
lastSelected: string,
413
}
514

615
type Command =
716
| { type: "SELECT ONE", id: string }
817
| { type: "TOGGLE SELECTION", id: string }
918
| { type: "DESELECT ALL" }
19+
| { type: "SELECT ADJACENT", id: string }
20+
21+
function listIncludesAndIsNotEmpty(list: string[], item: string) {
22+
return list.length > 0 && list.includes(item)
23+
}
1024

1125
export function multiselect(context: Context, command: Command): Context {
12-
if (command.type === "SELECT ONE" && context.list.includes(command.id)) {
26+
if (
27+
command.type === "SELECT ONE" &&
28+
listIncludesAndIsNotEmpty(context.list, command.id)
29+
) {
30+
return {
31+
...context,
32+
selected: [command.id],
33+
adjacentPivot: command.id,
34+
lastSelected: command.id,
35+
};
36+
} else if (
37+
command.type === 'TOGGLE SELECTION' &&
38+
listIncludesAndIsNotEmpty(context.list, command.id) &&
39+
context.selected.includes(command.id) &&
40+
context.selected.length === 1
41+
) {
42+
const index = context.selected.indexOf(command.id)
1343
return {
1444
...context,
15-
selected: [command.id]
45+
selected: [],
46+
adjacentPivot: undefined,
47+
lastSelected: undefined
1648
};
1749
} else if (
1850
command.type === 'TOGGLE SELECTION' &&
19-
context.list.includes(command.id) &&
51+
listIncludesAndIsNotEmpty(context.list, command.id) &&
2052
context.selected.includes(command.id)
21-
) {
53+
) {
54+
const index = context.selected.indexOf(command.id);
55+
const adjacentPivot= (index < (context.selected.length - 1))
56+
? context.selected[index + 1]
57+
: context.selected[index - 1];
2258
return {
2359
...context,
2460
selected: context.selected.filter(x => x !== command.id),
61+
adjacentPivot,
62+
lastSelected: adjacentPivot
2563
};
2664
} else if (
27-
command.type === 'TOGGLE SELECTION' &&
28-
context.list.includes(command.id)
29-
) {
65+
command.type === 'TOGGLE SELECTION' &&
66+
context.list.includes(command.id)
67+
) {
3068
return {
3169
...context,
32-
selected: context.selected.concat([command.id])
70+
selected: context.selected.concat([command.id]),
71+
adjacentPivot: command.id,
72+
lastSelected: command.id
3373
};
3474
} else if (command.type === "DESELECT ALL") {
3575
return {
3676
...context,
37-
selected: []
77+
selected: [],
78+
adjacentPivot: undefined,
79+
lastSelected: undefined
80+
}
81+
} else if (
82+
command.type === "SELECT ADJACENT" &&
83+
listIncludesAndIsNotEmpty(context.list, command.id) &&
84+
context.selected.length === 0
85+
) {
86+
const n = context.list.indexOf(command.id) + 1;
87+
return {
88+
...context,
89+
selected: take(n, context.list),
90+
adjacentPivot: head(context.list)!,
91+
lastSelected: command.id,
92+
}
93+
} else if (
94+
command.type === "SELECT ADJACENT" &&
95+
listIncludesAndIsNotEmpty(context.list, command.id) &&
96+
context.adjacentPivot
97+
) {
98+
const start = context.list.indexOf(context.adjacentPivot);
99+
const n = context.list.indexOf(command.id);
100+
return {
101+
...context,
102+
selected: context.list.slice(Math.min(start, n), Math.max(start, n) + 1),
103+
lastSelected: command.id
38104
}
39105
} else {
40106
return context;

src/spec/deselectAll.spec.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,32 @@ describe('Deselect All', () => {
2323
list,
2424
selected
2525
}) => {
26+
const context = (selected.length)
27+
? {
28+
list,
29+
selected,
30+
adjacentPivot: selected[selected.length - 1],
31+
lastSelected: selected[selected.length - 1],
32+
}
33+
: {
34+
list,
35+
selected,
36+
adjacentPivot: undefined,
37+
lastSelected: undefined,
38+
}
2639
expect(
2740
multiselect(
28-
{
29-
list,
30-
selected,
31-
},
41+
context,
3242
{
3343
type: "DESELECT ALL",
3444
}
3545
)
3646
)
3747
.toEqual({
3848
list,
39-
selected: []
49+
selected: [],
50+
adjacentPivot: undefined,
51+
lastSelected: undefined
4052
})
4153
}
4254
)

src/spec/selectAdjacent.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import fc from 'fast-check';
2+
import { take, last, head, startsWith } from "ramda";
3+
import { multiselect } from '../index';
4+
5+
describe('Select Adjacent', () => {
6+
test('should select from top to bottom in an empty list', () => {
7+
8+
fc.assert(
9+
fc.property(
10+
fc.set(
11+
fc.string(),
12+
{ minLength: 1 }
13+
)
14+
.chain(list =>
15+
fc.record({
16+
list: fc.constant(list),
17+
selection: fc.integer(1, list.length)
18+
.map(n => take(n, list))
19+
})
20+
),
21+
({
22+
list,
23+
selection,
24+
}) => {
25+
const id = last(selection)!;
26+
expect(
27+
multiselect({
28+
list,
29+
selected: [],
30+
adjacentPivot: undefined,
31+
lastSelected: undefined
32+
},
33+
{
34+
type: "SELECT ADJACENT",
35+
id,
36+
}
37+
)
38+
)
39+
.toEqual({
40+
list,
41+
selected: selection,
42+
adjacentPivot: head(selection)!,
43+
lastSelected: id
44+
})
45+
}
46+
)
47+
)
48+
});
49+
50+
test('should select from the last adjacent pivot', () => {
51+
52+
fc.assert(
53+
fc.property(
54+
fc.set(
55+
fc.string(),
56+
{ minLength: 1 }
57+
)
58+
.chain(list =>
59+
fc.record({
60+
list: fc.constant(list),
61+
selection: fc
62+
.integer(0, list.length - 1)
63+
.chain(start =>
64+
fc
65+
.integer(start + 1, list.length)
66+
.map(end =>
67+
list.slice(start, end)
68+
)
69+
),
70+
direction: fc.boolean(),
71+
})
72+
)
73+
.map(
74+
({
75+
list,
76+
selection,
77+
direction
78+
}) => ({
79+
list,
80+
selection,
81+
start: direction ? head(selection)! : last(selection)!,
82+
end: direction ? last(selection)! : head(selection)!
83+
})
84+
),
85+
({
86+
list,
87+
selection,
88+
start,
89+
end
90+
}) => {
91+
expect(
92+
multiselect(
93+
multiselect({
94+
list,
95+
selected: [],
96+
adjacentPivot: undefined,
97+
lastSelected: undefined
98+
},
99+
{
100+
type: "SELECT ONE",
101+
id: start,
102+
}
103+
),
104+
{
105+
type: "SELECT ADJACENT",
106+
id: end
107+
}
108+
)
109+
)
110+
.toEqual({
111+
list,
112+
selected: selection,
113+
adjacentPivot: start,
114+
lastSelected: end
115+
})
116+
}
117+
)
118+
);
119+
120+
});
121+
})

src/spec/selectOne.spec.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ describe('Select One Item', () => {
2525
multiselect({
2626
list,
2727
selected: [],
28+
adjacentPivot: undefined,
29+
lastSelected: undefined,
2830
},
2931
{
3032
type: "SELECT ONE",
@@ -34,7 +36,9 @@ describe('Select One Item', () => {
3436
)
3537
.toEqual({
3638
list,
37-
selected: [id]
39+
selected: [id],
40+
adjacentPivot: id,
41+
lastSelected: id,
3842
})
3943
}
4044
)
@@ -68,6 +72,8 @@ describe('Select One Item', () => {
6872
multiselect({
6973
list,
7074
selected: [selectedId],
75+
adjacentPivot: selectedId,
76+
lastSelected: selectedId,
7177
},
7278
{
7379
type: "SELECT ONE",
@@ -77,7 +83,9 @@ describe('Select One Item', () => {
7783
)
7884
.toEqual({
7985
list,
80-
selected: [id]
86+
selected: [id],
87+
adjacentPivot: id,
88+
lastSelected: id
8189
})
8290
}
8391
)

0 commit comments

Comments
 (0)