@startupjs-ui/rank 0.1.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/CHANGELOG.md +20 -0
- package/README.mdx +70 -0
- package/helpers.ts +25 -0
- package/index.cssx.styl +42 -0
- package/index.d.ts +21 -0
- package/index.tsx +162 -0
- package/package.json +24 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/rank
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
|
20
|
+
* **rank:** refactor Rank component ([f38b0df](https://github.com/startupjs/startupjs-ui/commit/f38b0dfb212fa77efe1fa0e52b2b60d1ff4f7c95))
|
package/README.mdx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import Rank, { _PropsJsonSchema as RankPropsJsonSchema } from './index'
|
|
3
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
4
|
+
|
|
5
|
+
# Rank
|
|
6
|
+
|
|
7
|
+
Rank input allows user to arrange options in specific order.
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
import { Rank } from 'startupjs-ui'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Simple example
|
|
14
|
+
|
|
15
|
+
```jsx example
|
|
16
|
+
const options = ['option1', 'option2', 'option3', 'option4', 'option5']
|
|
17
|
+
const [value, setValue] = useState(options)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Rank
|
|
21
|
+
value={value}
|
|
22
|
+
options={options}
|
|
23
|
+
onChange={setValue}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Disabled
|
|
29
|
+
|
|
30
|
+
```jsx example
|
|
31
|
+
const options = ['option1', 'option2', 'option3', 'option4', 'option5']
|
|
32
|
+
const [value, setValue] = useState(options)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Rank
|
|
36
|
+
disabled
|
|
37
|
+
value={value}
|
|
38
|
+
options={options}
|
|
39
|
+
onChange={setValue}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Readonly
|
|
45
|
+
|
|
46
|
+
```jsx example
|
|
47
|
+
const options = ['option1', 'option2', 'option3', 'option4', 'option5']
|
|
48
|
+
const [value, setValue] = useState(options)
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Rank
|
|
52
|
+
readonly
|
|
53
|
+
value={value}
|
|
54
|
+
options={options}
|
|
55
|
+
onChange={setValue}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Sandbox
|
|
61
|
+
|
|
62
|
+
<Sandbox
|
|
63
|
+
Component={Rank}
|
|
64
|
+
propsJsonSchema={RankPropsJsonSchema}
|
|
65
|
+
props={{
|
|
66
|
+
options: ['option1', 'option2', 'option3', 'option4', 'option5'],
|
|
67
|
+
onChange: value => alert('New order: ' + JSON.stringify(value))
|
|
68
|
+
}}
|
|
69
|
+
block
|
|
70
|
+
/>
|
package/helpers.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function stringifyValue (option: any): string {
|
|
2
|
+
try {
|
|
3
|
+
const value = getOptionValue(option)
|
|
4
|
+
return JSON.stringify(value)
|
|
5
|
+
} catch (err) {
|
|
6
|
+
console.error(err)
|
|
7
|
+
return ''
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getOptionValue (option: any): any {
|
|
12
|
+
return option?.value ?? option
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getOptionLabel (option: any): any {
|
|
16
|
+
return option?.label ?? option
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function move<T> (arr: T[], oldIndex: number, newIndex: number): T[] {
|
|
20
|
+
const arrCopy = arr.slice()
|
|
21
|
+
const element = arrCopy[oldIndex]
|
|
22
|
+
arrCopy.splice(oldIndex, 1)
|
|
23
|
+
arrCopy.splice(newIndex, 0, element)
|
|
24
|
+
return arrCopy
|
|
25
|
+
}
|
package/index.cssx.styl
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.droppable
|
|
2
|
+
margin-top 1u
|
|
3
|
+
|
|
4
|
+
.droppable
|
|
5
|
+
.span
|
|
6
|
+
flex-shrink 1
|
|
7
|
+
flex-grow 1
|
|
8
|
+
|
|
9
|
+
.readonly
|
|
10
|
+
&-index
|
|
11
|
+
min-width 2.5u
|
|
12
|
+
&-text
|
|
13
|
+
flex-shrink 1
|
|
14
|
+
flex-grow 1
|
|
15
|
+
|
|
16
|
+
.draggable
|
|
17
|
+
padding 1u
|
|
18
|
+
border-width 1px
|
|
19
|
+
radius()
|
|
20
|
+
margin-top 1u
|
|
21
|
+
|
|
22
|
+
.cursor
|
|
23
|
+
+web()
|
|
24
|
+
cursor pointer
|
|
25
|
+
|
|
26
|
+
.span
|
|
27
|
+
margin-left 2.5u
|
|
28
|
+
justify-content center
|
|
29
|
+
|
|
30
|
+
.right
|
|
31
|
+
flex-shrink 0
|
|
32
|
+
justify-content center
|
|
33
|
+
margin-left 4u
|
|
34
|
+
|
|
35
|
+
.icon
|
|
36
|
+
&.disabled
|
|
37
|
+
color var(--color-text-subtle)
|
|
38
|
+
|
|
39
|
+
.hint
|
|
40
|
+
font(caption)
|
|
41
|
+
margin-top 1u
|
|
42
|
+
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
5
|
+
export declare const _PropsJsonSchema: {};
|
|
6
|
+
export interface RankProps {
|
|
7
|
+
/** Custom styles applied to the root view */
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
/** Options list (strings, numbers, or `{ value, label }` objects) @default [] */
|
|
10
|
+
options?: any[];
|
|
11
|
+
/** Current order as a list of option values @default [] */
|
|
12
|
+
value?: any[];
|
|
13
|
+
/** Disable drag-and-drop interactions @default false */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Render a non-interactive ranked list @default false */
|
|
16
|
+
readonly?: boolean;
|
|
17
|
+
/** Fired when order changes; receives array of option values */
|
|
18
|
+
onChange?: (value: any[]) => void;
|
|
19
|
+
}
|
|
20
|
+
declare const _default: import("react").ComponentType<RankProps>;
|
|
21
|
+
export default _default;
|
package/index.tsx
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from 'react'
|
|
2
|
+
import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { pug, observer, $ } from 'startupjs'
|
|
4
|
+
import { themed, useColors } from '@startupjs-ui/core'
|
|
5
|
+
import { DragDropProvider, Draggable, Droppable } from '@startupjs-ui/draggable'
|
|
6
|
+
import Div from '@startupjs-ui/div'
|
|
7
|
+
import Icon from '@startupjs-ui/icon'
|
|
8
|
+
import Select from '@startupjs-ui/select'
|
|
9
|
+
import Span from '@startupjs-ui/span'
|
|
10
|
+
import { faGripVertical } from '@fortawesome/free-solid-svg-icons/faGripVertical'
|
|
11
|
+
import { getOptionLabel, getOptionValue, stringifyValue, move } from './helpers'
|
|
12
|
+
import STYLES from './index.cssx.styl'
|
|
13
|
+
|
|
14
|
+
export const _PropsJsonSchema = {/* RankProps */} // used in docs generation
|
|
15
|
+
|
|
16
|
+
export interface RankProps {
|
|
17
|
+
/** Custom styles applied to the root view */
|
|
18
|
+
style?: StyleProp<ViewStyle>
|
|
19
|
+
/** Options list (strings, numbers, or `{ value, label }` objects) @default [] */
|
|
20
|
+
options?: any[]
|
|
21
|
+
/** Current order as a list of option values @default [] */
|
|
22
|
+
value?: any[]
|
|
23
|
+
/** Disable drag-and-drop interactions @default false */
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
/** Render a non-interactive ranked list @default false */
|
|
26
|
+
readonly?: boolean
|
|
27
|
+
/** Fired when order changes; receives array of option values */
|
|
28
|
+
onChange?: (value: any[]) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Rank (props: RankProps): ReactNode {
|
|
32
|
+
const {
|
|
33
|
+
options = [],
|
|
34
|
+
readonly = false,
|
|
35
|
+
value = [],
|
|
36
|
+
style
|
|
37
|
+
} = props
|
|
38
|
+
|
|
39
|
+
const sortedValue = useMemo(() => {
|
|
40
|
+
return options.slice().sort((a, b) => {
|
|
41
|
+
return value.findIndex(i => stringifyValue(i) === stringifyValue(a)) -
|
|
42
|
+
value.findIndex(i => stringifyValue(i) === stringifyValue(b))
|
|
43
|
+
})
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, [value.toString()])
|
|
46
|
+
|
|
47
|
+
return pug`
|
|
48
|
+
if readonly
|
|
49
|
+
RankReadonly(value=sortedValue style=style)
|
|
50
|
+
else
|
|
51
|
+
RankInput(...props value=sortedValue)
|
|
52
|
+
`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const RankInput = observer(function RankInput ({
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
disabled,
|
|
59
|
+
style
|
|
60
|
+
}: {
|
|
61
|
+
value: any[]
|
|
62
|
+
onChange?: (value: any[]) => void
|
|
63
|
+
disabled?: boolean
|
|
64
|
+
style?: StyleProp<ViewStyle>
|
|
65
|
+
}): ReactNode {
|
|
66
|
+
const $width: any = $()
|
|
67
|
+
const dropId = useMemo(() => $.id(), [])
|
|
68
|
+
|
|
69
|
+
const getColor = useColors()
|
|
70
|
+
|
|
71
|
+
const selectOptions = useMemo(() => {
|
|
72
|
+
return value.map((_o, i) => ({ label: i + 1, value: i }))
|
|
73
|
+
}, [value])
|
|
74
|
+
|
|
75
|
+
function onLayout (event: any) {
|
|
76
|
+
$width.set(event.nativeEvent.layout.width)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onMoveItem (oldIndex: number, newIndex: number) {
|
|
80
|
+
const newItems = move(value, oldIndex, newIndex)
|
|
81
|
+
onChange && onChange(newItems.map(i => getOptionValue(i)))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onDragEnd ({ dragId, hoverIndex }: { dragId: string, hoverIndex: number }) {
|
|
85
|
+
const oldIndex = value.findIndex(item => stringifyValue(item) === dragId)
|
|
86
|
+
const newIndex = hoverIndex < oldIndex ? hoverIndex : hoverIndex - 1
|
|
87
|
+
onMoveItem(oldIndex, newIndex)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderDragItem (item: any, index: number): ReactNode {
|
|
91
|
+
const dragId = stringifyValue(item)
|
|
92
|
+
|
|
93
|
+
function onSelectChange (newIndex: any) {
|
|
94
|
+
onMoveItem(index, newIndex)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// HACK: Draggable component has some visual bugs if styles are not passed
|
|
98
|
+
// through style object
|
|
99
|
+
const extraStyle: any = disabled ? { backgroundColor: getColor('bg-main-subtle') } : STYLES.cursor
|
|
100
|
+
const itemStyle: any = [
|
|
101
|
+
{ ...STYLES.draggable, backgroundColor: getColor('bg-main-strong'), borderColor: getColor('border-main') },
|
|
102
|
+
{ width: $width.get() },
|
|
103
|
+
extraStyle
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
const Container: any = disabled ? Div : Draggable
|
|
107
|
+
|
|
108
|
+
return pug`
|
|
109
|
+
Container(
|
|
110
|
+
style=StyleSheet.flatten(itemStyle)
|
|
111
|
+
key=dragId
|
|
112
|
+
dragId=dragId
|
|
113
|
+
onDragEnd=onDragEnd
|
|
114
|
+
)
|
|
115
|
+
Div(row)
|
|
116
|
+
Select(
|
|
117
|
+
size='s'
|
|
118
|
+
disabled=disabled
|
|
119
|
+
showEmptyValue=false
|
|
120
|
+
options=selectOptions
|
|
121
|
+
value=index
|
|
122
|
+
onChange=onSelectChange
|
|
123
|
+
)
|
|
124
|
+
Div.span
|
|
125
|
+
Span= getOptionLabel(item)
|
|
126
|
+
Div.right
|
|
127
|
+
Icon.icon(icon=faGripVertical styleName={ disabled })
|
|
128
|
+
`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return pug`
|
|
132
|
+
Div(
|
|
133
|
+
style=style
|
|
134
|
+
onLayout=onLayout
|
|
135
|
+
)
|
|
136
|
+
Span.hint(italic) To rank the listed items drag and drop each item
|
|
137
|
+
DragDropProvider
|
|
138
|
+
Droppable.droppable(dropId=dropId)
|
|
139
|
+
each option, index in value
|
|
140
|
+
= renderDragItem(option, index)
|
|
141
|
+
`
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const RankReadonly = observer(function RankReadonly ({
|
|
145
|
+
value,
|
|
146
|
+
style
|
|
147
|
+
}: {
|
|
148
|
+
value: any[]
|
|
149
|
+
style?: StyleProp<ViewStyle>
|
|
150
|
+
}): ReactNode {
|
|
151
|
+
return pug`
|
|
152
|
+
Div(style=style)
|
|
153
|
+
each option, index in value
|
|
154
|
+
Div.readonly(key=index row)
|
|
155
|
+
Div.readonly-index
|
|
156
|
+
Span #{index + 1}.
|
|
157
|
+
Div.readonly-text
|
|
158
|
+
Span= getOptionLabel(option)
|
|
159
|
+
`
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export default observer(themed('Rank', Rank))
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/rank",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
12
|
+
"@startupjs-ui/div": "^0.1.3",
|
|
13
|
+
"@startupjs-ui/draggable": "^0.1.3",
|
|
14
|
+
"@startupjs-ui/icon": "^0.1.3",
|
|
15
|
+
"@startupjs-ui/select": "^0.1.3",
|
|
16
|
+
"@startupjs-ui/span": "^0.1.3"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": "*",
|
|
20
|
+
"react-native": "*",
|
|
21
|
+
"startupjs": "*"
|
|
22
|
+
},
|
|
23
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
24
|
+
}
|