Skip to content

Commit 1b9319e

Browse files
pclslopesclaude
andcommitted
add drag & drop, resize, and month timed event styling
- Add enableDragDrop option for moving events (preserves duration) - Add enableResize option for changing event duration - Horizontal resize for all-day events (drag right edge) - Vertical resize for timed events (drag bottom edge) - Add monthTimedEventStyle option ('list' or 'block') - List style shows dot + time + title in month view - Block style shows traditional colored background - Add snap-to-grid with live time feedback during drag - Add click suppression after drag operations - Add grab offset tracking for precise drag positioning - Add CSS variable --cal-loading-bg for loading overlay - Fix dark theme loading spinner background - Update documentation with new features and CSS variables Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent aea074b commit 1b9319e

5 files changed

Lines changed: 1398 additions & 14 deletions

File tree

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,15 @@ export class CalendarComponent {
178178
| `showTooltips` | boolean | `true` | Show tooltips on hover for events |
179179
| `listDaysForward` | number | `30` | Number of days forward to show in list view |
180180
| `enabledViews` | string[] | `['month', 'week', 'day']` | Available view modes. Add `'list'` to enable list view |
181+
| `enableDragDrop` | boolean | `false` | Enable drag and drop to move events |
182+
| `enableResize` | boolean | `false` | Enable resizing events to change duration |
183+
| `monthTimedEventStyle` | string | `'list'` | Display style for timed events in month view: `'list'` (schedule format) or `'block'` (traditional blocks) |
181184
| `fetchEvents` | function | `null` | Async function to fetch events: `async (start, end) => Event[]` |
182185
| `onEventClick` | function | `null` | Callback when event is clicked: `(event, mouseEvent) => void` |
183186
| `onSlotClick` | function | `null` | Callback when time slot is clicked: `(date, mouseEvent) => void` |
184187
| `onViewChange` | function | `null` | Callback when view changes: `(view) => void` |
185188
| `onNavigate` | function | `null` | Callback when date range changes: `(startDate, endDate) => void` |
189+
| `onEventDrop` | function | `null` | Callback when event is dropped: `(event, oldStart, oldEnd, newStart, newEnd) => void` |
186190

187191
## Event Object Format
188192

@@ -414,6 +418,142 @@ Tooltips use CSS custom properties and can be customized:
414418
- Special characters are automatically escaped for security
415419
- Maximum width is 250px by default (can be customized via CSS variables)
416420

421+
## Drag and Drop
422+
423+
SimpleCalendarJs supports drag and drop for moving events and resizing them to change their duration.
424+
425+
### Configuration
426+
427+
```javascript
428+
const calendar = new SimpleCalendarJs('#calendar', {
429+
enableDragDrop: true, // Enable moving events
430+
enableResize: true, // Enable resizing events
431+
432+
onEventDrop: (event, oldStart, oldEnd, newStart, newEnd) => {
433+
// Detect if this is a move or resize
434+
const isMoved = oldStart.getTime() !== newStart.getTime();
435+
const isResized = oldEnd.getTime() !== newEnd.getTime() && !isMoved;
436+
437+
if (isMoved) {
438+
console.log(`Event "${event.title}" moved to ${newStart}`);
439+
} else if (isResized) {
440+
console.log(`Event "${event.title}" resized to end at ${newEnd}`);
441+
}
442+
443+
// Update your backend
444+
await fetch(`/api/events/${event.id}`, {
445+
method: 'PATCH',
446+
headers: { 'Content-Type': 'application/json' },
447+
body: JSON.stringify({
448+
start: newStart.toISOString(),
449+
end: newEnd.toISOString(),
450+
allDay: event.allDay
451+
})
452+
});
453+
}
454+
});
455+
```
456+
457+
### Moving Events (`enableDragDrop`)
458+
459+
**How It Works:**
460+
- **Drag Initiation**: Click and hold on an event, then move at least 5px or wait 150ms
461+
- **Visual Feedback**: The event follows your cursor while dragging
462+
- **Snap to Grid**: Events snap to 15-minute intervals in week/day views
463+
- **Drop**: Release to drop the event at the new date/time
464+
- **Cancel**: Press ESC to cancel the drag operation
465+
- **Duration Preservation**: Events maintain their duration when moved
466+
- **Touch Support**: Full support for mobile/tablet touch gestures
467+
468+
**Cross-Boundary Conversion:**
469+
470+
When dragging events between different sections:
471+
472+
**Timed Event → All-Day Section (Week/Day Views):**
473+
- Converts to an all-day event
474+
- Preserves the day span
475+
476+
**All-Day Event → Timed Section (Week/Day Views):**
477+
- Converts to a timed event
478+
- Default duration: 1 hour
479+
- Snaps to the time slot where dropped
480+
481+
**Month View:**
482+
- Events maintain their original type (all-day stays all-day, timed stays timed)
483+
- Timed events preserve their original time of day
484+
485+
### Resizing Events (`enableResize`)
486+
487+
**Horizontal Resize (All-Day Events):**
488+
- **Visual Indicator**: Small vertical line appears on the right edge when hovering
489+
- **How It Works**: Drag the right edge to change the number of days the event spans
490+
- **Available In**: Month view, week/day all-day sections
491+
- **Minimum**: 1 day
492+
493+
**Vertical Resize (Timed Events):**
494+
- **Visual Indicator**: Small horizontal line appears at the bottom when hovering
495+
- **How It Works**: Drag the bottom edge to change the end time
496+
- **Available In**: Week/day timed sections
497+
- **Snap to Grid**: 15-minute intervals
498+
- **Minimum**: 15 minutes
499+
- **Live Feedback**: Time display updates to show start and end times as you drag
500+
501+
### Callback Parameters
502+
503+
The `onEventDrop` callback receives the same parameters for both move and resize operations:
504+
505+
| Parameter | Type | Description |
506+
|-----------|------|-------------|
507+
| `event` | Object | The updated event object (with new start/end) |
508+
| `oldStart` | Date | Original start date/time |
509+
| `oldEnd` | Date | Original end date/time |
510+
| `newStart` | Date | New start date/time |
511+
| `newEnd` | Date | New end date/time |
512+
513+
**Detecting Operation Type:**
514+
- **Move**: `oldStart !== newStart`
515+
- **Resize**: `oldStart === newStart` and `oldEnd !== newEnd`
516+
517+
### Important Notes
518+
519+
- The calendar updates the event internally before firing the callback
520+
- You **must** update your backend in the `onEventDrop` callback
521+
- If the backend update fails, call `calendar.refresh()` to revert to the previous state
522+
- Both features are disabled in list view (read-only)
523+
- You can enable one, both, or neither feature independently
524+
525+
## Month View Timed Event Display Style
526+
527+
The `monthTimedEventStyle` option controls how timed events are displayed in month view:
528+
529+
### List Style (Default: `'list'`)
530+
Schedule-style display with horizontal layout:
531+
- **Colored dot**: Shows event color as a small circle
532+
- **Time**: Displays start time (if `showTimeInItems` is enabled)
533+
- **Title**: Event title truncated with ellipsis if too long
534+
- **Compact**: Clean, minimal appearance similar to schedule apps
535+
536+
```javascript
537+
const calendar = new SimpleCalendarJs('#calendar', {
538+
monthTimedEventStyle: 'list', // Default
539+
showTimeInItems: true // Shows time next to dot
540+
});
541+
```
542+
543+
### Block Style (`'block'`)
544+
Traditional calendar block display:
545+
- **Colored Background**: Full event background in event color
546+
- **Time Display**: Start time shown inside block (if enabled)
547+
- **Classic Look**: Traditional calendar appearance
548+
549+
```javascript
550+
const calendar = new SimpleCalendarJs('#calendar', {
551+
monthTimedEventStyle: 'block'
552+
});
553+
```
554+
555+
**Note**: This option only affects timed events in month view. All-day events always display as blocks, and week/day views always use block style with duration-based heights.
556+
417557
## API Methods
418558

419559
```javascript
@@ -870,6 +1010,9 @@ Example with a complete brand color scheme:
8701010
- `--cal-tooltip-max-width`, `--cal-tooltip-padding`, `--cal-tooltip-radius` - Tooltip sizing
8711011
- `--cal-tooltip-font-size`, `--cal-tooltip-offset` - Tooltip typography
8721012

1013+
**Loading Overlay:**
1014+
- `--cal-loading-bg` - Loading spinner overlay background (default: `rgba(255, 255, 255, 0.7)` in light mode, `rgba(31, 41, 55, 0.7)` in dark mode)
1015+
8731016
All styles are scoped under `.uc-calendar` to prevent conflicts with your existing CSS.
8741017

8751018
## Framework Wrapper APIs

css/simple-calendar-js.css

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
--cal-tooltip-radius: 6px;
6262
--cal-tooltip-font-size: 12px;
6363
--cal-tooltip-offset: 8px;
64+
65+
/* Loading overlay */
66+
--cal-loading-bg: rgba(255, 255, 255, 0.7);
6467
}
6568

6669
/* ===== BASE CONTAINER ===== */
@@ -334,7 +337,7 @@
334337
display: flex;
335338
align-items: center;
336339
justify-content: center;
337-
background: rgba(255, 255, 255, 0.7);
340+
background: var(--cal-loading-bg);
338341
z-index: 100;
339342
pointer-events: none;
340343
}
@@ -1180,6 +1183,152 @@
11801183
--cal-tooltip-bg: #374151;
11811184
--cal-tooltip-text: #f9fafb;
11821185
--cal-tooltip-border: #4b5563;
1186+
1187+
/* Loading overlay dark mode */
1188+
--cal-loading-bg: rgba(31, 41, 55, 0.7);
1189+
}
1190+
1191+
/* ===== DRAG AND DROP ===== */
1192+
1193+
/* Calendar in dragging state */
1194+
.uc-calendar.uc-dragging {
1195+
cursor: grabbing !important;
1196+
user-select: none;
1197+
-webkit-user-select: none;
1198+
}
1199+
1200+
.uc-calendar.uc-dragging * {
1201+
cursor: grabbing !important;
1202+
}
1203+
1204+
/* The floating drag element */
1205+
.uc-dragging-element {
1206+
opacity: 0.8;
1207+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1208+
transition: none !important;
1209+
}
1210+
1211+
/* Make events grabbable when drag enabled */
1212+
.uc-calendar[data-drag-enabled] .uc-event-bar,
1213+
.uc-calendar[data-drag-enabled] .uc-timed-event {
1214+
cursor: grab;
1215+
}
1216+
1217+
.uc-calendar[data-drag-enabled] .uc-event-bar:active,
1218+
.uc-calendar[data-drag-enabled] .uc-timed-event:active {
1219+
cursor: grabbing;
1220+
}
1221+
1222+
/* Dark mode support for drag element shadow */
1223+
.uc-calendar.uc-dark .uc-dragging-element {
1224+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
1225+
}
1226+
1227+
/* Resize handle at bottom of timed events */
1228+
.uc-resize-handle {
1229+
position: absolute;
1230+
bottom: 0;
1231+
left: 0;
1232+
right: 0;
1233+
height: 8px;
1234+
cursor: ns-resize;
1235+
display: flex;
1236+
align-items: center;
1237+
justify-content: center;
1238+
opacity: 0;
1239+
transition: opacity 0.2s;
1240+
}
1241+
1242+
.uc-resize-handle::after {
1243+
content: '';
1244+
width: 24px;
1245+
height: 3px;
1246+
background: rgba(255, 255, 255, 0.6);
1247+
border-radius: 2px;
1248+
}
1249+
1250+
.uc-timed-event:hover .uc-resize-handle {
1251+
opacity: 1;
1252+
}
1253+
1254+
.uc-calendar.uc-dragging .uc-resize-handle {
1255+
cursor: ns-resize !important;
1256+
}
1257+
1258+
/* Resize handle on right side of all-day events */
1259+
.uc-resize-handle-right {
1260+
position: absolute;
1261+
top: 2px;
1262+
bottom: 2px;
1263+
right: 2px;
1264+
width: 6px;
1265+
cursor: ew-resize;
1266+
display: flex;
1267+
align-items: center;
1268+
justify-content: center;
1269+
opacity: 0;
1270+
transition: opacity 0.2s;
1271+
}
1272+
1273+
.uc-resize-handle-right::after {
1274+
content: '';
1275+
width: 2px;
1276+
height: 12px;
1277+
background: rgba(255, 255, 255, 0.6);
1278+
border-radius: 1px;
1279+
}
1280+
1281+
.uc-event-bar:hover .uc-resize-handle-right {
1282+
opacity: 1;
1283+
}
1284+
1285+
.uc-calendar.uc-dragging .uc-resize-handle-right {
1286+
cursor: ew-resize !important;
1287+
}
1288+
1289+
/* ===== MONTH VIEW TIMED EVENT STYLES ===== */
1290+
1291+
/* List-style timed events in month view (schedule format) */
1292+
.uc-event-bar--list {
1293+
display: flex;
1294+
align-items: center;
1295+
gap: 6px;
1296+
padding: 2px 6px;
1297+
background: transparent !important;
1298+
overflow: hidden;
1299+
}
1300+
1301+
.uc-event-dot {
1302+
flex-shrink: 0;
1303+
width: 8px;
1304+
height: 8px;
1305+
border-radius: 50%;
1306+
}
1307+
1308+
.uc-event-bar--list .uc-event-time {
1309+
flex-shrink: 0;
1310+
font-size: 11px;
1311+
font-weight: 500;
1312+
color: var(--cal-text);
1313+
opacity: 0.8;
1314+
}
1315+
1316+
.uc-event-bar--list .uc-event-title {
1317+
flex: 1;
1318+
font-size: 12px;
1319+
color: var(--cal-text);
1320+
white-space: nowrap;
1321+
overflow: hidden;
1322+
text-overflow: ellipsis;
1323+
}
1324+
1325+
/* List-style events should be grabbable */
1326+
.uc-calendar[data-drag-enabled] .uc-event-bar--list {
1327+
cursor: grab;
1328+
}
1329+
1330+
.uc-calendar[data-drag-enabled] .uc-event-bar--list:active {
1331+
cursor: grabbing;
11831332
}
11841333

11851334
/* ===== PRINT STYLES ===== */

0 commit comments

Comments
 (0)