react-big-schedule 7.1.1-beta.3 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,16 @@ We would like to acknowledge the following projects for their inspiration and co
107
107
  We are continuously working on enhancing react-big-schedule and welcome your feedback and suggestions for future improvements.
108
108
 
109
109
 
110
+ ## Supported versions
111
+
112
+ Only **8.0.0 or later** is supported. Issues and security reports for older versions are not handled; please upgrade to the latest release first.
113
+
114
+ ### Upgrading to 8.0
115
+
116
+ - `besidesWidth` and `SchedulerData.setBesidesWidth()` are removed. The scheduler measures its own container, so just delete the option.
117
+ - The locale is kept per `SchedulerData` instance (`setSchedulerLocale` / `localeDayjs.locale`) and no longer changes the global dayjs. Call `dayjs.locale(...)` yourself if your app needs the global locale to change.
118
+ - Events in the vertical resource view can now be moved and resized like in the horizontal views. Set `movable`, `startResizable` and `endResizable` to `false` in the config to keep that view read-only.
119
+
110
120
  ## Use and Setup
111
121
 
112
122
  1.) To install react-big-schedule, use your preferred package manager:
@@ -276,7 +286,11 @@ const prevClick = data => {
276
286
 
277
287
  #### Vertical resource view
278
288
 
279
- `ViewType.VerticalResource` shows resources as columns and time as rows. Events render and drag-to-create works across resources and time slots. Moving and resizing existing events by drag is not supported in this view yet (they are read-only there); use the horizontal views for that. Row height grows with the number of overlapping events, like the horizontal views. Refer to [this example](https://react-big-schedule.vercel.app/vertical-view).
289
+ `ViewType.VerticalResource` shows resources as columns and time as rows. Events render and drag-to-create works across resources and time slots. Drag an event to another resource column and/or time row to move it (`moveEvent` receives the resource as `slotId`), and drag its top or bottom edge to change its start or end (`updateEventStart` / `updateEventEnd`). Row height grows with the number of overlapping events, like the horizontal views. Refer to [this example](https://react-big-schedule.vercel.app/vertical-view).
290
+
291
+ #### Drop preview
292
+
293
+ While an event, task or resource is dragged over a row, the cells where it would be placed are highlighted. Configure it in the `SchedulerData` config: `dropPreviewEnabled` (default `true`), `dropPreviewClassName` and `dropPreviewStyle` (an inline style object, e.g. `{ border: '2px dashed #52c41a', background: 'rgba(82, 196, 26, 0.16)' }`). Refer to [this example](https://react-big-schedule.vercel.app/drag-and-drop).
280
294
 
281
295
  #### Weekly / monthly columns
282
296
 
@@ -1,4 +1,5 @@
1
1
  import PropTypes from 'prop-types';
2
+ import { DATE_FORMAT } from '../config/default';
2
3
  import AgendaEventItem from './AgendaEventItem';
3
4
  import { createElement as _createElement } from "react";
4
5
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -21,7 +22,8 @@ function AgendaResourceEvents(props) {
21
22
  const end = localeDayjs(endDate).add(1, 'days');
22
23
  const headerStart = localeDayjs(new Date(item.start));
23
24
  const headerEnd = localeDayjs(new Date(item.end));
24
- if (start === headerStart && end === headerEnd) {
25
+ // compare calendar dates: dayjs objects are never === each other
26
+ if (start.format(DATE_FORMAT) === headerStart.format(DATE_FORMAT) && end.format(DATE_FORMAT) === headerEnd.format(DATE_FORMAT)) {
25
27
  return item.events.map(evt => {
26
28
  const durationStart = localeDayjs(new Date(startDate));
27
29
  const durationEnd = localeDayjs(endDate).add(1, 'days');
@@ -21,6 +21,65 @@ export default class DnDContext {
21
21
  initialEnd
22
22
  };
23
23
  };
24
+
25
+ // Vertical resource view: rows are time slots and columns are resources, so the pointer column picks the
26
+ // resource and the pointer row, relative to the row the event was grabbed in, shifts its time.
27
+ getVerticalMove = (props, monitor, component) => {
28
+ const {
29
+ schedulerData,
30
+ resourceEvents
31
+ } = props;
32
+ const {
33
+ localeDayjs,
34
+ config
35
+ } = schedulerData;
36
+ const event = monitor.getItem();
37
+ const pos = getPos(component.eventContainer);
38
+ const column = Math.floor((monitor.getClientOffset().x - pos.x) / schedulerData.getContentCellWidth());
39
+ const header = resourceEvents.headerItems[Math.max(0, Math.min(column, resourceEvents.headerItems.length - 1))];
40
+ if (!header) return null;
41
+ const grabbedSlotId = this.sourceMap.get(monitor.getItemType())?.dragSlotId ?? resourceEvents.slotId;
42
+ const shift = localeDayjs(resourceEvents.slotId).diff(localeDayjs(grabbedSlotId));
43
+ const start = localeDayjs(event.start).add(shift, 'ms');
44
+ const end = start.add(localeDayjs(event.end).diff(localeDayjs(event.start)), 'ms');
45
+
46
+ // crossResourceMove disabled: the event stays in its resource and only moves in time
47
+ const keepResource = config.crossResourceMove === false;
48
+ const slotId = keepResource ? schedulerData._getEventSlotId(event) : header.id;
49
+ const slotName = schedulerData.getSlotById(slotId)?.name;
50
+ return {
51
+ slotId,
52
+ slotName,
53
+ start: start.format(DATETIME_FORMAT),
54
+ end: end.format(DATETIME_FORMAT),
55
+ isVertical: true
56
+ };
57
+ };
58
+
59
+ // Box shown while dragging: the cells (in this row) the dragged item would cover, as { left, width }.
60
+ getDropPreview = (resourceEvents, cellWidth, firstIndex, start, end) => {
61
+ const {
62
+ headerItems
63
+ } = resourceEvents;
64
+ let first = firstIndex;
65
+ let count = 1;
66
+ if (start && end) {
67
+ const from = new Date(start);
68
+ const to = new Date(end);
69
+ const covered = headerItems.reduce((acc, header, index) => {
70
+ if (new Date(header.end) > from && new Date(header.start) < to) acc.push(index);
71
+ return acc;
72
+ }, []);
73
+ if (covered.length > 0) {
74
+ first = covered[0];
75
+ count = covered.length;
76
+ }
77
+ }
78
+ return {
79
+ left: first * cellWidth,
80
+ width: count * cellWidth
81
+ };
82
+ };
24
83
  getDropSpec = () => ({
25
84
  drop: (props, monitor, component) => {
26
85
  const {
@@ -32,6 +91,9 @@ export default class DnDContext {
32
91
  localeDayjs
33
92
  } = schedulerData;
34
93
  const type = monitor.getItemType();
94
+ if (schedulerData.isVerticalResourceView() && type === DnDTypes.EVENT) {
95
+ return this.getVerticalMove(props, monitor, component);
96
+ }
35
97
  const pos = getPos(component.eventContainer);
36
98
  const cellWidth = schedulerData.getContentCellWidth();
37
99
  let initialStartTime = null;
@@ -75,6 +137,16 @@ export default class DnDContext {
75
137
  this.config = config;
76
138
  const item = monitor.getItem();
77
139
  const type = monitor.getItemType();
140
+ if (schedulerData.isVerticalResourceView() && type === DnDTypes.EVENT) {
141
+ const move = this.getVerticalMove(props, monitor, component);
142
+ const column = Math.floor((monitor.getClientOffset().x - getPos(component.eventContainer).x) / schedulerData.getContentCellWidth());
143
+ const columnIndex = Math.max(0, Math.min(column, resourceEvents.headerItems.length - 1));
144
+ component.setDropPreview?.(this.getDropPreview(resourceEvents, schedulerData.getContentCellWidth(), columnIndex));
145
+ if (move && movingEvent) {
146
+ movingEvent(schedulerData, move.slotId, move.slotName, move.start, move.end, 'Move', type, item);
147
+ }
148
+ return;
149
+ }
78
150
  const pos = getPos(component.eventContainer);
79
151
  const cellWidth = schedulerData.getContentCellWidth();
80
152
  let initialStart = null;
@@ -119,6 +191,7 @@ export default class DnDContext {
119
191
  }
120
192
  action = 'Move';
121
193
  }
194
+ component.setDropPreview?.(this.getDropPreview(resourceEvents, cellWidth, leftIndex, isEvent ? newStart : undefined, isEvent ? newEnd : undefined));
122
195
  if (movingEvent) {
123
196
  movingEvent(schedulerData, slotId, slotName, newStart, newEnd, action, type, item);
124
197
  }
@@ -4,12 +4,18 @@ export default class DnDSource {
4
4
  this.resolveDragObjFunc = resolveDragObjFunc;
5
5
  this.dndType = dndType;
6
6
  this.DnDEnabled = DnDEnabled;
7
+ // vertical view: the time row the user grabbed the event in (an event is drawn once per row it covers)
8
+ this.dragSlotId = undefined;
7
9
  }
8
10
  getDragSpec = () => ({
9
11
  // beginDrag: (props, monitor, component) => this.resolveDragObjFunc(props),
10
- beginDrag: props => this.resolveDragObjFunc(props),
12
+ beginDrag: props => {
13
+ this.dragSlotId = props.slotId;
14
+ return this.resolveDragObjFunc(props);
15
+ },
11
16
  // endDrag: (props, monitor, component) => {
12
17
  endDrag: (props, monitor) => {
18
+ this.dragSlotId = undefined;
13
19
  if (!monitor.didDrop()) return;
14
20
  const {
15
21
  moveEvent,
@@ -41,7 +47,9 @@ export default class DnDSource {
41
47
  const isEvent = type === DnDTypes.EVENT;
42
48
  if (isEvent) {
43
49
  const event = item;
44
- if (config.relativeMove) {
50
+ if (dropResult.isVertical) {
51
+ // the vertical drop already resolved the final start/end (see DnDContext.getVerticalMove)
52
+ } else if (config.relativeMove) {
45
53
  newStart = localeDayjs(event.start).add(localeDayjs(newStart).diff(localeDayjs(new Date(initialStart))), 'ms').format(DATETIME_FORMAT);
46
54
  } else if (viewType !== ViewType.Day) {
47
55
  const tmpDayjs = localeDayjs(newStart);
@@ -211,6 +211,10 @@ class EventItem extends Component {
211
211
  conflictOccurred
212
212
  } = this.props;
213
213
  schedulerData._stopResizing();
214
+ if (schedulerData.isVerticalResourceView()) {
215
+ await this.stopVerticalResize(ev, 'start');
216
+ return;
217
+ }
214
218
  const {
215
219
  width: stateWidth
216
220
  } = this.state;
@@ -293,6 +297,65 @@ class EventItem extends Component {
293
297
  this.subscribeResizeEvent(this.props);
294
298
  } else if (updateEventStart !== undefined) updateEventStart(schedulerData, eventItem, newStart);
295
299
  };
300
+
301
+ // Vertical resource view: rows are time slots, so the row under the pointer gives the new start (its top
302
+ // edge) or end (its bottom edge). The event is drawn once per row, so this works for any row height.
303
+ stopVerticalResize = async (ev, dragType) => {
304
+ const {
305
+ schedulerData,
306
+ eventItem,
307
+ updateEventStart,
308
+ updateEventEnd,
309
+ conflictOccurred
310
+ } = this.props;
311
+ const {
312
+ config,
313
+ events,
314
+ localeDayjs
315
+ } = schedulerData;
316
+ const row = document.elementFromPoint(ev.clientX, ev.clientY)?.closest('[data-slot-id]');
317
+ if (!row) return;
318
+ const rowStart = localeDayjs(row.dataset.slotId);
319
+ const isStart = dragType === 'start';
320
+ const newStart = isStart ? rowStart.format(DATETIME_FORMAT) : eventItem.start;
321
+ const newEnd = isStart ? eventItem.end : rowStart.add(config.minuteStep, 'minutes').format(DATETIME_FORMAT);
322
+ if (!localeDayjs(newEnd).isAfter(localeDayjs(newStart))) return;
323
+ if (isStart ? newStart === eventItem.start : newEnd === eventItem.end) return;
324
+ const slotId = schedulerData._getEventSlotId(eventItem);
325
+ const hasConflict = config.checkConflict && events.some(e => {
326
+ if (schedulerData._getEventSlotId(e) !== slotId || e.id === eventItem.id) return false;
327
+ return localeDayjs(newStart).isBefore(localeDayjs(e.end)) && localeDayjs(newEnd).isAfter(localeDayjs(e.start));
328
+ });
329
+ if (hasConflict) {
330
+ conflictOccurred?.(schedulerData, isStart ? 'StartResize' : 'EndResize', eventItem, DnDTypes.EVENT, slotId, schedulerData.getSlotById(slotId)?.name, newStart, newEnd);
331
+ } else if (isStart) updateEventStart?.(schedulerData, eventItem, newStart);else updateEventEnd?.(schedulerData, eventItem, newEnd);
332
+ };
333
+
334
+ // Vertical resource view: the event is drawn once per time row; only the row holding the event's start
335
+ // (or end) gets the top (or bottom) handle.
336
+ getVerticalEdges = () => {
337
+ const {
338
+ schedulerData,
339
+ eventItem,
340
+ slotId
341
+ } = this.props;
342
+ if (!schedulerData.isVerticalResourceView() || !slotId) return {
343
+ top: true,
344
+ bottom: true
345
+ };
346
+ const {
347
+ localeDayjs,
348
+ config
349
+ } = schedulerData;
350
+ const rowStart = localeDayjs(slotId);
351
+ const rowEnd = rowStart.add(config.minuteStep, 'minutes');
352
+ const eventStart = localeDayjs(eventItem.start);
353
+ const eventEnd = localeDayjs(eventItem.end);
354
+ return {
355
+ top: !eventStart.isBefore(rowStart) && eventStart.isBefore(rowEnd),
356
+ bottom: eventEnd.isAfter(rowStart) && !eventEnd.isAfter(rowEnd)
357
+ };
358
+ };
296
359
  cancelStartDrag = ev => {
297
360
  ev.stopPropagation();
298
361
  this.startResizer.removeEventListener('touchmove', this.doStartDrag, false);
@@ -364,6 +427,10 @@ class EventItem extends Component {
364
427
  conflictOccurred
365
428
  } = this.props;
366
429
  schedulerData._stopResizing();
430
+ if (schedulerData.isVerticalResourceView()) {
431
+ await this.stopVerticalResize(ev, 'end');
432
+ return;
433
+ }
367
434
  const {
368
435
  width: stateWidth
369
436
  } = this.state;
@@ -544,13 +611,14 @@ class EventItem extends Component {
544
611
  });
545
612
  const start = localeDayjs(new Date(eventItem.start));
546
613
  const eventTitle = isInPopover ? `${start.format('HH:mm')} ${titleText}` : titleText;
614
+ const verticalEdges = this.getVerticalEdges();
547
615
  let startResizeDiv = /*#__PURE__*/_jsx("div", {});
548
- if (startResizable(this.props)) startResizeDiv = /*#__PURE__*/_jsx("div", {
616
+ if (startResizable(this.props) && verticalEdges.top) startResizeDiv = /*#__PURE__*/_jsx("div", {
549
617
  className: "event-resizer event-start-resizer",
550
618
  ref: ref => this.startResizer = ref
551
619
  });
552
620
  let endResizeDiv = /*#__PURE__*/_jsx("div", {});
553
- if (endResizable(this.props)) endResizeDiv = /*#__PURE__*/_jsx("div", {
621
+ if (endResizable(this.props) && verticalEdges.bottom) endResizeDiv = /*#__PURE__*/_jsx("div", {
554
622
  className: "event-resizer event-end-resizer",
555
623
  ref: ref => this.endResizer = ref
556
624
  });
@@ -47,7 +47,9 @@ class ResourceEvents extends PureComponent {
47
47
  // Add vertical selection tracking
48
48
  originalStartRowIndex: -1,
49
49
  startRowIndex: -1,
50
- endRowIndex: -1
50
+ endRowIndex: -1,
51
+ // where the dragged item would land (set from the drop target's hover)
52
+ dropPreview: null
51
53
  };
52
54
  this.supportTouch = false; // 'ontouchstart' in window;
53
55
  }
@@ -81,6 +83,14 @@ class ResourceEvents extends PureComponent {
81
83
  width: 0
82
84
  });
83
85
  }
86
+ setDropPreview = dropPreview => {
87
+ if (!Number.isFinite(dropPreview.left) || !Number.isFinite(dropPreview.width)) return;
88
+ const current = this.state.dropPreview;
89
+ if (current?.left === dropPreview.left && current?.width === dropPreview.width) return;
90
+ this.setState({
91
+ dropPreview
92
+ });
93
+ };
84
94
  cleanupDragInteraction = () => {
85
95
  document.documentElement.removeEventListener('touchmove', this.doDrag, false);
86
96
  document.documentElement.removeEventListener('touchend', this.stopDrag, false);
@@ -574,6 +584,7 @@ class ResourceEvents extends PureComponent {
574
584
  isStart: isStart,
575
585
  isEnd: isEnd,
576
586
  isInPopover: false,
587
+ slotId: resourceEvents.slotId,
577
588
  left: left,
578
589
  width: width,
579
590
  top: top,
@@ -629,13 +640,27 @@ class ResourceEvents extends PureComponent {
629
640
  }
630
641
  }
631
642
  });
643
+
644
+ // drop preview: styled box where the item being dragged over this row would be placed
645
+ const {
646
+ dropPreview
647
+ } = this.state;
648
+ const dropPreviewBox = this.props.isOver && this.props.canDrop && config.dropPreviewEnabled !== false && dropPreview ? /*#__PURE__*/_jsx("div", {
649
+ className: `drop-preview${config.dropPreviewClassName ? ` ${config.dropPreviewClassName}` : ''}`,
650
+ style: {
651
+ left: dropPreview.left,
652
+ width: dropPreview.width,
653
+ ...config.dropPreviewStyle
654
+ }
655
+ }) : null;
632
656
  const eventContainer = /*#__PURE__*/_jsxs("div", {
633
657
  ref: this.eventContainerRef,
634
658
  className: "event-container",
659
+ "data-slot-id": resourceEvents.slotId,
635
660
  style: {
636
661
  height: resourceEvents.rowHeight
637
662
  },
638
- children: [selectedArea, sharedSelectedArea, verticalSelectionOverlay, eventList]
663
+ children: [selectedArea, sharedSelectedArea, verticalSelectionOverlay, dropPreviewBox, eventList]
639
664
  });
640
665
  return /*#__PURE__*/_jsx("tr", {
641
666
  children: /*#__PURE__*/_jsx("td", {
@@ -58,6 +58,10 @@ export default {
58
58
  // month row above the day cells (month names spanning their days), like the week number row
59
59
  showMonthRow: false,
60
60
  monthRowHeight: 24,
61
+ // box shown on the row/cell where a dragged event, task or resource would be placed
62
+ dropPreviewEnabled: true,
63
+ dropPreviewClassName: '',
64
+ dropPreviewStyle: undefined,
61
65
  startResizable: true,
62
66
  endResizable: true,
63
67
  movable: true,
@@ -165,6 +165,18 @@
165
165
  position: relative;
166
166
  }
167
167
 
168
+ .rbs .drop-preview {
169
+ position: absolute;
170
+ top: 0;
171
+ bottom: 0;
172
+ z-index: 3;
173
+ box-sizing: border-box;
174
+ pointer-events: none;
175
+ border: 2px dashed #1677ff;
176
+ border-radius: 4px;
177
+ background: rgba(22, 119, 255, 0.14);
178
+ }
179
+
168
180
  .rbs .event-item {
169
181
  margin: 1px 0;
170
182
  width: 100%;
@@ -277,6 +289,26 @@
277
289
  right: -1px;
278
290
  }
279
291
 
292
+ /* Vertical resource view: events resize from their top / bottom edge */
293
+ .rbs.vertical-view .event-resizer {
294
+ width: auto;
295
+ height: 7px;
296
+ left: -1px;
297
+ right: -1px;
298
+ top: auto;
299
+ bottom: auto;
300
+ }
301
+
302
+ .rbs.vertical-view .event-start-resizer {
303
+ cursor: n-resize;
304
+ top: -1px;
305
+ }
306
+
307
+ .rbs.vertical-view .event-end-resizer {
308
+ cursor: s-resize;
309
+ bottom: -1px;
310
+ }
311
+
280
312
  /* Buttons */
281
313
  .rbs-txt-btn-dis {
282
314
  border: none;
package/dist/index.d.ts CHANGED
@@ -422,6 +422,12 @@ export interface SchedulerDataConfig {
422
422
  showWeekNumber?: boolean;
423
423
  weekNumberRowHeight?: number;
424
424
  showMonthRow?: boolean;
425
+ /** Show a box where the dragged event / task / resource would be placed (default true). */
426
+ dropPreviewEnabled?: boolean;
427
+ /** Extra class name for the drop preview box. */
428
+ dropPreviewClassName?: string;
429
+ /** Inline style for the drop preview box, e.g. `{ border: '2px dashed green' }`. */
430
+ dropPreviewStyle?: React.CSSProperties;
425
431
  monthRowHeight?: number;
426
432
  weekNumberLabel?: string;
427
433
  headerBorderColor?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-big-schedule",
3
- "version": "7.1.1-beta.3",
3
+ "version": "8.1.0",
4
4
  "description": "React Big Schedule is a powerful and intuitive scheduler and resource planning solution built with React. Seamlessly integrate this modern browser-compatible component into your applications to effectively manage time, appointments, and resources. With drag-and-drop functionality, interactive UI, and granular views, react-big-schedule empowers users to effortlessly schedule and allocate resources with precision. Enhance productivity and streamline your workflow with this React-based solution, designed to optimize time management and simplify calendar-based operations. Perfect for applications requiring advanced scheduling capabilities, react-big-schedule offers a seamless and intuitive experience for managing appointments, resource allocation, and time slots. Unlock the potential of your React projects with react-big-schedule and revolutionize the way you handle scheduling and resource planning. It is the updated version of react-big-scheduler",
5
5
  "keywords": [
6
6
  "react-big-schedule",