import 'bootstrap.css';
import '@mescius/wijmo.styles/wijmo.css';
import './styles.css';
//
import * as wjcCore from '@mescius/wijmo';
import * as wjcScheduler from '@mescius/wijmo.scheduler';
import { getAttendees, getDisplayDate, getMeetings, getRooms } from './data';
//
const avatarUrls = {
'Maya Chen': 'resources/avatars/maya-chen.png',
'Noah Reed': 'resources/avatars/noah-reed.png',
'Priya Shah': 'resources/avatars/priya-shah.png',
'Liam Brooks': 'resources/avatars/liam-brooks.png',
'Sofia Martinez': 'resources/avatars/sofia-martinez.png',
};
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();
function init() {
let useCustomTemplate = true;
const attendees = new Map(getAttendees().map((person) => [person.id, person]));
const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', {
itemsSource: new wjcCore.CollectionView(getMeetings()),
resources: [
{
fieldExpr: 'roomId',
label: 'Room',
dataSource: getRooms(),
valueExpr: 'id',
displayExpr: 'name',
backgroundColorExpr: 'backgroundColor',
foreColorExpr: 'foreColor',
},
],
definition: createWeekDefinition((ctx, element) => renderEventItem(ctx, element, attendees)),
displayDate: getDisplayDate(),
toolbarViewItems: [{ name: 'Week', key: wjcScheduler.SchedulerBuiltInViewItem.Week }],
});
document.querySelectorAll('[data-template]').forEach((button) => {
button.addEventListener('click', () => {
useCustomTemplate = button.dataset.template === 'custom';
updateItemTemplate(scheduler, useCustomTemplate ? (ctx, element) => renderEventItem(ctx, element, attendees) : null);
setSelectedTemplate(useCustomTemplate);
});
});
scheduler.hostElement.addEventListener('click', (event) => {
const favoriteButton = event.target.closest('[data-favorite]');
if (!favoriteButton) {
return;
}
event.preventDefault();
event.stopPropagation();
const hit = scheduler.hitTest(event);
const item = hit.getHitInfo();
const meeting = item && item.item;
if (meeting) {
scheduler.update(meeting, { favorite: !meeting.favorite });
}
});
}
function updateItemTemplate(scheduler, itemTemplate) {
if (scheduler.definition.itemTemplate !== itemTemplate) {
scheduler.definition = Object.assign(Object.assign({}, scheduler.definition), { itemTemplate });
}
}
function createWeekDefinition(itemTemplate) {
const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.Week);
return Object.assign(Object.assign({}, definition), { itemTemplate, rows: {
bindings: [
{
type: wjcScheduler.SchedulerAxisBindingType.Time,
unit: wjcScheduler.SchedulerTimeUnit.Hour,
visible: (args) => args.hour >= 8 && args.hour < 17,
},
],
} });
}
function renderEventItem(ctx, element, attendees) {
const item = ctx.item;
const room = ctx.resources[0];
if (!item.status) {
item.status = 'On track';
}
if (item.progress === undefined) {
item.progress = Math.floor(Math.random() * 101);
}
if (!item.attendeeIds) {
item.attendeeIds = [];
}
const eventStatus = item.status;
const eventProgress = item.progress;
const header = createHeader(item);
const meta = document.createElement('span');
const progress = document.createElement('span');
const progressValue = document.createElement('span');
const footer = document.createElement('span');
const details = document.createElement('span');
const avatars = document.createElement('span');
const status = document.createElement('span');
const progressText = document.createElement('span');
meta.className = 'event-meta';
meta.append(createMeta('bi-clock', formatTime(item)), createMeta('bi-geo-alt', room ? room.name : ''));
progress.className = 'event-progress';
progressValue.style.width = `${eventProgress}%`;
progress.appendChild(progressValue);
footer.className = 'event-footer';
details.className = 'event-details';
status.className = `event-status status-${eventStatus.toLocaleLowerCase().replace(' ', '-')}`;
status.textContent = eventStatus;
progressText.className = 'event-progress-text';
progressText.textContent = `${eventProgress}%`;
details.append(status, progressText);
avatars.className = 'event-avatars';
item.attendeeIds.slice(0, 3).forEach((id) => {
const person = attendees.get(id);
const avatar = document.createElement('span');
avatar.className = 'event-avatar';
avatar.title = person.name;
const image = document.createElement('img');
image.src = avatarUrls[person.name] || '';
image.alt = person.name;
image.addEventListener('error', () => {
avatar.textContent = person.initials;
avatar.style.backgroundColor = person.color;
}, { once: true });
avatar.appendChild(image);
avatars.appendChild(avatar);
});
footer.append(details, avatars);
element.textContent = '';
element.dataset.status = eventStatus.toLocaleLowerCase().replace(' ', '-');
element.append(header, meta, progress, footer);
return null;
}
function createHeader(item) {
const header = document.createElement('span');
const icon = document.createElement('i');
const title = document.createElement('strong');
const favorite = document.createElement('button');
const type = document.createElement('span');
header.className = 'event-header';
type.className = 'event-type-icon';
icon.className = `bi ${getMeetingIcon(item.meetingType)}`;
icon.setAttribute('aria-hidden', 'true');
type.title = item.meetingType;
type.appendChild(icon);
title.textContent = item.subject;
favorite.type = 'button';
favorite.className = 'favorite-button';
favorite.dataset.favorite = '';
favorite.title = item.favorite ? 'Remove from favorites' : 'Add to favorites';
favorite.setAttribute('aria-label', favorite.title);
const favoriteIcon = document.createElement('i');
favoriteIcon.className = `bi ${item.favorite ? 'bi-star-fill' : 'bi-star'}`;
favoriteIcon.setAttribute('aria-hidden', 'true');
favorite.appendChild(favoriteIcon);
if (item.priority === 'High') {
const priority = document.createElement('i');
priority.className = 'bi bi-lightning-charge-fill event-priority';
priority.title = 'High priority';
priority.setAttribute('aria-label', 'High priority');
header.append(type, title, priority, favorite);
}
else {
header.append(type, title, favorite);
}
return header;
}
function formatTime(item) {
const options = { hour: 'numeric', minute: '2-digit' };
return `${item.start.toLocaleTimeString(undefined, options)}-${item.end.toLocaleTimeString(undefined, options)}`;
}
function createMeta(iconName, text) {
const meta = document.createElement('span');
const icon = document.createElement('i');
icon.className = `bi ${iconName}`;
icon.setAttribute('aria-hidden', 'true');
meta.append(icon, document.createTextNode(text));
return meta;
}
function getMeetingIcon(type) {
switch (type) {
case 'Design':
return 'bi-bezier2';
case 'Customer':
return 'bi-people';
case 'People':
return 'bi-person-badge';
case 'Research':
return 'bi-search';
case 'Release':
return 'bi-rocket-takeoff';
default:
return 'bi-calendar-event';
}
}
function setSelectedTemplate(useCustomTemplate) {
document.querySelectorAll('[data-template]').forEach((button) => {
const selected = (button.dataset.template === 'custom') === useCustomTemplate;
button.setAttribute('aria-pressed', String(selected));
});
document.querySelector('#templateSummary').textContent = useCustomTemplate
? 'Status, priority, time, room, progress and participants'
: 'Scheduler default item content';
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>MESCIUS Wijmo Scheduler Item Templates</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<!-- SystemJS -->
<script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.5/dist/system.src.js" integrity="sha512-skZbMyvYdNoZfLmiGn5ii6KmklM82rYX2uWctBhzaXPxJgiv4XBwJnFGr5k8s+6tE1pcR1nuTKghozJHyzMcoA==" crossorigin="anonymous"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('./src/app');
</script>
</head>
<body>
<main class="container-fluid">
<div class="template-toolbar">
<span id="templateLabel"><i class="bi bi-layout-text-window" aria-hidden="true"></i> Event display</span>
<div class="segmented-control" role="group" aria-labelledby="templateLabel">
<button type="button" data-template="default" aria-pressed="false">Default</button>
<button type="button" data-template="custom" aria-pressed="true">Custom</button>
</div>
<span id="templateSummary" class="template-summary" aria-live="polite">Status, priority, time, room, progress and participants</span>
</div>
<div id="theScheduler"></div>
</main>
</body>
</html>
function createDate(day, hour, minute = 0) {
return new Date(2026, 4, day, hour, minute);
}
export function getDisplayDate() {
return createDate(13, 9);
}
export function getRooms() {
return [
{ id: 'atlas', name: 'Atlas Room', backgroundColor: '#2563eb', foreColor: '#ffffff' },
{ id: 'cedar', name: 'Cedar Room', backgroundColor: '#0f766e', foreColor: '#ffffff' },
{ id: 'studio', name: 'Design Studio', backgroundColor: '#a21caf', foreColor: '#ffffff' },
{ id: 'summit', name: 'Summit Room', backgroundColor: '#b45309', foreColor: '#ffffff' },
];
}
export function getAttendees() {
return [
{ id: 1, name: 'Maya Chen', initials: 'MC', color: '#1d4ed8' },
{ id: 2, name: 'Noah Reed', initials: 'NR', color: '#047857' },
{ id: 3, name: 'Priya Shah', initials: 'PS', color: '#7e22ce' },
{ id: 4, name: 'Liam Brooks', initials: 'LB', color: '#b45309' },
{ id: 5, name: 'Sofia Martinez', initials: 'SM', color: '#be185d' },
];
}
export function getMeetings() {
return [
{
id: 1,
subject: 'Quarterly Planning',
start: createDate(11, 9),
end: createDate(11, 11),
roomId: 'atlas',
meetingType: 'Planning',
attendeeIds: [1, 2, 3],
progress: 35,
favorite: true,
status: 'On track',
priority: 'High',
},
{
id: 2,
subject: 'Homepage Critique',
start: createDate(11, 13),
end: createDate(11, 15, 30),
roomId: 'studio',
meetingType: 'Design',
attendeeIds: [3, 5],
progress: 70,
favorite: false,
status: 'On track',
priority: 'Normal',
},
{
id: 3,
subject: 'Customer Workshop',
start: createDate(12, 9, 30),
end: createDate(12, 11, 30),
roomId: 'summit',
meetingType: 'Customer',
attendeeIds: [2, 4],
progress: 50,
favorite: false,
status: 'At risk',
priority: 'High',
},
{
id: 4,
subject: 'Hiring Panel',
start: createDate(12, 14),
end: createDate(12, 15, 30),
roomId: 'cedar',
meetingType: 'People',
attendeeIds: [1, 4, 5],
progress: 80,
favorite: true,
status: 'On track',
priority: 'Normal',
},
{
id: 5,
subject: 'Product Sync',
start: createDate(13, 9),
end: createDate(13, 10, 30),
roomId: 'atlas',
meetingType: 'Product',
attendeeIds: [1, 3],
progress: 45,
favorite: false,
status: 'On track',
priority: 'Normal',
},
{
id: 6,
subject: 'Prototype Testing',
start: createDate(13, 11),
end: createDate(13, 13),
roomId: 'studio',
meetingType: 'Research',
attendeeIds: [3, 5],
progress: 60,
favorite: false,
status: 'Blocked',
priority: 'High',
},
{
id: 7,
subject: 'Partner Briefing',
start: createDate(14, 10),
end: createDate(14, 11, 30),
roomId: 'summit',
meetingType: 'Partner',
attendeeIds: [2, 4],
progress: 30,
favorite: true,
status: 'At risk',
priority: 'High',
},
{
id: 8,
subject: 'Engineering Retro',
start: createDate(14, 14),
end: createDate(14, 16),
roomId: 'cedar',
meetingType: 'Team',
attendeeIds: [1, 2, 5],
progress: 90,
favorite: false,
status: 'On track',
priority: 'Normal',
},
{
id: 9,
subject: 'Release Readiness',
start: createDate(15, 9, 30),
end: createDate(15, 11, 30),
roomId: 'atlas',
meetingType: 'Release',
attendeeIds: [1, 2, 3],
progress: 75,
favorite: true,
status: 'At risk',
priority: 'High',
},
{
id: 10,
subject: 'Brand Review',
start: createDate(15, 13),
end: createDate(15, 14, 30),
roomId: 'studio',
meetingType: 'Design',
attendeeIds: [3, 5],
progress: 55,
favorite: false,
status: 'On track',
priority: 'Normal',
},
];
}
body {
color: #1f2937;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
user-select: none;
margin: 0;
}
.container-fluid {
box-sizing: border-box;
margin: 0;
padding: 5px;
}
.template-toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
min-height: 46px;
padding: 5px 9px;
border: 1px solid #dbe2ea;
background: #f8fafc;
color: #334155;
}
#templateLabel {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.segmented-control {
display: inline-flex;
overflow: hidden;
border: 1px solid #64748b;
border-radius: 4px;
}
.segmented-control button {
min-width: 76px;
min-height: 32px;
padding: 4px 12px;
border: 0;
border-right: 1px solid #64748b;
background: #ffffff;
color: #334155;
font: inherit;
}
.segmented-control button:last-child {
border-right: 0;
}
.segmented-control button[aria-pressed="true"] {
background: #0f6cbd;
color: #ffffff;
}
.segmented-control button:focus-visible {
outline: 2px solid #0f6cbd;
outline-offset: -3px;
}
.template-summary {
color: #526170;
font-size: 13px;
}
.event-header,
.event-footer,
.event-details,
.event-meta {
display: flex;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.event-header {
gap: 5px;
min-width: 0;
}
.event-type-icon {
display: inline-grid;
width: 24px;
height: 24px;
place-items: center;
border: 1px solid rgb(255 255 255 / 40%);
border-radius: 4px;
background: rgb(255 255 255 / 16%);
flex: 0 0 auto;
}
.event-priority {
color: #fde68a;
filter: drop-shadow(0 1px 1px rgb(15 23 42 / 30%));
}
.event-header strong {
overflow: hidden;
min-width: 0;
font-size: 13px;
line-height: 1.25;
text-overflow: ellipsis;
}
.event-header > .bi {
flex: 0 0 auto;
}
.favorite-button {
display: inline-grid;
place-items: center;
width: 24px;
height: 24px;
margin-left: auto;
padding: 0;
border: 0;
border-radius: 3px;
background: transparent;
color: inherit;
flex: 0 0 auto;
}
.favorite-button:hover {
background: rgb(255 255 255 / 18%);
}
.favorite-button:focus-visible {
outline: 2px solid currentColor;
outline-offset: -2px;
}
.event-progress {
display: block;
overflow: hidden;
height: 4px;
margin: 4px 0;
border-radius: 2px;
background: rgb(255 255 255 / 26%);
}
.event-progress > span {
display: block;
height: 100%;
border-radius: inherit;
background: rgb(255 255 255 / 85%);
}
.event-footer {
justify-content: space-between;
gap: 5px;
}
.event-meta {
gap: 8px;
margin-top: 3px;
font-size: 11px;
opacity: .9;
}
.event-meta span {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 3px;
overflow: hidden;
text-overflow: ellipsis;
}
.event-details {
gap: 3px;
min-width: 0;
font-size: 13px;
opacity: 0.92;
}
.event-status {
padding: 1px 5px;
border: 1px solid rgb(255 255 255 / 40%);
border-radius: 8px;
background: rgb(15 23 42 / 20%);
font-size: 10px;
font-weight: 700;
}
.status-at-risk {
background: rgb(146 64 14 / 52%);
}
.status-blocked {
background: rgb(153 27 27 / 58%);
}
.event-progress-text {
font-size: 11px;
font-weight: 700;
}
.event-avatars {
display: flex;
padding-left: 5px;
flex: 0 0 auto;
}
.event-avatar {
display: inline-grid;
place-items: center;
width: 23px;
height: 23px;
margin-left: -5px;
border: 2px solid rgb(255 255 255 / 75%);
border-radius: 50%;
box-shadow: 0 1px 2px rgb(15 23 42 / 24%);
color: #ffffff;
font-size: 9px;
font-weight: 700;
pointer-events: none;
}
.event-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
#theScheduler {
height: 710px;
min-height: 560px;
}
@media (max-width: 640px) {
.template-toolbar {
flex-wrap: wrap;
}
.template-summary {
flex: 1 1 100%;
}
.segmented-control button {
min-width: 70px;
padding-right: 9px;
padding-left: 9px;
}
#theScheduler {
height: 760px;
min-height: 760px;
}
}
input,
textarea,
[contenteditable="true"] {
user-select: text;
}
(function (global) {
System.config({
transpiler: 'plugin-babel',
babelOptions: {
es2015: true,
},
meta: {
'*.css': { loader: 'css' },
},
paths: {
// paths serve as alias
'npm:': 'node_modules/',
'cdn:': 'https://cdn.mescius.io/demoapps/packages/wijmojs/5.20262.60-rc/',
'pcdn:': 'https://cdn.jsdelivr.net/npm/',
},
// map tells the System loader where to look for things
map: {
'@mescius/wijmo': 'cdn:@mescius/wijmo/index.js',
'@mescius/wijmo.input': 'cdn:@mescius/wijmo.input/index.js',
'@mescius/wijmo.scheduler': 'cdn:@mescius/wijmo.scheduler/index.js',
'@mescius/wijmo.styles': 'cdn:@mescius/wijmo.styles',
'@mescius/wijmo.cultures': 'cdn:@mescius/wijmo.cultures',
jszip: 'pcdn:jszip@3.8.0/dist/jszip.js',
'bootstrap.css': 'pcdn:bootstrap@5.3.8/dist/css/bootstrap.min.css',
css: 'pcdn:systemjs-plugin-css@0.1.37/css.js',
'plugin-babel': 'pcdn:systemjs-plugin-babel@0.0.25/plugin-babel.js',
'systemjs-babel-build': 'pcdn:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js',
},
packages: {
src: {
defaultExtension: 'js',
},
node_modules: {
defaultExtension: 'js',
},
},
});
})(this);