webcoreui 1.2.0 → 1.3.0
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 +11 -6
- package/astro.d.ts +3 -0
- package/astro.js +2 -0
- package/components/Badge/Badge.astro +2 -0
- package/components/Badge/Badge.svelte +2 -0
- package/components/Badge/Badge.tsx +2 -0
- package/components/Badge/badge.module.scss +26 -0
- package/components/Badge/badge.ts +1 -0
- package/components/Counter/Counter.astro +164 -0
- package/components/Counter/Counter.svelte +141 -0
- package/components/Counter/Counter.tsx +161 -0
- package/components/Counter/counter.module.scss +155 -0
- package/components/Counter/counter.ts +21 -0
- package/components/DataTable/DataTable.tsx +1 -1
- package/components/Icon/map.ts +2 -0
- package/components/List/List.astro +3 -0
- package/components/List/List.svelte +12 -9
- package/components/List/List.tsx +3 -0
- package/components/List/list.module.scss +5 -0
- package/components/List/list.ts +40 -39
- package/components/Menu/Menu.tsx +1 -1
- package/components/Pagination/Pagination.tsx +1 -1
- package/components/Pagination/pagination.module.scss +1 -0
- package/components/Popover/Popover.astro +28 -26
- package/components/Popover/Popover.svelte +2 -0
- package/components/Popover/Popover.tsx +2 -0
- package/components/Popover/popover.module.scss +6 -0
- package/components/Popover/popover.ts +17 -16
- package/components/Progress/Progress.astro +6 -2
- package/components/Progress/Progress.svelte +6 -2
- package/components/Progress/Progress.tsx +6 -2
- package/components/Progress/progress.module.scss +15 -0
- package/components/Progress/progress.ts +1 -0
- package/components/RangeSlider/RangeSlider.astro +5 -0
- package/components/RangeSlider/rangeslider.ts +1 -0
- package/icons/minus.svg +3 -0
- package/icons.d.ts +1 -0
- package/icons.js +1 -0
- package/package.json +111 -109
- package/react.d.ts +3 -0
- package/react.js +2 -0
- package/svelte.d.ts +3 -0
- package/svelte.js +2 -0
package/README.md
CHANGED
|
@@ -64,18 +64,18 @@ Webcore can be used as a standalone project, or it can be integrated into your e
|
|
|
64
64
|
|
|
65
65
|
Webcore components use Sass for styling. To use the component library, you must have the following packages installed:
|
|
66
66
|
|
|
67
|
-
- [Sass](https://www.npmjs.com/package/sass) - `v1.
|
|
68
|
-
- [TypeScript](https://www.npmjs.com/package/typescript) - `v5.
|
|
67
|
+
- [Sass](https://www.npmjs.com/package/sass) - `v1.94`
|
|
68
|
+
- [TypeScript](https://www.npmjs.com/package/typescript) - `v5.9`
|
|
69
69
|
|
|
70
70
|
Depending on your project setup, you'll also need the following packages:
|
|
71
71
|
|
|
72
72
|
- **For Astro projects**
|
|
73
|
-
- [Astro](https://www.npmjs.com/package/astro) - `v5.
|
|
73
|
+
- [Astro](https://www.npmjs.com/package/astro) - `v5.16`
|
|
74
74
|
- **For Svelte projects**
|
|
75
|
-
- [Svelte](https://www.npmjs.com/package/svelte) - `v5.
|
|
75
|
+
- [Svelte](https://www.npmjs.com/package/svelte) - `v5.45`
|
|
76
76
|
- **For React projects**
|
|
77
|
-
- [React](https://www.npmjs.com/package/react) - `v19.
|
|
78
|
-
- [React DOM](https://www.npmjs.com/package/react-dom) -`v19.
|
|
77
|
+
- [React](https://www.npmjs.com/package/react) - `v19.2`
|
|
78
|
+
- [React DOM](https://www.npmjs.com/package/react-dom) -`v19.2`
|
|
79
79
|
|
|
80
80
|
### Installation with CLI
|
|
81
81
|
|
|
@@ -167,6 +167,9 @@ html body {
|
|
|
167
167
|
--w-collapsible-initial-height: 0;
|
|
168
168
|
--w-collapsible-max-height: 100%;
|
|
169
169
|
|
|
170
|
+
// Counter component
|
|
171
|
+
--w-counter-width: 10ch;
|
|
172
|
+
|
|
170
173
|
// Masonry component
|
|
171
174
|
--w-masonry-gap: 5px;
|
|
172
175
|
|
|
@@ -287,6 +290,7 @@ import { Accordion } from 'webcoreui/react'
|
|
|
287
290
|
- [ConditionalWrapper](https://github.com/Frontendland/webcoreui/tree/main/src/components/ConditionalWrapper)
|
|
288
291
|
- [ContextMenu](https://github.com/Frontendland/webcoreui/tree/main/src/components/ContextMenu)
|
|
289
292
|
- [Copy](https://github.com/Frontendland/webcoreui/tree/main/src/components/Copy)
|
|
293
|
+
- [Counter](https://github.com/Frontendland/webcoreui/tree/main/src/components/Counter)
|
|
290
294
|
- [DataTable](https://github.com/Frontendland/webcoreui/tree/main/src/components/DataTable)
|
|
291
295
|
- [Flex](https://github.com/Frontendland/webcoreui/tree/main/src/components/Flex)
|
|
292
296
|
- [Footer](https://github.com/Frontendland/webcoreui/tree/main/src/components/Footer)
|
|
@@ -332,6 +336,7 @@ import { Accordion } from 'webcoreui/react'
|
|
|
332
336
|
- [BlogCard](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/BlogCard)
|
|
333
337
|
- [ComponentMap](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/ComponentMap)
|
|
334
338
|
- [DeviceMockup](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/DeviceMockup)
|
|
339
|
+
- [Empty](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/Empty)
|
|
335
340
|
- [ErrorPage](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/ErrorPage)
|
|
336
341
|
- [ExpandableTable](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/ExpandableTable)
|
|
337
342
|
- [FAQ](https://github.com/Frontendland/webcoreui/tree/main/src/blocks/FAQ)
|
package/astro.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { CollapsibleProps as WCollapsibleProps } from './components/Collaps
|
|
|
14
14
|
import type { ConditionalWrapperProps as WConditionalWrapperProps } from './components/ConditionalWrapper/conditionalwrapper'
|
|
15
15
|
import type { ContextMenuProps as WContextMenuProps } from './components/ContextMenu/contextmenu'
|
|
16
16
|
import type { CopyProps as WCopyProps } from './components/Copy/copy'
|
|
17
|
+
import type { CounterProps as WCounterProps } from './components/Counter/counter'
|
|
17
18
|
import type { DataTableProps as WDataTableProps } from './components/DataTable/datatable'
|
|
18
19
|
import type { FlexProps as WFlexProps } from './components/Flex/flex'
|
|
19
20
|
import type { FooterProps as WFooterProps } from './components/Footer/footer'
|
|
@@ -75,6 +76,7 @@ declare module 'webcoreui/astro' {
|
|
|
75
76
|
export function ConditionalWrapper(_props: WConditionalWrapperProps): any
|
|
76
77
|
export function ContextMenu(_props: WContextMenuProps): any
|
|
77
78
|
export function Copy(_props: WCopyProps): any
|
|
79
|
+
export function Counter(_props: WCounterProps): any
|
|
78
80
|
export function DataTable(_props: WDataTableProps): any
|
|
79
81
|
export function Flex(_props: WFlexProps): any
|
|
80
82
|
export function Footer(_props: WFooterProps): any
|
|
@@ -129,6 +131,7 @@ declare module 'webcoreui/astro' {
|
|
|
129
131
|
export type ConditionalWrapperProps = WConditionalWrapperProps
|
|
130
132
|
export type ContextMenuProps = WContextMenuProps
|
|
131
133
|
export type CopyProps = WCopyProps
|
|
134
|
+
export type CounterProps = WCounterProps
|
|
132
135
|
export type DataTableProps = WDataTableProps
|
|
133
136
|
export type FlexProps = WFlexProps
|
|
134
137
|
export type FooterProps = WFooterProps
|
package/astro.js
CHANGED
|
@@ -14,6 +14,7 @@ import CollapsibleComponent from './components/Collapsible/Collapsible.astro'
|
|
|
14
14
|
import ConditionalWrapperComponent from './components/ConditionalWrapper/ConditionalWrapper.astro'
|
|
15
15
|
import ContextMenuComponent from './components/ContextMenu/ContextMenu.astro'
|
|
16
16
|
import CopyComponent from './components/Copy/Copy.astro'
|
|
17
|
+
import CounterComponent from './components/Counter/Counter.astro'
|
|
17
18
|
import DataTableComponent from './components/DataTable/DataTable.astro'
|
|
18
19
|
import FlexComponent from './components/Flex/Flex.astro'
|
|
19
20
|
import FooterComponent from './components/Footer/Footer.astro'
|
|
@@ -68,6 +69,7 @@ export const Collapsible = CollapsibleComponent
|
|
|
68
69
|
export const ConditionalWrapper = ConditionalWrapperComponent
|
|
69
70
|
export const ContextMenu = ContextMenuComponent
|
|
70
71
|
export const Copy = CopyComponent
|
|
72
|
+
export const Counter = CounterComponent
|
|
71
73
|
export const DataTable = DataTableComponent
|
|
72
74
|
export const Flex = FlexComponent
|
|
73
75
|
export const Footer = FooterComponent
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
hover,
|
|
11
11
|
small,
|
|
12
12
|
rounded,
|
|
13
|
+
transparent,
|
|
13
14
|
className,
|
|
14
15
|
...rest
|
|
15
16
|
} = Astro.props
|
|
@@ -20,6 +21,7 @@ const classes = [
|
|
|
20
21
|
hover && styles.hover,
|
|
21
22
|
small && styles.small,
|
|
22
23
|
rounded && styles.round,
|
|
24
|
+
transparent && styles.transparent,
|
|
23
25
|
className
|
|
24
26
|
]
|
|
25
27
|
---
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
hover,
|
|
11
11
|
small,
|
|
12
12
|
rounded,
|
|
13
|
+
transparent,
|
|
13
14
|
className,
|
|
14
15
|
onClick,
|
|
15
16
|
children,
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
(onClick || hover) && styles.hover,
|
|
23
24
|
small && styles.small,
|
|
24
25
|
rounded && styles.round,
|
|
26
|
+
transparent && styles.transparent,
|
|
25
27
|
className
|
|
26
28
|
])
|
|
27
29
|
</script>
|
|
@@ -11,6 +11,7 @@ const Badge = ({
|
|
|
11
11
|
hover,
|
|
12
12
|
small,
|
|
13
13
|
rounded,
|
|
14
|
+
transparent,
|
|
14
15
|
className,
|
|
15
16
|
children,
|
|
16
17
|
...rest
|
|
@@ -21,6 +22,7 @@ const Badge = ({
|
|
|
21
22
|
(onClick || hover) && styles.hover,
|
|
22
23
|
small && styles.small,
|
|
23
24
|
rounded && styles.round,
|
|
25
|
+
transparent && styles.transparent,
|
|
24
26
|
className
|
|
25
27
|
])
|
|
26
28
|
|
|
@@ -91,4 +91,30 @@
|
|
|
91
91
|
&.round {
|
|
92
92
|
@include border-radius(lg);
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
&.transparent {
|
|
96
|
+
@include background(transparent);
|
|
97
|
+
@include typography(primary);
|
|
98
|
+
@include spacing(p0);
|
|
99
|
+
|
|
100
|
+
&.secondary {
|
|
101
|
+
@include typography(primary-20);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&.info {
|
|
105
|
+
@include typography(info);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&.success {
|
|
109
|
+
@include typography(success);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&.warning {
|
|
113
|
+
@include typography(warning);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
&.alert {
|
|
117
|
+
@include typography(alert);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
94
120
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CounterProps } from './counter'
|
|
3
|
+
|
|
4
|
+
import Icon from '../Icon/Icon.astro'
|
|
5
|
+
|
|
6
|
+
import minusIcon from '../../icons/minus.svg?raw'
|
|
7
|
+
import plusIcon from '../../icons/plus.svg?raw'
|
|
8
|
+
|
|
9
|
+
import styles from './counter.module.scss'
|
|
10
|
+
|
|
11
|
+
interface Props extends CounterProps {}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
type = 'compact',
|
|
15
|
+
theme,
|
|
16
|
+
rounded,
|
|
17
|
+
minIcon,
|
|
18
|
+
maxIcon,
|
|
19
|
+
className,
|
|
20
|
+
width,
|
|
21
|
+
value = 0,
|
|
22
|
+
disabled,
|
|
23
|
+
...rest
|
|
24
|
+
} = Astro.props
|
|
25
|
+
|
|
26
|
+
const classes = [
|
|
27
|
+
styles.counter,
|
|
28
|
+
styles[type],
|
|
29
|
+
theme && styles[theme],
|
|
30
|
+
rounded && styles.rounded,
|
|
31
|
+
className
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const subtractIcon = minIcon || minusIcon
|
|
35
|
+
const addIcon = maxIcon || plusIcon
|
|
36
|
+
|
|
37
|
+
const styleVariable = width
|
|
38
|
+
? `--w-counter-width: ${width};`
|
|
39
|
+
: null
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
<div class:list={classes} data-id="w-counter" style={styleVariable}>
|
|
43
|
+
<button data-id="w-counter-min" disabled={disabled}>
|
|
44
|
+
<Fragment>
|
|
45
|
+
{subtractIcon.startsWith('<svg')
|
|
46
|
+
? <Fragment set:html={subtractIcon} />
|
|
47
|
+
: <Icon type={subtractIcon} />
|
|
48
|
+
}
|
|
49
|
+
</Fragment>
|
|
50
|
+
</button>
|
|
51
|
+
<input
|
|
52
|
+
type="number"
|
|
53
|
+
value={value}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
{...rest}
|
|
56
|
+
/>
|
|
57
|
+
<button data-id="w-counter-max" disabled={disabled}>
|
|
58
|
+
<Fragment>
|
|
59
|
+
{addIcon.startsWith('<svg')
|
|
60
|
+
? <Fragment set:html={addIcon} />
|
|
61
|
+
: <Icon type={addIcon} />
|
|
62
|
+
}
|
|
63
|
+
</Fragment>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
import { off, on } from '../../utils/DOMUtils'
|
|
69
|
+
import { dispatch } from '../../utils/event'
|
|
70
|
+
|
|
71
|
+
const addEventListeners = () => {
|
|
72
|
+
const buttonSelector = '[data-id="w-counter"] button'
|
|
73
|
+
const inputSelector = '[data-id="w-counter"] input'
|
|
74
|
+
const eventName = 'counterOnChange'
|
|
75
|
+
|
|
76
|
+
let intervalId: ReturnType<typeof setTimeout>
|
|
77
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
78
|
+
let longPressDelay = 500
|
|
79
|
+
let isKeyDown = false
|
|
80
|
+
|
|
81
|
+
const updateValue = (input: HTMLInputElement, min?: boolean) => {
|
|
82
|
+
const step = input.step ? Number(input.step) : 1
|
|
83
|
+
const direction = min ? -1 : 1
|
|
84
|
+
const newValue = Number(input.value) + (direction * step)
|
|
85
|
+
|
|
86
|
+
if ((input.min && newValue < Number(input.min)) || (input.max && newValue > Number(input.max))) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
input.value = String(newValue)
|
|
91
|
+
|
|
92
|
+
dispatch(eventName, {
|
|
93
|
+
name: input.name,
|
|
94
|
+
value: newValue
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const startHold = (event: Event) => {
|
|
99
|
+
const target = event.currentTarget
|
|
100
|
+
|
|
101
|
+
if (target instanceof HTMLButtonElement && target.parentElement) {
|
|
102
|
+
const input = target.parentElement.querySelector('input') as HTMLInputElement
|
|
103
|
+
const min = target.dataset.id === 'w-counter-min'
|
|
104
|
+
|
|
105
|
+
updateValue(input, min)
|
|
106
|
+
|
|
107
|
+
timeoutId = setTimeout(function repeat() {
|
|
108
|
+
updateValue(input, min)
|
|
109
|
+
|
|
110
|
+
longPressDelay = Math.max(50, longPressDelay * 0.8)
|
|
111
|
+
|
|
112
|
+
intervalId = setTimeout(repeat, longPressDelay)
|
|
113
|
+
}, 500)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const stopHold = () => {
|
|
118
|
+
clearTimeout(timeoutId)
|
|
119
|
+
clearTimeout(intervalId)
|
|
120
|
+
|
|
121
|
+
isKeyDown = false
|
|
122
|
+
longPressDelay = 500
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
on(buttonSelector, 'mousedown', startHold, true)
|
|
126
|
+
on(buttonSelector, 'touchstart', startHold, true)
|
|
127
|
+
|
|
128
|
+
on(buttonSelector, 'mouseup', stopHold, true)
|
|
129
|
+
on(buttonSelector, 'mouseleave', stopHold, true)
|
|
130
|
+
on(buttonSelector, 'touchend', stopHold, true)
|
|
131
|
+
on(buttonSelector, 'touchcancel', stopHold, true)
|
|
132
|
+
|
|
133
|
+
on(buttonSelector, 'keydown', (event: KeyboardEvent) => {
|
|
134
|
+
if (event.key === 'Enter' && !isKeyDown) {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
startHold(event)
|
|
137
|
+
|
|
138
|
+
isKeyDown = true
|
|
139
|
+
}
|
|
140
|
+
}, true)
|
|
141
|
+
|
|
142
|
+
on(buttonSelector, 'keyup', (event: KeyboardEvent) => {
|
|
143
|
+
if (event.key === 'Enter') {
|
|
144
|
+
stopHold()
|
|
145
|
+
}
|
|
146
|
+
}, true)
|
|
147
|
+
|
|
148
|
+
on(inputSelector, 'input', (event: Event) => {
|
|
149
|
+
const target = event.target
|
|
150
|
+
|
|
151
|
+
if (target instanceof HTMLInputElement) {
|
|
152
|
+
dispatch(eventName, {
|
|
153
|
+
name: target.name,
|
|
154
|
+
value: Number(target.value)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}, true)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
off(document, 'astro:after-swap', addEventListeners)
|
|
161
|
+
on(document, 'astro:after-swap', addEventListeners)
|
|
162
|
+
|
|
163
|
+
addEventListeners()
|
|
164
|
+
</script>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SvelteCounterProps } from './counter'
|
|
3
|
+
|
|
4
|
+
import { classNames } from '../../utils/classNames'
|
|
5
|
+
|
|
6
|
+
import minusIcon from '../../icons/minus.svg?raw'
|
|
7
|
+
import plusIcon from '../../icons/plus.svg?raw'
|
|
8
|
+
|
|
9
|
+
import styles from './counter.module.scss'
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
type = 'compact',
|
|
13
|
+
theme,
|
|
14
|
+
rounded,
|
|
15
|
+
minIcon,
|
|
16
|
+
maxIcon,
|
|
17
|
+
className,
|
|
18
|
+
width,
|
|
19
|
+
value = $bindable(0),
|
|
20
|
+
disabled,
|
|
21
|
+
onChange,
|
|
22
|
+
step = 1,
|
|
23
|
+
min,
|
|
24
|
+
max,
|
|
25
|
+
...rest
|
|
26
|
+
}: SvelteCounterProps = $props()
|
|
27
|
+
|
|
28
|
+
const classes = classNames([
|
|
29
|
+
styles.counter,
|
|
30
|
+
styles[type],
|
|
31
|
+
theme && styles[theme],
|
|
32
|
+
rounded && styles.rounded,
|
|
33
|
+
className
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
const subtractIcon = minIcon || minusIcon
|
|
37
|
+
const addIcon = maxIcon || plusIcon
|
|
38
|
+
|
|
39
|
+
const styleVariable = width
|
|
40
|
+
? `--w-counter-width: ${width};`
|
|
41
|
+
: null
|
|
42
|
+
|
|
43
|
+
let intervalId: ReturnType<typeof setTimeout>
|
|
44
|
+
let timeoutId: ReturnType<typeof setTimeout>
|
|
45
|
+
let longPressDelay = 500
|
|
46
|
+
let isKeyDown = false
|
|
47
|
+
|
|
48
|
+
const updateValue = (isMin?: boolean) => {
|
|
49
|
+
const direction = isMin ? -1 : 1
|
|
50
|
+
const newValue = value + (direction * step)
|
|
51
|
+
|
|
52
|
+
if ((min !== undefined && newValue < min) || (max !== undefined && newValue > max)) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
value = newValue
|
|
57
|
+
|
|
58
|
+
onChange?.(newValue)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const startHold = (event: Event) => {
|
|
62
|
+
const target = event.currentTarget
|
|
63
|
+
|
|
64
|
+
if (target instanceof HTMLButtonElement) {
|
|
65
|
+
const isMin = target.dataset.id === 'w-counter-min'
|
|
66
|
+
|
|
67
|
+
updateValue(isMin)
|
|
68
|
+
|
|
69
|
+
timeoutId = setTimeout(function repeat() {
|
|
70
|
+
updateValue(isMin)
|
|
71
|
+
|
|
72
|
+
longPressDelay = Math.max(50, longPressDelay * 0.8)
|
|
73
|
+
intervalId = setTimeout(repeat, longPressDelay)
|
|
74
|
+
}, 500)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stopHold = () => {
|
|
79
|
+
clearTimeout(timeoutId)
|
|
80
|
+
clearTimeout(intervalId)
|
|
81
|
+
|
|
82
|
+
isKeyDown = false
|
|
83
|
+
longPressDelay = 500
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
87
|
+
if (event.key === 'Enter' && !isKeyDown) {
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
startHold(event)
|
|
90
|
+
|
|
91
|
+
isKeyDown = true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
96
|
+
if (event.key === 'Enter') {
|
|
97
|
+
stopHold()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<div class={classes} style={styleVariable}>
|
|
103
|
+
<button
|
|
104
|
+
data-id="w-counter-min"
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
onmousedown={startHold}
|
|
107
|
+
ontouchstart={startHold}
|
|
108
|
+
onmouseup={stopHold}
|
|
109
|
+
onmouseleave={stopHold}
|
|
110
|
+
ontouchend={stopHold}
|
|
111
|
+
ontouchcancel={stopHold}
|
|
112
|
+
onkeydown={handleKeyDown}
|
|
113
|
+
onkeyup={handleKeyUp}
|
|
114
|
+
>
|
|
115
|
+
{@html subtractIcon}
|
|
116
|
+
</button>
|
|
117
|
+
<input
|
|
118
|
+
bind:value={value}
|
|
119
|
+
type="number"
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
step={step}
|
|
122
|
+
min={min}
|
|
123
|
+
max={max}
|
|
124
|
+
oninput={() => onChange?.(value)}
|
|
125
|
+
{...rest}
|
|
126
|
+
/>
|
|
127
|
+
<button
|
|
128
|
+
data-id="w-counter-max"
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
onmousedown={startHold}
|
|
131
|
+
ontouchstart={startHold}
|
|
132
|
+
onmouseup={stopHold}
|
|
133
|
+
onmouseleave={stopHold}
|
|
134
|
+
ontouchend={stopHold}
|
|
135
|
+
ontouchcancel={stopHold}
|
|
136
|
+
onkeydown={handleKeyDown}
|
|
137
|
+
onkeyup={handleKeyUp}
|
|
138
|
+
>
|
|
139
|
+
{@html addIcon}
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react'
|
|
2
|
+
import type { ReactCounterProps } from './counter'
|
|
3
|
+
|
|
4
|
+
import { classNames } from '../../utils/classNames'
|
|
5
|
+
|
|
6
|
+
import minusIcon from '../../icons/minus.svg?raw'
|
|
7
|
+
import plusIcon from '../../icons/plus.svg?raw'
|
|
8
|
+
|
|
9
|
+
import styles from './counter.module.scss'
|
|
10
|
+
|
|
11
|
+
const Counter = ({
|
|
12
|
+
type = 'compact',
|
|
13
|
+
theme,
|
|
14
|
+
rounded,
|
|
15
|
+
minIcon,
|
|
16
|
+
maxIcon,
|
|
17
|
+
className,
|
|
18
|
+
width,
|
|
19
|
+
value = 0,
|
|
20
|
+
disabled,
|
|
21
|
+
onChange,
|
|
22
|
+
step = 1,
|
|
23
|
+
min,
|
|
24
|
+
max,
|
|
25
|
+
...rest
|
|
26
|
+
}: ReactCounterProps) => {
|
|
27
|
+
const [inputValue, setInputValue] = useState(value)
|
|
28
|
+
const intervalId = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
29
|
+
const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
30
|
+
const longPressDelay = useRef(500)
|
|
31
|
+
const isKeyDown = useRef(false)
|
|
32
|
+
|
|
33
|
+
const classes = classNames([
|
|
34
|
+
styles.counter,
|
|
35
|
+
styles[type],
|
|
36
|
+
theme && styles[theme],
|
|
37
|
+
rounded && styles.rounded,
|
|
38
|
+
className
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
const subtractIcon = minIcon || minusIcon
|
|
42
|
+
const addIcon = maxIcon || plusIcon
|
|
43
|
+
|
|
44
|
+
const styleVariable = width
|
|
45
|
+
? { '--w-counter-width': width } as React.CSSProperties
|
|
46
|
+
: undefined
|
|
47
|
+
|
|
48
|
+
const updateValue = (isMin?: boolean) => {
|
|
49
|
+
setInputValue((prevValue: number) => {
|
|
50
|
+
const direction = isMin ? -1 : 1
|
|
51
|
+
const newValue = prevValue + (direction * step)
|
|
52
|
+
|
|
53
|
+
if ((min !== undefined && newValue < min) || (max !== undefined && newValue > max)) {
|
|
54
|
+
return prevValue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onChange?.(newValue)
|
|
58
|
+
return newValue
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const startHold = (event: React.MouseEvent | React.TouchEvent | React.KeyboardEvent) => {
|
|
63
|
+
const target = event.currentTarget
|
|
64
|
+
|
|
65
|
+
if (target instanceof HTMLButtonElement) {
|
|
66
|
+
const isMin = target.dataset.id === 'w-counter-min'
|
|
67
|
+
|
|
68
|
+
updateValue(isMin)
|
|
69
|
+
|
|
70
|
+
timeoutId.current = setTimeout(function repeat() {
|
|
71
|
+
updateValue(isMin)
|
|
72
|
+
|
|
73
|
+
longPressDelay.current = Math.max(50, longPressDelay.current * 0.8)
|
|
74
|
+
intervalId.current = setTimeout(repeat, longPressDelay.current)
|
|
75
|
+
}, 500)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const stopHold = () => {
|
|
80
|
+
if (timeoutId.current) {
|
|
81
|
+
clearTimeout(timeoutId.current)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (intervalId.current) {
|
|
85
|
+
clearTimeout(intervalId.current)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
isKeyDown.current = false
|
|
89
|
+
longPressDelay.current = 500
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
93
|
+
if (event.key === 'Enter' && !isKeyDown.current) {
|
|
94
|
+
event.preventDefault()
|
|
95
|
+
startHold(event)
|
|
96
|
+
|
|
97
|
+
isKeyDown.current = true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handleKeyUp = (event: React.KeyboardEvent) => {
|
|
102
|
+
if (event.key === 'Enter') {
|
|
103
|
+
stopHold()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleInput = (event: React.FormEvent) => {
|
|
108
|
+
const target = event.target
|
|
109
|
+
|
|
110
|
+
if (target instanceof HTMLInputElement) {
|
|
111
|
+
const newValue = Number(target.value)
|
|
112
|
+
|
|
113
|
+
setInputValue(newValue)
|
|
114
|
+
onChange?.(newValue)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className={classes} style={styleVariable}>
|
|
120
|
+
<button
|
|
121
|
+
data-id="w-counter-min"
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
onMouseDown={startHold}
|
|
124
|
+
onTouchStart={startHold}
|
|
125
|
+
onMouseUp={stopHold}
|
|
126
|
+
onMouseLeave={stopHold}
|
|
127
|
+
onTouchEnd={stopHold}
|
|
128
|
+
onTouchCancel={stopHold}
|
|
129
|
+
onKeyDown={handleKeyDown}
|
|
130
|
+
onKeyUp={handleKeyUp}
|
|
131
|
+
dangerouslySetInnerHTML={{ __html: subtractIcon }}
|
|
132
|
+
/>
|
|
133
|
+
<input
|
|
134
|
+
value={inputValue}
|
|
135
|
+
type="number"
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
step={step}
|
|
138
|
+
min={min}
|
|
139
|
+
max={max}
|
|
140
|
+
onInput={handleInput}
|
|
141
|
+
{...rest}
|
|
142
|
+
/>
|
|
143
|
+
<button
|
|
144
|
+
data-id="w-counter-max"
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
onMouseDown={startHold}
|
|
147
|
+
onTouchStart={startHold}
|
|
148
|
+
onMouseUp={stopHold}
|
|
149
|
+
onMouseLeave={stopHold}
|
|
150
|
+
onTouchEnd={stopHold}
|
|
151
|
+
onTouchCancel={stopHold}
|
|
152
|
+
onKeyDown={handleKeyDown}
|
|
153
|
+
onKeyUp={handleKeyUp}
|
|
154
|
+
dangerouslySetInnerHTML={{ __html: addIcon }}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default Counter
|