Event Grouping & Bulk Actions
Usage
theme: 'ios' You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant: 'light' You can turn auto dark mode on based on system settings.
Usage
clickToCreate: true,
// click to create is on dragToCreate: true,
// drag to create is on dragToMove: true,
// reordering is on dragToResize: true,
// resizing is on eventDelete: true
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale: mobiscroll.localeEn Selecting the locale loads the localized settings and the translated UI text.
Event Grouping & Bulk Actions
When managing a large volume of events across multiple clients and time periods, handling items one by one quickly becomes inefficient. Users often need to move entire sets of related tasks together, not individual events. To support this, grouping and bulk interaction are essential.
The starting point is being able to look at the same dataset through different lenses. The demo supports switching between an assignee view, where each row represents a person responsible for the work, and a work type view, where each row represents a task category. A segmented control in the header allows adjusting the time granularity of the timeline while keeping a full year in view, enabling users to switch between quarterly, monthly, or weekly breakdowns. The view selector, grouping toggle, and segmented control are all rendered via the renderHeader function, keeping the controls integrated into the timeline header. The renderResource function adapts the resource column accordingly, displaying employee avatars and job titles in assignee view, and work type color indicators in type view.
Once the right perspective is selected, individual events are not rendered one by one. Using the renderTimelineEvent function, related tasks are automatically grouped into consolidated blocks per client and per quarter. These blocks can be expanded to reveal the individual tasks inside, or collapsed to keep the timeline readable at scale.
With grouping in place, bulk interaction becomes natural. An entire grouped block can be dragged along the timeline, moving all underlying events simultaneously. The system calculates the time delta and applies it uniformly to every event in the group. At the same time, individual tasks within a group remain accessible and can be edited through the edit button, which opens a range picker to update the event dates. Because both views are projections of the same event store, a change made in one view is immediately reflected in the other.
Demo

Code
mobiscroll.setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
var defaultEvents = [
{
id: 1,
title: 'Office Tower HVAC System Overhaul',
start: '2026-01-05T00:00',
end: '2026-03-15T00:00',
resource: 1,
type: 'upgrade',
clientGroup: 'Enterprise Solutions Group',
},
{
id: 2,
title: 'Executive Floor Climate Control Installation',
start: '2026-01-10T00:00',
end: '2026-03-20T00:00',
resource: 1,
type: 'installation',
clientGroup: 'Enterprise Solutions Group',
},
{
id: 3,
title: 'Headquarters Heating System Maintenance',
start: '2026-02-01T00:00',
end: '2026-03-31T00:00',
resource: 1,
type: 'maintenance',
clientGroup: 'Enterprise Solutions Group',
},
{
id: 4,
title: 'Building Infrastructure Assessment',
start: '2026-01-15T00:00',
end: '2026-03-25T00:00',
resource: 2,
type: 'inspection',
clientGroup: 'Enterprise Solutions Group',
},
{
id: 5,
title: 'Campus Facilities Modernization',
start: '2026-02-05T00:00',
end: '2026-03-31T00:00',
resource: 2,
type: 'upgrade',
clientGroup: 'Enterprise Solutions Group',
},
// Showing partial data. Download full source.
];
var assigneeResources = [
{
id: 1,
name: 'Sarah Johnson',
title: 'Senior HVAC Technician',
color: '#4a7c9e',
img: 'https://img.mobiscroll.com/demos/f1.png',
},
{
id: 2,
name: 'Mike Chen',
title: 'Lead Facilities Engineer',
color: '#7fa650',
img: 'https://img.mobiscroll.com/demos/m1.png',
},
{
id: 3,
name: 'Emily Rodriguez',
title: 'Electrical Systems Specialist',
color: '#c76b8a',
img: 'https://img.mobiscroll.com/demos/f2.png',
},
{
id: 4,
name: 'James Wilson',
title: 'Master Electrician',
color: '#4a9dad',
img: 'https://img.mobiscroll.com/demos/m2.png',
},
{
id: 5,
name: 'Lisa Anderson',
title: 'Building Automation Technician',
color: '#d4a056',
img: 'https://img.mobiscroll.com/demos/f3.png',
},
{
id: 6,
name: 'David Kim',
title: 'HVAC Project Manager',
color: '#5a9d76',
img: 'https://img.mobiscroll.com/demos/m3.png',
},
];
var typeResources = [
{
id: 'installation',
name: 'installation',
color: '#3b5998',
},
{
id: 'maintenance',
name: 'maintenance',
color: '#2d7a4f',
},
{
id: 'repair',
name: 'repair',
color: '#b8621b',
},
{
id: 'inspection',
name: 'inspection',
color: '#6b4fa3',
},
{
id: 'upgrade',
name: 'upgrade',
color: '#a03a3a',
},
];
var groupedEvents = [];
var groupBy = 'assignee'; // 'assignee' or 'type'
var groupByClient = false;
var zoomLevel = 'month'; // 'quarter', 'month', 'week'
var rawEvents = defaultEvents.slice();
// Edit popup state
var editingEventId = null;
function groupEventsByClientQuarter(events) {
var groups = {};
var result = [];
// Save old collapsed states including year and quarter
var oldCollapsedStates = {};
groupedEvents.forEach(function (ge) {
var stateKey = ge.resource + '-' + ge.clientGroup + '-' + ge.year + '-' + ge.quarter;
oldCollapsedStates[stateKey] = ge.collapsed;
});
// Group events by resource (assignee or type) + client + quarter
events.forEach(function (event) {
var resourceId = groupBy === 'assignee' ? event.resource : event.type;
var eventStart = new Date(event.start);
var month = eventStart.getMonth();
var year = eventStart.getFullYear();
var quarter = Math.floor(month / 3);
var groupKey = resourceId + '-' + event.clientGroup + '-' + year + '-' + quarter;
if (!groups[groupKey]) {
groups[groupKey] = {
resource: resourceId,
clientGroup: event.clientGroup,
year: year,
quarter: quarter,
events: [],
};
}
groups[groupKey].events.push(event);
});
// Create grouped events
Object.keys(groups).forEach(function (groupKey) {
var groupData = groups[groupKey];
var quarterEvents = groupData.events.sort(function (a, b) {
return new Date(a.start).getTime() - new Date(b.start).getTime();
});
// Get color based on groupBy mode
var color;
if (groupBy === 'assignee') {
var resourceItem = assigneeResources.find(function (r) {
return r.id === groupData.resource;
});
color = resourceItem.color;
} else {
var typeObj = typeResources.find(function (r) {
return r.id === groupData.resource;
});
color = typeObj.color;
}
var eventIds = quarterEvents
.map(function (e) {
return e.id;
})
.join('-');
var earliestStart = quarterEvents[0].start;
var latestEnd = quarterEvents.reduce(function (latest, event) {
return new Date(event.end).getTime() > new Date(latest).getTime() ? event.end : latest;
}, quarterEvents[0].end);
var newId = 'group-' + groupKey + '-' + eventIds;
// Use full key including year and quarter
var stateKey = groupData.resource + '-' + groupData.clientGroup + '-' + groupData.year + '-' + groupData.quarter;
var collapsedState = stateKey in oldCollapsedStates ? oldCollapsedStates[stateKey] : true;
result.push({
id: newId,
title: groupData.clientGroup,
resource: groupData.resource,
clientGroup: groupData.clientGroup,
year: groupData.year,
quarter: groupData.quarter,
start: earliestStart,
end: latestEnd,
color: color,
count: quarterEvents.length,
originalEvents: quarterEvents,
collapsed: collapsedState,
});
});
return result;
}
function prepareEventsForDisplay(events, colorByType) {
return events.map(function (event) {
return {
id: event.id,
title: event.title,
start: event.start,
end: event.end,
resource: colorByType ? event.resource : event.type,
type: event.type,
assignee: event.resource,
clientGroup: event.clientGroup,
color: '#f8f9fa',
};
});
}
function updateView() {
var currentResources = groupBy === 'assignee' ? assigneeResources : typeResources;
var currentEvents;
if (groupByClient) {
groupedEvents = groupEventsByClientQuarter(rawEvents);
currentEvents = groupedEvents;
} else {
groupedEvents = [];
currentEvents = prepareEventsForDisplay(rawEvents, groupBy === 'assignee');
}
var timelineConfig;
switch (zoomLevel) {
case 'quarter':
timelineConfig = { type: 'year', resolutionHorizontal: 'quarter', eventHeight: 'variable' };
break;
case 'month':
timelineConfig = { type: 'year', resolutionHorizontal: 'month', eventHeight: 'variable' };
break;
case 'week':
timelineConfig = { type: 'year', resolutionHorizontal: 'week', eventHeight: 'variable' };
break;
}
calendar.setOptions({
data: currentEvents,
resources: currentResources,
renderTimelineEvent: groupByClient ? renderGroupedEvent : renderSimpleEvent,
view: { timeline: timelineConfig },
dragBetweenResources: !groupByClient,
});
}
function renderGroupedEvent(event) {
var origEvent = event.original;
var isExpanded = !origEvent.collapsed;
// Get unique items (opposite of groupBy)
var uniqueItems = {};
origEvent.originalEvents.forEach(function (ev) {
if (groupBy === 'assignee') {
var typeObj = typeResources.find(function (r) {
return r.id === ev.type;
});
if (typeObj) {
uniqueItems[typeObj.id] = typeObj;
}
} else {
var emp = assigneeResources.find(function (r) {
return r.id === ev.resource;
});
if (emp) {
uniqueItems[emp.id] = emp;
}
}
});
var itemCount = Object.keys(uniqueItems).length;
var itemLabel = groupBy === 'assignee' ? 'type' : 'employee';
// Generate expanded content HTML if expanded
var expandedHTML = '';
if (isExpanded) {
var groupStart = new Date(origEvent.start).getTime();
var groupEnd = new Date(origEvent.end).getTime();
var groupDuration = groupEnd - groupStart;
expandedHTML = origEvent.originalEvents
.map(function (ev) {
var evStart = new Date(ev.start).getTime();
var evEnd = new Date(ev.end).getTime();
var leftPercent = groupDuration > 0 ? ((evStart - groupStart) / groupDuration) * 100 : 0;
var widthPercent = groupDuration > 0 ? ((evEnd - evStart) / groupDuration) * 100 : 100;
var detailText = '';
var typeDotColor = '';
var avatarUrl = '';
if (groupBy === 'assignee') {
var typeObj = typeResources.find(function (r) {
return r.id === ev.type;
});
if (typeObj) {
detailText = typeObj.name;
typeDotColor = typeObj.color;
}
} else {
var employee = assigneeResources.find(function (r) {
return r.id === ev.resource;
});
if (employee) {
detailText = employee.name;
avatarUrl = employee.img;
}
}
return (
'<div class="mds-event-group-original-event" title="' +
ev.title +
', ' +
mobiscroll.formatDate('MM/DD/YYYY', new Date(ev.start)) +
' - ' +
mobiscroll.formatDate('MM/DD/YYYY', new Date(ev.end)) +
'" style="margin-left:' +
leftPercent +
'%; width:' +
widthPercent +
'%;">' +
'<div class="mbsc-flex mds-event-group-event-content">' +
'<div class="mds-event-group-event-title mds-event-group-text-truncate">' +
ev.title +
'</div>' +
'<div class="mbsc-flex mds-event-group-event-right">' +
'<div class="mds-event-group-event-date mds-event-group-text-truncate">' +
mobiscroll.formatDate('DD MMM', new Date(ev.start)) +
' - ' +
mobiscroll.formatDate('DD MMM', new Date(ev.end)) +
'</div>' +
'<div class="mbsc-flex mds-event-group-event-detail">' +
(avatarUrl ? '<img src="' + avatarUrl + '" alt="' + detailText + '" class="mds-event-group-event-avatar" />' : '') +
(typeDotColor ? '<span class="mds-event-group-type-dot" style="background-color: ' + typeDotColor + '"></span>' : '') +
'<div class="mds-event-group-event-info mds-event-group-text-truncate">' +
detailText +
'</div>' +
'</div>' +
'</div>' +
'<div class="mds-event-group-edit-btn mbsc-flex mbsc-icon mbsc-font-icon mbsc-icon-pencil" data-event-id="' +
ev.id +
'"></div>' +
'</div>' +
'</div>'
);
})
.join('');
}
return (
'<div class="mbsc-flex mds-event-group-task mds-event-group-task-client' +
(isExpanded ? ' expanded' : '') +
'" style="border-left-color: ' +
origEvent.color +
'">' +
'<div class="mbsc-flex mds-event-group-content">' +
'<div class="mds-event-group-title-text mds-event-group-text-truncate">' +
origEvent.clientGroup +
'</div>' +
'<div class="mbsc-flex mds-event-group-right">' +
'<div class="mbsc-flex mds-event-group-meta">' +
'<div class="mds-event-group-date-range">' +
mobiscroll.formatDate('DD MMM', new Date(origEvent.start)) +
' - ' +
mobiscroll.formatDate('DD MMM', new Date(origEvent.end)) +
'</div>' +
'<div class="mds-event-group-count">' +
origEvent.count +
' task' +
(origEvent.count > 1 ? 's' : '') +
', ' +
itemCount +
' ' +
itemLabel +
(itemCount > 1 ? 's' : '') +
'</div>' +
'</div>' +
'<div class="mbsc-flex mds-event-group-icon mbsc-icon mbsc-font-icon mbsc-icon-material-keyboard-arrow-down"></div>' +
'</div>' +
'</div>' +
'<div class="mds-event-group-events"><div class="mds-event-group-events-inner">' +
expandedHTML +
'</div></div>' +
'</div>'
);
}
function renderSimpleEvent(event) {
var origEvent = event.original;
var detailText = '';
var avatarUrl = '';
var typeDotColor = '';
if (groupBy === 'assignee') {
// Show type with colored dot
var typeObj = typeResources.find(function (t) {
return t.id === origEvent.type;
});
if (typeObj) {
detailText = typeObj.name;
typeDotColor = typeObj.color;
}
} else {
// Show assignee with avatar
var employee = assigneeResources.find(function (r) {
return r.id === origEvent.assignee;
});
if (employee) {
detailText = employee.name;
avatarUrl = employee.img;
}
}
return (
'<div class="mbsc-flex mds-event-simple">' +
'<div class="mds-event-simple-title">' +
'<div class="mds-event-simple-title-inner mds-event-group-text-truncate">' +
origEvent.title +
'</div>' +
'</div>' +
'<div class="mbsc-flex mds-event-simple-right">' +
'<div class="mds-event-simple-date">' +
mobiscroll.formatDate('DD MMM', new Date(origEvent.start)) +
' - ' +
mobiscroll.formatDate('DD MMM', new Date(origEvent.end)) +
'</div>' +
(detailText
? '<div class="mbsc-flex mds-event-simple-subtitle-wrapper">' +
(avatarUrl ? '<img src="' + avatarUrl + '" alt="' + detailText + '" class="mds-event-simple-avatar" />' : '') +
(typeDotColor ? '<span class="mds-event-simple-type-dot" style="background-color: ' + typeDotColor + '"></span>' : '') +
'<div class="mbsc-flex mds-event-simple-subtitle" style="' +
(groupBy === 'assignee' ? '' : 'margin-left: 25px;') +
'">' +
detailText +
'</div>' +
'</div>'
: '') +
'</div>' +
'</div>'
);
}
var calendar = mobiscroll.eventcalendar('#demo-event-grouping', {
dragToMove: true,
dragToResize: true,
dragToCreate: false,
clickToCreate: false,
dragBetweenResources: true,
view: {
timeline: {
type: 'year',
resolutionHorizontal: 'month',
eventHeight: 'variable',
},
},
data: defaultEvents,
resources: assigneeResources,
renderTimelineEvent: renderSimpleEvent,
renderResource: function (resource) {
if (groupBy === 'assignee') {
// Employee resource
return (
'<div class="mbsc-flex">' +
'<img alt="' +
resource.name +
'" class="mds-event-group-avatar" src="' +
resource.img +
'"/>' +
'<div class="mds-event-group-cont">' +
'<div class="mds-event-group-name">' +
resource.name +
'</div>' +
'<div class="mds-event-group-title">' +
resource.title +
'</div>' +
'</div>' +
'</div>'
);
} else {
// Type resource
return (
'<div class="mbsc-flex mds-event-group-type-resource">' +
'<div class="mds-event-group-type-badge" style="background-color: ' +
resource.color +
'"></div>' +
'<div class="mds-event-group-type-name">' +
resource.name +
'</div>' +
'</div>'
);
}
},
renderHeader: function () {
return (
'<div mbsc-calendar-nav></div>' +
'<div class="mbsc-flex mbsc-flex-1-0 mbsc-justify-content-end mds-event-group-header-controls">' +
'<label>' +
'<input mbsc-checkbox type="checkbox" id="group-by-client-quarter" data-theme="material" data-label="Group by Client/Quarter" />' +
'</label>' +
'<label>' +
'<input mbsc-input id="event-grouping-input" class="mds-event-group-select" data-dropdown="true" data-input-style="box" />' +
'</label>' +
'<select id="group-by-select">' +
'<option value="assignee" selected>View by Assignee</option>' +
'<option value="type">View by Type</option>' +
'</select>' +
'</div>' +
'<label>Quarterly<input mbsc-segmented type="radio" name="zoom-level" value="quarter"/></label>' +
'<label>Monthly<input mbsc-segmented type="radio" name="zoom-level" value="month" checked/></label>' +
'<label>Weekly<input mbsc-segmented type="radio" name="zoom-level" value="week"/></label>' +
'<div mbsc-calendar-prev></div>' +
'<div mbsc-calendar-today></div>' +
'<div mbsc-calendar-next></div>'
);
},
onEventUpdate: function (args) {
var updatedEvent = args.event;
var oldEvent = args.oldEvent;
var newStart = new Date(updatedEvent.start).getTime();
var newEnd = new Date(updatedEvent.end).getTime();
if (groupByClient) {
var oldStart = new Date(oldEvent.start).getTime();
var oldEnd = new Date(oldEvent.end).getTime();
var startDelta = newStart - oldStart;
var endDelta = newEnd - oldEnd;
// Nothing changed
if (startDelta === 0 && endDelta === 0) return;
var movedGroupedEvent = groupedEvents.find(function (ge) {
return ge.id === oldEvent.id;
});
if (movedGroupedEvent) {
var clientGroupName = movedGroupedEvent.clientGroup;
var resourceId = movedGroupedEvent.resource;
var wasCollapsed = movedGroupedEvent.collapsed;
var isMove = startDelta === endDelta;
var eventsToUpdate;
if (isMove) {
// MOVE: shift all original events by the same delta
eventsToUpdate = movedGroupedEvent.originalEvents.map(function (originalEvent) {
return {
id: originalEvent.id,
title: originalEvent.title,
resource: originalEvent.resource,
type: originalEvent.type,
clientGroup: originalEvent.clientGroup,
start: new Date(new Date(originalEvent.start).getTime() + startDelta),
end: new Date(new Date(originalEvent.end).getTime() + startDelta),
};
});
} else {
// RESIZE: proportionally remap all child events within the new group span
var oldGroupStart = oldStart;
var oldGroupEnd = oldEnd;
var oldGroupDuration = oldGroupEnd - oldGroupStart;
var newGroupStart = newStart;
var newGroupEnd = newEnd;
var newGroupDuration = newGroupEnd - newGroupStart;
eventsToUpdate = movedGroupedEvent.originalEvents.map(function (originalEvent) {
var evStart = new Date(originalEvent.start).getTime();
var evEnd = new Date(originalEvent.end).getTime();
// Calculate relative positions (0 to 1) within the old group span
var relativeStart = oldGroupDuration > 0 ? (evStart - oldGroupStart) / oldGroupDuration : 0;
var relativeEnd = oldGroupDuration > 0 ? (evEnd - oldGroupStart) / oldGroupDuration : 1;
// Map to the new group span
var mappedStart = newGroupStart + relativeStart * newGroupDuration;
var mappedEnd = newGroupStart + relativeEnd * newGroupDuration;
// Ensure minimum duration for each child event
var childDurationDays = (mappedEnd - mappedStart) / (1000 * 60 * 60 * 24);
if (childDurationDays < 1) {
mappedEnd = mappedStart + 1000 * 60 * 60 * 24; // At least 1 day
}
return {
id: originalEvent.id,
title: originalEvent.title,
resource: originalEvent.resource,
type: originalEvent.type,
clientGroup: originalEvent.clientGroup,
start: new Date(mappedStart),
end: new Date(mappedEnd),
};
});
}
// Sync into rawEvents
var updatedIds = {};
eventsToUpdate.forEach(function (e) {
updatedIds[e.id] = e;
});
rawEvents = rawEvents.map(function (e) {
return updatedIds[e.id] ? updatedIds[e.id] : e;
});
// Rebuild grouped view from updated rawEvents
updateView();
// Restore collapsed state
var newYear = new Date(eventsToUpdate[0].start).getFullYear();
var newQuarter = Math.floor(new Date(eventsToUpdate[0].start).getMonth() / 3) + 1;
var newGroupedEvent = groupedEvents.find(function (ge) {
return ge.resource === resourceId && ge.clientGroup === clientGroupName && ge.year === newYear && ge.quarter === newQuarter;
});
if (newGroupedEvent) {
newGroupedEvent.collapsed = wasCollapsed;
calendar.setEvents(groupedEvents);
}
var actionLabel = isMove ? 'moved' : 'resized';
mobiscroll.toast({
message: eventsToUpdate.length + ' event(s) for ' + clientGroupName + ' have been ' + actionLabel + '.',
});
}
} else {
var oldResource = args.oldEvent.resource;
var newResource = updatedEvent.resource;
var resourceChanged = oldResource !== newResource;
// Sync into rawEvents (dates + resource if changed)
rawEvents = rawEvents.map(function (e) {
if (e.id === updatedEvent.id) {
var update = { start: updatedEvent.start, end: updatedEvent.end };
if (resourceChanged) {
if (groupBy === 'assignee') {
update.resource = newResource;
} else {
update.type = newResource;
}
}
return Object.assign({}, e, update);
}
return e;
});
if (resourceChanged) {
var fromName;
var toName;
if (groupBy === 'assignee') {
var fromRes = assigneeResources.find(function (r) {
return r.id === oldResource;
});
var toRes = assigneeResources.find(function (r) {
return r.id === newResource;
});
fromName = fromRes ? fromRes.name : oldResource;
toName = toRes ? toRes.name : newResource;
} else {
fromName = oldResource;
toName = newResource;
}
mobiscroll.toast({
message: '"' + updatedEvent.title + '" moved from ' + fromName + ' to ' + toName + '.',
});
}
}
},
});
var editDatePicker = mobiscroll.datepicker('#event-grouping-dates', {
controls: ['calendar'],
select: 'range',
display: 'center',
touchUi: false,
showRangeLabels: false,
onChange: function (args) {
var dates = args.value;
var startVal = dates[0];
var endVal = dates[1];
if (editingEventId !== null && startVal && endVal) {
var oldEvent = rawEvents.find(function (e) {
return e.id === editingEventId;
});
if (!oldEvent) {
return;
}
var eventTitle = oldEvent.title;
var oldQuarter = Math.floor(new Date(oldEvent.start).getMonth() / 3);
var newQuarter = Math.floor(new Date(startVal).getMonth() / 3);
var oldYear = new Date(oldEvent.start).getFullYear();
var newYear = new Date(startVal).getFullYear();
var quarterChanged = groupByClient && (oldQuarter !== newQuarter || oldYear !== newYear);
var applyUpdate = function () {
rawEvents = rawEvents.map(function (e) {
if (e.id === editingEventId) {
return Object.assign({}, e, { start: startVal, end: endVal });
}
return e;
});
updateView();
return true;
};
if (quarterChanged) {
var quarterNames = ['Q1', 'Q2', 'Q3', 'Q4'];
var fromLabel = quarterNames[oldQuarter] + ' ' + oldYear;
var toLabel = quarterNames[newQuarter] + ' ' + newYear;
mobiscroll.confirm({
title: 'Move to different group',
message: '"' + eventTitle + '" will move from ' + fromLabel + ' to ' + toLabel + '. Do you want to continue?',
okText: 'Move',
cancelText: 'Cancel',
callback: function (result) {
if (result) {
applyUpdate();
mobiscroll.toast({
message: '"' + eventTitle + '" moved to ' + toLabel + '.',
});
}
},
});
} else {
if (applyUpdate()) {
mobiscroll.toast({
message: '"' + eventTitle + '" dates updated.',
});
}
}
}
},
});
// Edit button click handler (on expanded subtasks)
document.addEventListener('click', function (event) {
var editBtn = event.target.closest('.mds-event-group-edit-btn');
if (editBtn) {
event.stopPropagation();
var eventId = Number(editBtn.getAttribute('data-event-id'));
var rawEvent = rawEvents.find(function (e) {
return e.id === eventId;
});
if (rawEvent) {
editingEventId = eventId;
editDatePicker.setOptions({ headerText: rawEvent.title });
editDatePicker.setVal([new Date(rawEvent.start), new Date(rawEvent.end)]);
editDatePicker.open();
}
}
});
mobiscroll.select('#group-by-select', {
inputElement: document.getElementById('event-grouping-input'),
touchUi: false,
onChange: function (args) {
groupBy = args.value;
updateView();
},
});
// Checkbox for client/quarter grouping
document.getElementById('group-by-client-quarter').addEventListener('change', function () {
groupByClient = this.checked;
updateView();
});
// Zoom level handler
document.addEventListener('change', function (event) {
if (event.target && event.target.name === 'zoom-level') {
zoomLevel = event.target.value;
updateView();
}
});
// Expand/collapse icon click handler
document.addEventListener('click', function (event) {
if (event.target && event.target.classList.contains('mds-event-group-icon')) {
event.stopPropagation();
var task = event.target.closest('.mds-event-group-task');
var eventEl = event.target.closest('.mbsc-schedule-event');
var eventId = eventEl ? eventEl.getAttribute('data-id') : null;
var groupedEvent = groupedEvents.find(function (e) {
return e.id === eventId;
});
if (groupedEvent) {
// Toggle collapsed state in data
groupedEvent.collapsed = !groupedEvent.collapsed;
// Toggle CSS class for animation
if (task) {
task.classList.toggle('expanded', !groupedEvent.collapsed);
}
// Refresh calendar after animation completes
setTimeout(function () {
calendar.refresh();
}, 200);
}
}
});
updateView();
<div class="mds-event-group-calendar">
<div id="demo-event-grouping"></div>
<div id="event-grouping-dates"></div>
</div>
/* Calendar base styles */
.mds-event-group-calendar {
height: 100%;
}
.mds-event-group-calendar .mbsc-timeline-row {
height: 140px;
}
.mds-event-group-calendar .mbsc-timeline-resource-col {
width: 240px;
}
.mds-event-group-calendar .mbsc-ios.mbsc-textfield-wrapper-box {
margin: 10px 20px;
}
.mds-event-group-calendar .mbsc-timeline-resource-title {
text-transform: capitalize;
}
/* Resource rendering - Employees */
.mds-event-group-avatar {
width: 40px;
height: 40px;
}
.mds-event-group-cont {
padding: 0 7px;
}
.mds-event-group-name {
font-size: 14px;
line-height: 24px;
}
.mds-event-group-title {
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
/* Resource rendering - Types */
.mds-event-group-type-resource {
align-items: center;
padding: 8px 0;
}
.mds-event-group-type-badge {
width: 12px;
height: 12px;
border-radius: 50%;
margin: 0 10px;
}
.mds-event-group-type-name {
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
/* Enforce minimum event width so text is always visible */
.mds-event-group-calendar .mbsc-schedule-event {
min-width: 72px;
}
.mds-event-group-calendar .mbsc-schedule-event:has(.mds-event-group-task-client) {
min-width: 120px;
}
/* Grouped event - collapsed state */
.mds-event-group-task-client {
background-color: #dddddd;
border-left: 4px solid;
border-radius: 0 8px 8px 0;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.12),
0 1px 2px rgba(0, 0, 0, 0.08);
flex-direction: column;
container-type: inline-size;
min-width: 120px;
}
/* Grouped event header content */
.mds-event-group-content {
justify-content: space-between;
align-items: center;
padding: 10px 14px;
height: 42px;
box-sizing: border-box;
gap: 8px;
}
/* Client group title */
.mds-event-group-title-text {
font-size: 14px;
font-weight: 600;
color: #1e293b;
line-height: 20px;
min-width: 0;
position: sticky;
left: 250px;
}
/* Right side container (meta + icon) */
.mds-event-group-right {
align-items: center;
z-index: 1;
background-color: #dddddd;
}
/* Meta information (dates + counts) */
.mds-event-group-meta {
flex-direction: column;
align-items: flex-end;
flex: 1;
margin: 0 12px;
}
.mds-event-group-date-range {
color: #575757;
font-size: 11px;
margin-bottom: 2px;
}
.mds-event-group-count {
font-size: 11px;
white-space: nowrap;
}
/* Expand/collapse icon */
.mds-event-group-icon {
font-size: 20px;
cursor: pointer;
transition: transform 0.2s ease;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
transform: rotate(0deg);
}
/* Icon rotation when expanded */
.mds-event-group-task.expanded .mds-event-group-icon {
transform: rotate(180deg);
}
/* Grid wrapper for smooth animation */
.mds-event-group-events {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.2s ease;
}
.mds-event-group-events-inner {
overflow: hidden;
padding: 0 14px 4px;
transition: padding 0.2s ease;
}
.mds-event-group-task.expanded .mds-event-group-events {
grid-template-rows: 1fr;
}
/* Individual event in expanded list */
.mds-event-group-original-event {
background: #f1f1f1;
border-radius: 6px;
margin-bottom: 6px;
padding: 6px 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.14);
box-sizing: border-box;
min-height: 38px;
min-width: 100px;
overflow: hidden;
container-type: inline-size;
}
@container (max-width: 180px) {
.mds-event-group-meta { display: none; }
}
@container (max-width: 130px) {
.mds-event-group-event-right { display: none; }
}
/* Event content layout */
.mds-event-group-event-content {
justify-content: space-between;
align-items: center;
line-height: 14px;
}
/* Event title */
.mds-event-group-event-title {
font-weight: 600;
color: #1e293b;
font-size: 13px;
line-height: 18px;
flex: 1;
min-width: 30px;
margin-right: 8px;
}
.mds-event-group-text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Right side (date + detail) */
.mds-event-group-event-right {
flex-direction: column;
align-items: flex-end;
min-width: 0;
}
.mds-event-group-event-date {
font-size: 11px;
color: #94a3b8;
text-align: right;
margin-bottom: 2px;
max-width: 100%;
}
/* Detail container (avatar/dot + text) */
.mds-event-group-event-detail {
align-items: center;
justify-content: flex-end;
overflow: hidden;
max-width: 100%;
}
.mds-event-group-event-info {
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: capitalize;
}
/* Avatar for assignee in type view */
.mds-event-group-event-avatar {
width: 16px;
height: 16px;
border-radius: 50%;
margin-right: 6px;
}
/* Colored dot for type in assignee view */
.mds-event-group-type-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
/* Edit button on subtask events */
.mds-event-group-edit-btn {
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
color: #94a3b8;
padding: 4px;
margin-left: 8px;
border-radius: 4px;
transition: color 0.15s ease, background-color 0.15s ease;
flex-shrink: 0;
}
.mds-event-group-edit-btn:hover {
color: #1e293b;
background-color: rgba(0, 0, 0, 0.06);
}
/* Simple event styling (no grouping) */
.mds-event-simple {
padding: 10px 12px;
border-radius: 6px;
box-shadow:
0 2px 4px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.1);
justify-content: space-between;
align-items: center;
height: 42px;
box-sizing: border-box;
color: #2c2c2c;
background-color: #dddddd;
container-type: inline-size;
gap: 8px;
}
@container (max-width: 130px) {
.mds-event-simple-right { display: none; }
}
/* Event title */
.mds-event-simple-title {
font-size: 13px;
font-weight: 600;
position: sticky;
left: 250px;
min-width: 0;
}
.mds-event-simple-title-inner {
display: inline-block;
max-width: 100%;
}
/* Right side container */
.mds-event-simple-right {
flex-direction: column;
z-index: 1;
background-color: #dddddd;
}
/* Date range */
.mds-event-simple-date {
font-size: 11px;
opacity: 0.85;
white-space: nowrap;
text-align: right;
margin-bottom: 2px;
}
/* Subtitle wrapper (avatar/dot + text) */
.mds-event-simple-subtitle-wrapper {
position: relative;
align-items: center;
justify-content: flex-end;
}
/* Avatar for assignee in type view */
.mds-event-simple-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 6px;
position: absolute;
left: 0;
}
/* Colored dot for type in assignee view */
.mds-event-simple-type-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin: 0 6px;
}
/* Subtitle text (type or assignee name) */
.mds-event-simple-subtitle {
font-size: 11px;
font-weight: 700;
text-transform: capitalize;
white-space: nowrap;
}
/* Header controls */
.mds-event-group-header-controls {
align-items: center;
}
/* Select input */
.mds-event-group-select.mbsc-textfield {
width: 210px;
max-height: 34px;
}
.mds-event-group-select + .mbsc-ios.mbsc-select-icon {
font-size: 13px;
top: 8px;
}
.mds-event-group-calendar .mbsc-ios.mbsc-segmented-button {
padding: 1px 12px;
}