├── .babelrc
├── .eslintrc
├── .gitignore
├── .node-version
├── .npmignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── circle.yml
├── example
├── basic.jsx
├── index.html
├── webpack.config.build.js
└── webpack.config.js
├── mocha.opts
├── package.json
├── src
├── actions
│ ├── cells.js
│ ├── events.js
│ └── range.js
├── cell.jsx
├── chart.jsx
├── constants.js
├── css
│ └── default.scss
├── date_range.jsx
├── event.jsx
├── header.jsx
├── index.js
├── layout.jsx
├── partial_event.jsx
├── range_date.js
├── range_selector.jsx
├── reducers
│ ├── cells.js
│ ├── event.js
│ ├── index.js
│ └── range.js
├── resources.jsx
├── scheduler.jsx
└── styles.js
├── tests
├── date_range.jsx
├── range_date.jsx
├── scheduler.jsx
└── setup.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ['es2015', 'react', 'stage-0'],
3 | plugins: ['transform-decorators-legacy', 'react-hot-loader/babel']
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 6,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "blockBindings": true,
8 | "forOf": true,
9 | "jsx": true
10 | }
11 | },
12 | "rules": {
13 | "no-underscore-dangle": 0,
14 | "indent": ["error", 2, { "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } }],
15 | "max-len": 0,
16 | "semi": 0,
17 | "quotes": 0,
18 | "no-console": 0,
19 | "no-trailing-spaces": 0,
20 | "no-shadow": 0,
21 | "curly": 0,
22 | "camelcase": 0,
23 | "react/jsx-boolean-value": 1,
24 | "react/jsx-quotes": 1,
25 | "react/jsx-no-undef": 1,
26 | "react/jsx-uses-react": 1,
27 | "react/jsx-uses-vars": 1,
28 | "react/no-did-mount-set-state": 1,
29 | "react/no-did-update-set-state": 1,
30 | "react/no-multi-comp": 1,
31 | "react/no-unknown-property": 1,
32 | "react/prop-types": 1,
33 | "react/react-in-jsx-scope": 1,
34 | "react/self-closing-comp": 1,
35 | "react/sort-comp": 1,
36 | "react/wrap-multilines": 1,
37 | "new-cap": [1, {newIsCap: true, capIsNew: false}],
38 | },
39 | "plugins": [
40 | "react"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.swp
3 | *.log
4 | example/main.js
5 | main.js
6 | index.html
7 | lib/
8 |
9 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 5.7.0
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | tests/
2 | src/
3 | mocha.opts
4 | example/
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 | This project adheres to [Semantic Versioning](http://semver.org/).
4 |
5 | ## 0.2.2 (12 Jul 2016)
6 | ### New Features
7 | - Adds a few minor performance enhancements to further speed up rendering.
8 |
9 | ### Bug Fixes
10 | - Fixes a bug where events that spilled over to the left side of the scheduler
11 | were displaying on top of the resource.
12 |
13 | ## 0.2.1 (12 Jul 2016)
14 | ### New Features
15 | - Adds a performance enhancement when rendering the chart component.
16 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Darin Haener
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codetree.com/projects/Q6Rz)
2 |
3 | # Legit Scheduler
4 | A pure React implementation of a drag and drop scheduler
5 |
6 | ## Usage
7 |
8 | Install it:
9 | ```bash
10 | $ npm install --save legit-scheduler
11 | ```
12 |
13 | Import it:
14 | ```js
15 | import Scheduler from 'legit-scheduler'
16 | ```
17 |
18 | The scheduler component has three required props:
19 | `events` - An array of event objects
20 | `resources` - An array of resources
21 | `width` - The width of the scheduler container, in pixels. An integer.
22 |
23 | The resources array is just strings:
24 | ```
25 | ['Resource 1', 'Resource 2', 'Resource 3']
26 | ```
27 |
28 | The events array is an array of objects:
29 | ```
30 | {
31 | title: 'A great event', // Required: The title of the event
32 | startDate: '2016-01-24', // Required: The start date, must be in the format of "YYYY-MM-DD"
33 | duration: 4, // Required: The duration of the event in days
34 | resource: 'Resource 1', // Required: The name of the resource the event belongs to. Must match the resource name from the resources prop
35 | id: '3829-fds89', // Required: A unique identifier. This can be anything you want as long as it's unique
36 | disabled: false, // Optional: Whether or not this event can be moved (it can still be resized). Defaults to false.
37 | styles: {} // Optional: An object of styles to apply to the event object
38 | }
39 | ```
40 |
41 | The scheduler component also takes more optional props:
42 |
43 | `onEventChanged` - A call back that is fired when the event is moved. It receives an object containing the new event props
44 | `onEventResized` - A call back that is fired when the event is resized. It receives an object containing the new event props
45 | `onEventClicked` - A call back that is fired when the event is clicked. It receives an object containing the event props
46 | `onCellClicked` - A call back that is fired when an empty cell on the scheduler is clicked. It receives the date and resource name as props
47 | `onRangeChanged` - A call back that is fired when the date range is changed. It receives a `DateRange` object with the new range.
48 | `from` - Either a date string or a `RangeDate` object defining the start date for the range.
49 | `to` - Either a date string or a `RangeDate` object defining the end date for the range.
50 |
51 | ## Development
52 | ```bash
53 | $ npm install
54 | $ npm run example
55 | ```
56 |
57 | Visit: `localhost:8080/example`
58 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 5.7.0
4 |
--------------------------------------------------------------------------------
/example/basic.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import Scheduler from '../src/scheduler';
4 | import RangeDate from '../src/range_date';
5 | import DateRange from '../src/date_range';
6 | import { whyDidYouUpdate } from 'why-did-you-update';
7 |
8 | // Uncomment this to examine where you can get performance boosts
9 | //whyDidYouUpdate(React);
10 |
11 | var resources = ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'],
12 | today = new RangeDate(new Date()),
13 | events = [
14 | {
15 | id: 'foobar',
16 | title: 'Do this',
17 | startDate: today.advance('days', 1).toRef(),
18 | duration: 5,
19 | resource: 'One'
20 | },
21 | {
22 | id: 'barfoo',
23 | title: 'Do that',
24 | startDate: today.advance('days', 3).toRef(),
25 | duration: 4,
26 | resource: 'Two'
27 | },
28 | {
29 | id: 'barfoobaz',
30 | title: 'I am disabled',
31 | startDate: today.advance('days', 2).toRef(),
32 | duration: 7,
33 | resource: 'Three',
34 | disabled: true
35 | },
36 | {
37 | id: 'foobah',
38 | title: 'Do another thing',
39 | startDate: today.advance('days', 6).toRef(),
40 | duration: 14,
41 | resource: 'Seven'
42 | },
43 | {
44 | id: 'foobaz',
45 | title: 'Do another thing next month',
46 | startDate: today.advance('days', 36).toRef(),
47 | duration: 14,
48 | resource: 'Seven'
49 | }
50 | ]
51 |
52 | class Basic extends React.Component {
53 | constructor(props) {
54 | super(props)
55 | let from = new RangeDate()
56 | let to = from.advance('weeks', 2)
57 |
58 | this.state = {
59 | events: props.events,
60 | range: new DateRange(from, to)
61 | }
62 | }
63 |
64 | eventChanged(props) {
65 | const index = this.state.events.findIndex(event => event.id === props.id)
66 | const newEvents = this.state.events
67 | newEvents[index] = props
68 | this.setState({ ...props, events: newEvents })
69 | console.log(props)
70 | }
71 |
72 | eventResized(props) {
73 | const index = this.state.events.findIndex(event => event.id === props.id)
74 | const newEvents = this.state.events
75 | newEvents[index] = props
76 | this.setState({ ...props, events: newEvents })
77 | console.log(props)
78 | }
79 |
80 | eventClicked(props) {
81 | alert(`${props.title} clicked!`)
82 | console.log(props)
83 | }
84 |
85 | cellClicked(resource, date) {
86 | alert(`You clicked on ${resource} - ${date}`)
87 | console.log(resource, date)
88 | }
89 |
90 | rangeChanged(range) {
91 | this.setState({ range: range })
92 | }
93 |
94 | render() {
95 | const { events, range, title, startDate, duration, resource } = this.state,
96 | { from, to } = range
97 |
98 | return (
99 |
100 |
112 |
113 |
114 |
Current Event
115 |
116 | - Title: {title}
117 | - Start Date: {startDate}
118 | - Duration: {duration} days
119 | - Resource: {resource}
120 |
121 |
122 |
123 | )
124 | }
125 | }
126 |
127 | require('../src/css/default.scss')
128 | render(, document.getElementById('react'))
129 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | React Resource Scheduler Example
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/webpack.config.build.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | entry: './basic.jsx',
6 | output: {
7 | path: __dirname,
8 | filename: "[name].js",
9 | },
10 | resolve: {
11 | extensions: ['', '.js', '.jsx', '.es6'],
12 | modulesDirectories: ['node_modules']
13 | },
14 | module: {
15 | loaders: [
16 | { test: /\.jsx$|\.es6$|\.js$/, loaders: ['react-hot-loader', 'babel-loader?stage=0'], exclude: /node_modules/ },
17 | { test: /\.scss$|\.css$/, loader: 'style-loader!style!css!sass' },
18 | { test: /\.(jpe?g|png|gif)$/i, loader: 'url?limit=10000!img?progressive=true' },
19 | { test: /\.json$/, loader: 'json' }
20 | ]
21 | },
22 | plugins: [
23 | new webpack.NoEmitOnErrorsPlugin()
24 | ],
25 | devtool: "eval-source-map"
26 | };
27 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 | entry: {
6 | 'basic': [
7 | 'webpack-dev-server/client?http://localhost:8080/',
8 | 'webpack/hot/only-dev-server',
9 | './example/basic.jsx'
10 | ]
11 | },
12 | output: {
13 | path: __dirname,
14 | filename: "[name].js",
15 | publicPath: 'http://localhost:8080/',
16 | chunkFilename: '[id].chunk.js',
17 | sourceMapFilename: '[name].map'
18 | },
19 | resolve: {
20 | extensions: ['*', '.js', '.jsx', '.es6'],
21 | modules: ['node_modules']
22 | },
23 | module: {
24 | loaders: [
25 | { test: /\.jsx$|\.es6$|\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ },
26 | { test: /\.scss$|\.css$/, loader: 'style-loader!style-loader!css-loader!sass-loader' },
27 | { test: /\.(jpe?g|png|gif)$/i, loader: 'url?limit=10000!img?progressive=true' },
28 | { test: /\.json/, loader: 'json-loader' }
29 | ]
30 | },
31 | plugins: [
32 | new webpack.NoEmitOnErrorsPlugin()
33 | ],
34 | devtool: "cheap-source-map"
35 | };
36 |
--------------------------------------------------------------------------------
/mocha.opts:
--------------------------------------------------------------------------------
1 | --require ./tests/setup
2 | --full-trace
3 | --compilers js:babel-core/register
4 | --recursive ./tests/**/*.jsx
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "legit-scheduler",
3 | "version": "0.3.2",
4 | "description": "A React drag and drop scheduler component",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "mocha --opts ./mocha.opts; eslint ./src/ --ext .jsx,.js --global require,exports:true",
8 | "compile": "babel src --ignore __tests__ --stage 0 --out-dir lib; node-sass src/css/default.scss lib/css/default.css",
9 | "example": "webpack-dev-server --config ./example/webpack.config.js --hot"
10 | },
11 | "author": "Darin Haener (https://github.com/dphaener)",
12 | "license": "MIT",
13 | "keywords": [
14 | "react-component",
15 | "react",
16 | "calendar",
17 | "scheduler",
18 | "gantt"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "git+ssh://git@github.com/legitcode/scheduler.git"
23 | },
24 | "dependencies": {
25 | "immutable": "3.8.1",
26 | "legit-rubyfill": "0.0.7",
27 | "moment": "2.18.0",
28 | "moment-timezone": "0.5.11",
29 | "prop-types": "^15.6.0",
30 | "react": "16.2.0",
31 | "react-dnd": "2.5.4",
32 | "react-dnd-html5-backend": "2.5.4",
33 | "react-dom": "16.2.0",
34 | "react-draggable": "2.2.3",
35 | "react-redux": "5.0.6",
36 | "redux": "3.7.2",
37 | "redux-batched-actions": "0.2.1",
38 | "strftime": "0.10.0"
39 | },
40 | "devDependencies": {
41 | "babel": "6.23.0",
42 | "babel-cli": "^6.24.0",
43 | "babel-core": "6.24.0",
44 | "babel-eslint": "7.1.1",
45 | "babel-loader": "6.4.1",
46 | "babel-plugin-transform-decorators-legacy": "1.3.4",
47 | "babel-preset-es2015": "6.24.0",
48 | "babel-preset-react": "6.23.0",
49 | "babel-preset-stage-0": "6.22.0",
50 | "chai": "3.5.0",
51 | "css-loader": "0.27.3",
52 | "eslint": "3.18.0",
53 | "eslint-plugin-react": "6.10.2",
54 | "estraverse-fb": "1.3.1",
55 | "expect": "1.20.2",
56 | "file-loader": "0.10.1",
57 | "img-loader": "2.0.0",
58 | "jsdom": "9.12.0",
59 | "json-loader": "0.5.7",
60 | "legit-tests": "1.1.2",
61 | "mocha": "3.2.0",
62 | "mocha-babel": "3.0.3",
63 | "node-libs-browser": "2.0.0",
64 | "node-sass": "4.5.0",
65 | "react-addons-perf": "15.4.2",
66 | "react-hot-loader": "3.1.3",
67 | "sass-loader": "6.0.3",
68 | "sinon": "2.1.0",
69 | "sinon-chai": "2.8.0",
70 | "style-loader": "0.14.1",
71 | "timekeeper": "1.0.0",
72 | "url-loader": "0.5.8",
73 | "webpack": "3.10.0",
74 | "webpack-dev-server": "2.11.0",
75 | "why-did-you-update": "0.1.0"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/actions/cells.js:
--------------------------------------------------------------------------------
1 | export function createCells(resources, range) {
2 | return {
3 | type: 'createCells',
4 | range,
5 | resources
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/actions/events.js:
--------------------------------------------------------------------------------
1 | export function moveEvent(event, cell, callback) {
2 | return {
3 | type: 'moveEvent',
4 | callback,
5 | event,
6 | cell
7 | }
8 | }
9 |
10 | export function updateEventDuration(event, duration, callback) {
11 | return {
12 | type: 'updateEventDuration',
13 | callback,
14 | event,
15 | duration
16 | }
17 | }
18 |
19 | export function replaceResources(resources) {
20 | return {
21 | type: 'replaceResources',
22 | resources
23 | }
24 | }
25 |
26 | export function replaceEvents(events) {
27 | return {
28 | type: 'replaceEvents',
29 | events
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/actions/range.js:
--------------------------------------------------------------------------------
1 | export function advanceRange() {
2 | return {
3 | type: 'advanceRange',
4 | nextAction: 'createCells'
5 | }
6 | }
7 |
8 | export function retardRange() {
9 | return {
10 | type: 'retardRange',
11 | nextAction: 'createCells'
12 | }
13 | }
14 |
15 | export function setRange(range) {
16 | return {
17 | type: 'setRange',
18 | range
19 | }
20 | }
21 |
22 | export function clearRangeFlag() {
23 | return {
24 | type: 'clearRangeFlag'
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/cell.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React from 'react'
3 | import PropTypes from 'prop-types';
4 | import { DropTarget } from 'react-dnd'
5 |
6 | // Local LIbraries
7 | import { ItemTypes } from './constants'
8 |
9 | // Styles
10 | import { cell } from './styles'
11 |
12 | const cellTarget = {
13 | drop(props, monitor, component) {
14 | return Object.assign(component.state, props)
15 | },
16 | canDrop(props) {
17 | return !props.children
18 | }
19 | }
20 |
21 | function collect(connect, monitor) {
22 | return {
23 | connectDropTarget: connect.dropTarget(),
24 | isOver: monitor.isOver()
25 | }
26 | }
27 |
28 | @DropTarget(ItemTypes.EVENT, cellTarget, collect)
29 | export default class Cell extends React.Component {
30 | static propTypes = {
31 | resource: PropTypes.string.isRequired,
32 | date: PropTypes.string.isRequired
33 | }
34 |
35 | constructor(props) {
36 | super(props);
37 | this.state = { cellWidth: 0 };
38 | }
39 |
40 | componentDidMount() {
41 | const node = this.wrapper;
42 | const rect = node.getBoundingClientRect();
43 | const cellWidth = rect.width + 2;
44 |
45 | this.setState({ cellWidth });
46 | }
47 |
48 | shouldComponentUpdate(nextProps, nextState) {
49 | //if (this.state.cellWidth !== nextState.cellWidth) { shouldUpdate = true }
50 | //if (nextProps.resource !== this.props.resource) { shouldUpdate = true }
51 | //if (nextProps.date !== this.props.date) { shouldUpdate = true }
52 | //if (nextProps.children && !this.props.children) { shouldUpdate = true }
53 |
54 | if (!this.areObjectsEqual(this.props.children, nextProps.children)) { return true }
55 |
56 | return false;
57 | }
58 |
59 | areObjectsEqual(first = {}, second = {}) {
60 | return Object.keys(first).reduce((prev, curr) => {
61 | if (first[curr] !== second[curr]) { return false }
62 | }, true);
63 | }
64 |
65 | render() {
66 | const { children, connectDropTarget, onClick } = this.props;
67 |
68 | return (
69 | connectDropTarget(
70 | { this.wrapper = el; }}>
71 | { React.Children.map(children, child => React.cloneElement(child, this.state)) }
72 |
73 | )
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/chart.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React, { Component } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { DragDropContext } from 'react-dnd';
5 | import HTML5Backend from 'react-dnd-html5-backend';
6 | import 'legit-rubyfill/array/each_slice';
7 | import 'legit-rubyfill/array/equals';
8 |
9 | // Local Libraries
10 | import Event from './event';
11 | import PartialEvent from './partial_event';
12 | import Cell from './cell';
13 | import RangeDate from './range_date';
14 |
15 | // Styles
16 | import { chart, cellWrapper } from './styles';
17 |
18 | @DragDropContext(HTML5Backend)
19 | export default class Chart extends Component {
20 | static propTypes = {
21 | events: PropTypes.array.isRequired,
22 | resources: PropTypes.array.isRequired,
23 | range: PropTypes.object.isRequired,
24 | cells: PropTypes.object.isRequired,
25 | eventChanged: PropTypes.func.isRequired,
26 | eventResized: PropTypes.func.isRequired,
27 | eventClicked: PropTypes.func.isRequired,
28 | cellClicked: PropTypes.func.isRequired,
29 | rowHeight: PropTypes.number.isRequired,
30 | width: PropTypes.number.isRequired
31 | }
32 |
33 | shouldComponentUpdate(nextProps, nextState) {
34 | let shouldUpdate = true;
35 |
36 | // First test to see if the resources are exactly the same
37 | if (nextProps.resources.equals(this.props.resources)) { shouldUpdate = false }
38 |
39 | // Now let's look at the events and see if they are different
40 | nextProps.events.forEach((event, idx) => {
41 | if (!this.areObjectsEqual(event, this.props.events[idx])) { shouldUpdate = true }
42 | })
43 |
44 | return shouldUpdate;
45 | }
46 |
47 | areObjectsEqual(first, second) {
48 | return Object.keys(first).reduce((prev, curr) => {
49 | if (first[curr] !== second[curr]) { return false }
50 | }, true);
51 | }
52 |
53 | renderEvent(resource, date) {
54 | const { rowHeight, eventChanged, eventResized, eventClicked } = this.props
55 | const currentEvent = this.props.events.find(event => {
56 | return event.resource === resource && event.startDate === date
57 | })
58 |
59 | if (currentEvent) {
60 | return (
61 |
68 | )
69 | } else {
70 | const partialEvent = this.props.events.find(event => {
71 | let eventEnd = new RangeDate(event.startDate).advance('days', event.duration),
72 | from = this.props.range.from.date,
73 | eventStart = new RangeDate(event.startDate).date
74 |
75 | return (
76 | eventEnd.toRef() === date &&
77 | from.isAfter(eventStart, 'day') &&
78 | event.resource === resource
79 | )
80 | })
81 |
82 | if (partialEvent) return (
83 |
88 | )
89 | }
90 | }
91 |
92 | cellClicked(ev, resource, date) {
93 | ev.stopPropagation()
94 | const targetClass = ev.target.attributes[0].value
95 | if (targetClass !== 'resizer') {
96 | this.props.cellClicked(resource, date)
97 | }
98 | }
99 |
100 | renderCell(resource, date) {
101 | const { width, range } = this.props
102 |
103 | return (
104 |
108 | ::this.cellClicked(ev, resource, date)}>
112 | { this.renderEvent(resource, date) }
113 | |
114 |
115 | )
116 | }
117 |
118 | renderRow(resource, idx) {
119 | const { range, width } = this.props
120 |
121 | return (
122 |
123 | { range.map(date => this.renderCell(resource, date.toRef())) }
124 |
125 | )
126 | }
127 |
128 | createCells() {
129 | const { resources } = this.props,
130 | rows = []
131 |
132 | resources.forEach((resource, idx) => {
133 | rows.push(this.renderRow(resource, idx))
134 | })
135 |
136 | return rows
137 | }
138 |
139 | render() {
140 | const { width } = this.props
141 |
142 | return (
143 |
144 | { this.createCells() }
145 |
146 | )
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | EVENT: 'event'
3 | }
4 |
--------------------------------------------------------------------------------
/src/css/default.scss:
--------------------------------------------------------------------------------
1 | div.event-box {
2 | span.resizer {
3 | &:hover {
4 | cursor: ew-resize;
5 | }
6 | }
7 |
8 | span.event-handle {
9 | &:hover {
10 | cursor: move;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/date_range.jsx:
--------------------------------------------------------------------------------
1 | import RangeDate from './range_date'
2 |
3 | export default class DateRange {
4 | constructor(...args) {
5 | let [ from, to ] = args
6 |
7 | this.from = new RangeDate(from)
8 | this.to = new RangeDate(to)
9 | }
10 |
11 | daysInRange() {
12 | return this.to.date.diff(this.from.date, 'days') + 1
13 | }
14 |
15 | toString() {
16 | return `${this.from.toString()} - ${this.to.toString()}`
17 | }
18 |
19 | advance(reverse = false) {
20 | const advanceAmount = reverse ? -this.daysInRange() : this.daysInRange(),
21 | from = this.from.advance('days', advanceAmount),
22 | to = this.to.advance('days', advanceAmount)
23 |
24 | return new DateRange(from, to)
25 | }
26 |
27 | map(func) {
28 | let current = this.from,
29 | dates = []
30 |
31 | while (current.value() <= this.to.value()) {
32 | dates.push(func(current))
33 | current = current.advance('days', 1)
34 | }
35 |
36 | return dates
37 | }
38 |
39 | forEach(func) {
40 | let current = this.from
41 |
42 | while (current.value() <= this.to.value()) {
43 | func(current)
44 | current = current.advance('days', 1)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/event.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React from 'react'
3 | import PropTypes from 'prop-types';
4 | import { DragSource } from 'react-dnd'
5 | import { connect } from 'react-redux'
6 |
7 | // Local Libraries
8 | import { moveEvent, updateEventDuration } from './actions/events'
9 | import { ItemTypes } from './constants'
10 |
11 | // Styles
12 | import { eventHandleStyles, eventStyles, resizerStyles, boxStyles } from './styles'
13 |
14 | /* globals document */
15 |
16 | const eventSource = {
17 | beginDrag(props) {
18 | return {
19 | resource: props.resource,
20 | date: props.startDate,
21 | id: props.id
22 | }
23 | },
24 | endDrag(props, monitor, component) {
25 | if (!monitor.didDrop()) return
26 |
27 | component.props.dispatch(
28 | moveEvent(
29 | props,
30 | monitor.getDropResult(),
31 | props.eventChanged
32 | )
33 | )
34 | },
35 | canDrag(props) {
36 | return !props.disabled
37 | }
38 | }
39 |
40 | function collect(connect, monitor) {
41 | return {
42 | connectDragSource: connect.dragSource(),
43 | isDragging: monitor.isDragging(),
44 | connectDragPreview: connect.dragPreview()
45 | }
46 | }
47 |
48 | @DragSource(ItemTypes.EVENT, eventSource, collect)
49 | class Event extends React.Component {
50 | static propTypes = {
51 | title: PropTypes.string.isRequired,
52 | startDate: PropTypes.string.isRequired,
53 | duration: PropTypes.number.isRequired,
54 | resource: PropTypes.string.isRequired,
55 | dispatch: PropTypes.func,
56 | eventChanged: PropTypes.func.isRequired,
57 | eventResized: PropTypes.func.isRequired,
58 | eventClicked: PropTypes.func.isRequired,
59 | cellWidth: PropTypes.number,
60 | disabled: PropTypes.bool,
61 | id: PropTypes.string.isRequired,
62 | styles: PropTypes.object,
63 | isDragging: PropTypes.bool.isRequired,
64 | connectDragSource: PropTypes.func.isRequired,
65 | connectDragPreview: PropTypes.func.isRequired,
66 | rowHeight: PropTypes.number.isRequired,
67 | children: PropTypes.node
68 | }
69 |
70 | constructor(props) {
71 | super(props)
72 |
73 | this.state = {}
74 | }
75 |
76 | componentWillMount() {
77 | const { duration, cellWidth } = this.props,
78 | width = (duration * cellWidth) === 0 ? cellWidth : (duration * cellWidth) - duration - 9
79 |
80 | this.setState({ cellWidth, width, startWidth: width })
81 | }
82 |
83 | componentDidMount() {
84 | this.refs.resizer.addEventListener('mousedown', this.initDrag, false)
85 | }
86 |
87 | componentWillReceiveProps(nextProps) {
88 | const { duration, cellWidth } = nextProps,
89 | width = (duration * cellWidth) === 0 ? cellWidth : duration * cellWidth - duration - 9
90 |
91 | this.setState({ duration, width, startWidth: width })
92 | }
93 |
94 | initDrag = (ev) => {
95 | ev.stopPropagation()
96 |
97 | this.setState({
98 | startX: ev.clientX
99 | })
100 |
101 | document.documentElement.addEventListener('mousemove', this.doDrag, false)
102 | document.documentElement.addEventListener('mouseup', this.stopDrag, false)
103 | }
104 |
105 | doDrag = (ev) => {
106 | ev.stopPropagation()
107 | const { startWidth, startX } = this.state,
108 | newWidth = (startWidth + ev.clientX - startX)
109 |
110 | this.setState({ width: newWidth })
111 | }
112 |
113 | stopDrag = (ev) => {
114 | ev.stopPropagation()
115 | const { eventResized, disabled, dispatch, id, title, startDate, resource, styles } = this.props,
116 | { width } = this.state,
117 | newDuration = this.roundToNearest(width)
118 |
119 | dispatch(
120 | updateEventDuration(
121 | { disabled, id, title, startDate, resource, styles },
122 | newDuration,
123 | eventResized
124 | )
125 | )
126 |
127 | document.documentElement.removeEventListener('mousemove', this.doDrag, false)
128 | document.documentElement.removeEventListener('mouseup', this.stopDrag, false)
129 | }
130 |
131 | roundToNearest(numToRound) {
132 | return Math.ceil(numToRound / this.props.cellWidth)
133 | }
134 |
135 | dispatchEventClick(ev) {
136 | ev.stopPropagation()
137 | this.props.eventClicked(this.props)
138 | }
139 |
140 | render() {
141 | const { styles, isDragging, connectDragSource, connectDragPreview, id, title, children, rowHeight, ...rest } = this.props,
142 | { width } = this.state,
143 | resizerStyleMerge = Object.assign({ height: '100%' }, resizerStyles),
144 | defaultStyles = { color: '#000', backgroundColor: 'darkgrey' },
145 | eventStyleMerge = Object.assign({ width }, styles || defaultStyles, eventStyles),
146 | opacity = isDragging ? 0 : 1,
147 | boxStyleMerge = Object.assign({ width, opacity }, boxStyles)
148 |
149 | return (
150 |
151 | { isDragging ? null :
152 | connectDragPreview(
153 |
154 | { connectDragSource(
155 |
156 | )
157 | }
158 | {title}
159 |
160 | )
161 | }
162 |
163 |
164 | )
165 | }
166 | }
167 |
168 | export default connect()(Event)
169 |
--------------------------------------------------------------------------------
/src/header.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React from 'react'
3 |
4 | // Styles
5 | import { headerWrapper, chartHeader } from './styles'
6 |
7 | export default ({ range, width }) => (
8 |
9 |
10 | { range.map(date => (
11 |
15 | {date.toCal()}
16 |
17 | ))
18 | }
19 |
20 |
21 | )
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Scheduler from './scheduler'
2 | import RangeDate from './range_date'
3 | import DateRange from './date_range'
4 |
5 | export { RangeDate, DateRange }
6 |
7 | export default Scheduler
8 |
--------------------------------------------------------------------------------
/src/layout.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types';
4 | import { connect } from 'react-redux'
5 | import 'legit-rubyfill/array/equals';
6 |
7 | // Local Libraries
8 | import Chart from './chart'
9 | import Header from './header'
10 | import Resources from './resources'
11 | import RangeSelector from './range_selector'
12 |
13 | class Layout extends Component {
14 | static propTypes = {
15 | resources: PropTypes.array.isRequired,
16 | range: PropTypes.object.isRequired,
17 | events: PropTypes.array.isRequired,
18 | cells: PropTypes.object.isRequired,
19 | eventChanged: PropTypes.func.isRequired,
20 | eventResized: PropTypes.func.isRequired,
21 | eventClicked: PropTypes.func.isRequired,
22 | cellClicked: PropTypes.func.isRequired,
23 | rangeChanged: PropTypes.func.isRequired,
24 | rangeDidChange: PropTypes.bool.isRequired,
25 | width: PropTypes.number.isRequired,
26 | rowHeight: PropTypes.number.isRequired
27 | }
28 |
29 | shouldComponentUpdate(nextProps, nextState) {
30 | let shouldUpdate = true;
31 |
32 | //if (nextProps.resources.equals(this.props.resources)) { shouldUpdate = false }
33 | //if (nextProps.events.length !== this.props.events.length) { shouldUpdate = true }
34 |
35 | return shouldUpdate;
36 | }
37 |
38 | render() {
39 | const { rangeDidChange, rangeChanged, width, range, resources, rowHeight } = this.props
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | export default connect(state => {
57 | const { rangeDidChange, range } = state.range.toJS()
58 | const { resources, events } = state.event.toJS()
59 | const { cells } = state.cells.toJS()
60 | return { rangeDidChange, cells, range, resources, events }
61 | })(Layout)
62 |
--------------------------------------------------------------------------------
/src/partial_event.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React from 'react'
3 | import PropTypes from 'prop-types';
4 |
5 | // Styles
6 | import { partialEventStyles, boxStyles } from './styles'
7 |
8 | export default class PartialEvent extends React.Component {
9 | static propTypes = {
10 | title: PropTypes.string.isRequired,
11 | startDate: PropTypes.string.isRequired,
12 | duration: PropTypes.number.isRequired,
13 | resource: PropTypes.string.isRequired,
14 | dispatch: PropTypes.func,
15 | cellWidth: PropTypes.number,
16 | disabled: PropTypes.bool,
17 | id: PropTypes.string.isRequired,
18 | styles: PropTypes.object,
19 | rowHeight: PropTypes.number.isRequired,
20 | children: PropTypes.node,
21 | eventClicked: PropTypes.func.isRequired
22 | }
23 |
24 | constructor(props) {
25 | super(props)
26 |
27 | this.state = {}
28 | }
29 |
30 | componentDidMount() {
31 | const { duration, cellWidth } = this.props,
32 | width = (duration * cellWidth) === 0 ? cellWidth : (duration * cellWidth) - duration - 9
33 |
34 | this.setState({ cellWidth, width, startWidth: width })
35 | }
36 |
37 | componentWillReceiveProps(nextProps) {
38 | const { duration, cellWidth } = nextProps,
39 | width = (duration * cellWidth) === 0 ? cellWidth : duration * cellWidth - duration - 9
40 |
41 | this.setState({ duration, width, startWidth: width })
42 | }
43 |
44 | dispatchEventClick(ev) {
45 | ev.stopPropagation()
46 | this.props.eventClicked(this.props)
47 | }
48 |
49 | render() {
50 | const { styles, id, title, children, rowHeight, ...rest } = this.props,
51 | { width } = this.state,
52 | defaultStyles = { color: '#000', backgroundColor: 'darkgrey' },
53 | eventStyleMerge = Object.assign({ width }, styles || defaultStyles, partialEventStyles),
54 | boxStyleMerge = Object.assign({ height: '100%', top: '2px', width }, boxStyles)
55 |
56 | return (
57 |
58 |
59 | {title}
60 |
61 |
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/range_date.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment-timezone'
2 |
3 | export default class RangeDate {
4 | constructor(date = null) {
5 | if (date) {
6 | this.date = date instanceof RangeDate ? moment(date.value()) : moment(date)
7 | } else {
8 | this.date = moment()
9 | }
10 | }
11 |
12 | toString() {
13 | return this.date.format('MMMM D, YYYY')
14 | }
15 |
16 | toCal() {
17 | return this.date.format('MMM[\n]M/D')
18 | }
19 |
20 | toRef() {
21 | return this.date.format('YYYY-MM-DD')
22 | }
23 |
24 | value() {
25 | return this.date._d
26 | }
27 |
28 | advance(increment, amount) {
29 | return new RangeDate(
30 | this.date.clone().add(amount, increment)
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/range_selector.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types';
4 | import { connect } from 'react-redux'
5 |
6 | // Actions
7 | import { advanceRange, retardRange, clearRangeFlag } from './actions/range'
8 |
9 | // Styles
10 | import { selectors, leftButton, leftButtonAfter, rightButton, rightButtonAfter } from './styles'
11 |
12 | class RangeSelector extends Component {
13 | static propTypes = {
14 | range: PropTypes.object.isRequired,
15 | rangeDidChange: PropTypes.bool.isRequired,
16 | dispatch: PropTypes.func.isRequired,
17 | rangeChanged: PropTypes.func.isRequired,
18 | leftCursor: PropTypes.string,
19 | rightCursor: PropTypes.string
20 | }
21 |
22 | constructor(props) {
23 | super(props)
24 |
25 | this.state = {}
26 | }
27 |
28 | shouldComponentUpdate(nextProps, nextState) {
29 | if (nextProps.rangeDidChange) { return true }
30 | return !this.areCursorsEqual(this.state, nextState);
31 | }
32 |
33 | areCursorsEqual(currentState, nextState) {
34 | return currentState.rightCursor === nextState.rightCursor &&
35 | currentState.leftCursor === nextState.leftCursor
36 | }
37 |
38 | componentWillReceiveProps(nextProps) {
39 | if (nextProps.rangeDidChange) {
40 | this.props.dispatch(clearRangeFlag())
41 | this.props.rangeChanged(nextProps.range)
42 | }
43 | }
44 |
45 | addLeftHover = () => {
46 | this.setState({ leftCursor: 'pointer' })
47 | }
48 |
49 | addRightHover = () => {
50 | this.setState({ rightCursor: 'pointer' })
51 | }
52 |
53 | removeLeftHover = () => {
54 | this.setState({ leftCursor: 'arrow' })
55 | }
56 |
57 | removeRightHover = () => {
58 | this.setState({ rightCursor: 'arrow' })
59 | }
60 |
61 | previousClicked() {
62 | this.props.dispatch(retardRange())
63 | }
64 |
65 | nextClicked() {
66 | this.props.dispatch(advanceRange())
67 | }
68 |
69 | render() {
70 | const { leftCursor, rightCursor } = this.state,
71 | { range } = this.props,
72 | mergedLeftButtonStyle = Object.assign({ cursor: leftCursor }, leftButton),
73 | mergedRightButtonStyle = Object.assign({ cursor: rightCursor }, rightButton)
74 |
75 | return (
76 |
77 |
85 |
86 | { range.toString() }
87 |
88 |
96 |
97 | )
98 | }
99 | }
100 |
101 | export default connect()(RangeSelector)
102 |
--------------------------------------------------------------------------------
/src/reducers/cells.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable'
2 |
3 | const defaultState = Map({
4 | cells: Map({})
5 | })
6 |
7 | export default (state = defaultState, action) => {
8 | switch(action.type) {
9 | case 'createCells':
10 | const { range, resources } = action
11 | let cells = Map({})
12 |
13 | resources.forEach(resource => {
14 | range.forEach(date => {
15 | cells = cells.setIn([`${resource}${date.toRef()}`], Map({ resource: resource, date: date.toRef()}))
16 | })
17 | })
18 |
19 | return fromJS({ cells: cells })
20 | default:
21 | return state
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/reducers/event.js:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable'
2 |
3 | const defaultState = Map({
4 | events: [],
5 | resources: []
6 | })
7 |
8 | export default (state = defaultState, action) => {
9 | let index, newEvent
10 |
11 | switch (action.type) {
12 | case 'replaceResources':
13 | const resources = fromJS(action.resources)
14 | return state.setIn(['resources'], resources)
15 | case 'replaceEvents':
16 | const events = fromJS(action.events)
17 | return state.setIn(['events'], events)
18 | case 'moveEvent':
19 | newEvent = Map(action.event).withMutations(map => {
20 | map.set('startDate', action.cell.date).
21 | set('resource', action.cell.resource)
22 | }).filter((value, key) => ['styles', 'duration', 'id', 'resource', 'startDate', 'title'].includes(key))
23 |
24 | index = state.get('events').findIndex(item => {
25 | return item.get('id') === action.event.id
26 | })
27 |
28 | return state.setIn(['events', index], newEvent)
29 | case 'updateEventDuration':
30 | newEvent = Map(action.event).withMutations(map => {
31 | map.set('duration', action.duration)
32 | })
33 | index = state.get('events').findIndex(item => {
34 | return item.get('id') === action.event.id
35 | })
36 |
37 | return state.setIn(['events', index], newEvent)
38 | default:
39 | return state
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import event from './event'
3 | import range from './range'
4 | import cells from './cells'
5 |
6 | export default combineReducers({
7 | event,
8 | range,
9 | cells
10 | })
11 |
--------------------------------------------------------------------------------
/src/reducers/range.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable'
2 |
3 | import RangeDate from '../range_date'
4 | import DateRange from '../date_range'
5 |
6 | const from = new RangeDate()
7 | const to = new RangeDate().advance('weeks', 4)
8 |
9 | const defaultState = Map({
10 | range: new DateRange(from, to),
11 | rangeDidChange: false
12 | })
13 |
14 | export default (state = defaultState, action) => {
15 | let newRange
16 |
17 | switch(action.type) {
18 | case 'setRange':
19 | return state.setIn(['range'], action.range)
20 | case 'advanceRange':
21 | newRange = state.get('range').advance()
22 | return state.withMutations(map => {
23 | map.set('range', newRange).
24 | set('rangeDidChange', true)
25 | })
26 | case 'retardRange':
27 | newRange = state.get('range').advance(true)
28 | return state.withMutations(map => {
29 | map.set('range', newRange).
30 | set('rangeDidChange', true)
31 | })
32 | case 'clearRangeFlag':
33 | return state.setIn(['rangeDidChange'], false)
34 | default:
35 | return state
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/resources.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React from 'react'
3 |
4 | // Styles
5 | import { resourceWrapper, resourceSideBar } from './styles'
6 |
7 | export default ({ width, resources, height }) => (
8 |
9 | { resources.map(resource => (
10 |
14 | {resource}
15 |
16 | ))
17 | }
18 |
19 | )
20 |
--------------------------------------------------------------------------------
/src/scheduler.jsx:
--------------------------------------------------------------------------------
1 | // Vendor Libraries
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types';
4 | import { Provider } from 'react-redux'
5 | import { createStore, applyMiddleware } from 'redux'
6 | import { batchActions, enableBatching } from 'redux-batched-actions'
7 |
8 | // Local Libraries
9 | import RangeDate from './range_date'
10 | import DateRange from './date_range'
11 | import Layout from './layout'
12 | import reducers from './reducers'
13 |
14 | // Actions
15 | import { createCells } from './actions/cells'
16 | import { replaceResources, replaceEvents } from './actions/events'
17 | import { setRange } from './actions/range'
18 |
19 | // Promise Middleware
20 | const promiseMiddleware = store => next => action => {
21 | const { callback, nextAction, type, ...rest } = action
22 | if (!nextAction && !callback) {
23 | return next(action)
24 | }
25 |
26 | var p = new Promise((resolve) => {
27 | next({ ...rest, type: type })
28 | resolve(store.getState())
29 | })
30 |
31 | p.then(state => {
32 | if (nextAction) {
33 | next({ ...rest, type: action.nextAction, range: state.range.toJS().range, resources: state.event.toJS().resources })
34 | } else {
35 | let id = action.event.id;
36 | let index = state.event.get('events').findIndex(i => i.get('id') === id);
37 |
38 | callback(state.event.getIn(['events', index]).toJS());
39 | }
40 | }).
41 | catch(ex => {
42 | next({ ...rest, ex, type: type + '_FAILURE' })
43 | throw new Error(ex)
44 | })
45 | }
46 |
47 | // Create the store
48 | const createStoreWithMiddleware = applyMiddleware(promiseMiddleware)(createStore)
49 | const store = createStoreWithMiddleware(enableBatching(reducers))
50 |
51 | export default class Scheduler extends Component {
52 | static propTypes = {
53 | resources: PropTypes.array.isRequired,
54 | events: PropTypes.array.isRequired,
55 | from: PropTypes.oneOfType([
56 | PropTypes.string,
57 | PropTypes.object
58 | ]),
59 | to: PropTypes.oneOfType([
60 | PropTypes.string,
61 | PropTypes.object
62 | ]),
63 | rowHeight: PropTypes.number,
64 | width: PropTypes.number.isRequired,
65 | onEventChanged: PropTypes.func,
66 | onEventResized: PropTypes.func,
67 | onEventClicked: PropTypes.func,
68 | onCellClicked: PropTypes.func,
69 | onRangeChanged: PropTypes.func
70 | }
71 |
72 | static defaultProps = {
73 | from: new RangeDate(),
74 | to: new RangeDate().advance('weeks', 4),
75 | rowHeight: 30,
76 | selectorStyles: {},
77 | chartStyles: {}
78 | }
79 |
80 | componentWillMount() {
81 | const { resources, from, to } = this.props,
82 | range = new DateRange(from, to)
83 |
84 | this.initializeStore(this.props)
85 | store.dispatch(createCells(resources, range))
86 | }
87 |
88 | componentWillReceiveProps(nextProps) {
89 | this.initializeStore(nextProps)
90 | }
91 |
92 | initializeStore(props) {
93 | const { resources, events, from, to } = props,
94 | range = new DateRange(from, to)
95 |
96 | store.dispatch(batchActions([
97 | setRange(range),
98 | replaceResources(resources),
99 | replaceEvents(events)
100 | ]))
101 | }
102 |
103 | fireEventChanged = (props) => {
104 | const { onEventChanged } = this.props,
105 | { id, title, startDate, duration, resource, disabled } = props
106 | if (onEventChanged) onEventChanged({ id, title, startDate, duration, resource, disabled })
107 | }
108 |
109 | fireEventResized = (props) => {
110 | const { onEventResized } = this.props,
111 | { id, title, startDate, duration, resource, disabled } = props
112 | if (onEventResized) onEventResized({ id, title, startDate, duration, resource, disabled })
113 | }
114 |
115 | fireEventClicked = (props) => {
116 | const { onEventClicked } = this.props,
117 | { id, title, startDate, duration, resource, disabled } = props
118 | if (onEventClicked) onEventClicked({ id, title, startDate, duration, resource, disabled })
119 | }
120 |
121 | fireCellClicked = (resource, date) => {
122 | const { onCellClicked } = this.props
123 | if (onCellClicked) onCellClicked(resource, date)
124 | }
125 |
126 | fireRangeChanged = (range) => {
127 | const { onRangeChanged } = this.props
128 | if (onRangeChanged) onRangeChanged(range)
129 | }
130 |
131 | render() {
132 | return (
133 |
134 |
142 |
143 | )
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | export const selectors = {
2 | textAlign: 'center',
3 | margin: '25px 0'
4 | }
5 |
6 | export const leftButton = {
7 | position: 'relative',
8 | marginRight: '10px',
9 | display: 'inline-block',
10 | width: '2em',
11 | height: '2em',
12 | border: '0.25em solid darkgrey',
13 | borderRadius: '50%',
14 | verticalAlign: 'middle'
15 | }
16 |
17 | export const leftButtonAfter = {
18 | position: 'absolute',
19 | display: 'inline-block',
20 | top: '0.4em',
21 | left: '0.5em',
22 | width: '0.7em',
23 | height: '0.7em',
24 | borderTop: '0.25em solid darkgrey',
25 | borderRight: '0.25em solid darkgrey',
26 | transform: 'rotate(-135deg)'
27 | }
28 |
29 | export const rightButton = {
30 | position: 'relative',
31 | marginLeft: '10px',
32 | display: 'inline-block',
33 | width: '2em',
34 | height: '2em',
35 | border: '0.25em solid darkgrey',
36 | borderRadius: '50%',
37 | verticalAlign: 'middle'
38 | }
39 |
40 | export const rightButtonAfter = {
41 | position: 'absolute',
42 | display: 'inline-block',
43 | top: '0.4em',
44 | right: '0.5em',
45 | width: '0.7em',
46 | height: '0.7em',
47 | borderTop: '0.25em solid darkgrey',
48 | borderLeft: '0.25em solid darkgrey',
49 | transform: 'rotate(135deg)'
50 | }
51 |
52 | export const chartHeader = {
53 | border: 'solid 1px darkgrey',
54 | margin: '0 -1px -1px 0',
55 | padding: '0 4px',
56 | flexGrow: 0
57 | }
58 |
59 | export const headerWrapper = {
60 | borderRight: 'solid 1px darkgrey'
61 | }
62 |
63 | export const resourceSideBar = {
64 | border: 'solid 1px darkgrey',
65 | margin: '0 -1px -1px 0',
66 | textAlign: 'center',
67 | zIndex: 99,
68 | backgroundColor: '#FFF'
69 | }
70 |
71 | export const cell = {
72 | width: '100%',
73 | height: '100%',
74 | backgroundColor: 'transparent',
75 | display: 'flex',
76 | alignItems: 'center'
77 | }
78 |
79 | export const chart = {
80 | display: 'flex',
81 | flexWrap: 'wrap',
82 | borderBottom: 'solid 1px darkgrey',
83 | borderRight: 'solid 1px darkgrey'
84 | }
85 |
86 | export const cellWrapper = {
87 | margin: '0 -1px -1px 0',
88 | border: 'solid 1px darkgrey'
89 | }
90 |
91 | export const resourceWrapper = {
92 | display: 'flex',
93 | flexDirection: 'column'
94 | }
95 |
96 | export const eventStyles = {
97 | position: 'relative',
98 | top: 0,
99 | left: '4px',
100 | borderRadius: '3px',
101 | padding: '2px 5px'
102 | }
103 |
104 | export const partialEventStyles = {
105 | position: 'absolute',
106 | top: 0,
107 | right: '4px',
108 | borderRadius: '3px',
109 | padding: '2px 5px',
110 | textAlign: 'right'
111 | }
112 |
113 | export const resizerStyles = {
114 | top: 0,
115 | right: 0,
116 | width: '5px',
117 | display: 'inline-block',
118 | position: 'absolute'
119 | }
120 |
121 | export const boxStyles = {
122 | position: 'relative',
123 | borderRadius: '3px'
124 | }
125 |
126 | export const eventHandleStyles = {
127 | position: 'absolute',
128 | top: 0,
129 | left: 0,
130 | height: '100%',
131 | width: 30,
132 | display: 'inline-block'
133 | }
134 |
--------------------------------------------------------------------------------
/tests/date_range.jsx:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import DateRange from '../src/date_range'
3 | import RangeDate from '../src/range_date'
4 |
5 | describe('DateRange', () => {
6 | describe('initialize', () => {
7 | it('should accept an instance of range date in the constructor', () => {
8 | let from = new RangeDate(2015, 8, 1),
9 | to = new RangeDate(2015, 9, 1),
10 | range = new DateRange(from, to)
11 |
12 | assert(range.from instanceof RangeDate)
13 | assert(range.to instanceof RangeDate)
14 | })
15 | })
16 |
17 | describe('#toString', () => {
18 | it('should convert the range to a formatted string', () => {
19 | let range = new DateRange(new Date(2015, 8, 1), new Date(2015, 9, 1))
20 |
21 | assert.equal("September 1, 2015 - October 1, 2015", range.toString())
22 | })
23 | })
24 |
25 | describe('#advance', () => {
26 | it('should advance the date range', () => {
27 | let range = new DateRange("2015-09-01", "2015-09-15")
28 |
29 | assert.equal("September 16, 2015 - September 30, 2015", range.advance().toString())
30 | })
31 |
32 | it('should advance the date range for the number of days in the range', () => {
33 | let range = new DateRange("2015-09-01", "2015-09-04")
34 |
35 | assert.equal("September 5, 2015 - September 8, 2015", range.advance().toString())
36 | })
37 | })
38 |
39 | describe('#daysInRange', () => {
40 | it('should return the number of days in the range', () => {
41 | let range = new DateRange("2016-01-01", "2016-01-03")
42 |
43 | assert.equal(3, range.daysInRange())
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/tests/range_date.jsx:
--------------------------------------------------------------------------------
1 | import RangeDate from '../src/range_date'
2 | import assert from 'assert'
3 | import timekeeper from 'timekeeper'
4 | import moment from 'moment-timezone'
5 |
6 | describe('RangeDate', () => {
7 | describe('initialize', () => {
8 | it('should accept an instance of itself', () => {
9 | let date = new RangeDate(new Date(2015, 8, 1)),
10 | date2 = new RangeDate(date)
11 |
12 | assert(+(moment(new Date(2015, 8, 1))) === +date2.value())
13 | })
14 |
15 | it('should set the date to now if undefined is passed in', () => {
16 | let now = new Date()
17 | timekeeper.freeze(now)
18 | let date = new RangeDate()
19 |
20 | assert(+now === +date.value())
21 | timekeeper.reset()
22 | })
23 |
24 | it('should set the date to now if null is passed in', () => {
25 | let now = new Date()
26 | timekeeper.freeze(now)
27 | let date = new RangeDate(null)
28 |
29 | assert(+now === +date.value())
30 | timekeeper.reset()
31 | })
32 | })
33 |
34 | describe('#toString', () => {
35 | let date = new RangeDate(new Date(2015, 8, 1))
36 |
37 | it('should convert the range date to a formatted string', () => {
38 | assert.equal("September 1, 2015", date.toString())
39 | })
40 | })
41 |
42 | describe('#advance', () => {
43 | it('should properly advance the date by days', () => {
44 | let date = new RangeDate(new Date(2015, 8, 1))
45 | assert.equal(+(new Date(2015, 8, 15)), +date.advance('days', 14).value())
46 | })
47 |
48 | it('should properly advance the date by weeks', () => {
49 | let date = new RangeDate(new Date(2015, 8, 1))
50 | assert.equal(+(new Date(2015, 8, 15)), +date.advance('weeks', 2).value())
51 | })
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/tests/scheduler.jsx:
--------------------------------------------------------------------------------
1 | import Test from 'legit-tests'
2 | import { expect } from 'chai'
3 |
4 | import Scheduler from '../src/scheduler'
5 | import RangeDate from '../src/range_date'
6 |
7 | describe('Scheduler component', () => {
8 | describe('initialize', () => {
9 | it('should create a default range of the next two weeks if no range is defined', () => {
10 | let expectedFrom = new RangeDate(new Date()),
11 | expectedTo = expectedFrom.advance('weeks', 4)
12 |
13 | Test().
14 | test(({ instance }) => {
15 | expect(instance.props.from.toString()).to.eq(expectedFrom.toString())
16 | expect(instance.props.to.toString()).to.eq(expectedTo.toString())
17 | })
18 | })
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | /* globals global */
2 |
3 | require('babel-core/register')();
4 |
5 | function propagateToGlobal (window) {
6 | for (let key in window) {
7 | if (!window.hasOwnProperty(key)) continue
8 | if (key in global) continue
9 |
10 | global[key] = window[key]
11 | }
12 | }
13 |
14 | var jsdom = require('jsdom');
15 |
16 | var doc = jsdom.jsdom('');
17 | var win = doc.defaultView;
18 |
19 | global.document = doc;
20 | global.window = win;
21 |
22 | propagateToGlobal(win);
23 |
--------------------------------------------------------------------------------