Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9448ce
Empty-Commit
MusaMyeni Jan 24, 2024
882c182
Yarn file
MusaMyeni Jan 30, 2024
998f6cf
Update pkg.json + lock file
MusaMyeni Jan 31, 2024
c6d2909
Use pnpm instead of npm
MusaMyeni Jan 31, 2024
cb16443
Use rm
MusaMyeni Jan 31, 2024
0e79d8f
Do not use cross-env
MusaMyeni Jan 31, 2024
42cc908
Added circleci file
MusaMyeni Feb 5, 2024
925d2a8
Added babel to main dependency
MusaMyeni Feb 5, 2024
8bf9bc1
Added pre-commit to build files
MusaMyeni Feb 7, 2024
d23db93
Removed lib from .gitignore
MusaMyeni Feb 7, 2024
20521b7
Adjusted pre-commit to pre-push
MusaMyeni Feb 7, 2024
1c33c7f
Adjusted pre-commit to pre-push & adjusted pre-push
MusaMyeni Feb 7, 2024
a05f2ce
Adjusted pre-push to commit changes
MusaMyeni Feb 7, 2024
94f4eb0
Build artifacts updated
MusaMyeni Feb 7, 2024
6313be0
add empty cell demo
ManuC84 Nov 18, 2025
7de1af4
update readme
ManuC84 Nov 18, 2025
f6b2649
implement empty cell renderer functionality
ManuC84 Nov 18, 2025
abc0c7b
Fix performance: compute getGroupOrders once before loop
ManuC84 Nov 18, 2025
70fcf24
Fix fallback: use lineHeight instead of total timeline height
ManuC84 Nov 18, 2025
01bd64b
Fix performance and edge cases for empty cell labels and adds demo te…
ManuC84 Nov 18, 2025
bb23fcb
Fix: handle Immutable.js Lists correctly in isTimeRangeEmpty
ManuC84 Nov 18, 2025
c89645d
remove unused destructured props
ManuC84 Nov 24, 2025
3467d58
add missing props to shouldComponentUpdate
ManuC84 Nov 24, 2025
e72a179
use arraysEqual for array comparison
ManuC84 Nov 24, 2025
7b64446
use _lenght instead of directly accessing .lenght for items
ManuC84 Nov 24, 2025
c4abfd9
Merge pull request #1 from guestready/mc-29832-test-empty-cell-renderer
ManuC84 Nov 24, 2025
606be05
Merge remote-tracking branch 'origin/master'
ManuC84 Nov 24, 2025
e8ad4c7
Build artifacts updated
ManuC84 Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
version: 2.1

jobs:
build:
docker:
- image: cimg/node:16.19.1
steps:
- checkout
- run:
name: Install pnpm
command: npm install -g pnpm
- run:
name: Install Dependencies with pnpm
command: pnpm install --production=false
- run:
name: Build the Package
command: pnpm run build

workflows:
version: 2
build_and_test:
jobs:
- build
38 changes: 38 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash

function build {
rm -rf lib && ./node_modules/.bin/babel src --out-dir lib && ./node_modules/.bin/node-sass src/lib/Timeline.scss lib/Timeline.css && sed -i'.bak' 's/Timeline\\.scss/Timeline\\.css/g' lib/lib/Timeline.js && rm lib/lib/Timeline.js.bak
}

function commit_changes {
# Check if there are any changes to commit
if [[ -n $(git status -s) ]]; then
git add lib/*

# Commit changes with a default message
git commit -m "Build artifacts updated"
else
echo "No changes to commit."
fi
}

function main {
exit_status=0

current_branch=$(git branch --show-current)
if [ "$current_branch" = "main" ]; then
build || exit_status=$?

if [ $exit_status -eq 0 ]; then
commit_changes
fi

fi

if [ $exit_status -ne 0 ]; then
echo "Pre-push hook failed (build failed)."
exit $exit_status
fi
}

main "$@"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ bower_components
modules
build
gh-pages
/lib
package-lock.json
# vscode stuff
.vscode
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,67 @@ groupRenderer = ({ group }) => {
}
```

## emptyCellLabelRenderer

Function that allows you to render custom labels in empty cells (cells where no items overlap). This is useful for displaying information like availability status, pricing, or other metadata for specific date ranges and groups.

The function is called for each empty cell in the timeline. The Timeline component automatically iterates over all time ranges and groups, checking for empty cells and calling your renderer function when needed.

**Function signature:**

```jsx
emptyCellLabelRenderer = ({ time, timeEnd, group, groupOrder }) => {
// Return JSX to render a label, or null/undefined to render nothing
}
```

**Parameters:**

| property | type | description |
| ------------ | --------------- | --------------------------------------------------------------------------- |
| `time` | `Moment` | Start time of the empty cell (moment object) |
| `timeEnd` | `Moment` | End time of the empty cell (moment object) |
| `group` | `object` | The group object for this row (same object passed in the `groups` prop) |
| `groupOrder` | `number` | The index/order of the group in the groups array |

**Return value:**

- Return a React element (JSX) to render a label in the empty cell
- Return `null` or `undefined` to render nothing for that cell

**Example:**

```jsx
emptyCellLabelRenderer = ({ time, timeEnd, group, groupOrder }) => {
// Access the date for this cell
const cellDate = moment(time).format('YYYY-MM-DD');

// Access group/rental data
const rental = group;

// Return custom label based on your logic
return (
<span style={{ fontSize: '11px', color: '#999' }}>
Available
</span>
);
}

// Usage in Timeline component
<Timeline
groups={groups}
items={items}
emptyCellLabelRenderer={this.emptyCellLabelRenderer}
/>
```

**Notes:**

- The renderer function is called automatically by the Timeline for each empty cell - you don't need to iterate over groups or time ranges yourself
- The function can access component state, props, or make API calls to fetch data for specific dates
- For performance, consider caching data or pre-fetching pricing/availability information rather than fetching it on every render call
- Empty cells are determined by checking if any items overlap with the time range for that group

## resizeDetector

The component automatically detects when the window has been resized. Optionally you can also detect when the component's DOM element has been resized.
Expand Down
240 changes: 240 additions & 0 deletions demo/app/demo-empty-cell-labels/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import React, { Component } from 'react'
import moment from 'moment'

import Timeline from 'react-calendar-timeline'

import generateFakeData from '../generate-fake-data'

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(5, 15)
const defaultTimeStart = moment()
.startOf('day')
.toDate()
const defaultTimeEnd = moment()
.startOf('day')
.add(7, 'days')
.toDate()

this.state = {
groups,
items,
defaultTimeStart,
defaultTimeEnd,
labelMode: 'available',
showNoItems: false
}
}

handleLabelModeChange = (mode) => {
this.setState({ labelMode: mode })
}

handleToggleNoItems = () => {
this.setState({ showNoItems: !this.state.showNoItems })
}

emptyCellLabelRenderer = ({ time, group }) => {
const { labelMode } = this.state

if (labelMode === 'none') {
return null
}

if (labelMode === 'available') {
return (
<span style={{
fontSize: '11px',
color: '#999',
opacity: 0.6,
fontWeight: 'normal'
}}>
Available
</span>
)
}

if (labelMode === 'date') {
return (
<span style={{
fontSize: '10px',
color: '#666',
fontWeight: '500'
}}>
{moment(time).format('MMM D')}
</span>
)
}

if (labelMode === 'price') {
const mockPrice = Math.floor(Math.random() * 200) + 50
return (
<span style={{
fontSize: '11px',
color: '#2c5aa0',
fontWeight: '600'
}}>
€{mockPrice}
</span>
)
}

if (labelMode === 'minStay') {
const mockMinStay = Math.floor(Math.random() * 5) + 1
return (
<span style={{
fontSize: '10px',
color: '#d97706',
fontWeight: '500'
}}>
{mockMinStay} nights min
</span>
)
}

if (labelMode === 'groupSpecific') {
const groupColors = {
1: '#2c5aa0',
2: '#d97706',
3: '#059669',
4: '#dc2626',
5: '#7c3aed'
}
const color = groupColors[group.id] || '#666'

return (
<span style={{
fontSize: '10px',
color: color,
fontWeight: '500',
opacity: 0.8
}}>
{group.title.substring(0, 3).toUpperCase()}
</span>
)
}

if (labelMode === 'conditional') {
const dayOfWeek = moment(time).day()
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6

if (isWeekend) {
return (
<span style={{
fontSize: '10px',
color: '#dc2626',
fontWeight: '600'
}}>
Weekend
</span>
)
}

return (
<span style={{
fontSize: '10px',
color: '#059669',
fontWeight: '500'
}}>
Weekday
</span>
)
}

return null
}

render() {
const { groups, items, defaultTimeStart, defaultTimeEnd, labelMode, showNoItems } = this.state
const displayItems = showNoItems ? [] : items

return (
<div style={{ padding: '20px' }}>
<div style={{ marginBottom: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
<h3 style={{ marginTop: 0, marginBottom: '15px' }}>Empty Cell Labels Demo</h3>
<p style={{ marginBottom: '15px', color: '#666' }}>
This demo shows how to use the <code>emptyCellLabelRenderer</code> prop to display labels in empty cells.
The renderer receives: <code>{`{ time, timeEnd, group, groupOrder }`}</code>
</p>

<div style={{ marginBottom: '10px' }}>
<strong>Label Mode:</strong>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{[
{ value: 'none', label: 'None' },
{ value: 'available', label: 'Available' },
{ value: 'date', label: 'Date' },
{ value: 'price', label: 'Price (Mock)' },
{ value: 'minStay', label: 'Min Stay (Mock)' },
{ value: 'groupSpecific', label: 'Group Specific' },
{ value: 'conditional', label: 'Conditional (Weekend/Weekday)' }
].map(mode => (
<button
key={mode.value}
onClick={() => this.handleLabelModeChange(mode.value)}
style={{
padding: '8px 16px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: labelMode === mode.value ? '#2c5aa0' : 'white',
color: labelMode === mode.value ? 'white' : '#333',
cursor: 'pointer',
fontSize: '14px'
}}
>
{mode.label}
</button>
))}
</div>
<div style={{ marginTop: '15px', paddingTop: '15px', borderTop: '1px solid #ddd' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={showNoItems}
onChange={this.handleToggleNoItems}
style={{ cursor: 'pointer' }}
/>
<strong>Test: Show no items</strong>
<span style={{ color: '#666', fontSize: '12px' }}>
(Tests empty cell labels when items array is empty)
</span>
</label>
</div>
</div>

<Timeline
key={`${labelMode}-${showNoItems}`}
groups={groups}
items={displayItems}
keys={keys}
sidebarWidth={150}
canMove
canResize="right"
canSelect
itemsSorted
stackItems
itemHeightRatio={0.75}
defaultTimeStart={defaultTimeStart}
defaultTimeEnd={defaultTimeEnd}
emptyCellLabelRenderer={this.emptyCellLabelRenderer}
/>
</div>
)
}
}

1 change: 1 addition & 0 deletions demo/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const demos = {
customInfoLabel: require('./demo-custom-info-label').default,
controledSelect: require('./demo-controlled-select').default,
controlledScrolling: require('./demo-controlled-scrolling').default,
emptyCellLabels: require('./demo-empty-cell-labels').default,
}

// A simple component that shows the pathname of the current location
Expand Down
Loading