1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
-- | The RangeSlider is a slider component with two knobs, allowing
-- | the user to specify both a minimum and maximum value to filter
-- | data by. It may be dragged with the mouse or moved with the
-- | keyboard like a regular slider component. The RangeSlider is
-- | designed to let the user adjust in multiples of a provided
-- | epsilon (smallest difference)
module Gargantext.Components.RangeSlider where
import Data.Generic.Rep (class Generic)
import Data.Eq.Generic (genericEq)
import Data.Foldable (maximum)
import Data.Int (fromNumber)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Number as DN
import Data.Number.Format as DNF
import Data.Nullable (Nullable, null)
import Data.Traversable (traverse_)
import DOM.Simple as DOM
import DOM.Simple.Document (document)
import DOM.Simple.Event as Event
import DOM.Simple.EventListener as EL
import DOM.Simple (DOMRect)
import Effect (Effect)
import Effect.Debouncing as Debounce
import Reactix as R
import Reactix.DOM.HTML as H
import Toestand as T
import Gargantext.Prelude
import Gargantext.Components.Bootstrap.Types (ComponentStatus(..))
import Gargantext.Utils.Math (roundToMultiple)
import Gargantext.Utils.Range as Range
import Gargantext.Utils.Reactix as R2
here :: R2.Here
here = R2.here "Gargantext.Components.RangeSlider"
-- data Axis = X | Y
type Bounds = Range.NumberRange
type Epsilon = Number
-- To avoid overloading the terms 'min' and 'max' here, we treat 'min'
-- and 'max' as being the bounds of the scale and 'low' and 'high' as
-- being the selected values
type Props =
( bounds :: Bounds -- The minimum and maximum values it is possible to select
, initialValue :: Range.NumberRange -- The user's selection of minimum and maximum values
, epsilon :: Number -- The smallest possible change (for mouse)
, step :: Number -- The 'standard' change (for keyboard)
-- , axis :: Axis -- Which direction to move in
, width :: Number
, height :: Number
, onChange :: Range.NumberRange -> Effect Unit
, throttleInterval :: Maybe Int -- then Nothing, no throttling is done
, status :: ComponentStatus )
data Knob = MinKnob | MaxKnob
derive instance Generic Knob _
instance Eq Knob where
eq = genericEq
data RangeUpdate = SetMin Number | SetMax Number
rangeSlider :: Record Props -> R.Element
rangeSlider props = R.createElement rangeSliderCpt props []
rangeSliderCpt :: R.Component Props
rangeSliderCpt = here.component "rangeSlider" cpt where
cpt props _ = do
let throttled = Debounce.throttleWithDebounceAtEnd props.onChange (fromMaybe 0 props.throttleInterval)
let onChangeThrottled = case props.throttleInterval of
Nothing -> props.onChange
Just ti -> \rng -> Debounce.call throttled rng
-- rounding precision (i.e. how many decimal digits are in epsilon)
let (Range.Closed { min: minR, max: maxR }) = props.initialValue
let decPrecision num =
-- int digits
(fromMaybe 0 $ fromNumber $ DN.ceil $ (DN.log num) / DN.ln10)
-- float digits
+ (fromMaybe 0 $ fromNumber $ DN.ceil $ -(DN.log (num - (DN.floor num))) / DN.ln10)
let epsilonPrecision = decPrecision props.epsilon
let minPrecision = decPrecision minR
let maxPrecision = decPrecision maxR
--let precision = fromMaybe 0 $ fromNumber $ max 0.0 epsilonPrecision
let precision = fromMaybe 0 $ maximum [0, epsilonPrecision, minPrecision, maxPrecision]
-- scale bar
scaleElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- dom ref
-- scale sel bar
scaleSelElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- dom ref
-- low knob
lowElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- a dom ref to the low knob
-- high knob
highElem <- (R.useRef null) :: R.Hooks (R.Ref (Nullable DOM.Element)) -- a dom ref to the high knob
-- The value of the user's selection
value <- T.useBox $ initialValue props
value' <- T.useLive T.unequal value
-- the knob we are currently in a drag for. set by mousedown on a knob
dragKnob <- T.useBox (Nothing :: Maybe Knob)
dragKnob' <- T.useLive T.unequal dragKnob
-- the handler functions for trapping mouse events, so they can be removed
mouseMoveHandler <- (R.useRef $ Nothing) :: R.Hooks (R.Ref (Maybe (EL.Callback Event.MouseEvent)))
mouseUpHandler <- (R.useRef $ Nothing) :: R.Hooks (R.Ref (Maybe (EL.Callback Event.MouseEvent)))
let destroy = \_ -> do
destroyEventHandler "mousemove" mouseMoveHandler
destroyEventHandler "mouseup" mouseUpHandler
R.setRef mouseMoveHandler $ Nothing
R.setRef mouseUpHandler $ Nothing
R2.useLayoutEffect1' dragKnob' $ \_ -> do
let scalePos = R2.readPositionRef scaleElem
let lowPos = R2.readPositionRef lowElem
let highPos = R2.readPositionRef highElem
case dragKnob' of
Just knob -> do
let drag = (getDragScale knob scalePos lowPos highPos) :: Maybe Range.NumberRange
let onMouseMove = EL.callback $ \(event :: Event.MouseEvent) -> do
case reproject drag scalePos props.bounds props.epsilon (R2.domMousePosition event) of
Just val -> do
setKnob knob value value' val
-- props.onChange $ knobSetter knob value' val
onChangeThrottled $ knobSetter knob value' val
Nothing -> destroy unit
let onMouseUp = EL.callback $ \(_event :: Event.MouseEvent) -> do
--props.onChange $ knobSetter knob value val
T.write_ Nothing dragKnob
destroy unit
EL.addEventListener document "mousemove" onMouseMove
EL.addEventListener document "mouseup" onMouseUp
R.setRef mouseMoveHandler $ Just onMouseMove
R.setRef mouseUpHandler $ Just onMouseUp
Nothing -> destroy unit
pure $ H.div { className, aria }
[ renderScale scaleElem props value'
, renderScaleSel scaleSelElem props value'
, renderKnob MinKnob lowElem value' props.bounds dragKnob precision props.status
, renderKnob MaxKnob highElem value' props.bounds dragKnob precision props.status
]
className = "range-slider"
aria = { label: "Range Slider Control. Expresses filtering data by a minimum and maximum value range through two slider knobs. Knobs can be adjusted with the arrow keys." }
destroyEventHandler
:: forall e
. Event.IsEvent e
=> String -> R.Ref (Maybe (EL.Callback e)) -> Effect Unit
destroyEventHandler name ref = traverse_ destroy $ R.readRef ref
where
destroy handler = do
EL.removeEventListener document name handler
R.setRef ref Nothing
setKnob :: Knob -> T.Box Range.NumberRange -> Range.NumberRange -> Number -> Effect Unit
setKnob knob value r val = T.write_ (knobSetter knob r val) value
knobSetter :: Knob -> Range.NumberRange -> Number -> Range.NumberRange
knobSetter MinKnob = Range.withMin
knobSetter MaxKnob = Range.withMax
getDragScale :: Knob -> Maybe DOMRect -> Maybe DOMRect -> Maybe DOMRect -> Maybe Range.NumberRange
getDragScale knob scalePos lowPos highPos = do
scale <- scalePos
low <- lowPos
high <- highPos
pure $ Range.Closed { min: min knob scale low, max: max knob scale high }
where
min MinKnob scale _ = scale.left
min MaxKnob _ low = low.left
max MinKnob _ high = high.left
max MaxKnob scale _ = scale.right
renderScale :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScale ref {width,height} (Range.Closed {min, max}) =
H.div { ref, className, width, height, aria } []
where
className = "range-slider__scale"
aria = { label: "Scale running from " <> show min <> " to " <> show max }
renderScaleSel :: R.Ref (Nullable DOM.Element) -> Record Props -> Range.NumberRange -> R.Element
renderScaleSel ref props (Range.Closed {min, max}) =
H.div { ref, className, style} []
where
className = "range-slider__scale-sel"
style = {left: computeLeft, width: computeWidth}
percOffsetMin = Range.normalise props.bounds min
percOffsetMax = Range.normalise props.bounds max
computeLeft = formatter $ 100.0 * percOffsetMin
computeWidth = formatter $ 100.0 * (percOffsetMax - percOffsetMin)
formatter n = (DNF.toStringWith (DNF.fixed 0) n) <> "%"
renderKnob :: Knob -> R.Ref (Nullable DOM.Element) -> Range.NumberRange -> Bounds -> T.Box (Maybe Knob) -> Int -> ComponentStatus -> R.Element
renderKnob knob ref (Range.Closed value) bounds set precision status =
H.div { ref, tabIndex, className, aria, on: { mouseDown: onMouseDown }, style } [
H.div { className: "range-slider__placeholder " }
[
H.text $ DNF.toStringWith (DNF.precision precision) val
]
]
where
tabIndex = 0
className = "range-slider__knob " <> (show status)
aria = { label: labelPrefix knob <> "value: " <> show val }
labelPrefix :: Knob -> String
labelPrefix MinKnob = "Minimum "
labelPrefix MaxKnob = "Maximum "
onMouseDown _ = case status of
Disabled -> pure unit
_ -> T.write_ (Just knob) set
percOffset = Range.normalise bounds val
style = { left: (show $ 100.0 * percOffset) <> "%" }
val :: Number
val = case knob of
MinKnob -> value.min
MaxKnob -> value.max
-- TODO round to nearest epsilon
reproject :: Maybe Range.NumberRange -> Maybe DOMRect -> Bounds -> Epsilon -> R2.Point -> Maybe Number
reproject drag scalePos bounds epsilon (R2.Point mousePos) = do
drag_ <- drag
scale_ <- rectRange <$> scalePos
let normal = Range.normalise scale_ (Range.clamp drag_ mousePos.x)
let val = Range.projectNormal bounds normal
pure $ round epsilon bounds val
rectRange :: DOMRect -> Range.NumberRange
rectRange rect = Range.Closed { min, max }
where min = rect.left
max = rect.right
initialValue :: Record Props -> Range.NumberRange
initialValue props = roundRange props.epsilon props.bounds props.initialValue
round :: Epsilon -> Bounds -> Number -> Number
round epsilon bounds = roundToMultiple epsilon <<< Range.clamp bounds
roundRange :: Epsilon -> Bounds -> Range.NumberRange -> Range.NumberRange
roundRange epsilon bounds (Range.Closed initial) = Range.Closed { min, max }
where min = round epsilon bounds initial.min
max = round epsilon bounds initial.max