16 |
17 | {title}
18 |
19 |
20 | {tooltip ? (
21 | // eslint-disable-next-line react/no-danger
22 |
') }} />
23 | ) : (
24 |
25 |
{title}
26 |
27 | 起始 {getDayMonth(start)}
28 |
29 |
30 | 终止 {getDayMonth(end)}
31 |
32 |
33 | )}
34 |
35 |
36 | )
37 |
38 | Basic.propTypes = {
39 | title: PropTypes.string.isRequired,
40 | start: PropTypes.instanceOf(Date).isRequired,
41 | end: PropTypes.instanceOf(Date).isRequired,
42 | style: PropTypes.shape({}),
43 | classes: PropTypes.arrayOf(PropTypes.string.isRequired),
44 | dataSet: PropTypes.shape({}),
45 | tooltip: PropTypes.string,
46 | }
47 |
48 | export default Basic
49 |
--------------------------------------------------------------------------------
/src/components/Timeline/Timebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { globalContext } from '../../index'
4 |
5 | const Cell = ({ title, start, end, style }) => {
6 | const { time } = useContext(globalContext)
7 | return (
8 |
13 | {title}
14 |
15 | )
16 | }
17 |
18 | const Row = ({ cells, style }) => {
19 | const { time } = useContext(globalContext)
20 | let props = {}
21 | if (time.timelineWidth / cells.length < 22) {
22 | props = {
23 | title: ''
24 | }
25 | }
26 | return (
27 |
28 | {cells.map(cell => (
29 | |
30 | ))}
31 |
32 | )
33 | }
34 |
35 | const Timebar = ({ rows }) => {
36 | const { time } = useContext(globalContext)
37 | return (
38 |
39 | {rows.map(({ id, title, cells, style }) => (
40 |
41 | ))}
42 |
43 | )
44 | }
45 |
46 | Timebar.propTypes = {
47 | rows: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
48 | }
49 |
50 | export default Timebar
51 |
--------------------------------------------------------------------------------
/src/hooks/useEvent.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export default function useEvent(eventName, handler, element = window) {
4 | // Create a ref that stores handler
5 | const savedHandler = useRef();
6 |
7 | // Update ref.current value if handler changes.
8 | // This allows our effect below to always get latest handler ...
9 | // ... without us needing to pass it in effect deps array ...
10 | // ... and potentially cause effect to re-run every render.
11 | useEffect(() => {
12 | savedHandler.current = handler;
13 | }, [handler]);
14 |
15 | useEffect(
16 | () => {
17 | // Make sure element supports addEventListener
18 | // On
19 | const isSupported = element && element.addEventListener;
20 | if (!isSupported) return;
21 |
22 | // Create event listener that calls handler function stored in ref
23 | const eventListener = event => savedHandler.current(event);
24 |
25 | // Add event listener
26 | element.addEventListener(eventName, eventListener);
27 |
28 | // Remove event listener on cleanup
29 | return () => {
30 | element.removeEventListener(eventName, eventListener);
31 | };
32 | },
33 | [eventName, element] // Re-run if eventName or element changes
34 | );
35 | };
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useCallback } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Layout from './components/Layout'
4 | import createTime from './utils/time'
5 | import useEvent from './hooks/useEvent'
6 | export const globalContext = React.createContext();
7 |
8 | function Gantt({
9 | start, end,
10 | zoom = 1,
11 | projects = [],
12 | now = new Date(),
13 | sidebarWidth = 120,
14 | minWidth = 400,
15 | scrollToNow = true,
16 | enableSticky = true,
17 | clickTask,
18 | }) {
19 | const [time, setTime] = useState(createTime(start, end, zoom, 0, minWidth))
20 | const [_projects, setProjects] = useState(projects)
21 |
22 | const toggleProjectOpen = project => {
23 | setProjects(prevState => {
24 | for (const _project of prevState) {
25 | if (_project.id === project.id) {
26 | _project.isOpen = !project.isOpen
27 | }
28 | }
29 | return [...prevState]
30 | })
31 | }
32 |
33 | const gantt = useRef(null)
34 |
35 | const buildMonthCells = () => {
36 | const v = []
37 | function getMonthAdd(y, m) {
38 | while (m >= 12) {
39 | m -= 12
40 | y += 1
41 | }
42 | return new Date(`${y}-${m + 1}-1 0:0:0`)
43 | }
44 | const month_count = end.getMonth() - start.getMonth() + (12 * (end.getFullYear() - start.getFullYear())) + 1
45 | for (let i = 0; i < month_count; i += 1) {
46 |
47 | const start_date = getMonthAdd(start.getFullYear(), start.getMonth() + i)
48 | const end_date = getMonthAdd(start.getFullYear(), start.getMonth() + i + 1)
49 | v.push({
50 | id: `m${i}`,
51 | title: `${(start.getMonth() + i) % 12 + 1}月`,
52 | start: start_date,
53 | end: end_date,
54 | })
55 | }
56 | return v
57 | }
58 | const buildDayCells = () => {
59 | const v = []
60 | const start_floor = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0)
61 | const day_count = Math.floor((end - start) / (1000 * 60 * 60 * 24)) + 1
62 | for (let i = 0; i < day_count; i += 1) {
63 | const start_date = new Date(start_floor.getTime() + i * 1000 * 60 * 60 * 24)
64 | const end_date = new Date(start_floor.getTime() + (i + 1) * 1000 * 60 * 60 * 24)
65 | v.push({
66 | id: `d${i}`,
67 | title: `${start_date.getDate()}`,
68 | start: start_date,
69 | end: end_date,
70 | style: {
71 | backgroundColor: start_date.getDay() === 0 ? '#1890ff' : '',
72 | color: start_date.getDay() === 0 ? '#fff' : ''
73 | }
74 | })
75 | }
76 | return v
77 | }
78 | const buildWeekCells = () => {
79 | const v = []
80 | const start_floor = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 0, 0, 0)
81 | const week_count = Math.floor((end - start) / (1000 * 60 * 60 * 24 * 7)) + 2
82 | for (let i = 0; i < week_count; i += 1) {
83 | const start_date = new Date(start_floor.getTime() + (i * 7 - start_floor.getDay()) * 1000 * 60 * 60 * 24)
84 | const end_date = new Date(start_floor.getTime() + ((i + 1) * 7 - start_floor.getDay()) * 1000 * 60 * 60 * 24)
85 | v.push({
86 | id: `w${i}`,
87 | title: ``,
88 | start: start_date,
89 | end: end_date
90 | })
91 | }
92 | return v
93 | }
94 |
95 | const timebar = [
96 | {
97 | id: 'weeks',
98 | title: '',
99 | cells: buildWeekCells(),
100 | useAsGrid: true,
101 | },
102 | {
103 | id: 'months',
104 | title: '月份',
105 | cells: buildMonthCells(),
106 |
107 | },
108 | {
109 | id: 'days',
110 | title: '日期',
111 | cells: buildDayCells(),
112 | }
113 | ]
114 |
115 | useEffect(() => {
116 | if (gantt.current) {
117 | setTime(createTime({
118 | start, end, zoom,
119 | viewportWidth: gantt.current.offsetWidth - sidebarWidth,
120 | minWidth: minWidth - sidebarWidth
121 | }))
122 | }
123 | }, [zoom, start, end])
124 |
125 | const handleResize = useCallback(() => {
126 | if (gantt.current) {
127 | setTime(createTime({
128 | start, end, zoom,
129 | viewportWidth: gantt.current.offsetWidth - sidebarWidth,
130 | minWidth: minWidth - sidebarWidth
131 | }))
132 | }
133 | })
134 |
135 | useEvent('resize', handleResize)
136 |
137 | return (
138 |
139 |
145 |
152 |
153 |
154 | )
155 | }
156 |
157 | Gantt.propTypes = {
158 | start: PropTypes.instanceOf(Date).isRequired,
159 | end: PropTypes.instanceOf(Date).isRequired,
160 | now: PropTypes.instanceOf(Date),
161 | zoom: PropTypes.number.isRequired,
162 | projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
163 | minWidth: PropTypes.number,
164 | sideWidth: PropTypes.number,
165 | clickTask: PropTypes.func,
166 | enableSticky: PropTypes.bool,
167 | scrollToNow: PropTypes.bool,
168 | }
169 |
170 | export default Gantt
171 |
--------------------------------------------------------------------------------
/src/scss/_utils.scss:
--------------------------------------------------------------------------------
1 | .rt-visually-hidden {
2 | position: absolute;
3 | overflow: hidden;
4 | clip: rect(0 0 0 0);
5 | height: 1px; width: 1px;
6 | margin: -1px; padding: 0; border: 0;
7 | }
--------------------------------------------------------------------------------
/src/scss/components/_controls.scss:
--------------------------------------------------------------------------------
1 | .rt-controls {
2 | display: inline-block;
3 | padding: 8px;
4 | margin: 0 0 $react-timelines-spacing 0;
5 | background-color: #fff;
6 | }
7 |
8 | .rt-controls__button {
9 | display: inline-block;
10 | width: $react-timelines-button-size;
11 | height: $react-timelines-button-size;
12 | overflow: hidden;
13 | background-color: $react-timelines-button-background-color;
14 | color: transparent;
15 | white-space: nowrap;
16 | padding: $react-timelines-spacing;
17 | outline: none;
18 |
19 | &:last-child {
20 | margin-right: 0;
21 | }
22 |
23 | &:hover {
24 | background-color: $react-timelines-button-background-color-hover;
25 | }
26 |
27 | &:focus,
28 | &:active {
29 | background-color: $react-timelines-button-background-color-hover;
30 | }
31 | }
32 |
33 | .rt-controls__button[disabled] {
34 | opacity: 0.5;
35 | }
36 |
37 | .rt-controls__button--toggle {
38 | @media (min-width: $react-timelines-auto-open-breakpoint) {
39 | display: none;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/scss/components/_grid.scss:
--------------------------------------------------------------------------------
1 | .rt-grid,
2 | .rt-grid__cell {
3 | position: absolute;
4 | top: 0;
5 | bottom: 0;
6 | }
7 |
8 | .rt-grid {
9 | left: 0;
10 | right: 0;
11 | }
12 |
13 | .rt-grid__cell {
14 | background: #fff;
15 | border-left: 1px solid $react-timelines-keyline-color;
16 | }
17 |
--------------------------------------------------------------------------------
/src/scss/components/_layout.scss:
--------------------------------------------------------------------------------
1 |
2 | .rt-layout__side {
3 | position: relative;
4 | z-index: 2;
5 | display: inline-block;
6 | vertical-align: top;
7 | }
8 |
9 | .rt-layout__main {
10 | display: inline-block;
11 | vertical-align: top;
12 | }
13 |
14 | .rt-layout__timeline {
15 | overflow-x: auto;
16 | }
17 |
--------------------------------------------------------------------------------
/src/scss/components/_marker.scss:
--------------------------------------------------------------------------------
1 | .rt-marker {
2 | position: absolute;
3 | z-index: 2;
4 | top: $react-timelines-header-row-height;
5 | bottom: 0;
6 | margin-left: -($react-timelines-marker-line-width / 2);
7 | border-left: $react-timelines-marker-line-width solid;
8 | opacity: 0;
9 | pointer-events: none;
10 | }
11 |
12 | .rt-marker.rt-is-visible {
13 | opacity: 1;
14 | }
15 |
16 |
17 | .rt-marker--now {
18 | color: $react-timelines-marker-now-background-color;
19 | border-color: rgba(
20 | $react-timelines-marker-now-background-color,
21 | $react-timelines-marker-line-transparency
22 | );
23 | }
24 |
25 | .rt-marker--pointer {
26 | color: $react-timelines-marker-pointer-background-color;
27 | border-color: rgba(
28 | $react-timelines-marker-pointer-background-color,
29 | $react-timelines-marker-line-transparency
30 | );
31 | }
32 |
33 | .rt-marker__label {
34 | position: absolute;
35 | bottom: 100%;
36 | left: 50%;
37 | display: table;
38 | min-width: 70px;
39 | height: $react-timelines-header-row-height;
40 | padding: 0 10px;
41 | line-height: 1.1;
42 | text-align: center;
43 | background: currentColor;
44 | transform: translateX(-50%);
45 | font-size: 14px;
46 |
47 | &::before {
48 | $size: 6px;
49 |
50 | position: absolute;
51 | top: 100%;
52 | left: 50%;
53 | margin-left: -$size;
54 | transform: translateX(-($react-timelines-marker-line-width / 2));
55 | border-left: $size solid transparent;
56 | border-right: $size solid transparent;
57 | border-top: $size solid currentColor;
58 | content: ' ';
59 | }
60 | }
61 |
62 | .rt-marker__content {
63 | display: table-cell;
64 | vertical-align: middle;
65 | white-space: nowrap;
66 | color: white;
67 | }
68 |
--------------------------------------------------------------------------------
/src/scss/components/_project-key.scss:
--------------------------------------------------------------------------------
1 | .rt-project-key__entry {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 |
6 | height: $react-timelines-project-height + $react-timelines-border-width;
7 | line-height: $react-timelines-project-height;
8 | text-align: left;
9 | border-bottom: $react-timelines-border-width solid $react-timelines-sidebar-separator-color;
10 | }
11 |
12 | .rt-icon {
13 | color: $react-feather-color;
14 | }
15 |
16 | .rt-project-keys > .rt-project-key >
17 | .rt-project-key__entry {
18 | padding-left: $react-timelines-sidebar-key-indent-width;
19 | }
20 |
21 | .rt-project-keys > .rt-project-key >
22 | .rt-project-keys > .rt-project-key >
23 | .rt-project-key__entry {
24 | padding-left: $react-timelines-sidebar-key-indent-width * 2;
25 | }
26 |
27 | .rt-project-keys > .rt-project-key >
28 | .rt-project-keys > .rt-project-key >
29 | .rt-project-keys > .rt-project-key >
30 | .rt-project-key__entry {
31 | padding-left: $react-timelines-sidebar-key-indent-width * 3;
32 | }
33 |
34 | .rt-project-keys > .rt-project-key >
35 | .rt-project-keys > .rt-project-key >
36 | .rt-project-keys > .rt-project-key >
37 | .rt-project-keys > .rt-project-key >
38 | .rt-project-key__entry {
39 | padding-left: $react-timelines-sidebar-key-indent-width * 4;
40 | }
41 |
42 | .rt-project-keys > .rt-project-key >
43 | .rt-project-keys > .rt-project-key >
44 | .rt-project-keys > .rt-project-key >
45 | .rt-project-keys > .rt-project-key >
46 | .rt-project-keys > .rt-project-key >
47 | .rt-project-key__entry {
48 | padding-left: $react-timelines-sidebar-key-indent-width * 5;
49 | }
50 |
51 | .rt-project-key__title {
52 | flex: 1;
53 | white-space: nowrap;
54 | text-overflow: ellipsis;
55 | overflow: hidden;
56 | }
57 |
58 | .rt-project-key__side-button {
59 | height: $react-timelines-project-height;
60 | width: $react-timelines-project-height;
61 | color: transparent;
62 | background: transparent;
63 |
64 | &:hover,
65 | &:focus {
66 | background: $react-timelines-sidebar-key-icon-hover-color;
67 | color: transparent;
68 | }
69 |
70 | &::before {
71 | position: absolute;
72 | width: $react-timelines-sidebar-key-icon-size;
73 | height: $react-timelines-sidebar-key-icon-size;
74 | margin-top: -$react-timelines-sidebar-key-icon-size / 2;
75 | margin-left: -$react-timelines-sidebar-key-icon-size / 2;
76 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDQ4Mi4xMzYgNDgyLjEzNSI+PHBhdGggZmlsbD0iIzc2NzY3NiIgZD0iTTQ1NS40ODIgMTk4LjE4NEwzMjYuODMgMzI2LjgzMmMtMzUuNTM2IDM1LjU0LTkzLjExIDM1LjU0LTEyOC42NDcgMGwtNDIuODgtNDIuODg2IDQyLjg4LTQyLjg3NiA0Mi44ODQgNDIuODc2YzExLjg0NSAxMS44MjIgMzEuMDY0IDExLjg0NiA0Mi44ODYgMGwxMjguNjQ0LTEyOC42NDNjMTEuODE2LTExLjgzIDExLjgxNi0zMS4wNjYgMC00Mi45bC00Mi44OC00Mi44OGMtMTEuODIzLTExLjgxNS0zMS4wNjUtMTEuODE1LTQyLjg4OCAwbC00NS45MyA0NS45MzVjLTIxLjI5LTEyLjUzLTQ1LjQ5LTE3LjkwNS02OS40NS0xNi4yOWw3Mi41LTcyLjUyN2MzNS41MzYtMzUuNTIgOTMuMTM3LTM1LjUyIDEyOC42NDUgMGw0Mi44ODYgNDIuODg0YzM1LjUzNiAzNS41MjMgMzUuNTM2IDkzLjE0IDAgMTI4LjY2MnpNMjAxLjIwNiAzNjYuNjk4bC00NS45MDMgNDUuOWMtMTEuODQ1IDExLjg0Ni0zMS4wNjQgMTEuODE3LTQyLjg4IDBsLTQyLjg4NS00Mi44OGMtMTEuODQ1LTExLjgyMi0xMS44NDUtMzEuMDQyIDAtNDIuODg3bDEyOC42NDYtMTI4LjY0NWMxMS44Mi0xMS44MTQgMzEuMDctMTEuODE0IDQyLjg4NCAwbDQyLjg4NiA0Mi44ODYgNDIuODc2LTQyLjg4Ni00Mi44NzYtNDIuODhjLTM1LjU0LTM1LjUyMi05My4xMTMtMzUuNTIyLTEyOC42NSAwbC0xMjguNjUgMTI4LjY0Yy0zNS41MzcgMzUuNTQ2LTM1LjUzNyA5My4xNDcgMCAxMjguNjUzTDY5LjU0IDQ1NS40OGMzNS41MSAzNS41NCA5My4xMSAzNS41NCAxMjguNjQ2IDBsNzIuNDk2LTcyLjVjLTIzLjk1NiAxLjU5OC00OC4wOTItMy43ODMtNjkuNDc0LTE2LjI4MnoiLz48L3N2Zz4=');
77 | content: ' ';
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/scss/components/_project-keys.scss:
--------------------------------------------------------------------------------
1 | .rt-project-keys {
2 | margin: 0;
3 | padding-left: 0;
4 | list-style: none;
5 | }
6 |
--------------------------------------------------------------------------------
/src/scss/components/_project.scss:
--------------------------------------------------------------------------------
1 | .rt-project {}
2 |
3 | .rt-project__tasks {
4 | position: relative;
5 | height: $react-timelines-project-height + $react-timelines-border-width;
6 | border-bottom: $react-timelines-border-width solid $react-timelines-keyline-color;
7 | }
8 |
9 | .rt-project__task {
10 | position: absolute;
11 | height: $react-timelines-project-height - 2 * $react-timelines-task-spacing;
12 | top: $react-timelines-task-spacing;
13 | }
14 |
--------------------------------------------------------------------------------
/src/scss/components/_projects.scss:
--------------------------------------------------------------------------------
1 | .rt-projects {}
2 |
--------------------------------------------------------------------------------
/src/scss/components/_sidebar.scss:
--------------------------------------------------------------------------------
1 | .rt-sidebar {
2 | background-color: $react-timelines-sidebar-background-color;
3 | border-right: 1px solid $react-timelines-sidebar-separator-color
4 | }
5 |
6 | .rt-sidebar.rt-sidebar-shadow {
7 | box-shadow: 10px 0 10px -5px rgba(12, 12, 12, 0.1);
8 | }
9 |
10 | .rt-sidebar__header {
11 | background-color: $react-timelines-timebar-cell-background-color;
12 | font-weight: 500;
13 | }
14 |
15 | .rt-sidebar__header.rt-is-sticky {
16 | position: fixed;
17 | top: 0;
18 | z-index: 2;
19 | direction: rtl;
20 | margin-left: ($react-timelines-sidebar-width - $react-timelines-sidebar-closed-offset);
21 | border-right: 1px solid $react-timelines-sidebar-separator-color;
22 | @media (min-width: $react-timelines-auto-open-breakpoint) {
23 | margin-left: 0;
24 | direction: ltr;
25 | }
26 | }
27 |
28 | .rt-sidebar__header.rt-is-sticky {
29 | margin-left: 0;
30 | direction: ltr;
31 | }
32 |
33 | .rt-sidebar__body {}
34 |
--------------------------------------------------------------------------------
/src/scss/components/_task.scss:
--------------------------------------------------------------------------------
1 | .rt-task {
2 | $height: $react-timelines-project-height - 2 * $react-timelines-task-spacing;
3 |
4 | position: relative;
5 | height: $height;
6 | line-height: $height;
7 | text-align: center;
8 | }
9 |
10 | .rt-task__content {
11 | padding: 0 10px;
12 | overflow: hidden;
13 | white-space: nowrap;
14 | text-overflow: ellipsis;
15 | }
16 |
17 | .rt-task__tooltip {
18 | position: absolute;
19 | bottom: 100%;
20 | left: 50%;
21 | z-index: 2;
22 | padding: 10px;
23 | line-height: 1.3;
24 | white-space: nowrap;
25 | text-align: left;
26 | background: $react-timelines-text-color;
27 | color: white;
28 | transform: translateX(-50%) scale(0);
29 | pointer-events: none;
30 |
31 | &::before {
32 | $size: 6px;
33 | position: absolute;
34 | top: 100%;
35 | left: 50%;
36 | border-top: $size solid $react-timelines-text-color;
37 | border-right: $size solid transparent;
38 | border-left: $size solid transparent;
39 | transform: translateX(-50%);
40 | content: ' ';
41 | }
42 | }
43 |
44 | .rt-task:hover > .rt-task__tooltip,
45 | .rt-task:focus > .rt-task__tooltip {
46 | $delay: 0.3s;
47 | transform: translateX(-50%) scale(1);
48 | transition: transform 0s $delay;
49 | }
50 |
--------------------------------------------------------------------------------
/src/scss/components/_timebar-key.scss:
--------------------------------------------------------------------------------
1 | .rt-timebar-key {
2 | height: $react-timelines-header-row-height + $react-timelines-border-width;
3 | padding-right: $react-timelines-sidebar-key-indent-width;
4 | line-height: $react-timelines-header-row-height;
5 | text-align: right;
6 | border-bottom: 1px solid $react-timelines-sidebar-separator-color;
7 |
8 | &:last-child {
9 | border-bottom-color: $react-timelines-header-separator-color;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/scss/components/_timebar.scss:
--------------------------------------------------------------------------------
1 | .rt-timebar {
2 | background-color: $react-timelines-timebar-cell-background-color;
3 | font-weight: 500;
4 | }
5 |
6 | .rt-timebar__row {
7 | position: relative;
8 | height: $react-timelines-header-row-height + $react-timelines-border-width;
9 | overflow: hidden;
10 | line-height: $react-timelines-header-row-height;
11 | border-bottom: $react-timelines-border-width solid $react-timelines-keyline-color;
12 | &:last-child {
13 | border-bottom-color: $react-timelines-header-separator-color;
14 | }
15 | }
16 |
17 | .rt-timebar__cell {
18 | position: absolute;
19 | text-align: center;
20 | background-color: $react-timelines-timebar-cell-background-color;
21 | border-left: 1px solid $react-timelines-keyline-color;
22 | text-overflow: ellipsis;
23 | overflow: hidden;
24 | white-space: nowrap;
25 | }
26 |
--------------------------------------------------------------------------------
/src/scss/components/_timeline.scss:
--------------------------------------------------------------------------------
1 | .rt-timeline {
2 | position: relative;
3 | overflow: hidden;
4 | }
5 |
6 | .rt-timeline__header {}
7 |
8 | .rt-timeline__header-scroll {
9 | overflow-x: auto;
10 |
11 | &::-webkit-scrollbar {
12 | display: none;
13 | }
14 | }
15 |
16 | .rt-timeline__header.rt-is-sticky {
17 | position: fixed;
18 | top: 0;
19 | z-index: 1;
20 | overflow: hidden;
21 | }
22 |
23 | .rt-timeline__body {
24 | position: relative;
25 | background: white;
26 | }
27 |
--------------------------------------------------------------------------------
/src/scss/style.scss:
--------------------------------------------------------------------------------
1 | // Common
2 | $react-timelines-spacing: 10px;
3 | $react-timelines-keyline-color: rgb(232, 232, 232) !default;
4 | $react-timelines-separator-dark-color: rgb(232, 232, 232) !default;
5 |
6 | $react-timelines-auto-open-breakpoint: 1000px !default;
7 | $react-timelines-font-family: sans-serif !default;
8 | $react-timelines-border-width: 1px !default;
9 | $react-timelines-text-color: #4c4c4c !default;
10 |
11 | // Header
12 | $react-timelines-header-row-height: 40px !default;
13 | $react-timelines-header-separator-color: $react-timelines-separator-dark-color !default;
14 |
15 | // Sidebar
16 | $react-timelines-sidebar-width: 240px !default;
17 | $react-timelines-sidebar-closed-offset: 40px;
18 | $react-timelines-sidebar-background-color: #fff !default;
19 | $react-timelines-sidebar-separator-color: $react-timelines-keyline-color !default;
20 | $react-timelines-sidebar-key-indent-width: 20px !default;
21 | $react-timelines-sidebar-key-icon-size: 16px !default;
22 | $react-timelines-sidebar-key-icon-hover-color: #eee !default;
23 |
24 | // Timebar
25 | $react-timelines-timebar-cell-background-color: #fafafa;
26 |
27 | // Project / Tasks
28 | $react-timelines-project-height: 60px !default;
29 | $react-timelines-task-spacing: $react-timelines-spacing !default;
30 |
31 | // Markers
32 | $react-timelines-marker-line-width: 2px !default;
33 | $react-timelines-marker-now-background-color: #bbb !default;
34 | $react-timelines-marker-pointer-background-color: #1890ff !default;
35 | $react-timelines-marker-line-transparency: 0.5 !default;
36 |
37 | // Controls
38 | $react-timelines-button-background-color: #fff !default;
39 | $react-timelines-button-background-color-hover: #f0f0f0 !default;
40 | $react-timelines-button-size: 44px !default;
41 |
42 | $react-feather-color: #1890ff !default;
43 | .rt {
44 | position: relative;
45 | z-index: 1;
46 | overflow: hidden;
47 | font-family: $react-timelines-font-family;
48 | color: $react-timelines-text-color;
49 | font-size: 14px;
50 |
51 | * {
52 | box-sizing: border-box;
53 | }
54 | }
55 |
56 | @import 'utils';
57 |
58 | @import 'components/controls';
59 | @import 'components/task';
60 | @import 'components/grid';
61 | @import 'components/layout';
62 | @import 'components/marker';
63 | @import 'components/sidebar';
64 | @import 'components/timebar';
65 | @import 'components/timebar-key';
66 | @import 'components/timeline';
67 | @import 'components/project';
68 | @import 'components/projects';
69 | @import 'components/project-key';
70 | @import 'components/project-keys';
71 |
--------------------------------------------------------------------------------
/src/utils/__tests__/classes.js:
--------------------------------------------------------------------------------
1 | import classes from '../classes'
2 |
3 | describe('classes', () => {
4 | it('returns the base class', () => {
5 | expect(classes('foo')).toBe('foo')
6 | })
7 |
8 | it('returns the base class plus additional class passed as string', () => {
9 | expect(classes('bar', 'hello')).toBe('bar hello')
10 | })
11 |
12 | it('returns the base class plus additional class passed as array', () => {
13 | expect(classes('bar', ['hello'])).toBe('bar hello')
14 | expect(classes('foo', ['hello', 'world'])).toBe('foo hello world')
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/utils/__tests__/formatDate.js:
--------------------------------------------------------------------------------
1 | import { getMonth, getDayMonth } from '../formatDate'
2 |
3 | describe('formatDate', () => {
4 | describe('getMonth', () => {
5 | it('returns the current month name for a given date', () => {
6 | expect(getMonth(new Date('2017-01-01'))).toEqual('Jan')
7 | expect(getMonth(new Date('2017-02-01'))).toEqual('Feb')
8 | expect(getMonth(new Date('2017-11-01'))).toEqual('Nov')
9 | })
10 | })
11 |
12 | describe('getDayMonth', () => {
13 | it('returns the current day and month', () => {
14 | expect(getDayMonth(new Date('2017-02-01'))).toEqual('1 Feb')
15 | expect(getDayMonth(new Date('2017-05-20'))).toEqual('20 May')
16 | expect(getDayMonth(new Date('2017-12-20'))).toEqual('20 Dec')
17 | })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getGrid.js:
--------------------------------------------------------------------------------
1 | import getGrid from '../getGrid'
2 |
3 | describe('getGrid', () => {
4 | it('returns the cells from the first timebar row that has "useAsGrid" set to true', () => {
5 | const timebar = [
6 | {
7 | cells: [{ id: 'row-1-cell-1' }],
8 | },
9 | {
10 | useAsGrid: true,
11 | cells: [{ id: 'row-2-cell-1' }],
12 | },
13 | {
14 | useAsGrid: true,
15 | cells: [{ id: 'row-3-cell-1' }],
16 | },
17 | ]
18 | const actual = getGrid(timebar)
19 | const expected = [{ id: 'row-2-cell-1' }]
20 | expect(actual).toEqual(expected)
21 | })
22 |
23 | it('returns "undefined" if none of the rows have "useAsGrid" set to true', () => {
24 | const timebar = [{ cells: [] }]
25 | const actual = getGrid(timebar)
26 | expect(actual).toEqual(undefined)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getMouseX.js:
--------------------------------------------------------------------------------
1 | import getMouseX from '../getMouseX'
2 |
3 | describe('getMouseX', () => {
4 | it('gets mouse x position for a given event', () => {
5 | const event = {
6 | clientX: 200,
7 | currentTarget: {
8 | getBoundingClientRect: () => ({ left: 10 }),
9 | },
10 | }
11 | expect(getMouseX(event)).toBe(190)
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/src/utils/__tests__/getNumericPropertyValue.js:
--------------------------------------------------------------------------------
1 | import getNumericPropertyValue from '../getNumericPropertyValue'
2 | import computedStyle from '../computedStyle'
3 |
4 | jest.mock('../computedStyle')
5 |
6 | describe('getNumericPropertyValue', () => {
7 | it('returns the numeric portion within a property value of a DOM node', () => {
8 | computedStyle.mockImplementation(node => ({
9 | getPropertyValue(prop) {
10 | return node.style[prop]
11 | },
12 | }))
13 | const node = {
14 | style: {
15 | height: '100px',
16 | },
17 | }
18 | const actual = getNumericPropertyValue(node, 'height')
19 | expect(actual).toBe(100)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/utils/__tests__/time.js:
--------------------------------------------------------------------------------
1 | import createTime from '../time'
2 |
3 | describe('createTime', () => {
4 | describe('timelineWidth', () => {
5 | it('calculates timelineWidth from start, end and scale', () => {
6 | const { timelineWidth } = createTime({
7 | start: new Date('2017-01-01T00:00:00.000Z'),
8 | end: new Date('2018-01-01T00:00:00.000Z'),
9 | zoom: 10, // 10px === 1 day
10 | })
11 | expect(timelineWidth).toBe(3650)
12 | })
13 |
14 | it('scale relates to pixel width of one day', () => {
15 | const newYear = new Date('2017-01-01T00:00:00.000Z')
16 | const newYearMidday = new Date('2017-01-01T12:00:00.000Z')
17 | const { timelineWidth } = createTime({
18 | start: newYear,
19 | end: newYearMidday,
20 | zoom: 100,
21 | })
22 | expect(timelineWidth).toBe(50)
23 | })
24 |
25 | it('uses viewportWidth if greater than daysZoomWidth', () => {
26 | const newYear = new Date('2017-01-01T00:00:00.000Z')
27 | const newYearMidday = new Date('2017-01-01T12:00:00.000Z')
28 | const { timelineWidth } = createTime({
29 | start: newYear,
30 | end: newYearMidday,
31 | zoom: 1,
32 | viewportWidth: 1000,
33 | })
34 | expect(timelineWidth).toBe(1000)
35 | })
36 |
37 | it('minTimelineWidth ensures timelineWidth does not fall below minimum', () => {
38 | const newYear = new Date('2017-01-01T00:00:00.000Z')
39 | const newYearMidday = new Date('2017-01-01T12:00:00.000Z')
40 | const { timelineWidth } = createTime({
41 | start: newYear,
42 | end: newYearMidday,
43 | zoom: 1,
44 | viewportWidth: 500,
45 | minWidth: 800,
46 | })
47 | expect(timelineWidth).toBe(800)
48 | })
49 | })
50 |
51 | describe('toX()', () => {
52 | it('calculates correct x pixel position for given date (with pixel rounding)', () => {
53 | const start = new Date('2017-01-01T00:00:00.000Z')
54 | const end = new Date('2018-01-01T00:00:00.000Z')
55 | const { toX } = createTime({ start, end, zoom: 2 })
56 | const nearMiddle = new Date('2017-07-01')
57 | const notClamped = new Date('2020-01-01')
58 | expect(toX(end)).toBe(730)
59 | expect(toX(start)).toBe(0)
60 | expect(toX(nearMiddle)).toBe(362)
61 | expect(toX(notClamped)).toBe(2190)
62 | })
63 | })
64 |
65 | describe('toStyleLeft()', () => {
66 | it('returns style object with correct "left" property', () => {
67 | const start = new Date('2017-01-01T00:00:00.000Z')
68 | const firstOfJune = new Date('2017-06-01T12:34:56.000Z')
69 | const end = new Date('2018-01-01T00:00:00.000Z')
70 | const { toStyleLeft } = createTime({ start, end, zoom: 2 })
71 | expect(toStyleLeft(start)).toEqual({ left: '0px' })
72 | expect(toStyleLeft(firstOfJune)).toEqual({ left: '303px' })
73 | expect(toStyleLeft(end)).toEqual({ left: '730px' })
74 | })
75 | })
76 |
77 | describe('toStyleLeftAndWidth()', () => {
78 | it('returns style object with correct "left" and "width" property', () => {
79 | const start = new Date('2017-01-01T00:00:00.000Z')
80 | const firstOfJune = new Date('2017-06-01T12:34:56.000Z')
81 | const end = new Date('2018-01-01T00:00:00.000Z')
82 | const { toStyleLeftAndWidth } = createTime({ start, end, zoom: 2 })
83 | expect(toStyleLeftAndWidth(start, end)).toEqual({ left: '0px', width: '730px' })
84 | expect(toStyleLeftAndWidth(firstOfJune, end)).toEqual({ left: '303px', width: '427px' })
85 | })
86 | })
87 |
88 | describe('fromX', () => {
89 | it('calculates the date from a given x value', () => {
90 | const start = new Date('2017-01-01')
91 | const firstOfDecember = new Date('2017-12-01')
92 | const end = new Date('2018-01-01')
93 | const { fromX, toX } = createTime({ start, end, zoom: 2 })
94 | expect(fromX(toX(start))).toEqual(start)
95 | expect(fromX(toX(firstOfDecember))).toEqual(firstOfDecember)
96 | expect(fromX(toX(end))).toEqual(end)
97 | })
98 | })
99 | })
100 |
--------------------------------------------------------------------------------
/src/utils/classes.js:
--------------------------------------------------------------------------------
1 | const classes = (base, additional) => {
2 | if (!additional) {
3 | return base
4 | }
5 | if (typeof additional === 'string') {
6 | return `${base} ${additional}`
7 | }
8 | return `${base} ${additional.join(' ')}`
9 | }
10 |
11 | export default classes
12 |
--------------------------------------------------------------------------------
/src/utils/formatDate.js:
--------------------------------------------------------------------------------
1 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
2 |
3 | export const getMonth = date => monthNames[date.getMonth()]
4 |
5 | export const getDayMonth = date => `${date.getMonth() + 1}月${date.getDate()}日`
6 |
--------------------------------------------------------------------------------
/src/utils/getGrid.js:
--------------------------------------------------------------------------------
1 | const getGrid = timebar => (timebar.find(row => row.useAsGrid) || {}).cells
2 |
3 | export default getGrid
4 |
--------------------------------------------------------------------------------
/src/utils/getMouseX.js:
--------------------------------------------------------------------------------
1 | const getMouseX = e => {
2 | const target = e.currentTarget
3 | const bounds = target.getBoundingClientRect()
4 | return e.clientX - bounds.left
5 | }
6 |
7 | export default getMouseX
8 |
--------------------------------------------------------------------------------
/src/utils/time.js:
--------------------------------------------------------------------------------
1 | const MILLIS_IN_A_DAY = 24 * 60 * 60 * 1000
2 |
3 | const create = ({ start, end, zoom, viewportWidth, minWidth }) => {
4 | const duration = end - start
5 |
6 | const days = duration / MILLIS_IN_A_DAY
7 | const daysZoomWidth = days * zoom
8 |
9 | let timelineWidth
10 | // if (daysZoomWidth > viewportWidth) {
11 | // timelineWidth = daysZoomWidth
12 | // } else {
13 | // timelineWidth = viewportWidth
14 | // }
15 |
16 | // if (timelineWidth < minWidth) {
17 | // timelineWidth = minWidth
18 | // }
19 |
20 | timelineWidth = Math.max(minWidth, viewportWidth * zoom)
21 |
22 | // console.log('daysZoomWidth ' + daysZoomWidth)
23 | // console.log('viewportWidth ' + viewportWidth)
24 | // console.log('timelineWidth ' + timelineWidth)
25 |
26 | const timelineWidthStyle = `${timelineWidth}px`
27 |
28 | const toX = from => {
29 | const value = (from - start) / duration
30 | return Math.round(value * timelineWidth)
31 | }
32 |
33 | const toStyleLeft = from => ({
34 | left: `${toX(from)}px`,
35 | })
36 |
37 | const toStyleLeftAndWidth = (from, to) => {
38 | const left = toX(from)
39 | return {
40 | left: `${left}px`,
41 | width: `${toX(to) - left}px`,
42 | }
43 | }
44 |
45 | const fromX = x => new Date(start.getTime() + (x / timelineWidth) * duration)
46 |
47 | return {
48 | timelineWidth,
49 | timelineWidthStyle,
50 | start,
51 | end,
52 | zoom,
53 | toX,
54 | toStyleLeft,
55 | toStyleLeftAndWidth,
56 | fromX,
57 | }
58 | }
59 |
60 | export default create
61 |
--------------------------------------------------------------------------------