1(function ($) { 2 3 let calendar; 4 5 let Calendar = function ($container, options) { 6 let obj = this; 7 jQuery.extend(obj.options, options); 8 9 // Special locale for moment 10 moment.locale('bookly', { 11 months: obj.options.l10n.datePicker.monthNames, 12 monthsShort: obj.options.l10n.datePicker.monthNamesShort, 13 weekdays: obj.options.l10n.datePicker.dayNames, 14 weekdaysShort: obj.options.l10n.datePicker.dayNamesShort, 15 meridiem: function (hours, minutes, isLower) { 16 return hours < 12 17 ? obj.options.l10n.datePicker.meridiem[isLower ? 'am' : 'AM'] 18 : obj.options.l10n.datePicker.meridiem[isLower ? 'pm' : 'PM']; 19 }, 20 }); 21 22 let allowEditAppointment = typeof BooklyAppointmentDialog !== 'undefined' && (!options.hasOwnProperty('disable_editor') || !options.disable_editor); 23 24 let viewTypes = { 25 dayGridMonth: 'resourceTimelineMonth', 26 timeGridWeek: 'resourceTimelineWeek', 27 resourceTimeGridDay: 'resourceTimelineDay' 28 } 29 // Event Popover 30 let $popover = $(); 31 let showPopover = 0; 32 let popoverEventEl; 33 34 function animate(fn) { 35 window.requestAnimationFrame(fn); 36 } 37 38 function popoverShow() { 39 ++showPopover; 40 $popover.css('opacity', 1); 41 } 42 43 function popoverClose() { 44 if (showPopover) { 45 --showPopover; 46 let start; 47 48 function fade(time) { 49 if (showPopover) { 50 return; 51 } 52 if (!start) { 53 start = time; 54 return animate(fade); 55 } 56 let progress = time - start; 57 if (progress < 100) { 58 animate(fade); 59 } else if (progress >= 100 && progress <= 300) { 60 $popover.css('opacity', 1 - (progress - 100) / 200); 61 animate(fade); 62 } else { 63 start = null; 64 $popover.remove(); 65 $popover.length = 0; 66 popoverEventEl = null; 67 } 68 } 69 70 animate(fade); 71 } 72 } 73 74 // Settings for Event Calendar 75 let settings = { 76 view: 'timeGridWeek', 77 customButtons: { 78 timeline: { 79 active: true, 80 text: {html: '<i class="far fa-fw fa-check-square"></i> ' + obj.options.l10n.timeline}, 81 click: function (e) { 82 let headerToolbar = calendar.getOption('headerToolbar'), 83 view = calendar.getOption('view'); 84 headerToolbar.end = 'dayGridMonth,timeGridWeek,resourceTimeGridDay,grid listWeek' 85 calendar.setOption('headerToolbar', headerToolbar); 86 let _viewTypes = Object.values(viewTypes); 87 if (_viewTypes.includes(view)) { 88 // Change view 89 Object.keys(viewTypes).forEach(function (key) { 90 if (viewTypes[key] === view) { 91 calendar.setOption('view', key); 92 } 93 }); 94 } else if (view === 'listWeek') { 95 setCookie('bookly_cal_view', 'listWeek'); 96 } 97 } 98 }, 99 grid: { 100 text: {html: '<i class="far fa-fw fa-square"></i> ' + obj.options.l10n.timeline}, 101 click: function () { 102 let headerToolbar = calendar.getOption('headerToolbar'), 103 view = calendar.getOption('view'); 104 headerToolbar.end = 'resourceTimelineMonth,resourceTimelineWeek,resourceTimelineDay,timeline listWeek' 105 calendar.setOption('headerToolbar', headerToolbar); 106 if (viewTypes.hasOwnProperty(view)) { 107 // Change view 108 calendar.setOption('view', viewTypes[view]); 109 } else if (view === 'listWeek') {
110 setCookie('bookly_cal_view', 'listWeekTimeline'); 111 } 112 } 113 }, 114 }, 115 views: { 116 dayGridMonth: { 117 dayHeaderFormat: function (date) { 118 return moment(date).locale('bookly').format('ddd'); 119 }, 120 displayEventEnd: true, 121 dayMaxEvents: obj.options.l10n.monthDayMaxEvents === '1', 122 theme: function (theme) { 123 if (obj.options.l10n.monthDayMaxEvents === '1') { 124 theme.view += ' ec-minimalistic'; 125 } 126 return theme; 127 } 128 }, 129 timeGridDay: { 130 dayHeaderFormat: function (date) { 131 return moment(date).locale('bookly').format('dddd'); 132 }, 133 pointer: true, 134 columnWidth: obj.options.l10n.calendar_version === 'latest' ? 'minmax(120px, 1fr)' : undefined 135 }, 136 timeGridWeek: {pointer: true}, 137 resourceTimeGridDay: { 138 pointer: true, 139 columnWidth: obj.options.l10n.calendar_version === 'latest' ? 'minmax(' + obj.options.l10n.clmn_min_width + 'px, 1fr)' : undefined 140 }, 141 resourceTimelineDay: { 142 pointer: true, 143 displayEventEnd: true, 144 slotWidth: obj.options.l10n.calendar_version === 'latest' ? 120 : 180 145 }, 146 resourceTimelineWeek: { 147 pointer: true, 148 displayEventEnd: true, 149 slotWidth: obj.options.l10n.calendar_version === 'latest' ? 120 : 180 150 }, 151 resourceTimelineMonth: { 152 pointer: true, 153 displayEventEnd: true, 154 columnWidth: obj.options.l10n.calendar_version === 'latest' ? 'minmax(120px, 1fr)' : undefined 155 } 156 }, 157 nowIndicator: true, 158 hiddenDays: obj.options.l10n.hiddenDays, 159 slotDuration: obj.options.l10n.slotDuration, 160 slotMinTime: obj.options.l10n.slotMinTime, 161 slotMaxTime: obj.options.l10n.slotMaxTime, 162 scrollTime: obj.options.l10n.scrollTime, 163 moreLinkContent: function (arg) { 164 return obj.options.l10n.more.replace('%d', arg.num) 165 }, 166 flexibleSlotTimeLimits: true, 167 eventStartEditable: false, 168 eventDurationEditable: false, 169 allDaySlot: false, 170 allDayContent: obj.options.l10n.allDay, 171 slotLabelFormat: function (date) { 172 return moment(date).locale('bookly').format(obj.options.l10n.mjsTimeFormat); 173 }, 174 dayHeaderFormat: function (date) { 175 return moment(date).locale('bookly').format('ddd, D'); 176 }, 177 listDayFormat: function (date) { 178 return moment(date).locale('bookly').format('dddd'); 179 }, 180 firstDay: obj.options.l10n.datePicker.firstDay, 181 locale: obj.options.l10n.locale.replace('_', '-'), 182 buttonText: { 183 today: obj.options.l10n.today, 184 dayGridMonth: obj.options.l10n.month, 185 timeGridWeek: obj.options.l10n.week, 186 timeGridDay: obj.options.l10n.day, 187 resourceTimeGridDay: obj.options.l10n.day, 188 resourceTimelineMonth: obj.options.l10n.month, 189 resourceTimelineWeek: obj.options.l10n.week, 190 resourceTimelineDay: obj.options.l10n.day, 191 listWeek: obj.options.l10n.list 192 }, 193 noEventsContent: obj.options.l10n.noEvents, 194 customScrollbars: true, 195 eventSources: [{ 196 url: ajaxurl, 197 method: 'POST', 198 extraParams: function () { 199 return { 200 action: 'bookly_get_staff_appointments', 201 csrf_token: BooklyL10nGlobal.csrf_token, 202 staff_ids: obj.options.getStaffMemberIds(), 203 location_ids: obj.options.getLocationIds(), 204 service_ids: obj.options.getServiceIds() 205 }; 206 } 207 }], 208 eventFilter: function (info) { 209 return !['dayGridMonth', 'timeGridWeek', 'listWeek'].includes(info.view.type) || !info.event.extendedProps.resources_only; 210 },
211 eventBackgroundColor: '#ccc', 212 eventMouseEnter: function (arg) { 213 arg.jsEvent.stopPropagation(); 214 if (arg.event.display === 'background') { 215 return ''; 216 } 217 let $event = $(arg.el); 218 if (arg.event.display === 'auto' && arg.view.type !== 'listWeek') { 219 if (popoverEventEl !== arg.el) { 220 $popover.remove(); 221 $popover.length = 0; 222 popoverEventEl = arg.el; 223 showPopover = 0; 224 } 225 if (!$popover.length) { 226 $popover = $('<div class="bookly-popover bs-popover-top bookly-ec-popover">') 227 let $calendar_container = $event.closest('.ec'); 228 let $arrow = $('<div class="arrow"></div><div class="bookly-arrow-background"></div>'); 229 let $body = $('<div class="popover-body">'); 230 let $buttons = allowEditAppointment ? popoverButtons(arg) : ''; 231 232 $body.append(arg.event.extendedProps.tooltip).append($buttons).css({minWidth: '180px'}); 233 $popover.append($arrow).append($body); 234 $calendar_container.append($popover); 235 $popover = $calendar_container.find('.bookly-ec-popover'); 236 237 let event_offset = $event.offset(), 238 popover_height = $popover.outerHeight(), 239 event_width = $event.outerWidth(), 240 popover_on_right = event_offset.left > window.innerWidth / 2; 241 242 $popover.css('min-width', (Math.min(400, event_width - 2)) + 'px'); 243 244 let popover_top = Math.min(arg.jsEvent.y, Math.max(0, Math.min(event_offset.top - $(document).scrollTop() + $event.outerHeight() - popover_height, $(window).height() - popover_height))); 245 let popover_left = popover_on_right ? event_offset.left - $popover.outerWidth() : Math.min(event_offset.left - 7 + event_width, $calendar_container.offset().left + $calendar_container.outerWidth() - $popover.outerWidth() - 32); 246 //Check if the popover is over a mouse pointer 247 if (popover_left <= arg.jsEvent.x && popover_left + $popover.outerWidth() >= arg.jsEvent.x && popover_top <= arg.jsEvent.y && popover_top + $popover.outerHeight() >= arg.jsEvent.y) { 248 popover_top = arg.jsEvent.y - popover_height - 8; 249 } 250 $popover.css('top', popover_top + 'px'); 251 $popover.css('left', popover_left + 'px'); 252 253 $arrow.css('top', Math.min(popover_height - 32, Math.max(arg.jsEvent.y - popover_top - 14, 8)) + 'px'); 254 if (popover_on_right) { 255 $popover.removeClass('bs-popover-top').removeClass('bs-popover-right').addClass('bs-popover-left'); 256 $arrow.css('right', '-8px'); 257 } else { 258 $popover.removeClass('bs-popover-top').removeClass('bs-popover-left').addClass('bs-popover-right'); 259 $arrow.css('left', '-8px'); 260 } 261 262 $popover.on('pointerenter', function () { 263 popoverShow(); 264 }); 265 $popover.on('pointerleave', function () { 266 popoverClose(); 267 }); 268 } 269 270 popoverShow(); 271 } 272 }, 273 eventMouseLeave: function () { 274 popoverClose(); 275 }, 276 eventContent: function (arg) { 277 if (arg.event.display === 'background') { 278 return ''; 279 } 280 let event = arg.event; 281 let props = event.extendedProps; 282 let nodes = []; 283 let $time = $('<div class="ec-event-time"/>'); 284 let $title = $('<div class="ec-event-title"/>'); 285 286 $time.append(props.header_text || (arg.event.display !== 'pointer' ? moment(arg.event.start).locale('bookly').format(
286obj.options.l10n.mjsTimeFormat) + ' - ' + moment(arg.event.end).locale('bookly').format(obj.options.l10n.mjsTimeFormat) : moment(arg.event.start).locale('bookly').format(obj.options.l10n.mjsTimeFormat))); 287 nodes.push($time.get(0)); 288 if (arg.view.type === 'listWeek') { 289 let dot = $('<div class="ec-event-dot"></div>').css('border-color', event.backgroundColor); 290 nodes.push($('<div/>').append(dot).get(0)); 291 } 292 $title.append(props.desc || ''); 293 nodes.push($title.get(0)); 294 295 switch (props.overall_status) { 296 case 'pending': 297 $time.addClass('text-muted'); 298 $title.addClass('text-muted'); 299 break; 300 case 'rejected': 301 case 'cancelled': 302 $time.addClass('text-muted').wrapInner('<s>'); 303 $title.addClass('text-muted'); 304 break; 305 } 306 307 if (arg.view.type === 'listWeek' && allowEditAppointment) { 308 $title.append(popoverButtons(arg)); 309 } 310 311 return {domNodes: nodes}; 312 }, 313 eventClick: function (arg) { 314 eventClick(arg) 315 }, 316 dateClick: function (arg) { 317 let staff_id, visible_staff_id; 318 if (['resourceTimeGridDay', 'resourceTimelineMonth', 'resourceTimelineWeek', 'resourceTimelineDay'].includes(arg.view.type)) { 319 staff_id = arg.resource.id; 320 visible_staff_id = 0; 321 } else { 322 staff_id = visible_staff_id = obj.options.getCurrentStaffId(); 323 } 324 addAppointmentDialog(arg.date, staff_id, visible_staff_id); 325 }, 326 noEventsClick: function (arg) { 327 let staffId = obj.options.getCurrentStaffId(); 328 addAppointmentDialog(arg.view.activeStart, staffId, staffId); 329 }, 330 loading: function (isLoading) { 331 if (!calendar) { 332 return; 333 } 334 if (isLoading) { 335 if (allowEditAppointment) { 336 BooklyL10nAppDialog.refreshed = true; 337 } 338 if (dateSetFromDatePicker) { 339 dateSetFromDatePicker = false; 340 } else { 341 calendar.setOption('highlightedDates', []); 342 } 343 $('.bookly-ec-loading').show(); 344 } else { 345 let allDay = false; 346 if (calendar.getEvents().length) { 347 calendar.getEvents().forEach(function (event) { 348 if (event.allDay) { 349 allDay = true; 350 } 351 }) 352 } 353 calendar.setOption('allDaySlot', allDay); 354 $('.bookly-ec-loading').hide(); 355 obj.options.refresh(); 356 } 357 }, 358 viewDidMount: function (info) { 359 calendar.setOption('highlightedDates', []); 360 obj.options.viewChanged(info.view); 361 }, 362 theme: function (theme) { 363 theme.button = 'btn btn-default'; 364 theme.buttonGroup = 'btn-group'; 365 theme.active = 'active'; 366 return theme; 367 } 368 }; 369 370 function eventClick(arg) { 371 if (arg.event.display === 'background') { 372 return; 373 } 374 if (arg.jsEvent) { 375 arg.jsEvent.stopPropagation(); 376 } 377 378 if (allowEditAppointment) { 379 if (arg.event.extendedProps.type === 'event') { 380 BooklyAttendeesDialog.showForm( 381 getBooklyModalContainer('bookly-attendees-dialog'), 382 {eventId: arg.event.extendedProps.id, onDone: function () { calendar.refetchEvents(); }} 383 ) 384 } else { 385 let visible_staff_id; 386 if (arg.view.type === 'resourceTimeGridDay') { 387 visible_staff_id = 0; 388 } else { 389 visible_staff_id = obj.options.getCurrentStaffId(); 390 } 391 BooklyAppointmentDialog.showDialog(
392 arg.event.id, 393 null, 394 null, 395 function (event) { 396 if (event == 'refresh') { 397 calendar.refetchEvents(); 398 } else { 399 if (event.start === null) { 400 // Task 401 calendar.removeEventById(event.id); 402 } else { 403 if (visible_staff_id == event.resourceId || visible_staff_id == 0) { 404 // Update event in calendar. 405 calendar.removeEventById(event.id); 406 calendar.addEvent(event); 407 } else { 408 // Switch to the event owner tab. 409 jQuery('li > a[data-staff_id=' + event.resourceId + ']').click(); 410 } 411 } 412 } 413 414 if (locationChanged) { 415 calendar.refetchEvents(); 416 locationChanged = false; 417 } 418 } 419 ); 420 } 421 } 422 } 423 424 function popoverButtons(arg) { 425 const $buttons = arg.view.type === 'listWeek' ? $('<div class="mt-2 d-flex"></div>') : $('<div class="mt-2 d-flex justify-content-end border-top pt-2"></div>'); 426 let props = arg.event.extendedProps; 427 if (props.hasOwnProperty('type') && props.type === 'event') { 428 $buttons.append( 429 $('<a class="btn btn-success btn-sm me-1">').append('<i class="fas fa-fw fa-users">') 430 .attr('title', obj.options.l10n.events.attendees) 431 .on('click', function (e) { 432 e.stopPropagation(); 433 BooklyAttendeesDialog.showForm( 434 getBooklyModalContainer('bookly-attendees-dialog'), 435 {eventId: arg.event.extendedProps.id, onDone: function () { calendar.refetchEvents(); }} 436 ) 437 }) 438 ); 439 } else { 440 $buttons.append($('<button class="btn btn-success btn-sm me-1">').append('<i class="far fa-fw fa-edit">').on('click', function (e) { 441 e.stopPropagation(); 442 eventClick(arg); 443 })); 444 if (props.participants == 'one') { 445 if (obj.options.l10n.recurring_appointments.active == '1' && props.series_id) { 446 $buttons.append( 447 $('<a class="btn btn-default btn-sm me-1">').append('<i class="fas fa-fw fa-link">') 448 .attr('title', obj.options.l10n.recurring_appointments.title) 449 .on('click', function (e) { 450 e.stopPropagation(); 451 BooklySeriesDialog.showDialog({ 452 series_id: props.series_id, 453 done: function () { 454 calendar.refetchEvents(); 455 } 456 }); 457 }) 458 ); 459 } 460 if (obj.options.l10n.waiting_list.active == '1' && props.waitlisted > 0) { 461 $buttons.append( 462 $('<a class="btn btn-default btn-sm me-1">').append('<i class="far fa-fw fa-list-alt">') 463 .attr('title', obj.options.l10n.waiting_list.title) 464 ); 465 } 466 if (obj.options.l10n.packages.active == '1' && props.package_id > 0) { 467 $buttons.append( 468 $('<a class="btn btn-default btn-sm me-1">').append('<i class="far fa-fw fa-calendar-alt">') 469 .attr('title', obj.options.l10n.packages.title) 470 .on('click', function (e) { 471 e.stopPropagation(); 472 if (obj.options.l10n.packages.active == '1' && props.package_id) { 473 $(document.body).trigger('bookly_packages.schedule_dialog', [props.package_id, function () { 474 calendar.refetchEvents(); 475 }]); 476 } 477 }) 478 ); 479 } 480 $buttons.append( 481 $('<a class="btn btn-danger btn-sm text-white">
481').append('<i class="far fa-fw fa-trash-alt">') 482 .attr('title', obj.options.l10n.delete) 483 .on('click', function(e) { 484 e.stopPropagation(); 485 // Localize contains only string values 486 if (obj.options.l10n.recurring_appointments.active == '1' && props.series_id) { 487 $(document.body).trigger('recurring_appointments.delete_dialog', [calendar, arg.event]); 488 } else { 489 new BooklyConfirmDeletingAppointment({ 490 action: 'bookly_delete_appointment', 491 appointment_id: arg.event.id, 492 csrf_token: BooklyL10nGlobal.csrf_token 493 }, 494 function(response) { 495 calendar.removeEventById(arg.event.id); 496 } 497 ); 498 } 499 }) 500 ); 501 } 502 } 503 504 return $buttons; 505 } 506 507 function addAppointmentDialog(date, staffId, visibleStaffId) { 508 if (allowEditAppointment) { 509 BooklyAppointmentDialog.showDialog( 510 null, 511 parseInt(staffId), 512 moment(date), 513 function (event) { 514 if (event == 'refresh') { 515 calendar.refetchEvents(); 516 } else { 517 if (visibleStaffId == event.resourceId || visibleStaffId == 0) { 518 if (event.start !== null) { 519 if (event.id) { 520 // Create event in calendar. 521 calendar.addEvent(event); 522 } else { 523 calendar.refetchEvents(); 524 } 525 } 526 } else { 527 // Switch to the event owner tab. 528 jQuery('li[data-staff_id=' + event.resourceId + ']').click(); 529 } 530 } 531 532 if (locationChanged) { 533 calendar.refetchEvents(); 534 locationChanged = false; 535 } 536 } 537 ); 538 } 539 } 540 541 542 /** 543 * Set cookie. 544 * 545 * @param key 546 * @param value 547 */ 548 function setCookie(key, value) { 549 var expires = new Date(); 550 expires.setFullYear(expires.getFullYear() + 3); 551 document.cookie = key + '=' + value + ';expires=' + expires.toUTCString(); 552 } 553 554 let dateSetFromDatePicker = false; 555 556 calendar = EventCalendar.create($container.get(0), $.extend(true, {}, settings, obj.options.calendar)); 557 558 $('.ec-toolbar .ec-title', $container).on('click', function () { 559 let picker = $(this).data('daterangepicker'); 560 picker.setStartDate(calendar.getOption('date')); 561 picker.setEndDate(calendar.getOption('date')); 562 }); 563 // Init date picker for fast navigation in Event Calendar. 564 $('.ec-toolbar .ec-title', $container).daterangepicker({ 565 parentEl: '.bookly-js-calendar', 566 singleDatePicker: true, 567 showDropdowns: true, 568 autoUpdateInput: false, 569 locale: obj.options.l10n.datePicker 570 }, function (start) { 571 dateSetFromDatePicker = true; 572 calendar.setOption('highlightedDates', [start.toDate()]); 573 calendar.setOption('date', start.toDate()); 574 }); 575 576 // Export calendar 577 this.ec = calendar; 578 }; 579 580 var locationChanged = false;
581 $('body').on('change', '#bookly-appointment-location', function () { 582 locationChanged = true; 583 }); 584 585 Calendar.prototype.options = { 586 calendar: {}, 587 getCurrentStaffId: function () { return -1; }, 588 getStaffMemberIds: function () { return [this.getCurrentStaffId()]; }, 589 getServiceIds: function () { return ['all']; }, 590 getLocationIds: function () { return ['all']; }, 591 refresh: function () {}, 592 viewChanged: function () {}, 593 l10n: {} 594 }; 595 596 window.BooklyCalendar = Calendar; 597})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.