@dhis2-ui/transfer 10.16.1 → 10.16.3-alpha.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/package.json +10 -9
- package/src/__e2e__/add_remove-highlighted-options.e2e.stories.js +30 -0
- package/src/__e2e__/common/options.js +90 -0
- package/src/__e2e__/common/stateful-decorator.js +33 -0
- package/src/__e2e__/common.js +0 -0
- package/src/__e2e__/disabled-transfer-buttons.e2e.stories.js +49 -0
- package/src/__e2e__/disabled-transfer-options.e2e.stories.js +21 -0
- package/src/__e2e__/display-order.e2e.stories.js +24 -0
- package/src/__e2e__/filter-options-list.e2e.stories.js +87 -0
- package/src/__e2e__/highlight-range-of-options.e2e.stories.js +52 -0
- package/src/__e2e__/loading_lists.e2e.stories.js +26 -0
- package/src/__e2e__/notify_at_end_of_list.e2e.stories.js +116 -0
- package/src/__e2e__/reorder-with-buttons.e2e.stories.js +35 -0
- package/src/__e2e__/set_unset-highlighted-option.e2e.stories.js +30 -0
- package/src/__e2e__/transferring-items.e2e.stories.js +27 -0
- package/src/__tests__/common.test.js +131 -0
- package/src/__tests__/helper/add-all-selectable-source-options.test.js +46 -0
- package/src/__tests__/helper/add-individual-source-options.test.js +80 -0
- package/src/__tests__/helper/default-filter-callback.test.js +45 -0
- package/src/__tests__/helper/is-reorder-down-disabled.test.js +96 -0
- package/src/__tests__/helper/is-reorder-up-disabled.test.js +96 -0
- package/src/__tests__/helper/move-highlighted-picked-option-down.test.js +111 -0
- package/src/__tests__/helper/move-highlighted-picked-option-to-bottom.test.js +101 -0
- package/src/__tests__/helper/move-highlighted-picked-option-to-top.test.js +101 -0
- package/src/__tests__/helper/move-highlighted-picked-option-up.test.js +111 -0
- package/src/__tests__/helper/remove-all-picked-options.test.js +29 -0
- package/src/__tests__/helper/remove-individual-picked-options.test.js +38 -0
- package/src/__tests__/helper/use-highlighted-option/create-toggle-highlighted-option.test.js +104 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-add.test.js +84 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-range.test.js +150 -0
- package/src/__tests__/helper/use-highlighted-option/toggle-replace.test.js +39 -0
- package/src/__tests__/helper/use-highlighted-option.test.js +41 -0
- package/src/__tests__/reordering-actions.test.js +165 -0
- package/src/__tests__/transfer.test.js +137 -0
- package/src/actions.js +33 -0
- package/src/add-all.js +27 -0
- package/src/add-individual.js +27 -0
- package/src/common/find-option-index.js +9 -0
- package/src/common/get-mode-by-modifier-key.js +35 -0
- package/src/common/index.js +5 -0
- package/src/common/is-option.js +7 -0
- package/src/common/modes.js +11 -0
- package/src/common/remove-option.js +19 -0
- package/src/common/toggle-value.js +18 -0
- package/src/container.js +23 -0
- package/src/end-intersection-detector.js +37 -0
- package/src/features/add_remove-highlighted-options/index.js +92 -0
- package/src/features/add_remove-highlighted-options.feature +41 -0
- package/src/features/common/index.js +8 -0
- package/src/features/disabled-transfer-buttons/index.js +118 -0
- package/src/features/disabled-transfer-buttons.feature +46 -0
- package/src/features/disabled-transfer-options/index.js +182 -0
- package/src/features/disabled-transfer-options.feature +42 -0
- package/src/features/display-order/index.js +205 -0
- package/src/features/display-order.feature +30 -0
- package/src/features/filter-options-list/index.js +133 -0
- package/src/features/filter-options-list.feature +40 -0
- package/src/features/highlight-range-of-options/index.js +336 -0
- package/src/features/highlight-range-of-options.feature +70 -0
- package/src/features/loading_lists/index.js +43 -0
- package/src/features/loading_lists.feature +19 -0
- package/src/features/notify_at_end_of_list/index.js +125 -0
- package/src/features/notify_at_end_of_list.feature +64 -0
- package/src/features/reorder-with-buttons/index.js +181 -0
- package/src/features/reorder-with-buttons.feature +138 -0
- package/src/features/set_unset-highlighted-option/index.js +121 -0
- package/src/features/set_unset-highlighted-option.feature +42 -0
- package/src/features/transferring-items/index.js +375 -0
- package/src/features/transferring-items.feature +44 -0
- package/src/filter.js +38 -0
- package/src/icons.js +194 -0
- package/src/index.js +2 -0
- package/src/left-footer.js +22 -0
- package/src/left-header.js +22 -0
- package/src/left-side.js +34 -0
- package/src/locales/en/translations.json +7 -0
- package/src/locales/index.js +16 -0
- package/src/options-container.js +127 -0
- package/src/remove-all.js +27 -0
- package/src/remove-individual.js +27 -0
- package/src/reordering-actions.js +136 -0
- package/src/right-footer.js +22 -0
- package/src/right-header.js +22 -0
- package/src/right-side.js +33 -0
- package/src/transfer/add-all-selectable-source-options.js +37 -0
- package/src/transfer/add-individual-source-options.js +61 -0
- package/src/transfer/create-double-click-handlers.js +36 -0
- package/src/transfer/default-filter-callback.js +17 -0
- package/src/transfer/get-highlighted-picked-indices.js +26 -0
- package/src/transfer/get-option-click-handlers.js +19 -0
- package/src/transfer/index.js +17 -0
- package/src/transfer/is-reorder-down-disabled.js +34 -0
- package/src/transfer/is-reorder-up-disabled.js +30 -0
- package/src/transfer/move-highlighted-picked-option-down.js +54 -0
- package/src/transfer/move-highlighted-picked-option-to-bottom.js +44 -0
- package/src/transfer/move-highlighted-picked-option-to-top.js +38 -0
- package/src/transfer/move-highlighted-picked-option-up.js +47 -0
- package/src/transfer/remove-all-picked-options.js +13 -0
- package/src/transfer/remove-individual-picked-options.js +49 -0
- package/src/transfer/use-filter.js +17 -0
- package/src/transfer/use-highlighted-options/create-toggle-highlighted-option.js +64 -0
- package/src/transfer/use-highlighted-options/toggle-add.js +20 -0
- package/src/transfer/use-highlighted-options/toggle-range.js +61 -0
- package/src/transfer/use-highlighted-options/toggle-replace.js +26 -0
- package/src/transfer/use-highlighted-options.js +34 -0
- package/src/transfer/use-options-key-monitor.js +41 -0
- package/src/transfer-option.js +91 -0
- package/src/transfer.js +539 -0
- package/src/transfer.prod.stories.js +621 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './add-all-selectable-source-options.js'
|
|
2
|
+
export * from './add-individual-source-options.js'
|
|
3
|
+
export * from './create-double-click-handlers.js'
|
|
4
|
+
export * from './default-filter-callback.js'
|
|
5
|
+
export * from './get-highlighted-picked-indices.js'
|
|
6
|
+
export * from './get-option-click-handlers.js'
|
|
7
|
+
export * from './is-reorder-down-disabled.js'
|
|
8
|
+
export * from './is-reorder-up-disabled.js'
|
|
9
|
+
export * from './move-highlighted-picked-option-down.js'
|
|
10
|
+
export * from './move-highlighted-picked-option-to-bottom.js'
|
|
11
|
+
export * from './move-highlighted-picked-option-to-top.js'
|
|
12
|
+
export * from './move-highlighted-picked-option-up.js'
|
|
13
|
+
export * from './remove-all-picked-options.js'
|
|
14
|
+
export * from './remove-individual-picked-options.js'
|
|
15
|
+
export * from './use-filter.js'
|
|
16
|
+
export * from './use-highlighted-options.js'
|
|
17
|
+
export * from './use-options-key-monitor.js'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} args
|
|
5
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
6
|
+
* @param {string[]} args.selected
|
|
7
|
+
* @param {boolean} [args.filterActivePicked] reorder is disabled while a filter is applied to the picked side
|
|
8
|
+
* @returns {bool}
|
|
9
|
+
*/
|
|
10
|
+
export const isReorderDownDisabled = ({
|
|
11
|
+
highlightedPickedOptions,
|
|
12
|
+
selected,
|
|
13
|
+
filterActivePicked = false,
|
|
14
|
+
}) => {
|
|
15
|
+
if (filterActivePicked) {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const indices = getHighlightedPickedIndices({
|
|
20
|
+
selected,
|
|
21
|
+
highlightedPickedOptions,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (indices.length === 0) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lastIndex = selected.length - 1
|
|
29
|
+
|
|
30
|
+
// Flush to the bottom: indices are [len-n, ..., len-1]
|
|
31
|
+
return indices.every(
|
|
32
|
+
(index, i) => index === lastIndex - (indices.length - 1 - i)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} args
|
|
5
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
6
|
+
* @param {string[]} args.selected
|
|
7
|
+
* @param {boolean} [args.filterActivePicked] reorder is disabled while a filter is applied to the picked side
|
|
8
|
+
* @returns {bool}
|
|
9
|
+
*/
|
|
10
|
+
export const isReorderUpDisabled = ({
|
|
11
|
+
highlightedPickedOptions,
|
|
12
|
+
selected,
|
|
13
|
+
filterActivePicked = false,
|
|
14
|
+
}) => {
|
|
15
|
+
if (filterActivePicked) {
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const indices = getHighlightedPickedIndices({
|
|
20
|
+
selected,
|
|
21
|
+
highlightedPickedOptions,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (indices.length === 0) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Flush to the top: indices are [0, 1, ..., n-1]
|
|
29
|
+
return indices.every((index, i) => index === i)
|
|
30
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moves the highlighted picked options down by one slot as a group.
|
|
5
|
+
* If the selection is non-contiguous, the group collapses into a contiguous
|
|
6
|
+
* block (preserving relative order) with its bottom edge landing at
|
|
7
|
+
* `min(selected.length - 1, bottommostHighlightedIndex + 1)`.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} args
|
|
10
|
+
* @param {string[]} args.selected
|
|
11
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
12
|
+
* @param {Function} args.onChange
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export const moveHighlightedPickedOptionDown = ({
|
|
16
|
+
selected,
|
|
17
|
+
highlightedPickedOptions,
|
|
18
|
+
onChange,
|
|
19
|
+
}) => {
|
|
20
|
+
const indices = getHighlightedPickedIndices({
|
|
21
|
+
selected,
|
|
22
|
+
highlightedPickedOptions,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (indices.length === 0) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lastIndex = selected.length - 1
|
|
30
|
+
|
|
31
|
+
// Already flush to the bottom — nothing to do
|
|
32
|
+
if (
|
|
33
|
+
indices.every(
|
|
34
|
+
(index, i) => index === lastIndex - (indices.length - 1 - i)
|
|
35
|
+
)
|
|
36
|
+
) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const indexSet = new Set(indices)
|
|
41
|
+
const highlightedBlock = indices.map((index) => selected[index])
|
|
42
|
+
const remaining = selected.filter((_, index) => !indexSet.has(index))
|
|
43
|
+
const bottommost = indices.at(-1)
|
|
44
|
+
const targetBottom = Math.min(lastIndex, bottommost + 1)
|
|
45
|
+
const insertPos = targetBottom - (indices.length - 1)
|
|
46
|
+
|
|
47
|
+
const reordered = [
|
|
48
|
+
...remaining.slice(0, insertPos),
|
|
49
|
+
...highlightedBlock,
|
|
50
|
+
...remaining.slice(insertPos),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
onChange({ selected: reordered })
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moves the highlighted picked options to the very bottom of the list as a
|
|
5
|
+
* single contiguous block, preserving their relative order. Non-contiguous
|
|
6
|
+
* selections are collapsed.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} args
|
|
9
|
+
* @param {string[]} args.selected
|
|
10
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
11
|
+
* @param {Function} args.onChange
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
export const moveHighlightedPickedOptionToBottom = ({
|
|
15
|
+
selected,
|
|
16
|
+
highlightedPickedOptions,
|
|
17
|
+
onChange,
|
|
18
|
+
}) => {
|
|
19
|
+
const indices = getHighlightedPickedIndices({
|
|
20
|
+
selected,
|
|
21
|
+
highlightedPickedOptions,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (indices.length === 0) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lastIndex = selected.length - 1
|
|
29
|
+
|
|
30
|
+
// Already a contiguous block flush to the bottom — nothing to do
|
|
31
|
+
if (
|
|
32
|
+
indices.every(
|
|
33
|
+
(index, i) => index === lastIndex - (indices.length - 1 - i)
|
|
34
|
+
)
|
|
35
|
+
) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const indexSet = new Set(indices)
|
|
40
|
+
const highlightedBlock = indices.map((index) => selected[index])
|
|
41
|
+
const remaining = selected.filter((_, index) => !indexSet.has(index))
|
|
42
|
+
|
|
43
|
+
onChange({ selected: [...remaining, ...highlightedBlock] })
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moves the highlighted picked options to the very top of the list as a
|
|
5
|
+
* single contiguous block, preserving their relative order. Non-contiguous
|
|
6
|
+
* selections are collapsed.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} args
|
|
9
|
+
* @param {string[]} args.selected
|
|
10
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
11
|
+
* @param {Function} args.onChange
|
|
12
|
+
* @returns {void}
|
|
13
|
+
*/
|
|
14
|
+
export const moveHighlightedPickedOptionToTop = ({
|
|
15
|
+
selected,
|
|
16
|
+
highlightedPickedOptions,
|
|
17
|
+
onChange,
|
|
18
|
+
}) => {
|
|
19
|
+
const indices = getHighlightedPickedIndices({
|
|
20
|
+
selected,
|
|
21
|
+
highlightedPickedOptions,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (indices.length === 0) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Already a contiguous block flush to the top — nothing to do
|
|
29
|
+
if (indices.every((index, i) => index === i)) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const indexSet = new Set(indices)
|
|
34
|
+
const highlightedBlock = indices.map((index) => selected[index])
|
|
35
|
+
const remaining = selected.filter((_, index) => !indexSet.has(index))
|
|
36
|
+
|
|
37
|
+
onChange({ selected: [...highlightedBlock, ...remaining] })
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getHighlightedPickedIndices } from './get-highlighted-picked-indices.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moves the highlighted picked options up by one slot as a group.
|
|
5
|
+
* If the selection is non-contiguous, the group collapses into a contiguous
|
|
6
|
+
* block (preserving relative order) with its top edge landing at
|
|
7
|
+
* `max(0, topmostHighlightedIndex - 1)`.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} args
|
|
10
|
+
* @param {string[]} args.selected
|
|
11
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
12
|
+
* @param {Function} args.onChange
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export const moveHighlightedPickedOptionUp = ({
|
|
16
|
+
selected,
|
|
17
|
+
highlightedPickedOptions,
|
|
18
|
+
onChange,
|
|
19
|
+
}) => {
|
|
20
|
+
const indices = getHighlightedPickedIndices({
|
|
21
|
+
selected,
|
|
22
|
+
highlightedPickedOptions,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (indices.length === 0) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Already flush to the top — nothing to do
|
|
30
|
+
if (indices.every((index, i) => index === i)) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const indexSet = new Set(indices)
|
|
35
|
+
const highlightedBlock = indices.map((index) => selected[index])
|
|
36
|
+
const remaining = selected.filter((_, index) => !indexSet.has(index))
|
|
37
|
+
const topmost = indices[0]
|
|
38
|
+
const insertPos = Math.max(0, topmost - 1)
|
|
39
|
+
|
|
40
|
+
const reordered = [
|
|
41
|
+
...remaining.slice(0, insertPos),
|
|
42
|
+
...highlightedBlock,
|
|
43
|
+
...remaining.slice(insertPos),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
onChange({ selected: reordered })
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} args
|
|
3
|
+
* @param {Function} args.setHighlightedPickedOptions
|
|
4
|
+
* @param {Function} args.onChange
|
|
5
|
+
* @returns {void}
|
|
6
|
+
*/
|
|
7
|
+
export const removeAllPickedOptions = ({
|
|
8
|
+
setHighlightedPickedOptions,
|
|
9
|
+
onChange,
|
|
10
|
+
}) => {
|
|
11
|
+
setHighlightedPickedOptions([])
|
|
12
|
+
onChange({ selected: [] })
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} args
|
|
3
|
+
* @param {string[]} args.highlightedPickedOptions
|
|
4
|
+
* @param {string[]} args.selected
|
|
5
|
+
* @param {Function} args.setHighlightedPickedOptions
|
|
6
|
+
* @param {Function} args.onChange
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export const removeIndividualPickedOptions = ({
|
|
10
|
+
filterablePicked,
|
|
11
|
+
pickedOptions,
|
|
12
|
+
highlightedPickedOptions,
|
|
13
|
+
onChange,
|
|
14
|
+
selected,
|
|
15
|
+
setHighlightedPickedOptions,
|
|
16
|
+
}) => {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a subset of the highlighted options to reflect a changed
|
|
19
|
+
* filter value in case previously highlighted options are now
|
|
20
|
+
* hidden.
|
|
21
|
+
*
|
|
22
|
+
* This enables us to keep items highlighted while searching for
|
|
23
|
+
* a particular one.
|
|
24
|
+
*
|
|
25
|
+
* With this subset we only select the subset when the user
|
|
26
|
+
* clicks the "add individuals" button
|
|
27
|
+
*/
|
|
28
|
+
const filteredHighlightedPickedOptions = filterablePicked
|
|
29
|
+
? highlightedPickedOptions.filter((value) =>
|
|
30
|
+
pickedOptions.find(
|
|
31
|
+
(filteredOption) => filteredOption.value === value
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
: highlightedPickedOptions
|
|
35
|
+
|
|
36
|
+
const newSelected = selected.filter(
|
|
37
|
+
(selectedOption) =>
|
|
38
|
+
!filteredHighlightedPickedOptions.includes(selectedOption)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
setHighlightedPickedOptions([])
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* as the maximum amount of selected items
|
|
45
|
+
* is already restricted through the selection mechanism,
|
|
46
|
+
* there's no need to handle `maxSelection` here
|
|
47
|
+
*/
|
|
48
|
+
onChange({ selected: newSelected })
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const identity = (value) => value
|
|
4
|
+
|
|
5
|
+
export const useFilter = ({
|
|
6
|
+
initialSearchTerm,
|
|
7
|
+
onFilterChange,
|
|
8
|
+
externalSearchTerm,
|
|
9
|
+
filterable,
|
|
10
|
+
filterCallback,
|
|
11
|
+
}) => {
|
|
12
|
+
const [internalFilter, setInternalFilter] = useState(initialSearchTerm)
|
|
13
|
+
const filterValue = onFilterChange ? externalSearchTerm : internalFilter
|
|
14
|
+
const filter = filterable ? filterCallback : identity
|
|
15
|
+
|
|
16
|
+
return { filterValue, filter, setInternalFilter }
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ADD_MODE, RANGE_MODE } from '../../common/index.js'
|
|
2
|
+
import { toggleAdd } from './toggle-add.js'
|
|
3
|
+
import { toggleRange } from './toggle-range.js'
|
|
4
|
+
import { toggleReplace } from './toggle-replace.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} args
|
|
8
|
+
* @param {bool} args.disabled
|
|
9
|
+
* @param {string[]} args.highlightedOptions
|
|
10
|
+
* @param {Function} args.setHighlightedOptions
|
|
11
|
+
* @param {number} args.maxSelections
|
|
12
|
+
* @param {Function} args.setLastClicked
|
|
13
|
+
* @param {Object[]} args.options
|
|
14
|
+
* @param {string} args.lastClicked
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export const createToggleHighlightedOption =
|
|
18
|
+
({
|
|
19
|
+
disabled,
|
|
20
|
+
highlightedOptions,
|
|
21
|
+
setHighlightedOptions,
|
|
22
|
+
maxSelections,
|
|
23
|
+
setLastClicked,
|
|
24
|
+
options,
|
|
25
|
+
lastClicked,
|
|
26
|
+
}) =>
|
|
27
|
+
({ option, mode }) => {
|
|
28
|
+
if (disabled) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setHighlightedOptions([])
|
|
33
|
+
|
|
34
|
+
if (mode === ADD_MODE) {
|
|
35
|
+
setLastClicked(option.value)
|
|
36
|
+
|
|
37
|
+
return toggleAdd({
|
|
38
|
+
highlightedOptions,
|
|
39
|
+
maxSelections,
|
|
40
|
+
option,
|
|
41
|
+
setHighlightedOptions,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (mode === RANGE_MODE) {
|
|
46
|
+
return toggleRange({
|
|
47
|
+
highlightedOptions,
|
|
48
|
+
options,
|
|
49
|
+
option,
|
|
50
|
+
setHighlightedOptions,
|
|
51
|
+
lastClicked,
|
|
52
|
+
maxSelections,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// REPLACE_MODE
|
|
57
|
+
setLastClicked(option.value)
|
|
58
|
+
|
|
59
|
+
return toggleReplace({
|
|
60
|
+
option,
|
|
61
|
+
highlightedOptions,
|
|
62
|
+
setHighlightedOptions,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { toggleValue } from '../../common/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} args
|
|
5
|
+
* @param {number} args.maxSelections
|
|
6
|
+
* @param {string[]} args.highlightedOptions
|
|
7
|
+
* @param {Object} args.option
|
|
8
|
+
* @param {Function} args.setHighlightedOption
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
export const toggleAdd = ({
|
|
12
|
+
highlightedOptions,
|
|
13
|
+
maxSelections,
|
|
14
|
+
option,
|
|
15
|
+
setHighlightedOptions,
|
|
16
|
+
}) => {
|
|
17
|
+
const afterToggled = toggleValue(highlightedOptions, option.value)
|
|
18
|
+
const capped = afterToggled.slice(-1 * maxSelections)
|
|
19
|
+
setHighlightedOptions(capped)
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { findOptionIndex } from '../../common/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object} args
|
|
5
|
+
* @param {number} args.maxSelections
|
|
6
|
+
* @param {string[]} args.highlightedOptions
|
|
7
|
+
* @param {Object[]} args.options
|
|
8
|
+
* @param {Object} args.option
|
|
9
|
+
* @param {string} args.lastClicked
|
|
10
|
+
* @param {Function} args.setHighlightedOption
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
export const toggleRange = ({
|
|
14
|
+
highlightedOptions,
|
|
15
|
+
options,
|
|
16
|
+
option,
|
|
17
|
+
setHighlightedOptions,
|
|
18
|
+
lastClicked,
|
|
19
|
+
maxSelections,
|
|
20
|
+
}) => {
|
|
21
|
+
if (highlightedOptions.length === 0) {
|
|
22
|
+
setHighlightedOptions([option.value])
|
|
23
|
+
} else {
|
|
24
|
+
let from, to
|
|
25
|
+
|
|
26
|
+
const clickedOptionIndex = findOptionIndex(options, option)
|
|
27
|
+
const lastClickedSourceOptionWithoutRangeModeIndex = lastClicked
|
|
28
|
+
? options.findIndex((curOption) => curOption.value === lastClicked)
|
|
29
|
+
: -1
|
|
30
|
+
|
|
31
|
+
if (lastClickedSourceOptionWithoutRangeModeIndex !== -1) {
|
|
32
|
+
from = lastClickedSourceOptionWithoutRangeModeIndex
|
|
33
|
+
to = clickedOptionIndex
|
|
34
|
+
} else {
|
|
35
|
+
/**
|
|
36
|
+
* A filter-change has removed the most recently highlighted option
|
|
37
|
+
*/
|
|
38
|
+
const firstHighlightedInList = options.findIndex((option) =>
|
|
39
|
+
highlightedOptions.find(
|
|
40
|
+
(highlightedOption) => highlightedOption === option.value
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from = firstHighlightedInList
|
|
45
|
+
to = clickedOptionIndex
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// this is so we can also selected
|
|
49
|
+
// a range of options above "from" option.
|
|
50
|
+
// -> Just how slice works ;)
|
|
51
|
+
const lower = Math.min(from, to)
|
|
52
|
+
const higher = Math.max(from, to)
|
|
53
|
+
const newHighlightedSourceOptions = options
|
|
54
|
+
.slice(lower, higher + 1)
|
|
55
|
+
.filter((option) => !option.disabled)
|
|
56
|
+
.slice(maxSelections * -1)
|
|
57
|
+
.map(({ value }) => value)
|
|
58
|
+
|
|
59
|
+
setHighlightedOptions(newHighlightedSourceOptions)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} args
|
|
3
|
+
* @param {string[]} args.highlightedOptions
|
|
4
|
+
* @param {Object} args.option
|
|
5
|
+
* @param {Function} args.setHighlightedOption
|
|
6
|
+
* @returns {void}
|
|
7
|
+
*/
|
|
8
|
+
export const toggleReplace = ({
|
|
9
|
+
option,
|
|
10
|
+
highlightedOptions,
|
|
11
|
+
setHighlightedOptions,
|
|
12
|
+
}) => {
|
|
13
|
+
if (highlightedOptions.length > 1) {
|
|
14
|
+
setHighlightedOptions([option.value])
|
|
15
|
+
} else {
|
|
16
|
+
const optionIndex = highlightedOptions.findIndex(
|
|
17
|
+
(highlightedOption) => highlightedOption === option.value
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (optionIndex === -1) {
|
|
21
|
+
setHighlightedOptions([option.value])
|
|
22
|
+
} else {
|
|
23
|
+
setHighlightedOptions([])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { createToggleHighlightedOption } from './use-highlighted-options/create-toggle-highlighted-option.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object} args
|
|
6
|
+
* @param {bool} args.disabled
|
|
7
|
+
* @param {number} args.maxSelection
|
|
8
|
+
* @param {Object[]} args.options
|
|
9
|
+
* @returns {Object} highlighted options & helpers
|
|
10
|
+
*/
|
|
11
|
+
export const useHighlightedOptions = ({ disabled, maxSelections, options }) => {
|
|
12
|
+
/**
|
|
13
|
+
* These are important so the stored element can be used
|
|
14
|
+
* as range-start when using shift multiple times consecutively
|
|
15
|
+
*/
|
|
16
|
+
const [lastClicked, setLastClicked] = useState(null)
|
|
17
|
+
const [highlightedOptions, setHighlightedOptions] = useState([])
|
|
18
|
+
|
|
19
|
+
const toggleHighlightedOption = createToggleHighlightedOption({
|
|
20
|
+
disabled,
|
|
21
|
+
highlightedOptions,
|
|
22
|
+
setHighlightedOptions,
|
|
23
|
+
maxSelections,
|
|
24
|
+
setLastClicked,
|
|
25
|
+
options,
|
|
26
|
+
lastClicked,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
highlightedOptions,
|
|
31
|
+
setHighlightedOptions,
|
|
32
|
+
toggleHighlightedOption,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { INTERSECTION_DETECTOR_HEIGHT } from '../end-intersection-detector.js'
|
|
3
|
+
|
|
4
|
+
const isEndIntersectionDetectorWithinScrollBox = (scrollBoxRef, listRef) => {
|
|
5
|
+
if (!scrollBoxRef.current || !listRef.current) {
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
8
|
+
const scrollBoxRect = scrollBoxRef.current.getBoundingClientRect()
|
|
9
|
+
const listRect = listRef.current.getBoundingClientRect()
|
|
10
|
+
return listRect.bottom - scrollBoxRect.bottom < INTERSECTION_DETECTOR_HEIGHT
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useOptionsKeyMonitor = ({
|
|
14
|
+
scrollBoxRef,
|
|
15
|
+
listRef,
|
|
16
|
+
allOptionsKey,
|
|
17
|
+
onEndReached,
|
|
18
|
+
}) => {
|
|
19
|
+
/* Store in ref so this works even if a consumer does not pass a stable
|
|
20
|
+
* function reference */
|
|
21
|
+
const onEndReachedRef = useRef(onEndReached)
|
|
22
|
+
const prevAllOptionsKey = useRef(allOptionsKey)
|
|
23
|
+
onEndReachedRef.current = onEndReached
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
/* When new options are loaded and the list end is (still) in
|
|
27
|
+
* view we need to call onEndReached, because the end of the list
|
|
28
|
+
* has indeed been reached but the interception detector will not pick
|
|
29
|
+
* up on this */
|
|
30
|
+
if (
|
|
31
|
+
onEndReachedRef.current &&
|
|
32
|
+
prevAllOptionsKey.current !== allOptionsKey &&
|
|
33
|
+
isEndIntersectionDetectorWithinScrollBox(scrollBoxRef, listRef)
|
|
34
|
+
) {
|
|
35
|
+
onEndReachedRef.current()
|
|
36
|
+
}
|
|
37
|
+
prevAllOptionsKey.current = allOptionsKey
|
|
38
|
+
/* This effect will only run on mount and when allOptionsKey
|
|
39
|
+
* changes because scrollBoxRef, listRef are stable references */
|
|
40
|
+
}, [scrollBoxRef, listRef, allOptionsKey])
|
|
41
|
+
}
|