Merge pull request #13 from react-toolbox/es6

Rewrite react toolbox in ES6
old
Javi Velasco 2015-09-22 09:48:00 +02:00
commit 44d96682d9
136 changed files with 4350 additions and 3479 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
!node_modules

206
.eslintrc Normal file
View File

@ -0,0 +1,206 @@
{
"env": {
"browser": true,
"node": true,
"mocha": true,
"es6": true
},
"ecmaFeatures": {
"jsx": true,
"templateStrings": true,
"superInFunctions": false,
"classes": false,
"modules": [2]
},
"plugins": [
"react"
],
"rules": {
"block-scoped-var": [0],
"brace-style": [2, "1tbs", {
"allowSingleLine": true
}],
"camelcase": [0],
"comma-dangle": [2, "never"],
"comma-spacing": [2],
"comma-style": [2, "last"],
"complexity": [0, 11],
"consistent-return": [2],
"consistent-this": [0, "that"],
"curly": [2, "multi-line"],
"default-case": [2],
"dot-notation": [2, {
"allowKeywords": true
}],
"eol-last": [2],
"eqeqeq": [2],
"func-names": [0],
"func-style": [0, "declaration"],
"generator-star-spacing": [2, "after"],
"strict": [2, "always"],
"guard-for-in": [0],
"handle-callback-err": [0],
"key-spacing": [2, {
"beforeColon": false,
"afterColon": true
}],
"quotes": [2, "single", "avoid-escape"],
"max-depth": [0, 4],
"max-len": [0, 80, 4],
"max-nested-callbacks": [0, 2],
"max-params": [0, 3],
"max-statements": [0, 10],
"new-parens": [2],
"new-cap": [2, {
"capIsNewExceptions": ["ToInteger", "ToObject", "ToPrimitive", "ToUint32"]
}],
"newline-after-var": [0],
"no-alert": [2],
"no-array-constructor": [2],
"no-bitwise": [0],
"no-caller": [2],
"no-catch-shadow": [2],
"no-cond-assign": [2],
"no-console": [0],
"no-constant-condition": [1],
"no-continue": [2],
"no-control-regex": [2],
"no-debugger": [2],
"no-delete-var": [2],
"no-div-regex": [0],
"no-dupe-args": [2],
"no-dupe-keys": [2],
"no-duplicate-case": [2],
"no-else-return": [0],
"no-empty": [2],
"no-empty-character-class": [2],
"no-empty-label": [2],
"no-eq-null": [0],
"no-eval": [2],
"no-ex-assign": [2],
"no-extend-native": [1],
"no-extra-bind": [2],
"no-extra-boolean-cast": [2],
"no-extra-parens": [0],
"no-extra-semi": [1],
"no-fallthrough": [2],
"no-floating-decimal": [2],
"no-func-assign": [2],
"no-implied-eval": [2],
"no-inline-comments": [0],
"no-inner-declarations": [2, "functions"],
"no-invalid-regexp": [2],
"no-irregular-whitespace": [2],
"no-iterator": [2],
"no-label-var": [2],
"no-labels": [2],
"no-lone-blocks": [2],
"no-lonely-if": [2],
"no-loop-func": [2],
"no-mixed-requires": [0, false],
"no-mixed-spaces-and-tabs": [2, false],
"no-multi-spaces": [2],
"no-multi-str": [2],
"no-multiple-empty-lines": [2, {
"max": 2
}],
"no-native-reassign": [1],
"no-negated-in-lhs": [2],
"no-nested-ternary": [0],
"no-new": [2],
"no-new-func": [2],
"no-new-object": [2],
"no-new-require": [0],
"no-new-wrappers": [2],
"no-obj-calls": [2],
"no-octal": [2],
"no-octal-escape": [2],
"no-path-concat": [0],
"no-param-reassign": [2],
"no-plusplus": [0],
"no-process-env": [0],
"no-process-exit": [2],
"no-proto": [2],
"no-redeclare": [2],
"no-regex-spaces": [2],
"no-reserved-keys": [0],
"no-restricted-modules": [0],
"no-return-assign": [2],
"no-script-url": [2],
"no-self-compare": [0],
"no-sequences": [2],
"no-shadow": [2],
"no-shadow-restricted-names": [2],
"semi-spacing": [2],
"no-spaced-func": [2],
"no-sparse-arrays": [2],
"no-sync": [0],
"no-ternary": [0],
"no-throw-literal": [2],
"no-trailing-spaces": [2],
"no-undef": [2],
"no-undef-init": [2],
"no-undefined": [0],
"no-underscore-dangle": [0],
"no-unreachable": [2],
"no-unused-expressions": [2],
"no-unused-vars": [1, {
"vars": "all",
"args": "after-used"
}],
"no-use-before-define": [2],
"no-void": [0],
"no-warning-comments": [0, {
"terms": ["todo", "fixme", "xxx"],
"location": "start"
}],
"no-with": [2],
"one-var": [0],
"operator-assignment": [0, "always"],
"operator-linebreak": [2, "after"],
"padded-blocks": [0],
"quote-props": [0],
"radix": [0],
"semi": [2],
"semi-spacing": [2, {
"before": false,
"after": true
}],
"sort-vars": [0],
"space-after-keywords": [2, "always"],
"space-before-function-paren": [2, {
"anonymous": "always",
"named": "always"
}],
"space-before-blocks": [0, "always"],
"space-in-brackets": [0, "never", {
"singleValue": true,
"arraysInArrays": false,
"arraysInObjects": false,
"objectsInArrays": true,
"objectsInObjects": true,
"propertyName": false
}],
"space-in-parens": [2, "never"],
"space-infix-ops": [2],
"space-return-throw-case": [2],
"space-unary-ops": [2, {
"words": true,
"nonwords": false
}],
"spaced-line-comment": [0, "always"],
"strict": [1],
"use-isnan": [2],
"valid-jsdoc": [0],
"valid-typeof": [2],
"vars-on-top": [0],
"wrap-iife": [2],
"wrap-regex": [2],
"yoda": [2, "never", {
"exceptRange": true
}]
}
}

View File

@ -1,40 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
active : React.PropTypes.bool
className : React.PropTypes.string
hideable : React.PropTypes.bool
type : React.PropTypes.string
getDefaultProps: ->
className : ''
type : 'left'
getInitialState: ->
active : @props.active
# -- Events
onClick: (event) ->
@setState active: false if event.target is @getDOMNode()
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
className += ' hideable' if @props.hideable
className += ' active' if @state.active
<div data-react-toolbox='aside' className={className} onClick={@onClick}>
<aside className={localCSS.container}>
{ @props.children }
</aside>
</div>
# -- Extends
show: ->
@setState active: true
hide: ->
@setState active: false

View File

@ -1,48 +0,0 @@
@import '../constants'
WIDTH = (4 * UNIT)
:local(.container)
position : absolute
top : 0
height : 100%
width : WIDTH
background-color : WHITE
transition-property : transform
transition-duration : ANIMATION_DURATION
transition-timing-function : ANIMATION_EASE
:local(.root)
z-index : 2
position : fixed
top : 0
width : 100vw
height : 100vh
pointer-events : none
transition-property : background-color
transition-duration : ANIMATION_DURATION
transition-timing-function : ANIMATION_EASE
// -- Overrides
&:not(.hideable)
z-index : 2
width : WIDTH
&.hideable
z-index : 3
&.active
background-color : rgba(0,0,0,0.5)
&.left
left : 0
&:not(.active) > :local(.container)
left : 0
transform : translateX(-100%)
&.right
right : 0
&:not(.active) > :local(.container)
right : 0
transform : translateX(100%)
&.active
pointer-events : all
> :local(.container)
box-shadow : ZDEPTH_SHADOW_1
transform : translateX(0%)

View File

@ -19,11 +19,9 @@ var data = [
| Name | Type | Default | Description|
|:- |:-: | :- |:-|
| **className** | String | | Sets the class-styles of the Component.|
| **colors** | Object | | JSON data representing all colors per key in the dropdown.||
| **dataSource** | Object | | JSON data representing all items in the component.|
| **disabled** | Boolean | | If true, component will be disabled.|
| **error** | String | | Sets the error string.|
| **exact** | Bool | true | If true, component only accepts values from dataSource property..|
| **label** | String | | The text string to use for the floating label element.|
| **multiple** | Bool | true | If true, component can hold multiple values.|
| **onChange** | Function | | Callback function that is fired when the components's value changes.|

View File

@ -1,172 +0,0 @@
localCSS = require './style'
Input = require '../input'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
colors : React.PropTypes.object
dataSource : React.PropTypes.any
disabled : React.PropTypes.bool
error : React.PropTypes.string
exact : React.PropTypes.bool
label : React.PropTypes.string
multiple : React.PropTypes.bool
onChange : React.PropTypes.func
required : React.PropTypes.bool
type : React.PropTypes.string
value : React.PropTypes.any
getDefaultProps: ->
className : ''
colors : {}
dataSource : {}
exact : true
multiple : true
type : 'text'
getInitialState: ->
focus : false
dataSource : _index @props.dataSource
suggestions : {}
values : {}
# -- Lifecycle
componentDidMount: ->
@setValue @props.value if @props.value
componentWillReceiveProps: (next_props) ->
@setState dataSource: _index next_props.dataSource if next_props.dataSource
# -- Events
onFocus: ->
@refs.suggestions.getDOMNode().scrollTop = 0
@setState focus: true, suggestions: @_getSuggestions()
onChange: ->
suggestions = {}
value = @refs.input.getValue().toLowerCase().trim()
if value.length > 0
@setState focus: true, suggestions: suggestions = @_getSuggestions value
@setState focus: false if Object.keys(suggestions).length is 0
onKeyPress: (event) ->
key_ascii = event.which
query = @refs.input.getValue().trim()
if @state.focus
children = @refs.suggestions.getDOMNode().children
for child, index in children when child.classList.contains 'active'
child.classList.remove 'active'
query = child.getAttribute 'id'
break
if key_ascii is 13 and query isnt ''
for key, label of @state.suggestions when query.toLowerCase() is label.toLowerCase()
suggestion = {"#{key}": label}
break
unless @props.exact
@_addValue suggestion or {"#{query}": query}
else if suggestion
@_addValue suggestion
else if @state.focus and key_ascii in [40, 38]
if key_ascii is 40
index = if index >= children.length - 1 then 0 else index + 1
else
index = if index is 0 then children.length - 1 else index - 1
children[index].classList.add 'active'
onBlur: (event) ->
setTimeout (=> @setState focus: false, suggestions: {}), 300
onSelect: (event) ->
key = event.target.getAttribute 'id'
@_addValue {"#{key}": @state.suggestions[key]}
onRefreshSelection: (event) ->
children = @refs.suggestions.getDOMNode().children
for child, index in children when child.classList.contains 'active'
child.classList.remove 'active'
break
onDelete: (event) ->
delete @state.values[event.target.getAttribute 'id']
@setState focus: false, values: @state.values
@props.onChange? @
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
className += ' focus' if @state.focus
<div data-react-toolbox='autocomplete' className={className}>
{ <label>{@props.label}</label> if @props.label }
{
if @props.multiple
<ul className={localCSS.values} data-flex='horizontal wrap'
onClick={@onDelete}>
{
for key, label of @state.values
<li key={key} id={key} style={backgroundColor: @props.colors[key]}>{label}</li>
}
</ul>
}
<Input {...@props} ref='input' value='' label=''
onBlur={@onBlur}
onChange={@onChange}
onFocus={@onFocus}
onKeyDown={@onKeyPress}
/>
<ul ref='suggestions' className={localCSS.suggestions}
onClick={@onSelect} onMouseEnter={@onRefreshSelection}>
{<li key={key} id={key}>{label}</li> for key, label of @state.suggestions}
</ul>
</div>
# -- Extends
getValue: ->
if @props.multiple
(key for key of @state.values)
else
Object.keys(@state.values)?[0]
setValue: (data = []) ->
values = {}
data = [data] if typeof data is 'string'
values[key] = label for key, label of @state?.dataSource when key in data
@state.values = values
@setState values: values
@refs.input.setValue values[Object.keys(values)?[0]] unless @props.multiple
setError: (data) ->
@refs.input.setError data
# -- Internal methods
_addValue: (value) ->
key = Object.keys(value)[0]
if @props.multiple
values = @state.values
values[key] = value[key]
@props.onChange? @
else
values = value
setTimeout (=> @props.onChange? @), 10
@setState focus: false, values: values
@refs.input.setValue if @props.multiple then '' else value[key]
_getSuggestions: (query) ->
suggestions = {}
for key, label of @state.dataSource when not @state.values[key]
if not query or label.toLowerCase().trim().indexOf(query) is 0
suggestions[key] = label
suggestions
# -- Private methods
_index = (data = {}) ->
indexed = data
if data.length?
indexed = {}
indexed[item] = item for item in data
indexed

View File

@ -0,0 +1,203 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import utils from '../utils';
import Input from '../input';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Autocomplete',
propTypes: {
className: React.PropTypes.string,
dataSource: React.PropTypes.any,
disabled: React.PropTypes.bool,
error: React.PropTypes.string,
label: React.PropTypes.string,
multiple: React.PropTypes.bool,
onChange: React.PropTypes.func,
required: React.PropTypes.bool,
type: React.PropTypes.string,
value: React.PropTypes.any
},
getDefaultProps () {
return {
className: '',
dataSource: {},
multiple: true,
type: 'text'
};
},
getInitialState () {
return {
dataSource: this._indexDataSource(this.props.dataSource),
focus: false,
query: '',
values: new Map()
};
},
componentDidMount () {
if (this.props.value) this.setValue(this.props.value);
},
componentWillReceiveProps (props) {
if (props.dataSource) {
this.setState({dataSource: this._indexDataSource(props.dataSource)});
}
},
componentWillUpdate (props, state) {
this.refs.input.setValue(state.query);
},
handleQueryChange () {
const query = this.refs.input.getValue();
if (this.state.query !== query) {
this.setState({query: query});
}
},
handleKeyPress (event) {
if (event.which === 13 && this.state.active) {
this._selectOption(this.state.active);
}
if ([40, 38].indexOf(event.which) !== -1) {
const suggestionsKeys = [...this._getSuggestions().keys()];
let index = suggestionsKeys.indexOf(this.state.active) + (event.which === 40 ? +1 : -1);
if (index < 0) index = suggestionsKeys.length - 1;
if (index >= suggestionsKeys.length) index = 0;
this.setState({active: suggestionsKeys[index]});
}
},
handleFocus () {
this.refs.suggestions.getDOMNode().scrollTop = 0;
this.setState({active: '', focus: true});
},
handleBlur () {
if (this.state.focus) this.setState({focus: false});
},
handleHover (event) {
this.setState({active: event.target.getAttribute('id')});
},
handleSelect (event) {
utils.events.pauseEvent(event);
this._selectOption(event.target.getAttribute('id'));
},
handleUnselect (event) {
this._unselectOption(event.target.getAttribute('id'));
},
_indexDataSource (data = {}) {
if (data.length) {
return new Map(data.map((item) => [item, item]));
} else {
return new Map(Object.keys(data).map((key) => [key, data[key]]));
}
},
_getSuggestions () {
let query = this.state.query.toLowerCase().trim() || '';
let suggestions = new Map();
for (let [key, value] of this.state.dataSource) {
if (!this.state.values.has(key) && value.toLowerCase().trim().startsWith(query)) {
suggestions.set(key, value);
}
}
return suggestions;
},
_selectOption (key) {
let { values, dataSource } = this.state;
let query = !this.props.multiple ? dataSource.get(key) : '';
values = new Map(values);
if (!this.props.multiple) values.clear();
values.set(key, dataSource.get(key));
this.setState({focus: false, query: query, values: values}, () => {
this.refs.input.blur();
if (this.props.onChange) this.props.onChange(this);
});
},
_unselectOption (key) {
if (key) {
let values = new Map(this.state.values);
values.delete(key);
this.setState({focus: false, values: values}, () => {
if (this.props.onChange) this.props.onChange(this);
});
}
},
getValue () {
let values = [...this.state.values.keys()];
return this.props.multiple ? values : (values.length > 0 ? values[0] : null);
},
setValue (dataParam = []) {
let values = new Map();
let data = (typeof dataParam === 'string') ? [dataParam] : dataParam;
for (let [key, value] of this.state.dataSource) {
if (data.indexOf(key) !== -1) values.set(key, value);
}
this.setState({values: values, query: this.props.multiple ? '' : values.get(data[0])});
},
setError (data) {
this.input.setError(data);
},
renderSelected () {
if (this.props.multiple) {
return (
<ul className={css.values} data-flex='horizontal wrap' onClick={this.handleUnselect}>
{[...this.state.values].map(([key, value]) => {
return (<li key={key} id={key}>{value}</li>);
})}
</ul>
);
}
},
render () {
let className = `${css.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
if (this.state.focus) className += ' focus';
return (
<div data-react-toolbox='autocomplete' className={className}>
{this.props.label ? (<label>{this.props.label}</label>) : ''}
{this.renderSelected()}
<Input {...this.props} ref='input' label='' value=''
onBlur={this.handleBlur}
onChange={this.handleQueryChange}
onFocus={this.handleFocus}
onKeyUp={this.handleKeyPress} />
<ul ref='suggestions'
className={css.suggestions}
onMouseDown={this.handleSelect}
onMouseOver={this.handleHover}>
{[...this._getSuggestions()].map(([key, value]) => {
return (
<li id={key} key={key} className={this.state.active === key ? 'active' : ''}>
{value}
</li>
);
})}
</ul>
</div>
);
}
});

View File

@ -29,7 +29,7 @@
> *
cursor : pointer
padding : (SPACE / 2)
&:hover, &.active
&.active
color : WHITE
background-color : PRIMARY_LIGHT

View File

@ -2,28 +2,24 @@
```
var Button = require('react-toolbox/components/button');
<Button caption="Login" />
<Button caption="Primary" className="primary" icon="access_alarm" />
<Button caption="Secondary" className="accent" />
<Button caption="Disabled" disabled />
<Button type="circle" icon="access_alarm" />
<Button type="circle" icon="explore" className="primary" />
<Button type="circle" icon="zoom_in" className="accent" />
<Button type="circle" icon="input" disabled={true} />
<Button className="accent" label="Flat button" />
<Button className="primary" type="raised" label="Raised" />
<Button className="accent" type="raised" label="Raised" icon="assignment_turned_in" />
<Button className="primary" type="floating" icon="add" />
<Button className="accent mini" type="floating" icon="add" />
```
## Properties
| Name | Type | Default | Description|
|:- |:-: | :- |:-|
| **caption** | String | | The text string to use for the floating label element.|
| **className** | String | | Set the class-styles of the Component.|
| **disabled** | Boolean | | If true, component will be disabled.|
| **icon** | String | | Default value using JSON data.|
| **label** | String | | The text string to use for the floating label element.|
| **loading** | Boolean | | If true, component will be disabled and show a loading animation.|
| **type** | String | "text" | Type of the component, overwrite this property if you need set a different stylesheet.|
| **ripple** | Boolean | | If true, component will have a ripple effect on click.|
| **type** | String | "flat" | Type of the component, overwrite this property if you need set a different stylesheet.|
## Methods

View File

@ -1,61 +0,0 @@
localCSS = require './style'
FontIcon = require '../font_icon'
Ripple = require '../ripple'
module.exports = React.createClass
displayName : 'Button'
# -- States & Properties
propTypes:
caption : React.PropTypes.string
className : React.PropTypes.string
disabled : React.PropTypes.bool
icon : React.PropTypes.string
loading : React.PropTypes.bool
type : React.PropTypes.string
getDefaultProps: ->
className : ''
type : 'raised'
getInitialState: ->
loading : @props.loading
focused : false
ripple : undefined
# -- Lifecycle
componentWillReceiveProps: ->
@setState ripple: undefined
# -- Events
onClick: (event) ->
event.preventDefault()
client = event.target.getBoundingClientRect?()
@setState
focused: true
ripple:
left : event.pageX - client?.left
top : event.pageY - client?.top
width : (client?.width * 2.5)
@props.onClick? event, @
setTimeout (=> @setState focused: false), 450
# -- Render
render: ->
className = @props.className
className += " #{@props.type}" if @props.type
className += ' focused' if @state.focused
<button data-react-toolbox='button'
onClick={@onClick}
className={localCSS.root + ' ' + className}
disabled={@props.disabled or @state.loading}
data-flex='horizontal center'>
{ <FontIcon value={@props.icon} /> if @props.icon }
{ <abbr>{@props.caption}</abbr> if @props.caption }
<Ripple origin={@state.ripple} loading={@state.loading} />
</button>
# -- Extends
loading: (value) ->
@setState loading: value

View File

@ -0,0 +1,62 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import FontIcon from '../font_icon';
import Ripple from '../ripple';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Button',
propTypes: {
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
icon: React.PropTypes.string,
label: React.PropTypes.string,
loading: React.PropTypes.bool,
ripple: React.PropTypes.bool,
type: React.PropTypes.string
},
getDefaultProps () {
return {
className: '',
ripple: true,
type: 'flat'
};
},
getInitialState () {
return { loading: this.props.loading };
},
handleClick (event) {
if (this.props.onClick) this.props.onClick(event, this);
},
render () {
let className = this.props.className;
if (this.props.type) className += ` ${this.props.type}`;
if (this.state.focused) className += ' focused';
return (
<button
className={css.root + ' ' + className}
data-flex='horizontal center'
data-react-toolbox='button'
disabled={this.props.disabled || this.state.loading}
onClick={this.handleClick}
>
{ this.props.icon ? <FontIcon value={this.props.icon}/> : null }
{ this.props.label ? <abbr>{this.props.label}</abbr> : null }
{ this.props.ripple ? <Ripple loading={this.props.loading}/> : null }
</button>
);
},
loading (value) {
this.setState({loading: value});
}
});

View File

@ -51,10 +51,17 @@
> [data-react-toolbox='icon']
line-height : BUTTON_CIRCLE_HEIGHT
&.mini
width : BUTTON_CIRCLE_MINI_HEIGHT
height : BUTTON_CIRCLE_MINI_HEIGHT
> [data-react-toolbox='icon']
line-height : BUTTON_CIRCLE_MINI_HEIGHT
// Overrides
&[disabled]
color : darken(DIVIDER, 25%)
background-color : DIVIDER
pointer-events : none
&:not([disabled])
cursor : pointer
@ -65,7 +72,7 @@
&:not(.primary):not(.accent)
color : TEXT
background-color : WHITE
&.focused
&:active
box-shadow : ZDEPTH_SHADOW_2, inset 0 0 0 UNIT alpha(WHITE, 10%)
&.primary, &.accent
color : WHITE
@ -76,6 +83,3 @@
&:not(.primary):not(.accent) > [data-react-toolbox='ripple']
background-color : DIVIDER
> *
pointer-events: none

View File

@ -1,29 +0,0 @@
css = require './style'
dateUtils = require '../date_utils'
module.exports = React.createClass
displayName: 'Day',
propTypes:
day : React.PropTypes.number
onClick : React.PropTypes.func
selectedDate : React.PropTypes.object
viewDate : React.PropTypes.object
_dayStyle: ->
marginLeft: "#{dateUtils.firstWeekDay(@props.viewDate) * 100/7}%"
_isSelected: () ->
isSameYear = @props.viewDate.getFullYear() == @props.selectedDate.getFullYear()
isSameMonth = @props.viewDate.getMonth() == @props.selectedDate.getMonth()
isSameDay = @props.day == @props.selectedDate.getDate()
isSameYear && isSameMonth && isSameDay
render: ->
className = " #{css.day}"
className += " active" if @_isSelected()
dayStyle = @_dayStyle() if @props.day == 1
<div className={className} style={dayStyle}>
<span onClick={@props.onClick}>{ @props.day }</span>
</div>

View File

@ -0,0 +1,41 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import time from '../utils/time';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Day',
propTypes: {
day: React.PropTypes.number,
onClick: React.PropTypes.func,
selectedDate: React.PropTypes.object,
viewDate: React.PropTypes.object
},
dayStyle () {
if (this.props.day === 1) {
return {
marginLeft: `${time.getFirstWeekDay(this.props.viewDate) * 100 / 7}%`
};
}
},
isSelected () {
const sameYear = this.props.viewDate.getFullYear() === this.props.selectedDate.getFullYear();
const sameMonth = this.props.viewDate.getMonth() === this.props.selectedDate.getMonth();
const sameDay = this.props.day === this.props.selectedDate.getDate();
return sameYear && sameMonth && sameDay;
},
render () {
return (
<div className={this.isSelected() ? `${css.day} active` : css.day} style={this.dayStyle()}>
<span onClick={this.props.onClick}>{this.props.day}</span>
</div>
);
}
});

View File

@ -1,91 +0,0 @@
CTG = React.addons.CSSTransitionGroup
css = require './style'
dateUtils = require '../date_utils'
FontIcon = require '../font_icon'
Month = require './month'
module.exports = React.createClass
displayName: 'Calendar',
# -- States & Properties
propTypes:
display : React.PropTypes.oneOf(['months', 'years'])
onChange : React.PropTypes.func
selectedDate : React.PropTypes.object
viewDate : React.PropTypes.object
getDefaultProps: ->
display : 'months'
selectedDate : new Date()
getInitialState: ->
selectedDate : @props.selectedDate
viewDate : @props.selectedDate
# -- Lifecycle
componentDidUpdate: (prevProps, prevState) ->
@_scrollToActive() if @refs.activeYear
if prevState.selectedDate.getTime() != @state.selectedDate.getTime() && @props.onChange
@props.onChange? @
# -- Events
onDayClick: (event) ->
@setState
selectedDate: dateUtils.setDay(@state.viewDate, parseInt(event.target.textContent))
onYearClick: (event) ->
newDate = dateUtils.setYear(@state.viewDate, parseInt(event.target.textContent))
@setState
selectedDate: newDate
viewDate: newDate
# -- Private methods
_scrollToActive: ->
@refs.years.getDOMNode().scrollTop =
@refs.activeYear.getDOMNode().offsetTop -
@refs.years.getDOMNode().offsetHeight/2 +
@refs.activeYear.getDOMNode().offsetHeight/2
# -- Public methods
getValue: ->
@state.selectedDate
incrementViewMonth: ->
@setState
direction: 'right'
viewDate: dateUtils.addMonths(@state.viewDate, 1)
decrementViewMonth: ->
@setState
direction: 'left'
viewDate: dateUtils.addMonths(@state.viewDate, -1)
# -- Render
renderYear: (year) ->
props =
className : if year == @state.viewDate.getFullYear() then 'active' else ''
key : "year-#{year}"
onClick : @onYearClick
props.ref = 'activeYear' if year == @state.viewDate.getFullYear()
return <li {...props}>{ year }</li>
render: ->
<div className={css.root}>
{ if @props.display == 'months'
<div className={@state.direction}>
<FontIcon className={css.prev} value='chevron_left' onClick={@decrementViewMonth} />
<FontIcon className={css.next} value='chevron_right' onClick={@incrementViewMonth} />
<CTG transitionName='slide-horizontal'>
<Month
key={@state.viewDate.getMonth()}
viewDate={@state.viewDate}
selectedDate={@state.selectedDate}
onDayClick={@onDayClick} />
</CTG>
</div>
else if @props.display == 'years'
<ul ref="years" className={css.years}>
{ @renderYear(i) for i in [1900..2100] }
</ul>
}
</div>

View File

@ -0,0 +1,121 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import utils from '../utils';
import FontIcon from '../font_icon';
import Month from './month';
const { CSSTransitionGroup: CTG } = React.addons;
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Calendar',
propTypes: {
display: React.PropTypes.oneOf(['months', 'years']),
onChange: React.PropTypes.func,
selectedDate: React.PropTypes.object,
viewDate: React.PropTypes.object
},
getDefaultProps () {
return {
display: 'months',
selectedDate: new Date()
};
},
getInitialState () {
return {
selectedDate: this.props.selectedDate,
viewDate: this.props.selectedDate
};
},
componentDidUpdate () {
if (this.refs.activeYear) {
this.scrollToActive();
}
},
onDayClick (event) {
let newDate = utils.time.setDay(this.state.viewDate, parseInt(event.target.textContent));
this.setState({selectedDate: newDate});
if (this.props.onChange) this.props.onChange(newDate);
},
onYearClick (event) {
let newDate = utils.time.setYear(this.state.selectedDate, parseInt(event.target.textContent));
this.setState({selectedDate: newDate, viewDate: newDate});
if (this.props.onChange) this.props.onChange(newDate);
},
scrollToActive () {
this.refs.years.getDOMNode().scrollTop =
this.refs.activeYear.getDOMNode().offsetTop -
this.refs.years.getDOMNode().offsetHeight / 2 +
this.refs.activeYear.getDOMNode().offsetHeight / 2;
},
incrementViewMonth () {
this.setState({
direction: 'right',
viewDate: utils.time.addMonths(this.state.viewDate, 1)
});
},
decrementViewMonth () {
this.setState({
direction: 'left',
viewDate: utils.time.addMonths(this.state.viewDate, -1)
});
},
renderYear (year) {
let props = {
className: year === this.state.viewDate.getFullYear() ? 'active' : '',
key: `year-${year}`,
onClick: this.onYearClick
};
if (year === this.state.viewDate.getFullYear()) {
props.ref = 'activeYear';
}
return (<li {...props}>{ year }</li>);
},
renderYears () {
return (
<ul ref="years" className={css.years}>
{ utils.range(1900, 2100).map(i => { return this.renderYear(i); })}
</ul>
);
},
renderMonths () {
return (
<div className={this.state.direction}>
<FontIcon className={css.prev} value='chevron_left' onClick={this.decrementViewMonth}/>
<FontIcon className={css.next} value='chevron_right' onClick={this.incrementViewMonth}/>
<CTG transitionName='slide-horizontal'>
<Month
key={this.state.viewDate.getMonth()}
viewDate={this.state.viewDate}
selectedDate={this.state.selectedDate}
onDayClick={this.onDayClick} />
</CTG>
</div>
);
},
render () {
return (
<div className={css.root}>
{ this.props.display === 'months' ? this.renderMonths() : this.renderYears() }
</div>
);
}
});

View File

@ -1,29 +0,0 @@
css = require './style'
Day = require './day'
util = require '../date_utils'
module.exports = React.createClass
displayName: 'Month',
propTypes:
onDayClick : React.PropTypes.func
selectedDate : React.PropTypes.object
viewDate : React.PropTypes.object
render: ->
<div>
<span className={css.title}>
{ util.monthInWords(@props.viewDate)} {@props.viewDate.getFullYear() }
</span>
<div className={css.week}>
{ <span key={"dw#{i}"}>{ util.weekDayInWords(i).charAt(0) }</span> for i in [0..6] }
</div>
<div className={css.days}>
{ for i in [1..util.daysInMonth(@props.viewDate)]
<Day key={"d#{i}"}
day={i}
onClick={@props.onDayClick}
selectedDate={@props.selectedDate}
viewDate={@props.viewDate} /> }
</div>
</div>

View File

@ -0,0 +1,52 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import utils from '../utils';
import Day from './day';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Month',
propTypes: {
onDayClick: React.PropTypes.func,
selectedDate: React.PropTypes.object,
viewDate: React.PropTypes.object
},
renderWeeks () {
return utils.range(0, 7).map(i => {
return (
<span key={`dw${i}`}>
{ utils.time.getFullDayOfWeek(i).charAt(0) }
</span>
);
});
},
renderDays () {
return utils.range(1, utils.time.getDaysInMonth(this.props.viewDate) + 1).map(i => {
return (
<Day key={`d${i}`}
day={i}
onClick={this.props.onDayClick}
selectedDate={this.props.selectedDate}
viewDate={this.props.viewDate} />
);
});
},
render () {
return (
<div>
<span className={css.title}>
{ utils.time.getFullMonth(this.props.viewDate)} {this.props.viewDate.getFullYear() }
</span>
<div className={css.week}>{ this.renderWeeks() }</div>
<div className={css.days}>{ this.renderDays() }</div>
</div>
);
}
});

View File

@ -1,72 +0,0 @@
localCSS = require './style'
Navigation = require '../navigation'
Ripple = require '../ripple'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
color : React.PropTypes.string
image : React.PropTypes.string
text : React.PropTypes.string
legend : React.PropTypes.string
loading : React.PropTypes.bool
onClick : React.PropTypes.func
title : React.PropTypes.string
type : React.PropTypes.string
getDefaultProps: ->
className : ''
loading : false
type : 'default'
getInitialState: ->
loading : @props.loading
ripple : undefined
# -- Lifecycle
componentWillReceiveProps: ->
@setState ripple: undefined
# -- Events
onClick: (event) ->
event.preventDefault() if @props.onClick?
client = event.target.getBoundingClientRect?()
@setState ripple:
left : event.pageX - client?.left
top : event.pageY - client?.top
width : (client?.width * 2.5)
@props.onClick? event, @
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
className += ' touch' if @props.onClick?
className += ' image' if @props.image?
className += ' color' if @props.color?
className += ' loading' if @state.loading
style = {}
style.backgroundImage = "url(#{@props.image})" if @props.image?
style.backgroundColor = @props.color if @props.color
<div data-react-toolbox='card' className={className} onClick={@onClick}>
{
if @props.title or @props.image
<figure className={localCSS.figure} style={style}>
{ <small>{@props.subtitle}</small> if @props.subtitle }
{ <h2>{@props.title}</h2> if @props.title }
</figure>
}
{ <p>{@props.text}</p> if @props.text }
{ <small>{@props.legend}</small> if @props.legend }
{ <Navigation className={localCSS.navigation} actions={@props.actions} /> if @props.actions }
{ <Ripple className={localCSS.ripple} origin={@state.ripple} loading={@state.loading} /> }
</div>
# -- Extends
loading: (value) ->
attributes = loading: value
attributes.ripple = undefined unless value
@setState attributes

82
components/card/index.jsx Normal file
View File

@ -0,0 +1,82 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
import Navigation from '../navigation';
import Ripple from '../ripple';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Card',
propTypes: {
className: React.PropTypes.string,
color: React.PropTypes.string,
image: React.PropTypes.string,
text: React.PropTypes.string,
legend: React.PropTypes.string,
loading: React.PropTypes.bool,
onClick: React.PropTypes.func,
title: React.PropTypes.string,
type: React.PropTypes.string
},
getDefaultProps () {
return {
className: '',
loading: false,
type: 'default'
};
},
getInitialState () {
return {
loading: this.props.loading
};
},
onClick (event) {
if (this.props.onClick) {
event.preventDefault();
this.props.onClick(event, this);
}
},
renderHeading () {
let headingStyle = {};
if (this.props.image) headingStyle.backgroundImage = `url(${this.props.image})`;
if (this.props.color) headingStyle.backgroundColor = this.props.color;
if (this.props.title || this.props.image) {
return (
<figure className={style.figure} style={headingStyle}>
{ this.props.subtitle ? <small>{this.props.subtitle}</small> : null }
{ this.props.title ? <h2>{this.props.title}</h2> : null }
</figure>
);
}
},
render () {
let className = `${style.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
if (this.props.onClick) className += ' touch';
if (this.props.image) className += ' image';
if (this.props.color) className += ' color';
if (this.state.loading) className += ' loading';
return (
<div data-react-toolbox='card' className={className} onMouseDown={this.onClick}>
{ this.renderHeading() }
{ this.props.text ? <p>{this.props.text}</p> : null }
{ this.props.legend ? <small>{this.props.legend}</small> : null}
{ this.props.actions ? <Navigation className={style.navigation} actions={this.props.actions} /> : null }
{ <Ripple ref="ripple" className={style.ripple} loading={this.state.loading} /> }
</div>
);
},
loading (value) {
this.setState({loading: value});
}
});

View File

@ -6,7 +6,6 @@ OFFSET = (SPACE / 1.25)
:local(.navigation)
padding : OFFSET = (OFFSET / 2)
> *
pointer-events : all
padding-left : OFFSET
padding-right : OFFSET
box-shadow : none !important
@ -33,17 +32,14 @@ OFFSET = (SPACE / 1.25)
background : WHITE
// -- Children
*:not(button)
pointer-events : none
> *:not(:local(.ripple)):not(:local(.navigation))
padding : OFFSET
&:not(.color):not(.image) > :local(.ripple)
background-color : DIVIDER
background-color : red
&:not(.color) > *:not(figure), > *:not(:last-child)
box-shadow : 0 1px darken(BACKGROUND, 5%)
> :local(.figure)
// -- Overrides
&.touch
cursor : pointer
@ -56,10 +52,11 @@ OFFSET = (SPACE / 1.25)
color : WHITE
&.loading
cursor : none
pointer-events : none
-webkit-filter : grayscale(100%)
&, &:hover
box-shadow : 0 0 0 1px DIVIDER
> :local(.ripple)
:local(.ripple)
width : SIZE = (SIZE * 2)
height : SIZE
&.small > :local(.figure)
@ -68,3 +65,6 @@ OFFSET = (SPACE / 1.25)
height : SIZE
&.wide
width : (SIZE * 2)
:local(.ripple)
background-color: #888

View File

@ -1,35 +0,0 @@
css = require './style'
module.exports = React.createClass
displayName : 'Face'
# -- States & Properties
getDefaultProps: ->
active : null
numbers : []
radius : 0
# -- Internal methods
_numberStyle: (radius, num) ->
position : 'absolute'
left : (radius + radius * Math.sin(360 * (Math.PI/180) / 12 * (num - 1)) + @props.spacing)
top : (radius - radius * Math.cos(360 * (Math.PI/180) / 12 * (num - 1)) + @props.spacing)
_faceStyle: ->
height : @props.radius * 2
width : @props.radius * 2
# -- Render
render: ->
<div ref="root"
className={css.face}
onTouchStart={@props.onTouchStart}
onMouseDown={@props.onMouseDown}
style={@_faceStyle()}>
{ for i, k in @props.numbers
<span className={css.number + (if parseInt(i) == @props.active then ' active' else '')}
style={@_numberStyle(@props.radius - @props.spacing, k + 1)}
key={i} >
{i}
</span> }
</div>

56
components/clock/face.jsx Normal file
View File

@ -0,0 +1,56 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Face',
getDefaultProps () {
return {
active: null,
numbers: [],
radius: 0,
twoDigits: false
};
},
numberStyle (rad, num) {
return {
position: 'absolute',
left: (rad + rad * Math.sin(360 * (Math.PI / 180) / 12 * (num - 1)) + this.props.spacing),
top: (rad - rad * Math.cos(360 * (Math.PI / 180) / 12 * (num - 1)) + this.props.spacing)
};
},
faceStyle () {
return {
height: this.props.radius * 2,
width: this.props.radius * 2
};
},
renderNumber (number, idx) {
return (
<span className={css.number + (number === this.props.active ? ' active' : '')}
style={this.numberStyle(this.props.radius - this.props.spacing, idx + 1)}
key={number}>
{ this.props.twoDigits ? ('0' + number).slice(-2) : number }
</span>
);
},
render () {
return (
<div ref="root"
className={css.face}
onTouchStart={this.props.onTouchStart}
onMouseDown={this.props.onMouseDown}
style={this.faceStyle()}>
{ this.props.numbers.map(this.renderNumber)}
</div>
);
}
});

View File

@ -1,125 +0,0 @@
css = require './style'
prefixer = require "../prefixer"
module.exports = React.createClass
displayName : 'Hand'
# -- States & Properties
propTypes:
className : React.PropTypes.string
initialAngle : React.PropTypes.number
onHandChange : React.PropTypes.func
onHandMoved : React.PropTypes.func
getDefaultProps: ->
className : ''
initialAngle : 0
length : 0
origin : {}
getInitialState: ->
angle : @props.initialAngle
knobWidth : 0
radius : 0
# -- Lifecycle
componentDidMount: ->
@setState knobWidth: @refs.knob.getDOMNode().offsetWidth
componentWillUpdate: (nextProps, nextState) ->
if nextState.angle != @state.angle ||
nextProps.length != @props.length &&
@props.length != 0
@props.onHandChange(nextState.angle)
# -- Event handlers
_getMouseEventMap: ->
mousemove : @onMouseMove
mouseup : @onMouseUp
_getTouchEventMap: ->
touchmove: @onTouchMove
touchend: @onTouchEnd
onMouseMove: (event) ->
@_move(_getMousePosition(event))
onTouchMove: (event) ->
@_move(_getTouchPosition(event))
onMouseUp: ->
@_end(@_getMouseEventMap())
onTouchEnd: ->
@_end(@_getTouchEventMap())
# -- Public API
mouseStart: (event) ->
_addEventsToDocument(@_getMouseEventMap())
@_move(_getMousePosition(event))
touchStart: (event) ->
_addEventsToDocument(@_getTouchEventMap())
@_move(_getTouchPosition(event))
_pauseEvent(event)
# -- Internal methods
_move: (position) ->
@props.onHandMouseMove(@_getPositionRadius(position)) if @props.onHandMouseMove
newDegrees = @_trimAngleToValue(@_positionToAngle(position))
newDegrees = if newDegrees == 360 then 0 else newDegrees
@setState(angle: newDegrees) if @state.angle != newDegrees
_getPositionRadius: (position) ->
x = @props.origin.x - position.x
y = @props.origin.y - position.y
Math.sqrt(x * x + y * y)
_trimAngleToValue: (angle) ->
@props.step * Math.round(angle/@props.step)
_positionToAngle: (position) ->
_angle360(@props.origin.x, @props.origin.y, position.x, position.y)
_end: (events) ->
@props.onHandMoved() if @props.onHandMoved
_removeEventsFromDocument(events)
# -- Render
render: ->
style = prefixer.transform("rotate(#{@state.angle}deg)")
style.height = @props.length - @state.knobWidth/2
<div className={css.hand + ' ' + @props.className} style={style}>
<div ref='knob' className={css.knob}></div>
</div>
# -- Static Helper functions
_addEventsToDocument = (events) ->
document.addEventListener(key, events[key], false) for key of events
_removeEventsFromDocument = (events) ->
document.removeEventListener(key, events[key], false) for key of events
_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']
_angle360 = (cx, cy, ex, ey) ->
theta = _angle(cx, cy, ex, ey)
if (theta < 0) then 360 + theta else theta
_angle = (cx, cy, ex, ey) ->
theta = Math.atan2(ey - cy, ex - cx) + Math.PI/2
theta * 180 / Math.PI

114
components/clock/hand.jsx Normal file
View File

@ -0,0 +1,114 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import utils from '../utils';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Hand',
propTypes: {
className: React.PropTypes.string,
angle: React.PropTypes.number,
onMove: React.PropTypes.func,
onMoved: React.PropTypes.func
},
getDefaultProps () {
return {
className: '',
angle: 0,
length: 0,
origin: {}
};
},
getInitialState () {
return { knobWidth: 0 };
},
componentDidMount () {
this.setState({knobWidth: this.refs.knob.getDOMNode().offsetWidth});
},
getMouseEventMap () {
return {
mousemove: this.onMouseMove,
mouseup: this.onMouseUp
};
},
getTouchEventMap () {
return {
touchmove: this.onTouchMove,
touchend: this.onTouchEnd
};
},
onMouseMove (event) {
this.move(utils.events.getMousePosition(event));
},
onTouchMove (event) {
this.move(utils.events.getTouchPosition(event));
},
onMouseUp () {
this.end(this.getMouseEventMap());
},
onTouchEnd () {
this.end(this.getTouchEventMap());
},
mouseStart (event) {
utils.events.addEventsToDocument(this.getMouseEventMap());
this.move(utils.events.getMousePosition(event));
},
touchStart (event) {
utils.events.addEventsToDocument(this.getTouchEventMap());
this.move(utils.events.getTouchPosition(event));
utils.events.pauseEvent(event);
},
getPositionRadius (position) {
let x = this.props.origin.x - position.x;
let y = this.props.origin.y - position.y;
return Math.sqrt(x * x + y * y);
},
trimAngleToValue (angle) {
return this.props.step * Math.round(angle / this.props.step);
},
positionToAngle (position) {
return utils.angle360FromPositions(this.props.origin.x, this.props.origin.y, position.x, position.y);
},
end (events) {
if (this.props.onMoved) this.props.onMoved();
utils.events.removeEventsFromDocument(events);
},
move (position) {
let degrees = this.trimAngleToValue(this.positionToAngle(position));
let radius = this.getPositionRadius(position);
if (this.props.onMove) this.props.onMove(degrees === 360 ? 0 : degrees, radius);
},
render () {
let style = utils.prefixer({
height: this.props.length - this.state.knobWidth / 2,
transform: `rotate(${this.props.angle}deg)`
});
return (
<div className={css.hand + ' ' + this.props.className} style={style}>
<div ref='knob' className={css.knob}></div>
</div>
);
}
});

View File

@ -1,77 +0,0 @@
Face = require './face'
Hand = require './hand'
module.exports = React.createClass
displayName : 'Hours'
# -- States & Properties
propTypes:
format : React.PropTypes.oneOf(['24hr', 'ampm'])
onChange : React.PropTypes.func
onHandMoved : React.PropTypes.func
selected : React.PropTypes.number
getInitialState: ->
innerNumber : @props.format == '24hr' && 0 < @props.selected <= 12
# -- Events
_onHandMouseMove: (radius) ->
if @props.format == '24hr'
currentInner = radius < @props.radius - @props.spacing * 2
@setState innerNumber: currentInner if @state.innerNumber != currentInner
_onHandChange: (degrees) ->
@props.onChange(@_valueFromDegrees(degrees))
_onMouseDown: (event)->
@refs.hand.mouseStart(event)
_onTouchStart: (event) ->
@refs.hand.touchStart(event)
# -- Internal Methods
_valueFromDegrees: (degrees) ->
if @props.format == 'ampm' || @props.format == '24hr' && @state.innerNumber
parseInt(INNER_NUMBERS[degrees/STEP])
else
parseInt(OUTER_NUMBERS[degrees/STEP])
# -- Render
render: ->
innerRadius = @props.radius - @props.spacing * 2
handRadius = if @state.innerNumber then innerRadius else @props.radius
handLength = handRadius - @props.spacing
ampmActive = if @props.format == '24hr' then @props.selected else @props.selected % 12 || 12
<div>
<Face
onTouchStart={@_onTouchStart}
onMouseDown={@_onMouseDown}
numbers={if @props.format == '24hr' then OUTER_NUMBERS else INNER_NUMBERS}
spacing={@props.spacing}
radius={@props.radius}
active={ampmActive} />
{
if @props.format == '24hr'
<Face
onMouseDown={@_onMouseDown}
numbers={INNER_NUMBERS}
spacing={@props.spacing}
radius={innerRadius}
active={@props.selected} />
}
<Hand ref='hand'
degrees={@state.degrees}
initialAngle={@props.selected * STEP}
length={handLength}
onHandMouseMove={@_onHandMouseMove}
onHandMoved={@props.onHandMoved}
onHandChange={@_onHandChange}
origin={@props.center}
step={STEP} />
</div>
# -- Private constants
INNER_NUMBERS = [12].concat([1..11])
OUTER_NUMBERS = ['00'].concat([13..23])
STEP = 360/12

View File

@ -0,0 +1,93 @@
/* global React */
import { addons } from 'react/addons';
import utils from '../utils';
import Face from './face';
import Hand from './hand';
const outerNumbers = [0, ...utils.range(13, 24)];
const innerNumbers = [12, ...utils.range(1, 12)];
const step = 360 / 12;
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Hours',
propTypes: {
format: React.PropTypes.oneOf(['24hr', 'ampm']),
onChange: React.PropTypes.func,
onHandMoved: React.PropTypes.func,
selected: React.PropTypes.number
},
getInitialState () {
return {
inner: this.props.format === '24hr' && this.props.selected > 0 && this.props.selected <= 12
};
},
onHandMove (degrees, radius) {
let currentInner = radius < this.props.radius - this.props.spacing * 2;
this.props.onChange(this.valueFromDegrees(degrees));
if (this.props.format === '24hr' && this.state.inner !== currentInner) {
this.setState({inner: currentInner});
}
},
onMouseDown (event) {
this.refs.hand.mouseStart(event);
},
onTouchStart (event) {
this.refs.hand.touchStart(event);
},
valueFromDegrees (degrees) {
if (this.props.format === 'ampm' || this.props.format === '24hr' && this.state.inner) {
return innerNumbers[degrees / step];
} else {
return outerNumbers[degrees / step];
}
},
renderInnerFace (innerRadius) {
if (this.props.format === '24hr') {
return (
<Face
onTouchStart={this.onTouchStart}
onMouseDown={this.onMouseDown}
numbers={innerNumbers}
spacing={this.props.spacing}
radius={innerRadius}
active={this.props.selected} />
);
}
},
render () {
const { format, selected, radius, spacing, center, onHandMoved } = this.props;
const is24hr = format === '24hr';
return (
<div>
<Face
onTouchStart={this.onTouchStart}
onMouseDown={this.onMouseDown}
numbers={is24hr ? outerNumbers : innerNumbers}
spacing={spacing}
radius={radius}
twoDigits={is24hr}
active={is24hr ? selected : (selected % 12 || 12)} />
{ this.renderInnerFace(radius - spacing * 2) }
<Hand ref='hand'
angle={selected * step}
length={(this.state.inner ? radius - spacing * 2 : radius) - spacing}
onMove={this.onHandMove}
onMoved={onHandMoved}
origin={center}
step={step} />
</div>
);
}
});

View File

@ -1,100 +0,0 @@
css = require './style'
dateUtils = require '../date_utils'
Hours = require './hours'
Minutes = require './minutes'
module.exports = React.createClass
displayName : 'Clock'
# -- States & Properties
propTypes:
className : React.PropTypes.string
display : React.PropTypes.oneOf(['hours', 'minutes'])
format : React.PropTypes.oneOf(['24hr', 'ampm'])
initialTime : React.PropTypes.object
onChange : React.PropTypes.func
getDefaultProps: ->
className : ''
display : 'hours'
format : '24hr'
initialTime : new Date()
getInitialState: ->
radius : 0
time : @props.initialTime
# -- Lifecycle
componentDidMount: ->
window.addEventListener('resize', @handleResize)
@setState radius: @_getRadius()
componentWillUpdate: (props, state) ->
center = @_getCenter()
if state.time.getTime() != @state.time.getTime() && @props.onChange
@props.onChange(state.time)
if @state.center?.x != center.x && @state.center?.y != center.y
@setState center: center
componentWillUnmount: ->
window.removeEventListener('resize', @handleResize)
# -- Events handlers
onHourChange: (hours) ->
@setState time: dateUtils.setHours(@state.time, @_adaptHourToFormat(hours))
onMinuteChange: (minutes) ->
@setState time: dateUtils.setMinutes(@state.time, minutes)
# -- Helper methods
_getRadius: ->
@refs.wrapper.getDOMNode().getBoundingClientRect().width/2
_adaptHourToFormat: (hour) ->
if @props.format == 'ampm'
if dateUtils.timeMode(@state.time) == 'pm'
if hour < 12 then hour + 12 else hour
else
if hour == 12 then 0 else hour
else
hour
handleResize: ->
@setState
center: @_getCenter()
radius: @_getRadius()
_getCenter: ->
bounds = @getDOMNode().getBoundingClientRect()
{
x: bounds.left + (bounds.right - bounds.left)/2
y: bounds.top + (bounds.bottom - bounds.top) /2
}
# -- Public methods
toggleTimeMode: ->
@setState time: dateUtils.toggleTimeMode(@state.time)
# -- Render
render: ->
<div className={css.root}>
<div ref="wrapper" className={css.wrapper} style={height: @state.radius * 2} >
{
if @props.display == 'minutes'
<Minutes
center={@state.center}
onChange={@onMinuteChange}
radius={@state.radius}
selected={@state.time.getMinutes()}
spacing={@state.radius * 0.16} />
else if @props.display == 'hours'
<Hours
center={@state.center}
format={@props.format}
onChange={@onHourChange}
radius={@state.radius}
selected={@state.time.getHours()}
spacing={@state.radius * 0.16} />
}
</div>
</div>

123
components/clock/index.jsx Normal file
View File

@ -0,0 +1,123 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import time from '../utils/time';
import Hours from './hours';
import Minutes from './minutes';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Clock',
propTypes: {
className: React.PropTypes.string,
display: React.PropTypes.oneOf(['hours', 'minutes']),
format: React.PropTypes.oneOf(['24hr', 'ampm']),
initialTime: React.PropTypes.object,
onChange: React.PropTypes.func
},
getDefaultProps () {
return {
className: '',
display: 'hours',
format: '24hr',
initialTime: new Date()
};
},
getInitialState () {
return {
center: {x: null, y: null},
radius: 0,
time: this.props.initialTime
};
},
componentDidMount () {
window.addEventListener('resize', this.calculateShape);
this.calculateShape();
},
componentWillUnmount () {
window.removeEventListener('resize', this.calculateShape);
},
onHourChange (hours) {
if (this.state.time.getHours() !== hours) {
const newTime = time.setHours(this.state.time, this.adaptHourToFormat(hours));
this.setState({time: newTime});
if (this.props.onChange) this.props.onChange(newTime);
}
},
onMinuteChange (minutes) {
if (this.state.time.getMinutes() !== minutes) {
const newTime = time.setMinutes(this.state.time, minutes);
this.setState({time: newTime});
if (this.props.onChange) this.props.onChange(newTime);
}
},
toggleTimeMode () {
const newTime = time.toggleTimeMode(this.state.time);
this.setState({time: newTime});
if (this.props.onChange) this.props.onChange(newTime);
},
adaptHourToFormat (hour) {
if (this.props.format === 'ampm') {
if (time.getTimeMode(this.state.time) === 'pm') {
return hour < 12 ? hour + 12 : hour;
} else {
return hour === 12 ? 0 : hour;
}
} else {
return hour;
}
},
calculateShape () {
let { top, left, width } = this.refs.wrapper.getDOMNode().getBoundingClientRect();
this.setState({
center: { x: left + width / 2, y: top + width / 2 },
radius: width / 2
});
},
renderHours () {
return (
<Hours
center={this.state.center}
format={this.props.format}
onChange={this.onHourChange}
radius={this.state.radius}
selected={this.state.time.getHours()}
spacing={this.state.radius * 0.16} />
);
},
renderMinutes () {
return (
<Minutes
center={this.state.center}
onChange={this.onMinuteChange}
radius={this.state.radius}
selected={this.state.time.getMinutes()}
spacing={this.state.radius * 0.16} />
);
},
render () {
return (
<div className={css.root}>
<div ref="wrapper" className={css.wrapper} style={{height: this.state.radius * 2}}>
{ this.props.display === 'hours' ? this.renderHours() : '' }
{ this.props.display === 'minutes' ? this.renderMinutes() : '' }
</div>
</div>
);
}
});

View File

@ -1,49 +0,0 @@
Face = require './face'
Hand = require './hand'
module.exports = React.createClass
displayName: 'Minutes'
# -- States & Properties
propTypes:
selected : React.PropTypes.number
onChange : React.PropTypes.func
getDefaultProps: ->
selected : 0
onChange : null
# -- Events
_onHandChange: (degrees) ->
@props.onChange(degrees/STEP)
_onMouseDown: (event)->
@refs.hand.mouseStart(event)
_onTouchStart: (event) ->
@refs.hand.touchStart(event)
# -- Render
render: ->
handClass = if MINUTES.indexOf(('0' + @props.selected).slice(-2)) == -1 then 'smallKnob' else ''
<div>
<Face
onTouchStart={@_onTouchStart}
onMouseDown={@_onMouseDown}
numbers={MINUTES}
spacing={@props.spacing}
radius={@props.radius}
active={@props.selected} />
<Hand ref='hand'
className={handClass}
initialAngle={@props.selected * STEP}
length={@props.radius - @props.spacing}
onHandChange={@_onHandChange}
origin={@props.center}
step={STEP} />
</div>
# -- Private constants
MINUTES = (('0' + i).slice(-2) for i in [0..55] by 5)
STEP = 360/60

View File

@ -0,0 +1,61 @@
/* global React */
import { addons } from 'react/addons';
import utils from '../utils';
import Face from './face';
import Hand from './hand';
const minutes = utils.range(0, 60, 5);
const step = 360 / 60;
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Minutes',
propTypes: {
selected: React.PropTypes.number,
onChange: React.PropTypes.func
},
getDefaultProps () {
return {
selected: 0,
onChange: null
};
},
onHandMove (degrees) {
this.props.onChange(degrees / step);
},
onMouseDown (event) {
this.refs.hand.mouseStart(event);
},
onTouchStart (event) {
this.refs.hand.touchStart(event);
},
render () {
return (
<div>
<Face
onTouchStart={this.onTouchStart}
onMouseDown={this.onMouseDown}
numbers={minutes}
spacing={this.props.spacing}
radius={this.props.radius}
twoDigits={true}
active={this.props.selected} />
<Hand ref='hand'
className={minutes.indexOf(this.props.selected) === -1 ? 'smallKnob' : ''}
angle={this.props.selected * step}
length={this.props.radius - this.props.spacing}
onMove={this.onHandMove}
origin={this.props.center}
step={step} />
</div>
);
}
});

View File

@ -36,6 +36,7 @@ HEADER_HEIGHT = (1.65 * UNIT)
INPUT_HEIGHT = (2 * SPACE)
BUTTON_HEIGHT = (2.5 * SPACE)
BUTTON_CIRCLE_HEIGHT = (2.75 * SPACE)
BUTTON_CIRCLE_MINI_HEIGHT = (2 * SPACE)
LOADING_HEIGHT = (1.5 * UNIT)
PROGRESS_BAR_HEIGHT = (SPACE / 4)

View File

@ -1,70 +0,0 @@
css = require './style'
dateUtils = require '../date_utils'
Dialog = require '../dialog'
Calendar = require '../calendar'
module.exports = React.createClass
displayName: 'CalendarDialog'
# -- States & Properties
propTypes:
initialDate : React.PropTypes.object
onDateSelected : React.PropTypes.func
getDefaultProps: ->
initialDate : new Date()
getInitialState: ->
date : @props.initialDate
display : 'months'
actions : [
{ caption: "Cancel", type: "flat accent", onClick: @onDateCancel },
{ caption: "Ok", type: "flat accent", onClick: @onDateSelected }
]
# -- Events
onCalendarChange: (calendar) ->
@setState
date: dateUtils.cloneDatetime(calendar.getValue())
display: 'months'
onDateCancel: (ref, method) ->
@refs.dialog.hide()
onDateSelected: ->
@props.onDateSelected(@state.date) if @props.onDateSelected
@refs.dialog.hide()
# -- Public methods
show: ->
@refs.dialog.show()
displayMonths: ->
@setState display: 'months'
displayYears: ->
@setState display: 'years'
# -- Render
render: ->
className = "display-#{@state.display}"
<Dialog ref="dialog" type={css.dialog} className={className} actions={@state.actions}>
<header className={css.header}>
<span className={css.headerWeekday}>{dateUtils.weekDayInWords(@state.date.getDay())}</span>
<div onClick={@displayMonths}>
<span className={css.headerMonth}>{dateUtils.monthInShortWords(@state.date)}</span>
<span className={css.headerDay}>{@state.date.getDate()}</span>
</div>
<span className={css.headerYear} onClick={@displayYears}>
{@state.date.getFullYear()}
</span>
</header>
<div className={css.calendarWrapper}>
<Calendar
ref="calendar"
display={@state.display}
onChange={@onCalendarChange}
selectedDate={@state.date} />
</div>
</Dialog>

View File

@ -0,0 +1,88 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import time from '../utils/time';
import Calendar from '../calendar';
import Dialog from '../dialog';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'CalendarDialog',
propTypes: {
initialDate: React.PropTypes.object,
onDateSelected: React.PropTypes.func
},
getDefaultProps () {
return {
initialDate: new Date()
};
},
getInitialState () {
return {
date: this.props.initialDate,
display: 'months',
actions: [
{ label: 'Cancel', type: 'flat accent', onClick: this.onDateCancel },
{ label: 'Ok', type: 'flat accent', onClick: this.onDateSelected }
]
};
},
onCalendarChange (date) {
this.setState({date: date, display: 'months'});
},
onDateCancel () {
this.refs.dialog.hide();
},
onDateSelected () {
if (this.props.onDateSelected) this.props.onDateSelected(this.state.date);
this.refs.dialog.hide();
},
show () {
this.refs.dialog.show();
},
displayMonths () {
this.setState({display: 'months'});
},
displayYears () {
this.setState({display: 'years'});
},
render () {
const className = `display-${this.state.display}`;
return (
<Dialog ref="dialog" className={className} type={css.dialog} actions={this.state.actions}>
<header className={css.header}>
<span className={css.headerWeekday}>
{time.getFullDayOfWeek(this.state.date.getDay())}
</span>
<div onClick={this.displayMonths}>
<span className={css.headerMonth}>{time.getShortMonth(this.state.date)}</span>
<span className={css.headerDay}>{this.state.date.getDate()}</span>
</div>
<span className={css.headerYear} onClick={this.displayYears}>
{this.state.date.getFullYear()}
</span>
</header>
<div className={css.calendarWrapper}>
<Calendar
ref="calendar"
display={this.state.display}
onChange={this.onCalendarChange}
selectedDate={this.props.date} />
</div>
</Dialog>
);
}
});

View File

@ -1,52 +0,0 @@
css = require './style'
dateUtils = require '../date_utils'
Input = require '../input'
CalendarDialog = require './dialog'
module.exports = React.createClass
displayName : 'DatePicker'
propTypes:
className : React.PropTypes.string
value : React.PropTypes.object
getDefaultProps: ->
className : ''
getInitialState: ->
value : @props.value
# -- Events
openCalendarDialog: ->
@refs.dialog.show()
onDateSelected: (value) ->
@refs.input.setValue(@formatDate(value))
@setState value: value
# -- Private methods
formatDate: (date) ->
day = date.getDate()
month = dateUtils.monthInWords(date)
year = date.getFullYear()
"#{day} #{month} #{year}"
# -- Public methods
getValue: ->
@state.value
# -- Render
render: ->
<div>
<Input
ref="input"
type="text"
disabled={true}
onClick={@openCalendarDialog}
placeholder="Pick up date"
value={@formatDate(@state.value) if @state.value} />
<CalendarDialog
ref="dialog"
initialDate={@state.value}
onDateSelected={@onDateSelected} />
</div>

View File

@ -0,0 +1,64 @@
/* global React */
import { addons } from 'react/addons';
import time from '../utils/time';
import CalendarDialog from './dialog';
import Input from '../input';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'DatePicker',
propTypes: {
className: React.PropTypes.string,
value: React.PropTypes.object
},
getDefaultProps () {
return {
className: ''
};
},
getInitialState () {
return {
value: this.props.value
};
},
openCalendarDialog () {
this.refs.dialog.show();
},
onDateSelected (value) {
this.refs.input.setValue(this.formatDate(value));
this.setState({value: value});
},
formatDate (date) {
return `${date.getDate()} ${time.getFullMonth(date)} ${date.getFullYear()}`;
},
getValue () {
return this.state.value;
},
render () {
return (
<div>
<Input
ref="input"
type="text"
disabled={true}
onClick={this.openCalendarDialog}
placeholder="Pick up date"
value={this.state.value ? this.formatDate(this.state.value) : null} />
<CalendarDialog
ref="dialog"
initialDate={this.state.value}
onDateSelected={this.onDateSelected} />
</div>
);
}
});

View File

@ -1,103 +0,0 @@
module.exports =
daysInMonth: (date) ->
(new Date(date.getFullYear(), date.getMonth() + 1, 0)).getDate()
firstWeekDay: (date) ->
(new Date(date.getFullYear(), date.getMonth(), 1)).getDay()
monthInWords: (date) ->
switch (date.getMonth())
when 0 then 'January'
when 1 then 'February'
when 2 then 'March'
when 3 then 'April'
when 4 then 'May'
when 5 then 'June'
when 6 then 'July'
when 7 then 'August'
when 8 then 'September'
when 9 then 'October'
when 10 then 'November'
when 11 then 'December'
monthInShortWords: (date) ->
switch (date.getMonth())
when 0 then 'Jan'
when 1 then 'Feb'
when 2 then 'Mar'
when 3 then 'Apr'
when 4 then 'May'
when 5 then 'Jun'
when 6 then 'Jul'
when 7 then 'Aug'
when 8 then 'Sep'
when 9 then 'Oct'
when 10 then 'Nov'
when 11 then 'Dec'
weekDayInWords: (day) ->
switch (day)
when 0 then 'Sunday'
when 1 then 'Monday'
when 2 then 'Tuesday'
when 3 then 'Wednesday'
when 4 then 'Thursday'
when 5 then 'Friday'
when 6 then 'Saturday'
weekDayInShortWords: (day) ->
switch (day)
when 0 then 'Sun'
when 1 then 'Mon'
when 2 then 'Tue'
when 3 then 'Wed'
when 4 then 'Thu'
when 5 then 'Fri'
when 6 then 'Sat'
addDays: (date, days) ->
newDate = @cloneDatetime(date)
newDate.setDate(date.getDate() + days)
newDate
addMonths: (date, months) ->
newDate = @cloneDatetime(date)
newDate.setMonth(date.getMonth() + months)
newDate
addYears: (date, years) ->
newDate = @cloneDatetime(date)
newDate.setFullYear(date.getFullYear() + years)
newDate
setDay: (date, day) ->
newDate = @cloneDatetime(date)
newDate.setDate(day)
newDate
setYear: (date, year) ->
newDate = @cloneDatetime(date)
newDate.setFullYear(year)
newDate
cloneDatetime: (date) ->
new Date(date.getTime())
timeMode: (datetime) ->
if datetime.getHours() >= 12 then 'pm' else 'am'
toggleTimeMode: (datetime) ->
newDatetime = @cloneDatetime(datetime)
hours = datetime.getHours()
if hours > 12 then newDatetime.setHours(hours - 12) else newDatetime.setHours(hours + 12)
newDatetime
setHours: (datetime, hours) ->
newDatetime = @cloneDatetime(datetime)
newDatetime.setHours(hours)
newDatetime
setMinutes: (datetime, minutes) ->
newDatetime = @cloneDatetime(datetime)
newDatetime.setMinutes(minutes)
newDatetime

View File

@ -1,43 +0,0 @@
localCSS = require './style'
Button = require '../button'
Navigation = require '../navigation'
module.exports = React.createClass
displayName : 'Dialog'
# -- States & Properties
propTypes:
actions : React.PropTypes.array
active : React.PropTypes.bool
className : React.PropTypes.string
title : React.PropTypes.string
type : React.PropTypes.string
getDefaultProps: ->
actions : []
className : 'normal'
getInitialState: ->
active : @props.active
# -- Render
render: ->
rootClass = localCSS.root
rootClass += ' active' if @state.active
containerClass = "#{localCSS.container} #{@props.className}"
containerClass += " #{@props.type}" if @props.type
<div data-react-toolbox='dialog' data-flex='vertical center' className={rootClass}>
<div className={containerClass}>
{<h1>{@props.title}</h1> if @props.title}
{@props.children}
{<Navigation actions={@props.actions}/> if @props.actions.length > 0}
</div>
</div>
# -- Extends
show: ->
@setState active: true
hide: ->
@setState active: false

View File

@ -0,0 +1,52 @@
/* global React */
import style from './style';
import Navigation from '../navigation';
export default React.createClass({
displayName: 'Dialog',
propTypes: {
actions: React.PropTypes.array,
active: React.PropTypes.bool,
className: React.PropTypes.string,
title: React.PropTypes.string,
type: React.PropTypes.string
},
getDefaultProps () {
return {
actions: [],
className: 'normal'
};
},
getInitialState () {
return { active: this.props.active };
},
render () {
let rootClass = style.root;
let containerClass = `${style.container} ${this.props.className}`;
if (this.state.active) rootClass += ' active';
if (this.props.type) containerClass += ` ${this.props.type}`;
return (
<div data-react-toolbox='dialog' data-flex='vertical center' className={rootClass}>
<div className={containerClass}>
{ this.props.title ? <h1>{this.props.title}</h1> : null }
{ this.props.children }
{ this.props.actions.length > 0 ? <Navigation actions={this.props.actions}/> : null }
</div>
</div>
);
},
show () {
this.setState({active: true});
},
hide () {
this.setState({active: false});
}
});

View File

@ -0,0 +1,58 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Drawer',
propTypes: {
active: React.PropTypes.bool,
className: React.PropTypes.string,
hideable: React.PropTypes.bool,
type: React.PropTypes.string
},
getDefaultProps () {
return {
className: '',
type: 'left'
};
},
getInitialState () {
return { active: this.props.active };
},
handleOverlayClick () {
if (this.props.hideable) {
this.setState({active: false});
}
},
render () {
let className = `${css.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
if (this.props.hideable) className += ' hideable';
if (this.state.active) className += ' active';
return (
<div className={className}>
<div className={css.overlay} onClick={this.handleOverlayClick}></div>
<aside className={css.container}>
{ this.props.children }
</aside>
</div>
);
},
show () {
this.setState({active: true});
},
hide () {
this.setState({active: false});
}
});

View File

@ -0,0 +1,60 @@
@import '../constants'
WIDTH = (4 * UNIT)
:local(.root)
height : 100vh
pointer-events : none
position : fixed
top : 0
left : 0
right : 0
width : 100vw
z-index : 2
:local(.overlay)
background-color : black
height : 100%
opacity : 0
transition-duration : .5 * ANIMATION_DURATION
transition-property : opacity
transition-timing-function : ANIMATION_EASE
width : 100%
:local(.container)
background-color : WHITE
box-shadow : ZDEPTH_SHADOW_1
display : block
height : 100%
overflow-y : scroll
position : absolute
top : 0
transform-style : preserve-3d
transition-delay : 0s
transition-duration : .5 * ANIMATION_DURATION
transition-property : transform
transition-timing-function : ANIMATION_EASE
width : WIDTH
will-change : transform
:local(.root)
&.left
> :local(.container)
left : 0
&:not(.active) > :local(.container)
transform : translateX(- WIDTH)
&.right
> :local(.container)
right : 0
&:not(.active) > :local(.container)
transform : translateX(WIDTH)
&.active
pointer-events : all
> :local(.container)
transition-delay : ANIMATION_DELAY
transform : translateX(0)
> :local(.overlay)
opacity : .5

View File

@ -1,102 +0,0 @@
###
v2
- can set a icon like dispatcher
###
localCSS = require './style'
Ripple = require '../ripple'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
dataSource : React.PropTypes.array
disabled : React.PropTypes.bool
label : React.PropTypes.string
onChange : React.PropTypes.funca
template : React.PropTypes.func
type : React.PropTypes.string
value : React.PropTypes.string
getDefaultProps: ->
className : ''
dataSource : []
type : 'normal'
getInitialState: ->
active : false
ripple : undefined
selected : _selectValue @props.value, @props.dataSource
# -- Lifecycle
componentDidMount: ->
@setState
height: @refs.values.getDOMNode().firstElementChild.getBoundingClientRect().height
componentDidUpdate: (prev_props, prev_state) ->
@props.onChange? @ if prev_state.selected isnt @state.selected and prev_state.active
# -- Events
onSelect: (event) ->
@setState active: true, ripple: undefined unless @props.disabled
onItem: (event) ->
unless @props.disabled
client = event.target.getBoundingClientRect?()
value = event.target.getAttribute('id').toString()
for item in @props.dataSource when item.value.toString() is value
@setState
active : false
selected : item
ripple :
left : event.pageX - client?.left
top : event.pageY - client?.top
width : (client?.width * 2)
break
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
className += ' disabled' if @props.disabled
if @state.active is true
className += ' active'
stylesheet = height: @state.height * @props.dataSource.length
<div data-react-toolbox='dropdown' className={className}>
{ <label>{@props.label}</label> if @props.label }
<ul ref='values' className={localCSS.values} style={stylesheet} onClick={@onItem}>
{
for item, index in @props.dataSource
<li id={item.value} key={index} className={'selected' if item.value is @state.selected.value}>
{ if @props.template then @props.template item else item.label }
{ <Ripple className={localCSS.ripple} origin={@state.ripple}/> if item.value is @state.selected.value }
</li>
}
</ul>
<div ref='value' className={localCSS.value} onClick={@onSelect}>
{
if @props.template
@props.template @state.selected
else
<span>{@state.selected.label}</span>
}
</div>
</div>
# -- Extends
getValue: ->
@state.selected.value
setValue: (data) ->
@setState selected: data
# -- Internal methods
_selectValue = (value, dataSource) ->
if value
for item in dataSource when item.value.toString() is value.toString()
return item
break
else
dataSource[0]

View File

@ -0,0 +1,126 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
import Ripple from '../ripple';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Dropdown',
propTypes: {
className: React.PropTypes.string,
dataSource: React.PropTypes.array,
disabled: React.PropTypes.bool,
label: React.PropTypes.string,
onChange: React.PropTypes.func,
template: React.PropTypes.func,
type: React.PropTypes.string,
value: React.PropTypes.string
},
getDefaultProps () {
return {
className: '',
dataSource: [],
type: 'normal'
};
},
getInitialState () {
return {
active: false,
selected: _selectValue(this.props.value, this.props.dataSource)
};
},
componentDidMount () {
this.setState({
height: this.refs.values.getDOMNode().firstElementChild.getBoundingClientRect().height
});
},
componentDidUpdate (prev_props, prev_state) {
if (this.props.onChange && prev_state.selected !== this.state.selected && prev_state.active) {
this.props.onChange(this);
}
},
onSelect () {
if (!this.props.disabled) {
this.setState({active: true});
}
},
onItem (id) {
if (!this.props.disabled) {
let value = id.toString();
for (let item of this.props.dataSource) {
if (item.value.toString() === value) {
this.setState({
active: false,
selected: item
});
break;
}
}
}
},
render () {
let stylesheet;
let className = `${style.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
if (this.props.disabled) className += ' disabled';
if (this.state.active === true) {
className += ' active';
stylesheet = { height: this.state.height * this.props.dataSource.length };
}
const items = this.props.dataSource.map((item, index) => {
return (
<li
key={index}
id={item.value}
onClick={this.onItem.bind(this, item.value)}
style={{position: 'relative'}}
className={ item.value === this.state.selected.value ? 'selected' : null}
>
{ this.props.template ? this.props.template(item) : item.label }
<Ripple className={style.ripple}/>
</li>
);
});
return (
<div data-react-toolbox='dropdown' className={className}>
{this.props.label ? <label>{this.props.label}</label> : null}
<ul ref='values' className={style.values} style={stylesheet}>{ items }</ul>
<div ref='value' className={style.value} onClick={this.onSelect}>
{ this.props.template ? this.props.template(this.state.selected) : <span>{this.state.selected.label}</span> }
</div>
</div>
);
},
getValue () {
return this.state.selected.value;
},
setValue (data) {
this.setState({selected: data});
}
});
function _selectValue (value, dataSource) {
let item;
if (value) {
for (item of dataSource) {
if (item.value.toString() === value.toString()) break;
}
return item;
} else {
return dataSource[0];
}
}

View File

@ -26,10 +26,6 @@
cursor : pointer
&.selected
color : PRIMARY
> *
pointer-events : none
> :local(.ripple)
background-color : DIVIDER
:local(.value)
display : block
@ -74,8 +70,6 @@
opacity : 1
transition : opacity ANIMATION_DURATION ANIMATION_EASE
&:not(.active)
> ul
pointer-events : none
> *, > * > li
transition-delay : ANIMATION_DURATION
@ -99,3 +93,8 @@
bottom : -(SPACE / 4)
font-size : FONT_SIZE_TINY
color : TEXT_SECONDARY
:local(.ripple)
background-color : DIVIDER
opacity : 1
z-index : Z_INDEX_LOW

View File

@ -1,112 +0,0 @@
###
@todo
###
# -- Components
Autocomplete = require './autocomplete'
Dropdown = require './dropdown'
Input = require './input'
Switch = require './switch'
# -- Constants
TYPE =
AUTOCOMPLETE: 'autocomplete'
CHECKBOX : 'checkbox'
DROPDOWN : 'dropdown'
LABEL : 'label'
RADIO : 'radio'
SWITCH : 'switch'
module.exports = React.createClass
# -- States & Properties
propTypes:
attributes : React.PropTypes.array
className : React.PropTypes.string
label : React.PropTypes.string
value : React.PropTypes.any
onChange : React.PropTypes.func
getDefaultProps: ->
attributes : []
className : ''
getInitialState: ->
attributes : @props.attributes
type : _determineType @props.attributes
# -- Lifecycle
componentWillReceiveProps: (next_props) ->
if attributes = next_props.attributes
@setState attributes: attributes, type: _determineType attributes
@setValue next_props.value or @props.value
componentDidUpdate: ->
if @state.type is TYPE.RADIO
no_active = true
no_active = false for key, ref of @refs when ref.getValue?() is true
@refs[default].setValue true if no_active and default = Object.keys(@refs)?[0]
# -- Events
onChange: (event) ->
if @state.type is TYPE.RADIO
for ref, el of @refs when el.refs.input.getDOMNode() isnt event.target
el.setValue false
is_valid = true
value = @getValue()
for attr in @state.attributes when attr.required and value[attr.ref]?.trim() is ''
is_valid = false
@refs[attr.ref].setError? 'Required field'
break
setTimeout (=> @props.onChange event, @), 10 if @props.onChange?
# -- Render
render: ->
<div data-react-toolbox='fieldset'
className={@props.className} onChange={@onChange}>
{ <label>{@props.label}</label> if @props.label }
{
for attribute, index in @state.attributes
if attribute.type is TYPE.LABEL
<label>{attribute.caption}</label>
else if attribute.type is TYPE.AUTOCOMPLETE
<Autocomplete key={index} {...attribute} onChange={@onChange}/>
else if attribute.type is TYPE.DROPDOWN
<Dropdown key={index} {...attribute} onChange={@onChange}/>
else if attribute.type is TYPE.SWITCH
<Switch key={index} {...attribute} onChange={@onChange}/>
else
<Input key={index} {...attribute} />
}
{ @props.children }
</div>
# -- Extends
clean: ->
instance.setValue? undefined for key, instance of @refs
getValue: ->
value = {}
if @state.type isnt TYPE.RADIO
value[ref] = input.getValue() for ref, input of @refs when input.getValue?
else
value = ref for ref, input of @refs when input.getValue?() is true
value
setValue: (data = {}) ->
if data instanceof Object
@refs[key]?.setValue? value for key, value of data
else
@refs[key].setValue? key is data for key of @refs
# -- Internal methods
_determineType = (attributes) ->
type = ''
group_radio = true
group_checkbox = true
for attribute in attributes when attribute.type isnt TYPE.LABEL
group_radio = false if attribute.type isnt TYPE.RADIO
group_checkbox = false if attribute.type isnt TYPE.CHECKBOX
type = TYPE.RADIO if group_radio
type = TYPE.CHECKBOX if group_checkbox
type

View File

@ -1,20 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
displayName: 'FontIcon',
# -- States & Properties
propTypes:
className : React.PropTypes.string
value : React.PropTypes.string
getDefaultProps: ->
className : ''
onClick: (event) ->
@props.onClick? @props.onClick(event)
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className} #{@props.value}"
<span data-react-toolbox='icon' className={className} onClick={@onClick} />

View File

@ -0,0 +1,37 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'FontIcon',
propTypes: {
className: React.PropTypes.string,
value: React.PropTypes.string
},
getDefaultProps () {
return {
className: ''
};
},
onClick (event) {
if (this.props.onClick) {
this.props.onClick(event);
}
},
render () {
return (
<span
data-react-toolbox='icon'
className={`${style.root} ${this.props.className} ${this.props.value}`}
onClick={this.props.onClick}
/>
);
}
});

View File

@ -1,105 +0,0 @@
localCSS = require './style'
Autocomplete = require '../autocomplete'
Dropdown = require '../dropdown'
Button = require '../button'
Input = require '../input'
Switch = require '../switch'
module.exports = React.createClass
# -- States & Properties
propTypes:
attributes : React.PropTypes.array
className : React.PropTypes.string
onChange : React.PropTypes.func
onError : React.PropTypes.func
onSubmit : React.PropTypes.func
onValid : React.PropTypes.func
storage : React.PropTypes.string
getDefaultProps: ->
attributes : []
className : ''
getInitialState: ->
attributes : @storage @props
# -- Lifecycle
componentWillReceiveProps: (next_props) ->
if next_props.attributes
attributes = @storage next_props
@setState attributes: attributes
@setValue (item for item in attributes)
# -- Events
onSubmit: (event) ->
event.preventDefault()
@props.onSubmit? event, @
onChange: (event) ->
is_valid = true
value = @getValue()
for attr in @state.attributes when attr.required and value[attr.ref]?.trim() is ""
is_valid = false
@refs[attr.ref].setError? 'Required field'
break
@props.onChange? event, @
@storage @props, value if @props.storage
if is_valid
@refs.submit?.getDOMNode().removeAttribute 'disabled'
@props.onValid? event, @
else
@refs.submit?.getDOMNode().setAttribute 'disabled', true
@props.onError? event, @
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
<form data-react-toolbox='form' className={className}
onChange={@onChange} onSubmit={@onSubmit}>
{
for attribute, index in @state.attributes
if attribute.type is 'submit'
<Button key={index} {...attribute} type='square' ref='submit' onClick={@onSubmit}/>
else if attribute.type is 'autocomplete'
<Autocomplete key={index} {...attribute} onChange={@onChange}/>
else if attribute.type is 'dropdown'
<Dropdown key={index} {...attribute} onChange={@onChange}/>
else if attribute.type is 'switch'
<Switch key={index} {...attribute} onChange={@onChange}/>
else
<Input key={index} {...attribute} />
}
{ @props.children }
</form>
# -- Extends
storage: (props, value) ->
key = "react-toolbox-form-#{props.storage}"
if value
store = {}
store[attr.ref] = value[attr.ref] for attr in props.attributes when attr.storage
window.localStorage.setItem key, JSON.stringify store
else if props.storage
store = JSON.parse window.localStorage.getItem key or {}
input.value = store?[input.ref] or input.value for input in props.attributes
props.attributes
getValue: ->
value = {}
for ref, el of @refs when el.getValue?
if ref.indexOf('.') is -1
value[ref] = el.getValue()
else
parent = value
for attr, index in hierarchy = ref.split('.')
if index is hierarchy.length - 1
parent[attr] = el.getValue()
else
parent[attr] = parent[attr] or {}
parent = parent[attr]
value
setValue: (data = {}) ->
@refs[field.ref].setValue? field.value for field in data

159
components/form/index.jsx Normal file
View File

@ -0,0 +1,159 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
import Autocomplete from '../autocomplete';
import Dropdown from '../dropdown';
import Button from '../button';
import Input from '../input';
import Switch from '../switch';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Form',
propTypes: {
attributes: React.PropTypes.array,
className: React.PropTypes.string,
onChange: React.PropTypes.func,
onError: React.PropTypes.func,
onSubmit: React.PropTypes.func,
onValid: React.PropTypes.func,
storage: React.PropTypes.string
},
getDefaultProps () {
return {
attributes: [],
className: ''
};
},
getInitialState () {
return {
attributes: this.storage(this.props)
};
},
componentWillReceiveProps (next_props) {
if (next_props.attributes) {
let attributes = this.storage(next_props);
this.setState({attributes: attributes});
this.setValue(attributes.map((item) => { return item; }));
}
},
onSubmit (event) {
event.preventDefault();
if (this.props.onSubmit) {
this.props.onSubmit(event, this);
}
},
onChange (event) {
let is_valid = true;
let value = this.getValue();
for (let attr of this.state.attributes) {
if (attr.required && value[attr.ref] !== undefined && value[attr.ref].trim() === '') {
is_valid = false;
console.log('NOT VALUD');
if (this.refs[attr.ref].setError) this.refs[attr.ref].setError('Requited field');
break;
}
}
if (this.props.onChange) this.props.onChange(event, this);
if (this.props.storage) this.storage(this.props, value);
if (is_valid) {
if (this.refs.submit) this.refs.submit.getDOMNode().removeAttribute('disabled');
if (this.props.onValid) this.props.onValid(event, this);
} else {
if (this.refs.submit) this.refs.submit.getDOMNode().setAttribute('disabled', true);
if (this.props.onError) this.props.onError(event, this);
}
},
render () {
let className = `${style.root} ${this.props.className}`;
const attributes = this.state.attributes.map((attribute, index) => {
if (attribute.type === 'submit') {
return <Button key={index} {...attribute} type='square' ref='submit' onClick={this.onSubmit}/>;
} else if (attribute.type === 'autocomplete') {
return <Autocomplete key={index} {...attribute} onChange={this.onChange}/>;
} else if (attribute.type === 'dropdown') {
return <Dropdown key={index} {...attribute} onChange={this.onChange}/>;
} else if (attribute.type === 'switch') {
return <Switch key={index} {...attribute} onChange={this.onChange}/>;
} else {
return <Input key={index} {...attribute} />;
}
});
return (
<form
data-react-toolbox='form'
className={className}
onChange={this.onChange}
onSubmit={this.onSubmit}
>
{ attributes }
{ this.props.children }
</form>
);
},
storage (props, value) {
let key = `react-toolbox-form-${props.storage}`;
if (value) {
let store = {};
for (let attr of props.attributes) {
if (attr.storage) store[attr.ref] = value[attr.ref];
}
window.localStorage.setItem(key, JSON.stringify(store));
} else if (props.storage) {
let store = JSON.parse(window.localStorage.getItem(key) || {});
for (let input of props.attributes) {
if (store && store[input.ref]) {
input.value = store[input.ref];
}
}
}
return props.attributes;
},
getValue () {
let value = {};
for (let ref of Object.keys(this.refs)) {
let el = this.refs[ref];
if (el.getValue) {
if (ref.indexOf('.') === -1) {
value[ref] = el.getValue();
} else {
let parent = value;
let hierarchy = ref.split('.');
hierarchy.forEach((attr, index) => {
if (index === hierarchy.length - 1) {
parent[attr] = el.getValue();
} else {
parent[attr] = parent[attr] || {};
parent = parent[attr];
}
});
}
}
}
return value;
},
setValue (data = {}) {
for (let field of data) {
if (this.refs[field.ref].setValue) {
this.refs[field.ref].setValue(field.value);
}
}
}
});

View File

@ -1,25 +0,0 @@
"use strict"
module.exports =
# -- Components
Aside : require "./aside"
Autocomplete : require "./autocomplete"
Button : require "./button"
Card : require "./card"
Dialog : require "./dialog"
Dropdown : require "./dropdown"
Fieldset : require "./fieldset"
FontIcon : require "./font_icon"
Form : require "./form"
Input : require "./input"
Link : require "./link"
List : require "./list"
Loading : require "./loading"
Navigation : require "./navigation"
ProgressBar : require "./progress_bar"
Ripple : require "./ripple"
Slider : require "./slider"
Switch : require "./switch"
Tab : require "./tabs/tab"
Tabs : require "./tabs/tabs"
# -- Tools

22
components/index.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
Aside: require('./aside'),
Autocomplete: require('./autocomplete'),
Button: require('./button'),
Card: require('./card'),
Dialog: require('./dialog'),
Dropdown: require('./dropdown'),
Fieldset: require('./fieldset'),
FontIcon: require('./font_icon'),
Form: require('./form'),
Input: require('./input'),
Link: require('./link'),
List: require('./list'),
Loading: require('./loading'),
Navigation: require('./navigation'),
ProgressBar: require('./progress_bar'),
Ripple: require('./ripple'),
Slider: require('./slider'),
Switch: require('./switch'),
Tab: require('./tabs/tab'),
Tabs: require('./tabs/tabs')
};

View File

@ -1,119 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
displayName : 'Input'
# -- States & Properties
propTypes:
className : React.PropTypes.string
disabled : React.PropTypes.bool
error : React.PropTypes.string
label : React.PropTypes.string
multiline : React.PropTypes.bool
onBlur : React.PropTypes.func
onChange : React.PropTypes.func
onKeyPress : React.PropTypes.func
onFocus : React.PropTypes.func
onBlur : React.PropTypes.func
required : React.PropTypes.bool
type : React.PropTypes.string
value : React.PropTypes.any
getDefaultProps: ->
className : ''
disabled : false
multiline : false
required : false
type : 'text'
getInitialState: ->
checked : @props.value
error : @props.error
touch : @props.type in ['checkbox', 'radio']
value : @props.value
focus : false
valid : false
# -- Events
onBlur: (event) ->
@setState focus: false
@props.onBlur? event, @
onChange: (event) ->
if @state.touch
@setState checked: event.target.checked, error: undefined
else
@setState value: event.target.value, error: undefined
@props.onChange? event, @
onFocus: (event) ->
@setState focus: true
@props.onFocus? event, @
onKeyPress: (event) ->
@setState focus: true
@props.onKeyPress? event, @
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
className += ' checked' if @state.checked
className += ' disabled' if @props.disabled
className += ' error' if @state.error
className += ' focus' if @state.focus
className += ' hidden' if @props.type is 'hidden'
className += ' touch' if @state.touch
className += ' radio' if @props.type is 'radio'
className += ' valid' if @state.value? and @state.value.length > 0
<div data-react-toolbox='input' className={className}>
{
if @props.multiline
<textarea ref='input' {...@props}
value={@state.value}
onChange={@onChange}
onKeyPress={@onKeyPress}
onFocus={@onFocus}
onBlur={@onBlur}
value={@state.value} />
else if @props.type is 'file'
delete @props.value
<input ref="input" {...@props} onChange={@onChange} />
else
<input ref='input' {...@props}
value={@state.value}
checked={@state.checked} 
onBlur={@onBlur}
onChange={@onChange}
onFocus={@onFocus}
onKeyPress={@onKeyPress} />
}
<span className='bar'></span>
{ <label>{@props.label}</label> if @props.label }
{ <span className='error'>{@state.error}</span> if @state.error }
</div>
# -- Extends
blur: ->
@refs.input.blur?()
focus: ->
@refs.input.focus?()
getValue: ->
if @props.type is 'file'
@state.value
else
@refs.input?.getDOMNode()[if @state.touch then 'checked' else 'value']
setError: (data = 'Unknown error') ->
@setState error: @props.error or data
setValue: (data) ->
data = false if @state.touch and data is undefined
attributes = value: data
attributes.checked = data if @state.touch and data?
@setState attributes

154
components/input/index.jsx Normal file
View File

@ -0,0 +1,154 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Input',
propTypes: {
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
error: React.PropTypes.string,
label: React.PropTypes.string,
multiline: React.PropTypes.bool,
onBlur: React.PropTypes.func,
onChange: React.PropTypes.func,
onFocus: React.PropTypes.func,
onKeyPress: React.PropTypes.func,
required: React.PropTypes.bool,
type: React.PropTypes.string,
value: React.PropTypes.any
},
getDefaultProps () {
return {
className: '',
disabled: false,
multiline: false,
required: false,
type: 'text'
};
},
getInitialState () {
return {
checked: this.props.value,
error: this.props.error,
touch: ['checkbox', 'radio'].indexOf(this.props.type) !== -1,
value: this.props.value,
focus: false,
valid: false
};
},
onBlur (event) {
this.setState({focus: false});
if (this.props.onBlur) this.props.onBlur(event, this);
},
onChange (event) {
if (this.state.touch) {
this.setState({checked: event.target.checked, error: undefined});
} else {
this.setState({value: event.target.value, error: undefined});
}
if (this.props.onChange) this.props.onChange(event, this);
},
onFocus (event) {
this.setState({focus: true});
if (this.props.onFocus) this.props.onFocus(event, this);
},
onKeyPress (event) {
this.setState({focus: true});
if (this.props.onKeyPress) this.props.onKeyPress(event, this);
},
renderInput () {
if (this.props.multiline) {
return (
<textarea
ref='input'
{...this.props}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
onFocus={this.onFocus}
onBlur={this.onBlur}
value={this.state.value} />
);
} else if (this.props.type === 'file') {
return (
<input
ref='input'
{...this.props}
value={undefined}
onChange={this.onChange} />
);
} else {
return (
<input
ref='input'
{...this.props}
value={this.state.value}
checked={this.state.checked}
onBlur={this.onBlur}
onChange={this.onChange}
onFocus={this.onFocus}
onKeyPress={this.onKeyPress} />
);
}
},
render () {
let className = `${style.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
if (this.state.checked) className += ' checked';
if (this.props.disabled) className += ' disabled';
if (this.state.error) className += ' error';
if (this.state.focus) className += ' focus';
if (this.props.type === 'hidden') className += ' hidden';
if (this.state.touch) className += ' touch';
if (this.props.type === 'radio') className += ' radio';
if (this.state.value && this.state.value.length > 0) className += ' valid';
return (
<div data-react-toolbox='input' className={className}>
{ this.renderInput() }
<span className='bar'></span>
{ this.props.label ? <label>{this.props.label}</label> : null }
{ this.state.error ? <span className='error'>{this.state.error}</span> : null }
</div>
);
},
blur () {
this.refs.input.getDOMNode().blur();
},
focus () {
this.refs.input.getDOMNode().focus();
},
getValue () {
if (this.props.type === 'file') {
return this.state.value;
} else if (this.refs.input) {
return this.refs.input.getDOMNode()[this.state.touch ? 'checked' : 'value'];
}
},
setError (data = 'Unknown error') {
this.setState({error: this.props.error || data});
},
setValue (argData) {
let data = this.state.touch && argData === undefined ? false : argData;
let attributes = { value: data };
if (this.state.touch && data) attributes.checked = data;
this.setState(attributes);
}
});

View File

@ -1,31 +0,0 @@
localCSS = require './style'
FontIcon = require '../font_icon'
module.exports = React.createClass
# -- States & Properties
propTypes:
caption : React.PropTypes.string
className : React.PropTypes.string
count : React.PropTypes.number
icon : React.PropTypes.string
onClick : React.PropTypes.func
route : React.PropTypes.array
getDefaultProps: ->
attributes : ''
className : ''
# -- Events
onClick: (event) ->
@props.onClick? event, @
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
<a data-react-toolbox='link' href={"##{@props.route}"} className={className}
onClick={@onClick} data-flex='horizontal center'>
{ <FontIcon className={localCSS.icon} value={@props.icon} /> if @props.icon }
{ <abbr>{@props.caption}</abbr> if @props.caption }
{ <small>{@props.count}</small> if @props.count and parseInt(@props.count) isnt 0}
</a>

49
components/link/index.jsx Normal file
View File

@ -0,0 +1,49 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
import FontIcon from '../font_icon';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Link',
propTypes: {
label: React.PropTypes.string,
className: React.PropTypes.string,
count: React.PropTypes.number,
icon: React.PropTypes.string,
onClick: React.PropTypes.func,
route: React.PropTypes.string
},
getDefaultProps () {
return {
attributes: '',
className: ''
};
},
onClick (event) {
if (this.props.onClick) {
this.props.onClick(event, this);
}
},
render () {
return (
<a
data-react-toolbox='link'
data-flex='horizontal center'
href={`${this.props.route}`}
className={`${style.root} ${this.props.className}`}
onClick={this.onClick}
>
{ this.props.icon ? <FontIcon className={style.icon} value={this.props.icon} /> : null }
{ this.props.label ? <abbr>{this.props.label}</abbr> : null }
{ this.props.count && parseInt(this.props.count) !== 0 ? <small>{this.props.count}</small> : null}
</a>
);
}
});

View File

@ -3,7 +3,7 @@
```
var Link = require('react-toolbox/components/link');
<Link route='http://google.com' caption='Go to Google.com' />
<Link route='http://google.com' label='Go to Google.com' />
<Link route='/profile/soyjavi' icon='user' />
```
@ -11,9 +11,9 @@ var Link = require('react-toolbox/components/link');
| Name | Type | Default | Description|
|:- |:-: | :- |:-|
| **caption** | String | "normal" | he text string to use for the floating label element.|
| **label** | String | "normal" | he text string to use for the floating label element.|
| **className** | String | | Sets the class-styles of the Component.|
| **count** | Number | | Sets a count number behind caption property.|
| **count** | Number | | Sets a count number behind label property.|
| **icon** | String | | Sets a <FontIcon/> sub-component.|
| **onClick** | Function | | Dispatch event when user clicks on component.|
| **route** | String | | URL String|

View File

@ -1,35 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
dataSource : React.PropTypes.array
ItemFactory : React.PropTypes.func
onClick : React.PropTypes.func
type : React.PropTypes.string
getDefaultProps: ->
attributes : ''
className : ''
dataSource : []
type : 'default'
# -- Events
onClick: (event, data, index) ->
@props.onClick? event, item, (@refs[index] if @refs[index]?)
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
<ul data-react-toolbox='list' className={className}>
{
for data, index in @props.dataSource
<li key={index} onClick={@onClick.bind null, data, index}>
{@props.itemFactory data, index}
</li>
}
</ul>

52
components/list/index.jsx Normal file
View File

@ -0,0 +1,52 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'List',
propTypes: {
className: React.PropTypes.string,
dataSource: React.PropTypes.array,
ItemFactory: React.PropTypes.func,
onClick: React.PropTypes.func,
type: React.PropTypes.string
},
getDefaultProps () {
return {
attributes: '',
className: '',
dataSource: [],
type: 'default'
};
},
onClick (event, data, index) {
if (this.props.onClick) {
this.props.onClick(event, data, (this.refs[index] ? this.refs[index] : null));
}
},
render () {
let className = `${style.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
const items = this.props.dataSource.map((data, index) => {
return (
<li key={index} onClick={this.onClick.bind(null, data, index)}>
{this.props.itemFactory(data, index)}
</li>
);
});
return (
<ul data-react-toolbox='list' className={className}>
{ items }
</ul>
);
}
});

View File

@ -1,20 +0,0 @@
###
@todo
###
require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
type : React.PropTypes.string
getDefaultProps: ->
type : "normal"
# -- Render
render: ->
<div data-component-loading={@props.type} data-flex="vertical center">
<div></div><div></div><div></div>
</div>

View File

@ -0,0 +1,28 @@
/* global React */
import { addons } from 'react/addons';
require('./style');
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Loading',
propTypes: {
type: React.PropTypes.string
},
getDefaultProps () {
return {
type: 'normal'
};
},
render () {
return (
<div data-component-loading={this.props.type} data-flex="vertical center">
<div></div><div></div><div></div>
</div>
);
}
});

View File

@ -1,30 +0,0 @@
localCSS = require './style'
Button = require '../button'
Link = require '../link'
module.exports = React.createClass
displayName : 'Navigation'
# -- States & Properties
propTypes:
actions : React.PropTypes.array
className : React.PropTypes.string
routes : React.PropTypes.array
type : React.PropTypes.string
getDefaultProps: ->
actions : []
className : ''
type : 'default'
routes : []
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += " #{@props.type}" if @props.type
<nav data-react-toolbox='navigation' className={className}>
{ <Link key={index} {...route} /> for route, index in @props.routes }
{ <Button key={index} {...action} /> for action, index in @props.actions }
{ @props.children }
</nav>

View File

@ -0,0 +1,49 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
import Button from '../button';
import Link from '../link';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Navigation',
propTypes: {
actions: React.PropTypes.array,
className: React.PropTypes.string,
routes: React.PropTypes.array,
type: React.PropTypes.string
},
getDefaultProps () {
return {
actions: [],
className: '',
type: 'default',
routes: []
};
},
render () {
let className = `${style.root} ${this.props.className}`;
if (this.props.type) className += ` ${this.props.type}`;
const buttons = this.props.actions.map((action, index) => {
return <Button key={index} {...action} />;
});
const links = this.props.routes.map((route, index) => {
return <Link key={index} {...route} />;
});
return (
<nav data-react-toolbox='navigation' className={className}>
{ links }
{ buttons }
{ this.props.children }
</nav>
);
}
});

View File

@ -1,10 +0,0 @@
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,48 +0,0 @@
TestUtils = React.addons.TestUtils
expect = require('expect')
utils = require('../../test_utils')
ProgressBar = require('../index')
describe 'ProgressBar', ->
describe '#calculateRatio', ->
before ->
@progressBar = utils.renderComponent(ProgressBar, { min: 100, max: 300 })
it 'calculates the right ratio', ->
expect(@progressBar.calculateRatio(150)).toEqual(0.25)
it 'gets 0 when value is less than min', ->
expect(@progressBar.calculateRatio(10)).toEqual(0)
it 'gets 1 when value is more than max', ->
expect(@progressBar.calculateRatio(400)).toEqual(1)
describe '#render', ->
it 'renders the value and buffer bars when it is linear', ->
progressBarWrapper = utils.shallowRenderComponent(ProgressBar).props.children
expect(progressBarWrapper.props.children.length).toEqual(2)
expect(progressBarWrapper.props.children[0].ref).toEqual('buffer')
expect(progressBarWrapper.props.children[1].ref).toEqual('value')
it 'renders the proper scaleX for buffer and value when its linear and determinate', ->
progressBar = utils.shallowRenderComponent(ProgressBar, {mode: 'determinate', value: 30, buffer: 60})
buffer = (progressBar.props.children.props.children[0])
value = (progressBar.props.children.props.children[1])
expect(buffer.props.style.transform).toEqual("scaleX(#{0.6})")
expect(value.props.style.transform).toEqual("scaleX(#{0.3})")
it 'renders the svg circle when it is circular', ->
progressBar = utils.shallowRenderComponent(ProgressBar, {type: 'circular'})
expect(progressBar.props.children.type).toEqual('svg')
expect(progressBar.props.children.props.children.type).toEqual('circle')
it 'renders the proper circle length style when it is circular and determinate', ->
progressBar = utils.shallowRenderComponent(ProgressBar, {type: 'circular', mode: 'determinate', value: 30})
circle = progressBar.props.children.props.children
strokeLength = 2 * Math.PI * circle.props.r * 0.3
expect(circle.props.style.strokeDasharray).toEqual("#{strokeLength}, 400")
it 'contains mode and className in its className', ->
progressBar = utils.shallowRenderComponent(ProgressBar, {mode: 'determinate', className: 'tight'})
expect(progressBar.props.className).toContain('determinate')
expect(progressBar.props.className).toContain('tight')

View File

@ -0,0 +1,63 @@
import expect from 'expect';
import utils from '../../utils/testing';
import ProgressBar from '../index';
describe('ProgressBar', function () {
let progressBar;
describe('#calculateRatio', function () {
before(function () {
progressBar = utils.renderComponent(ProgressBar, {min: 100, max: 300});
});
it('calculates the right ratio', function () {
expect(progressBar.calculateRatio(150)).toEqual(0.25);
});
it('gets 0 when value is less than min', function () {
expect(progressBar.calculateRatio(10)).toEqual(0);
});
it('gets 1 when value is more than max', function () {
expect(progressBar.calculateRatio(400)).toEqual(1);
});
});
describe('#render', function () {
let buffer, value, wrapper, circle, strokeLength;
it('renders the value and buffer bars when it is linear', function () {
wrapper = utils.shallowRenderComponent(ProgressBar).props.children;
expect(wrapper.props.children.length).toEqual(2);
expect(wrapper.props.children[0].ref).toEqual('buffer');
expect(wrapper.props.children[1].ref).toEqual('value');
});
it('renders the value and buffer bars when it is linear', function () {
progressBar = utils.shallowRenderComponent(ProgressBar, {mode: 'determinate', value: 30, buffer: 60});
buffer = (progressBar.props.children.props.children[0]);
value = (progressBar.props.children.props.children[1]);
expect(buffer.props.style.transform).toEqual(`scaleX(${0.6})`);
expect(value.props.style.transform).toEqual(`scaleX(${0.3})`);
});
it('renders the svg circle when it is circular', function () {
progressBar = utils.shallowRenderComponent(ProgressBar, {type: 'circular'});
expect(progressBar.props.children.type).toEqual('svg');
expect(progressBar.props.children.props.children.type).toEqual('circle');
});
it('renders the proper circle length style when it is circular and determinate', function () {
progressBar = utils.shallowRenderComponent(ProgressBar, {type: 'circular', mode: 'determinate', value: 30});
circle = progressBar.props.children.props.children;
strokeLength = 2 * Math.PI * circle.props.r * 0.3;
expect(circle.props.style.strokeDasharray).toEqual(`${strokeLength}, 400`);
});
it('contains mode and className in its className', function () {
progressBar = utils.shallowRenderComponent(ProgressBar, {mode: 'determinate', className: 'tight'});
expect(progressBar.props.className).toContain('determinate');
expect(progressBar.props.className).toContain('tight');
});
});
});

View File

@ -1,68 +0,0 @@
localCSS = require './style'
prefixer = require '../prefixer'
module.exports = React.createClass
# -- Properties
propTypes:
buffer : React.PropTypes.number
className : React.PropTypes.string
max : React.PropTypes.number
min : React.PropTypes.number
mode : React.PropTypes.string
multicolor : React.PropTypes.bool
type : React.PropTypes.string
value : React.PropTypes.number
getDefaultProps: ->
buffer : 0
className : ''
max : 100
min : 0
mode : 'indeterminate'
multicolor : false
type : 'linear'
value : 0
# -- Helper methods
calculateRatio: (value) ->
return 0 if value < @props.min
return 1 if value > @props.max
return (value - @props.min) / (@props.max - @props.min)
# -- Render
render: ->
className = if @props.type == 'linear' then localCSS.linearBar else localCSS.circularBar
className += " #{@props.className}" if @props.className
className += " #{@props.mode}" if @props.mode
className += " multicolor" if @props.multicolor
<div className={className} role="progressbar"
aria-valuenow={@props.value}
aria-valuemin={@props.min}
aria-valuemax={@props.max}>
{ if @props.type == 'circular' then @renderCircular() else @renderLinear() }
</div>
renderCircular: ->
<svg className={localCSS.circle}>
<circle className={localCSS.circlePath} style={@circularStyle()} cx="30" cy="30" r="25"/>
</svg>
circularStyle: ->
_transformDasharray(@calculateRatio(@props.value)) unless @props.mode == 'indeterminate'
renderLinear: ->
<div>
<span ref="buffer" data-ref="buffer" className={localCSS.bufferBar} style={@linearStyles()?.buffer}></span>
<span ref="value" data-ref="value" className={localCSS.valueBar} style={@linearStyles()?.value}></span>
</div>
linearStyles: ->
unless @props.mode == 'indeterminate'
buffer: prefixer.transform("scaleX(#{@calculateRatio(@props.buffer)})")
value: prefixer.transform("scaleX(#{@calculateRatio(@props.value)})")
# -- Private methods
_transformDasharray = (ratio) ->
strokeDasharray: "#{2 * Math.PI * 25 * ratio}, 400"

View File

@ -0,0 +1,92 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import prefixer from '../utils/prefixer';
export default React.createClass({
mixins: [addons.PureRenderMixin],
propTypes: {
buffer: React.PropTypes.number,
className: React.PropTypes.string,
max: React.PropTypes.number,
min: React.PropTypes.number,
mode: React.PropTypes.string,
multicolor: React.PropTypes.bool,
type: React.PropTypes.string,
value: React.PropTypes.number
},
getDefaultProps () {
return {
buffer: 0,
className: '',
max: 100,
min: 0,
mode: 'indeterminate',
multicolor: false,
type: 'linear',
value: 0
};
},
calculateRatio (value) {
if (value < this.props.min) return 0;
if (value > this.props.max) return 1;
return (value - this.props.min) / (this.props.max - this.props.min);
},
circularStyle () {
if (this.props.mode !== 'indeterminate') {
return {strokeDasharray: `${2 * Math.PI * 25 * this.calculateRatio(this.props.value)}, 400`};
}
},
renderCircular () {
return (
<svg className={css.circle}>
<circle className={css.circlePath} style={this.circularStyle()} cx="30" cy="30" r="25" />
</svg>
);
},
linearStyle () {
if (this.props.mode !== 'indeterminate') {
return {
buffer: prefixer({transform: `scaleX(${this.calculateRatio(this.props.buffer)})`}),
value: prefixer({transform: `scaleX(${this.calculateRatio(this.props.value)})`})
};
} else {
return {};
}
},
renderLinear () {
const {buffer, value} = this.linearStyle();
return (
<div>
<span ref="buffer" data-ref="buffer" className={css.bufferBar} style={buffer}></span>
<span ref="value" data-ref="value" className={css.valueBar} style={value}></span>
</div>
);
},
render () {
let className = this.props.type === 'linear' ? css.linearBar : css.circularBar;
if (this.props.className) className += ` ${this.props.className}`;
if (this.props.mode) className += ` ${this.props.mode}`;
if (this.props.multicolor) className += ` multicolor`;
return (
<div
className={className}
role="progressbar"
aria-valuenow={this.props.value}
aria-valuemin={this.props.min}
aria-valuemax={this.props.max}>
{ this.props.type === 'circular' ? this.renderCircular() : this.renderLinear() }
</div>
);
}
});

View File

@ -1,37 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
loading : React.PropTypes.bool
origin : React.PropTypes.object
getDefaultProps: ->
className : ''
loading : false
getInitialState: ->
className : undefined
# -- Lifecycle
componentWillReceiveProps: (next_props) ->
@setState className: "active" if next_props.origin?
componentDidMount: ->
el = @getDOMNode()
for key in ['animationend', 'webkitAnimationEnd', 'oAnimationEnd', 'MSAnimationEnd']
el.addEventListener key, (=> @setState className: undefined), false
@setState className: 'active' if @props.origin?
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className} #{@state.className}"
className += ' loading' if @props.loading
<div data-react-toolbox='ripple' className={className}
style={
left : @props.origin?.left,
top : @props.origin?.top,
width : @props.origin?.width,
height: @props.origin?.width} />

View File

@ -0,0 +1,77 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Ripple',
propTypes: {
auto: React.PropTypes.bool,
className: React.PropTypes.string,
loading: React.PropTypes.bool
},
getDefaultProps () {
return {
auto: true,
className: '',
loading: false
};
},
getInitialState () {
return {
active: false,
restarting: false,
top: null,
left: null,
width: null
};
},
start ({ pageX, pageY }) {
const {top, left, width} = this._getDescriptor(pageX, pageY);
this.setState({active: false, restarting: true, width: 0}, () => {
this.refs.ripple.getDOMNode().offsetWidth; //eslint-disable-line no-unused-expressions
this.setState({active: true, restarting: false, top: top, left: left, width: width});
});
},
end () {
this.setState({active: false});
},
_getDescriptor (pageX, pageY) {
let { left, top, width } = this.getDOMNode().getBoundingClientRect();
return {
left: pageX - left,
top: pageY - top,
width: width * 2.5
};
},
render () {
let { left, top, width } = this.state;
let className = `${style.ripple} ${this.props.className}`;
if (this.state.active) className += ' active';
if (this.state.restarting) className += ' restarting';
if (this.props.loading) className += ' loading';
return (
<span
className={style.root}
onMouseDown={this.props.auto ? this.start : null}
onMouseUp={this.end}>
<span
ref="ripple"
className={className}
style={{left: left, top: top, width: width, height: width}}>
</span>
</span>
);
}
});

View File

@ -1,30 +1,47 @@
@import '../constants'
FINAL_OPACITY = .4
:local(.root)
position : absolute
background-color : alpha(WHITE, 0.65)
transform translateX(-50%) translateY(-50%)
top : 0
left : 0
right : 0
bottom : 0
:local(.ripple)
position : absolute
background-color : currentColor
transform : translateX(-50%) translateY(-50%)
border-radius : 50%
width : SIZE = (4 * UNIT)
height : SIZE
pointer-events : none
opacity : 0
// -- Classes
&.active, &.loading
animation-name ripple
animation-timing-function ANIMATION_EASE
animation-duration (2 * ANIMATION_DURATION)
&.active
animation-iteration-count 1
width : 0
height : 0
opacity : FINAL_OPACITY
transition-duration : 1.4 * ANIMATION_DURATION
transition-property : height, width
&:not(.active).restarting
transition-property : none
&:not(.active)
opacity : 0
transition-property : opacity, height, width
&.loading
animation-name : ripple
animation-iteration-count : infinite
animation-timing-function : ANIMATION_EASE
animation-duration : (2 * ANIMATION_DURATION)
height : (4 * UNIT)
width : (4 * UNIT)
left : 50%
opacity : 0
top : 50%
animation-iteration-count infinite
@keyframes ripple
0%
width : 0
height : 0
opacity : 1
opacity : FINAL_OPACITY
100%
opacity : 0

View File

@ -1,149 +0,0 @@
TestUtils = React.addons.TestUtils
expect = require('expect')
sinon = require('sinon')
utils = require('../../test_utils')
ProgressBar = require('../../progress_bar')
Input = require('../../input')
Slider = require('../index')
describe 'Slider', ->
describe '#positionToValue', ->
before ->
props = { min: -500, max: 500 }
state = { sliderStart: 500, sliderLength: 100 }
@slider = utils.renderComponent(Slider, props, state)
it 'returns min when position is less than origin', ->
expect(@slider.positionToValue({x: 400})).toEqual(-500)
it 'returns max when position is more and origin plus length', ->
expect(@slider.positionToValue({x: 900})).toEqual(500)
it 'returns the proper position when the position is inside slider', ->
expect(@slider.positionToValue({x: 520})).toEqual(-300)
describe '#endPositionToValue', ->
before ->
props = { min: -500, max: 500 }
state = { sliderStart: 500, sliderLength: 100, startPosition: 520, startValue: -300 }
@slider = utils.renderComponent(Slider, props, state)
it 'returns the proper value when is moved left', ->
expect(@slider.endPositionToValue({x: 510})).toEqual(-400)
it 'returns the proper value when is moved right', ->
expect(@slider.endPositionToValue({x: 570})).toEqual(200)
it 'returns the proper value when is not moved', ->
expect(@slider.endPositionToValue({x: 520})).toEqual(-300)
describe '#trimValue', ->
before ->
props = { min: 0, max: 100, step: 0.1 }
@slider = utils.renderComponent(Slider, props)
it 'rounds to the proper number', ->
expect(@slider.trimValue(57.16)).toEqual(57.2)
expect(@slider.trimValue(57.12)).toEqual(57.10)
it 'returns min if number is less than min', ->
expect(@slider.trimValue(-57.16)).toEqual(0)
it 'returns max if number is more than max', ->
expect(@slider.trimValue(257.16)).toEqual(100)
describe '#valueForInput', ->
before ->
props = { min: 0, max: 100, step: 0.01 }
@slider = utils.renderComponent(Slider, props)
it 'returns a fixed number when an integer is given', ->
expect(@slider.valueForInput(4)).toEqual('4.00')
it 'returns a fixed number when a float is given', ->
expect(@slider.valueForInput(4.06)).toEqual('4.06')
describe '#calculateKnobOffset', ->
it 'returns the corresponding offset for a given value and slider length/start', ->
props = { min: -500, max: 500, value: -250 }
state = { sliderStart: 500, sliderLength: 100 }
slider = utils.renderComponent(Slider, props, state)
expect(slider.calculateKnobOffset()).toEqual(25)
describe '#getValue', ->
it 'retrieves the current value', ->
slider = utils.renderComponent(Slider, {value: 10})
expect(slider.getValue()).toEqual(slider.state.value)
describe '#setValue', ->
it 'set the current value', ->
slider = utils.renderComponent(Slider, {value: 10})
slider.setValue(50)
expect(slider.state.value).toEqual(50)
describe '#render', ->
it "contains a linear progress bar with proper properties", ->
slider = utils.renderComponent(Slider, {min: 100, max: 1000, value: 140})
progress = TestUtils.findRenderedComponentWithType(slider, ProgressBar)
expect(progress.props.mode).toEqual('determinate')
expect(progress.props.type).toEqual('linear')
expect(progress.props.value).toEqual(140)
expect(progress.props.min).toEqual(100)
expect(progress.props.max).toEqual(1000)
it "contains an input component if its editable", ->
slider = utils.renderComponent(Slider, {editable: true, value: 130})
input = TestUtils.findRenderedComponentWithType(slider, Input)
expect(input.props.value).toEqual(slider.props.value)
it "contains the proper number of snaps when snapped", ->
slider = utils.renderComponent(Slider, {snaps: true, step: 10})
snaps = slider.refs.snaps
expect(snaps.props.children.length).toEqual(10)
it "has the proper classes for pinned, editable and ring", ->
slider = utils.shallowRenderComponent(Slider, {editable: true, pinned: true})
expect(slider.props.className).toContain("ring")
expect(slider.props.className).toContain("pinned")
slider = utils.shallowRenderComponent(Slider, {editable: true, value: 50})
expect(slider.props.className).toNotContain("ring")
describe 'events', ->
before ->
props = { min: -500, max: 500 }
state = { sliderStart: 0, sliderLength: 1000 }
@slider = utils.renderComponent(Slider, props, state)
it "sets pressed state when knob is clicked", ->
TestUtils.Simulate.mouseDown(@slider.refs.knob)
expect(@slider.state.pressed).toEqual(true)
it "sets pressed state when knob is touched", ->
TestUtils.Simulate.touchStart(@slider.refs.knob, {touches: [{pageX: 200}]})
expect(@slider.state.pressed).toEqual(true)
it "sets a proper value when the slider is clicked", ->
TestUtils.Simulate.mouseDown(@slider.refs.slider, { pageX: 200 })
expect(@slider.state.value).toEqual(-300)
it "sets a proper value when the slider is touched", ->
TestUtils.Simulate.touchStart(@slider.refs.slider, {touches: [{pageX: 200, pageY: 0}]})
expect(@slider.state.value).toEqual(-300)
it "changes its value when input changes", ->
slider = utils.renderComponent(Slider, {editable: true, value: 50})
input = TestUtils.findRenderedComponentWithType(slider, Input)
TestUtils.Simulate.change(input.refs.input, {target: {value: '80'}})
expect(slider.state.value).toEqual(80)
it "changes input value when slider changes", ->
slider = utils.renderComponent(Slider, {editable: true}, {sliderStart: 0, sliderLength: 1000})
input = TestUtils.findRenderedComponentWithType(slider, Input)
TestUtils.Simulate.mouseDown(slider.refs.slider, { pageX: 900 })
expect(input.state.value).toEqual(90)
it "calls onChange callback when the value is changed", ->
onChangeSpy = sinon.spy()
slider = utils.renderComponent(Slider, {onChange: onChangeSpy}, {sliderStart: 0, sliderLength: 1000})
TestUtils.Simulate.mouseDown(slider.refs.slider, { pageX: 900 })
expect(onChangeSpy.called).toEqual(true)

View File

@ -0,0 +1,167 @@
/* global React */
import expect from 'expect';
import sinon from 'sinon';
import utils from '../../utils/testing';
import ProgressBar from '../../progress_bar';
import Input from '../../input';
import Slider from '../index';
describe('Slider', function () {
const TestUtils = React.addons.TestUtils;
let props, state, slider, progress, input;
describe('#positionToValue', function () {
before(function () {
props = { min: -500, max: 500 };
state = { sliderStart: 500, sliderLength: 100 };
slider = utils.renderComponent(Slider, props, state);
});
it('returns min when position is less than origin', function () {
expect(slider.positionToValue({x: 400})).toEqual(-500);
});
it('returns max when position is more and origin plus length', function () {
expect(slider.positionToValue({x: 900})).toEqual(500);
});
it('returns the proper position when the position is inside slider', function () {
expect(slider.positionToValue({x: 520})).toEqual(-300);
});
});
describe('#trimValue', function () {
before(function () {
props = { min: 0, max: 100, step: 0.1 };
slider = utils.renderComponent(Slider, props);
});
it('rounds to the proper number', function () {
expect(slider.trimValue(57.16)).toEqual(57.2);
expect(slider.trimValue(57.12)).toEqual(57.10);
});
it('returns min if number is less than min', function () {
expect(slider.trimValue(-57.16)).toEqual(0);
});
it('returns max if number is more than max', function () {
expect(slider.trimValue(257.16)).toEqual(100);
});
});
describe('#valueForInput', function () {
before(function () {
props = { min: 0, max: 100, step: 0.01 };
slider = utils.renderComponent(Slider, props);
});
it('returns a fixed number when an integer is given', function () {
expect(slider.valueForInput(4)).toEqual('4.00');
});
it('returns a fixed number when a float is given', function () {
expect(slider.valueForInput(4.06)).toEqual('4.06');
});
});
describe('#knobOffset', function () {
it('returns the corresponding offset for a given value and slider length/start', function () {
props = { min: -500, max: 500, value: -250 };
state = { sliderStart: 500, sliderLength: 100 };
slider = utils.renderComponent(Slider, props, state);
expect(slider.knobOffset()).toEqual(25);
});
});
describe('#getValue', function () {
it('retrieves the current value', function () {
slider = utils.renderComponent(Slider, {value: 10});
expect(slider.getValue()).toEqual(slider.state.value);
});
});
describe('#setValue', function () {
it('set the current value', function () {
slider = utils.renderComponent(Slider, {value: 10});
slider.setValue(50);
expect(slider.state.value).toEqual(50);
});
});
describe('#render', function () {
it('contains a linear progress bar with proper properties', function () {
slider = utils.renderComponent(Slider, {min: 100, max: 1000, value: 140});
progress = TestUtils.findRenderedComponentWithType(slider, ProgressBar);
expect(progress.props.mode).toEqual('determinate');
expect(progress.props.type).toEqual('linear');
expect(progress.props.value).toEqual(140);
expect(progress.props.min).toEqual(100);
expect(progress.props.max).toEqual(1000);
});
it('contains an input component if its editable', function () {
slider = utils.renderComponent(Slider, {editable: true, value: 130});
input = TestUtils.findRenderedComponentWithType(slider, Input);
expect(input.props.value).toEqual(slider.props.value);
});
it('contains the proper number of snaps when snapped', function () {
slider = utils.shallowRenderComponent(Slider, {editable: true, pinned: true});
expect(slider.props.className).toContain('ring');
expect(slider.props.className).toContain('pinned');
slider = utils.shallowRenderComponent(Slider, {editable: true, value: 50});
expect(slider.props.className).toNotContain('ring');
});
});
describe('#events', function () {
before(function () {
props = { min: -500, max: 500 };
state = { sliderStart: 0, sliderLength: 1000 };
slider = utils.renderComponent(Slider, props, state);
});
it('sets pressed state when knob is clicked', function () {
TestUtils.Simulate.mouseDown(slider.refs.knob);
expect(slider.state.pressed).toEqual(true);
});
it('sets pressed state when knob is touched', function () {
TestUtils.Simulate.touchStart(slider.refs.knob, {touches: [{pageX: 200}]});
expect(slider.state.pressed).toEqual(true);
});
it('sets a proper value when the slider is clicked', function () {
TestUtils.Simulate.mouseDown(slider.refs.slider, { pageX: 200 });
expect(slider.state.value).toEqual(-300);
});
it('sets a proper value when the slider is touched', function () {
TestUtils.Simulate.touchStart(slider.refs.slider, {touches: [{pageX: 200, pageY: 0}]});
expect(slider.state.value).toEqual(-300);
});
it('changes its value when input changes', function () {
slider = utils.renderComponent(Slider, {editable: true, value: 50});
input = TestUtils.findRenderedComponentWithType(slider, Input);
TestUtils.Simulate.change(input.refs.input, {target: {value: '80'}});
expect(slider.state.value).toEqual(80);
});
it('changes input value when slider changes', function () {
slider = utils.renderComponent(Slider, {editable: true}, {sliderStart: 0, sliderLength: 1000});
input = TestUtils.findRenderedComponentWithType(slider, Input);
TestUtils.Simulate.mouseDown(slider.refs.slider, { pageX: 900 });
expect(input.state.value).toEqual(90);
});
it('calls onChange callback when the value is changed', function () {
let onChangeSpy = sinon.spy();
slider = utils.renderComponent(Slider, {onChange: onChangeSpy}, {sliderStart: 0, sliderLength: 1000});
TestUtils.Simulate.mouseDown(slider.refs.slider, { pageX: 900 });
expect(onChangeSpy.called).toEqual(true);
});
});
});

View File

@ -1,245 +0,0 @@
prefixer = require "../prefixer"
localCSS = require './style'
ProgressBar = require "../progress_bar"
Input = require "../input"
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(event)
onSliderFocus: (event) ->
_addEventsToDocument(@getKeyboardEvents())
onSliderBlur: (event) ->
_removeEventsFromDocument(@getKeyboardEvents())
onInputChange: (event) ->
@setState value: @trimValue(event.target.value)
onKeyDown: (event) ->
event.stopPropagation()
@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) + @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)
_round(value, @stepDecimals())
stepDecimals: ->
(@props.step.toString().split('.')[1] || []).length
addToValue: (value) ->
@setState value: @trimValue(@state.value + value)
valueForInput: (value) ->
decimals = @stepDecimals()
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"
onFocus={@onSliderFocus}
onBlur={@onSliderBlur} >
<div ref="slider"
className={localCSS.container}
onTouchStart={@onSliderTouchStart}
onMouseDown={@onSliderMouseDown} >
<div ref="knob" 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 ref="snaps" 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
_round = (n, decimals) ->
if (!isNaN(parseFloat(n)) && isFinite(n))
decimalPower = Math.pow(10, decimals)
return Math.round(parseFloat(n) * decimalPower) / decimalPower
return NaN

256
components/slider/index.jsx Normal file
View File

@ -0,0 +1,256 @@
/* global React */
import css from './style';
import utils from '../utils';
import ProgressBar from '../progress_bar';
import Input from '../input';
export default React.createClass({
displayName: 'Slider',
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 () {
return {
className: '',
editable: false,
max: 100,
min: 0,
pinned: false,
snaps: false,
step: 0.01,
value: 0
};
},
getInitialState () {
return {
sliderStart: 0,
sliderLength: 0,
value: this.props.value
};
},
componentDidMount () {
window.addEventListener('resize', this.onResize);
this.onResize();
},
componentWillUnmount () {
window.removeEventListener('resize', this.onResize);
},
componentDidUpdate (prevProps, prevState) {
if (prevState.value !== this.state.value) {
if (this.props.onChange) this.props.onChange(this);
if (this.refs.input) this.refs.input.setValue(this.valueForInput(this.state.value));
}
},
onResize () {
const {left, right} = this.refs.progressbar.getDOMNode().getBoundingClientRect();
this.setState({sliderStart: left, sliderLength: right - left});
},
onSliderFocus () {
utils.events.addEventsToDocument(this.getKeyboardEvents());
},
onSliderBlur () {
utils.events.removeEventsFromDocument(this.getKeyboardEvents());
},
onInputChange (event) {
this.setState({value: this.trimValue(event.target.value) });
},
onKeyDown (event) {
event.stopPropagation();
if (event.keyCode in [13, 27]) this.getDOMNode().blur();
if (event.keyCode === 38) this.addToValue(this.props.step);
if (event.keyCode === 40) this.addToValue(-this.props.step);
},
onMouseDown (event) {
utils.events.addEventsToDocument(this.getMouseEventMap());
this.start(utils.events.getMousePosition(event));
utils.events.pauseEvent(event);
},
onTouchStart (event) {
this.start(utils.events.getTouchPosition(event));
utils.events.addEventsToDocument(this.getTouchEventMap());
utils.events.pauseEvent(event);
},
onMouseMove (event) {
utils.events.pauseEvent(event);
this.move(utils.events.getMousePosition(event));
},
onTouchMove (event) {
this.move(utils.events.getTouchPosition(event));
},
onMouseUp () {
this.end(this.getMouseEventMap());
},
onTouchEnd () {
this.end(this.getTouchEventMap());
},
getMouseEventMap () {
return {
mousemove: this.onMouseMove,
mouseup: this.onMouseUp
};
},
getTouchEventMap () {
return {
touchmove: this.onTouchMove,
touchend: this.onTouchEnd
};
},
getKeyboardEvents () {
return {
keydown: this.onKeyDown
};
},
start (position) {
this.setState({pressed: true, value: this.positionToValue(position)});
},
move (position) {
this.setState({value: this.positionToValue(position)});
},
end (revents) {
utils.events.removeEventsFromDocument(revents);
this.setState({pressed: false});
},
positionToValue (position) {
let { sliderStart: start, sliderLength: length } = this.state;
let { max, min } = this.props;
return this.trimValue((position.x - start) / length * (max - min) + min);
},
trimValue (value) {
if (value < this.props.min) return this.props.min;
if (value > this.props.max) return this.props.max;
return utils.round(value, this.stepDecimals());
},
stepDecimals () {
return (this.props.step.toString().split('.')[1] || []).length;
},
addToValue (value) {
this.setState({value: this.trimValue(this.state.value + value)});
},
valueForInput (value) {
const decimals = this.stepDecimals();
return decimals > 0 ? value.toFixed(decimals) : value.toString();
},
knobOffset () {
let { max, min } = this.props;
return this.state.sliderLength * (this.state.value - min) / (max - min);
},
renderSnaps () {
if (this.props.snaps) {
return (
<div ref='snaps' className={css.snaps}>
{
utils.range(0, (this.props.max - this.props.min) / this.props.step).map(i => {
return (<div key={`span-${i}`} className={css.snap}></div>);
})
}
</div>
);
}
},
renderInput () {
if (this.props.editable) {
return (
<Input
ref='input'
className={css.input}
onChange={this.onInputChange}
value={this.valueForInput(this.state.value)} />
);
}
},
render () {
let knobStyles = utils.prefixer({transform: `translateX(${this.knobOffset()}px)`});
let className = this.props.className;
if (this.props.editable) className += ' editable';
if (this.props.pinned) className += ' pinned';
if (this.state.pressed) className += ' pressed';
if (this.state.value === this.props.min) className += ' ring';
return (
<div
className={css.root + className}
tabIndex='0'
onFocus={this.onSliderFocus}
onBlur={this.onSliderBlur} >
<div
ref='slider'
className={css.container}
onTouchStart={this.onTouchStart}
onMouseDown={this.onMouseDown} >
<div
ref='knob'
className={css.knob}
style={knobStyles}
onMouseDown={this.onMouseDown}
onTouchStart={this.onTouchStart} >
<div className={css.knobInner} data-value={parseInt(this.state.value)}></div>
</div>
<div className={css.progress}>
<ProgressBar
ref='progressbar'
mode='determinate'
className={css.progressInner}
value={this.state.value}
max={this.props.max}
min={this.props.min}/>
{ this.renderSnaps() }
</div>
</div>
{ this.renderInput() }
</div>
);
},
getValue () {
return this.state.value;
},
setValue (value) {
this.setState({value: value});
}
});

View File

@ -1,49 +0,0 @@
localCSS = require './style'
Ripple = require '../ripple'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
disabled : React.PropTypes.bool
label : React.PropTypes.string
onChange : React.PropTypes.func
value : React.PropTypes.bool
getDefaultProps: ->
className : ''
getInitialState: ->
value : @props.value
ripple : undefined
# -- Lifecycle
componentWillReceiveProps: (next_props) ->
@setState value: next_props.value if next_props.value
# -- Events
onClick: (event) ->
unless @props.disabled
@setState
value : not @state.value
ripple: change: true
setTimeout (=> @props.onChange? event, @), 10
# -- Render
render: ->
className = "#{localCSS.root} #{@props.className}"
className += ' checked' if @state.value
className += ' disabled' if @props.disabled
<div data-react-toolbox='switch' className={className} onClick={@onClick}>
<span></span>
{ <label>{@props.label}</label> if @props.label }
<Ripple className={localCSS.ripple} origin={@state.ripple} />
</div>
# -- Extends
getValue: ->
@state.value
setValue: (data) ->
@setState value: data

View File

@ -0,0 +1,70 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Switch',
propTypes: {
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
label: React.PropTypes.string,
onChange: React.PropTypes.func,
value: React.PropTypes.bool
},
getDefaultProps () {
return {
className: ''
};
},
getInitialState () {
return {
value: this.props.value
};
},
componentWillReceiveProps (next_props) {
if (next_props.value) {
this.setState({value: next_props.value});
}
},
onClick (event) {
if (!this.props.disabled) {
this.setState({value: !this.state.value});
setTimeout(() => {
if (this.props.onChange) this.props.onChange(event, this);
}, 10);
}
},
render () {
let className = `${style.root} ${this.props.className}`;
if (this.state.value) className += ' checked';
if (this.props.disabled) className += ' disabled';
return (
<div
data-react-toolbox='switch'
className={className}
onClick={this.onClick}
>
<span></span>
{ this.props.label ? <label>{this.props.label}</label> : null }
</div>
);
},
getValue () {
return this.state.value;
},
setValue (data) {
this.setState({value: data});
}
});

View File

@ -1,16 +1,6 @@
@import '../constants'
SIZE = (SPACE / 1.25)
:local(.ripple)
z-index : -1
overflow : hidden
max-width : (SIZE * 2.7)
max-height : (SIZE * 2.7)
top : (SIZE / 2)
left : (SIZE * 2)
background-color : alpha(TEXT, 10%)
opacity : 0
animation-duration : (1.0 * ANIMATION_DURATION)
:local(.root)
position : relative
@ -55,6 +45,3 @@ SIZE = (SPACE / 1.25)
background-color : PRIMARY
right : 0
box-shadow : ZDEPTH_SHADOW_2
> :local(.ripple)
left : 0
background-color : alpha(PRIMARY, 15%)

View File

@ -1,7 +0,0 @@
###
@todo
###
module.exports =
Tab : require './tab'
Tabs : require './tabs'

View File

@ -0,0 +1,4 @@
module.exports = {
Tab: require('./tab'),
Tabs: require('./tabs')
};

View File

@ -1,41 +0,0 @@
localCSS = require './style'
module.exports = React.createClass
# -- States & Properties
propTypes:
active : React.PropTypes.boolean
className : React.PropTypes.string
disabled : React.PropTypes.boolean
hidden : React.PropTypes.boolean
label : React.PropTypes.string.required
onActive : React.PropTypes.func
tabIndex : React.PropTypes.number
getDefaultProps: ->
className : ""
# -- Lifecycle
componentDidMount: ->
@active @props.active
componentWillReceiveProps: (next_props) ->
@active next_props.active if next_props.active
# -- Render
render: ->
className = @props.className
className += ' active' if @props.active
className += ' disabled' if @props.disabled
className += ' hidden' if @props.hidden
<section data-react-toolbox='tab'
className={localCSS.tab + ' ' + className}
data-flex='vertical'
tabIndex={@props.tabIndex}>
{ @props.children }
</section>
# -- Extends
active: (value) ->
@setState active: value
@props.onActive? @ if value

57
components/tabs/tab.jsx Normal file
View File

@ -0,0 +1,57 @@
/* global React */
import style from './style';
export default React.createClass({
displayName: 'Tab',
propTypes: {
active: React.PropTypes.bool,
className: React.PropTypes.string,
disabled: React.PropTypes.bool,
hidden: React.PropTypes.bool,
label: React.PropTypes.string.isRequired,
onActive: React.PropTypes.func,
tabIndex: React.PropTypes.number
},
getDefaultProps () {
return {
className: ''
};
},
componentDidMount () {
this.active(this.props.active);
},
componentWillReceiveProps (next_props) {
if (next_props.active) this.active(next_props.active);
},
render () {
let className = `${style.tab} ${this.props.className}`;
if (this.props.active) className += ' active';
if (this.props.disabled) className += ' disabled';
if (this.props.hidden) className += ' hidden';
return (
<section
data-react-toolbox='tab'
className={className}
data-flex='vertical'
tabIndex={this.props.tabIndex}
>
{ this.props.children }
</section>
);
},
active (value) {
this.setState({active: value});
if (this.props.onActive && value) {
this.props.onActive(this);
}
}
});

View File

@ -1,74 +0,0 @@
localCSS = require './style'
Tab = require './tab'
module.exports = React.createClass
# -- States & Properties
propTypes:
className : React.PropTypes.string
index : React.PropTypes.number.required
onChange : React.PropTypes.func
getDefaultProps: ->
className : ""
index : 0
getInitialState: ->
index : @props.index
pointer : {}
# -- Lifecycle
componentDidMount: ->
@setState pointer: _pointerPosition @state.index, @refs.navigation.getDOMNode()
componentWillReceiveProps: (next_props) ->
index = next_props.index or @state.index
@setState
index : index
pointer : _pointerPosition index, @refs.navigation.getDOMNode()
# -- Events
onClick: (index, event, ref) ->
@setState
index : index
pointer : _pointerPosition index, @refs.navigation.getDOMNode()
@props.onChange? @
# -- Render
render: ->
labels = []
tabs = React.Children.map @props.children, (tab, index) =>
active = @state.index is index
className = tab.props.className
className += ' active' if active
className += ' disabled' if tab.props.disabled
className += ' hidden' if tab.props.hidden
labels.push
className : className
label : tab.props.label
key : index
onClick : (@onClick.bind null, index unless tab.props.disabled)
React.addons.cloneWithProps tab,
active : active
key : index
tabIndex : index
<div data-react-toolbox='tabs'
className={localCSS.root + ' ' + @props.className}
data-flex='vertical'>
<nav ref='navigation' data-flex='horizontal'>
{ <label {...props}>{props.label}</label> for props in labels }
</nav>
<span className={localCSS.pointer} style={@state.pointer}></span>
{ tabs }
</div>
# -- Private methods
_pointerPosition = (index = 0, navigation) ->
label = navigation.children[index].getBoundingClientRect()
style =
top : "#{navigation.getBoundingClientRect().height}px"
left : "#{label.left}px"
width : "#{label.width}px"

103
components/tabs/tabs.jsx Normal file
View File

@ -0,0 +1,103 @@
/* global React */
import { addons } from 'react/addons';
import style from './style';
function _pointerPosition (index = 0, navigation) {
const label = navigation.children[index].getBoundingClientRect();
return {
top: `${navigation.getBoundingClientRect().height}px`,
left: `${label.left}px`,
width: `${label.width}px`
};
}
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'Tabs',
propTypes: {
className: React.PropTypes.string,
index: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func
},
getDefaultProps () {
return {
className: '',
index: 0
};
},
getInitialState () {
return {
index: this.props.index,
pointer: {}
};
},
componentDidMount () {
this.setState({
pointer: _pointerPosition(this.state.index, this.refs.navigation.getDOMNode())
});
},
componentWillReceiveProps (next_props) {
const index = next_props.index || this.state.index;
this.setState({
index: index,
pointer: _pointerPosition(index, this.refs.navigation.getDOMNode())
});
},
onClick (index) {
this.setState({
index: index,
pointer: _pointerPosition(index, this.refs.navigation.getDOMNode())
});
if (this.props.onChange) this.props.onChange(this);
},
render () {
let labels = [];
const tabs = this.props.children.map((tab, index) => {
let active = this.state.index === index;
let className = tab.props.className;
if (active) className += ' active';
if (tab.props.disabled) className += ' disabled';
if (tab.props.hidden) className += ' hidden';
labels.push({
className: className,
label: tab.props.label,
key: index,
onClick: !tab.props.disabled ? this.onClick.bind(null, index) : null
});
return React.cloneElement(tab, {active: active, key: index, tabIndex: index });
});
return (
<div
data-react-toolbox='tabs'
className={style.root + ' ' + this.props.className}
data-flex='vertical'
>
<nav ref='navigation' data-flex='horizontal'>
{ labels.map((props) => { return <label {...props}>{props.label}</label>; }) }
</nav>
<span className={style.pointer} style={this.state.pointer}></span>
{ tabs }
</div>
);
},
active (value) {
this.setState({active: value});
if (this.props.onActive && value) {
this.props.onActive(this);
}
}
});

View File

@ -1,13 +0,0 @@
TestUtils = React.addons.TestUtils
module.exports =
renderComponent: (Component, props={}, state={}) ->
component = TestUtils.renderIntoDocument(<Component {...props}/>)
component.setState(state) unless state == {}
component
shallowRenderComponent: (component, props, children...) ->
shallowRenderer = TestUtils.createRenderer()
shallowRenderer.render(React.createElement(component, props,
children.length > 1 ? children : children[0]))
shallowRenderer.getRenderOutput()

View File

@ -1,92 +0,0 @@
css = require './style'
dateUtils = require '../date_utils'
Button = require '../button'
Clock = require '../clock'
Dialog = require '../dialog'
module.exports = React.createClass
displayName : 'TimePickerDialog'
# -- States & Properties
propTypes:
className : React.PropTypes.string
initialTime : React.PropTypes.object
format : React.PropTypes.oneOf(['24hr', 'ampm'])
onTimeSelected : React.PropTypes.func
getDefaultProps: ->
className : ''
initialTime : new Date()
format : '24hr'
getInitialState: ->
display : 'hours'
time : @props.initialTime
actions: [
{ caption: "Cancel", type: "flat accent", onClick: @onTimeCancel },
{ caption: "Ok", type: "flat accent", onClick: @onTimeSelected }
]
# -- Events
onClockChange: (time) ->
@setState time: time
onTimeCancel: (ref, method) ->
@refs.dialog.hide()
onTimeSelected: ->
@props.onTimeSelected(@state.time) if @props.onTimeSelected
@refs.dialog.hide()
# -- Public methods
displayMinutes: ->
@setState display: 'minutes'
displayHours: ->
@setState display: 'hours'
toggleTimeMode: ->
@refs.clock.toggleTimeMode()
show: ->
@refs.dialog.show()
setTimeout @refs.clock.handleResize, 500
# -- Private helpers
_formatHours: ->
if @props.format == 'ampm' then @state.time.getHours() % 12 || 12 else @state.time.getHours()
# -- Render
render: ->
className = " "
className += " display-#{@state.display}"
className += " format-#{dateUtils.timeMode(@state.time)}"
<Dialog ref="dialog" type={css.dialog} className={className} actions={@state.actions}>
<header className={css.header}>
<span className={css.hours} onClick={@displayHours} >
{_twoDigits(@_formatHours())}
</span>
<span className={css.separator}>:</span>
<span className={css.minutes} onClick={@displayMinutes}>
{_twoDigits(@state.time.getMinutes())}
</span>
{
if @props.format == 'ampm'
<div className={css.ampm}>
<span className={css.am} onClick={@toggleTimeMode}>AM</span>
<span className={css.pm} onClick={@toggleTimeMode}>PM</span>
</div>
}
</header>
<Clock ref="clock"
display={@state.display}
format={@props.format}
initialTime={@props.initialTime}
onChange={@onClockChange} />
</Dialog>
# -- Private helpers
_twoDigits = (number) ->
('0' + number).slice(-2)

View File

@ -0,0 +1,112 @@
/* global React */
import { addons } from 'react/addons';
import css from './style';
import time from '../utils/time';
import Clock from '../clock';
import Dialog from '../dialog';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'TimePickerDialog',
propTypes: {
className: React.PropTypes.string,
initialTime: React.PropTypes.object,
format: React.PropTypes.oneOf(['24hr', 'ampm']),
onTimeSelected: React.PropTypes.func
},
getDefaultProps () {
return {
className: '',
initialTime: new Date(),
format: '24hr'
};
},
getInitialState () {
return {
display: 'hours',
time: this.props.initialTime,
actions: [
{ label: 'Cancel', type: 'flat accent', onClick: this.onTimeCancel },
{ label: 'Ok', type: 'flat accent', onClick: this.onTimeSelected }
]
};
},
onClockChange (newTime) {
this.setState({time: newTime});
},
onTimeCancel () {
this.refs.dialog.hide();
},
onTimeSelected () {
if (this.props.onTimeSelected) this.props.onTimeSelected(this.state.time);
this.refs.dialog.hide();
},
displayMinutes () {
this.setState({display: 'minutes'});
},
displayHours () {
this.setState({display: 'hours'});
},
toggleTimeMode () {
this.refs.clock.toggleTimeMode();
},
show () {
this.refs.dialog.show();
setTimeout(this.refs.clock.calculateShape, 1000);
},
formatHours () {
if (this.props.format === 'ampm') {
return this.state.time.getHours() % 12 || 12;
} else {
return this.state.time.getHours();
}
},
renderAMPMLabels () {
if (this.props.format === 'ampm') {
return (<div className={css.ampm}>
<span className={css.am} onClick={this.toggleTimeMode}>AM</span>
<span className={css.pm} onClick={this.toggleTimeMode}>PM</span>
</div>);
}
},
render () {
let className = ` display-${this.state.display}`;
className += ` format-${time.getTimeMode(this.state.time)}`;
return (
<Dialog ref="dialog" className={className} type={css.dialog} actions={this.state.actions}>
<header className={css.header}>
<span className={css.hours} onClick={this.displayHours}>
{ ('0' + this.formatHours()).slice(-2) }
</span>
<span className={css.separator}>:</span>
<span className={css.minutes} onClick={this.displayMinutes}>
{ ('0' + this.state.time.getMinutes()).slice(-2) }
</span>
{ this.renderAMPMLabels() }
</header>
<Clock
ref="clock"
display={this.state.display}
format={this.props.format}
initialTime={this.props.initialTime}
onChange={this.onClockChange} />
</Dialog>
);
}
});

View File

@ -1,64 +0,0 @@
css = require './style'
Input = require '../input'
TimeDialog = require './dialog'
module.exports = React.createClass
displayName : 'TimePicker'
# -- States & Properties
propTypes:
format : React.PropTypes.oneOf(['24hr', 'ampm'])
value : React.PropTypes.object
getDefaultProps: ->
format : '24hr'
getInitialState: ->
value : @props.value
# -- Events
onTimeSelected: (time) ->
@refs.input.setValue(@formatTime(time))
@setState value: time
openTimeDialog: ->
@refs.dialog.show()
# -- Private methods
formatTime: (date) ->
hours = date.getHours()
mins = date.getMinutes().toString()
if (@props.format == "ampm")
isAM = hours < 12
hours = hours % 12
additional = if isAM then " am" else " pm"
hours = (hours || 12).toString()
mins = "0" + mins if (mins.length < 2 )
return hours + (if mins == "00" then "" else ":" + mins) + additional
hours = hours.toString()
hours = "0" + hours if (hours.length < 2)
mins = "0" + mins if (mins.length < 2)
return hours + ":" + mins
# -- Public methods
getValue: ->
@state.value
# -- Render
render: ->
<div>
<Input
ref="input"
type="text"
disabled={true}
onClick={@openTimeDialog}
placeholder="Pick up time"
value={@formatTime(@state.value) if @state.value} />
<TimeDialog
ref="dialog"
initialTime={@state.value}
format={@props.format}
onTimeSelected={@onTimeSelected} />
</div>

View File

@ -0,0 +1,67 @@
/* global React */
import { addons } from 'react/addons';
import time from '../utils/time';
import Input from '../input';
import TimeDialog from './dialog';
export default React.createClass({
mixins: [addons.PureRenderMixin],
displayName: 'TimePicker',
propTypes: {
format: React.PropTypes.oneOf(['24hr', 'ampm']),
value: React.PropTypes.object
},
getDefaultProps () {
return {
format: '24hr'
};
},
getInitialState () {
return {
value: this.props.value
};
},
onTimeSelected (newTime) {
this.refs.input.setValue(time.formatTime(newTime, this.props.format));
this.setState({value: newTime});
},
openTimeDialog () {
this.refs.dialog.show();
},
formatTime () {
if (this.state.value) {
return time.formatTime(this.state.value, this.props.format);
}
},
getValue () {
return this.state.value;
},
render () {
return (
<div>
<Input
ref="input"
type="text"
disabled={true}
onClick={this.openTimeDialog}
placeholder="Pick up time"
value={this.formatTime()} />
<TimeDialog
ref="dialog"
initialTime={this.state.value}
format={this.props.format}
onTimeSelected={this.onTimeSelected} />
</div>
);
}
});

View File

@ -0,0 +1,36 @@
module.exports = {
getMousePosition (event) {
return {
x: event.pageX,
y: event.pageY
};
},
getTouchPosition (event) {
return {
x: event.touches[0].pageX,
y: event.touches[0].pageY
};
},
pauseEvent (event) {
event.stopPropagation();
event.preventDefault();
event.returnValue = false;
event.cancelBubble = true;
},
addEventsToDocument (eventMap) {
for (let key in eventMap) {
document.addEventListener(key, eventMap[key], false);
}
},
removeEventsFromDocument (eventMap) {
for (let key in eventMap) {
document.removeEventListener(key, eventMap[key], false);
}
}
};

37
components/utils/index.js Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
angleFromPositions (cx, cy, ex, ey) {
let theta = Math.atan2(ey - cy, ex - cx) + Math.PI / 2;
return theta * 180 / Math.PI;
},
angle360FromPositions (cx, cy, ex, ey) {
let angle = this.angleFromPositions(cx, cy, ex, ey);
return angle < 0 ? 360 + angle : angle;
},
range (start = 0, stop = null, step = 1) {
let [_start, _stop] = (stop !== null) ? [start, stop] : [0, start];
let length = Math.max(Math.ceil((_stop - _start) / step), 0);
let range = Array(length);
for (let idx = 0; idx < length; idx++, _start += step) {
range[idx] = _start;
}
return range;
},
round (number, decimals) {
if (!isNaN(parseFloat(number)) && isFinite(number)) {
let decimalPower = Math.pow(10, decimals);
return Math.round(parseFloat(number) * decimalPower) / decimalPower;
}
return NaN;
},
events: require('./events'),
prefixer: require('./prefixer'),
time: require('./time'),
testing: require('./testing')
};

View File

@ -0,0 +1,31 @@
const WEBKIT = 'Webkit';
const MICROSOFT = 'Ms';
const properties = {
transform: [WEBKIT, MICROSOFT]
};
function capitalize (string) {
return string.charAt(0).toUpperCase() + string.substr(1);
}
function getPrefixes (property, value) {
return properties[property].reduce(function (acc, item) {
acc[`${item}${capitalize(property)}`] = value;
return acc;
}, {});
}
function prefixer (style) {
let _style = style;
for (let property in properties) {
if (style[property]) {
_style = Object.assign(_style, getPrefixes(property, style[property]));
}
}
return _style;
}
module.exports = prefixer;

View File

@ -0,0 +1,19 @@
/* global React */
const TestUtils = React.addons.TestUtils;
module.exports = {
renderComponent (Component, props = {}, state = {}) {
let component = TestUtils.renderIntoDocument(<Component {...props} />);
if (state !== {}) { component.setState(state); }
return component;
},
shallowRenderComponent (component, props, ...children) {
let shallowRenderer = TestUtils.createRenderer();
shallowRenderer.render(React.createElement(component, props, children.length > 1 ? children : children[0]));
return shallowRenderer.getRenderOutput();
}
};

177
components/utils/time.js Normal file
View File

@ -0,0 +1,177 @@
module.exports = {
getDaysInMonth (d) {
let resultDate = this.getFirstDayOfMonth(d);
resultDate.setMonth(resultDate.getMonth() + 1);
resultDate.setDate(resultDate.getDate() - 1);
return resultDate.getDate();
},
getFirstDayOfMonth (d) {
return new Date(d.getFullYear(), d.getMonth(), 1);
},
getFirstWeekDay (d) {
return this.getFirstDayOfMonth(d).getDay();
},
getTimeMode (d) {
return d.getHours() >= 12 ? 'pm' : 'am';
},
getFullMonth (d) {
let month = d.getMonth();
switch (month) {
default: return 'Unknown';
case 0: return 'January';
case 1: return 'February';
case 2: return 'March';
case 3: return 'April';
case 4: return 'May';
case 5: return 'June';
case 6: return 'July';
case 7: return 'August';
case 8: return 'September';
case 9: return 'October';
case 10: return 'November';
case 11: return 'December';
}
},
getShortMonth (d) {
let month = d.getMonth();
switch (month) {
default: return 'Unknown';
case 0: return 'Jan';
case 1: return 'Feb';
case 2: return 'Mar';
case 3: return 'Apr';
case 4: return 'May';
case 5: return 'Jun';
case 6: return 'Jul';
case 7: return 'Aug';
case 8: return 'Sep';
case 9: return 'Oct';
case 10: return 'Nov';
case 11: return 'Dec';
}
},
getFullDayOfWeek (day) {
switch (day) {
default: return 'Unknown';
case 0: return 'Sunday';
case 1: return 'Monday';
case 2: return 'Tuesday';
case 3: return 'Wednesday';
case 4: return 'Thursday';
case 5: return 'Friday';
case 6: return 'Saturday';
}
},
getShortDayOfWeek (day) {
switch (day) {
default: return 'Unknown';
case 0: return 'Sun';
case 1: return 'Mon';
case 2: return 'Tue';
case 3: return 'Wed';
case 4: return 'Thu';
case 5: return 'Fri';
case 6: return 'Sat';
}
},
clone (d) {
return new Date(d.getTime());
},
cloneAsDate (d) {
let clonedDate = this.clone(d);
clonedDate.setHours(0, 0, 0, 0);
return clonedDate;
},
isDateObject (d) {
return d instanceof Date;
},
addDays (d, days) {
let newDate = this.clone(d);
newDate.setDate(d.getDate() + days);
return newDate;
},
addMonths (d, months) {
let newDate = this.clone(d);
newDate.setMonth(d.getMonth() + months);
return newDate;
},
addYears (d, years) {
let newDate = this.clone(d);
newDate.setFullYear(d.getFullYear() + years);
return newDate;
},
setDay (d, day) {
let newDate = this.clone(d);
newDate.setDate(day);
return newDate;
},
setMonth (d, month) {
let newDate = this.clone(d);
newDate.setMonth(month);
return newDate;
},
setYear (d, year) {
let newDate = this.clone(d);
newDate.setFullYear(year);
return newDate;
},
setHours (d, hours) {
let newDate = this.clone(d);
newDate.setHours(hours);
return newDate;
},
setMinutes (d, minutes) {
let newDate = this.clone(d);
newDate.setMinutes(minutes);
return newDate;
},
toggleTimeMode (d) {
let newDate = this.clone(d);
let hours = newDate.getHours();
newDate.setHours(hours - (hours > 12 ? -12 : 12));
return newDate;
},
formatTime (date, format) {
let hours = date.getHours();
let mins = date.getMinutes().toString();
if (format === 'ampm') {
let isAM = hours < 12;
let additional = isAM ? ' am' : ' pm';
hours = hours % 12;
hours = (hours || 12).toString();
if (mins.length < 2) mins = '0' + mins;
return hours + (mins === '00' ? '' : ':' + mins) + additional;
}
hours = hours.toString();
if (hours.length < 2) hours = '0' + hours;
if (mins.length < 2) mins = '0' + mins;
return hours + ':' + mins;
}
};

View File

@ -19,6 +19,9 @@
"react": ">=0.13"
},
"devDependencies": {
"babel-core": "^5.8.23",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.20",
"coffee-jsx-loader": "^0.1.2",
"css-loader": "^0.15.1",
"extract-text-webpack-plugin": "^0.8.2",

View File

@ -1,32 +0,0 @@
"use strict"
# React-Toolbox full dependency way:
# Toolbox = require 'react-toolbox'
# Button = Toolbox.Button
# Form = Toolbox.Form
# Standalone dependencies way:
Button = require 'react-toolbox/components/button'
Form = require 'react-toolbox/components/form'
App = React.createClass
# --
getInitialState: ->
fields: [
ref: "username", label: "Your username", required: true
,
ref: "password", type: "password", label: "Your password", required: true
,
type: "submit", caption: "Login", disabled: true
]
render: ->
<app data-toolbox={true}>
<h1>Hello React-Toolbox</h1>
<Form attributes={@state.fields} />
<Button caption="Hello world!" type="square" style="primary"/>
<Button icon="adb" type="circle" style="accent" />
</app>
React.render <App/>, document.body

33
example/src/app.jsx Normal file
View File

@ -0,0 +1,33 @@
/* global React */
// React-Toolbox full dependency way:
// import {Button, Form} from 'react-toolbox'
// Standalone dependencies way:
import Button from 'react-toolbox/components/button';
import Form from 'react-toolbox/components/form';
const App = React.createClass({
getInitialState () {
return {
fields: [
{ ref: 'username', label: 'Your username', required: true},
{ ref: 'password', type: 'password', label: 'Your password', required: true},
{ type: 'submit', label: 'Login', disabled: true}
]
};
},
render () {
return (
<app data-toolbox={true}>
<h1>Hello React-Toolbox</h1>
<Form attributes={this.state.fields} />
<Button label='Hello world!' type='square' style='primary'/>
<Button icon='adb' type='circle' style='accent' />
</app>
);
}
});
React.render(<App />, document.body);

View File

@ -1,38 +0,0 @@
'use strict'
pkg = require './package.json'
node_modules = __dirname + '/node_modules'
ExtractTextPlugin = require('extract-text-webpack-plugin')
environment = process.env.NODE_ENV
module.exports =
cache : true
resolve : extensions: ['', '.cjsx', '.coffee', '.js', '.json', '.styl']
context : __dirname
entry:
commons : ['./node_modules/react-toolbox/components/commons.styl']
test : ['webpack/hot/dev-server', './src/app.cjsx']
output:
path : if environment is 'production' then './dist' else './build'
filename : pkg.name + '.[name].js'
publicPath : '/build/'
devServer:
host : 'localhost'
port : 8080
inline : true
module:
loaders: [
test : /\.cjsx$/, loader: 'coffee-jsx-loader'
,
test : /\.coffee$/, loader: 'coffee-jsx-loader'
,
test : /\.styl$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!stylus-loader!')
]
plugins: [
new ExtractTextPlugin pkg.name + '.[name].css', allChunks: false
]

36
example/webpack.config.js Normal file
View File

@ -0,0 +1,36 @@
var pkg = require('./package.json');
var node_modules = __dirname + '/node_modules';
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var environment = process.env.NODE_ENV;
module.exports = {
cache: true,
resolve: {
extensions: ['', '.jsx', '.cjsx', '.coffee', '.js', '.json', '.styl']
},
context: __dirname,
entry: {
commons: ['./node_modules/react-toolbox/components/commons.styl'],
test: ['webpack/hot/dev-server', './src/app.jsx']
},
output: {
path: environment === 'production' ? './dist' : './build',
filename: pkg.name + '.[name].js',
publicPath: '/build/'
},
devServer: {
host: '0.0.0.0',
port: 8080,
inline: true
},
module: {
noParse: [node_modules + '/react/dist/*.js'],
loaders: [
{ test: /(\.js|\.jsx)$/, exclude: /(node_modules)/, loader: 'babel?optional=runtime'},
{ test: /\.cjsx$/, loader: 'coffee-jsx-loader'},
{ test: /\.coffee$/, loader: 'coffee-jsx-loader'},
{ test: /\.styl$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader!stylus-loader!')}
]
},
plugins: [new ExtractTextPlugin(pkg.name + '.[name].css', {allChunks: false})]
};

Some files were not shown because too many files have changed in this diff Show More