Merge pull request #9 from soyjavi/slider

Slider
old
Javi Velasco 2015-07-21 22:29:55 +02:00
commit ae1cab3a77
9 changed files with 536 additions and 90 deletions

View File

@ -0,0 +1,10 @@
capitalized = (prop) ->
prop[0].toUpperCase() + prop[1..-1].toLowerCase()
vendorNoMoz = (prop, args) ->
"Webkit#{capitalized(prop)}": args
"Ms#{capitalized(prop)}": args
"#{prop}": args
module.exports =
transform: (value) -> vendorNoMoz('transform', value)

View File

@ -1,12 +1,5 @@
###
@todo
v2
- can set different sizes for circular progress
- maybe a multicolor indeterminate circular progress bar
- refactor vendor prefixes adding to a module
###
require './style'
localCSS = require './style'
prefixer = require "../prefixer"
module.exports = React.createClass
@ -17,6 +10,7 @@ module.exports = React.createClass
max : React.PropTypes.number
min : React.PropTypes.number
mode : React.PropTypes.string
multicolor : React.PropTypes.bool
type : React.PropTypes.string
value : React.PropTypes.number
@ -26,6 +20,7 @@ module.exports = React.createClass
max : 100
min : 0
mode : 'indeterminate'
multicolor : false
type : 'linear'
value : 0
@ -35,9 +30,11 @@ module.exports = React.createClass
# -- Render
render: ->
className = "#{@props.type} #{@props.className} #{@props.mode}"
<div data-component-progressbar role="progressbar"
className={className}
className = if @props.type == 'linear' then localCSS.linearBar else localCSS.circularBar
className += " #{localCSS.root} #{@props.mode} #{@props.className}"
className += " multicolor" if @props.multicolor
<div className={className} role="progressbar"
aria-valuenow={@props.value}
aria-valuemin={@props.min}
aria-valuemax={@props.max}>
@ -45,25 +42,21 @@ module.exports = React.createClass
</div>
renderCircular: ->
style = transformDasharray(@calculateRatio(@props.value)) unless @props.mode == 'indeterminate'
<svg data-component-progressbar-circle>
<circle style={style} data-component-progressbar-circle-path cx="50" cy="50" r="45"/>
unless @props.mode == 'indeterminate'
style = _transformDasharray(@calculateRatio(@props.value))
<svg className={localCSS.circle}>
<circle id="circle" className={localCSS.circlePath} style={style} cx="30" cy="30" r="25"/>
</svg>
renderLinear: ->
unless @props.mode == 'indeterminate'
bufferStyle = transformProgress(@calculateRatio(@props.buffer))
valueStyle = transformProgress(@calculateRatio(@props.value))
bufferStyle = prefixer.transform("scaleX(#{@calculateRatio(@props.buffer)})")
valueStyle = prefixer.transform("scaleX(#{@calculateRatio(@props.value)})")
<div>
<span data-component-progressbar-buffer style={bufferStyle}></span>
<span data-component-progressbar-value style={valueStyle}></span>
<span id="buffer" className={localCSS.bufferBar} style={bufferStyle}></span>
<span id="value" className={localCSS.valueBar} style={valueStyle}></span>
</div>
# -- Private methods
transformDasharray = (ratio) ->
_transformDasharray = (ratio) ->
strokeDasharray: "#{2 * Math.PI * 45 * ratio}, 400"
transformProgress = (ratio) ->
WebkitTransform: "scaleX(#{ratio})"
MsTransform: "scaleX(#{ratio})"
transform: "scaleX(#{ratio})"

View File

@ -1,89 +1,105 @@
@import '../constants.styl'
// -- NOTE: Parameters depend on the component SVG Markup too
CIRCLE_WRAPPER_WIDTH = 100
CIRCLE_RADIUS = 45
SCALE_RATIO = CIRCLE_RADIUS/20
CIRCLE_WRAPPER_WIDTH = 60 // You need to change cx and cy in markup too
CIRCLE_RADIUS = 25 // You need to change r in the markup too
SCALE_RATIO = CIRCLE_RADIUS/20
[data-component-progressbar]
display : inline-block
// -- Linear progress bar
:local(.linearBar)
display : inline-block
position : relative
height : PROGRESS_BAR_HEIGHT
width : 100%
background : darken(BACKGROUND, 7.5%)
overflow : hidden
&.linear
position : relative
height : PROGRESS_BAR_HEIGHT
width : 100%
background : darken(BACKGROUND, 7.5%)
overflow : hidden
:local(.valueBar), :local(.bufferBar)
position : absolute
bottom : 0
left : 0
right : 0
top : 0
transform : scaleX(0)
transform-origin : left center
transition-duration : ANIMATION_DURATION
transition-timing-function : ANIMATION_EASE
[data-component-progressbar-value], [data-component-progressbar-buffer]
position : absolute
bottom : 0
left : 0
right : 0
top : 0
transform scaleX(0)
transform-origin left center
transition-duration ANIMATION_DURATION
transition-timing-function ANIMATION_EASE
:local(.valueBar)
background-color : ACCENT
[data-component-progressbar-value]
background-color : ACCENT
:local(.bufferBar)
background-color : alpha(ACCENT, 15%)
[data-component-progressbar-buffer]
background-color : alpha(ACCENT, 15%)
:local(.linearBar).indeterminate :local(.valueBar)
transform-origin : center center
animation : linear-indeterminate-bar 1s linear infinite
&.indeterminate [data-component-progressbar-value]
transform-origin center center
animation linear-indeterminate-bar 1s linear infinite
// -- Circular progress bar
:local(.circularBar)
display : inline-block
position : relative
height : CIRCLE_WRAPPER_WIDTH * 1px
width : CIRCLE_WRAPPER_WIDTH * 1px
transform : rotate(-90deg)
&.circular
position : relative
height : CIRCLE_WRAPPER_WIDTH * 1px
width : CIRCLE_WRAPPER_WIDTH * 1px
transform rotate(-90deg)
:local(.circle)
height : 100%
width : 100%
[data-component-progressbar-circle]
height : 100%
width : 100%
:local(.circlePath)
stroke-dasharray : 0, SCALE_RATIO * 200
stroke-dashoffset : 0
stroke-linecap : round
stroke-miterlimit : 20
stroke-width : 4
stroke : ACCENT
fill : none
transition : stroke-dasharray ANIMATION_DURATION ANIMATION_EASE
[data-component-progressbar-circle-path]
stroke-dasharray : 0, SCALE_RATIO * 200
stroke-dashoffset : 0
stroke-linecap : round
stroke-miterlimit : 20
stroke-width : 4
stroke : ACCENT
fill : none
transition stroke-dasharray ANIMATION_DURATION ANIMATION_EASE
:local(.circularBar).indeterminate
:local(.circle)
animation : circular-indeterminate-bar-rotate 2s linear infinite
&.indeterminate
[data-component-progressbar-circle]
animation circular-indeterminate-bar-rotate 2s linear infinite
:local(.circlePath)
stroke-dasharray : SCALE_RATIO * 1, SCALE_RATIO * 200
stroke-dashoffset : 0
animation : circular-indeterminate-bar-dash 1.5s ease-in-out infinite
[data-component-progressbar-circle-path]
stroke-dasharray : SCALE_RATIO * 1, SCALE_RATIO * 200
stroke-dashoffset : 0
animation circular-indeterminate-bar-dash 1.5s ease-in-out infinite
&.multicolor :local(.circlePath)
animation : circular-indeterminate-bar-dash 1.5s ease-in-out infinite, colors (1.5s*4) ease-in-out infinite
// -- Animations
@keyframes linear-indeterminate-bar
0%
transform translate(-50%) scaleX(0)
transform : translate(-50%) scaleX(0)
50%
transform translate(-0%) scaleX(0.3)
transform : translate(-0%) scaleX(0.3)
100%
transform translate(50%) scaleX(0)
transform : translate(50%) scaleX(0)
@keyframes circular-indeterminate-bar-rotate
100%
transform : rotate(360deg)
transform : rotate(360deg)
@keyframes circular-indeterminate-bar-dash
0%
stroke-dasharray : SCALE_RATIO * 1, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * 0
stroke-dasharray : SCALE_RATIO * 1, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * 0
50%
stroke-dasharray : SCALE_RATIO * 89, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * -35
stroke-dasharray : SCALE_RATIO * 89, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * -35
100%
stroke-dasharray : SCALE_RATIO * 89, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * -124
stroke-dasharray : SCALE_RATIO * 89, SCALE_RATIO * 200
stroke-dashoffset : SCALE_RATIO * -124
@keyframes colors
0%
stroke: #4285F4
25%
stroke: #DE3E35
50%
stroke: #F7C223
75%
stroke: #1B9A59
100%
stroke: #4285F4

View File

@ -0,0 +1,240 @@
prefixer = require "../prefixer"
ProgressBar = require "../progress_bar"
Input = require "../input"
localCSS = require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
editable : React.PropTypes.bool
max : React.PropTypes.number
min : React.PropTypes.number
onChange : React.PropTypes.func
pinned : React.PropTypes.bool
snaps : React.PropTypes.bool
step : React.PropTypes.number
value : React.PropTypes.number
getDefaultProps: ->
className : ""
editable : false
max : 100
min : 0
pinned : false
snaps : false
step : 0.01
value : 0
getInitialState: ->
sliderStart : 0
sliderLength : 0
value : @props.value
# -- Lifecycle
componentDidMount: ->
@onResize()
window.addEventListener('resize', @onResize)
componentWillUnmount: ->
window.removeEventListener('resize', @onResize)
componentDidUpdate: (prevProps, prevState) ->
if prevState.value != @state.value
@props.onChange? @
if @state.value != parseFloat(@refs.input?.getValue())
@refs.input?.setValue(@valueForInput(@state.value))
# -- Events
onResize: (event) ->
sliderBounds = @refs.progressbar.getDOMNode().getBoundingClientRect()
@setState
sliderStart: sliderBounds['left'],
sliderLength: (sliderBounds['right'] - sliderBounds['left'])
onSliderMouseDown: (event) ->
position = _getMousePosition(event)
value = @positionToValue(position)
@setState value: value, =>
@start(position)
_addEventsToDocument(@getMouseEventMap())
_pauseEvent(event)
onSliderTouchStart: (event) ->
position = _getTouchPosition(event)
value = @positionToValue(position)
@setState value: value, =>
@start(position)
_addEventsToDocument(@getTouchEventMap())
_pauseEvent(e)
onSliderFocus: (event) ->
_addEventsToDocument(@getKeyboardEvents())
onSliderBlur: (event) ->
_removeEventsFromDocument(@getKeyboardEvents())
onInputChange: ->
@setState value: @trimValue(@refs.input.getValue())
onKeyDown: (event) ->
event.stopPropagation()
@refs.slider.getDOMNode().blur() if event.keyCode in [13, 27]
@addToValue(@props.step) if event.keyCode == 38
@addToValue(-@props.step) if event.keyCode == 40
onMouseDown: (event) ->
@start(_getMousePosition(event))
_addEventsToDocument(@getMouseEventMap())
onTouchStart: (event) ->
event.stopPropagation()
@start(_getTouchPosition(event))
_addEventsToDocument(@getTouchEventMap())
onMouseMove: (event) ->
_pauseEvent(event)
@move(_getMousePosition(event))
onTouchMove: (event) ->
@move(_getTouchPosition(event))
onMouseUp: ->
@end(@getMouseEventMap())
onTouchEnd: ->
@end(@getTouchEventMap())
# -- Internal methods
getMouseEventMap: ->
mousemove: @onMouseMove
mouseup: @onMouseUp
getTouchEventMap: ->
touchmove: @onTouchMove
touchend: @onTouchEnd
getKeyboardEvents: ->
keydown: @onKeyDown
positionToValue: (position) ->
offset = position.x - @state.sliderStart
@trimValue(offset / @state.sliderLength * (@props.max - @props.min))
start: (position) ->
@setState
pressed: true
startPosition: position.x
startValue: @state.value
move: (position) ->
value = @endPositionToValue(position)
@setState value: value
end: (events) ->
_removeEventsFromDocument(events)
@setState pressed: false
endPositionToValue: (position) ->
offset = position.x - @state.startPosition
diffValue = offset / @state.sliderLength * (@props.max - @props.min)
@trimValue(diffValue + @state.startValue)
trimValue: (value) ->
value = @props.min if (value < @props.min)
value = @props.max if (value > @props.max)
@nearest(value)
nearest: (value) ->
steps = (@props.max - @props.min) / @props.step
zerone = Math.round((value - @props.min) * steps / (@props.max - @props.min))/steps
return zerone * (@props.max - @props.min) + @props.min
addToValue: (value) ->
@setState value: @trimValue(@state.value + value)
valueForInput: (value) ->
decimals = (@props.step.toString().split('.')[1] || []).length
if decimals > 0 then value.toFixed(decimals) else value.toString()
calculateKnobOffset: ->
@state.sliderLength * (@state.value - @props.min) / (@props.max - @props.min)
render: ->
className = @props.className
className += " editable" if @props.editable
className += " pinned" if @props.pinned
className += " pressed" if @state.pressed
className += " ring" if @state.value == @props.min
knobStyles = prefixer.transform("translateX(#{@calculateKnobOffset()}px)")
<div className={localCSS.root + className}
tabIndex="0" ref="slider"
onFocus={@onSliderFocus}
onBlur={@onSliderBlur} >
<div className={localCSS.container}
onTouchStart={@onSliderTouchStart}
onMouseDown={@onSliderMouseDown} >
<div className={localCSS.knob} style={knobStyles}
onMouseDown={@onMouseDown}
onTouchStart={@onTouchStart} >
<div className={localCSS.knobInner} data-value={parseInt(@state.value)}></div>
</div>
<div className={localCSS.progress} >
<ProgressBar ref="progressbar" mode="determinate"
className={localCSS.progressInner}
value={@state.value}
max={@props.max}
min={@props.min}/>
{
if @props.snaps
<div className={localCSS.snaps}>
{
for i in [1..((@props.max - @props.min) / @props.step)]
<div key="span-#{i}" className={localCSS.snap}></div>
}
</div>
}
</div>
</div>
{
if @props.editable
<Input ref="input" className={localCSS.input}
onChange={@onInputChange}
value={@valueForInput(@state.value)} />
}
</div>
# -- Extends
getValue: ->
@state.value
setValue: (value) ->
@setState value: value
# -- Private methods
_pauseEvent = (event) ->
event.stopPropagation()
event.preventDefault()
event.returnValue = false
event.cancelBubble = true
return null
_getMousePosition = (event) ->
x: event.pageX
y: event.pageY
_getTouchPosition = (event) ->
x: event.touches[0]['pageX']
y: event.touches[0]['pageY']
_addEventsToDocument = (events) ->
document.addEventListener(key, events[key], false) for key of events
_removeEventsFromDocument = (events) ->
document.removeEventListener(key, events[key], false) for key of events

View File

@ -0,0 +1,162 @@
@import "../constants"
RANGE_SIDE_SEPARATION = 20px
:local(.container)
height : 32px
margin-right : 32px
position : relative
user-select : none
width : calc(100% - 32px)
&:not(:last-child)
margin-right : RANGE_SIDE_SEPARATION + 32px
&:not(:first-child)
margin-left : RANGE_SIDE_SEPARATION
:local(.knob)
align-items : center
background-color : transparent
display : flex
flex-direction : row
height : 32px
justify-content : center
left : 0
position : relative
top : 0
width : 32px
z-index : 5
:local(.knobInner)
background-color : ACCENT
border-radius : 50%
height : 12px
transition-duration : 0.1s
transition-property : height, width, background-color, border
transition-timing-function : ease
width : 12px
z-index : 4
:local(.snaps)
display : flex
flex-direction : row
height : 2px
left : 0
pointer-events : none
position : absolute
top : 15px
width : calc(100% + 2px)
&:after
background-color : black
border-radius : 50%
content : ""
display : block
height : 2px
width : 2px
:local(.snap)
flex : 1
&:after
background-color : black
border-radius : 50%
content : ""
display : block
height : 2px
width : 2px
:local(.input)
margin-bottom : 0
text-align : center
width : 40px
:local(.progress)
height : 100%
left : 16px
position : absolute
top : 0
width : 100%
:local(.progressInner)
height : 2px
position : absolute
top : 15px
#value
transition-duration : 0s
// When its focused
:local(.root):focus :local(.knob):before
background-color : ACCENT
border-radius : 50%
bottom : 0
content : ""
left : 0
position : absolute
right : 0
top : 0
opacity : .26
z-index : 2
:local(.root).editable
display : flex
align-items : center
flex-direction : row
// When its pinned
:local(.root).pinned :local(.knobInner)
&:before
content : ""
position : absolute
top : 0
background-color : ACCENT
left : 0
width : 26px
height : 26px
margin-left : 3px
border-radius : 50% 50% 50% 0
transition-delay : 1s
transform : rotate(-45deg) scale(0) translate(0)
transition : transform .2s ease, background-color .18s ease
&:after
color : white
content : attr(data-value)
font-size : 10px
height : 26px
left : 0
position : absolute
text-align : center
top : 0
transform : scale(0) translate(0)
transition : transform .2s ease, background-color .18s ease
width : 32px
// When the knob is pressed
:local(.root).pressed.pinned :local(.knobInner)
&:before
transition-delay : 100ms
transform : rotate(-45deg) scale(1) translate(17px, -17px)
&:after
transition-delay : 100ms
transform : scale(1) translate(0, -17px)
:local(.root).pressed:not(.pinned) :local(.knobInner)
height : 100%
transform : translateZ(0)
width : 100%
:local(.root).pressed:not(.pinned).ring :local(.progress)
left : 30px
width : calc(100% - 14px)
// When there is no current value
:local(.root).ring :local(.knobInner)
background-color : transparent
border : 2px solid #c8c8c8
:local(.root).ring :local(.knobInner):before
background-color : #c8c8c8
:local(.root).ring :local(.progress)
left : 20px
transition : left 0.18s ease, width 0.18s ease
width : calc(100% - 4px)

View File

@ -26,10 +26,12 @@
"react" : "^0.13.2"
},
"devDependencies": {
"autoprefixer-core" : "^5.1.11",
"coffee-jsx-loader" : "^0.1.2",
"css-loader" : "^0.14.5",
"extract-text-webpack-plugin" : "^0.8.1",
"node-libs-browser" : "^0.5.2",
"postcss-loader" : "^0.4.3",
"style-loader" : "^0.12.3",
"stylus-loader" : "^1.2.0",
"webpack" : "^1.9.10",

View File

@ -0,0 +1,17 @@
Slider = require '../../components/slider'
module.exports = React.createClass
test: ->
marginTop: "1rem"
render: ->
<section>
<h2 style={@test()}>Sliders</h2>
<p>Normal slider</p>
<Slider />
<p>With steps, initial value and editable</p>
<Slider value={5} min={0} max={10} editable />
<p>Pinned and with snaps</p>
<Slider pinned value={1} min={0} max={10} step={1} editable />
</section>

View File

@ -10,6 +10,7 @@ Dropdown = require './components/dropdown'
FontIcon = require './components/font_icon'
Form = require './components/form'
Progress = require './components/progress'
Slider = require './components/slider'
Switch = require './components/switch'
Test = React.createClass
@ -29,6 +30,7 @@ Test = React.createClass
<FontIcon />
<Form />
<Progress />
<Slider />
</app>
React.render <Test/>, document.body

View File

@ -22,7 +22,7 @@ module.exports =
devServer:
# contentBase : './build'
host : 'localhost'
host : '0.0.0.0'
port : 8080
# colors : true
# progress : true
@ -38,9 +38,13 @@ module.exports =
,
test : /\.coffee$/, loader: 'coffee-jsx-loader'
,
test : /\.styl$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader!')
test : /\.styl$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader!stylus-loader!')
]
postcss: [
require('autoprefixer-core')
],
plugins: [
new ExtractTextPlugin pkg.name + '.[name].css', allChunks: false
]