diff --git a/README.md b/README.md index 8f226e9ce..5ad25df48 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ 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, + height?: 30 } ``` @@ -234,7 +236,7 @@ Append a special `.rct-drag-right` handle to the elements and only resize if dra ### stackItems -Stack items under each other, so there is no visual overlap when times collide. Defaults to `false`. +Stack items under each other, so there is no visual overlap when times collide. Can be overridden in the `groups` array. Defaults to `false`. ## traditionalZoom @@ -773,6 +775,541 @@ Custom renderer for this marker. Ensure that you always pass `styles` to the roo ``` +# 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 diff --git a/__fixtures__/groupOrderAndItemDimentions.js b/__fixtures__/groupOrderAndItemDimentions.js new file mode 100644 index 000000000..4637688eb --- /dev/null +++ b/__fixtures__/groupOrderAndItemDimentions.js @@ -0,0 +1,131 @@ +export const orderedGroups = { + '1': { + group: { + id: '1' + }, + index: 0 + }, + '2': { + group: { + id: '2' + }, + index: 1 + }, + '3': { + group: { + id: '3' + }, + index: 2 + } +} + +export const dimensionItems = [ + { + dimensions: { + collisionLeft: 1540540000000, + collisionWidth: 6803877, + height: 22.5, + left: 907.4074074074074, + order: { + group: { + id: '1' + }, + index: 0 + }, + stack: true, + top: 7.5, + width: 78.74857638888886 + }, + id: '0' + }, + { + dimensions: { + collisionLeft: 1540532800000, + collisionWidth: 21203877, + height: 22.5, + left: 824.074074074074, + order: { + group: { + id: '1' + }, + index: 0 + }, + stack: true, + top: 37.5, + width: 245.4152430555556 + }, + id: '5' + }, + { + dimensions: { + collisionLeft: 1540550800000, + collisionWidth: 24803877, + height: 22.5, + left: 1032.4074074074074, + order: { + group: { + id: '1' + }, + index: 0 + }, + stack: true, + top: 7.5, + width: 287.08190972222224 + }, + id: '6' + }, + { + dimensions: { + collisionLeft: 1540570000000, + collisionWidth: 14875919, + height: 22.5, + left: 1254.6296296296296, + order: { + group: { + id: '1' + }, + index: 0 + }, + stack: true, + top: 37.5, + width: 172.1749884259259 + }, + id: '1' + }, + { + dimensions: { + collisionLeft: 1540620000000, + collisionWidth: 20397548, + height: 22.5, + left: 1833.3333333333333, + order: { + group: { + id: '1' + }, + index: 0 + }, + stack: true, + top: 7.5, + width: 236.08273148148123 + }, + id: '2' + }, + { + dimensions: { + collisionLeft: 1540656000000, + collisionWidth: 20397548, + height: 22.5, + left: 2250, + order: { + group: { + id: '3' + }, + index: 2 + }, + stack: true, + top: 105, + width: 236.08273148148146 + }, + id: '3' + } +] diff --git a/__fixtures__/itemsAndGroups.js b/__fixtures__/itemsAndGroups.js new file mode 100644 index 000000000..0ad6371d2 --- /dev/null +++ b/__fixtures__/itemsAndGroups.js @@ -0,0 +1,58 @@ +import moment from 'moment' + +export const items = [ + { + id: '0', + group: '1', + start_time: moment('2018-10-26T10:46:40.000').valueOf(), + end_time: moment('2018-10-26T12:40:03.877').valueOf(), + canMove: false, + canResize: false + }, + { + id: '5', + group: '1', + start_time: moment('2018-10-26T08:46:40.000').valueOf(), + end_time: moment('2018-10-26T14:40:03.877').valueOf(), + canMove: false, + canResize: false, + className: '' + }, + { + id: '6', + group: '1', + start_time: moment('2018-10-26T13:46:40.000').valueOf(), + end_time: moment('2018-10-26T20:40:03.877').valueOf(), + canMove: false, + canResize: false, + className: '' + }, + { + id: '1', + group: '1', + start_time: moment('2018-10-26T19:06:40.000').valueOf(), + end_time: moment('2018-10-26T23:14:35.919').valueOf(), + canMove: true, + canResize: 'both' + }, + { + id: '2', + group: '1', + start_time: moment('2018-10-27T08:00:00.000').valueOf(), + end_time: moment('2018-10-27T13:39:57.548').valueOf(), + canMove: false, + canResize: false, + className: '' + }, + { + id: '3', + group: '3', + start_time: moment('2018-10-27T18:00:00.000').valueOf(), + end_time: moment('2018-10-27T23:39:57.548').valueOf(), + canMove: false, + canResize: false, + className: '' + } +] + +export const groups = [{ id: '1' }, { id: '2' }, { id: '3' }] diff --git a/__fixtures__/stateAndProps.js b/__fixtures__/stateAndProps.js new file mode 100644 index 000000000..c343c4905 --- /dev/null +++ b/__fixtures__/stateAndProps.js @@ -0,0 +1,56 @@ +import { defaultKeys } from 'lib/default-config' +import moment from 'moment' +import {items} from './itemsAndGroups' +export const props = { + keys: defaultKeys, + lineHeight: 30, + stackItems: true, + itemHeightRatio: 0.75 +} + +export const propsNoStack = { + ...props, + stackItems: false, +} + +export const visibleTimeStart = moment('2018-10-26T00:00:00.000') +export const visibleTimeEnd = moment('2018-10-27T00:00:00.000') + +export const state = { + draggingItem: undefined, + dragTime: null, + resizingItem: null, + resizingEdge: null, + resizeTime: null, + newGroupOrder: null, + canvasTimeStart: moment('2018-10-25T00:00:00.000').valueOf(), + visibleTimeEnd: visibleTimeEnd.valueOf(), + visibleTimeStart: visibleTimeStart.valueOf(), + canvasTimeEnd: 1540674000000, + width: 1000, +} + +//offset 1 hour +const timeOffset = 1 * 60 *60 *1000 + +export const stateMoveItem = { + ...state, + draggingItem: items[0].id, + dragTime: items[0].start_time+timeOffset, + newGroupOrder: 0, +} +export const stateResizeItemLeft = { + ...state, + resizingItem: items[0].id, + resizingEdge: 'left', + resizeTime: items[0].start_time+timeOffset, + newGroupOrder: 0, +} + +export const stateResizeItemRight = { + ...state, + resizingItem: items[0].id, + resizingEdge: 'right', + resizeTime: items[0].end_time+timeOffset, + newGroupOrder: 0, +} \ No newline at end of file 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/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__/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 new file mode 100644 index 000000000..0d4c752b1 --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/calculate-scroll-canvas.js.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`calculateScrollCanvas should calculate new scroll state correctly 1`] = ` +Object { + "canvasTimeEnd": 1540720800000, + "canvasTimeStart": 1540461600000, + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 907.4074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 78.74857638888886, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 824.074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 245.4152430555556, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 1032.4074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 287.08190972222224, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 1254.6296296296296, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 172.1749884259259, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 1833.3333333333333, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 236.08273148148123, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 2250, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 236.08273148148146, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, + "visibleTimeEnd": 1540634400000, + "visibleTimeStart": 1540548000000, +} +`; + +exports[`calculateScrollCanvas should calculate new state if zoom changed correctly 1`] = ` +Object { + "canvasTimeEnd": 1540681200000, + "canvasTimeStart": 1540411200000, + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 1431.111111111111, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 75.59863333333351, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 1351.111111111111, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 235.5986333333335, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 1551.111111111111, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 275.5986333333335, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 1764.4444444444446, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 165.28798888888878, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 2320, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 226.6394222222225, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 2720, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 226.6394222222225, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, + "visibleTimeEnd": 1540591200000, + "visibleTimeStart": 1540501200000, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/get-group-orders.js.snap b/__tests__/utils/calendar/__snapshots__/get-group-orders.js.snap new file mode 100644 index 000000000..08e41b54f --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/get-group-orders.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getGroupOrders works as expected 1`] = ` +Object { + "1": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "2": Object { + "group": Object { + "id": "2", + }, + "index": 1, + }, + "3": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/get-grouped-items.js.snap b/__tests__/utils/calendar/__snapshots__/get-grouped-items.js.snap new file mode 100644 index 000000000..b4a4b264b --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/get-grouped-items.js.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getGroupedItems works as expected 1`] = ` +Object { + "0": Object { + "group": Object { + "id": "1", + }, + "index": 0, + "items": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 907.4074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 78.74857638888886, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 824.074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 245.4152430555556, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 1032.4074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 287.08190972222224, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 1254.6296296296296, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 172.1749884259259, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 1833.3333333333333, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 236.08273148148123, + }, + "id": "2", + }, + ], + }, + "1": Object { + "group": Object { + "id": "2", + }, + "index": 1, + "items": Array [], + }, + "2": Object { + "group": Object { + "id": "3", + }, + "index": 2, + "items": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 2250, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 236.08273148148146, + }, + "id": "3", + }, + ], + }, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/get-item-dimensions.js.snap b/__tests__/utils/calendar/__snapshots__/get-item-dimensions.js.snap new file mode 100644 index 000000000..06f90cb2d --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/get-item-dimensions.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getItemDimensions should evaluate dimensions for an item 1`] = ` +Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 60, + "left": 1449.074074074074, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": null, + "width": 78.74857638888898, + }, + "id": "0", +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/group-no-stack.js.snap b/__tests__/utils/calendar/__snapshots__/group-no-stack.js.snap new file mode 100644 index 000000000..90095e18b --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/group-no-stack.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`groupNoStack works as expected 1`] = ` +Object { + "groupHeight": 0, + "itemTop": 7.5, + "verticalMargin": 0, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/group-stack.js.snap b/__tests__/utils/calendar/__snapshots__/group-stack.js.snap new file mode 100644 index 000000000..6f22034d7 --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/group-stack.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`groupStack works as expected 1`] = ` +Object { + "groupHeight": 0, + "itemTop": 7.5, + "verticalMargin": 37.5, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/stack-all.js.snap b/__tests__/utils/calendar/__snapshots__/stack-all.js.snap new file mode 100644 index 000000000..7121386fb --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/stack-all.js.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stackAll works as expected not stacked 1`] = ` +Object { + "groupHeights": Array [ + 60, + 60, + 60, + ], + "groupTops": Array [ + 0, + 60, + 120, + ], + "height": 180, +} +`; + +exports[`stackAll works as expected stacked 1`] = ` +Object { + "groupHeights": Array [ + 60, + 60, + 60, + ], + "groupTops": Array [ + 0, + 60, + 120, + ], + "height": 180, +} +`; diff --git a/__tests__/utils/calendar/__snapshots__/stack-group.js.snap b/__tests__/utils/calendar/__snapshots__/stack-group.js.snap new file mode 100644 index 000000000..bbe832509 --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/stack-group.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stackGroup should not stack list of items 1`] = ` +Object { + "groupHeight": 0, + "verticalMargin": 0, +} +`; + +exports[`stackGroup should stack list of items 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 new file mode 100644 index 000000000..8d27dc42d --- /dev/null +++ b/__tests__/utils/calendar/__snapshots__/stack-items.js.snap @@ -0,0 +1,631 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stackItems should stack items while moving an item 1`] = ` +Object { + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540543600000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 4472.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 236.24572916666602, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 4097.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 736.245729166666, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 4722.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 861.245729166666, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 5388.888888888889, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 516.5249652777784, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 7125, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 708.2481944444444, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 8375, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 625, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, +} +`; + +exports[`stackItems should stack items while resize item left 1`] = ` +Object { + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540543600000, + "collisionWidth": 3203877, + "height": 22.5, + "left": 4472.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 111.24572916666602, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 4097.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 736.245729166666, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 4722.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 861.245729166666, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 5388.888888888889, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 516.5249652777784, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 7125, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 708.2481944444444, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 8375, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 625, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, +} +`; + +exports[`stackItems should stack items while resize item right 1`] = ` +Object { + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 10403877, + "height": 22.5, + "left": 4347.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 361.245729166666, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 4097.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 736.245729166666, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 4722.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 861.245729166666, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 5388.888888888889, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 516.5249652777784, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 7125, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 708.2481944444444, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 8375, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 625, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, +} +`; + +exports[`stackItems work as expected 1`] = ` +Object { + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 4347.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 236.24572916666602, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 4097.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 736.245729166666, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 4722.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 861.245729166666, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 5388.888888888889, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 37.5, + "width": 516.5249652777784, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 7125, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 7.5, + "width": 708.2481944444444, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 8375, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 105, + "width": 625, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 67.5, + 30, + 37.5, + ], + "groupTops": Array [ + 0, + 67.5, + 97.5, + ], + "height": 135, +} +`; + +exports[`stackItems work as expected no stack 1`] = ` +Object { + "dimensionItems": Array [ + Object { + "dimensions": Object { + "collisionLeft": 1540540000000, + "collisionWidth": 6803877, + "height": 22.5, + "left": 4347.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 3.75, + "width": 236.24572916666602, + }, + "id": "0", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540532800000, + "collisionWidth": 21203877, + "height": 22.5, + "left": 4097.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 3.75, + "width": 736.245729166666, + }, + "id": "5", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540550800000, + "collisionWidth": 24803877, + "height": 22.5, + "left": 4722.222222222223, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 3.75, + "width": 861.245729166666, + }, + "id": "6", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540570000000, + "collisionWidth": 14875919, + "height": 22.5, + "left": 5388.888888888889, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 3.75, + "width": 516.5249652777784, + }, + "id": "1", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540620000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 7125, + "order": Object { + "group": Object { + "id": "1", + }, + "index": 0, + }, + "stack": true, + "top": 3.75, + "width": 708.2481944444444, + }, + "id": "2", + }, + Object { + "dimensions": Object { + "collisionLeft": 1540656000000, + "collisionWidth": 20397548, + "height": 22.5, + "left": 8375, + "order": Object { + "group": Object { + "id": "3", + }, + "index": 2, + }, + "stack": true, + "top": 63.75, + "width": 625, + }, + "id": "3", + }, + ], + "groupHeights": Array [ + 30, + 30, + 30, + ], + "groupTops": Array [ + 0, + 30, + 60, + ], + "height": 90, +} +`; diff --git a/__tests__/utils/calendar/calculate-dimensions.js b/__tests__/utils/calendar/calculate-dimensions.js index 8de052f50..5b5a18e58 100644 --- a/__tests__/utils/calendar/calculate-dimensions.js +++ b/__tests__/utils/calendar/calculate-dimensions.js @@ -27,48 +27,15 @@ describe('calculateDimensions', () => { }) }) - it('the item is moved and snapped to the grid', () => { - const dimension = calculateDimensions({ - itemTimeStart: 200, - itemTimeEnd: 300, - isDragging: true, - isResizing: false, - canvasTimeStart: 0, - canvasTimeEnd: 500, - canvasWidth: 500, - dragSnap: 10, - dragTime: 192, - resizingItem: false, - resizingEdge: false, - resizeTime: false, // we are not resizing right now - visibleTimeStart: 0, - visibleTimeEnd: 500 - }) - - expect(dimension).toMatchObject({ - collisionLeft: 192, - collisionWidth: 100, - left: 192, - width: 100 - }) - }) - it('items timeStart is less than canvasTimeStart', () => { let example = { itemTimeStart: 0, itemTimeEnd: 300, - isDragging: false, - isResizing: false, canvasTimeStart: 100, canvasTimeEnd: 500, - canvasWidth: 400, - dragSnap: 0, - dragTime: false, // we are not draging right now - resizingItem: false, - resizingEdge: false, - resizeTime: false, // we are not resizing right now visibleTimeStart: 100, - visibleTimeEnd: 500 + visibleTimeEnd: 500, + canvasWidth: 400, } expect(calculateDimensions(example)).toMatchObject({ @@ -83,18 +50,11 @@ describe('calculateDimensions', () => { let example = { itemTimeStart: 400, itemTimeEnd: 700, - isDragging: false, - isResizing: false, canvasTimeStart: 100, canvasTimeEnd: 500, - canvasWidth: 400, - dragSnap: 0, - dragTime: false, // we are not draging right now - resizingItem: false, - resizingEdge: false, - resizeTime: false, // we are not resizing right now visibleTimeStart: 100, - visibleTimeEnd: 500 + visibleTimeEnd: 500, + canvasWidth: 400, } expect(calculateDimensions(example)).toMatchObject({ @@ -108,18 +68,11 @@ describe('calculateDimensions', () => { let example = { itemTimeStart: 0, // item range extends before and after canvas itemTimeEnd: 600, - isDragging: false, - isResizing: false, canvasTimeStart: 100, canvasTimeEnd: 500, canvasWidth: 400, - dragSnap: 0, - dragTime: false, // we are not draging right now - resizingItem: false, - resizingEdge: false, - resizeTime: false, // we are not resizing right now visibleTimeStart: 100, - visibleTimeEnd: 500 + visibleTimeEnd: 500, } expect(calculateDimensions(example)).toMatchObject({ @@ -129,80 +82,4 @@ describe('calculateDimensions', () => { width: 400 }) }) - - it('the item is dragged', () => { - const dimension = calculateDimensions({ - itemTimeStart: 200, - itemTimeEnd: 300, - isDragging: true, - isResizing: false, - canvasTimeStart: 0, - canvasTimeEnd: 500, - canvasWidth: 500, - dragSnap: 0, - dragTime: 300, - resizingItem: false, - resizingEdge: false, - resizeTime: false, // we are not resizing right now - visibleTimeStart: 0, - visibleTimeEnd: 500 - }) - - expect(dimension).toMatchObject({ - collisionLeft: 300, - collisionWidth: 100, - left: 300, - width: 100 - }) - }) - - it('the item is resized right', () => { - const dimension = calculateDimensions({ - itemTimeStart: 200, - itemTimeEnd: 300, - isDragging: false, - isResizing: true, - canvasTimeStart: 0, - canvasTimeEnd: 500, - canvasWidth: 500, - dragSnap: 0, - dragTime: false, // we are not draging right now - resizingItem: true, - resizingEdge: 'right', - resizeTime: 250 - }) - - expect(dimension).toMatchObject({ - collisionLeft: 200, - collisionWidth: 50, - left: 200, - width: 50 - }) - }) - - it('the item is resized left', () => { - const dimension = calculateDimensions({ - itemTimeStart: 200, - itemTimeEnd: 300, - isDragging: false, - isResizing: true, - canvasTimeStart: 0, - canvasTimeEnd: 500, - canvasWidth: 500, - dragSnap: 0, - dragTime: false, // we are not draging right now - resizingItem: true, - resizingEdge: 'left', - resizeTime: 210, - visibleTimeStart: 0, - visibleTimeEnd: 500 - }) - - expect(dimension).toMatchObject({ - collisionLeft: 210, - collisionWidth: 90, - left: 210, - width: 90 - }) - }) }) diff --git a/__tests__/utils/calendar/calculate-interaction-new-times.js b/__tests__/utils/calendar/calculate-interaction-new-times.js new file mode 100644 index 000000000..5d7cbe804 --- /dev/null +++ b/__tests__/utils/calendar/calculate-interaction-new-times.js @@ -0,0 +1,57 @@ +import { calculateInteractionNewTimes } from 'lib/utility/calendar' + +describe('calculateInteractionNewTimes', () => { + it('should return the original time start and end if no interaction', () => { + expect( + calculateInteractionNewTimes({ + itemTimeStart: 200, + itemTimeEnd: 300, + dragTime: false, + isDragging: false, + isResizing: false, + resizingEdge: false, + resizeTime: false + }) + ).toMatchObject([200, 300]) + }) + it('should calculate new time start and end if being moved', () => { + expect( + calculateInteractionNewTimes({ + itemTimeStart: 200, + itemTimeEnd: 300, + dragTime: 192, + isDragging: true, + isResizing: false, + resizingEdge: false, + resizeTime: false + }) + ).toMatchObject([192, 292]) + }) + it('should calculate new time start and end if being resized right', () => { + expect( + calculateInteractionNewTimes({ + itemTimeStart: 200, + itemTimeEnd: 300, + dragTime: false, + isDragging: false, + isResizing: true, + resizingEdge: 'right', + resizeTime: 250 + }) + ).toMatchObject([200, 250]) + }) + it('should calculate new time start and end if being resized left', () => { + expect( + calculateInteractionNewTimes({ + itemTimeStart: 200, + itemTimeEnd: 300, + dragTime: false, + isDragging: false, + isResizing: true, + resizingEdge: 'left', + resizeTime: 210 + }) + ).toMatchObject([210, 300]) + }) + xit('the item is moved and snapped to the grid', () => {}) +}) diff --git a/__tests__/utils/calendar/calculate-scroll-canvas.js b/__tests__/utils/calendar/calculate-scroll-canvas.js index 6ab8e051a..ed5ee7048 100644 --- a/__tests__/utils/calendar/calculate-scroll-canvas.js +++ b/__tests__/utils/calendar/calculate-scroll-canvas.js @@ -1,68 +1,9 @@ import { calculateScrollCanvas } from 'lib/utility/calendar' +import {defaultKeys} from 'lib/default-config' import moment from 'moment' +import {items, groups} from '../../../__fixtures__/itemsAndGroups' +import {props, state, visibleTimeStart, visibleTimeEnd} from '../../../__fixtures__/stateAndProps' -const keys = { - groupIdKey: 'id', - groupTitleKey: 'title', - groupRightTitleKey: 'rightTitle', - itemIdKey: 'id', - itemTitleKey: 'title', - itemDivTitleKey: 'title', - itemGroupKey: 'group', - itemTimeStartKey: 'start', - itemTimeEndKey: 'end' -} -const props = { - keys, - lineHeight: 30, - stackItems: true, - itemHeightRatio: 0.75 -} - -const visibleTimeStart = moment('2018-10-26T00:00:00.000') -const visibleTimeEnd = moment('2018-10-27T00:00:00.000') - -const state = { - draggingItem: undefined, - dragTime: null, - resizingItem: null, - resizingEdge: null, - resizeTime: null, - newGroupOrder: null, - canvasTimeStart: moment('2018-10-25T00:00:00.000').valueOf(), - visibleTimeEnd: visibleTimeEnd.valueOf(), - visibleTimeStart: visibleTimeStart.valueOf() -} - -const items = [ - { - id: '0', - group: '1', - start: moment('2018-10-26T10:46:40.000').valueOf(), - end: moment('2018-10-26T12:40:03.877').valueOf(), - canMove: false, - canResize: false - }, - { - id: '1', - group: '1', - start: moment('2018-10-26T19:06:40.000').valueOf(), - end: moment('2018-10-26T23:14:35.919').valueOf(), - canMove: true, - canResize: 'both' - }, - { - id: '2', - group: '1', - start: moment('2018-10-27T08:00:00.000').valueOf(), - end: moment('2018-10-27T13:39:57.548').valueOf(), - canMove: false, - canResize: false, - className: '' - } -] - -const groups = [{ id: '1' }, { id: '2' }] describe('calculateScrollCanvas', () => { it('should calculate new scroll state', () => { @@ -81,6 +22,20 @@ describe('calculateScrollCanvas', () => { expect(result).toHaveProperty('visibleTimeEnd') expect(result).toHaveProperty('dimensionItems') }) + it('should calculate new scroll state correctly', () => { + const newStartTime = visibleTimeStart.clone().add(13, 'h') + const newEndTime = visibleTimeEnd.clone().add(13, 'h') + const result = calculateScrollCanvas( + newStartTime.valueOf(), + newEndTime.valueOf(), + false, + items, + groups, + props, + state + ) + expect(result).toMatchSnapshot() + }) it('should skip new calculation if new visible start and visible end in canvas', () => { const newStartTime = visibleTimeStart.clone().add(1, 'h') const newEndTime = visibleTimeEnd.clone().add(1, 'h') @@ -129,4 +84,18 @@ describe('calculateScrollCanvas', () => { expect(result).toHaveProperty('visibleTimeEnd') expect(result).toHaveProperty('dimensionItems') }) + it('should calculate new state if zoom changed correctly', () => { + const newStartTime = visibleTimeStart.clone() + const newEndTime = visibleTimeEnd.clone().add(1, 'h') + const result = calculateScrollCanvas( + newStartTime.valueOf(), + newEndTime.valueOf(), + false, + items, + groups, + props, + state + ) + expect(result).toMatchSnapshot() + }) }) diff --git a/__tests__/utils/calendar/get-group-orders.js b/__tests__/utils/calendar/get-group-orders.js index c00addbac..6a2d4a459 100644 --- a/__tests__/utils/calendar/get-group-orders.js +++ b/__tests__/utils/calendar/get-group-orders.js @@ -1,6 +1,8 @@ -/* eslint-disable */ import { getGroupOrders } from 'lib/utility/calendar' - +import { groups} from '../../../__fixtures__/itemsAndGroups' +import {defaultKeys} from 'lib/default-config' describe('getGroupOrders', () => { - xit('WRITE UNIT TEST HERE', () => {}) + it('works as expected', () => { + expect(getGroupOrders(groups, defaultKeys)).toMatchSnapshot() + }) }) diff --git a/__tests__/utils/calendar/get-grouped-items.js b/__tests__/utils/calendar/get-grouped-items.js index e9ad1ac6a..e9bf0957f 100644 --- a/__tests__/utils/calendar/get-grouped-items.js +++ b/__tests__/utils/calendar/get-grouped-items.js @@ -1,6 +1,8 @@ -/* eslint-disable */ import { getGroupedItems } from 'lib/utility/calendar' +import {orderedGroups, dimensionItems} from '../../../__fixtures__/groupOrderAndItemDimentions' describe('getGroupedItems', () => { - xit('WRITE UNIT TEST HERE', () => {}) + it('works as expected', () => { + expect(getGroupedItems(dimensionItems,orderedGroups)).toMatchSnapshot() + }) }) diff --git a/__tests__/utils/calendar/get-item-dimensions.js b/__tests__/utils/calendar/get-item-dimensions.js new file mode 100644 index 000000000..4a47aa021 --- /dev/null +++ b/__tests__/utils/calendar/get-item-dimensions.js @@ -0,0 +1,22 @@ +import { getItemDimensions } from 'lib/utility/calendar' +import {items} from '../../../__fixtures__/itemsAndGroups' +import {state} from '../../../__fixtures__/stateAndProps' +import { defaultKeys } from 'lib/default-config' +import {orderedGroups} from '../../../__fixtures__/groupOrderAndItemDimentions' + +describe('getItemDimensions', () => { + it("should evaluate dimensions for an item", ()=>{ + const item = items[0] + const {canvasTimeStart, canvasTimeEnd} = state + expect(getItemDimensions({ + item, + keys: defaultKeys, + canvasTimeStart, + canvasTimeEnd, + canvasWidth: 3000, + groupOrders: orderedGroups, + lineHeight: 60, + itemHeightRatio: 1, + })).toMatchSnapshot() + }) +}) diff --git a/__tests__/utils/calendar/get-item-with-interactions.js b/__tests__/utils/calendar/get-item-with-interactions.js new file mode 100644 index 000000000..f0c17067d --- /dev/null +++ b/__tests__/utils/calendar/get-item-with-interactions.js @@ -0,0 +1,110 @@ +import { getItemWithInteractions } from 'lib/utility/calendar' +import { items, groups } from '../../../__fixtures__/itemsAndGroups' +import { defaultKeys } from 'lib/default-config' + +describe('getItemWithInteractions', () => { + it('should return the same item if no interaction occurred', () => { + const item = items[0] + expect( + getItemWithInteractions({ + item, + keys: defaultKeys, + draggingItem: undefined, + resizingItem: undefined, + dragTime: false, + resizingEdge: false, + resizeTime: false, + groups, + newGroupOrder: 0 + }) + ).toBe(item) + }) + it('should return new item with new start and end time if dragged with no changed group', () => { + const item = items[0] + //moved 1 hour + const dragOffset = 60 * 60 * 1000 + expect( + getItemWithInteractions({ + item, + keys: defaultKeys, + draggingItem: item.id, + resizingItem: undefined, + dragTime: item.start_time + dragOffset, + resizingEdge: false, + resizeTime: false, + groups, + newGroupOrder: 0 + }) + ).toMatchObject({ + ...item, + start_time: item.start_time + dragOffset, + end_time: item.end_time + dragOffset, + group: item.group + }) + }) + it('should return new item with new start and end time if dragged with changed group', () => { + const item = items[0] + //moved 1 hour + const dragOffset = 60 * 60 * 1000 + expect( + getItemWithInteractions({ + item, + keys: defaultKeys, + draggingItem: item.id, + resizingItem: undefined, + dragTime: item.start_time + dragOffset, + resizingEdge: false, + resizeTime: false, + groups, + newGroupOrder: 1 + }) + ).toMatchObject({ + ...item, + start_time: item.start_time + dragOffset, + end_time: item.end_time + dragOffset, + group: groups[1].id + }) + }) + it('should return new item with new start time if resized left', () => { + const item = items[0] + //moved 1 hour + const dragOffset = 60 * 60 * 1000 + expect( + getItemWithInteractions({ + item, + keys: defaultKeys, + draggingItem: undefined, + resizingItem: item.id, + dragTime: undefined, + resizingEdge: 'left', + resizeTime: item.start_time + dragOffset, + groups, + newGroupOrder: 0 + }) + ).toMatchObject({ + ...item, + start_time: item.start_time + dragOffset, + }) + }) + it('should return new item with end start time if resized right', () => { + const item = items[0] + //moved 1 hour + const dragOffset = 60 * 60 * 1000 + expect( + getItemWithInteractions({ + item, + keys: defaultKeys, + draggingItem: undefined, + resizingItem: item.id, + dragTime: undefined, + resizingEdge: 'right', + resizeTime: item.end_time + dragOffset, + groups, + newGroupOrder: 0 + }) + ).toMatchObject({ + ...item, + end_time: item.end_time + dragOffset, + }) + }) +}) diff --git a/__tests__/utils/calendar/group-no-stack.js b/__tests__/utils/calendar/group-no-stack.js new file mode 100644 index 000000000..758927266 --- /dev/null +++ b/__tests__/utils/calendar/group-no-stack.js @@ -0,0 +1,11 @@ +import {groupNoStack} from 'lib/utility/calendar' +import {orderedGroups, dimensionItems} from '../../../__fixtures__/groupOrderAndItemDimentions' + +describe('groupNoStack', ()=>{ + it('works as expected', ()=>{ + const groupHeight = 0; + const totalHeight = 0; + const index = 0; + expect(groupNoStack(60, dimensionItems[index], groupHeight, totalHeight, index)).toMatchSnapshot() + }) +}) diff --git a/__tests__/utils/calendar/group-stack.js b/__tests__/utils/calendar/group-stack.js new file mode 100644 index 000000000..2abd4eea6 --- /dev/null +++ b/__tests__/utils/calendar/group-stack.js @@ -0,0 +1,11 @@ +import {groupStack} from 'lib/utility/calendar' +import {orderedGroups, dimensionItems} from '../../../__fixtures__/groupOrderAndItemDimentions' + +describe('groupStack', ()=>{ + it('works as expected', ()=>{ + const groupHeight = 0; + const totalHeight = 0; + const index = 0; + expect(groupStack(60, dimensionItems[index], dimensionItems, groupHeight, totalHeight, index)).toMatchSnapshot() + }) +}) diff --git a/__tests__/utils/calendar/no-stack.js b/__tests__/utils/calendar/no-stack.js deleted file mode 100644 index 178c28983..000000000 --- a/__tests__/utils/calendar/no-stack.js +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable */ -import { nostack } from 'lib/utility/calendar' - -describe('nostack', () => { - xit('WRITE UNIT TEST HERE', () => {}) -}) diff --git a/__tests__/utils/calendar/stack-all.js b/__tests__/utils/calendar/stack-all.js new file mode 100644 index 000000000..7d2ba07d1 --- /dev/null +++ b/__tests__/utils/calendar/stack-all.js @@ -0,0 +1,13 @@ +import { stackAll } from 'lib/utility/calendar' +import {orderedGroups, dimensionItems} from '../../../__fixtures__/groupOrderAndItemDimentions' + +const lineHeight = 60 + +describe('stackAll', () => { + it('works as expected stacked', () => { + expect(stackAll(dimensionItems, orderedGroups, lineHeight, true)).toMatchSnapshot() + }) + it('works as expected not stacked', () => { + expect(stackAll(dimensionItems, orderedGroups, lineHeight, false)).toMatchSnapshot() + }) +}) diff --git a/__tests__/utils/calendar/stack-group.js b/__tests__/utils/calendar/stack-group.js new file mode 100644 index 000000000..be91fa399 --- /dev/null +++ b/__tests__/utils/calendar/stack-group.js @@ -0,0 +1,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 not stack list of items', ()=>{ + expect(stackGroup(dimensionItems, false, 30, 0)).toMatchSnapshot() + }) +}) \ No newline at end of file diff --git a/__tests__/utils/calendar/stack-items.js b/__tests__/utils/calendar/stack-items.js new file mode 100644 index 000000000..fb3f07ff5 --- /dev/null +++ b/__tests__/utils/calendar/stack-items.js @@ -0,0 +1,143 @@ +import { stackTimelineItems } from 'lib/utility/calendar' +import { items, groups } from '../../../__fixtures__/itemsAndGroups' +import { + props, + state, + stateMoveItem, + stateResizeItemLeft, + stateResizeItemRight, + propsNoStack, +} from '../../../__fixtures__/stateAndProps' +describe('stackItems', () => { + it('work as expected', () => { + expect( + stackTimelineItems( + items, + groups, + 9000, + state.canvasTimeStart, + state.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + state.draggingItem, + state.resizingItem, + state.dragTime, + state.resizingEdge, + state.resizeTime, + state.newGroupOrder + ) + ).toMatchSnapshot() + }) + it('work as expected no stack', () => { + expect( + stackTimelineItems( + items, + groups, + 9000, + state.canvasTimeStart, + state.canvasTimeEnd, + propsNoStack.keys, + propsNoStack.lineHeight, + propsNoStack.itemHeightRatio, + propsNoStack.stackItems, + state.draggingItem, + state.resizingItem, + state.dragTime, + state.resizingEdge, + state.resizeTime, + state.newGroupOrder + ) + ).toMatchSnapshot() + }) + it('should stack items while moving an item', () => { + expect( + stackTimelineItems( + items, + groups, + 9000, + stateMoveItem.canvasTimeStart, + stateMoveItem.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + stateMoveItem.draggingItem, + stateMoveItem.resizingItem, + stateMoveItem.dragTime, + stateMoveItem.resizingEdge, + stateMoveItem.resizeTime, + stateMoveItem.newGroupOrder + ) + ).toMatchSnapshot() + }) + it('should stack items while resize item left', () => { + expect( + stackTimelineItems( + items, + groups, + 9000, + stateResizeItemLeft.canvasTimeStart, + stateResizeItemLeft.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + stateResizeItemLeft.draggingItem, + stateResizeItemLeft.resizingItem, + stateResizeItemLeft.dragTime, + stateResizeItemLeft.resizingEdge, + stateResizeItemLeft.resizeTime, + stateResizeItemLeft.newGroupOrder + ) + ).toMatchSnapshot() + }) + it('should stack items while resize item right', () => { + expect( + stackTimelineItems( + items, + groups, + 9000, + stateResizeItemRight.canvasTimeStart, + stateResizeItemRight.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + stateResizeItemRight.draggingItem, + stateResizeItemRight.resizingItem, + stateResizeItemRight.dragTime, + stateResizeItemRight.resizingEdge, + stateResizeItemRight.resizeTime, + stateResizeItemRight.newGroupOrder + ) + ).toMatchSnapshot() + }) + it('should return empty dimensions if groups are empty', () => { + expect( + stackTimelineItems( + items, + [], + 9000, + state.canvasTimeStart, + state.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + state.draggingItem, + state.resizingItem, + state.dragTime, + state.resizingEdge, + state.resizeTime, + state.newGroupOrder + ) + ).toMatchObject({ + dimensionItems: [], + height: 0, + groupHeights: [], + groupTops: [] + }) + }) +}) diff --git a/__tests__/utils/calendar/stack.js b/__tests__/utils/calendar/stack.js deleted file mode 100644 index 15c4e6e9d..000000000 --- a/__tests__/utils/calendar/stack.js +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable */ -import { stack } from 'lib/utility/calendar' - -describe('stack', () => { - xit('WRITE UNIT TEST HERE', () => {}) -}) diff --git a/demo/app/demo-headers/index.js b/demo/app/demo-headers/index.js new file mode 100644 index 000000000..0fafd2e29 --- /dev/null +++ b/demo/app/demo-headers/index.js @@ -0,0 +1,405 @@ +/* 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) => { + 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()) + } + + 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 + > + + + {({ 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-performance/index.js b/demo/app/demo-performance/index.js index a1c4922ed..97c940d00 100644 --- a/demo/app/demo-performance/index.js +++ b/demo/app/demo-performance/index.js @@ -37,6 +37,8 @@ export default class App extends Component { .endOf('month') .toDate() + groups[0].stackItems = false; + groups[0].height = 300; this.state = { groups, items, 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 63e41e3af..e0673619b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.js b/src/index.js index 15936d2da..33839f0d7 100644 --- a/src/index.js +++ b/src/index.js @@ -6,5 +6,9 @@ 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 {default as ItemHeader} from './lib/headers/ItemHeader' export default Timeline diff --git a/src/lib/Timeline.js b/src/lib/Timeline.js index 0e45445e7..b152356e2 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' @@ -16,9 +15,11 @@ import windowResizeDetector from '../resize-detector/window' import { getMinUnit, getNextUnit, - stackItems, + calculateTimeForXPosition, calculateScrollCanvas, - calculateTimeForXPosition + getCanvasBoundariesFromVisibleTime, + getCanvasWidth, + stackTimelineItems, } from './utility/calendar' import { _get, _length } from './utility/generic' import { @@ -29,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, @@ -254,10 +257,9 @@ export default class ReactCalendarTimeline extends Component { width, visibleTimeStart, visibleTimeEnd, - canvasTimeStart + canvasTimeStart, + canvasTimeEnd, } = this.state - const zoom = visibleTimeEnd - visibleTimeStart - const canvasTimeEnd = canvasTimeStart + zoom * 3 return { timelineWidth: width, @@ -287,13 +289,14 @@ export default class ReactCalendarTimeline extends Component { ) } + const [canvasTimeStart, canvasTimeEnd] = getCanvasBoundariesFromVisibleTime(visibleTimeStart, visibleTimeEnd) + this.state = { width: 1000, - visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd, - canvasTimeStart: visibleTimeStart - (visibleTimeEnd - visibleTimeStart), - + canvasTimeStart: canvasTimeStart, + canvasTimeEnd: canvasTimeEnd, selectedItem: null, dragTime: null, dragGroupTitle: null, @@ -302,15 +305,24 @@ export default class ReactCalendarTimeline extends Component { resizingEdge: null } - const { dimensionItems, height, groupHeights, groupTops } = stackItems( - props.items, - props.groups, - this.state.canvasTimeStart, - this.state.visibleTimeStart, - this.state.visibleTimeEnd, - this.state.width, - this.props, - this.state + const canvasWidth= getCanvasWidth(this.state.width) + + const { dimensionItems, height, groupHeights, groupTops } = stackTimelineItems( + props.items, + props.groups, + canvasWidth, + this.state.canvasTimeStart, + this.state.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + this.state.draggingItem, + this.state.resizingItem, + this.state.dragTime, + this.state.resizingEdge, + this.state.resizeTime, + this.state.newGroupOrder ) /* eslint-disable react/no-direct-mutation-state */ @@ -343,16 +355,11 @@ export default class ReactCalendarTimeline extends Component { } static getDerivedStateFromProps(nextProps, prevState) { - const { - visibleTimeStart, - visibleTimeEnd, - items, - groups - } = nextProps + const { visibleTimeStart, visibleTimeEnd, items, groups } = nextProps // 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 } // if the items or groups have changed we must re-render const forceUpdate = items !== prevState.items || groups !== prevState.groups @@ -360,7 +367,8 @@ export default class ReactCalendarTimeline extends Component { // We are a controlled component if (visibleTimeStart && visibleTimeEnd) { // Get the new canvas position - Object.assign(derivedState, + Object.assign( + derivedState, calculateScrollCanvas( visibleTimeStart, visibleTimeEnd, @@ -369,18 +377,29 @@ 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) Object.assign(derivedState, - stackItems(items, - groups, + stackTimelineItems( + items, + groups, + canvasWidth, prevState.canvasTimeStart, - prevState.visibleTimeStart, - prevState.visibleTimeEnd, - prevState.width, - nextProps, - prevState)) + prevState.canvasTimeEnd, + nextProps.keys, + nextProps.lineHeight, + nextProps.itemHeightRatio, + nextProps.stackItems, + prevState.draggingItem, + prevState.resizingItem, + prevState.dragTime, + prevState.resizingEdge, + prevState.resizeTime, + prevState.newGroupOrder + )) } return derivedState @@ -389,20 +408,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 } @@ -418,16 +446,23 @@ export default class ReactCalendarTimeline extends Component { } = this.container.getBoundingClientRect() let width = containerWidth - props.sidebarWidth - props.rightSidebarWidth - - const { dimensionItems, height, groupHeights, groupTops } = stackItems( + const canvasWidth = getCanvasWidth(width) + const { dimensionItems, height, groupHeights, groupTops } = stackTimelineItems( props.items, props.groups, + canvasWidth, this.state.canvasTimeStart, - this.state.visibleTimeStart, - this.state.visibleTimeEnd, - width, - this.props, - this.state + this.state.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + this.state.draggingItem, + this.state.resizingItem, + this.state.dragTime, + this.state.resizingEdge, + this.state.resizeTime, + this.state.newGroupOrder ) // this is needed by dragItem since it uses pageY from the drag events @@ -438,13 +473,13 @@ export default class ReactCalendarTimeline extends Component { dimensionItems, height, groupHeights, - groupTops + groupTops, }) - + this.scrollComponent.scrollLeft = width - this.headerRef.scrollLeft = width + this.scrollHeaderRef.scrollLeft = width } - + onScroll = scrollX => { const width = this.state.width let newScrollX = scrollX @@ -457,14 +492,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 || @@ -478,7 +513,6 @@ export default class ReactCalendarTimeline extends Component { } } - // called when the visible time changes updateScrollCanvas = ( visibleTimeStart, @@ -489,17 +523,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) => { @@ -520,31 +556,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, @@ -594,21 +615,16 @@ export default class ReactCalendarTimeline extends Component { const { width, canvasTimeStart, - visibleTimeStart, - visibleTimeEnd + canvasTimeEnd, } = this.state // this gives us distance from left of row element, so event is in // context of the row element, not client or page const { offsetX } = e.nativeEvent - // FIXME: DRY up way to calculate canvasTimeEnd - const zoom = visibleTimeEnd - visibleTimeStart - const canvasTimeEnd = zoom * 3 + canvasTimeStart - let time = calculateTimeForXPosition( canvasTimeStart, canvasTimeEnd, - width * 3, + getCanvasWidth(width), offsetX ) time = Math.floor(time / dragSnap) * dragSnap @@ -744,7 +760,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} /> ) @@ -802,52 +820,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) { @@ -883,6 +862,7 @@ export default class ReactCalendarTimeline extends Component { ) } + groups childrenWithProps( canvasTimeStart, canvasTimeEnd, @@ -928,11 +908,42 @@ export default class ReactCalendarTimeline extends Component { 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 + } + render() { const { items, @@ -950,28 +961,35 @@ export default class ReactCalendarTimeline extends Component { width, visibleTimeStart, visibleTimeEnd, - canvasTimeStart + canvasTimeStart, + canvasTimeEnd, } = this.state let { dimensionItems, height, groupHeights, groupTops } = this.state const zoom = visibleTimeEnd - visibleTimeStart - const canvasTimeEnd = canvasTimeStart + zoom * 3 - const canvasWidth = width * 3 + const canvasWidth = getCanvasWidth(width) const minUnit = getMinUnit(zoom, width, timeSteps) const headerHeight = headerLabelGroupHeight + headerLabelHeight const isInteractingWithItem = !!draggingItem || !!resizingItem if (isInteractingWithItem) { - const stackResults = stackItems( + const stackResults = stackTimelineItems( items, groups, - canvasTimeStart, - visibleTimeStart, - visibleTimeEnd, - width, - this.props, - this.state + canvasWidth, + this.state.canvasTimeStart, + this.state.canvasTimeEnd, + this.props.keys, + this.props.lineHeight, + this.props.itemHeightRatio, + this.props.stackItems, + this.state.draggingItem, + this.state.resizingItem, + this.state.dragTime, + this.state.resizingEdge, + this.state.resizeTime, + this.state.newGroupOrder ) dimensionItems = stackResults.dimensionItems height = stackResults.height @@ -990,83 +1008,82 @@ export default class ReactCalendarTimeline extends Component { canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} + 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..b7c98a559 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 diff --git a/src/lib/columns/Columns.js b/src/lib/columns/Columns.js index 00077c085..81dfa5c8b 100644 --- a/src/lib/columns/Columns.js +++ b/src/lib/columns/Columns.js @@ -92,4 +92,4 @@ export default class Columns extends Component { return
{lines}
} -} +} \ No newline at end of file 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..bc286c8c8 --- /dev/null +++ b/src/lib/headers/CustomHeader.js @@ -0,0 +1,241 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { TimelineHeadersConsumer } from './HeadersContext' +import { TimelineStateConsumer } from '../timeline/TimelineStateContext' +import { iterateTimes } 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 + } + constructor(props) { + super(props) + const { + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod + } = props + const ratio = this.calculateRatio( + canvasWidth, + canvasTimeEnd, + canvasTimeStart + ) + const intervals = this.getHeaderIntervals({ + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + ratio, + }) + + 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 + } = nextProps + const ratio = this.calculateRatio( + canvasWidth, + canvasTimeEnd, + canvasTimeStart + ) + const intervals = this.getHeaderIntervals({ + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + unit, + timeSteps, + showPeriod, + ratio, + }) + + this.setState({ intervals, ratio }) + } + } + + getHeaderIntervals = ({ + canvasTimeStart, + canvasTimeEnd, + unit, + timeSteps, + ratio, + }) => { + const intervals = [] + iterateTimes( + canvasTimeStart, + canvasTimeEnd, + unit, + timeSteps, + (startTime, endTime) => { + const labelWidth = Math.ceil( + (endTime.valueOf() - startTime.valueOf()) * ratio + ) + intervals.push({ + startTime, + endTime, + labelWidth + }) + } + ) + return intervals + } + + rootProps = { + style: { + position: 'relative' + } + } + + getRootProps = (props = {}) => { + const { style } = props + return { + style: Object.assign({}, style ? style : {}, this.rootProps.style) + } + } + + getIntervalProps = (props = {}) => { + const { interval, style } = props + if (!interval) throw new Error("you should provide interval to the prop getter") + const { startTime, labelWidth } = interval + return { + style: this.getIntervalStyle({ + style, + startTime, + labelWidth, + canvasTimeStart: this.props.canvasTimeStart, + unit: this.props.unit, + ratio: this.state.ratio + }), + key: `label-${startTime.valueOf()}` + } + } + + calculateRatio(canvasWidth, canvasTimeEnd, canvasTimeStart) { + return canvasWidth / (canvasTimeEnd - canvasTimeStart) + } + + 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' + } + } + + 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 }) => { + 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..7afb326dd --- /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.Component { + 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) + } else { + 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..5bcc61053 --- /dev/null +++ b/src/lib/headers/HeadersContext.js @@ -0,0 +1,44 @@ +import React from 'react' +import PropTypes from 'prop-types' +import createReactContext from 'create-react-context' +import { noop } from '../utility/generic' +import { LEFT_SIDEBAR_ID, RIGHT_SIDEBAR_ID } from './constants' +import { getNextUnit } from '../utility/calendar' + +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..840f1d54b --- /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..7dc24530c --- /dev/null +++ b/src/lib/headers/TimelineHeaders.js @@ -0,0 +1,115 @@ +import React from 'react' +import { TimelineHeadersConsumer } from './HeadersContext' +import PropTypes from 'prop-types' +import SidebarHeader from './SidebarHeader' +import { RIGHT_VARIANT, LEFT_VARIANT } from './constants' +class TimelineHeaders extends React.Component { + 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 + } + + constructor(props) { + super(props) + } + + getRootStyle = () => { + return { + background: '#c52020', + borderBottom: '1px solid #bbb', + ...this.props.style, + display: 'flex', + width: '100%' + } + } + + getCalendarHeaderStyle = () => { + const { + leftSidebarWidth, + rightSidebarWidth, + calendarHeaderStyle + } = this.props + return { + border: '1px solid #bbb', + ...calendarHeaderStyle, + overflow: 'hidden', + width: `calc(100% - ${leftSidebarWidth + rightSidebarWidth}px)` + } + } + + 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 +}) => ( + + {({ 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 7e28e9e9a..42a6ff9d8 100644 --- a/src/lib/items/Item.js +++ b/src/lib/items/Item.js @@ -27,7 +27,7 @@ export default class Item extends Component { canvasTimeStart: PropTypes.number.isRequired, canvasTimeEnd: PropTypes.number.isRequired, canvasWidth: PropTypes.number.isRequired, - order: PropTypes.number, + order: PropTypes.object, dragSnap: PropTypes.number, minResizeWidth: PropTypes.number, @@ -104,7 +104,8 @@ export default class Item extends Component { nextProps.canvasTimeStart !== this.props.canvasTimeStart || nextProps.canvasTimeEnd !== this.props.canvasTimeEnd || nextProps.canvasWidth !== this.props.canvasWidth || - nextProps.order !== this.props.order || + (nextProps.order ? nextProps.order.index : undefined) !== + (this.props.order ? this.props.order.index : undefined) || nextProps.dragSnap !== this.props.dragSnap || nextProps.minResizeWidth !== this.props.minResizeWidth || nextProps.canChangeGroup !== this.props.canChangeGroup || @@ -184,14 +185,14 @@ export default class Item extends Component { for (var key of Object.keys(groupTops)) { var groupTop = groupTops[key] if (e.pageY - offset + scrolls.scrollTop > groupTop) { - groupDelta = parseInt(key, 10) - order + groupDelta = parseInt(key, 10) - order.index } else { break } } - if (this.props.order + groupDelta < 0) { - return 0 - this.props.order + if (this.props.order.index + groupDelta < 0) { + return 0 - this.props.order.index } else { return groupDelta } @@ -260,7 +261,7 @@ export default class Item extends Component { if (this.state.dragging) { let dragTime = this.dragTime(e) let dragGroupDelta = this.dragGroupDelta(e) - + console.log(dragGroupDelta) if (this.props.moveResizeValidator) { dragTime = this.props.moveResizeValidator( 'move', @@ -273,7 +274,7 @@ export default class Item extends Component { this.props.onDrag( this.itemId, dragTime, - this.props.order + dragGroupDelta + this.props.order.index + dragGroupDelta ) } @@ -299,7 +300,7 @@ export default class Item extends Component { this.props.onDrop( this.itemId, dragTime, - this.props.order + this.dragGroupDelta(e) + this.props.order.index + this.dragGroupDelta(e) ) } diff --git a/src/lib/items/Items.js b/src/lib/items/Items.js index bfd5aa5b3..87173f895 100644 --- a/src/lib/items/Items.js +++ b/src/lib/items/Items.js @@ -81,12 +81,6 @@ export default class Items extends Component { ) } - getGroupOrders() { - const { keys, groups } = this.props - - return getGroupOrders(groups, keys) - } - isSelected(item, itemIdKey) { if (!this.props.selected) { return this.props.selectedItem === _get(item, itemIdKey) @@ -107,10 +101,12 @@ export default class Items extends Component { canvasTimeStart, canvasTimeEnd, dimensionItems, + keys, + groups } = this.props - const { itemIdKey, itemGroupKey } = this.props.keys + const { itemIdKey, itemGroupKey } = keys - const groupOrders = this.getGroupOrders() + const groupOrders = getGroupOrders(groups, keys) const visibleItems = this.getVisibleItems( canvasTimeStart, canvasTimeEnd, 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/row/GroupRows.js b/src/lib/row/GroupRows.js index 6f29a72b7..3b72a3988 100644 --- a/src/lib/row/GroupRows.js +++ b/src/lib/row/GroupRows.js @@ -37,7 +37,6 @@ export default class GroupRows extends Component { onRowContextClick, } = this.props let lines = [] - for (let i = 0; i < lineCount; i++) { lines.push( { console.warn('"getDateFromLeftOffsetPosition" default func is being used') + }, + showPeriod: () => { + console.warn('"showPeriod" default func is being used') } } /* eslint-enable */ @@ -37,7 +40,11 @@ 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, } constructor(props) { @@ -47,13 +54,33 @@ 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, + } = this.props + return { + visibleTimeStart, + visibleTimeEnd, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + timelineUnit, + timelineWidth, + keys, + } // REVIEW, } getLeftOffsetFromDate = date => { diff --git a/src/lib/utility/calendar.js b/src/lib/utility/calendar.js index 8bdc1d1d3..3adcd9fb6 100644 --- a/src/lib/utility/calendar.js +++ b/src/lib/utility/calendar.js @@ -64,7 +64,7 @@ export function iterateTimes(start, end, unit, timeSteps, callback) { if (timeSteps[unit] && timeSteps[unit] > 1) { let value = time.get(unit) - time.set(unit, value - (value % timeSteps[unit])) + time.set(unit, value - value % timeSteps[unit]) } while (time.valueOf() < end) { @@ -154,72 +154,121 @@ export function getNextUnit(unit) { return nextUnits[unit] || '' } -export function calculateDimensions({ +/** + * get the new start and new end time of item that is being + * dragged or resized + * @param {*} itemTimeStart original item time in milliseconds + * @param {*} itemTimeEnd original item time in milliseconds + * @param {*} dragTime new start time if item is dragged in milliseconds + * @param {*} isDragging is item being dragged + * @param {*} isResizing is item being resized + * @param {`right` or `left`} resizingEdge resize edge + * @param {*} resizeTime new resize time in milliseconds + */ +export function calculateInteractionNewTimes({ itemTimeStart, itemTimeEnd, + dragTime, isDragging, isResizing, - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - dragTime, resizingEdge, - resizeTime + resizeTime, }) { + const originalItemRange = itemTimeEnd - itemTimeStart const itemStart = isResizing && resizingEdge === 'left' ? resizeTime : itemTimeStart const itemEnd = isResizing && resizingEdge === 'right' ? resizeTime : itemTimeEnd + return [ + isDragging ? dragTime : itemStart, + isDragging ? dragTime + originalItemRange : itemEnd + ] +} - const itemTimeRange = itemEnd - itemStart - - let newItemStart = isDragging ? dragTime : itemStart +export function calculateDimensions({ + itemTimeStart, + itemTimeEnd, + canvasTimeStart, + canvasTimeEnd, + canvasWidth +}) { + const itemTimeRange = itemTimeEnd - itemTimeStart - const ratio = - 1 / coordinateToTimeRatio(canvasTimeStart, canvasTimeEnd, canvasWidth) + // restrict startTime and endTime to be bounded by canvasTimeStart and canvasTimeEnd + const effectiveStartTime = Math.max(itemTimeStart, canvasTimeStart) + const effectiveEndTime = Math.min(itemTimeEnd, canvasTimeEnd) - // restrict startTime and endTime to be bounded by canvasTimeStart and canasTimeEnd - const effectiveStartTime = Math.max(itemStart, canvasTimeStart) - const effectiveEndTime = Math.min(itemEnd, canvasTimeEnd) - const itemWidth = (effectiveEndTime - effectiveStartTime) * ratio + const left = calculateXPositionForTime( + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + effectiveStartTime + ) + const right = calculateXPositionForTime( + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + effectiveEndTime + ) + const itemWidth = right - left const dimensions = { - left: Math.max(newItemStart - canvasTimeStart, 0) * ratio, + left: left, width: Math.max(itemWidth, 3), - collisionLeft: newItemStart, + collisionLeft: itemTimeStart, collisionWidth: itemTimeRange } return dimensions } +/** + * Get the order of groups based on their keys + * @param {*} groups array of groups + * @param {*} keys the keys object + * @returns Ordered hash of objects with their array index and group + */ export function getGroupOrders(groups, keys) { const { groupIdKey } = keys let groupOrders = {} for (let i = 0; i < groups.length; i++) { - groupOrders[_get(groups[i], groupIdKey)] = i + groupOrders[_get(groups[i], groupIdKey)] = { index: i, group: groups[i] } } return groupOrders } +/** + * Adds items relevant to each group to the result of getGroupOrders + * @param {*} items list of all items + * @param {*} groupOrders the result of getGroupOrders + */ export function getGroupedItems(items, groupOrders) { - var arr = [] - - // Initialize with empty arrays for each group - for (let i = 0; i < Object.keys(groupOrders).length; i++) { - arr[i] = [] + var groupedItems = {} + var keys = Object.keys(groupOrders) + // Initialize with result object for each group + for (let i = 0; i < keys.length; i++) { + const groupOrder = groupOrders[keys[i]] + groupedItems[i] = { + index: groupOrder.index, + group: groupOrder.group, + items: [] + } } + // Populate groups for (let i = 0; i < items.length; i++) { if (items[i].dimensions.order !== undefined) { - arr[items[i].dimensions.order].push(items[i]) + const groupItem = groupedItems[items[i].dimensions.order.index] + if (groupItem) { + groupItem.items.push(items[i]) + } } } - return arr + return groupedItems } export function getVisibleItems(items, canvasTimeStart, canvasTimeEnd, keys) { @@ -247,141 +296,202 @@ export function collision(a, b, lineHeight, collisionPadding = EPSILON) { ) } -export function stack(items, groupOrders, lineHeight, groups) { - var i, iMax - var k = 0 - var totalHeight = 0 - - var groupHeights = [] - var groupTops = [] - - var groupedItems = getGroupedItems(items, groupOrders) - - groupedItems.forEach(function(group) { - var groupVal = groups[k++] - - // calculate new, non-overlapping positions - groupTops.push(totalHeight) - - var groupHeight = 0 - var verticalMargin = 0 - for (i = 0, iMax = group.length; i < iMax; i++) { - var item = group[i] - verticalMargin = lineHeight - item.dimensions.height - - if (item.dimensions.stack && item.dimensions.top === null) { - item.dimensions.top = totalHeight + verticalMargin - groupHeight = Math.max(groupHeight, lineHeight) - do { - var collidingItem = null - for (var j = 0, jj = group.length; j < jj; j++) { - var other = group[j] - if ( - other.dimensions.top !== null && - other !== item && - other.dimensions.stack && - collision(item.dimensions, other.dimensions, lineHeight) - ) { - collidingItem = other - break - } else { - // console.log('dont test', other.top !== null, other !== item, other.stack); - } - } - - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.dimensions.top = collidingItem.dimensions.top + lineHeight - groupHeight = Math.max( - groupHeight, - item.dimensions.top + item.dimensions.height - totalHeight - ) - } - } while (collidingItem) +/** + * Calculate the position of a given item for a group that + * is being stacked + */ +export function groupStack( + lineHeight, + item, + group, + groupHeight, + groupTop, + itemIndex +) { + // calculate non-overlapping positions + let curHeight = groupHeight + let verticalMargin = lineHeight - item.dimensions.height + if (item.dimensions.stack && item.dimensions.top === null) { + item.dimensions.top = groupTop + verticalMargin + curHeight = Math.max(curHeight, lineHeight) + do { + var collidingItem = null + //Items are placed from i=0 onwards, only check items with index < i + for (var j = itemIndex - 1, jj = 0; j >= jj; j--) { + var other = group[j] + if ( + other.dimensions.top !== null && + other.dimensions.stack && + collision(item.dimensions, other.dimensions, lineHeight) + ) { + collidingItem = other + break + } else { + // console.log('dont test', other.top !== null, other !== item, other.stack); + } } - } - if (groupVal.height) { - groupHeights.push(groupVal.height) - totalHeight += groupVal.height - } else { - groupHeights.push(Math.max(groupHeight + verticalMargin, lineHeight)) - totalHeight += Math.max(groupHeight + verticalMargin, lineHeight) - } - }) + if (collidingItem != null) { + // There is a collision. Reposition the items above the colliding element + item.dimensions.top = collidingItem.dimensions.top + lineHeight + curHeight = Math.max( + curHeight, + item.dimensions.top + item.dimensions.height - groupTop + ) + } + } while (collidingItem) + } return { - height: totalHeight, - groupHeights, - groupTops + groupHeight: curHeight, + verticalMargin, + itemTop: item.dimensions.top } } -export function nostack(items, groupOrders, lineHeight, groups) { - var i, - j = 0, - iMax +// Calculate the position of this item for a group that is not being stacked +export function groupNoStack(lineHeight, item, groupHeight, groupTop) { + let verticalMargin = (lineHeight - item.dimensions.height) / 2 + if (item.dimensions.top === null) { + item.dimensions.top = groupTop + verticalMargin + groupHeight = Math.max(groupHeight, lineHeight) + } + return { groupHeight, verticalMargin: 0, itemTop: item.dimensions.top } +} - var totalHeight = 0 +function sum(arr = []) { + return arr.reduce((acc, i) => acc + i, 0) +} +/** + * Stack all groups + * @param {*} items items to be stacked + * @param {*} groupOrders the groupOrders object + * @param {*} lineHeight + * @param {*} stackItems should items be stacked? + */ +export function stackAll(itemsDimensions, groupOrders, lineHeight, stackItems) { var groupHeights = [] var groupTops = [] - var groupedItems = getGroupedItems(items, groupOrders) - - groupedItems.forEach(function(group) { - var groupVal = groups[j++] - - // calculate new, non-overlapping positions - groupTops.push(totalHeight) - - var groupHeight = 0 - for (i = 0, iMax = group.length; i < iMax; i++) { - var item = group[i] - var verticalMargin = (lineHeight - item.dimensions.height) / 2 - - if (item.dimensions.top === null) { - item.dimensions.top = totalHeight + verticalMargin - groupHeight = Math.max(groupHeight, lineHeight) - } - } - - if (groupVal.height) { - groupHeights.push(groupVal.height) - totalHeight += groupVal.height + var groupedItems = getGroupedItems(itemsDimensions, groupOrders) + + for (var index in groupedItems) { + const groupItems = groupedItems[index] + const { items: itemsDimensions, group } = groupItems + const groupTop = sum(groupHeights) + + // Is group being stacked? + const isGroupStacked = + group.stackItems !== undefined ? group.stackItems : stackItems + const { groupHeight, verticalMargin } = stackGroup( + itemsDimensions, + isGroupStacked, + lineHeight, + groupTop + ) + // If group height is overridden, push new height + // Do this late as item position still needs to be calculated + groupTops.push(groupTop) + if (group.height) { + groupHeights.push(group.height) } else { - groupHeights.push(Math.max(groupHeight, lineHeight)) - totalHeight += Math.max(groupHeight, lineHeight) + groupHeights.push(Math.max(groupHeight + verticalMargin, lineHeight)) } - }) + } return { - height: totalHeight, + height: sum(groupHeights), groupHeights, groupTops } } +/** + * + * @param {*} itemsDimensions + * @param {*} isGroupStacked + * @param {*} lineHeight + * @param {*} 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) { + r = groupStack( + lineHeight, + itemsDimensions[itemIndex], + itemsDimensions, + groupHeight, + groupTop, + itemIndex + ) + } else { + r = groupNoStack(lineHeight, itemsDimensions[itemIndex], groupHeight, groupTop) + } + groupHeight = r.groupHeight + verticalMargin = r.verticalMargin + } + return { groupHeight, verticalMargin } +} + /** * Stack the items that will be visible * within the canvas area * @param {item[]} items * @param {group[]} groups + * @param {number} canvasWidth * @param {number} canvasTimeStart - * @param {number} visibleTimeStart - * @param {number} visibleTimeEnd - * @param {number} width - * @param {*} props - * @param {*} state + * @param {number} canvasTimeEnd + * @param {*} keys + * @param {number} lineHeight + * @param {number} itemHeightRatio + * @param {boolean} stackItems + * @param {*} draggingItem + * @param {*} resizingItem + * @param {number} dragTime + * @param {left or right} resizingEdge + * @param {number} resizeTime + * @param {number} newGroupOrder */ -export function stackItems( +export function stackTimelineItems( items, groups, + canvasWidth, canvasTimeStart, - visibleTimeStart, - visibleTimeEnd, - width, - props, - state + canvasTimeEnd, + keys, + lineHeight, + itemHeightRatio, + stackItems, + draggingItem, + resizingItem, + dragTime, + resizingEdge, + resizeTime, + newGroupOrder ) { + const visibleItems = getVisibleItems( + items, + canvasTimeStart, + canvasTimeEnd, + keys + ) + const visibleItemsWithInteraction = visibleItems.map(item => + getItemWithInteractions({ + item, + keys, + draggingItem, + resizingItem, + dragTime, + resizingEdge, + resizeTime, + groups, + newGroupOrder + }) + ) + // if there are no groups return an empty array of dimensions if (groups.length === 0) { return { @@ -392,82 +502,145 @@ export function stackItems( } } - const { keys, lineHeight, stackItems, itemHeightRatio } = props - const { - draggingItem, - dragTime, - resizingItem, - resizingEdge, - resizeTime, - newGroupOrder - } = state - const zoom = visibleTimeEnd - visibleTimeStart - const canvasTimeEnd = canvasTimeStart + zoom * 3 - const canvasWidth = width * 3 - - // Find items that fit within canvasTimeStart and canvasTimeEnd - // this is used when calculating the number of 'lines' each group - // will use. - const visibleItems = getVisibleItems( - items, - canvasTimeStart, - canvasTimeEnd, - keys - ) - // Get the order of groups based on their id key const groupOrders = getGroupOrders(groups, keys) - - - let dimensionItems = visibleItems.reduce((memo, item) => { - const itemId = _get(item, keys.itemIdKey) - const isDragging = itemId === draggingItem - const isResizing = itemId === resizingItem - - let dimension = calculateDimensions({ - itemTimeStart: _get(item, keys.itemTimeStartKey), - itemTimeEnd: _get(item, keys.itemTimeEndKey), - isDragging, - isResizing, - canvasTimeStart, - canvasTimeEnd, - canvasWidth, - dragTime, - resizingEdge, - resizeTime - }) - - if (dimension) { - dimension.top = null - dimension.order = isDragging - ? newGroupOrder - : groupOrders[_get(item, keys.itemGroupKey)] - dimension.stack = !item.isOverlay - dimension.height = lineHeight * itemHeightRatio - dimension.isDragging = isDragging - - memo.push({ - id: itemId, - dimensions: dimension + let dimensionItems = visibleItemsWithInteraction + .map(item => + getItemDimensions({ + item, + keys, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + groupOrders, + lineHeight, + itemHeightRatio }) - } - - return memo - }, []) - - const stackingMethod = stackItems ? stack : nostack - + ) + .filter(item => !!item) // Get a new array of groupOrders holding the stacked items - const { height, groupHeights, groupTops } = stackingMethod( + const { height, groupHeights, groupTops } = stackAll( dimensionItems, groupOrders, lineHeight, - groups + stackItems ) - return { dimensionItems, height, groupHeights, groupTops } } +/** + * get canvas width from visible width + * @param {*} width + * @param {*} buffer + */ +export function getCanvasWidth(width, buffer = 3) { + return width * buffer +} + +/** + * get item's position, dimensions and collisions + * @param {*} item + * @param {*} keys + * @param {*} canvasTimeStart + * @param {*} canvasTimeEnd + * @param {*} canvasWidth + * @param {*} groupOrders + * @param {*} lineHeight + * @param {*} itemHeightRatio + */ +export function getItemDimensions({ + item, + keys, + canvasTimeStart, + canvasTimeEnd, + canvasWidth, + groupOrders, + lineHeight, + itemHeightRatio +}) { + const itemId = _get(item, keys.itemIdKey) + let dimension = calculateDimensions({ + itemTimeStart: _get(item, keys.itemTimeStartKey), + itemTimeEnd: _get(item, keys.itemTimeEndKey), + canvasTimeStart, + canvasTimeEnd, + canvasWidth + }) + if (dimension) { + dimension.top = null + dimension.order = groupOrders[_get(item, keys.itemGroupKey)] + dimension.stack = !item.isOverlay + dimension.height = lineHeight * itemHeightRatio + return { + id: itemId, + dimensions: dimension + } + } +} + +/** + * get new item with changed `itemTimeStart` , `itemTimeEnd` and `itemGroupKey` according to user interaction + * user interaction is dragging an item and resize left and right + * @param {*} item + * @param {*} keys + * @param {*} draggingItem + * @param {*} resizingItem + * @param {*} dragTime + * @param {*} resizingEdge + * @param {*} resizeTime + * @param {*} groups + * @param {*} newGroupOrder + */ +export function getItemWithInteractions({ + item, + keys, + draggingItem, + resizingItem, + dragTime, + resizingEdge, + resizeTime, + groups, + newGroupOrder +}) { + if (!resizingItem && !draggingItem) return item + const itemId = _get(item, keys.itemIdKey) + const isDragging = itemId === draggingItem + const isResizing = itemId === resizingItem + const [itemTimeStart, itemTimeEnd] = calculateInteractionNewTimes({ + itemTimeStart: _get(item, keys.itemTimeStartKey), + itemTimeEnd: _get(item, keys.itemTimeEndKey), + isDragging, + isResizing, + dragTime, + resizingEdge, + resizeTime + }) + const newItem = { + ...item, + [keys.itemTimeStartKey]: itemTimeStart, + [keys.itemTimeEndKey]: itemTimeEnd, + [keys.itemGroupKey]: isDragging + ? _get(groups[newGroupOrder], keys.groupIdKey) + : _get(item, keys.itemGroupKey) + } + return newItem +} + +/** + * get canvas start and end time from visible start and end time + * @param {number} visibleTimeStart + * @param {number} visibleTimeEnd + */ +export function getCanvasBoundariesFromVisibleTime( + visibleTimeStart, + visibleTimeEnd +) { + const zoom = visibleTimeEnd - visibleTimeStart + const canvasTimeStart = visibleTimeStart - (visibleTimeEnd - visibleTimeStart) + const canvasTimeEnd = canvasTimeStart + zoom * 3 + return [canvasTimeStart, canvasTimeEnd] +} + /** * Get the the canvas area for a given visible time * Will shift the start/end of the canvas if the visible time @@ -503,20 +676,38 @@ export function calculateScrollCanvas( visibleTimeEnd <= oldCanvasTimeStart + oldZoom * 2.5 if (!canKeepCanvas || forceUpdateDimensions) { - newState.canvasTimeStart = - visibleTimeStart - (visibleTimeEnd - visibleTimeStart) + const [canvasTimeStart, canvasTimeEnd] = getCanvasBoundariesFromVisibleTime( + visibleTimeStart, + visibleTimeEnd + ) + newState.canvasTimeStart = canvasTimeStart + newState.canvasTimeEnd = canvasTimeEnd + const mergedState = { + ...state, + ...newState + } + + const canvasWidth = getCanvasWidth(mergedState.width) + // The canvas cannot be kept, so calculate the new items position Object.assign( newState, - stackItems( + stackTimelineItems( items, groups, - newState.canvasTimeStart, - visibleTimeStart, - visibleTimeEnd, - state.width, - props, - state + canvasWidth, + mergedState.canvasTimeStart, + mergedState.canvasTimeEnd, + props.keys, + props.lineHeight, + props.itemHeightRatio, + props.stackItems, + mergedState.draggingItem, + mergedState.resizingItem, + mergedState.dragTime, + mergedState.resizingEdge, + mergedState.resizeTime, + mergedState.newGroupOrder ) ) } 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"