diff --git a/.gitignore b/.gitignore index 362b95682..2db738b4f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ modules build gh-pages /lib - +package-lock.json # vscode stuff .vscode diff --git a/README.md b/README.md index 52b50a5d5..9f06b30a7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Calendar Timeline -A modern and responsive react timeline component. +A modern and responsive React timeline component. ![calendar demo](https://raw.githubusercontent.com/namespace-ee/react-calendar-timeline/master/demo.gif) @@ -16,7 +16,7 @@ yarn add react-calendar-timeline npm install --save react-calendar-timeline ``` -`react-calendar-timeline` has `react`, `react-dom`, [`moment`](http://momentjs.com/) and [`interactjs`](http://interactjs.io/docs/) as peer dependencies. +`react-calendar-timeline` has [react](https://reactjs.org/), [react-dom](https://reactjs.org/docs/react-dom.html), [`moment`](http://momentjs.com/) and [`interactjs`](http://interactjs.io/docs/) as peer dependencies. # Usage @@ -83,12 +83,12 @@ Expects either a vanilla JS array or an immutableJS array, consisting of objects id: 1, title: 'group 1', rightTitle: 'title in the right sidebar', - stackItems?: true, + stackItems?: false or 'space' or 'line', height?: 30 } ``` -If you use right sidebar, you can pass optional `rightTitle` property here. +If you use the right sidebar, you can pass optional `rightTitle` property here. If you want to overwrite the calculated height with a custom height, you can pass a `height` property as an int in pixels here. This can be very useful for categorized groups. ## items @@ -118,7 +118,7 @@ Expects either a vanilla JS array or an immutableJS array, consisting of objects } ``` -The preferred (fastest) option is to give unix timestamps in milliseconds for `start_time` and `end_time`. Objects that convert to them (JavaScript Date or moment()) will also work, but will be a lot slower. +The preferred (fastest) option is to give Unix timestamps in milliseconds for `start_time` and `end_time`. Objects that convert to them (JavaScript `Date` or `moment()`) will also work, but will be a lot slower. ## defaultTimeStart and defaultTimeEnd @@ -126,7 +126,7 @@ Unless overridden by `visibleTimeStart` and `visibleTimeEnd`, specify where the ## visibleTimeStart and visibleTimeEnd -The exact viewport of the calendar. When these are specified, scrolling in the calendar must be orchestrated by the `onTimeChange` function. This parameter expects a unix timestamp in milliseconds. +The exact viewport of the calendar. When these are specified, scrolling in the calendar must be orchestrated by the `onTimeChange` function. This parameter expects a Unix timestamp in milliseconds. **Note that you need to provide either `defaultTimeStart/End` or `visibleTimeStart/End` for the timeline to function** @@ -179,8 +179,7 @@ The minimum width, in pixels, of a timeline entry when it's possible to resize. ## stickyOffset -At what height from the top of the screen should we start "sticking" the header (i.e. position: sticky)? This is useful if for example you already have -a sticky navbar and want to push the timeline header down further. Defaults `0`. +At what height from the top of the screen should we start "sticking" the header (i.e. position: sticky)? This is useful if for example you already have a sticky navbar and want to push the timeline header down further. Defaults `0`. ## stickyHeader @@ -216,7 +215,7 @@ Largest time the calendar can zoom to in milliseconds. Default `5 * 365.24 * 864 ## clickTolerance -How many pixels we can drag the background for it to be counted as a click on the background. Defualt: `3` +How many pixels we can drag the background for it to be counted as a click on the background. Default `3` ## canMove @@ -238,6 +237,11 @@ Append a special `.rct-drag-right` handle to the elements and only resize if dra Stack items under each other, so there is no visual overlap when times collide. Can be overridden in the `groups` array. Defaults to `false`. +can be assigned to +- false +- space (saves space in stacking) +- line (stack each item in a line) + ## traditionalZoom Zoom in when scrolling the mouse up/down. Defaults to `false` @@ -332,6 +336,11 @@ function (action, item, time, resizeEdge) { } ``` +## onUpdateMove(itemId, time, newGroup, action,resizeEdge) + +this function is called after moveResizeValidator on every drag update + + ## headerLabelFormats and subHeaderLabelFormats The formats passed to moment to render times in the header and subheader. Defaults to these: @@ -435,9 +444,9 @@ Called when the bounds in the calendar's canvas change. Use it for example to lo ## itemRenderer -Render prop function used to render a customized item. The function provides multiple paramerters that can be used to render each item. +Render prop function used to render a customized item. The function provides multiple parameters that can be used to render each item. -Paramters provided to the function has two types: context params which have the state of the item and timeline, and prop getters functions +Parameters provided to the function has two types: context params which have the state of the item and timeline, and prop getters functions #### Render props params @@ -490,7 +499,7 @@ Rather than applying props on the element yourself and to avoid your props being * `getItemProps` returns the props you should apply to the root item element. The returned props are: * key: item id - * ref: function to get item referance + * ref: function to get item reference * className: classnames to be applied to the item * onMouseDown: event handler * onMouseUp: event handler @@ -500,9 +509,9 @@ Rather than applying props on the element yourself and to avoid your props being * onContextMenu: event handler * style: inline object style - \*\* _the given styles will only override the styles that are not a requirement for postioning the item. Other styles like `color`, `radius` and others_ + \*\* _the given styles will only override the styles that are not a requirement for positioning the item. Other styles like `color`, `radius` and others_ - These properties can be override using the prop argument with proprties: + These properties can be override using the prop argument with properties: * className: class names to be added * onMouseDown: event handler will be called after the component's event handler @@ -711,10 +720,10 @@ const twoSeconds = 2000 {({ styles, date }) => - // date is value of current date. Use this to render special styles for the marker - // or any other custom logic based on date: - // e.g. styles = {...styles, backgroundColor: isDateInAfternoon(date) ? 'red' : 'limegreen'} - return
+ // date is value of current date. Use this to render special styles for the marker + // or any other custom logic based on date: + // e.g. styles = {...styles, backgroundColor: isDateInAfternoon(date) ? 'red' : 'limegreen'} +
} ``` @@ -737,9 +746,7 @@ const today = Date.now() //custom renderer - {({ styles, date }) => - return
- } + {({ styles, date }) =>
} // multiple CustomMarkers @@ -769,14 +776,549 @@ Custom renderer for this marker. Ensure that you always pass `styles` to the roo //custom renderer {({ styles, date }) => - // date is value of current date. Use this to render special styles for the marker - // or any other custom logic based on date: - // e.g. styles = {...styles, backgroundColor: isDateInAfternoon(date) ? 'red' : 'limegreen'} - return
+ // date is value of current date. Use this to render special styles for the marker + // or any other custom logic based on date: + // e.g. styles = {...styles, backgroundColor: isDateInAfternoon(date) ? 'red' : 'limegreen'} +
} ``` +# Timeline Headers + +Timeline headers are the section above the timeline which consist of two main parts: First, the calender header which is a scrolable div containing the dates of the calendar called `DateHeader`. Second, is the headers for the sidebars, called `SidebarHeader`, the left one and optionally the right one. + +## Default usage + +For the default case, two `DateHeader`s are rendered above the timeline, one `primary` and `secondary`. The secondary has the same date unit as the timeline and a `primary` which has a unit larger than the timeline unit by one. + +For the `SidebarHeader`s an empty `SidebarHeader` will be render for the left and optionally an empty right sidebar header if `rightSidebarWith` exists. + +## Overview + +To provide any custom headers for `DateHeader` or `SidebarHeader`. You need to provide basic usage to provide any custom headers. These Custom headers should be always included inside `TimelineHeaders` component in the component's children. + +```jsx +import Timeline, { + TimelineHeaders, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + +
+ +``` +## Components + +Custom headers are implemented through a set of component with mostly [function as a child component](https://medium.com/merrickchristensen/function-as-child-components-5f3920a9ace9) pattern, designed to give the user the most control on how to render the headers. + +### `TimelineHeader` + +Is the core component wrapper component for custom headers + +#### props + +| Prop | type | description | +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `style`| `object`| applied to the root component of headers | +| `className` | `string`| applied to the root component of the headers| +| `calendarHeaderStyle`| `object`| applied to the root component of the calendar headers -scrolable div- `DateHeader` and `CustomHeader`)| +| `calendarHeaderClassName`| `string`| applied to the root component of the calendar headers -scrolable div- `DateHeader` and `CustomHeader`)| + + +### `SidebarHeader` + +Responsible for rendering the headers above the left and right sidebars. + +#### props + +| Prop | type | description | +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `variant`| `left` (default), `right`| renders above the left or right sidebar | +| `children` | `Function`| function as a child component to render the header| + +#### Child function renderer + +a Function provides multiple parameters that can be used to render the sidebar headers + +##### Prop getters functions + +Rather than applying props on the element yourself and to avoid your props being overridden (or overriding the props returned). You can pass an object to the prop getters to avoid any problems. This object will only accept some properties that our component manage so the component make sure to combine them correctly. + +| property | type | description| +| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `getRootProps` | `function(props={})` | returns the props you should apply to the root div element.| + +* `getRootProps` The returned props are: + + * style: inline object style + + These properties can be override using the prop argument with properties: + + * style: extra inline styles to be applied to the component + +#### example + +```jsx +import Timeline, { + TimelineHeaders, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + {({ getRootProps }) => { + return
Right
+ }} +
+ + +
+ +``` + +### `DateHeader` + + +Responsible for rendering the headers above calendar part of the timeline. Consists of time intervals dividing the headers in columns. + +#### props + +| Prop | type | description| +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `style`| `object`| applied to the root of the header | +| `className` | `string`| applied to the root of the header| +| `unit`| `second`, `minute`, `hour`, `day`, `week`, `month`, `year` | intervals between columns | +| `primaryHeader`| `boolean` | main header with interval unit larger than timeline unit by 1 | +| `secondaryHeader` | `boolean` (`true` by default) | sub header with interval equal to timeline unit | +| `labelFormat` | `Function` or `object` or `string`| controls the how to format the interval label | +| `intervalRenderer`| `Function`| render prop to render each interval in the header | + +#### Interval unit + +intervals are decided through three props: `unit`, `primaryHeader` and `secondaryHeader` (default true). `secondaryHeader` is the default if no prop are set. The unit of the intervals will be the same the timeline and a special style is matches the default style of the secondary header from when no custom headers are applied. + +If `primaryHeader` is set to true, it will override `secondaryHeader` and the unit if the timeline will be larger by 1 of the timeline unit. The default style will match the primary header from when no custom headers are applied. + +If `unit` is set, it will override both `primaryHeader` and `secondaryHeader`. The unit of the header will be the unit passed though the prop and can be any `unit of time` from `momentjs`. The default style will match the primary header from when no custom headers are applied. + +#### Label format + +To format each interval label you can use 3 types of props to format which are: + +- `string`: if a string was passed it will be passed to `startTime` method `format` which is a `momentjs` object . + +- `object`: this will give you more flexibility to format the label with respect to `labelWidth`. Internally the `startTime` will be formated with the string corresponding to `formatObject[unit][range]` + + The object will be in the following type: + ```typescript + type unit = `second` | `minute` | `hour` | `day` | `week` | `month` | `year` + interface LabelFormat { + [unit]: { + long: string, + mediumLong: string, + medium: string, + short: string + } + } + // default format object + const format : LabelFormat = { + year: { + long: 'YYYY', + mediumLong: 'YYYY', + medium: 'YYYY', + short: 'YY' + }, + month: { + long: 'MMMM YYYY', + mediumLong: 'MMMM', + medium: 'MMMM', + short: 'MM/YY' + }, + day: { + long: 'dddd, LL', + mediumLong: 'dddd, LL', + medium: 'dd D', + short: 'D' + }, + hour: { + long: 'dddd, LL, HH:00', + mediumLong: 'L, HH:00', + medium: 'HH:00', + short: 'HH' + }, + minute: { + long: 'HH:mm', + mediumLong: 'HH:mm', + medium: 'HH:mm', + short: 'mm', + } + } + ``` + + The `long`, `mediumLong`, `medium` and `short` will be be decided through the `labelWidth` value according to where it lays upon the following scale: + + ``` + |-----`short`-----50px-----`medium`-----100px-----`mediumLong`-----150px--------`long`----- + ``` + +- `Function`: This is the more powerful method and offers the most control over what is rendered. The returned `string` will be rendered inside the interval + + ```typescript + type Unit = `second` | `minute` | `hour` | `day` | `month` | `year` + ([startTime, endTime] : [Moment, Moment], unit: Unit, labelWidth: number, formatOptions: LabelFormat = defaultFormat ) => string + ``` + +#### intervalRenderer + +Render prop function used to render a customized interval. The function provides multiple parameters that can be used to render each interval. + +Paramters provided to the function has two types: context params which have the state of the item and timeline, and prop getters functions + +##### interval context + +An object contains the following properties: + +| property | type | description | +| ------------------ | -------- | ---------------------------------------------------- | +| `interval` | `array : [Moment, Moment]` | an tuple array conating two moment object the first `startTime` and the second `endTime`| +| `intervalText` | `string` | the string returned from `labelFormat` prop | + + +##### Prop getters functions + +Rather than applying props on the element yourself and to avoid your props being overridden (or overriding the props returned). You can pass an object to the prop getters to avoid any problems. This object will only accept some properties that our component manage so the component make sure to combine them correctly. + +| property | type | description| +| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `getIntervalProps` | `function(props={})` | returns the props you should apply to the root div element.| + +* `getIntervalProps` The returned props are: + + * style: inline object style + * onClick: event handler + * key + + These properties can be extended using the prop argument with properties: + + * style: extra inline styles to be applied to the component + * onClick: extra click handler added to the normal `showPeriod callback` + +#### example + +```jsx +import Timeline, { + TimelineHeaders, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + { + return
+ {intervalContext.intervalText} +
+ }} + /> +
+
+``` + +### `ItemHeader` + + +Responsible for rendering group of items in the header. + +#### props + +| Prop | type | description| +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `style`| `object`| applied to the root of the header | +| `className` | `string`| applied to the root of the header| +| `itemHeight`| `number` | item height | +| `stackItems` | `boolean` (`false` by default) | optionally stack items in header | +| `itemRenderer`| `Function`| render prop to render each interval in the header | +| `props` | `object` | pass extra props to itemRenderer | + +#### itemRenderer + +Render prop function used to render a customized item. The function provides multiple parameters that can be used to render each item. + +Paramters provided to the function has two types: context params which have the state of the item and timeline, and prop getters functions + +##### item + +The object of the item to render + +##### timelineContext + +timeline context + +##### itemContext + +item context + +##### Prop getters functions + +Rather than applying props on the element yourself and to avoid your props being overridden (or overriding the props returned). You can pass an object to the prop getters to avoid any problems. This object will only accept some properties that our component manage so the component make sure to combine them correctly. + +| property | type | description| +| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `getRootProps` | `function(props={})` | returns the props you should apply to the root div element.| + +* `getRootProps` The returned props are: + + * style: inline object style + + These properties can be extended using the prop argument with properties: + + * style: extra inline styles to be applied to the component + +#### example + +```jsx +import Timeline, { + TimelineHeaders, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + +const items = [ + { + id: 1, + title: 'item 1', + start_time: moment(), + end_time: moment().add(1, 'hour') + }, + { + id: 2, + title: 'item 2', + start_time: moment().add(-0.5, 'hour'), + end_time: moment().add(0.5, 'hour') + }, + { + id: 3, + title: 'item 3', + start_time: moment().add(2, 'hour'), + end_time: moment().add(3, 'hour') + } +] + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ { + return ( +
+ {item.title} +
+ ) + }} + /> + + +
+
+``` + +### `CustomHeader` + +Responsible for rendering the headers above calendar part of the timeline. This is the base component for `DateHeader` and `ItemHeader`. This offers more control with less features. + +#### props + +| Prop | type | description| +| ----------------- | --------------- | ---| +| `unit`| `second`, `minute`, `hour`, `day`, `week`, `month`, `year` (default `timelineUnit`) | intervals | +| `children` | `Function`| function as a child component to render the header| + +#### unit + +The unit of the header will be the unit passed though the prop and can be any `unit of time` from `momentjs`. The default value for unit is `timelineUnit` + +#### Children + +Function as a child component to render the header + +Paramters provided to the function has three types: context params which have the state of the item and timeline, prop getters functions and helper functions. + +``` +({ + timelineContext: { + timelineWidth, + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd + }, + headerContext: { + unit, + intervals: this.state.intervals + }, + getRootProps: this.getRootProps, + getIntervalProps: this.getIntervalProps, + showPeriod +})=> React.Node +``` + +##### context + +An object contains context for `timeline` and `header`: + + +###### Timeline context + +| property | type | description | +| ------------------ | -------- | ---------------------------------------------------- | +| `timelineWidth` | `array : [Moment, Moment]` | an tuple array conating two moment object the first `startTime` and the second `endTime`| +| `visibleTimeStart` | `string` | the string returned from `labelFormat` prop | +| `visibleTimeEnd` | `array : [Moment, Moment]` | an tuple array conating two moment object the first `startTime` and the second `endTime`| +| `canvasTimeStart` | `string` | the string returned from `labelFormat` prop | +| `canvasTimeEnd` | `array : [Moment, Moment]` | an tuple array conating two moment object the first `startTime` and the second `endTime`| + +###### Header context + +| property | type | description | +| ------------------ | -------- | ---------------------------------------------------- | +| `intervals` | `array` | an array with all intervals| +| `unit` | `string` | unit passed or timelineUnit | + +** `interval`: `[startTime: Moment, endTime: Moment]` + +##### Prop getters functions + +Rather than applying props on the element yourself and to avoid your props being overridden (or overriding the props returned). You can pass an object to the prop getters to avoid any problems. This object will only accept some properties that our component manage so the component make sure to combine them correctly. + +| property | type | description| +| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `getRootProps` | `function(props={})` | returns the props you should apply to the root div element.| +| `getIntervalProps` | `function(props={})` | returns the props you should apply to the interval div element.| + +* `getIntervalProps` The returned props are: + + * style: inline object style + * onClick: event handler + * key + + These properties can be extended using the prop argument with properties: + + * style: extra inline styles to be applied to the component + * onClick: extra click handler added to the normal `showPeriod callback` + +##### helpers: + +| property | type | description| +| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `showPeriod` | `function(props={})` | returns the props you should apply to the root div element.| + + + +#### example + +```jsx +import Timeline, { + TimelineHeaders, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }) => { + return ( +
+ {intervals.map(interval => { + const intervalStyle = { + // height: 30, + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer', + backgroundColor: 'Turquoise', + color: 'white' + } + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > +
+ {interval.startTime.format('YYYY')} +
+
+ ) + })} +
+ ) + }} +
+
+
+``` + # FAQ ## My timeline is unstyled @@ -785,7 +1327,7 @@ You need to include the `Timeline.css` file, either via static file reference or ## How can I have items with different colors? -Now you can use item renderer for rendering items with different colors [itemRenderer](https://github.com/namespace-ee/react-calendar-timeline#itemrenderer). +Now you can use item renderer for rendering items with different colors [itemRenderer](https://github.com/namespace-ee/react-calendar-timeline#itemrenderer). Please refer to [examples](https://github.com/namespace-ee/react-calendar-timeline/tree/master/examples#custom-item-rendering) for a sandbox example ## How can I add a sidebar on the right? @@ -917,3 +1459,6 @@ npm version patch ``` --> + +## License +[MIT licensed](/LICENSE.md). diff --git a/__fixtures__/stateAndProps.js b/__fixtures__/stateAndProps.js index 20bf62783..6427947ec 100644 --- a/__fixtures__/stateAndProps.js +++ b/__fixtures__/stateAndProps.js @@ -3,7 +3,7 @@ import {items} from './itemsAndGroups' export const props = { keys: defaultKeys, lineHeight: 30, - stackItems: true, + stackItems: 'space', itemHeightRatio: 0.75 } diff --git a/__tests__/components/Header/Header.test.js b/__tests__/components/Header/Header.test.js deleted file mode 100644 index ec7f8a1ea..000000000 --- a/__tests__/components/Header/Header.test.js +++ /dev/null @@ -1,199 +0,0 @@ -import React from 'react' -import { shallow, mount } from 'enzyme' -import { sel } from 'test-utility' -import Header from 'lib/layout/Header' -import { - defaultHeaderLabelFormats, - defaultSubHeaderLabelFormats -} from 'lib/default-config' - -const defaultProps = { - hasRightSidebar: false, - showPeriod: () => {}, - canvasTimeStart: 1000 * 60 * 60 * 8, // eight hours into the epoch - need to adjust for Mike Joyce being in CST :) - canvasTimeEnd: 1000 * 60 * 60 * 10, // ten hours into the epoch - canvasWidth: 1000, - minUnit: 'day', - timeSteps: {}, - width: 400, - headerLabelFormats: defaultHeaderLabelFormats, - subHeaderLabelFormats: defaultSubHeaderLabelFormats, - stickyOffset: 5, - stickyHeader: true, - headerLabelGroupHeight: 15, - headerLabelHeight: 15, - scrollHeaderRef: () => {}, - headerRef: () => {} -} - -const selectors = { - headerElementsContainer: sel('timeline-elements-header-container'), - headerElements: sel('timeline-elements-header') -} - -describe('Header', () => { - describe('timeline-elements-header', () => { - it('accepts scrollHeaderRef callback', () => { - const scrollHeaderRef = jest.fn() - - const props = { - ...defaultProps, - scrollHeaderRef: scrollHeaderRef - } - - mount(
) - - expect(scrollHeaderRef).toHaveBeenCalledTimes(1) - - const mockCallParam = scrollHeaderRef.mock.calls[0][0] - - expect(mockCallParam.dataset.testid).toBe('header') - }) - - it('accepts headerRef callback', () => { - const headerRefMock = jest.fn() - - const props = { - ...defaultProps, - headerRef: headerRefMock - } - - mount(
) - - expect(headerRefMock).toHaveBeenCalledTimes(1) - - const mockCallParam = headerRefMock.mock.calls[0][0] - - expect(mockCallParam.dataset.testid).toBe('timeline-elements-container') - }) - - it('container recieves width property', () => { - const props = { - ...defaultProps, - width: 1500 - } - - const wrapper = shallow(
) - - expect( - wrapper.find(selectors.headerElementsContainer).props().style.width - ).toBe(props.width) - }) - }) - describe('sticky header', () => { - it('sets "header-sticky" class if stickyHeader is true', () => { - const props = { - ...defaultProps, - stickyHeader: true - } - - const wrapper = shallow(
) - - expect(wrapper.props().className).toMatch('header-sticky') - }) - it('does not set "header-sticky" class if stickyHeader is false', () => { - const props = { - ...defaultProps, - stickyHeader: false - } - - const wrapper = shallow(
) - - expect(wrapper.props().className).not.toMatch('header-sticky') - }) - it('style.top is 0 if stickyHeader is false', () => { - const props = { - ...defaultProps, - stickyHeader: false, - stickyOffset: 10 - } - - const wrapper = shallow(
) - - expect(wrapper.props().style.top).toBe(0) - }) - it('style.top is set to stickyOffset if stickyHeader is true', () => { - const props = { - ...defaultProps, - stickyHeader: true, - stickyOffset: 10 - } - - const wrapper = shallow(
) - - expect(wrapper.props().style.top).toBe(props.stickyOffset) - }) - it('style.top is set to 0 if stickyHeader is true and no stickyOffset is passed in', () => { - const props = { - ...defaultProps, - stickyHeader: true, - stickyOffset: null - } - - const wrapper = shallow(
) - - expect(wrapper.props().style.top).toBe(0) - }) - // TODO: fix these tests so that they're time zone agnostic. Right now these will fail if your timezone is - // way behind UTC offset - it('should update headers format when subHeaderLabelFormats and subHeaderLabelFormats change', () => { - const wrapper = mount(
) - expect( - wrapper - .find('.rct-label-group') - .text() - .includes('January 1970') - ).toBeTruthy() - expect( - wrapper - .find('.rct-label') - .text() - .includes('Thursday, 1st') - ).toBeTruthy() - wrapper.setProps({ - headerLabelFormats: { - yearShort: 'YY', - yearLong: 'YYYY', - monthShort: 'YY', - monthMedium: 'YYYY', - monthMediumLong: 'YYYY', - monthLong: 'YYYY', - dayShort: 'L', - dayLong: 'dddd', - hourShort: 'HH', - hourMedium: 'HH:00', - hourMediumLong: 'L, HH:00', - hourLong: 'dddd, LL, HH:00', - time: 'LLL' - }, - subHeaderLabelFormats: { - yearShort: 'YY', - yearLong: 'YYYY', - monthShort: 'MM', - monthMedium: 'MMM', - monthLong: 'MMMM', - dayShort: 'D', - dayMedium: 'dd', - dayMediumLong: 'ddd', - dayLong: 'dddd', - hourShort: 'HH', - hourLong: 'HH:00', - minuteShort: 'mm', - minuteLong: 'HH:mm' - } - }) - expect( - wrapper - .find('.rct-label-group') - .text() - .includes('1970') - ).toBeTruthy() - expect( - wrapper - .find('.rct-label') - .text() - .includes('Thursday') - ).toBeTruthy() - }) - }) -}) diff --git a/__tests__/components/Header/TimelineElementsHeader.test.js b/__tests__/components/Header/TimelineElementsHeader.test.js deleted file mode 100644 index 77cd846c4..000000000 --- a/__tests__/components/Header/TimelineElementsHeader.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { mount } from 'enzyme' -import { sel, noop } from 'test-utility' -import TimelineElementsHeader from 'lib/layout/TimelineElementsHeader' - -const defaultProps = { - hasRightSidebar: false, - showPeriod: noop, - canvasTimeStart: 0, - canvasTimeEnd: 0, - canvasWidth: 1000, - minUnit: 'day', - timeSteps: {}, - width: 0, - headerLabelFormats: {}, - subHeaderLabelFormats: {}, - headerLabelGroupHeight: 0, - headerLabelHeight: 0, - scrollHeaderRef: () => {} -} - -describe('Header', () => { - it('renders', () => { - mount() - }) - - it('prevents mouse down from bubbling', () => { - const mouseDownMock = jest.fn() - const wrapper = mount( -
- -
- ) - - wrapper.find(sel('header')).simulate('mousedown') - - expect(mouseDownMock).not.toHaveBeenCalled() - }) - - it('accepts scrollHeaderRef callback', () => { - const scrollHeaderRef = jest.fn() - - const props = { - ...defaultProps, - scrollHeaderRef: scrollHeaderRef - } - - mount() - - expect(scrollHeaderRef).toHaveBeenCalledTimes(1) - - const mockCallParam = scrollHeaderRef.mock.calls[0][0] - - expect(mockCallParam.dataset.testid).toBe('header') - }) -}) diff --git a/__tests__/components/Headers/CustomHeader.test.js b/__tests__/components/Headers/CustomHeader.test.js new file mode 100644 index 000000000..06883ce1f --- /dev/null +++ b/__tests__/components/Headers/CustomHeader.test.js @@ -0,0 +1,242 @@ +import React from 'react' +import { render, cleanup, prettyDOM } from 'react-testing-library' +import Timeline from 'lib/Timeline' +import DateHeader from 'lib/headers/DateHeader' +import SidebarHeader from 'lib/headers/SidebarHeader' +import TimelineHeaders from 'lib/headers/TimelineHeaders' +import CustomHeader from 'lib/headers/CustomHeader' + +import 'jest-dom/extend-expect' +import moment from 'moment' + +import { items, groups } from '../../../__fixtures__/itemsAndGroups' +import { visibleTimeEnd, visibleTimeStart } from '../../../__fixtures__/stateAndProps' + +const defaultProps = { + groups, + items, + defaultTimeStart: moment(visibleTimeStart, 'x'), + defaultTimeEnd: moment(visibleTimeEnd, 'x'), +} + +describe('CustomHeader Component Test', () => { + + afterEach(cleanup) + // Render The Example In The Docs + it('Given CustomHeader When render Then it should renderd Correctly in the timeline', () => { + const { getByTestId } = render( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }) => { + return ( +
+ {intervals.map(interval => { + const intervalStyle = { + // height: 30, + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer', + backgroundColor: 'Turquoise', + color: 'white' + } + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > +
+ {interval.startTime.format('YYYY')} +
+
+ ) + })} +
+ ) + }} +
+
+
+ ) + + expect(getByTestId('customHeader')).toBeInTheDocument() + }) + it('Given CustomHeader When pass a unit to it Then it should take it', () => { + const { getByTestId, rerender } = render(getCustomHeadersInTimeline({ unit: "year" })); + expect(getByTestId('customHeader')).toHaveTextContent('01/01/2018') + + rerender(getCustomHeadersInTimeline({ unit: "month" })); + expect(getByTestId('customHeader')).toHaveTextContent('10/01/2018') + + rerender(getCustomHeadersInTimeline({ unit: "day" })); + expect(getByTestId('customHeader')).toHaveTextContent('10/25/2018') + expect(getByTestId('customHeader')).toHaveTextContent('10/26/2018') + expect(getByTestId('customHeader')).toHaveTextContent('10/27/2018') + }) + it('Given CustomHeader When pass a style props with (width, position) Then it should not ovverride the default values', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ props: { style: { width: 0, position: 'fixed' } } })); + const { width, position } = getComputedStyle(getByTestId('customHeader')) + expect(width).not.toBe('0px') + expect(position).not.toBe('fixed') + }) + + it('Given CustomHeader When pass a style props other than (width, position) Then it should renderd Correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ props: { style: { height: 150 } } })); + const { height } = getComputedStyle(getByTestId('customHeader')) + expect(height).toBe("150px") + }) + + it('Given CustomHeader When pass an interval style with (width, position, left) Then it should not ovverride the default values', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ + intervalStyle: { + width: 0, + position: 'fixed', + left: 0 + } + })); + const { width, position, left } = getComputedStyle(getByTestId('customHeaderInterval')) + expect(width).not.toBe('0px') + expect(position).not.toBe('fixed') + expect(left).not.toBe('0px') + }) + it('Given CustomHeader When pass an interval style other than (width, position) Then it should rendered correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ + intervalStyle: { + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer', + color: 'white' + } + })); + const { lineHeight, textAlign, borderLeft, cursor, color } = getComputedStyle(getByTestId('customHeaderInterval')) + expect(lineHeight).toBe('30px') + expect(textAlign).toBe('center') + expect(borderLeft).toBe('1px solid black') + expect(cursor).toBe('pointer') + expect(color).toBe('white') + }) + it('Given a CustomHeader When pass a jsx as a children Then it Should be rendered Correctly', () => { + const { getByText } = render(getCustomHeadersInTimeline()) + expect(getByText('Should Be Rendered')).toBeInTheDocument() + }) + it('Given a CustomHeader When not pass any unit prop Then it Should take the default timeline unit (year)', () => { + const { getByTestId } = render(getCustomHeadersInTimeline()) + expect(getByTestId('customHeader')).toHaveTextContent('01/01/2018') + }) + it('Given DateHeader When Rendered with day unit Then getIntervalStyle function shuold be act correctly', () => { + const props = getIntervalStyle({ startTime: moment("2018-10-27T21:00:00.000"), canvasTimeStart: 1540414800000, ratio: 0.5, unit: 'month', labelWidth: 150 }) + expect(props).toEqual({ + left: 124200000, + width: 150, + position: 'absolute' + }) + }) + it('Given CustomHeader When pass a style in props obj to props renderer Then it should not override correctly render it', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({props: {'aria-hidden': false}})) + expect(getByTestId('customHeader')).toHaveAttribute('aria-hidden') + + }) + describe('Testing Diffrent Unit Values', () => { + it('Given CustomHeader When pass a year unit then it should render it correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ unit: 'year' })); + expect(getByTestId('customHeader')).toHaveTextContent('01/01/2018') + }) + it('Given CustomHeader When pass a month unit then it should render it correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ unit: 'month' })); + expect(getByTestId('customHeader')).toHaveTextContent('10/01/2018') + }) + it('Given CustomHeader When pass a day unit then it should render it correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ unit: 'day' })); + expect(getByTestId('customHeader')).toHaveTextContent('10/25/2018') + }) + it('Given CustomHeader When pass a hour unit then it should render it correctly', () => { + const { getByTestId } = render(getCustomHeadersInTimeline({ unit: 'hour' })); + expect(getByTestId('customHeader')).toHaveTextContent('10/25/2018') + }) + }) + +}) + +function getCustomHeadersInTimeline({ unit = "year", props, intervalStyle } = {}) { + + return ( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }, extraProps) => { + return ( +
+ {intervals.map(interval => { + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > +
+ {interval.startTime.format('MM/DD/YYYY')} +
+
+ ) + })} +
+ Should Be Rendered +
+
+ + ) + }} +
+
+
+ ) +} + + +const getIntervalStyle = ({ startTime, canvasTimeStart, ratio, unit, labelWidth, style }) => { + const left = Math.round((startTime.valueOf() - canvasTimeStart) * ratio) + const unitValue = startTime.get(unit === 'day' ? 'date' : unit) + const firstOfType = unitValue === (unit === 'day' ? 1 : 0) + const leftCorrect = firstOfType ? 1 : 0 + return { + ...style, + left: left - leftCorrect, + width: labelWidth, + position: 'absolute' + } +} diff --git a/__tests__/components/Headers/DateHeader.test.js b/__tests__/components/Headers/DateHeader.test.js new file mode 100644 index 000000000..857d2dbf0 --- /dev/null +++ b/__tests__/components/Headers/DateHeader.test.js @@ -0,0 +1,401 @@ +import React from 'react' +import { render, cleanup, within } from 'react-testing-library' +import Timeline from 'lib/Timeline' +import DateHeader from 'lib/headers/DateHeader' +import SidebarHeader from 'lib/headers/SidebarHeader' +import TimelineHeaders from 'lib/headers/TimelineHeaders' +import 'jest-dom/extend-expect' +import moment from 'moment' +import { items, groups } from '../../../__fixtures__/itemsAndGroups' +import { visibleTimeEnd, visibleTimeStart } from '../../../__fixtures__/stateAndProps' +import { TimelineStateProvider } from 'lib/timeline/TimelineStateContext' +import { TimelineHeadersProvider } from 'lib/headers/HeadersContext' +const defaultProps = { + groups, + items, + defaultTimeStart: moment(visibleTimeStart, 'x'), + defaultTimeEnd: moment(visibleTimeEnd, 'x'), +} + +describe("Testing DateHeader Component", () => { + beforeEach(() => { + + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { + width: 1000, + height: 120, + top: 0, + left: 0, + bottom: 0, + right: 0, + } + }); + }) + afterEach(cleanup) + + // Testing The Example In The Docs + it("Given DateHeader When rendered Then it should be rendered correctly in the timeLine", () => { + const { getAllByTestId } = render( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + { + return
+ {intervalContext.intervalText} +
+ }} + /> +
+
+ ) + + expect(getAllByTestId('dateHeader')).toHaveLength(3) + + }) + + it("Given Dateheader When pass a string typed labelFormat Then it should render the intervals with hte given format", () => { + const { getAllByTestId } = render(dateHeaderComponent({ unit: "day", labelFormat: "MM/DD" })); + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/25') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/26') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/27') + expect(getAllByTestId('dateHeader')[0]).toHaveTextContent('Thursday, October 25') + + }) + + it("Given Dateheader When pass an object typed labelFormat Then it should render the intervals with hte given format", () => { + const { getAllByTestId } = render(dateHeaderComponent({ unit: "day", labelFormat: { day: { long: "MM/DD/YYYY" } } })); + + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/25/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/26/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/27/2018') + expect(getAllByTestId('dateHeader')[0]).toHaveTextContent('Thursday, October 25') + }) + it("Given Dateheader When pass a function typed labelFormat Then it should render the intervals with hte given format", () => { + const formatlabel = jest.fn((interval, unit, labelWidth) => interval[0].format("MM/DD/YYYY")) + const { getAllByTestId } = render(dateHeaderComponent({ unit: "day", labelFormat: formatlabel })); + + expect(formatlabel).toHaveBeenCalled() + + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/25/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/26/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/27/2018') + + expect(getAllByTestId('dateHeader')[0]).toHaveTextContent('Thursday, October 25') + + }) + + it("Given Dateheader When pass a string typed labelFormat Then it should be called with the right params", () => { + const formatlabel = jest.fn((interval, unit, labelWidth) => interval[0].format("MM/DD/YYYY")) + render(dateHeaderComponent({ unit: "day", labelFormat: formatlabel })) + expect(formatlabel).toHaveBeenCalled() + expect(formatlabel).toHaveBeenCalledWith(expect.any(Array), "day", expect.any(Number)) + }) + + + + it("Given Dateheader When click on the primary header Then it should change the unit", async () => { + + const formatlabel = jest.fn((interval, unit, labelWidth) => interval[0].format("MM/DD/YYYY")) + const { getByTestId, getAllByTestId } = render(dateHeaderComponent({ unit: "day", labelFormat: formatlabel })); + // Arrange + const primaryHeader = getByTestId('dateHeader') + const seconderyHeader = getAllByTestId('dateHeader')[2] + + // Act + const primaryFirstClick = within(primaryHeader).getByText('Friday, October 26, 2018').parentElement + primaryFirstClick.click() + const primarySecondClick = within(primaryHeader).getByText('October 2018').parentElement + primarySecondClick.click() + + // Assert + expect(seconderyHeader).toHaveTextContent('January') + expect(primaryHeader).toHaveTextContent('2018') + }) + + it("Given Dateheader When click on the secondary header Then it should change the unit", async () => { + + const formatlabel = jest.fn((interval, unit, labelWidth) => interval[0].format("MM/DD/YYYY")) + const { getByTestId, getAllByTestId } = render(dateHeaderComponent({ unit: "day", labelFormat: formatlabel })); + // Arrange + const primaryHeader = getByTestId('dateHeader') + const seconderyHeader = getAllByTestId('dateHeader')[2] + + const primaryFirstClick = within(primaryHeader).getByText('Friday, October 26, 2018').parentElement + primaryFirstClick.click() + const primarySecondClick = within(primaryHeader).getByText('October 2018').parentElement + + primarySecondClick.click() + + // Act + const secondaryFirstClick = within(seconderyHeader).queryByText('January') + secondaryFirstClick.click() + expect(primaryHeader).toHaveTextContent('December 2016') + const secondarySecondClick = within(seconderyHeader).queryByText('1') + secondarySecondClick.click() + expect(primaryHeader).toHaveTextContent('Wednesday, November 30, 2016') + }) + + it('Given DateHeadr When click on primary or secondary Then onTimeChange function should be called with the right params', () => { + const handleTimeChange = jest.fn((visibleTimeStart, visibleTimeEnd, updateScrollCanvas) => + updateScrollCanvas(visibleTimeStart, visibleTimeEnd)) + const { getByTestId, getAllByTestId, debug } = render(dateHeaderComponent({ unit: "day", handleTimeChange: handleTimeChange })); + const primaryHeader = within(getByTestId('dateHeader')).getByText('Friday, October 26, 2018').parentElement + primaryHeader.click() + const secondaryHeader = within(getAllByTestId('dateHeader')[1]).getByText('1').parentElement + expect(handleTimeChange).toBeCalled() + expect(handleTimeChange).toBeCalledTimes(1) + expect(handleTimeChange).toBeCalledWith(expect.any(Number), expect.any(Number), expect.any(Function)) + expect(handleTimeChange).toReturn() + + secondaryHeader.click(); + expect(handleTimeChange).toBeCalled() + expect(handleTimeChange).toBeCalledTimes(1) + expect(handleTimeChange).toBeCalledWith(expect.any(Number), expect.any(Number), expect.any(Function)) + expect(handleTimeChange).toReturn() + }) + + it('Given Dateheader When pass a className Then it should be applied to DateHeader', () => { + const { getAllByTestId } = render(dateHeaderComponent({ labelFormat: "MM/DD/YYYY", className: 'test-class-name' })); + expect(getAllByTestId('dateHeader')[1]).toHaveClass('test-class-name') + }) + + it('Given Interval When pass an ovveride values for (width, left, position) it should not ovverride the default values', () => { + const { getAllByTestId } = render(dateHeaderComponent({ labelFormat: "MM/DD/YYYY", props: { style: { width: 100, position: 'fixed' } } })); + const { width, position } = getComputedStyle(getAllByTestId('interval')[0]) + expect(width).not.toBe('100px') + expect(position).not.toBe('fixed') + }) + + it('Given Interval When pass an override (width, position) Then it should render the default values for it', () => { + const { getAllByTestId } = render(dateHeaderComponent({ labelFormat: "MM/DD/YYYY", props: { style: { width: 100, position: 'fixed' } } })); + const { width, position } = getComputedStyle(getAllByTestId('interval')[0]) + expect(width).toBe('36px') + expect(position).toBe('absolute') + + + }) + it('Given Interval When pass any style other than (position, width, left) through the Dateheader Then it should take it', () => { + const { getAllByTestId } = render(dateHeaderComponent({ labelFormat: "MM/DD/YYYY", props: { style: { display: 'flex' } } })); + const { display } = getComputedStyle(getAllByTestId('interval')[0]) + + expect(display).toBe('flex') + + }) + it('Given unit Dateheader When pass a style Object Then it should render the given style correctly', () => { + const { getAllByTestId } = render(dateHeaderComponent({ style: { height: 50 }, labelFormat: "MM/DD/YYYY" })); + const { height } = getComputedStyle(getAllByTestId('dateHeader')[1]) + + expect(height).toBe('50px') + }) + + it('Given DateHeader component When pass an intervalRenderer prop then it should be called with the right params', () => { + const intervalRenderer = jest.fn(({ getIntervalProps, intervalContext }, props) =>
{intervalContext.intervalText}
) + const { getByTestId, rerender } = render(dateHeaderWithIntervalRenderer({ intervalRenderer: intervalRenderer })) + const bluePrint = { + getIntervalProps: expect.any(Function), + intervalContext: expect.any(Object) + } + expect(intervalRenderer).toBeCalled() + expect(intervalRenderer).toReturn() + // because we did not pass a props then the function will called with undefined props + expect(intervalRenderer).toBeCalledWith(expect.objectContaining(bluePrint), undefined) + rerender(dateHeaderWithIntervalRenderer({ intervalRenderer: intervalRenderer, props: { style: { height: 50 } } })) + expect(intervalRenderer).toBeCalledWith(expect.objectContaining(bluePrint), expect.any(Object)) + expect(getByTestId('myAwesomeInterval')).toBeInTheDocument() + }) + describe('Testing The Label Format Diffrent Cases', () => { + it('Given DateHeader When resize the width of the screen to 1000 Then it Should take the long format', () => { + Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 1000 })); + const { getAllByTestId, rerender } = render(dateHeaderComponent({ + unit: "day", labelFormat: { + day: { short: "DD", medium: "DD/MM", mediumLong: "MM/YYYY", long: "MM/DD/YYYY", } + } + })); + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/26/2018') + }) + + it('Given DateHeader When resize the width of the screen to 250 Then it Should take the mediumLong format', () => { + Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 250 })); + const { getAllByTestId } = render(dateHeaderComponent({ + unit: "day", labelFormat: { + day: { short: "DD", medium: "DD/MM", mediumLong: "MM/YYYY", long: "MM/DD/YYYY", } + } + })); + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/2018') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('10/2018') + }) + it('Given DateHeader When resize the width of the screen to 200 Then it Should take the medium format', () => { + Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 200 })); + const { getAllByTestId } = render(dateHeaderComponent({ + unit: "day", labelFormat: { + day: { short: "DD", medium: "DD/MM", mediumLong: "MM/YYYY", long: "MM/DD/YYYY", } + } + })); + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('25/10') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('26/10') + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('27/10') + }) + it('Given DateHeader When resize the width of the screen to 100 Then it Should take the short format', () => { + Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 100 })); + const { getAllByTestId } = render(dateHeaderComponent({ + unit: "day", labelFormat: { + day: { short: "DD", medium: "DD/MM", mediumLong: "MM/YYYY", long: "MM/DD/YYYY", } + } + })); + expect(getAllByTestId('dateHeader')[1]).toHaveTextContent('26') + }) + }) + + describe('Testing Diffrent Unit Values', () => { + it('Given DateHeader When pass a year unit to the timeline then it should take it as default', () => { + const children = + const {getAllByTestId} = renderDateHeaderWithContext({unit: 'year', children: children}); + const primaryHeader = getAllByTestId('dateHeader')[0] + const secondaryHeader = getAllByTestId('dateHeader')[1] + expect(primaryHeader).toHaveTextContent('2019') + expect(secondaryHeader).toHaveTextContent('2019') + }) + it('Given DateHeader When pass a month unit to the timeline then it should take it as default', () => { + const children = + const {getAllByTestId} = renderDateHeaderWithContext({unit: 'month', children: children}); + const primaryHeader = getAllByTestId('dateHeader')[0] + const secondaryHeader = getAllByTestId('dateHeader')[1] + expect(primaryHeader).toHaveTextContent('2019') + expect(secondaryHeader).toHaveTextContent('January 2019') + }) + it('Given DateHeader When pass a day unit to the timeline then it should take it as default', () => { + const children = + const {getAllByTestId} = renderDateHeaderWithContext({unit: 'day', children: children}); + const primaryHeader = getAllByTestId('dateHeader')[0] + const secondaryHeader = getAllByTestId('dateHeader')[1] + expect(primaryHeader).toHaveTextContent('January 2019') + expect(secondaryHeader).toHaveTextContent('Monday, January 21, 2019') + }) + it('Given DateHeader When pass a hour unit to the timeline then it should take it as default', () => { + const children = + const {getAllByTestId} = renderDateHeaderWithContext({unit: 'hour', children: children}); + const primaryHeader = getAllByTestId('dateHeader')[0] + const secondaryHeader = getAllByTestId('dateHeader')[1] + expect(primaryHeader).toHaveTextContent('Monday, January 21, 2019') + expect(secondaryHeader).toHaveTextContent('30') + }) + }) + +}) + +function dateHeaderComponent({ labelFormat, unit, props, className, style, handleTimeChange } = {}) { + + return ( + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + { + return
+ {intervalContext.intervalText} + +
+ }} + /> + +
+
+ ) +} + +function dateHeaderWithIntervalRenderer({ intervalRenderer, props } = {}) { + + return ( + + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + + +
+
+ ) +} + + +function renderDateHeaderWithContext({unit, children} = {}) +{ + const oneDay = 1000 * 60 * 60 * 24 + const now = Date.now() + const visibleTimeStart = now - oneDay + const visibleTimeEnd = now + oneDay + const defaultTimelineState = { + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart: visibleTimeStart - oneDay, + canvasTimeEnd: visibleTimeEnd + oneDay, + canvasWidth: 3000, + visibleWidth: 1000, + showPeriod:()=>{}, + timelineWidth:1000, + timelineUnit:unit + } + + return render( + + React.createRef()} + rightSidebarWidth={0} + > + {children} + + + ) + +} diff --git a/__tests__/components/Headers/SideBarHeader.test.js b/__tests__/components/Headers/SideBarHeader.test.js new file mode 100644 index 000000000..b15c13a3b --- /dev/null +++ b/__tests__/components/Headers/SideBarHeader.test.js @@ -0,0 +1,164 @@ +import React from 'react' +import { render, cleanup, within } from 'react-testing-library' +import Timeline from 'lib/Timeline' +import DateHeader from 'lib/headers/DateHeader' +import SidebarHeader from 'lib/headers/SidebarHeader' +import TimelineHeaders from 'lib/headers/TimelineHeaders' +import 'jest-dom/extend-expect' + +import moment from 'moment' + +import { items, groups } from '../../../__fixtures__/itemsAndGroups' + + +const defaultProps = { + groups, + items, + defaultTimeStart: moment('1995-12-25').add(-12, 'hour'), + defaultTimeEnd: moment('1995-12-25').add(12, 'hour') +} + +describe("Testing SidebarHeader Component", () => { + afterEach(cleanup) + // Testing The Example In The Docs + it("Given SidebarHeader When rendered Then it should shown correctly in the timeline", () => { + const { container } = render( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + {({ getRootProps }) => { + return
Right
+ }} +
+ + +
+
+ ) + + const { getByTestId } = within(container) + + expect(getByTestId('leftSidebarHeader')).toBeInTheDocument() + expect(getByTestId('rightSidebarHeader')).toBeInTheDocument() + + expect(getByTestId('leftSidebarHeader').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + expect(getByTestId('rightSidebarHeader').previousElementSibling).toHaveAttribute('data-testid', 'headerContainer') + }) + + it("Given SidebarHeader When passing no variant prop Then it should rendered above the left sidebar", () => { + const { getByTestId, getAllByTestId } = renderSidebarHeaderWithCustomValues() + expect(getByTestId('sidebarHeader')).toBeInTheDocument() + expect(getByTestId('sidebarHeader').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + expect(getAllByTestId('sidebarHeader')).toHaveLength(1) + + + }) + it("Given SidebarHeader When passing variant prop with left value Then it should rendered above the left sidebar", () => { + const { getByTestId, getAllByTestId } = renderSidebarHeaderWithCustomValues({ variant: "left" }) + expect(getByTestId('sidebarHeader')).toBeInTheDocument() + expect(getByTestId('sidebarHeader').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + expect(getAllByTestId('sidebarHeader')).toHaveLength(1) + }) + it("Given SidebarHeader When passing variant prop with right value Then it should rendered above the right sidebar", () => { + const { getByTestId, getAllByTestId } = renderSidebarHeaderWithCustomValues({ variant: "right" }) + expect(getByTestId('sidebarHeader')).toBeInTheDocument() + expect(getByTestId('sidebarHeader').previousElementSibling).toHaveAttribute('data-testid', 'headerContainer') + expect(getAllByTestId('sidebarHeader')).toHaveLength(1) + }) + + it("Given SidebarHeader When passing variant prop with unusual value Then it should rendered above the left sidebar by default", () => { + const { getByTestId } = renderSidebarHeaderWithCustomValues({ variant: "" }) + expect(getByTestId('sidebarHeader')).toBeInTheDocument() + expect(getByTestId('sidebarHeader').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + }) + + it("Given SidebarHeader When passing props to the props getter Then it should rendered correctly", () => { + const { getByTestId } = renderSidebarHeaderWithCustomValues({ props: { style: { width: 250 } } }) + const { width } = getComputedStyle(getByTestId("sidebarHeader")) + expect(width).toBe("250px") + }) + + it("Given SidebarHeader When passing children to it Then it should rendered correctly", () => { + const { getByText } = renderSidebarHeaderWithCustomValues() + expect(getByText("Should Be Rendred")).toBeInTheDocument() + }) + + it("Given sidebarheader When pass a variant and props Then it should render both correctly", () => { + const { getByTestId } = renderSidebarHeaderWithCustomValues({ props: { style: { width: 250 }, variant: "left" } }) + const { width } = getComputedStyle(getByTestId("sidebarHeader")) + expect(width).toBe("250px") + expect(getByTestId('sidebarHeader').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + }) + + it("Given two sidebarheaders When pass a variants and props Then it should render both correctly", () => { + const { getByText } = renderTwoSidebarHeadersWithCustomValues({ props: { style: { width: 250 } } }) + const { width: leftWidth } = getComputedStyle(getByText('LeftSideBar')) + const { width: rightWidth } = getComputedStyle(getByText('RightSideBar')) + expect(leftWidth).toBe("250px") + expect(rightWidth).toBe("250px") + expect(getByText('LeftSideBar').nextElementSibling).toHaveAttribute('data-testid', 'headerContainer') + expect(getByText('RightSideBar').previousElementSibling).toHaveAttribute('data-testid', 'headerContainer') + }) + + it('Given SidebarHeader When Pass an props obj to props renderer Then it should render it correctly', () => { + const { getByTestId } = renderSidebarHeaderWithCustomValues({ props: { 'aria-hidden': false } }) + expect(getByTestId("sidebarHeader")).toHaveAttribute('aria-hidden') + }) + +}) + + +function renderSidebarHeaderWithCustomValues({ variant = undefined, props, rightSidebarWidth } = {}) { + return render( + + + {({ getRootProps }, extraProps) => { + return
SidebarHeader +
Should Be Rendred
+
+ }} +
+ + +
+
+ ) +} + + +function renderTwoSidebarHeadersWithCustomValues({ props, rightSidebarWidth } = {}) { + return render( + + + {({ getRootProps }) => { + return
LeftSideBar +
Should Be Rendred
+
+ }} +
+ + {({ getRootProps }, props) => { + return
RightSideBar +
+ }} +
+ + +
+
+ ) +} + diff --git a/__tests__/components/Headers/TimelineHeader.test.js b/__tests__/components/Headers/TimelineHeader.test.js new file mode 100644 index 000000000..96ab239f0 --- /dev/null +++ b/__tests__/components/Headers/TimelineHeader.test.js @@ -0,0 +1,239 @@ +import { render, cleanup } from 'react-testing-library' +import Timeline from 'lib/Timeline' +import SidebarHeader from 'lib/headers/SidebarHeader' +import DateHeader from 'lib/headers/DateHeader' +import TimelineHeaders from 'lib/headers/TimelineHeaders' +import 'jest-dom/extend-expect' + +import React from 'react' +import moment from 'moment' + +import { items, groups } from '../../../__fixtures__/itemsAndGroups' +import { + visibleTimeStart, + visibleTimeEnd +} from '../../../__fixtures__/stateAndProps' + +const defaultProps = { + groups, + items, + defaultTimeStart: moment('1995-12-25').add(-12, 'hour'), + defaultTimeEnd: moment('1995-12-25').add(12, 'hour') +} + +describe('TimelineHeader', () => { + beforeEach(() => { + Element.prototype.getBoundingClientRect = jest.fn(() => { + return { + width: 1000, + height: 120, + top: 0, + left: 0, + bottom: 0, + right: 0, + } + }); + }) + afterEach(cleanup) + /** + * Testing The Default Functionality + */ + describe('renders default headers correctly', () => { + it('Given TimelineHeader When rendered Then it should render tow date headers by default', () => { + const { getAllByTestId, getByTestId } = renderDefaultTimeline() + expect(getAllByTestId('dateHeader')).toHaveLength(2) + expect(getByTestId('headerContainer').children).toHaveLength(2) + }) + it('Given TimelineHeader When rendered Then it should render a left sidebar by default', () => { + const { getByTestId } = renderDefaultTimeline() + expect(getByTestId("sidebarHeader")).toBeInTheDocument() + }) + it('Given TimelineHeader When pass a rightsidebarWidth Then it should render two sidebar headers', () => { + let rightSidebarWidth = 150; + const { getAllByTestId } = renderDefaultTimeline({ rightSidebarWidth }); + const sidebarHeaders = getAllByTestId('sidebarHeader') + + expect(sidebarHeaders).toHaveLength(2) + expect(sidebarHeaders[0]).toBeInTheDocument() + expect(sidebarHeaders[1]).toBeInTheDocument() + const { width } = getComputedStyle(sidebarHeaders[1]) + expect(width).toBe("150px") + + + }) + it('Given TimelineHeader When rendered Then it should render a two dateHeadrs default', () => { + const { getAllByTestId } = renderDefaultTimeline(); + const dateHeaders = getAllByTestId("dateHeader") + + expect(dateHeaders).toHaveLength(2) + expect(dateHeaders[1].childElementCount).toBeGreaterThan(dateHeaders[0].childElementCount) + + }) + + it("Given TimelineHeader When pass a left sidebar as a child Then it should render a left sidebar", () => { + const { getByTestId, getAllByTestId } = renderTimelineWithVariantSidebar({ variant: "left" }); + expect(getByTestId('sidebarHeader')).toBeInTheDocument(); + expect(getAllByTestId('sidebarHeader')).toHaveLength(1) + }) + it("Given TimelineHeader When pass a right sidebar as a child Then it should render a right sidebar", () => { + const { getByTestId, getAllByTestId } = renderTimelineWithVariantSidebar({ variant: "right" }); + expect(getByTestId('sidebarHeader')).toBeInTheDocument(); + expect(getAllByTestId('sidebarHeader')).toHaveLength(1) + }) + it("Given TimelineHeader When pass a left and right sidebars as children Then it should render a left and right sidebars", () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar(); + expect(getByTestId('left-header')).toBeInTheDocument(); + expect(getByTestId('right-header')).toBeInTheDocument(); + }) + + it("Given TimelineHeader When pass calendarHeaderStyle with overrided (overflow, width) Then it should not override the deaful values", () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar({ calendarHeaderStyle: { overflow: 'unset', width: 0 } }); + const headerContainer = getByTestId('headerContainer') + const { width, overflow } = getComputedStyle(headerContainer) + + + expect(overflow).not.toBe('unset') + expect(width).not.toBe("0px") + + }) + it("Given TimelineHeader When pass rootStyle with overrided (display, width) Then it should not override the deaful values", () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar({ style: { display: 'none', width: 0 } }); + const rootDiv = getByTestId('headerRootDiv') + const { width, display } = getComputedStyle(rootDiv) + + expect(display).not.toBe('none') + expect(width).not.toBe("0px") + + }) + it("Given TimelineHeader When pass calendarHeaderClassName Then it should be applied to the date header container", () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar({ calendarHeaderClassName: "testClassName" }); + expect(getByTestId("headerContainer")).toHaveClass("testClassName") + }) + }) + + it('Given TimelineHeader When rendered Then it should render the default styles of the date header container', () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar(); + const headerContainer = getByTestId('headerContainer') + const { overflow } = getComputedStyle(headerContainer) + expect(overflow).toBe('hidden') + // The JSDOM will not fire the calc css function + }) + + it('Given TimelineHeader When rendered Then it should render the default styles of the rootStyle', () => { + const { getByTestId } = renderTimelineWithLeftAndRightSidebar(); + const rootDiv = getByTestId('headerRootDiv') + const { width, display } = getComputedStyle(rootDiv) + + expect(display).toBe('flex') + expect(width).toBe("100%") + }) + + /** + * Testing The Example Provided In The Docs + */ + it('Given TimelineHeader When pass a headers as children Then it should render them correctly', () => { + const { getByText, rerender, queryByText } = render( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + {({ getRootProps }) => { + return ( +
+ Right +
+ ) + }} +
+ +
+
+ ) + expect(getByText('Left')).toBeInTheDocument() + expect(getByText('Right')).toBeInTheDocument() + rerender( + + + + {({ getRootProps }) => { + return
Left
+ }} +
+ +
+
+ ) + expect(queryByText('Right')).toBeNull() + }) +}) + +function renderDefaultTimeline(props = {}) { + const timelineProps = { + ...defaultProps, + ...props + } + return render() +} + +function renderTimelineWithVariantSidebar({ props, variant } = {}) { + const timelineProps = { + ...defaultProps, + ...props + } + return render( + + + + {({ getRootProps }) => { + return
Header
+ }} +
+
+
+ ) +} + + +function renderTimelineWithLeftAndRightSidebar({ props, calendarHeaderClassName, calendarHeaderStyle, style } = {}) { + + const timelineProps = { + ...defaultProps, + ...props + } + + return render( + + + + {({ getRootProps }) => { + return
Right
+ }} +
+ + {({ getRootProps }) => { + return
Left
+ }} +
+
+
+ ) +} + + + diff --git a/__tests__/components/Markers/CustomMarker.test.js b/__tests__/components/Markers/CustomMarker.test.js index 55a965b2f..e307780c3 100644 --- a/__tests__/components/Markers/CustomMarker.test.js +++ b/__tests__/components/Markers/CustomMarker.test.js @@ -4,6 +4,7 @@ import 'jest-dom/extend-expect' import TimelineMarkers from 'lib/markers/public/TimelineMarkers' import CustomMarker from 'lib/markers/public/CustomMarker' import { RenderWrapper } from 'test-utility/marker-renderer' +import { defaultKeys } from '../../../src/lib/default-config'; describe('CustomMarker', () => { afterEach(cleanup) @@ -67,7 +68,11 @@ describe('CustomMarker', () => { visibleTimeEnd, canvasTimeStart: visibleTimeStart - oneDay, canvasTimeEnd: visibleTimeEnd + oneDay, - canvasWidth + canvasWidth, + showPeriod: () => {}, + timelineWidth: 1000, + timelineUnit: 'day', + keys: defaultKeys, } const markerDate = now + oneDay / 2 diff --git a/__tests__/index.js b/__tests__/index.js index 3122950ba..b93609565 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -132,7 +132,7 @@ xdescribe('Timeline', () => { diff --git a/__tests__/test-utility/marker-renderer.js b/__tests__/test-utility/marker-renderer.js index 640dbb539..7fd787b01 100644 --- a/__tests__/test-utility/marker-renderer.js +++ b/__tests__/test-utility/marker-renderer.js @@ -2,6 +2,7 @@ import React from 'react' import TimelineMarkersRenderer from 'lib/markers/TimelineMarkersRenderer' import { TimelineMarkersProvider } from 'lib/markers/TimelineMarkersContext' import { TimelineStateProvider } from 'lib/timeline/TimelineStateContext' +import { defaultKeys } from '../../src/lib/default-config'; const oneDay = 1000 * 60 * 60 * 24 // eslint-disable-next-line @@ -15,7 +16,11 @@ export const RenderWrapper = ({ children, timelineState }) => { canvasTimeStart: visibleTimeStart - oneDay, canvasTimeEnd: visibleTimeEnd + oneDay, canvasWidth: 3000, - visibleWidth: 1000 + visibleWidth: 1000, + showPeriod:()=>{}, + timelineWidth:1000, + timelineUnit:'day', + keys: defaultKeys } timelineState = timelineState != null ? timelineState : defaultTimelineState diff --git a/__tests__/utils/calendar/__snapshots__/calculate-scroll-canvas.js.snap b/__tests__/utils/calendar/__snapshots__/calculate-scroll-canvas.js.snap index 0d4c752b1..757fcc868 100644 --- a/__tests__/utils/calendar/__snapshots__/calculate-scroll-canvas.js.snap +++ b/__tests__/utils/calendar/__snapshots__/calculate-scroll-canvas.js.snap @@ -18,7 +18,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 78.74857638888886, }, "id": "0", @@ -36,7 +36,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 245.4152430555556, }, "id": "5", @@ -54,7 +54,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 287.08190972222224, }, "id": "6", @@ -72,7 +72,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 172.1749884259259, }, "id": "1", @@ -90,7 +90,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 236.08273148148123, }, "id": "2", @@ -108,23 +108,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 236.08273148148146, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, "visibleTimeEnd": 1540634400000, "visibleTimeStart": 1540548000000, } @@ -148,7 +148,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 75.59863333333351, }, "id": "0", @@ -166,7 +166,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 235.5986333333335, }, "id": "5", @@ -184,7 +184,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 275.5986333333335, }, "id": "6", @@ -202,7 +202,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 165.28798888888878, }, "id": "1", @@ -220,7 +220,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 226.6394222222225, }, "id": "2", @@ -238,23 +238,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 226.6394222222225, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, "visibleTimeEnd": 1540591200000, "visibleTimeStart": 1540501200000, } diff --git a/__tests__/utils/calendar/__snapshots__/get-next-unit.js.snap b/__tests__/utils/calendar/__snapshots__/get-next-unit.js.snap new file mode 100644 index 000000000..b56de34c6 --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/get-next-unit.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getNextUnit unknown value to throw error 1`] = `"unit foo in not acceptable"`; diff --git a/__tests__/utils/calendar/__snapshots__/group-stack.js.snap b/__tests__/utils/calendar/__snapshots__/group-stack.js.snap index 6f22034d7..bf2b6c159 100644 --- a/__tests__/utils/calendar/__snapshots__/group-stack.js.snap +++ b/__tests__/utils/calendar/__snapshots__/group-stack.js.snap @@ -4,6 +4,6 @@ exports[`groupStack works as expected 1`] = ` Object { "groupHeight": 0, "itemTop": 7.5, - "verticalMargin": 37.5, + "verticalMargin": 18.75, } `; diff --git a/__tests__/utils/calendar/__snapshots__/stack-group.js.snap b/__tests__/utils/calendar/__snapshots__/stack-group.js.snap index bbe832509..9fe6769be 100644 --- a/__tests__/utils/calendar/__snapshots__/stack-group.js.snap +++ b/__tests__/utils/calendar/__snapshots__/stack-group.js.snap @@ -8,6 +8,20 @@ Object { `; exports[`stackGroup should stack list of items 1`] = ` +Object { + "groupHeight": 0, + "verticalMargin": 3.75, +} +`; + +exports[`stackGroup should stack list of items lines 1`] = ` +Object { + "groupHeight": 180, + "verticalMargin": 7.5, +} +`; + +exports[`stackGroup should stack list of items space 1`] = ` Object { "groupHeight": 0, "verticalMargin": 7.5, diff --git a/__tests__/utils/calendar/__snapshots__/stack-items.js.snap b/__tests__/utils/calendar/__snapshots__/stack-items.js.snap index 8d27dc42d..9d0000a46 100644 --- a/__tests__/utils/calendar/__snapshots__/stack-items.js.snap +++ b/__tests__/utils/calendar/__snapshots__/stack-items.js.snap @@ -16,7 +16,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 236.24572916666602, }, "id": "0", @@ -34,7 +34,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 736.245729166666, }, "id": "5", @@ -52,7 +52,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 861.245729166666, }, "id": "6", @@ -70,7 +70,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 516.5249652777784, }, "id": "1", @@ -88,7 +88,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 708.2481944444444, }, "id": "2", @@ -106,23 +106,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 625, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, } `; @@ -142,7 +142,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 111.24572916666602, }, "id": "0", @@ -160,7 +160,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 736.245729166666, }, "id": "5", @@ -178,7 +178,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 861.245729166666, }, "id": "6", @@ -196,7 +196,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 516.5249652777784, }, "id": "1", @@ -214,7 +214,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 708.2481944444444, }, "id": "2", @@ -232,23 +232,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 625, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, } `; @@ -268,7 +268,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 361.245729166666, }, "id": "0", @@ -286,7 +286,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 736.245729166666, }, "id": "5", @@ -304,7 +304,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 861.245729166666, }, "id": "6", @@ -322,7 +322,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 516.5249652777784, }, "id": "1", @@ -340,7 +340,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 708.2481944444444, }, "id": "2", @@ -358,23 +358,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 625, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, } `; @@ -394,7 +394,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 236.24572916666602, }, "id": "0", @@ -412,7 +412,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 736.245729166666, }, "id": "5", @@ -430,7 +430,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 861.245729166666, }, "id": "6", @@ -448,7 +448,7 @@ Object { "index": 0, }, "stack": true, - "top": 37.5, + "top": 33.75, "width": 516.5249652777784, }, "id": "1", @@ -466,7 +466,7 @@ Object { "index": 0, }, "stack": true, - "top": 7.5, + "top": 3.75, "width": 708.2481944444444, }, "id": "2", @@ -484,23 +484,23 @@ Object { "index": 2, }, "stack": true, - "top": 105, + "top": 93.75, "width": 625, }, "id": "3", }, ], "groupHeights": Array [ - 67.5, + 60, + 30, 30, - 37.5, ], "groupTops": Array [ 0, - 67.5, - 97.5, + 60, + 90, ], - "height": 135, + "height": 120, } `; diff --git a/__tests__/utils/calendar/get-next-unit.js b/__tests__/utils/calendar/get-next-unit.js index 40b19bb81..306e4353f 100644 --- a/__tests__/utils/calendar/get-next-unit.js +++ b/__tests__/utils/calendar/get-next-unit.js @@ -23,12 +23,11 @@ describe('getNextUnit', () => { const result = getNextUnit('month') expect(result).toBe('year') }) - it('year to empty string', () => { + it('year to year', () => { const result = getNextUnit('year') - expect(result).toBe('') + expect(result).toBe('year') }) - it('unknown value to empty string', () => { - const result = getNextUnit('foo') - expect(result).toBe('') + it('unknown value to throw error', () => { + expect(() => getNextUnit('foo')).toThrowErrorMatchingSnapshot() }) }) diff --git a/__tests__/utils/calendar/stack-group.js b/__tests__/utils/calendar/stack-group.js index be91fa399..678cf455d 100644 --- a/__tests__/utils/calendar/stack-group.js +++ b/__tests__/utils/calendar/stack-group.js @@ -2,8 +2,11 @@ import { stackGroup } from 'lib/utility/calendar' import { dimensionItems } from '../../../__fixtures__/groupOrderAndItemDimentions' describe('stackGroup', ()=>{ - it('should stack list of items', ()=>{ - expect(stackGroup(dimensionItems, true, 30, 0)).toMatchSnapshot() + it('should stack list of items space', ()=>{ + expect(stackGroup(dimensionItems, 'space', 30, 0)).toMatchSnapshot() + }) + it('should stack list of items lines', ()=>{ + expect(stackGroup(dimensionItems, 'lines', 30, 0)).toMatchSnapshot() }) it('should not stack list of items', ()=>{ expect(stackGroup(dimensionItems, false, 30, 0)).toMatchSnapshot() diff --git a/demo/app/demo-custom-items/index.js b/demo/app/demo-custom-items/index.js index 73a3e8d10..bf598b8a9 100644 --- a/demo/app/demo-custom-items/index.js +++ b/demo/app/demo-custom-items/index.js @@ -207,7 +207,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} lineHeight={40} showCursorLine diff --git a/demo/app/demo-element-resize/index.js b/demo/app/demo-element-resize/index.js index ff25558fe..ca74a17d2 100644 --- a/demo/app/demo-element-resize/index.js +++ b/demo/app/demo-element-resize/index.js @@ -69,7 +69,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} resizeDetector={containerResizeDetector} defaultTimeStart={defaultTimeStart} diff --git a/demo/app/demo-headers/index.js b/demo/app/demo-headers/index.js new file mode 100644 index 000000000..9f8fc2498 --- /dev/null +++ b/demo/app/demo-headers/index.js @@ -0,0 +1,411 @@ +/* eslint-disable no-console */ +import React, { Component } from 'react' +import moment from 'moment' + +import Timeline, { + TimelineMarkers, + TodayMarker, + CustomMarker, + CursorMarker, + SidebarHeader, + CustomHeader, + TimelineHeaders, + DateHeader, + ItemHeader +} from 'react-calendar-timeline' + +import generateFakeData from '../generate-fake-data' + +var minTime = moment() + .add(-6, 'months') + .valueOf() +var maxTime = moment() + .add(6, 'months') + .valueOf() + +var keys = { + groupIdKey: 'id', + groupTitleKey: 'title', + groupRightTitleKey: 'rightTitle', + itemIdKey: 'id', + itemTitleKey: 'title', + itemDivTitleKey: 'title', + itemGroupKey: 'group', + itemTimeStartKey: 'start', + itemTimeEndKey: 'end' +} + +export default class App extends Component { + constructor(props) { + super(props) + + const { groups, items } = generateFakeData() + const {items: headerItems } = generateFakeData(2, 5, 1) + const defaultTimeStart = moment() + .startOf('day') + .toDate() + const defaultTimeEnd = moment() + .startOf('day') + .add(1, 'day') + .toDate() + + this.state = { + groups, + items, + defaultTimeStart, + defaultTimeEnd, + format: false, + showHeaders: false, + headerItems, + } + } + + handleClick = () => { + this.setState({ format: true }) + } + + handleCanvasClick = (groupId, time) => { + this.setState(state => ({ + groups: state.groups + })) + console.log('Canvas clicked', groupId, moment(time).format()) + } + + handleCanvasDoubleClick = (groupId, time) => { + console.log('Canvas double clicked', groupId, moment(time).format()) + } + + handleCanvasContextMenu = (group, time) => { + console.log('Canvas context menu', group, moment(time).format()) + } + + handleItemClick = (itemId, _, time) => { + console.log('Clicked: ' + itemId, moment(time).format()) + } + + handleItemSelect = (itemId, _, time) => { + console.log('Selected: ' + itemId, moment(time).format()) + this.setState((state)=>({ + groups: state.groups.filter(_ => Math.random() > 0.5 ) + })) + } + + handleItemDoubleClick = (itemId, _, time) => { + console.log('Double Click: ' + itemId, moment(time).format()) + } + + handleItemContextMenu = (itemId, _, time) => { + console.log('Context Menu: ' + itemId, moment(time).format()) + } + + handleItemMove = (itemId, dragTime, newGroupOrder) => { + const { items, groups } = this.state + + const group = groups[newGroupOrder] + + this.setState({ + items: items.map( + item => + item.id === itemId + ? Object.assign({}, item, { + start: dragTime, + end: dragTime + (item.end - item.start), + group: group.id + }) + : item + ) + }) + + console.log('Moved', itemId, dragTime, newGroupOrder) + } + + handleItemResize = (itemId, time, edge) => { + const { items } = this.state + + this.setState({ + items: items.map( + item => + item.id === itemId + ? Object.assign({}, item, { + start: edge === 'left' ? time : item.start, + end: edge === 'left' ? item.end : time + }) + : item + ) + }) + + console.log('Resized', itemId, time, edge) + } + + // this limits the timeline to -6 months ... +6 months + handleTimeChange = (visibleTimeStart, visibleTimeEnd, updateScrollCanvas) => { + if (visibleTimeStart < minTime && visibleTimeEnd > maxTime) { + updateScrollCanvas(minTime, maxTime) + } else if (visibleTimeStart < minTime) { + updateScrollCanvas(minTime, minTime + (visibleTimeEnd - visibleTimeStart)) + } else if (visibleTimeEnd > maxTime) { + updateScrollCanvas(maxTime - (visibleTimeEnd - visibleTimeStart), maxTime) + } else { + updateScrollCanvas(visibleTimeStart, visibleTimeEnd) + } + } + + moveResizeValidator = (action, item, time) => { + if (time < new Date().getTime()) { + var newTime = + Math.ceil(new Date().getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000) + return newTime + } + + return time + } + + handleClickChangeHeaders = () => { + this.setState(state => ({ + showHeaders: !state.showHeaders + })) + } + + render() { + const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state + + return ( +
+ + + Above The Left
} + canMove + canResize="right" + canSelect + itemsSorted + itemTouchSendsClick={false} + itemHeightRatio={0.75} + defaultTimeStart={defaultTimeStart} + defaultTimeEnd={defaultTimeEnd} + onCanvasClick={this.handleCanvasClick} + onCanvasDoubleClick={this.handleCanvasDoubleClick} + onCanvasContextMenu={this.handleCanvasContextMenu} + onItemClick={this.handleItemClick} + onItemSelect={this.handleItemSelect} + onItemContextMenu={this.handleItemContextMenu} + onItemMove={this.handleItemMove} + onItemResize={this.handleItemResize} + onItemDoubleClick={this.handleItemDoubleClick} + onTimeChange={this.handleTimeChange} + // moveResizeValidator={this.moveResizeValidator} + rightSidebarWidth={150} + rightSidebarContent={
Above The Right
} + stackItems="space" + > + + + {({ getRootProps }) => { + return
Left
+ }} +
+ + {({ getRootProps }) => { + return
Right
+ }} +
+ { + return ( +
+ {item.title} +
+ ) + }} + /> + + + + + + {( + { + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }, + props + ) => { + return ( +
+ {intervals.map(interval => { + const intervalStyle = { + // height: 30, + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer', + backgroundColor: 'Turquoise', + color: 'white' + } + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > +
+ {interval.startTime.format('YYYY')} +
+
+ ) + })} +
+ ) + }} +
+ + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }) => { + return ( +
+ {intervals.map(interval => { + const intervalStyle = { + // height: 30, + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer', + backgroundColor: 'indianred', + color: 'white' + } + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > +
+ {interval.startTime.format('MM/DD')} +
+
+ ) + })} +
+ ) + }} +
+ + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }) => { + return ( +
+ {intervals.map(interval => { + const intervalStyle = { + lineHeight: '30px', + textAlign: 'center', + borderLeft: '1px solid black', + cursor: 'pointer' + } + return ( +
{ + showPeriod(interval.startTime, interval.endTime) + }} + {...getIntervalProps({ + interval, + style: intervalStyle + })} + > + {interval.startTime.format('HH')} +
+ ) + })} +
+ ) + }} +
+ { + return ( +
+ {intervalContext.intervalText} +
+ ) + }} + /> + {this.state.showHeaders + ? [ + , + + ] + : null} +
+ + + + + {({ styles }) => { + const newStyles = { ...styles, backgroundColor: 'blue' } + return
+ }} + + + + +
+ ) + } +} diff --git a/demo/app/demo-linked-timelines/index.js b/demo/app/demo-linked-timelines/index.js index 8ec0af119..b68a3a5d6 100644 --- a/demo/app/demo-linked-timelines/index.js +++ b/demo/app/demo-linked-timelines/index.js @@ -70,7 +70,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} visibleTimeStart={visibleTimeStart} visibleTimeEnd={visibleTimeEnd} @@ -94,7 +94,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} visibleTimeStart={visibleTimeStart} visibleTimeEnd={visibleTimeEnd} diff --git a/demo/app/demo-main/index.js b/demo/app/demo-main/index.js index e18cc0b40..c72697609 100644 --- a/demo/app/demo-main/index.js +++ b/demo/app/demo-main/index.js @@ -4,9 +4,13 @@ import moment from 'moment' import Timeline, { TimelineMarkers, + TimelineHeaders, TodayMarker, CustomMarker, - CursorMarker + CursorMarker, + CustomHeader, + SidebarHeader, + DateHeader } from 'react-calendar-timeline' import generateFakeData from '../generate-fake-data' @@ -34,15 +38,151 @@ export default class App extends Component { constructor(props) { super(props) - const { groups, items } = generateFakeData() - const defaultTimeStart = moment() + const items = [ + // { + // id: '4', + // group: '1', + // title: + // '4', + // start: 1550538000000, + // end: 1550560702870, + // canResize: false, + // className: '', + // bgColor: 'rgba(209, 154, 237, 0.6)', + // selectedBgColor: 'rgba(209, 154, 237, 1)', + // color: '#55077c', + // itemProps: { + // 'data-tip': + // 'The AGP bus is down, calculate the multi-byte alarm so we can generate the THX driver!' + // } + // }, + // { + // id: '5', + // group: '1', + // title: + // '5', + // start: 1550549700000, + // end: 1550569678295, + // canMove: true, + // canResize: 'both', + // className: '', + // bgColor: 'rgba(235, 171, 242, 0.6)', + // selectedBgColor: 'rgba(235, 171, 242, 1)', + // color: '#ad0fbf', + // itemProps: { + // 'data-tip': + // "I'll parse the virtual AI monitor, that should microchip the SDD circuit!" + // } + // }, + { + id: '1', + group: '1', + title: + '1', + start: 1550562300000, + end: 1550566800000, + canMove: true, + canResize: 'both', + className: '', + bgColor: 'rgba(119, 126, 249, 0.6)', + selectedBgColor: 'rgba(119, 126, 249, 1)', + color: '#010887', + itemProps: { + 'data-tip': + 'The IB alarm is down, parse the virtual driver so we can copy the COM bus!' + } + }, + { + id: '3', + group: '2', + title: '3', + start: 1550538000000, + end: 1550557157371, + canResize: false, + className: '', + bgColor: 'rgba(184, 141, 239, 0.6)', + selectedBgColor: 'rgba(184, 141, 239, 1)', + color: '#3c0584', + itemProps: { + 'data-tip': + 'Try to synthesize the AI circuit, maybe it will calculate the cross-platform interface!' + } + }, + + + { + id: '6', + group: '2', + title: '6', + start: 1550551500000, + end: 1550571478295, + canResize: false, + className: '', + bgColor: 'rgba(252, 191, 243, 0.6)', + selectedBgColor: 'rgba(252, 191, 243, 1)', + color: '#ea15ca', + itemProps: { + 'data-tip': 'We need to input the haptic USB panel!' + } + }, + { + id: '7', + group: '2', + title: + "7", + start: 1550539800000, + end: 1550559571292, + canResize: false, + className: '', + bgColor: 'rgba(247, 116, 197, 0.6)', + selectedBgColor: 'rgba(247, 116, 197, 1)', + color: '#db0288', + itemProps: { + 'data-tip': + "bypassing the driver won't do anything, we need to compress the haptic XML monitor!" + } + }, + { + id: '8', + group: '2', + title: '8', + start: 1550535300000, + end: 1550550380987, + canResize: false, + className: '', + bgColor: 'rgba(244, 129, 173, 0.6)', + selectedBgColor: 'rgba(244, 129, 173, 1)', + color: '#99043d', + itemProps: { + 'data-tip': + "copying the system won't do anything, we need to quantify the neural SCSI protocol!" + } + } + ] + + const groups = [ + { + id: '1', + title: 'Dee', + rightTitle: 'Kuhn', + label: 'Label Amari', + bgColor: '#c0d0f9' + }, + { + id: '2', + title: 'Brennon', + rightTitle: 'Cronin', + label: 'Label Maude', + bgColor: '#777ef9' + } + ] + + const defaultTimeStart = moment('19/2/2019', 'dd/mm/yyyy') .startOf('day') .toDate() - const defaultTimeEnd = moment() - .startOf('day') - .add(1, 'day') + const defaultTimeEnd = moment('19/2/2019', 'dd/mm/yyyy') + .endOf('day') .toDate() - this.state = { groups, items, @@ -141,6 +281,10 @@ export default class App extends Component { return time } + handleMoveUpdate= (...args)=> { + console.log(args) + } + render() { const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state @@ -156,7 +300,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems='lines' itemHeightRatio={0.75} defaultTimeStart={defaultTimeStart} defaultTimeEnd={defaultTimeEnd} @@ -171,6 +315,8 @@ export default class App extends Component { onItemDoubleClick={this.handleItemDoubleClick} onTimeChange={this.handleTimeChange} moveResizeValidator={this.moveResizeValidator} + width={1000} + onUpdateMove={this.handleMoveUpdate} > diff --git a/demo/app/demo-performance/index.js b/demo/app/demo-performance/index.js index 97c940d00..147db94a9 100644 --- a/demo/app/demo-performance/index.js +++ b/demo/app/demo-performance/index.js @@ -168,7 +168,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} // resizeDetector={containerResizeDetector} diff --git a/demo/app/demo-renderers/index.js b/demo/app/demo-renderers/index.js index 898dfb426..7a3229647 100644 --- a/demo/app/demo-renderers/index.js +++ b/demo/app/demo-renderers/index.js @@ -187,7 +187,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} // resizeDetector={containerResizeDetector} diff --git a/demo/app/demo-sticky-header/index.js b/demo/app/demo-sticky-header/index.js index adb26d533..d0685849e 100644 --- a/demo/app/demo-sticky-header/index.js +++ b/demo/app/demo-sticky-header/index.js @@ -65,7 +65,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} resizeDetector={containerResizeDetector} defaultTimeStart={defaultTimeStart} @@ -113,7 +113,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} resizeDetector={containerResizeDetector} defaultTimeStart={defaultTimeStart} diff --git a/demo/app/demo-tree-groups/index.js b/demo/app/demo-tree-groups/index.js index 63cb3ea2e..41dafbcea 100644 --- a/demo/app/demo-tree-groups/index.js +++ b/demo/app/demo-tree-groups/index.js @@ -111,7 +111,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} defaultTimeStart={defaultTimeStart} defaultTimeEnd={defaultTimeEnd} diff --git a/demo/app/demo-vertical-classes/index.js b/demo/app/demo-vertical-classes/index.js index c8f0fbc5f..8d023e3b5 100644 --- a/demo/app/demo-vertical-classes/index.js +++ b/demo/app/demo-vertical-classes/index.js @@ -93,7 +93,7 @@ export default class App extends Component { canSelect itemsSorted itemTouchSendsClick={false} - stackItems + stackItems="space" itemHeightRatio={0.75} defaultTimeStart={defaultTimeStart} defaultTimeEnd={defaultTimeEnd} diff --git a/demo/app/index.js b/demo/app/index.js index 79b1f118c..fbe44aeab 100644 --- a/demo/app/index.js +++ b/demo/app/index.js @@ -14,7 +14,8 @@ const demos = { stickyHeader: require('./demo-sticky-header').default, renderers: require('./demo-renderers').default, verticalClasses: require('./demo-vertical-classes').default, - customItems: require('./demo-custom-items').default + customItems: require('./demo-custom-items').default, + customHeaders: require('./demo-headers').default, } // A simple component that shows the pathname of the current location diff --git a/demo/app/styles.scss b/demo/app/styles.scss index f9632ab87..c80509aec 100644 --- a/demo/app/styles.scss +++ b/demo/app/styles.scss @@ -65,3 +65,17 @@ body { z-index: 999; background-color: darkgray !important; } + +.sticky { + position: sticky; + position: -webkit-sticky; + left: 45%; + display: inline-block; + border-radius: 2px; + padding: 0 6px; + height: 100%; +} + +.header-background { + background: azure; +} diff --git a/package.json b/package.json index 4837be6cb..7242ece7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-calendar-timeline", - "version": "0.23.0", + "name": "@r365/react-calendar-timeline", + "version": "0.25.0-beta.22", "description": "react calendar timeline", "main": "lib/index.js", "scripts": { @@ -18,10 +18,10 @@ "lib", "src" ], - "homepage": "https://github.com/namespace-ee/react-calendar-timeline", + "homepage": "https://github.com/FoothillSolutions/react-calendar-timeline", "repository": { "type": "git", - "url": "https://github.com/namespace-ee/react-calendar-timeline.git" + "url": "https://github.com/FoothillSolutions/react-calendar-timeline.git" }, "author": "Marius Andra ", "contributors": [ @@ -92,7 +92,8 @@ "dependencies": { "create-react-context": "^0.2.2", "element-resize-detector": "^1.1.12", - "lodash.isequal": "^4.5.0" + "lodash.isequal": "^4.5.0", + "memoize-one": "^4.0.3" }, "peerDependencies": { "interactjs": "^1.3.4", @@ -126,7 +127,7 @@ "eslint-plugin-standard": "^2.0.1", "faker": "^4.1.0", "interactjs": "^1.3.4", - "jest": "^23.1.0", + "jest": "^23.6.0", "jest-dom": "^1.12.1", "jest-watch-typeahead": "^0.1.0", "jsdom": "^11.5.1", diff --git a/src/index.js b/src/index.js index 15936d2da..62f4c62e4 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,11 @@ export { export { default as TodayMarker } from './lib/markers/public/TodayMarker' export { default as CustomMarker } from './lib/markers/public/CustomMarker' export { default as CursorMarker } from './lib/markers/public/CursorMarker' - +export { default as TimelineHeaders } from './lib/headers/TimelineHeaders' +export {default as SidebarHeader} from './lib/headers/SidebarHeader' +export {default as CustomHeader} from './lib/headers/CustomHeader' +export {default as DateHeader} from './lib/headers/DateHeader' +export {TimelineStateConsumer} from './lib/timeline/TimelineStateContext' +export {default as ItemHeader} from './lib/headers/ItemHeader' +export * from './lib/utility/calendar' export default Timeline diff --git a/src/lib/Timeline.js b/src/lib/Timeline.js index 4d30fd4ff..e059451e4 100644 --- a/src/lib/Timeline.js +++ b/src/lib/Timeline.js @@ -5,7 +5,6 @@ import moment from 'moment' import Items from './items/Items' import InfoLabel from './layout/InfoLabel' import Sidebar from './layout/Sidebar' -import Header from './layout/Header' import Columns from './columns/Columns' import GroupRows from './row/GroupRows' import ScrollElement from './scroll/ScrollElement' @@ -31,15 +30,17 @@ import { } from './default-config' import { TimelineStateProvider } from './timeline/TimelineStateContext' import { TimelineMarkersProvider } from './markers/TimelineMarkersContext' +import { TimelineHeadersProvider } from './headers/HeadersContext' +import TimelineHeaders from './headers/TimelineHeaders' +import DateHeader from './headers/DateHeader' +import SidebarHeader from './headers/SidebarHeader' export default class ReactCalendarTimeline extends Component { static propTypes = { groups: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, items: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, sidebarWidth: PropTypes.number, - sidebarContent: PropTypes.node, rightSidebarWidth: PropTypes.number, - rightSidebarContent: PropTypes.node, dragSnap: PropTypes.number, minResizeWidth: PropTypes.number, stickyOffset: PropTypes.number, @@ -60,7 +61,7 @@ export default class ReactCalendarTimeline extends Component { useResizeHandle: PropTypes.bool, canSelect: PropTypes.bool, - stackItems: PropTypes.bool, + stackItems: PropTypes.oneOf([false,'space', 'lines']), traditionalZoom: PropTypes.bool, @@ -159,7 +160,9 @@ export default class ReactCalendarTimeline extends Component { verticalLineClassNamesForTime: PropTypes.func, - children: PropTypes.node + children: PropTypes.node, + width: PropTypes.number, + onUpdateMove: PropTypes.func, } static defaultProps = { @@ -236,7 +239,8 @@ export default class ReactCalendarTimeline extends Component { headerLabelFormats: defaultHeaderLabelFormats, subHeaderLabelFormats: defaultSubHeaderLabelFormats, - selected: null + selected: null, + onUpdateMove: null, } static childContextTypes = { @@ -272,6 +276,10 @@ export default class ReactCalendarTimeline extends Component { constructor(props) { super(props) + this.getSelected = this.getSelected.bind(this) + this.hasSelectedItem = this.hasSelectedItem.bind(this) + this.isItemSelected= this.isItemSelected.bind(this) + let visibleTimeStart = null let visibleTimeEnd = null @@ -291,7 +299,7 @@ export default class ReactCalendarTimeline extends Component { const [canvasTimeStart, canvasTimeEnd] = getCanvasBoundariesFromVisibleTime(visibleTimeStart, visibleTimeEnd) this.state = { - width: 1000, + width: this.props.width? this.props.width: 1000, visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd, canvasTimeStart: canvasTimeStart, @@ -334,7 +342,11 @@ export default class ReactCalendarTimeline extends Component { } componentDidMount() { - this.resize(this.props) + if(!this.props.width) {this.resize(this.props)} + else { + this.scrollComponent.scrollLeft = this.props.width + this.scrollHeaderRef.scrollLeft = this.props.width + } if (this.props.resizeDetector && this.props.resizeDetector.addListener) { this.props.resizeDetector.addListener(this) @@ -358,20 +370,26 @@ export default class ReactCalendarTimeline extends Component { visibleTimeStart, visibleTimeEnd, items, - groups + groups, } = nextProps + + // if the items or groups have changed we must re-render + const forceUpdate = items !== prevState.items || groups !== prevState.groups || nextProps.width!==prevState.width + + //get width from prop is passed or from state if not passed by user + const width = nextProps.width? nextProps.width : prevState.width + // This is a gross hack pushing items and groups in to state only to allow // For the forceUpdate check - let derivedState = {items, groups} + let derivedState = {items, groups, width} - // if the items or groups have changed we must re-render - const forceUpdate = items !== prevState.items || groups !== prevState.groups // We are a controlled component if (visibleTimeStart && visibleTimeEnd) { // Get the new canvas position - Object.assign(derivedState, + Object.assign( + derivedState, calculateScrollCanvas( visibleTimeStart, visibleTimeEnd, @@ -380,10 +398,11 @@ export default class ReactCalendarTimeline extends Component { groups, nextProps, prevState - )) + ) + ) } else if (forceUpdate) { // Calculate new item stack position as canvas may have changed - const canvasWidth = getCanvasWidth(prevState.width) + const canvasWidth = getCanvasWidth(width) Object.assign(derivedState, stackTimelineItems( items, @@ -400,7 +419,7 @@ export default class ReactCalendarTimeline extends Component { prevState.dragTime, prevState.resizingEdge, prevState.resizeTime, - prevState.newGroupOrder + prevState.newGroupOrder, )) } @@ -410,20 +429,29 @@ export default class ReactCalendarTimeline extends Component { componentDidUpdate(prevProps, prevState) { const newZoom = this.state.visibleTimeEnd - this.state.visibleTimeStart const oldZoom = prevState.visibleTimeEnd - prevState.visibleTimeStart - + // are we changing zoom? Report it! if (this.props.onZoom && newZoom !== oldZoom) { - this.props.onZoom(this.getTimelineContext()) + this.props.onZoom(this.getTimelineContext()) } // The bounds have changed? Report it! - if (this.props.onBoundsChange && this.state.canvasTimeStart !== prevState.canvasTimeStart) { - this.props.onBoundsChange(this.state.canvasTimeStart, this.state.canvasTimeStart + newZoom * 3) + if ( + this.props.onBoundsChange && + this.state.canvasTimeStart !== prevState.canvasTimeStart + ) { + this.props.onBoundsChange( + this.state.canvasTimeStart, + this.state.canvasTimeStart + newZoom * 3 + ) } // Check the scroll is correct const scrollLeft = Math.round( - this.state.width * (this.state.visibleTimeStart - this.state.canvasTimeStart) / newZoom) + (this.state.width * + (this.state.visibleTimeStart - this.state.canvasTimeStart)) / + newZoom + ) if (this.scrollComponent.scrollLeft !== scrollLeft) { this.scrollComponent.scrollLeft = scrollLeft } @@ -438,7 +466,7 @@ export default class ReactCalendarTimeline extends Component { width: containerWidth, } = this.container.getBoundingClientRect() - let width = containerWidth - props.sidebarWidth - props.rightSidebarWidth + let width = props.width? props.width : containerWidth - props.sidebarWidth - props.rightSidebarWidth const canvasWidth = getCanvasWidth(width) const { dimensionItems, height, groupHeights, groupTops } = stackTimelineItems( props.items, @@ -468,11 +496,11 @@ export default class ReactCalendarTimeline extends Component { groupHeights, groupTops, }) - + this.scrollComponent.scrollLeft = width - this.headerRef.scrollLeft = width + this.scrollHeaderRef.scrollLeft = width } - + onScroll = scrollX => { const width = this.state.width let newScrollX = scrollX @@ -485,14 +513,14 @@ export default class ReactCalendarTimeline extends Component { newScrollX -= width } - this.headerRef.scrollLeft = newScrollX + this.scrollHeaderRef.scrollLeft = newScrollX this.scrollComponent.scrollLeft = newScrollX const canvasTimeStart = this.state.canvasTimeStart const zoom = this.state.visibleTimeEnd - this.state.visibleTimeStart - - const visibleTimeStart = canvasTimeStart + zoom * scrollX / width + + const visibleTimeStart = canvasTimeStart + (zoom * scrollX) / width if ( this.state.visibleTimeStart !== visibleTimeStart || @@ -506,7 +534,6 @@ export default class ReactCalendarTimeline extends Component { } } - // called when the visible time changes updateScrollCanvas = ( visibleTimeStart, @@ -517,17 +544,19 @@ export default class ReactCalendarTimeline extends Component { ) => { this.setState( calculateScrollCanvas( - visibleTimeStart, - visibleTimeEnd, - forceUpdateDimensions, - items, - groups, - this.props, - this.state)) + visibleTimeStart, + visibleTimeEnd, + forceUpdateDimensions, + items, + groups, + this.props, + this.state + ) + ) } handleWheelZoom = (speed, xPosition, deltaY) => { - this.changeZoom(1.0 + speed * deltaY / 500, xPosition / this.state.width) + this.changeZoom(1.0 + (speed * deltaY) / 500, xPosition / this.state.width) } changeZoom = (scale, offset = 0.5) => { @@ -548,31 +577,16 @@ export default class ReactCalendarTimeline extends Component { ) } - showPeriod = (from, unit) => { + showPeriod = (from, to) => { let visibleTimeStart = from.valueOf() - let visibleTimeEnd = moment(from) - .add(1, unit) - .valueOf() - let zoom = visibleTimeEnd - visibleTimeStart + let visibleTimeEnd = to.valueOf() + let zoom = visibleTimeEnd - visibleTimeStart // can't zoom in more than to show one hour if (zoom < 360000) { return } - // clicked on the big header and already focused here, zoom out - if ( - unit !== 'year' && - this.state.visibleTimeStart === visibleTimeStart && - this.state.visibleTimeEnd === visibleTimeEnd - ) { - let nextUnit = getNextUnit(unit) - - visibleTimeStart = from.startOf(nextUnit).valueOf() - visibleTimeEnd = moment(visibleTimeStart).add(1, nextUnit) - zoom = visibleTimeEnd - visibleTimeStart - } - this.props.onTimeChange( visibleTimeStart, visibleTimeStart + zoom, @@ -582,7 +596,7 @@ export default class ReactCalendarTimeline extends Component { selectItem = (item, clickType, e) => { if ( - this.state.selectedItem === item || + this.isItemSelected(item) || (this.props.itemTouchSendsClick && clickType === 'touch') ) { if (item && this.props.onItemClick) { @@ -661,7 +675,9 @@ export default class ReactCalendarTimeline extends Component { dragItem = (item, dragTime, newGroupOrder) => { let newGroup = this.props.groups[newGroupOrder] const keys = this.props.keys - + if(this.props.onUpdateMove){ + this.props.onUpdateMove(item, dragTime, newGroupOrder, 'move') + } this.setState({ draggingItem: item, dragTime: dragTime, @@ -678,6 +694,9 @@ export default class ReactCalendarTimeline extends Component { } resizingItem = (item, resizeTime, edge) => { + if(this.props.onUpdateMove){ + this.props.onUpdateMove(item, resizeTime, undefined, 'resize', edge) + } this.setState({ resizingItem: item, resizingEdge: edge, @@ -716,7 +735,7 @@ export default class ReactCalendarTimeline extends Component { handleRowClick = (e, rowIndex) => { // shouldnt this be handled by the user, as far as when to deselect an item? - if (this.state.selectedItem) { + if (this.hasSelectedItem()) { this.selectItem(null) } @@ -767,7 +786,9 @@ export default class ReactCalendarTimeline extends Component { clickTolerance={this.props.clickTolerance} onRowClick={this.handleRowClick} onRowDoubleClick={this.handleRowDoubleClick} - horizontalLineClassNamesForGroup={this.props.horizontalLineClassNamesForGroup} + horizontalLineClassNamesForGroup={ + this.props.horizontalLineClassNamesForGroup + } onRowContextClick={this.handleScrollContextMenu} /> ) @@ -825,52 +846,13 @@ export default class ReactCalendarTimeline extends Component { } else if (this.state.resizeTime) { label = moment(this.state.resizeTime).format('LLL') } - + return label ? : undefined } handleHeaderRef = el => { - this.headerRef = el - this.props.headerRef(el) - } - - handleScrollHeaderRef = el => { this.scrollHeaderRef = el - } - - header( - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - minUnit, - timeSteps, - headerLabelGroupHeight, - headerLabelHeight - ) { - return ( -
0} - canvasTimeEnd={canvasTimeEnd} - canvasWidth={canvasWidth} - minUnit={minUnit} - timeSteps={timeSteps} - headerLabelGroupHeight={headerLabelGroupHeight} - headerLabelHeight={headerLabelHeight} - width={this.state.width} - stickyOffset={this.props.stickyOffset} - stickyHeader={this.props.stickyHeader} - showPeriod={this.showPeriod} - headerLabelFormats={this.props.headerLabelFormats} - subHeaderLabelFormats={this.props.subHeaderLabelFormats} - headerRef={this.handleHeaderRef} - scrollHeaderRef={this.handleScrollHeaderRef} - leftSidebarWidth={this.props.sidebarWidth} - rightSidebarWidth={this.props.rightSidebarWidth} - leftSidebarHeader={this.props.sidebarContent} - rightSidebarHeader={this.props.rightSidebarContent} - /> - ) + this.props.headerRef(el) } sidebar(height, groupHeights) { @@ -906,6 +888,7 @@ export default class ReactCalendarTimeline extends Component { ) } + groups childrenWithProps( canvasTimeStart, canvasTimeEnd, @@ -941,21 +924,64 @@ export default class ReactCalendarTimeline extends Component { keys: this.props.keys, groupHeights: groupHeights, groupTops: groupTops, - selected: - this.state.selectedItem && !this.props.selected - ? [this.state.selectedItem] - : this.props.selected || [], + selected: this.getSelected(), height: height, headerHeight: headerHeight, minUnit: minUnit, timeSteps: timeSteps } - return React.Children.map(childArray, child => - React.cloneElement(child, childProps) + return React.Children.map(childArray, child => { + if (child.type !== TimelineHeaders) { + return React.cloneElement(child, childProps) + } else { + return null + } + }) + } + + renderHeaders = () => { + if (this.props.children) { + let headerRenderer + React.Children.map(this.props.children, child => { + if (child.type === TimelineHeaders) { + headerRenderer = child + } + }) + if (headerRenderer) { + return headerRenderer + } + } + return ( + + + + + {this.props.rightSidebarWidth ? : null} + ) } + getScrollElementRef = el => { + this.props.scrollRef(el) + this.scrollComponent = el + } + getSelected() { + return this.state.selectedItem && !this.props.selected + ? [this.state.selectedItem] + : this.props.selected || []; + } + + hasSelectedItem(){ + if(!Array.isArray(this.props.selected)) return !!this.state.selectedItem + return this.props.selected.length > 0 + } + + isItemSelected(itemId){ + const selectedItems = this.getSelected() + return selectedItems.some(i => i === itemId) + } + render() { const { items, @@ -1020,83 +1046,83 @@ export default class ReactCalendarTimeline extends Component { canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} + width={width} + showPeriod={this.showPeriod} + timelineUnit={minUnit} + timelineWidth={this.state.width} + keys={this.props.keys} > -
(this.container = el)} - className="react-calendar-timeline" + - {this.header( - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - minUnit, - timeSteps, - headerLabelGroupHeight, - headerLabelHeight - )} - {sidebarWidth > 0 && this.sidebar(height, groupHeights, headerHeight)} -
- +
(this.container = el)} + className="react-calendar-timeline" + > + {this.renderHeaders()}
- { - this.props.scrollRef(el); - this.scrollComponent = el - }} - width={width} - height={height} - onZoom={this.changeZoom} - onWheelZoom={this.handleWheelZoom} - traditionalZoom={traditionalZoom} - onScroll={this.onScroll} - isInteractingWithItem={isInteractingWithItem} - > - - {this.items( - canvasTimeStart, - zoom, - canvasTimeEnd, - canvasWidth, - minUnit, - dimensionItems, - groupHeights, - groupTops - )} - {this.columns( - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - minUnit, - timeSteps, - height, - headerHeight - )} - {this.rows(canvasWidth, groupHeights, groups)} - {this.infoLabel()} - {this.childrenWithProps( - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - dimensionItems, - groupHeights, - groupTops, - height, - headerHeight, - visibleTimeStart, - visibleTimeEnd, - minUnit, - timeSteps - )} - - + {sidebarWidth > 0 ? this.sidebar(height, groupHeights) : null} + + + {this.items( + canvasTimeStart, + zoom, + canvasTimeEnd, + canvasWidth, + minUnit, + dimensionItems, + groupHeights, + groupTops + )} + {this.columns( + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + minUnit, + timeSteps, + height, + headerHeight + )} + {this.rows(canvasWidth, groupHeights, groups)} + {this.infoLabel()} + {this.childrenWithProps( + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + dimensionItems, + groupHeights, + groupTops, + height, + headerHeight, + visibleTimeStart, + visibleTimeEnd, + minUnit, + timeSteps + )} + + + {rightSidebarWidth > 0 + ? this.rightSidebar(height, groupHeights) + : null}
- {rightSidebarWidth > 0 && this.rightSidebar(height, groupHeights, headerHeight)} -
+
) } -} +} \ No newline at end of file diff --git a/src/lib/Timeline.scss b/src/lib/Timeline.scss index a55a0c04d..7fd88e0f5 100644 --- a/src/lib/Timeline.scss +++ b/src/lib/Timeline.scss @@ -71,77 +71,6 @@ $weekend: rgba(250, 246, 225, 0.5); } } - .rct-header { - margin: 0; - overflow-x: hidden; - z-index: 90; - - .rct-top-header, - .rct-bottom-header { - position: relative; - } - - .rct-label-group { - padding: 0 5px; - position: absolute; - top: 0; - font-size: 14px; - text-align: center; - cursor: pointer; - border-left: $thick-border-width solid $border-color; - color: $header-color; - background: $header-background-color; - border-bottom: $border-width solid $border-color; - cursor: pointer; - &.rct-has-right-sidebar { - border-right: ($thick-border-width / 2) solid $border-color; - border-left: ($thick-border-width / 2) solid $border-color; - } - - & > span { - position: sticky; - left: 5px; - right: 5px; - } - } - - .rct-label { - position: absolute; - // overflow: hidden; - text-align: center; - cursor: pointer; - border-left: $border-width solid $border-color; - color: $lower-header-color; - background: $lower-header-background-color; - border-bottom: $border-width solid $border-color; - cursor: pointer; - - &.rct-label-only { - color: $header-color; - background: $header-background-color; - } - - &.rct-first-of-type { - border-left: $thick-border-width solid $border-color; - } - } - } - - .rct-sidebar-header { - margin: 0; - color: $sidebar-color; - background: $sidebar-background-color; - border-right: $border-width solid $border-color; - box-sizing: border-box; - border-bottom: $border-width solid $border-color; - overflow: hidden; - - &.rct-sidebar-right { - border-right: 0; - border-left: $border-width solid $border-color; - } - } - .rct-sidebar { overflow: hidden; white-space: normal; // was set to nowrap in .rct-outer @@ -179,7 +108,7 @@ $weekend: rgba(250, 246, 225, 0.5); .rct-vertical-lines { .rct-vl { position: absolute; - border-left: 1px solid $border-color; + border-right: 1px solid $border-color; z-index: 30; &.rct-vl-first { border-left-width: 2px; diff --git a/src/lib/columns/Columns.js b/src/lib/columns/Columns.js index 00077c085..86bcb82d0 100644 --- a/src/lib/columns/Columns.js +++ b/src/lib/columns/Columns.js @@ -2,17 +2,23 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { iterateTimes } from '../utility/calendar' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext' -export default class Columns extends Component { +const passThroughPropTypes = { + canvasTimeStart: PropTypes.number.isRequired, + canvasTimeEnd: PropTypes.number.isRequired, + canvasWidth: PropTypes.number.isRequired, + lineCount: PropTypes.number.isRequired, + minUnit: PropTypes.string.isRequired, + timeSteps: PropTypes.object.isRequired, + height: PropTypes.number.isRequired, + verticalLineClassNamesForTime: PropTypes.func +} + +class Columns extends Component { static propTypes = { - canvasTimeStart: PropTypes.number.isRequired, - canvasTimeEnd: PropTypes.number.isRequired, - canvasWidth: PropTypes.number.isRequired, - lineCount: PropTypes.number.isRequired, - minUnit: PropTypes.string.isRequired, - timeSteps: PropTypes.object.isRequired, - height: PropTypes.number.isRequired, - verticalLineClassNamesForTime: PropTypes.func + ...passThroughPropTypes, + getLeftOffsetFromDate: PropTypes.func.isRequired } shouldComponentUpdate(nextProps) { @@ -37,7 +43,8 @@ export default class Columns extends Component { minUnit, timeSteps, height, - verticalLineClassNamesForTime + verticalLineClassNamesForTime, + getLeftOffsetFromDate } = this.props const ratio = canvasWidth / (canvasTimeEnd - canvasTimeStart) @@ -49,13 +56,8 @@ export default class Columns extends Component { minUnit, timeSteps, (time, nextTime) => { - const left = Math.round((time.valueOf() - canvasTimeStart) * ratio, -2) const minUnitValue = time.get(minUnit === 'day' ? 'date' : minUnit) const firstOfType = minUnitValue === (minUnit === 'day' ? 1 : 0) - const lineWidth = firstOfType ? 2 : 1 - const labelWidth = - Math.ceil((nextTime.valueOf() - time.valueOf()) * ratio) - lineWidth - const leftPush = firstOfType ? -1 : 0 let classNamesForTime = [] if (verticalLineClassNamesForTime) { @@ -74,6 +76,8 @@ export default class Columns extends Component { : '') + classNamesForTime.join(' ') + const left = getLeftOffsetFromDate(time.valueOf()) + const right = getLeftOffsetFromDate(nextTime.valueOf()) lines.push(
@@ -93,3 +97,19 @@ export default class Columns extends Component { return
{lines}
} } + +const ColumnsWrapper = ({ ...props }) => { + return ( + + {({ getLeftOffsetFromDate }) => ( + + )} + + ) +} + +ColumnsWrapper.defaultProps = { + ...passThroughPropTypes +} + +export default ColumnsWrapper diff --git a/src/lib/default-config.js b/src/lib/default-config.js index 7523af171..d3d6cbc7a 100644 --- a/src/lib/default-config.js +++ b/src/lib/default-config.js @@ -20,6 +20,40 @@ export const defaultTimeSteps = { year: 1 } +export const defaultHeaderFormats = { + year: { + long: 'YYYY', + mediumLong: 'YYYY', + medium: 'YYYY', + short: 'YY' + }, + month: { + long: 'MMMM YYYY', + mediumLong: 'MMMM', + medium: 'MMMM', + short: 'MM/YY' + }, + day: { + long: 'dddd, LL', + mediumLong: 'dddd, LL', + medium: 'dd D', + short: 'D' + }, + hour: { + long: 'dddd, LL, HH:00', + mediumLong: 'L, HH:00', + medium: 'HH:00', + short: 'HH' + }, + minute: { + long: 'HH:mm', + mediumLong: 'HH:mm', + medium: 'HH:mm', + short: 'mm', + } +} + +//TODO: delete this export const defaultHeaderLabelFormats = { yearShort: 'YY', yearLong: 'YYYY', @@ -36,6 +70,7 @@ export const defaultHeaderLabelFormats = { time: 'LLL' } +//TODO: delete this export const defaultSubHeaderLabelFormats = { yearShort: 'YY', yearLong: 'YYYY', diff --git a/src/lib/headers/CustomHeader.js b/src/lib/headers/CustomHeader.js new file mode 100644 index 000000000..b1cc46103 --- /dev/null +++ b/src/lib/headers/CustomHeader.js @@ -0,0 +1,247 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { TimelineHeadersConsumer } from './HeadersContext' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext' +import { iterateTimes, calculateXPositionForTime } from '../utility/calendar' + +export class CustomHeader extends React.Component { + static propTypes = { + //component props + children: PropTypes.func.isRequired, + unit: PropTypes.string.isRequired, + timeSteps: PropTypes.object.isRequired, + //Timeline context + visibleTimeStart: PropTypes.number.isRequired, + visibleTimeEnd: PropTypes.number.isRequired, + canvasTimeStart: PropTypes.number.isRequired, + canvasTimeEnd: PropTypes.number.isRequired, + canvasWidth: PropTypes.number.isRequired, + showPeriod: PropTypes.func.isRequired, + props: PropTypes.object, + getLeftOffsetFromDate: PropTypes.func.isRequired, + } + constructor(props) { + super(props) + const { + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + getLeftOffsetFromDate, + } = props + const ratio = this.calculateRatio( + canvasWidth, + canvasTimeEnd, + canvasTimeStart + ) + const intervals = this.getHeaderIntervals({ + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + getLeftOffsetFromDate, + }) + + this.state = { + intervals, + ratio + } + } + + shouldComponentUpdate(nextProps) { + if ( + nextProps.canvasTimeStart !== this.props.canvasTimeStart || + nextProps.canvasTimeEnd !== this.props.canvasTimeEnd || + nextProps.canvasWidth !== this.props.canvasWidth || + nextProps.unit !== this.props.unit || + nextProps.timeSteps !== this.props.timeSteps || + nextProps.showPeriod !== this.props.showPeriod || + nextProps.children !== this.props.children + ) { + return true + } + return false + } + + componentWillReceiveProps(nextProps) { + if ( + nextProps.canvasTimeStart !== this.props.canvasTimeStart || + nextProps.canvasTimeEnd !== this.props.canvasTimeEnd || + nextProps.canvasWidth !== this.props.canvasWidth || + nextProps.unit !== this.props.unit || + nextProps.timeSteps !== this.props.timeSteps || + nextProps.showPeriod !== this.props.showPeriod + ) { + const { + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + getLeftOffsetFromDate, + } = nextProps + const ratio = this.calculateRatio( + canvasWidth, + canvasTimeEnd, + canvasTimeStart + ) + const intervals = this.getHeaderIntervals({ + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + getLeftOffsetFromDate, + }) + + this.setState({ intervals, ratio }) + } + } + + getHeaderIntervals = ({ + canvasTimeStart, + canvasTimeEnd, + unit, + timeSteps, + getLeftOffsetFromDate, + }) => { + const intervals = [] + iterateTimes( + canvasTimeStart, + canvasTimeEnd, + unit, + timeSteps, + (startTime, endTime) => { + const left = getLeftOffsetFromDate(startTime.valueOf()) + const right = getLeftOffsetFromDate(endTime.valueOf()) + const width = right-left + intervals.push({ + startTime, + endTime, + labelWidth: width, + left, + }) + } + ) + return intervals + } + + rootProps = { + style: { + position: 'relative' + } + } + + getRootProps = (props = {}) => { + const { style } = props + return { + style: Object.assign({}, style ? style : {}, { + position: 'relative', + width: this.props.canvasWidth + }) + } + } + + getIntervalProps = (props = {}) => { + const { interval, style } = props + if (!interval) throw new Error("you should provide interval to the prop getter") + const { startTime, labelWidth, left } = interval + return { + style: this.getIntervalStyle({ + style, + startTime, + labelWidth, + canvasTimeStart: this.props.canvasTimeStart, + unit: this.props.unit, + ratio: this.state.ratio, + left, + }), + key: `label-${startTime.valueOf()}` + } + } + + calculateRatio(canvasWidth, canvasTimeEnd, canvasTimeStart) { + return canvasWidth / (canvasTimeEnd - canvasTimeStart) + } + + getIntervalStyle = ({ startTime, canvasTimeStart, ratio, unit, left,labelWidth, style, }) => { + + return { + ...style, + left, + width: labelWidth, + position: 'absolute' + } + } + + getStateAndHelpers = () => { + const { + canvasTimeStart, + canvasTimeEnd, + unit, + showPeriod, + timelineWidth, + visibleTimeStart, + visibleTimeEnd + } = this.props + //TODO: only evaluate on changing params + return { + timelineContext: { + timelineWidth, + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd + }, + headerContext: { + unit, + intervals: this.state.intervals + }, + getRootProps: this.getRootProps, + getIntervalProps: this.getIntervalProps, + showPeriod + } + } + + render() { + const props = this.getStateAndHelpers() + return this.props.children(props, this.props.props) + } +} + +const CustomHeaderWrapper = ({ children, unit, props }) => ( + + {({ getTimelineState, showPeriod, getLeftOffsetFromDate }) => { + const timelineState = getTimelineState() + return ( + + {({ timeSteps }) => ( + + )} + + ) + }} + +) + +CustomHeaderWrapper.propTypes = { + children: PropTypes.func.isRequired, + unit: PropTypes.string, + props: PropTypes.object, +} + +export default CustomHeaderWrapper diff --git a/src/lib/headers/DateHeader.js b/src/lib/headers/DateHeader.js new file mode 100644 index 000000000..d5a1d3962 --- /dev/null +++ b/src/lib/headers/DateHeader.js @@ -0,0 +1,173 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext' +import CustomHeader from './CustomHeader' +import { getNextUnit } from '../utility/calendar' +import { defaultHeaderFormats } from '../default-config' +import Interval from './Interval' + +class DateHeader extends React.PureComponent { + static propTypes = { + primaryHeader: PropTypes.bool, + secondaryHeader: PropTypes.bool, + unit: PropTypes.string, + style: PropTypes.object, + className: PropTypes.string, + timelineUnit: PropTypes.string, + labelFormat: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + PropTypes.string + ]).isRequired, + intervalRenderer: PropTypes.func, + props: PropTypes.object, + } + + getHeaderUnit = () => { + if (this.props.unit) { + return this.props.unit + } else if (this.props.primaryHeader) { + return getNextUnit(this.props.timelineUnit) + } + return this.props.timelineUnit + } + + render() { + const unit = this.getHeaderUnit() + const {props} = this.props; + return ( + + {({ + headerContext: { intervals }, + getRootProps, + getIntervalProps, + showPeriod + }, props) => { + const unit = this.getHeaderUnit() + + return ( +
+ {intervals.map(interval => { + const intervalText = this.getLabelFormat( + [interval.startTime, interval.endTime], + unit, + interval.labelWidth + ) + return ( + + ) + })} +
+ ) + }} +
+ ) + } + + getRootStyle = () => { + return { + height: 30, + ...this.props.style + } + } + + getLabelFormat(interval, unit, labelWidth) { + const { labelFormat } = this.props + if (typeof labelFormat === 'string') { + const startTime = interval[0] + return startTime.format(labelFormat) + } else if (typeof labelFormat === 'object') { + return formatLabel(interval, unit, labelWidth, labelFormat) + } else if (typeof labelFormat === 'function') { + return labelFormat(interval, unit, labelWidth) + } else { + throw new Error('labelFormat should be function, object or string') + } + } +} + +const DateHeaderWrapper = ({ + primaryHeader, + secondaryHeader, + unit, + labelFormat, + style, + className, + intervalRenderer, + props, +}) => ( + + {({ getTimelineState }) => { + const timelineState = getTimelineState() + return ( + + ) + }} + +) + +DateHeaderWrapper.propTypes = { + style: PropTypes.object, + className: PropTypes.string, + primaryHeader: PropTypes.bool, + secondaryHeader: PropTypes.bool, + unit: PropTypes.string, + labelFormat: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + PropTypes.string + ]), + intervalRenderer: PropTypes.func, + props: PropTypes.object, +} + +DateHeaderWrapper.defaultProps = { + secondaryHeader: true, + labelFormat: formatLabel +} + +function formatLabel( + [timeStart, timeEnd], + unit, + labelWidth, + formatOptions = defaultHeaderFormats +) { + let format + if (labelWidth >= 150) { + format = formatOptions[unit]['long'] + } else if (labelWidth >= 100) { + format = formatOptions[unit]['mediumLong'] + } else if (labelWidth >= 50) { + format = formatOptions[unit]['medium'] + } else { + format = formatOptions[unit]['short'] + } + return timeStart.format(format) +} + +export default DateHeaderWrapper diff --git a/src/lib/headers/HeadersContext.js b/src/lib/headers/HeadersContext.js new file mode 100644 index 000000000..d2a6830fe --- /dev/null +++ b/src/lib/headers/HeadersContext.js @@ -0,0 +1,42 @@ +import React from 'react' +import PropTypes from 'prop-types' +import createReactContext from 'create-react-context' +import { noop } from '../utility/generic' + +const defaultContextState = { + registerScroll: () => { + // eslint-disable-next-line + console.warn('default registerScroll header used') + return noop + }, + rightSidebarWidth: 0, + leftSidebarWidth: 150, + timeSteps: {} +} + +const { Consumer, Provider } = createReactContext(defaultContextState) + + +export class TimelineHeadersProvider extends React.Component { + static propTypes = { + children: PropTypes.element.isRequired, + rightSidebarWidth: PropTypes.number, + leftSidebarWidth: PropTypes.number.isRequired, + //TODO: maybe this should be skipped? + timeSteps: PropTypes.object.isRequired, + registerScroll: PropTypes.func.isRequired, + } + + + render() { + const contextValue = { + rightSidebarWidth: this.props.rightSidebarWidth, + leftSidebarWidth: this.props.leftSidebarWidth, + timeSteps: this.props.timeSteps, + registerScroll: this.props.registerScroll, + } + return {this.props.children} + } +} + +export const TimelineHeadersConsumer = Consumer diff --git a/src/lib/headers/Interval.js b/src/lib/headers/Interval.js new file mode 100644 index 000000000..7da76f3a7 --- /dev/null +++ b/src/lib/headers/Interval.js @@ -0,0 +1,84 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { getNextUnit } from '../utility/calendar' +import { composeEvents } from '../utility/events' + +class Interval extends React.PureComponent { + static propTypes = { + intervalRenderer: PropTypes.func, + unit: PropTypes.string.isRequired, + interval: PropTypes.object.isRequired, + showPeriod: PropTypes.func.isRequired, + intervalText: PropTypes.string.isRequired, + primaryHeader: PropTypes.bool.isRequired, + secondaryHeader: PropTypes.bool.isRequired, + getIntervalProps: PropTypes.func.isRequired, + props: PropTypes.object + } + + getIntervalStyle = () => { + return { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: + this.props.secondaryHeader && !this.props.primaryHeader + ? 'rgb(240, 240, 240)' + : 'initial', + height: '100%', + borderLeft: this.props.primaryHeader + ? '1px solid #bbb' + : '2px solid #bbb', + borderRight: this.props.primaryHeader ? '1px solid #bbb' : 'none', + borderBottom: '1px solid #bbb', + color: this.props.primaryHeader ? '#fff' : 'initial', + cursor: 'pointer', + fontSize: '14px' + } + } + + onIntervalClick = () => { + const { primaryHeader, interval, unit, showPeriod } = this.props + if (primaryHeader) { + const nextUnit = getNextUnit(unit) + const newStartTime = interval.startTime.clone().startOf(nextUnit) + const newEndTime = interval.startTime.clone().endOf(nextUnit) + showPeriod(newStartTime, newEndTime) + } else { + showPeriod(interval.startTime, interval.endTime) + } + } + + getIntervalProps = (props={}) => { + return { + ...this.props.getIntervalProps({ + interval: this.props.interval, + ...props + }), + onClick: composeEvents(this.onIntervalClick, props.onClick) + } + } + + render() { + const { intervalText, interval, intervalRenderer, props } = this.props + if (intervalRenderer) + return intervalRenderer({ + getIntervalProps: this.getIntervalProps, + intervalContext: { + interval, + intervalText + } + }, props) + return ( +
+ {intervalText} +
+ ) + } +} + +export default Interval diff --git a/src/lib/headers/ItemHeader.js b/src/lib/headers/ItemHeader.js new file mode 100644 index 000000000..e97137db0 --- /dev/null +++ b/src/lib/headers/ItemHeader.js @@ -0,0 +1,238 @@ +import React from 'react' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext' +import CustomHeader from './CustomHeader' +import PropTypes from 'prop-types' +import { + getItemDimensions, + stackGroup +} from '../utility/calendar' +import { _get } from '../utility/generic' + +const passThroughPropTypes = { + style: PropTypes.object, + className: PropTypes.string, + props: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + itemHeight: PropTypes.number, + stackItems: PropTypes.bool, + itemRenderer: PropTypes.func, +} + +class ItemHeader extends React.PureComponent { + static propTypes = { + visibleTimeStart: PropTypes.number.isRequired, + visibleTimeEnd: PropTypes.number.isRequired, + canvasTimeStart: PropTypes.number.isRequired, + canvasTimeEnd: PropTypes.number.isRequired, + canvasWidth: PropTypes.number.isRequired, + keys: PropTypes.object.isRequired, + ...passThroughPropTypes + } + + static defaultProps = { + itemHeight: 30, + stackItems: false, + itemRenderer: ({ item, getRootProps }) => { + return ( +
+ {item.title} +
+ ) + } + } + + getStateAndHelpers = (props, item, itemDimensions) => { + const { + canvasWidth: timelineWidth, + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd, + itemHeight + } = props + return { + timelineContext: { + timelineWidth, + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd + }, + item, + itemContext: { + dimensions: itemDimensions, + width: itemDimensions.width + }, + itemHeight + } + } + + render() { + const { + keys, + items, + itemHeight, + itemRenderer, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + stackItems + } = this.props + const itemDimensions = items.map(item => { + return getItemDimensions({ + item, + keys, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + groupOrders: {}, + lineHeight: itemHeight, + itemHeightRatio: 1 + }) + }) + + const { groupHeight } = stackGroup( + itemDimensions, + stackItems, + itemHeight, + 0 + ) + const height = Math.max(itemHeight, groupHeight) + + return ( + + {({ getRootProps }) => { + return ( +
+ {items.map(item => { + const itemId = _get(item, keys.itemIdKey) + const dimensions = itemDimensions.find( + itemDimension => itemDimension.id === itemId + ).dimensions + return ( + + ) + })} +
+ ) + }} +
+ ) + } + + getRootStyles(height) { + return { + ...this.props.style, + height + } + } +} + +class Item extends React.PureComponent { + static propTypes = { + item: PropTypes.object.isRequired, + timelineContext: PropTypes.shape({ + timelineWidth: PropTypes.number, + visibleTimeStart: PropTypes.number, + visibleTimeEnd: PropTypes.number, + canvasTimeStart: PropTypes.number, + canvasTimeEnd: PropTypes.number + }).isRequired, + itemContext: PropTypes.shape({ + dimensions: PropTypes.object, + width: PropTypes.number + }).isRequired, + itemRenderer: passThroughPropTypes['itemRenderer'], + itemHeight: passThroughPropTypes['itemHeight'], + props: PropTypes.object, + } + + getStyles = (style = {}, dimensions, itemHeight) => { + return { + position: 'absolute', + left: dimensions.left, + top: dimensions.top, + width: dimensions.width, + height: itemHeight, + ...style + } + } + + getRootProps = (props = {}) => { + const { style, ...rest } = props + return { + style: this.getStyles( + style, + this.props.itemContext.dimensions, + this.props.itemHeight + ), + rest + } + } + + render() { + const { item, timelineContext, itemContext, props } = this.props + return this.props.itemRenderer({ + item, + timelineContext, + itemContext, + getRootProps: this.getRootProps, + props, + }) + } +} + +const ItemHeaderWrapper = ({ + style, + className, + props, + items, + stackItems, + itemHeight, + itemRenderer +}) => ( + + {({ getTimelineState }) => { + const timelineState = getTimelineState() + return ( + + ) + }} + +) + +ItemHeaderWrapper.propTypes = { + ...passThroughPropTypes +} + +export default ItemHeaderWrapper diff --git a/src/lib/headers/SidebarHeader.js b/src/lib/headers/SidebarHeader.js new file mode 100644 index 000000000..3ad4aa603 --- /dev/null +++ b/src/lib/headers/SidebarHeader.js @@ -0,0 +1,68 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { TimelineHeadersConsumer } from './HeadersContext' +import { LEFT_VARIANT, RIGHT_VARIANT } from './constants' + +class SidebarHeader extends React.PureComponent { + static propTypes = { + children: PropTypes.func.isRequired, + rightSidebarWidth: PropTypes.number, + leftSidebarWidth: PropTypes.number.isRequired, + variant: PropTypes.string, + props: PropTypes.object + } + + getRootProps = (props = {}) => { + const { style } = props + const width = + this.props.variant === RIGHT_VARIANT + ? this.props.rightSidebarWidth + : this.props.leftSidebarWidth + return { + style: { + width, + ...style + } + } + } + + getStateAndHelpers = () => { + return { + getRootProps: this.getRootProps + } + } + + render() { + const props = this.getStateAndHelpers() + return this.props.children(props, this.props.props) + } +} + +const SidebarWrapper = ({ children, variant, props }) => ( + + {({ leftSidebarWidth, rightSidebarWidth }) => { + return ( + + ) + }} + +) + +SidebarWrapper.propTypes = { + children: PropTypes.func.isRequired, + variant: PropTypes.string, + props: PropTypes.object +} + +SidebarWrapper.defaultProps = { + variant: LEFT_VARIANT, + children: ({ getRootProps }) =>
+} + +export default SidebarWrapper diff --git a/src/lib/headers/TimelineHeaders.js b/src/lib/headers/TimelineHeaders.js new file mode 100644 index 000000000..080513253 --- /dev/null +++ b/src/lib/headers/TimelineHeaders.js @@ -0,0 +1,126 @@ +import React from 'react' +import { TimelineHeadersConsumer } from './HeadersContext' +import PropTypes from 'prop-types' +import SidebarHeader from './SidebarHeader' +import { RIGHT_VARIANT, LEFT_VARIANT } from './constants' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext'; +class TimelineHeaders extends React.PureComponent { + static propTypes = { + registerScroll: PropTypes.func.isRequired, + leftSidebarWidth: PropTypes.number.isRequired, + rightSidebarWidth: PropTypes.number.isRequired, + style: PropTypes.object, + className: PropTypes.string, + calendarHeaderStyle: PropTypes.object, + calendarHeaderClassName: PropTypes.string, + width: PropTypes.number.isRequired + } + + constructor(props) { + super(props) + } + + getRootStyle = () => { + return { + background: '#c52020', + borderBottom: '1px solid #bbb', + ...this.props.style, + display: 'flex', + width: 'max-content' + } + } + + getCalendarHeaderStyle = () => { + const { + leftSidebarWidth, + rightSidebarWidth, + calendarHeaderStyle + } = this.props + return { + border: '1px solid #bbb', + ...calendarHeaderStyle, + overflow: 'hidden', + width: this.props.width + } + } + + render() { + let rightSidebarHeader + let leftSidebarHeader + let calendarHeaders = [] + const children = Array.isArray(this.props.children) + ? this.props.children.filter(c => c) + : [this.props.children] + React.Children.map(children, child => { + if ( + child.type === SidebarHeader && + child.props.variant === RIGHT_VARIANT + ) { + rightSidebarHeader = child + } else if ( + child.type === SidebarHeader && + child.props.variant === LEFT_VARIANT + ) { + leftSidebarHeader = child + } else { + calendarHeaders.push(child) + } + }) + return ( +
+ {leftSidebarHeader} +
+ {calendarHeaders} +
+ {rightSidebarHeader} +
+ ) + } +} + +const TimelineHeadersWrapper = ({ + children, + style, + className, + calendarHeaderStyle, + calendarHeaderClassName +}) => ( + + {({ getTimelineState, showPeriod }) => { + const state = getTimelineState() + return ( + + {({ leftSidebarWidth, rightSidebarWidth, registerScroll }) => { + return ( + + ) + }} + + ) + }} + +) + +TimelineHeadersWrapper.propTypes = { + style: PropTypes.object, + className: PropTypes.string, + calendarHeaderStyle: PropTypes.object, + calendarHeaderClassName: PropTypes.string +} + +export default TimelineHeadersWrapper diff --git a/src/lib/headers/constants.js b/src/lib/headers/constants.js new file mode 100644 index 000000000..6f7a4686c --- /dev/null +++ b/src/lib/headers/constants.js @@ -0,0 +1,3 @@ +export const LEFT_VARIANT= 'left' +export const RIGHT_VARIANT= 'right' + diff --git a/src/lib/items/Item.js b/src/lib/items/Item.js index af11c26b2..a9b095009 100644 --- a/src/lib/items/Item.js +++ b/src/lib/items/Item.js @@ -113,7 +113,7 @@ export default class Item extends Component { nextProps.canMove !== this.props.canMove || nextProps.canResizeLeft !== this.props.canResizeLeft || nextProps.canResizeRight !== this.props.canResizeRight || - nextProps.dimensions !== this.props.dimensions + !deepObjectCompare(nextProps.dimensions, this.props.dimensions) return shouldUpdate } @@ -261,7 +261,6 @@ export default class Item extends Component { if (this.state.dragging) { let dragTime = this.dragTime(e) let dragGroupDelta = this.dragGroupDelta(e) - if (this.props.moveResizeValidator) { dragTime = this.props.moveResizeValidator( 'move', @@ -384,7 +383,9 @@ export default class Item extends Component { } }) .on('tap', e => { - this.actualClick(e, e.pointerType === 'mouse' ? 'click' : 'touch') + if(e.pointerType !== 'mouse'){ + this.actualClick(e, 'touch') + } }) this.setState({ @@ -414,7 +415,6 @@ export default class Item extends Component { componentDidUpdate(prevProps) { this.cacheDataFromProps(this.props) - let { interactMounted } = this.state const couldDrag = prevProps.selected && this.canMove(prevProps) const couldResizeLeft = @@ -427,46 +427,40 @@ export default class Item extends Component { const willBeAbleToResizeRight = this.props.selected && this.canResizeRight(this.props) - if (this.props.selected && !interactMounted) { - this.mountInteract() - interactMounted = true - } - - if ( - interactMounted && - (couldResizeLeft !== willBeAbleToResizeLeft || - couldResizeRight !== willBeAbleToResizeRight) - ) { - const leftResize = this.props.useResizeHandle ? this.dragLeft : true - const rightResize = this.props.useResizeHandle ? this.dragRight : true - - interact(this.item).resizable({ - enabled: willBeAbleToResizeLeft || willBeAbleToResizeRight, - edges: { - top: false, - bottom: false, - left: willBeAbleToResizeLeft && leftResize, - right: willBeAbleToResizeRight && rightResize - } - }) - } - if (interactMounted && couldDrag !== willBeAbleToDrag) { - interact(this.item).draggable({ enabled: willBeAbleToDrag }) + if(!!this.item){ + if (this.props.selected && !interactMounted) { + this.mountInteract() + interactMounted = true + } + if ( + interactMounted && + (couldResizeLeft !== willBeAbleToResizeLeft || + couldResizeRight !== willBeAbleToResizeRight) + ) { + const leftResize = this.props.useResizeHandle ? this.dragLeft : true + const rightResize = this.props.useResizeHandle ? this.dragRight : true + + interact(this.item).resizable({ + enabled: willBeAbleToResizeLeft || willBeAbleToResizeRight, + edges: { + top: false, + bottom: false, + left: willBeAbleToResizeLeft && leftResize, + right: willBeAbleToResizeRight && rightResize + } + }) + } + if (interactMounted && couldDrag !== willBeAbleToDrag) { + interact(this.item).draggable({ enabled: willBeAbleToDrag }) + } } - } - - onMouseDown = e => { - if (!this.state.interactMounted) { - e.preventDefault() - this.startedClicking = true + else{ + interactMounted= false; } - } + this.setState({ + interactMounted, + }) - onMouseUp = e => { - if (!this.state.interactMounted && this.startedClicking) { - this.startedClicking = false - this.actualClick(e, 'click') - } } onTouchStart = e => { @@ -519,13 +513,12 @@ export default class Item extends Component { ref: this.getItemRef, title: this.itemDivTitle, className: classNames + ` ${props.className ? props.className : ''}`, - onMouseDown: composeEvents(this.onMouseDown, props.onMouseDown), - onMouseUp: composeEvents(this.onMouseUp, props.onMouseUp), onTouchStart: composeEvents(this.onTouchStart, props.onTouchStart), onTouchEnd: composeEvents(this.onTouchEnd, props.onTouchEnd), onDoubleClick: composeEvents(this.handleDoubleClick, props.onDoubleClick), onContextMenu: composeEvents(this.handleContextMenu, props.onContextMenu), - style: Object.assign({}, this.getItemStyle(props)) + style: Object.assign({}, this.getItemStyle(props)), + onClick : (e) => {this.actualClick(e, 'click')} } } diff --git a/src/lib/layout/Header.js b/src/lib/layout/Header.js deleted file mode 100644 index 711e53251..000000000 --- a/src/lib/layout/Header.js +++ /dev/null @@ -1,110 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import TimelineElementsHeader from './TimelineElementsHeader' - -class Header extends Component { - static propTypes = { - hasRightSidebar: PropTypes.bool.isRequired, - showPeriod: PropTypes.func.isRequired, - canvasTimeStart: PropTypes.number.isRequired, - canvasTimeEnd: PropTypes.number.isRequired, - canvasWidth: PropTypes.number.isRequired, - minUnit: PropTypes.string.isRequired, - timeSteps: PropTypes.object.isRequired, - width: PropTypes.number.isRequired, - headerLabelFormats: PropTypes.object.isRequired, - subHeaderLabelFormats: PropTypes.object.isRequired, - stickyOffset: PropTypes.number, - stickyHeader: PropTypes.bool.isRequired, - headerLabelGroupHeight: PropTypes.number.isRequired, - headerLabelHeight: PropTypes.number.isRequired, - leftSidebarHeader: PropTypes.node, - rightSidebarHeader: PropTypes.node, - leftSidebarWidth: PropTypes.number, - rightSidebarWidth: PropTypes.number, - headerRef: PropTypes.func.isRequired, - scrollHeaderRef: PropTypes.func.isRequired - } - - render() { - const { - width, - stickyOffset, - stickyHeader, - headerRef, - scrollHeaderRef, - hasRightSidebar, - showPeriod, - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - minUnit, - timeSteps, - headerLabelFormats, - subHeaderLabelFormats, - headerLabelGroupHeight, - headerLabelHeight, - leftSidebarHeader, - rightSidebarHeader, - leftSidebarWidth, - rightSidebarWidth - } = this.props - - const headerStyle = { - top: stickyHeader ? stickyOffset || 0 : 0 - } - - const headerClass = stickyHeader ? 'header-sticky' : '' - - const leftSidebar = leftSidebarHeader && leftSidebarWidth > 0 && ( -
- {leftSidebarHeader} -
- ) - - const rightSidebar = rightSidebarHeader && rightSidebarWidth > 0 && ( -
- {rightSidebarHeader} -
- ) - - return ( -
- {leftSidebar} -
- -
- {rightSidebar} -
- ) - } -} - -export default Header diff --git a/src/lib/layout/TimelineElementsHeader.js b/src/lib/layout/TimelineElementsHeader.js deleted file mode 100644 index d5e51efad..000000000 --- a/src/lib/layout/TimelineElementsHeader.js +++ /dev/null @@ -1,249 +0,0 @@ -import PropTypes from 'prop-types' -import React, { Component } from 'react' -import moment from 'moment' - -import { iterateTimes, getNextUnit } from '../utility/calendar' - -export default class TimelineElementsHeader extends Component { - static propTypes = { - hasRightSidebar: PropTypes.bool.isRequired, - showPeriod: PropTypes.func.isRequired, - canvasTimeStart: PropTypes.number.isRequired, - canvasTimeEnd: PropTypes.number.isRequired, - canvasWidth: PropTypes.number.isRequired, - minUnit: PropTypes.string.isRequired, - timeSteps: PropTypes.object.isRequired, - width: PropTypes.number.isRequired, - headerLabelFormats: PropTypes.object.isRequired, - subHeaderLabelFormats: PropTypes.object.isRequired, - headerLabelGroupHeight: PropTypes.number.isRequired, - headerLabelHeight: PropTypes.number.isRequired, - scrollHeaderRef: PropTypes.func.isRequired - } - - constructor(props) { - super(props) - - this.state = { - touchTarget: null, - touchActive: false - } - } - - handleHeaderMouseDown(evt) { - //dont bubble so that we prevent our scroll component - //from knowing about it - evt.stopPropagation() - } - - headerLabel(time, unit, width) { - const { headerLabelFormats: f } = this.props - - if (unit === 'year') { - return time.format(width < 46 ? f.yearShort : f.yearLong) - } else if (unit === 'month') { - return time.format( - width < 65 - ? f.monthShort - : width < 75 - ? f.monthMedium - : width < 120 ? f.monthMediumLong : f.monthLong - ) - } else if (unit === 'day') { - return time.format(width < 150 ? f.dayShort : f.dayLong) - } else if (unit === 'hour') { - return time.format( - width < 50 - ? f.hourShort - : width < 130 - ? f.hourMedium - : width < 150 ? f.hourMediumLong : f.hourLong - ) - } else { - return time.format(f.time) - } - } - - subHeaderLabel(time, unit, width) { - const { subHeaderLabelFormats: f } = this.props - - if (unit === 'year') { - return time.format(width < 46 ? f.yearShort : f.yearLong) - } else if (unit === 'month') { - return time.format( - width < 37 ? f.monthShort : width < 85 ? f.monthMedium : f.monthLong - ) - } else if (unit === 'day') { - return time.format( - width < 47 - ? f.dayShort - : width < 80 ? f.dayMedium : width < 120 ? f.dayMediumLong : f.dayLong - ) - } else if (unit === 'hour') { - return time.format(width < 50 ? f.hourShort : f.hourLong) - } else if (unit === 'minute') { - return time.format(width < 60 ? f.minuteShort : f.minuteLong) - } else { - return time.get(unit) - } - } - - handlePeriodClick = (time, unit) => { - if (time && unit) { - this.props.showPeriod(moment(time - 0), unit) - } - } - - shouldComponentUpdate(nextProps) { - const willUpate = - nextProps.canvasTimeStart != this.props.canvasTimeStart || - nextProps.canvasTimeEnd != this.props.canvasTimeEnd || - nextProps.width != this.props.width || - nextProps.canvasWidth != this.props.canvasWidth || - nextProps.subHeaderLabelFormats != this.props.subHeaderLabelFormats || - nextProps.headerLabelFormats != this.props.headerLabelFormats || - nextProps.hasRightSidebar != this.props.hasRightSidebar - - return willUpate - } - - render() { - const { - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - minUnit, - timeSteps, - headerLabelGroupHeight, - headerLabelHeight, - hasRightSidebar - } = this.props - - const ratio = canvasWidth / (canvasTimeEnd - canvasTimeStart) - const twoHeaders = minUnit !== 'year' - - const topHeaderLabels = [] - // add the top header - if (twoHeaders) { - const nextUnit = getNextUnit(minUnit) - - iterateTimes( - canvasTimeStart, - canvasTimeEnd, - nextUnit, - timeSteps, - (time, nextTime) => { - const left = Math.round((time.valueOf() - canvasTimeStart) * ratio) - const right = Math.round( - (nextTime.valueOf() - canvasTimeStart) * ratio - ) - - const labelWidth = right - left - // this width applies to the content in the header - // it simulates stickyness where the content is fixed in the center - // of the label. when the labelWidth is less than visible time range, - // have label content fill the entire width - const contentWidth = Math.min(labelWidth, canvasWidth) - - topHeaderLabels.push( -
this.handlePeriodClick(time, nextUnit)} - style={{ - left: `${left - 1}px`, - width: `${labelWidth}px`, - height: `${headerLabelGroupHeight}px`, - lineHeight: `${headerLabelGroupHeight}px`, - cursor: 'pointer' - }} - > - - {this.headerLabel(time, nextUnit, labelWidth)} - -
- ) - } - ) - } - - const bottomHeaderLabels = [] - iterateTimes( - canvasTimeStart, - canvasTimeEnd, - minUnit, - timeSteps, - (time, nextTime) => { - const left = Math.round((time.valueOf() - canvasTimeStart) * ratio) - const minUnitValue = time.get(minUnit === 'day' ? 'date' : minUnit) - const firstOfType = minUnitValue === (minUnit === 'day' ? 1 : 0) - const labelWidth = Math.round( - (nextTime.valueOf() - time.valueOf()) * ratio - ) - const leftCorrect = firstOfType ? 1 : 0 - - bottomHeaderLabels.push( -
this.handlePeriodClick(time, minUnit)} - style={{ - left: `${left - leftCorrect}px`, - width: `${labelWidth}px`, - height: `${ - minUnit === 'year' - ? headerLabelGroupHeight + headerLabelHeight - : headerLabelHeight - }px`, - lineHeight: `${ - minUnit === 'year' - ? headerLabelGroupHeight + headerLabelHeight - : headerLabelHeight - }px`, - fontSize: `${ - labelWidth > 30 ? '14' : labelWidth > 20 ? '12' : '10' - }px`, - cursor: 'pointer' - }} - > - {this.subHeaderLabel(time, minUnit, labelWidth)} -
- ) - } - ) - - let headerStyle = { - height: `${headerLabelGroupHeight + headerLabelHeight}px` - } - - return ( -
-
- {topHeaderLabels} -
-
- {bottomHeaderLabels} -
-
- ) - } -} diff --git a/src/lib/markers/MarkerCanvas.js b/src/lib/markers/MarkerCanvas.js index d41750c95..21d1697c6 100644 --- a/src/lib/markers/MarkerCanvas.js +++ b/src/lib/markers/MarkerCanvas.js @@ -17,7 +17,7 @@ const staticStyles = { * Renders registered markers and exposes a mouse over listener for * CursorMarkers to subscribe to */ -class MarkerCanvas extends React.Component { +class MarkerCanvas extends React.PureComponent { static propTypes = { getDateFromLeftOffsetPosition: PropTypes.func.isRequired, children: PropTypes.node diff --git a/src/lib/row/GroupRow.js b/src/lib/row/GroupRow.js index 65aaa7ff3..2b0c333bd 100644 --- a/src/lib/row/GroupRow.js +++ b/src/lib/row/GroupRow.js @@ -1,29 +1,36 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import PreventClickOnDrag from '../interaction/PreventClickOnDrag' -class GroupRow extends Component { +class GroupRow extends PureComponent { static propTypes = { onClick: PropTypes.func.isRequired, onDoubleClick: PropTypes.func.isRequired, onContextMenu: PropTypes.func.isRequired, isEvenRow: PropTypes.bool.isRequired, - style: PropTypes.object.isRequired, clickTolerance: PropTypes.number.isRequired, group: PropTypes.object.isRequired, - horizontalLineClassNamesForGroup: PropTypes.func + horizontalLineClassNamesForGroup: PropTypes.func, + order: PropTypes.number.isRequired, + canvasWidth: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + } + onGroupRowContextMenuClick = evt => this.props.onContextMenu(evt, this.props.order); + + onGroupRowClick = evt => this.props.onClick(evt, this.props.order) + + onGroupRowDoubleClick =evt => this.props.onDoubleClick(evt, this.props.order) + render() { const { - onContextMenu, - onDoubleClick, isEvenRow, - style, - onClick, clickTolerance, horizontalLineClassNamesForGroup, - group + group, + canvasWidth, + height, } = this.props let classNamesForGroup = []; @@ -32,16 +39,19 @@ class GroupRow extends Component { } return ( - +
) } } -export default GroupRow +export default GroupRow \ No newline at end of file diff --git a/src/lib/row/GroupRows.js b/src/lib/row/GroupRows.js index 6f29a72b7..4ea498177 100644 --- a/src/lib/row/GroupRows.js +++ b/src/lib/row/GroupRows.js @@ -12,7 +12,7 @@ export default class GroupRows extends Component { clickTolerance: PropTypes.number.isRequired, groups: PropTypes.array.isRequired, horizontalLineClassNamesForGroup: PropTypes.func, - onRowContextClick: PropTypes.func.isRequired, + onRowContextClick: PropTypes.func.isRequired } shouldComponentUpdate(nextProps) { @@ -34,25 +34,23 @@ export default class GroupRows extends Component { clickTolerance, groups, horizontalLineClassNamesForGroup, - onRowContextClick, + onRowContextClick } = this.props let lines = [] - for (let i = 0; i < lineCount; i++) { lines.push( onRowContextClick(evt, i)} - onClick={evt => onRowClick(evt, i)} - onDoubleClick={evt => onRowDoubleClick(evt, i)} + onContextMenu={onRowContextClick} + onClick={onRowClick} + onDoubleClick={onRowDoubleClick} key={`horizontal-line-${i}`} isEvenRow={i % 2 === 0} group={groups[i]} horizontalLineClassNamesForGroup={horizontalLineClassNamesForGroup} - style={{ - width: `${canvasWidth}px`, - height: `${groupHeights[i] - 1}px` - }} + canvasWidth={canvasWidth} + height={groupHeights[i]} /> ) } diff --git a/src/lib/scroll/ScrollElement.js b/src/lib/scroll/ScrollElement.js index cbd8b7b8f..70dc1e8e0 100644 --- a/src/lib/scroll/ScrollElement.js +++ b/src/lib/scroll/ScrollElement.js @@ -1,8 +1,8 @@ -import React, { Component } from 'react' +import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import { getParentPosition } from '../utility/dom-helpers' -class ScrollElement extends Component { +class ScrollElement extends PureComponent { static propTypes = { children: PropTypes.element.isRequired, width: PropTypes.number.isRequired, diff --git a/src/lib/timeline/TimelineStateContext.js b/src/lib/timeline/TimelineStateContext.js index a58de362e..15cd24369 100644 --- a/src/lib/timeline/TimelineStateContext.js +++ b/src/lib/timeline/TimelineStateContext.js @@ -23,6 +23,9 @@ const defaultContextState = { }, getDateFromLeftOffsetPosition: () => { console.warn('"getDateFromLeftOffsetPosition" default func is being used') + }, + showPeriod: () => { + console.warn('"showPeriod" default func is being used') } } /* eslint-enable */ @@ -37,7 +40,12 @@ export class TimelineStateProvider extends React.Component { visibleTimeEnd: PropTypes.number.isRequired, canvasTimeStart: PropTypes.number.isRequired, canvasTimeEnd: PropTypes.number.isRequired, - canvasWidth: PropTypes.number.isRequired + canvasWidth: PropTypes.number.isRequired, + showPeriod: PropTypes.func.isRequired, + timelineUnit: PropTypes.string.isRequired, + timelineWidth: PropTypes.number.isRequired, + keys:PropTypes.object.isRequired, + width: PropTypes.number.isRequired } constructor(props) { @@ -47,13 +55,35 @@ export class TimelineStateProvider extends React.Component { timelineContext: { getTimelineState: this.getTimelineState, getLeftOffsetFromDate: this.getLeftOffsetFromDate, - getDateFromLeftOffsetPosition: this.getDateFromLeftOffsetPosition + getDateFromLeftOffsetPosition: this.getDateFromLeftOffsetPosition, + showPeriod: this.props.showPeriod, } } } getTimelineState = () => { - return this.state.timelineState // REVIEW: return copy or object.freeze? + const { + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + timelineUnit, + timelineWidth, + keys, + width, + } = this.props + return { + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + timelineUnit, + timelineWidth, + keys, + width, + } // REVIEW, } getLeftOffsetFromDate = date => { diff --git a/src/lib/utility/calendar.js b/src/lib/utility/calendar.js index 7d68cff41..12d8a68cb 100644 --- a/src/lib/utility/calendar.js +++ b/src/lib/utility/calendar.js @@ -68,8 +68,9 @@ export function iterateTimes(start, end, unit, timeSteps, callback) { } while (time.valueOf() < end) { - let nextTime = moment(time).add(timeSteps[unit] || 1, `${unit}s`) - callback(time, nextTime) + let nextTime = moment(time).add(timeSteps[unit] || 1, `${unit}s`).startOf(`${unit}s`); + let endTime = moment(time).endOf(`${unit}s`) + callback(time, endTime) time = nextTime } } @@ -148,10 +149,13 @@ export function getNextUnit(unit) { minute: 'hour', hour: 'day', day: 'month', - month: 'year' + month: 'year', + year: 'year' } - - return nextUnits[unit] || '' + if (!nextUnits[unit]) { + throw new Error(`unit ${unit} in not acceptable`) + } + return nextUnits[unit] } /** @@ -172,7 +176,7 @@ export function calculateInteractionNewTimes({ isDragging, isResizing, resizingEdge, - resizeTime + resizeTime, }) { const originalItemRange = itemTimeEnd - itemTimeStart const itemStart = @@ -296,6 +300,30 @@ export function collision(a, b, lineHeight, collisionPadding = EPSILON) { ) } +/** + * Calculate the position of a given item for a group that each in a line + * is being stacked + */ +export function groupStackInLines( + lineHeight, + item, + items, + groupHeight, + groupTop, + itemIndex +) { + // calculate non-overlapping positions + let verticalMargin = lineHeight - item.dimensions.height + if (item.dimensions.stack && item.dimensions.top === null) { + item.dimensions.top = groupTop + verticalMargin + itemIndex * lineHeight + } + return { + groupHeight: lineHeight * items.length, + verticalMargin, + itemTop: item.dimensions.top + } +} + /** * Calculate the position of a given item for a group that * is being stacked @@ -310,7 +338,7 @@ export function groupStack( ) { // calculate non-overlapping positions let curHeight = groupHeight - let verticalMargin = lineHeight - item.dimensions.height + let verticalMargin = (lineHeight - item.dimensions.height) / 2 if (item.dimensions.stack && item.dimensions.top === null) { item.dimensions.top = groupTop + verticalMargin curHeight = Math.max(curHeight, lineHeight) @@ -336,7 +364,7 @@ export function groupStack( item.dimensions.top = collidingItem.dimensions.top + lineHeight curHeight = Math.max( curHeight, - item.dimensions.top + item.dimensions.height - groupTop + item.dimensions.top + item.dimensions.height + verticalMargin - groupTop ) } } while (collidingItem) @@ -346,7 +374,6 @@ export function groupStack( verticalMargin, itemTop: item.dimensions.top } - } // Calculate the position of this item for a group that is not being stacked @@ -396,7 +423,7 @@ export function stackAll(itemsDimensions, groupOrders, lineHeight, stackItems) { if (group.height) { groupHeights.push(group.height) } else { - groupHeights.push(Math.max(groupHeight + verticalMargin, lineHeight)) + groupHeights.push(Math.max(groupHeight, lineHeight)) } } return { @@ -407,19 +434,24 @@ export function stackAll(itemsDimensions, groupOrders, lineHeight, stackItems) { } /** - * - * @param {*} itemsDimensions - * @param {*} isGroupStacked - * @param {*} lineHeight - * @param {*} groupTop + * + * @param {*} itemsDimensions + * @param {*} isGroupStacked + * @param {*} lineHeight + * @param {*} groupTop */ -export function stackGroup(itemsDimensions, isGroupStacked, lineHeight, groupTop) { +export function stackGroup( + itemsDimensions, + isGroupStacked, + lineHeight, + groupTop +) { var groupHeight = 0 var verticalMargin = 0 // Find positions for each item in group for (let itemIndex = 0; itemIndex < itemsDimensions.length; itemIndex++) { let r = {} - if (isGroupStacked) { + if (isGroupStacked === 'space') { r = groupStack( lineHeight, itemsDimensions[itemIndex], @@ -428,8 +460,22 @@ export function stackGroup(itemsDimensions, isGroupStacked, lineHeight, groupTop groupTop, itemIndex ) + } else if (isGroupStacked === 'lines') { + r = groupStackInLines( + lineHeight, + itemsDimensions[itemIndex], + itemsDimensions, + groupHeight, + groupTop, + itemIndex + ) } else { - r = groupNoStack(lineHeight, itemsDimensions[itemIndex], groupHeight, groupTop) + r = groupNoStack( + lineHeight, + itemsDimensions[itemIndex], + groupHeight, + groupTop + ) } groupHeight = r.groupHeight verticalMargin = r.verticalMargin diff --git a/webpack.config.js b/webpack.config.js index 3f1dc0ceb..439be1be9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,7 @@ const path = require('path') const port = process.env.PORT || 8888 const config = { - devtool: 'cheap-eval-source-map', + devtool: 'source-map', context: path.join(__dirname, './demo'), entry: { // vendor: ['react', 'react-dom', 'faker', 'interactjs', 'moment'], diff --git a/yarn.lock b/yarn.lock index ea234bb40..2ce970fe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4841,6 +4841,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.3.tgz#cdfdd942853f1a1b4c71c5336b8c49da0bf0273c" + memory-fs@^0.4.0, memory-fs@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"