window.maps = window.maps || {}
window.maps.gdp = {
"af": 16.63,
"al": 11.58,
"dz": 158.97,
"ao": 85.81,
"ag": 1.1,
"ar": 351.02,
"am": 8.83,
"au": 1219.72,
"at": 366.26,
"az": 52.17,
"bs": 7.54,
"bh": 21.73,
"bd": 105.4,
"bb": 3.96,
"by": 52.89,
"be": 461.33,
"bz": 1.43,
"bj": 6.49,
"bt": 1.4,
"bo": 19.18,
"ba": 16.2,
"bw": 12.5,
"br": 2023.53,
"bn": 11.96,
"bg": 44.84,
"bf": 8.67,
"bi": 1.47,
"kh": 11.36,
"cm": 21.88,
"ca": 1563.66,
"cv": 1.57,
"cf": 2.11,
"td": 7.59,
"cl": 199.18,
"cn": 5745.13,
"co": 283.11,
"km": 0.56,
"cd": 12.6,
"cg": 11.88,
"cr": 35.02,
"ci": 22.38,
"hr": 59.92,
"cy": 22.75,
"cz": 195.23,
"dk": 304.56,
"dj": 1.14,
"dm": 0.38,
"do": 50.87,
"ec": 61.49,
"eg": 216.83,
"sv": 21.8,
"gq": 14.55,
"er": 2.25,
"ee": 19.22,
"et": 30.94,
"fj": 3.15,
"fi": 231.98,
"fr": 2555.44,
"ga": 12.56,
"gm": 1.04,
"ge": 11.23,
"de": 3305.9,
"gh": 18.06,
"gr": 305.01,
"gd": 0.65,
"gt": 40.77,
"gn": 4.34,
"gw": 0.83,
"gy": 2.2,
"ht": 6.5,
"hn": 15.34,
"hk": 226.49,
"hu": 132.28,
"is": 12.77,
"in": 1430.02,
"id": 695.06,
"ir": 337.9,
"iq": 84.14,
"ie": 204.14,
"il": 201.25,
"it": 2036.69,
"jm": 13.74,
"jp": 5390.9,
"jo": 27.13,
"kz": 129.76,
"ke": 32.42,
"ki": 0.15,
"kr": 986.26,
"undefined": 5.73,
"kw": 117.32,
"kg": 4.44,
"la": 6.34,
"lv": 23.39,
"lb": 39.15,
"ls": 1.8,
"lr": 0.98,
"ly": 77.91,
"lt": 35.73,
"lu": 52.43,
"mk": 9.58,
"mg": 8.33,
"mw": 5.04,
"my": 218.95,
"mv": 1.43,
"ml": 9.08,
"mt": 7.8,
"mr": 3.49,
"mu": 9.43,
"mx": 1004.04,
"md": 5.36,
"mn": 5.81,
"me": 3.88,
"ma": 91.7,
"mz": 10.21,
"mm": 35.65,
"na": 11.45,
"np": 15.11,
"nl": 770.31,
"nz": 138,
"ni": 6.38,
"ne": 5.6,
"ng": 206.66,
"no": 413.51,
"om": 53.78,
"pk": 174.79,
"pa": 27.2,
"pg": 8.81,
"py": 17.17,
"pe": 153.55,
"ph": 189.06,
"pl": 438.88,
"pt": 223.7,
"qa": 126.52,
"ro": 158.39,
"ru": 1476.91,
"rw": 5.69,
"ws": 0.55,
"st": 0.19,
"sa": 434.44,
"sn": 12.66,
"rs": 38.92,
"sc": 0.92,
"sl": 1.9,
"sg": 217.38,
"sk": 86.26,
"si": 46.44,
"sb": 0.67,
"za": 354.41,
"es": 1374.78,
"lk": 48.24,
"kn": 0.56,
"lc": 1,
"vc": 0.58,
"sd": 65.93,
"sr": 3.3,
"sz": 3.17,
"se": 444.59,
"ch": 522.44,
"sy": 59.63,
"tw": 426.98,
"tj": 5.58,
"tz": 22.43,
"th": 312.61,
"tl": 0.62,
"tg": 3.07,
"to": 0.3,
"tt": 21.2,
"tn": 43.86,
"tr": 729.05,
"tm": 0,
"ug": 17.12,
"ua": 136.56,
"ae": 239.65,
"gb": 2258.57,
"us": 14624.18,
"uy": 40.71,
"uz": 37.72,
"vu": 0.72,
"ve": 285.21,
"vn": 101.99,
"ye": 30.02,
"zm": 15.69,
"zw": 5.57
}
jQuery.fn.vectorMap('addMap', 'mall', {"width":694,"height":213,"paths":{
"f102": {
"name": "Nike",
"path": "M13.74 162.23L30.83 160.72L36.51 151.85L31.53 65.98L23.53 59.28L7.96 60.36"
},
"f103": {
"name": "La Maison",
"path": "M40.51 57.55L41.81 84.48L52.62 94.54L74.14 93.02L71.98 55.50"
},
"f105": {
"name": "",
"path": "M72.73 55.60L89.17 54.74L91.44 91.61L74.68 92.91"
},
"f106": {
"name": "",
"path": "M90.04 54.52L120.21 52.47L122.81 89.35L91.98 91.51"
},
"f108": {
"name": "Hairsmiths Unisex Hair Salon",
"path": "M121.08 52.47L137.08 51.50L139.35 88.37L123.45 89.24"
},
"f109": {
"name": "Terra Plana",
"path": "M137.84 51.49L152.87 50.52L154.71 87.18L140.11 88.26"
},
"f110": {
"name": "Mariella Nails",
"path": "M153.95 50.52L169.74 49.55L172.01 86.21L155.79 87.18"
},
"f111": {
"name": "Goveci",
"path": "M170.93 49.55L204.91 47.59L207.27 83.94L173.09 86.21"
},
"f114": {
"name": "SoEasy",
"path": "M205.68 47.42L225.02 46.44L227.22 82.86L208.13 83.98"
},
"f166": {
"name": "Espressamente Illy",
"path": "M225.98 46.22L260.40 44.25L251.39 64.09L252.72 81.12L228.38 82.86"
},
"f167": {
"name": "An’ge Paris",
"path": "M308.57 71.34c-2.03 3.49 -2.68 6.70 -2.68 6.70l-52.24 3.08l-0.95 -16.84l9.16 -20.03l46.71 27.01"
},
"f169v": {
"name": "Veranda",
"path": "M283.35 44.43l-9.68 -8.62c0.00 0.00 21.95 -26.06 58.17 -30.98c7.14 -0.97 6.54 10.85 6.71 11.84C338.71 17.66 309.52 17.29 283.35 44.43z"
},
"f169": {
"name": "Ocean Basket",
"path": "M310.27 68.09c0.00 0.00 9.24 -16.87 30.50 -20.28c0.23 -0.04 -2.27 -31.79 -2.27 -31.79s-33.42 3.24 -55.16 28.39L310.27 68.09z"
},
"f172v": {
"name": "Veranda",
"path": "M411.35 45.06l9.50 -8.60c0.00 0.00 -23.48 -27.41 -57.86 -31.15c-7.10 -0.77 -6.64 11.36 -6.80 12.33S385.39 18.46 411.35 45.06z"
},
"f172": {
"name": "T.G.I Friday’s",
"path": "M384.25 68.99c0.00 0.00 -8.84 -17.27 -30.17 -21.01c-0.23 -0.04 2.12 -31.31 2.12 -31.31s33.42 3.24 55.15 28.39L384.25 68.99z"
},
"f170": {
"name": "Video Arcade & Gaming Zone",
"path": "M344.18 47.33l-1.30 -17.52h9.90 0.00l-1.30 17.68C351.48 47.49 348.07 47.17 344.18 47.33z"
},
"f174": {
"name": "Taco Bell",
"path": "M388.79 80.67l68.38 2.03l2.97 -36.50L434.50 44.25l-48.18 29.20C386.32 73.45 387.65 76.29 388.79 80.67z"
},
"f115": {
"name": "Early Learning Centre",
"path": "M460.97 46.36L492.28 48.47L489.52 84.96L458.22 82.69"
},
"f117": {
"name": "",
"path": "M490.50 84.96L505.42 85.94L507.74 49.62L493.06 48.80"
},
"f118": {
"name": "Hart",
"path": "M506.41 85.98L521.38 87.02L524.08 50.74L508.99 49.76"
},
"f119": {
"name": "",
"path": "M522.13 87.03L541.70 88.44L544.19 52.33L525.05 50.90"
},
"f120": {
"name": "",
"path": "M542.39 88.38L545.11 52.27L572.20 54.06L570.00 90.19"
},
"f122": {
"name": "Mavros",
"path": "M570.77 90.19L573.08 54.13L601.62 56.09L598.67 92.05"
},
"f124": {
"name": "",
"path": "M599.75 92.05L602.58 56.19L622.37 57.48L619.75 93.34"
},
"f125": {
"name": "Energiers",
"path": "M620.49 93.46L623.19 57.74L655.39 59.82L653.20 86.10L643.28 95.41"
},
"f126": {
"name": "",
"path": "M662.77 67.61L671.37 61.12L686.29 62.09L678.99 158.29L663.75 157.48L656.77 149.20"
},
"f128": {
"name": "Pablosky",
"path": "M627.57 160.48L646.57 161.73L649.63 125.68L630.65 124.38"
},
"f129": {
"name": "",
"path": "M605.83 159.10L626.46 160.48L629.68 124.22L609.08 122.76"
},
"f130": {
"name": "",
"path": "M584.42 157.71L604.86 159.10L608.43 122.60L587.67 121.30"
},
"f132": {
"name": "Cinnabon",
"path": "M558.95 155.93L583.61 157.71L587.02 121.14L561.87 119.52"
},
"f133": {
"name": "",
"path": "M542.89 154.88L558.33 155.93L560.90 119.52L545.81 118.38"
},
"f134": {
"name": "Crocodilino",
"path": "M517.75 153.42L541.79 154.80L544.84 118.38L520.67 116.60"
},
"f135": {
"name": "Krystalo - Fanelaki",
"path": "M501.69 152.28L516.78 153.26L519.70 116.76L504.29 115.62"
},
"f136": {
"name": "Lepus",
"path": "M454.00 149.04L500.72 152.28L503.31 115.62L455.46 112.54"
},
"f139": {
"name": "Engino",
"path": "M410.80 131.47L414.37 134.71L441.29 136.33L449.24 112.33L417.77 109.73"
},
"fr01": {
"name": "Rest Rooms",
"path": "M429.13 135.93l11.92 0.89c0.00 0.00 -5.84 15.17 -13.87 26.36l-8.76 -6.57C418.42 156.61 426.49 145.28 429.13 135.93z"
},
"f153": {
"name": "Al Rifai",
"path": "M283.19 130.87L279.58 134.12L253.17 135.90L244.41 111.08L276.21 109.38"
},
"fr02": {
"name": "Rest Rooms",
"path": "M265.09 135.56l-11.68 0.91c0.00 0.00 5.24 15.60 13.63 26.52l9.65 -7.14C276.69 155.85 268.03 144.82 265.09 135.56z"
},
"ffc": {
"name": "Food Court",
"path": "M280.37 109.08l26.93 -2.92c0.00 0.00 8.16 25.94 39.88 26.98c19.71 0.65 34.63 -14.85 39.42 -26.93c0.12 -0.30 26.15 3.35 26.15 3.35s-4.54 18.49 -15.57 30.98c-0.11 0.12 29.66 22.84 29.66 22.84c-0.00 0.00 -12.98 18.33 -36.01 29.69c-32.08 15.81 -86.95 17.77 -123.58 -29.50c-0.40 -0.51 28.88 -20.77 28.88 -20.77S278.21 110.65 280.37 109.08z"
},
"f155": {
"name": "155 Fashion",
"path": "M239.87 148.15L224.95 148.39L221.87 112.38L243.12 111.08L245.88 136.71L243.93 133.31L239.55 133.63"
},
"f156": {
"name": "Giacomo de Luca",
"path": "M201.43 113.52L220.89 112.22L223.81 148.55L204.35 150.01"
},
"f157": {
"name": "G+ Collections",
"path": "M179.85 115.30L200.62 113.67L203.54 150.01L182.93 151.47"
},
"f158": {
"name": "Femme Femme",
"path": "M157.14 116.76L178.80 115.30L181.79 151.56L160.12 153.19"
},
"f159": {
"name": "",
"path": "M134.92 154.72L159.01 153.26L156.33 116.76L132.00 118.22"
},
"f160": {
"name": "Le Rouge",
"path": "M110.10 156.50L133.78 154.88L131.03 118.22L107.50 120.00"
},
"f162": {
"name": "",
"path": "M91.77 157.64L109.29 156.50L106.53 120.08L89.33 121.14"
},
"f164": {
"name": "",
"path": "M67.27 159.42L90.96 157.64L88.52 121.30L64.76 122.68"
},
"f165": {
"name": "Ose",
"path": "M47.97 160.72L66.30 159.42L63.95 122.68L45.62 123.82"
},
"fr03": {
"name": "Rest Rooms",
"path": "M239.93 133.90L243.82 133.69L245.79 136.96L245.79 140.93L240.03 141.01"
},
"fr04": {
"name": "Rest Rooms",
"path": "M245.77 148.04L240.20 148.07L240.03 141.25L245.77 141.19"
}
}});
window.maps = window.maps || {}
window.maps.mall = {
"f102": "SPORTS & OUTDOOR",
"f103": "HOME DECOR",
"f105": "FASHION",
"f106": "OTHER",
"f108": "BEAUTY & SPA",
"f109": "FASHION",
"f110": "BEAUTY & SPA",
"f111": "URBAN FAVORITES",
"f114": "SERVICES",
"f166": "DINING",
"f167": "FASHION",
"f169": "DINING",
"f170": "ENTERTAINMENT",
"f172": "DINING",
"f174": "DINING",
"f115": "KIDS STUFF",
"f117": "LIFESTYLE",
"f118": "URBAN FAVORITES",
"f119": "FASHION",
"f120": "FASHION",
"f122": "KIDS STUFF",
"f124": "KIDS STUFF",
"f125": "KIDS STUFF",
"f126": "KIDS STUFF",
"f128": "KIDS STUFF",
"f129": "LIFESTYLE",
"f130": "HOME DECOR",
"f132": "DINING",
"f133": "SPORTS & OUTDOOR",
"f134": "KIDS STUFF",
"f135": "LIFESTYLE",
"f136": "LIFESTYLE",
"f139": "KIDS STUFF",
"f153": "DINING",
"f155": "FASHION",
"f156": "URBAN FAVORITES",
"f157": "URBAN FAVORITES",
"f158": "LINGERIE & UNDERWEAR",
"f159": "FASHION",
"f160": "FASHION",
"f162": "FASHION",
"f164": "FASHION",
"f165": "FASHION",
"fr01": "REST ROOMS",
"fr02": "REST ROOMS",
"fr03": "REST ROOMS",
"fr04": "REST ROOMS",
"ffc": "DINING"
}
//= require stack/core-scripts
// Self Initialize DOM Factory Components
domFactory.handler.autoInit()
// Connect button(s) to drawer(s)
var sidebarToggle = Array.prototype.slice.call(document.querySelectorAll('[data-toggle="sidebar"]'))
sidebarToggle.forEach(function (toggle) {
toggle.addEventListener('click', function (e) {
var selector = e.currentTarget.getAttribute('data-target') || '#default-drawer'
var drawer = document.querySelector(selector)
if (drawer) {
drawer.mdkDrawer.toggle()
}
})
})
$('[data-toggle="fullcalendar"]').each(function() {
const element = $(this)
const options = {
themeSystem: 'bootstrap4',
closeButton: void 0 !== element.data('toastr-close-button')
? element.data('toastr-close-button')
: false,
slotDuration: '00:15:00',
minTime: '08:00:00',
maxTime: '19:00:00',
defaultView: 'month',
handleWindowResize: true,
height: $(window).height() - 200,
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
events,
editable: true,
droppable: true,
eventLimit: true,
selectable: true,
drop: function(date) {
onDrop($(this), date, element)
},
select: function(start, end, jsEvent) {
onSelect(start, end, jsEvent, element)
},
eventClick: function(event, jsEvent, view) {
onEventClick(event, jsEvent, view, element)
}
}
element.fullCalendar(options)
})
let drawers = document.querySelectorAll('.mdk-drawer')
drawers = Array.prototype.slice.call(drawers)
drawers.forEach((drawer) => {
drawer.addEventListener('mdk-drawer-change', (e) => {
if (!e.target.mdkDrawer) {
return
}
document.querySelector('body').classList[e.target.mdkDrawer.opened ? 'add' : 'remove']('has-drawer-opened')
let button = document.querySelector('[data-target="#' + e.target.id + '"]')
if (button) {
button.classList[e.target.mdkDrawer.opened ? 'add' : 'remove']('active')
}
})
})
// SIDEBAR COLLAPSE MENUS
$('.sidebar .collapse').on('show.bs.collapse', function (e) {
e.stopPropagation()
var parent = $(this).parents('.sidebar-submenu').get(0) || $(this).parents('.sidebar-menu').get(0)
$(parent).find('.open').find('.collapse').collapse('hide');
$(this).closest('li').addClass('open');
});
$('.sidebar .collapse').on('hidden.bs.collapse', function (e) {
e.stopPropagation()
$(this).closest('li').removeClass('open');
});
// ENABLE TOOLTIPS
$('[data-toggle="tooltip"]').tooltip()
// ENABLE TABS
$('[data-toggle="tab"]').on('hide.bs.tab', function (e) {
$(e.target).removeClass('active')
})
document.addEventListener("turbolinks:load", function() {
domFactory.handler.upgradeAll();
// Connect button(s) to drawer(s)
var sidebarToggle = Array.prototype.slice.call(document.querySelectorAll('[data-toggle="sidebar"]'))
sidebarToggle.forEach(function (toggle) {
toggle.addEventListener('click', function (e) {
var selector = e.currentTarget.getAttribute('data-target') || '#default-drawer'
var drawer = document.querySelector(selector)
if (drawer) {
drawer.mdkDrawer.toggle()
}
})
})
// SIDEBAR COLLAPSE MENUS
$('.sidebar .collapse').on('show.bs.collapse', function (e) {
e.stopPropagation()
var parent = $(this).parents('.sidebar-submenu').get(0) || $(this).parents('.sidebar-menu').get(0)
$(parent).find('.open').find('.collapse').collapse('hide');
$(this).closest('li').addClass('open');
});
$('.sidebar .collapse').on('hidden.bs.collapse', function (e) {
e.stopPropagation()
$(this).closest('li').removeClass('open');
});
// DRAG AND DROP
$('[data-toggle="dragula"]').each(function() {
var element = $(this)
var options = {
moves: function(el, source, handle, sibling) {
return void 0 !== element.data('dragula-moves')
? handle.classList.contains(element.data('dragula-moves'))
: true
}
}
var containers = $(this).data('dragula-containers')
var elements = []
if (containers) {
for (var i = 0; i < containers.length; i++) {
elements.push(document.querySelector(containers[i]))
}
}
else {
elements.push(element[0])
}
dragula(elements, options)
})
// END
$('[data-toggle="fullcalendar"]').each(function() {
const element = $(this)
const options = {
themeSystem: 'bootstrap4',
closeButton: void 0 !== element.data('toastr-close-button')
? element.data('toastr-close-button')
: false,
slotDuration: '00:15:00',
minTime: '08:00:00',
maxTime: '19:00:00',
defaultView: 'month',
handleWindowResize: true,
height: $(window).height() - 200,
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
events,
editable: true,
droppable: true,
eventLimit: true,
selectable: true,
drop: function(date) {
onDrop($(this), date, element)
},
select: function(start, end, jsEvent) {
onSelect(start, end, jsEvent, element)
},
eventClick: function(event, jsEvent, view) {
onEventClick(event, jsEvent, view, element)
}
}
element.fullCalendar(options)
})
// DATERANGE
$('[data-toggle="daterangepicker"]').each(function() {
const element = $(this)
const format = void 0 !== element.data('daterangepicker-locale-format')
? element.data('daterangepicker-locale-format')
: 'YYYY/MM/DD'
const ranges = {
'Today': [moment(), moment()],
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
const options = {
showDropdowns: void 0 !== element.data('daterangepicker-show-dropdowns')
? element.data('daterangepicker-show-dropdowns')
: false,
drops: void 0 !== element.data('daterangepicker-drops')
? element.data('daterangepicker-drops')
: 'down',
opens: void 0 !== element.data('daterangepicker-opens')
? element.data('daterangepicker-opens')
: 'right',
startDate: void 0 !== element.data('daterangepicker-start-date')
? element.data('daterangepicker-start-date')
: moment(),
endDate: void 0 !== element.data('daterangepicker-end-date')
? element.data('daterangepicker-end-date')
: moment().add(1, 'month'),
singleDatePicker: void 0 !== element.data('daterangepicker-single-date-picker')
? element.data('daterangepicker-single-date-picker')
: false,
autoApply: void 0 !== element.data('daterangepicker-auto-apply')
? element.data('daterangepicker-auto-apply')
: true,
timePicker: void 0 !== element.data('daterangepicker-time-picker')
? element.data('daterangepicker-time-picker')
: false,
timePicker24Hour: void 0 !== element.data('daterangepicker-time-picker-24-hour')
? element.data('daterangepicker-time-picker-24-hour')
: false,
ranges: void 0 !== element.data('daterangepicker-ranges')
? ranges
: false,
locale: {
format,
separator: void 0 !== element.data('daterangepicker-locale-separator')
? element.data('daterangepicker-locale-separator')
: ' to ',
}
}
element.daterangepicker(options)
})
// SELECT2
$.fn.select2.defaults.set('theme', 'bootstrap4')
function templateResult(a) {
if (!a.id)
return a.text;
var e = $(a.element).data("avatar-src");
return e ? $('' + a.text + "") : a.text
}
$('[data-toggle="select"]').each(function() {
var element = $(this)
var options = {
dropdownParent: element.closest(".modal").length ? element.closest(".modal") : $(document.body),
minimumResultsForSearch: element.data("minimum-results-for-search"),
templateResult
}
element.select2(options)
})
// LISTS
$('[data-toggle="lists"]').each(function() {
var element = $(this)
var options = {
valueNames: void 0 !== element.data('lists-values')
? element.data('lists-values')
: [],
listClass: void 0 !== element.data('lists-class')
? element.data('lists-class')
: 'list'
}
var listElement = element.get(0)
var list = new List(listElement, options)
Object.defineProperty(listElement, 'List', {
configurable: true,
writable: false,
value: list
})
})
// VECTOR
$('[data-toggle="vector-map"]').each(function() {
var element = $(this)
var values = element.data('vector-map-values') ? maps[element.data('vector-map-values')] || {} : {}
let pins = {}
try {
pins = element.data('vector-map-pins')
for (var key in pins) {
if (pins.hasOwnProperty(key)) {
pins[key] = escapeXml(pins[key])
}
}
}
catch(e) {}
var options = {
map: element.data('vector-map-map'),
zoomOnScroll: void 0 !== element.data('vector-map-zoom-on-scroll')
? element.data('vector-map-zoom-on-scroll')
: false,
enableZoom: void 0 !== element.data('vector-map-enable-zoom')
? element.data('vector-map-enable-zoom')
: false,
showTooltip: void 0 !== element.data('vector-map-show-tooltip')
? element.data('vector-map-show-tooltip')
: true,
focusOnSelect: void 0 !== element.data('vector-map-focus-on-select')
? element.data('vector-map-focus-on-select')
: false,
backgroundColor: void 0 !== element.data('vector-map-background-color')
? element.data('vector-map-background-color')
: 'transparent',
values,
color: settings.colors.gray[50],
selectedColor: settings.colors.primary[300],
hoverColor: settings.colors.primary[100],
scaleColors: [ settings.colors.primary[50], settings.colors.primary[500] ],
borderWidth: 1,
borderColor: '#ffffff',
borderOpacity: 1,
normalizeFunction: 'polynomial',
colors: {},
pins,
pinMode: 'content',
onLabelShow: function (e, label, code) {
label.html(label.html() + ' - ' + values[ code ])
},
onRegionSelect: function(event, code, region) {
if (options.focusOnSelect) {
map.setFocus(code)
}
}
}
var colors = element.data('vector-map-values-colors')
if (colors) {
for (var key in values) {
if (values.hasOwnProperty(key) && colors[values[key]] !== undefined) {
const color = colors[values[key]]
options.colors[key] = settings.colors.get(color) || color
}
}
}
element.vectorMap(options)
const map = $(this).data().mapObject
if (Object.keys(options.colors)) {
map.setColors(options.colors)
}
const scale = element.data('vector-map-scale')
const focus = element.data('vector-map-focus')
if (scale) {
map.setScale(scale)
map.positionPins()
}
else if (focus) {
map.setFocus(focus)
}
})
$('[data-toggle=vector-map-focus]').on('click', function(e) {
e.preventDefault()
const element = $(this)
const target = $(element.data('target'))
if (!target) {
return
}
const map = target.data().mapObject
const focus = element.data('focus')
const animate = element.data('animate')
if (focus) {
map.setFocus(focus, animate)
}
})
});
(function(){
'use strict';
$('[data-toggle="quill"]').each(function() {
var element = $(this)
var options = {
modules: {
toolbar: void 0 !== element.data('quill-modules-toolbar')
? element.data('quill-modules-toolbar')
: {}
},
theme: void 0 !== element.data('quill-theme')
? element.data('quill-theme')
: 'snow',
placeholder: void 0 !== element.data('quill-placeholder')
? element.data('quill-placeholder')
: Quill.DEFAULTS.placeholder,
readOnly: void 0 !== element.data('quill-read-only')
? element.data('quill-read-only')
: Quill.DEFAULTS.readOnly,
debug: void 0 !== element.data('quill-debug')
? element.data('quill-debug')
: Quill.DEFAULTS.debug,
formats: void 0 !== element.data('quill-formats')
? element.data('quill-formats')
: Quill.DEFAULTS.formats
}
var quillElement = element.get(0)
var quill = new Quill(quillElement, options)
Object.defineProperty(quillElement, 'Quill', {
configurable: true,
writable: false,
value: quill
})
})
})()
const drawRoundedTopRectangle = (ctx, x, y, width, height, radius) => {
ctx.beginPath()
ctx.moveTo(x + radius, y)
// top right corner
ctx.lineTo(x + width - radius, y)
ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
// bottom right corner
ctx.lineTo(x + width, y + height)
// bottom left corner
ctx.lineTo(x, y + height);
// top left
ctx.lineTo(x, y + radius)
ctx.quadraticCurveTo(x, y, x + radius, y)
ctx.closePath()
}
Chart.elements.RoundedTopRectangle = Chart.elements.Rectangle.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, signX, signY, borderSkipped;
var borderWidth = vm.borderWidth;
if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
signX = 1;
signY = bottom > top? 1: -1;
borderSkipped = vm.borderSkipped || 'bottom';
} else {
// horizontal bar
left = vm.base;
right = vm.x;
top = vm.y - vm.height / 2;
bottom = vm.y + vm.height / 2;
signX = right > left? 1: -1;
signY = 1;
borderSkipped = vm.borderSkipped || 'left';
}
// Canvas doesn't allow us to stroke inside the width so we can
// adjust the sizes to fit if we're setting a stroke on the line
if (borderWidth) {
// borderWidth shold be less than bar width and bar height.
var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
borderWidth = borderWidth > barSize? barSize: borderWidth;
var halfStroke = borderWidth / 2;
// Adjust borderWidth when bar top position is near vm.base(zero).
var borderLeft = left + (borderSkipped !== 'left'? halfStroke * signX: 0);
var borderRight = right + (borderSkipped !== 'right'? -halfStroke * signX: 0);
var borderTop = top + (borderSkipped !== 'top'? halfStroke * signY: 0);
var borderBottom = bottom + (borderSkipped !== 'bottom'? -halfStroke * signY: 0);
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop;
bottom = borderBottom;
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft;
right = borderRight;
}
}
// calculate the bar width and roundess
var barWidth = Math.abs(left - right);
var roundness = this._chart.config.options.barRoundness || 0.5;
var radius = barWidth * roundness * 0.5;
// keep track of the original top of the bar
var prevTop = top;
// move the top down so there is room to draw the rounded top
top = prevTop + radius;
var barRadius = top - prevTop;
ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = borderWidth;
// draw the rounded top rectangle
drawRoundedTopRectangle(ctx, left, (top - barRadius + 1), barWidth, bottom - prevTop, barRadius);
ctx.fill();
if (borderWidth) {
ctx.stroke();
}
// restore the original top value so tooltips and scales still work
top = prevTop;
},
})
Chart.defaults.roundedBar = Chart.helpers.clone(Chart.defaults.bar)
Chart.controllers.roundedBar = Chart.controllers.bar.extend({
dataElementType: Chart.elements.RoundedTopRectangle
})
(function(){
'use strict';
$('[data-toggle="lists"]').each(function() {
var element = $(this)
var options = {
valueNames: void 0 !== element.data('lists-values')
? element.data('lists-values')
: [],
listClass: void 0 !== element.data('lists-class')
? element.data('lists-class')
: 'list'
}
var listElement = element.get(0)
var list = new List(listElement, options)
Object.defineProperty(listElement, 'List', {
configurable: true,
writable: false,
value: list
})
})
})()
(function(){
'use strict';
$('[data-toggle="daterangepicker"]').each(function() {
const element = $(this)
const format = void 0 !== element.data('daterangepicker-locale-format')
? element.data('daterangepicker-locale-format')
: 'YYYY/MM/DD'
const ranges = {
'Today': [moment(), moment()],
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
}
const options = {
showDropdowns: void 0 !== element.data('daterangepicker-show-dropdowns')
? element.data('daterangepicker-show-dropdowns')
: false,
drops: void 0 !== element.data('daterangepicker-drops')
? element.data('daterangepicker-drops')
: 'down',
opens: void 0 !== element.data('daterangepicker-opens')
? element.data('daterangepicker-opens')
: 'right',
startDate: void 0 !== element.data('daterangepicker-start-date')
? element.data('daterangepicker-start-date')
: moment(),
endDate: void 0 !== element.data('daterangepicker-end-date')
? element.data('daterangepicker-end-date')
: moment().add(1, 'month'),
singleDatePicker: void 0 !== element.data('daterangepicker-single-date-picker')
? element.data('daterangepicker-single-date-picker')
: false,
autoApply: void 0 !== element.data('daterangepicker-auto-apply')
? element.data('daterangepicker-auto-apply')
: true,
timePicker: void 0 !== element.data('daterangepicker-time-picker')
? element.data('daterangepicker-time-picker')
: false,
timePicker24Hour: void 0 !== element.data('daterangepicker-time-picker-24-hour')
? element.data('daterangepicker-time-picker-24-hour')
: false,
ranges: void 0 !== element.data('daterangepicker-ranges')
? ranges
: false,
locale: {
format,
separator: void 0 !== element.data('daterangepicker-locale-separator')
? element.data('daterangepicker-locale-separator')
: ' to ',
}
}
element.daterangepicker(options)
})
})()
(function(){
'use strict';
JQVMap.prototype.resizeContainer = function () {
this.width = this.container.width()
this.height = this.container.height()
this.resize()
this.canvas.setSize(this.width, this.height)
this.applyTransform()
this.positionPins()
}
JQVMap.prototype.setFocus = function (codes, animate = true) {
if (typeof codes === 'string') {
codes = [codes]
}
var map = this
var coords, newCoords;
codes.forEach(cc => {
var el = $('#' + this.getCountryId(cc))
var path = el[0]
var bbox = path.getBBox()
if (typeof coords == 'undefined') {
coords = bbox
}
else {
newCoords = {
x: Math.min(coords.x, bbox.x),
y: Math.min(coords.y, bbox.y),
width: Math.max(coords.x + coords.width, bbox.x + bbox.width) - Math.min(coords.x, bbox.x),
height: Math.max(coords.y + coords.height, bbox.y + bbox.height) - Math.min(coords.y, bbox.y)
}
coords = newCoords
}
})
var scale = Math.min(this.width / coords.width, this.height / coords.height)
if (scale > this.zoomMaxStep * this.baseScale) {
scale = this.zoomMaxStep * this.baseScale
}
else if (scale < this.baseScale) {
scale = this.baseScale
}
var zoomStep = scale / this.scale
this.zoomCurStep = this.zoomCurStep * Math.round(zoomStep)
var cX = coords.x * scale
var cY = coords.y * scale
var middleX = coords.width / 2
var middleY = coords.height / 2
var anchorX = (cX - middleX) / scale * -1
var anchorY = (cY - middleY) / scale * -1
var transX = anchorX + this.defaultWidth * (this.width / (this.defaultWidth * scale)) / 2
var transY = anchorY + this.defaultHeight * (this.height / (this.defaultHeight * scale)) / 2
var count = Math.abs(Math.round((scale - this.scale) * 60 / Math.max(scale, this.scale))) || 30
if (animate) {
var i = 0,
scaleStart,
scaleDiff,
transXStart,
transXDiff,
transYStart,
transYDiff
scaleStart = this.scale;
scaleDiff = (scale - scaleStart) / count;
transXStart = this.transX * this.scale;
transYStart = this.transY * this.scale;
transXDiff = (transX * scale - transXStart) / count;
transYDiff = (transY * scale - transYStart) / count;
function frame() {
i += 1
map.scale = scaleStart + scaleDiff * i
map.transX = (transXStart + transXDiff * i) / map.scale
map.transY = (transYStart + transYDiff * i) / map.scale
map.applyTransform()
map.positionPins()
if (i < count) {
requestAnimationFrame(frame)
}
}
requestAnimationFrame(frame)
}
else {
this.transX = transX
this.transY = transY
this.setScale(scale)
this.positionPins()
}
}
function escapeXml(string) {
return string.replace(/[<>]/g, function (c) {
switch (c) {
case '<': return '\u003c';
case '>': return '\u003e';
}
});
}
function resizeContainer() {
$('[data-toggle="vector-map"]').each(function() {
const element = $(this)
const map = element.data().mapObject
map.resizeContainer()
const focus = element.data('vector-map-focus')
if (focus) {
map.setFocus(focus)
}
})
}
$('[data-toggle="vector-map"]').each(function() {
var element = $(this)
var values = element.data('vector-map-values') ? maps[element.data('vector-map-values')] || {} : {}
let pins = {}
try {
pins = element.data('vector-map-pins')
for (var key in pins) {
if (pins.hasOwnProperty(key)) {
pins[key] = escapeXml(pins[key])
}
}
}
catch(e) {}
var options = {
map: element.data('vector-map-map'),
zoomOnScroll: void 0 !== element.data('vector-map-zoom-on-scroll')
? element.data('vector-map-zoom-on-scroll')
: false,
enableZoom: void 0 !== element.data('vector-map-enable-zoom')
? element.data('vector-map-enable-zoom')
: false,
showTooltip: void 0 !== element.data('vector-map-show-tooltip')
? element.data('vector-map-show-tooltip')
: true,
focusOnSelect: void 0 !== element.data('vector-map-focus-on-select')
? element.data('vector-map-focus-on-select')
: false,
backgroundColor: void 0 !== element.data('vector-map-background-color')
? element.data('vector-map-background-color')
: 'transparent',
values,
color: settings.colors.gray[50],
selectedColor: settings.colors.primary[300],
hoverColor: settings.colors.primary[100],
scaleColors: [ settings.colors.primary[50], settings.colors.primary[500] ],
borderWidth: 1,
borderColor: '#ffffff',
borderOpacity: 1,
normalizeFunction: 'polynomial',
colors: {},
pins,
pinMode: 'content',
onLabelShow: function (e, label, code) {
label.html(label.html() + ' - ' + values[ code ])
},
onRegionSelect: function(event, code, region) {
if (options.focusOnSelect) {
map.setFocus(code)
}
}
}
var colors = element.data('vector-map-values-colors')
if (colors) {
for (var key in values) {
if (values.hasOwnProperty(key) && colors[values[key]] !== undefined) {
const color = colors[values[key]]
options.colors[key] = settings.colors.get(color) || color
}
}
}
element.vectorMap(options)
const map = $(this).data().mapObject
if (Object.keys(options.colors)) {
map.setColors(options.colors)
}
const scale = element.data('vector-map-scale')
const focus = element.data('vector-map-focus')
if (scale) {
map.setScale(scale)
map.positionPins()
}
else if (focus) {
map.setFocus(focus)
}
})
$('[data-toggle=vector-map-focus]').on('click', function(e) {
e.preventDefault()
const element = $(this)
const target = $(element.data('target'))
if (!target) {
return
}
const map = target.data().mapObject
const focus = element.data('focus')
const animate = element.data('animate')
if (focus) {
map.setFocus(focus, animate)
}
})
const drawer = document.querySelector('.mdk-drawer')
if (drawer) {
drawer.addEventListener('mdk-drawer-change', () => requestAnimationFrame(resizeContainer))
}
})()
(function(){
'use strict';
Dropzone.autoDiscover = false
function mockFile(file, dzInstance, error) {
var name_pieces = file.split(/[\s\/]+/)
var name = name_pieces[name_pieces.length-1]
// Create the mock file:
var mockFile = {
name: name,
size: 12345,
accepted: true
}
// Call the default addedfile event handler
dzInstance.emit('addedfile', mockFile);
// And optionally show the thumbnail of the file:
dzInstance.emit('thumbnail', mockFile, file);
// Or if the file on your server is not yet in the right
// size, you can let Dropzone download and resize it
// callback and crossOrigin are optional.
// dzInstance.createThumbnailFromUrl(file, imageUrl, callback, crossOrigin);
dzInstance.createThumbnailFromUrl(mockFile, file);
// fake error / success
mockFile.status = Dropzone[(error ? 'ERROR' : 'SUCCESS')];
dzInstance.emit((error ? 'error' : 'success'), mockFile, (error ? 'The error message' : 'success'), null);
// Make sure that there is no progress bar, etc...
dzInstance.emit('complete', mockFile);
// If you use the maxFiles option, make sure you adjust it to the
// correct amount:
// var existingFileCount = 1; // The number of files already uploaded
// dzInstance.options.maxFiles = dzInstance.options.maxFiles - existingFileCount;
}
$('[data-toggle="dropzone"]').each(function() {
var element = $(this)
var template = element.find('.dz-preview')
var isMultiple = void 0 !== element.data('dropzone-multiple')
var currentFile = void 0
var options = {
url: element.data("dropzone-url"),
thumbnailWidth: null,
thumbnailHeight: null,
previewsContainer: template.get(0),
previewTemplate: template.html(),
maxFiles: isMultiple ? null : 1,
acceptedFiles: 'image/*',
clickable: void 0 !== element.data('dropzone-clickable')
? element.data('dropzone-clickable') : true,
init: function() {
this.on('addedfile', function(file) {
if (!isMultiple && currentFile) {
this.removeFile(currentFile)
}
currentFile = file
})
this.on('maxfilesexceeded', function(file) {
this.removeAllFiles()
this.addFile(file)
})
var filesOnServer = element.data('dropzone-files')
var dzInstance = this
filesOnServer.forEach(function(file, index) {
mockFile(file, dzInstance, index >= (filesOnServer.length - 1) / 2)
})
}
}
template.html('')
element.dropzone(options)
})
})()
(function(){
'use strict';
toastr.primary = function(message, title, optionsOverride) {
return this.success(message, title, $.extend({ iconClass: 'toast-primary' }, optionsOverride))
}
$('[data-toggle="toastr"]').on('click', function(e) {
e.preventDefault()
const element = $(this)
const type = element.data('toastr-type') || 'success'
const message = element.data('toastr-message')
const title = element.data('toastr-title')
const options = {
closeButton: void 0 !== element.data('toastr-close-button')
? element.data('toastr-close-button')
: false,
debug: false,
newestOnTop: void 0 !== element.data('toastr-newest-on-top')
? element.data('toastr-newest-on-top')
: true,
progressBar: void 0 !== element.data('toastr-progress-bar')
? element.data('toastr-progress-bar')
: true,
positionClass: void 0 !== element.data('toastr-position-class')
? element.data('toastr-position-class')
: "toast-top-right",
preventDuplicates: void 0 !== element.data('toastr-prevent-duplicates')
? element.data('toastr-prevent-duplicates')
: false,
onclick: null,
showDuration: void 0 !== element.data('toastr-show-duration')
? element.data('toastr-show-duration')
: 300,
hideDuration: void 0 !== element.data('toastr-hide-duration')
? element.data('toastr-hide-duration')
: 1000,
timeOut: void 0 !== element.data('toastr-time-out')
? element.data('toastr-time-out')
: 5000,
extendedTimeOut: void 0 !== element.data('toastr-extended-time-out')
? element.data('toastr-extended-time-out')
: 1000,
showEasing: void 0 !== element.data('toastr-show-easing')
? element.data('toastr-show-easing')
: 'swing',
hideEasing: void 0 !== element.data('toastr-hide-easing')
? element.data('toastr-hide-easing')
: 'linear',
showMethod: void 0 !== element.data('toastr-show-method')
? element.data('toastr-show-method')
: 'fadeIn',
hideMethod: void 0 !== element.data('toastr-hide-method')
? element.data('toastr-hide-method')
: 'fadeOut'
}
toastr[type](message, title, options)
})
})()
(function(){
'use strict';
$('[data-toggle="dragula"]').each(function() {
var element = $(this)
var options = {
moves: function(el, source, handle, sibling) {
return void 0 !== element.data('dragula-moves')
? handle.classList.contains(element.data('dragula-moves'))
: true
}
}
var containers = $(this).data('dragula-containers')
var elements = []
if (containers) {
for (var i = 0; i < containers.length; i++) {
elements.push(document.querySelector(containers[i]))
}
}
else {
elements.push(element[0])
}
dragula(elements, options)
})
})()
const extend = (object, value) => {
for (var key in value) "object" != typeof value[key] ? object[key] = value[key] : extend(object[key], value[key])
}
const add = (element) => {
var data = element.data("add")
var chart = $(element.data("target")).data("chart")
if (element.is(":checked")) {
function addRecursive(chart, data) {
for (var o in data) Array.isArray(data[o]) ? data[o].forEach(function(a) {
chart[o].push(a)
}) : addRecursive(chart[o], data[o])
}
addRecursive(chart, data)
}
else {
function removeRecursive(chart, data) {
for (var o in data) Array.isArray(data[o]) ? data[o].forEach(function(a) {
chart[o].pop()
}) : removeRecursive(chart[o], data[o])
}
removeRecursive(chart, data)
}
chart.update()
}
const update = (element) => {
var data = element.data("update")
var chart = $(element.data("target")).data("chart")
extend(chart, data)
if (void 0 !== element.data("prefix") || void 0 !== element.data("suffix")) {
let prefix = element.data("prefix") ? element.data("prefix") : ""
let suffix = element.data("suffix") ? element.data("suffix") : ""
if (chart.options.scales !== undefined) {
chart.options.scales.yAxes[0].ticks.callback = function(a) {
if (!(a % 10)) return prefix + a + suffix
}
}
chart.options.tooltips.callbacks.label = function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel || e.datasets[0].data[a.index],
r = "";
return 1 < e.datasets.length && (r += '' + t + ""), r += '' + prefix + o + suffix + ""
}
}
chart.update()
}
const globalSettings = {
responsive: true,
maintainAspectRatio: false,
defaultColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[700] : settings.colors.gray[600],
defaultFontColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[700] : settings.colors.gray[600],
defaultFontFamily: settings.fonts.base,
defaultFontSize: 13,
layout: {
padding: 0
},
legend: {
display: false,
position: "bottom",
labels: {
usePointStyle: true,
padding: 16
}
},
elements: {
point: {
radius: 0,
backgroundColor: settings.colors.primary[700]
},
line: {
tension: .4,
borderWidth: 3,
borderColor: settings.colors.primary[700],
backgroundColor: settings.colors.transparent,
borderCapStyle: "rounded"
},
rectangle: {
backgroundColor: settings.colors.primary[700]
},
arc: {
backgroundColor: settings.colors.primary[700],
borderColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[800] : settings.colors.white,
borderWidth: 4
}
},
tooltips: {
enabled: !1,
mode: "index",
intersect: !1,
custom: function(r) {
var a = $("#chart-tooltip");
if (a.length || (a = $('
} else a.css("display", "none")
},
callbacks: {
label: function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel,
r = "";
return 1 < e.datasets.length && (r += '' + t + ""), r += '' + o + ""
}
}
}
}
const doughnutSettings = {
cutoutPercentage: 83,
tooltips: {
callbacks: {
title: function(a, e) {
return e.labels[a[0].index]
},
label: function(a, e) {
var t = "";
return t += '' + e.datasets[0].data[a.index] + ""
}
}
},
legendCallback: function(a) {
var o = a.data,
r = "";
return o.labels.forEach(function(a, e) {
var t = o.datasets[0].backgroundColor[e];
r += '', r += '', r += a, r += ""
}), r
}
}
const create = (id, type = 'line', options = {}, data = {}) => {
var element = $(id)
var chart = new Chart(element, {
type: type,
options: options,
data: data
})
element.data("chart", chart)
if (element.data("chart-legend")) {
document.querySelector(element.data("chart-legend"))
.innerHTML = chart.generateLegend()
}
}
const init = () => {
extend(Chart, {
defaults: {
global: globalSettings,
doughnut: doughnutSettings
}
})
Chart.scaleService.updateScaleDefaults("linear", {
gridLines: {
borderDash: [2],
borderDashOffset: [2],
color: "dark" == settings.charts.colorScheme ? settings.colors.gray[900] : settings.colors.gray[100],
drawBorder: !1,
drawTicks: !1,
lineWidth: 0,
zeroLineWidth: 0,
zeroLineColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[900] : settings.colors.gray[100],
zeroLineBorderDash: [2],
zeroLineBorderDashOffset: [2]
},
ticks: {
beginAtZero: !0,
padding: 10,
callback: function(a) {
if (!(a % 10)) return a
}
}
})
Chart.scaleService.updateScaleDefaults("category", {
gridLines: {
drawBorder: !1,
drawOnChartArea: !1,
drawTicks: !1
},
ticks: {
padding: 20
},
maxBarThickness: 10
})
$('[data-toggle="chart"]').on({
change: function() {
var el = $(this)
if (el.is("[data-add]")) {
add(el)
}
},
click: function() {
var el = $(this)
if (el.is("[data-update]")) {
update(el)
}
}
})
}
const Charts = {
settings,
init,
add,
update,
create
}
if (window !== undefined) {
window.Charts = Charts
}
(function(){
'use strict';
$.fn.select2.defaults.set('theme', 'bootstrap4')
function templateResult(a) {
if (!a.id)
return a.text;
var e = $(a.element).data("avatar-src");
return e ? $('' + a.text + "") : a.text
}
$('[data-toggle="select"]').each(function() {
var element = $(this)
var options = {
dropdownParent: element.closest(".modal").length ? element.closest(".modal") : $(document.body),
minimumResultsForSearch: element.data("minimum-results-for-search"),
templateResult
}
element.select2(options)
})
})()
// SETTINGS
// APP SETTINGS
const hexToRGB = (hex, alpha) => {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
if (alpha) {
return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")"
} else {
return "rgb(" + r + ", " + g + ", " + b + ")"
}
}
const fonts = { base: "Helvetica Neue" }
// https://material.io/design/color/the-color-system.html#tools-for-picking-colors
const colors = {
bodyBg: '#F7F8F9',
border: '#efefef',
gray: {
50: '#edeef0',
100: '#d0d6dc',
200: '#b2bac4',
300: '#939fad',
400: '#7c8a9b',
500: '#65778a',
600: '#586879',
700: '#485563',
800: '#39424d',
900: '#272e36'
},
primary: {
50: '#e4f3ff',
100: '#bddfff',
200: '#93ccff',
300: '#68b8ff',
400: '#49a8ff',
500: '#3099ff',
600: '#308AF3', // $primary
700: '#2d79df',
800: '#2b67cd',
900: '#2648ad'
},
success: {
50: '#edf9e6',
100: '#d1f0c2',
200: '#b2e599',
300: '#91db6e',
400: '#75d34a',
500: '#58ca1f',
600: '#47ba16', // $success
700: '#2ba607',
800: '#009200',
900: '#006f00',
},
danger: {
50: '#feeaef',
100: '#fccad5',
200: '#ec949f',
300: '#e26979',
400: '#ee405a',
500: '#f52042',
600: '#e51240',
700: '#d3003a',
800: '#c60032',
900: '#b70026'
},
black: '#12263F',
white: '#FFFFFF',
transparent: 'transparent',
get(color) {
return color.split('.').reduce((o,i)=>o[i], this)
}
}
const charts = {
colorScheme: 'light',
colors: {
area: hexToRGB(colors.primary[100], 0.5)
}
}
const settings = {
fonts,
colors,
charts,
hexToRGB
}
if (typeof window !== 'undefined') {
window.settings = settings
}
// END SETTINGS
//** CORE
//= require popper.js/dist/umd/popper.js
//= require bootstrap/dist/js/bootstrap.min.js
//= require simplebar/dist/simplebar.min.js
//= require dom-factory/dist/dom-factory.js
//= require material-design-kit/dist/material-design-kit.js
//** PLUGIN SCRIPTS (NODE_MODULES)
//= require jquery-mask-plugin/dist/jquery.mask.min.js
//= require select2/dist/js/select2.min.js
//= require moment/min/moment.min.js
//= require moment-range/dist/moment-range.js
//= require chart.js/dist/Chart.min.js
//= require jqvmap/dist/jquery.vmap.min.js
//= require jqvmap/dist/maps/jquery.vmap.world.js
//= require stack/vector-maps/mall-map.js
//= require stack/vector-maps/gdp-data.js
//= require dropzone/dist/dropzone.js
//= require list.js/dist/list.min.js
//= require toastr/toastr.js
//= require dragula/dist/dragula.min.js
//= require jquery-ui-dist/jquery-ui.min.js
//= require quill/dist/quill.min.js
//=require daterangepicker/daterangepicker.js
//** APP SETTINGS
//= require stack/settings.js
//** PLUGIN WRAPPERS & INITS
//= require stack/plugins/chartjs-rounded-bar.js
//= require stack/plugins/charts.js
//= require stack/plugins/daterangepicker.js
//= require stack/plugins/dragula.js
//= require stack/plugins/dropzone.js
//= require stack/plugins/list.js
//= require stack/plugins/quill.js
//= require stack/plugins/select2.js
//= require stack/plugins/toastr.js
//= require stack/plugins/vector-maps.js
window['moment-range'].extendMoment(moment);
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function() {
$('#img-form-field').bind('change', function() { //updates the text path so the user sees the path
var fileName = $(this).val();
$("#img-dummy-field").html(fileName);
});
$("#img-dummy-field").bind('click', function() { //binds text path to opening the image explorer
$("#img-form-field").click();
});
$("#new_customer_form").on("ajax:success", function(event){
data = event.detail[0];
SetSelect2Option("customer-name-search", data["full_name"], data["id"]);
});
$("#new_location_form").on("ajax:success", function(event){
data = event.detail[0];
SetSelect2Option("customer-location-search", data["display_address"], data["id"]);
});
$('#new_customer_contact_form').on("ajax:success", function(event){
data = event.detail[0];
SetSelect2Option("customer-contact-search", data["full_name"], data["id"]);
});
// new QuerySelect('job_status_select',
// '/job_statuses.json',
// function(params) {return {q: {name_cont: params.term}}},
// function (data) {
// // Transforms the top-level key of the response object from 'items' to 'results'
// return { results: $.map(data, function (val) { return { "id": val.id, "text": val.status }})}
// },
// function (evt){
// const jobStatusId = evt.currentTarget.value;
// $('#job_status_id').val(jobStatusId);
// },
// {placeholder: "Select a Job Status"}
// );
});
//= require cable
//= require_self
//= require_tree .
this.App = {};
App.cable = ActionCable.createConsumer();
class @NewContactModalHelper
constructor: (customerFieldId) ->
@_customerFieldId = customerFieldId
setFormCustomerId: (customerIdToSaveContactTo) ->
$("##{@_customerFieldId}").val(customerIdToSaveContactTo); # Set the hidden customer_contact field FOR OUR CUSTOMER CONTACT so our controller can assign the right customer to any new locations we add
class @NewLocationModalHelper
constructor: (customerFieldId) ->
@_customerFieldId = customerFieldId
# @_submitButtonId = submitButtonId
setFormCustomerId: (customerIdToSaveContactTo) ->
$("##{@_customerFieldId}").val(customerIdToSaveContactTo); # Set the hidden customer_contact field FOR OUR CUSTOMER CONTACT so our controller can assign the right customer to any new locations we add
//1: Controller: Having two status codes: Accepted means updated.
//2: Client-Javascript Callbacks: Having two types of ajax success callbacks: after updated, and after new.
// If after updated
// Find & edit the existing row
// If after new
// Add new row to the table
// If after delete
// Delete the row from the table
//3: Javascript: Handling Modal State
// If 'New' Button clicked, modal state must be set to "new"
// If 'Edit' Button clicked, modal state must be set to "update"
/**
* This class is so used so a modal can double as both a submit and an edit modal state, originally designed
* for easy access when viewing a table for editing rows and/or adding new entries into the table.
*/
class SubmitAndEditModalState{
constructor(
resourceModalId, //Used to open and close the modal as necessary.
resourceFormId, //Id of form, necessary for setting up events for listening on it's ajax:success or ajax:error events.
submitFormButtonId, //Button on form that's used to submit, important for keeping track of whether it's a new or edit modal.
resource, //the resource that is being added/editing. ex: customer_contacts, locations, employee_jobs
editResourceButtonClass, //The button class that when clicked should show the and set the edit modal
newResourceButton, //The button id that when clicked should show the modal and set it to be a "new" modal
){
this.editButtonThatWasClicked = null;//Variable for storing state, important when editing a row on the page (so user can see without reloading) after an update
this.resourceModalId = resourceModalId;
this.resourceFormId = resourceFormId;
this.resource = resource;
this.submitFormButtonId = submitFormButtonId;
this.editResourceButtonClass = editResourceButtonClass;
this.newResourceButton = newResourceButton;
//Sets up a listener for all the resources if they are clicked on to edit, it toggles the modal to be opened
$(`.${editResourceButtonClass}`).click((event) => {
const resourceId = event.currentTarget.dataset.custom;
this.ToggleEditModal(resourceId);
});
$(`#${newResourceButton}`).click((event) => {
this.ToggleNewModal();
});
$(`#${resourceFormId}`).on("ajax:success", (event) => {
const data = event.detail[0];
var wasAnUpdate = event.detail[1] == "Accepted" || event.detail[2].status == 202; //Means was a PUT request to server. On production, event.detail[1] is always an empty string.
if (wasAnUpdate){
gritterSuccess("Successful Update", `Successfully updated ${resource}`)
this.resetModalFields();
if (this.editButtonThatWasClicked != null){
this.editResourceRowCallback(this.editButtonThatWasClicked,data)
}
else {
gritterError("Can't display changes", "Couldn't update the resource on this page, refresh to see the change")
}
}
else {
this.insertResourceRowToTableCallback(event);
}
$(`#${resourceModalId}`).modal('hide'); //Close modal regardless of it's an update, since it was successful.
});
$(`#${resourceFormId}`).on("ajax:error", (event) => {showErrorForm(event)});
}
/**
* Fetches the resource's data, and sets it up ready to be edited with a PUT request to the.
*/
ToggleEditModal(resourceId){ //Called after an or is clicked.
this.editButtonThatWasClicked = event.currentTarget;
const url = `/${this.resource}/${resourceId}.json`
// Make a get request for the resource info, then fill in all the modal fields.
$.get({
url: url,
success: (data) => {
// Need to set all of the fields with the correct info...
this.fillInFormWithDataCallback(data);
//Change button to PUT a url that will update the employee job & reload the job page.
$(`#${this.submitFormButtonId}`).attr('formaction', url);
$(`#${this.submitFormButtonId}`).attr('formmethod', 'put');
$(`#${this.resourceModalId}`).modal('show');
},
error: (data) => {showErrorForCalendar(data)},
});
this.callWhenEditModalToggled();
$(`#${this.resourceModalId}`).modal('show');
}
ToggleNewModal(){
this.resetModalFields();
$(`#${this.submitFormButtonId}`).removeAttr('formaction');
$(`#${this.submitFormButtonId}`).removeAttr('formmethod');
this.callWhenNewModalToggled();
$(`#${this.resourceModalId}`).modal('show');
}
/**
* Returns a edit table data that can be appended to a table row to trigger editing this resource
* @param {*} data
* @returns
*/
getEditButtonElement(data){
const $editButton = $(` `)
$editButton.find('a').click((event) => { //the click listener that opens the modal to edit this specific resource
const resourceId = event.currentTarget.dataset.custom;
this.ToggleEditModal(resourceId)//Set up the edit modal with this resource to be edited
});
return $editButton;
}
fillInFormWithDataCallback(data){};
insertResourceRowToTableCallback(event){};
editResourceRowCallback(editResourceButtonElement, data){};
resetModalFields(){};
callWhenNewModalToggled(){};
callWhenEditModalToggled(){};
}
class ContactNewUpdateModalState extends SubmitAndEditModalState{
constructor(...args){
super(...args);
}
fillInFormWithDataCallback(data){
$('#customer_contact_first_name').val(data['first_name'])
$('#customer_contact_last_name').val(data["last_name"])
$('#customer_contact_phone').val(formatPhoneNumber(data["phone"]))
$('#customer_contact_email').val(data["email"])
$('#customer_contact_notes').val(data["notes"])
$('#customer_contact_customer_id').val(data["customer_id"])
$('#customer_contact_business_id').val(data["business_id"])
};
insertResourceRowToTableCallback(event){
const data = event.detail[0]
var table = $("[id='Customer Contacts']");
const $addedContact = $(
`
${data["full_name"]}
${formatPhoneNumber(data["phone"])}
${data["email"]}
${data["notes"]}
`);
$addedContact.find('td:nth-last-of-type(2)').after(this.getEditButtonElement(data));
table.append($addedContact);
};
editResourceRowCallback(editResourceButtonElement, data){
editResourceButtonElement.parentElement.parentElement.children[0].innerHTML = data["full_name"];
editResourceButtonElement.parentElement.parentElement.children[1].innerHTML = formatPhoneNumber(data["phone"]);
editResourceButtonElement.parentElement.parentElement.children[2].innerHTML = data["email"];
editResourceButtonElement.parentElement.parentElement.children[3].innerHTML = data["notes"];
};
resetModalFields(){
var customerId = $('#customer_contact_customer_id').val(); //This order is important. We need to save this id before submit and reset it after, that way they can add multiple contacts from the job modal page.
$('#new_customer_contact_form')[0].reset();
$('#customer_contact_customer_id').val(customerId); //Need to set it after the form resets..
};
callWhenNewModalToggled(){};
callWhenEditModalToggled(){};
}
class LocationModalState extends SubmitAndEditModalState{
constructor(...args){
super(...args);
}
fillInFormWithDataCallback(data){//Overridden
$('#location_id').val(data["id"]);
$('#location_street_address').val(data["street_address"]);
$('#location_city').val(data["city"]);
$('#location_zip_code').val(data["zip_code"]);
$('#location_state').val(data["state"]);
$('#location_customer_id').val(data["customer_id"]);
$('#location_business_id').val(data["business_id"]);
}
insertResourceRowToTableCallback(event){//Overriden
const data = event.detail[0]
var locationsTable = $("[id='Customer Locations']");
const $addedLocationRow = $(
`
${data["display_address"]}
`);
$addedLocationRow.find('td:nth-last-of-type(2)').after(this.getEditButtonElement(data));
locationsTable.append($addedLocationRow);
}
editResourceRowCallback(editLocationButtonElement, data){//Overriden
editLocationButtonElement.parentElement.parentElement.children[0].innerHTML = data["display_address"];
}
resetModalFields(){//Overriden
var customerId = $('#location_customer_id').val(); //This order is important. We need to save this id before submit and reset it after, that way they can add multiple locations from the job modal page.
$("#new_location_form")[0].reset(); //https://www.formget.com/jquery-reset-form/
$('#location_customer_id').val(customerId); //Need to set it after the form resets..
}
callWhenNewModalToggled(){ //Don't set as default from here..
$('#set_as_default_checkbox').hide();
};
callWhenEditModalToggled(){ //Don't allow setting as default from here..
$('#set_as_default_checkbox').hide();
};
}
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function() {
$(".cancel-modal").bind("click", function(){
closeModal(doRefresh = false);
});
$(".cancel-with-confirmation").bind("click", function () {
closeJobModalWithConfirmation();
});
$('.cancel-with-autosave').bind("click", function (e) {
e.currentTarget.disabled = true;
if (modalAppointmentContext._isNewJob){
closeJobModalWithConfirmation();
} else {
$('#job_appointment_close').addClass('is-loading');
$('#submitnewjob').click();
}
});
$(".delete-current-job").bind("click", function () {
var userConfirmedExitingWithoutSaving = confirm('Are you sure you want to delete this appointment?');
if(userConfirmedExitingWithoutSaving == true){
deleteCurrentJobContext();
}
});
// When the user clicks anywhere outside of the modal, close it
window.onclick = function (event) {
if (event.target == document.getElementById("job-modal")) {
//Default to saving the job (better ui/ux)
// if (modalAppointmentContext._isNewJob){
closeJobModalWithConfirmation();
// } else {
// $('#submitnewjob').click();
// }
}
}
});
function closeJobModalWithConfirmation(){
var userConfirmedExitingWithoutSaving = confirm("Discard unsaved changes?");
$('.cancel-with-autosave').prop('disabled', false);
$('#job_appointment_close').removeClass('is-loading');
if(userConfirmedExitingWithoutSaving == true){
closeModal(doRefresh = true)
}
}
$('.modal').css('overflow-y', 'auto'); //https://stackoverflow.com/a/28077674
function drawMarketingPieChart(start, end) {
// Create the data table.
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: '/marketing_report.json',
data: {start: start, end: end},
success: function(marketingData){
var data = new google.visualization.DataTable();
data.addColumn('string', 'Marketing Source');
data.addColumn('number', 'New Customers');
data.addRows(marketingData);
// Set chart options
var options = {'title':'New Customer by Marketing Source',
'width':400,
'height':300};
// Instantiate and draw our chart, passing in some options.
var chart = new google.visualization.PieChart(document.getElementById('marketing_chart_div'));
chart.draw(data, options);
let numCustomers = marketingData.reduce(function(a, e) {return a + parseInt(e[1])}, 0);
$('#new_customers_amount').text(numCustomers)
}
})
}
function drawIncomeChart(start, end) {
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: "/income_report.json",
data: {start: start, end: end},
success: function(incomeData){
var data = google.visualization.arrayToDataTable(
incomeData,
);
var options = {
title: 'Company Performance',
curveType: 'function',
legend: { position: 'bottom' }
};
var chart = new google.visualization.LineChart(document.getElementById('income_chart_graph'));
chart.draw(data, options);
let totalEarnings = incomeData.slice(1).reduce(function(a, e) {return a + (isNaN(parseInt(e[1])) ? 0 : parseInt(e[1]))}, 0); //slice is to remove the header element ['Day', 'Earnings']
$('#total_income_amount').text(formatCurrency(totalEarnings)) //Returning 0.. why?
}
})
}
function drawIncomeByCategoryChart(start, end) {
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: "/income_category_report.json",
data: {start: start, end: end},
success: function(incomeData){
var data = google.visualization.arrayToDataTable(
incomeData,
);
var options = {
title : 'Income by Products/Services',
vAxis: {title: 'Income'},
hAxis: {title: 'Month'},
seriesType: 'bars',
series: {5: {type: 'line'}} //TODO: This number will be based on the number of products/services we're doing..
};
var chart = new google.visualization.ComboChart(document.getElementById('income_category_chart_graph'));
chart.draw(data, options);
// let totalEarnings = incomeData.slice(1).reduce(function(a, e) {return a + (isNaN(parseInt(e[1])) ? 0 : parseInt(e[1]))}, 0); //slice is to remove the header element ['Day', 'Earnings']
// $('#total_income_amount').text(formatCurrency(totalEarnings)) //Returning 0.. why?
}
})
}
function drawRevenueBySalesRepChart(start, end){
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get(
{url: '/revenue_by_sales_rep_report',
data: {start: start, end: end},
success: function(salesRepData){
var data = google.visualization.arrayToDataTable(salesRepData);
var options = {
chart: {
title: 'Company Performance',
subtitle: 'Sales, Estimates, by Customer Rep',
}
};
var chart = new google.charts.Bar(document.getElementById('revenue_by_sales_rep'));
chart.draw(data, google.charts.Bar.convertOptions(options));
}
})
}
function drawRevenueByMarketingSourceChart(start, end){
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: '/revenue_by_marketing_source.json',
data: {start: start, end: end},
success: function(marketIncomeData){
var data = google.visualization.arrayToDataTable(marketIncomeData);
var options = {
title: 'Company Performance',
curveType: 'function',
legend: { position: 'right' },
crosshair: {orientation: 'vertical', trigger: 'focus'},
// Allow multiple
// simultaneous selections.
selectionMode: 'multiple',
// Trigger tooltips
// on selections.
tooltip: {trigger: 'selection'},
// Group selections
// by x-value.
aggregationTarget: 'auto',
width: 900,
height: 500
};
var chart = new google.visualization.LineChart(document.getElementById('marketing_revenue_chart'));
chart.draw(data, options);
}
})
}
function drawProductionRevenueByEmployee(start, end){
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: '/production_revenue_by_employee.json',
data: {start: start, end: end},
success: function(productionRevenueData){
var data = google.visualization.arrayToDataTable(productionRevenueData);
var options = {
title: 'Production Revenue',
chartArea: {width: '50%'},
hAxis: {
title: 'Revenue',
minValue: 0
},
vAxis: {
title: 'Employee'
}
};
var chart = new google.visualization.BarChart(document.getElementById('production_revenue_by_employee'));
chart.draw(data, options);
}
})
}
function drawTimeClockedByEmployee(start, end){
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
$.get({
url: '/timeclocked_by_employee.json',
data: {start: start, end: end},
success: function(productionRevenueData){
var data = google.visualization.arrayToDataTable(productionRevenueData);
var options = {
title: `Time Sheets: ${formatReadableDate(start)} - ${formatReadableDate(end)}`,
chartArea: {width: '50%'},
hAxis: {
title: 'Time (In Hours)',
minValue: 0
},
vAxis: {
title: 'Employee'
}
};
var NumberFormat = new google.visualization.NumberFormat( //https://stackoverflow.com/questions/49766493/rounding-value-in-google-chart
//You can explore various type of patterns in Documentation
{pattern:'##.##'}
);
NumberFormat.format(data, 1); // Apply formatter to second column
var chart = new google.visualization.BarChart(document.getElementById('timeclocked_by_employee'));
chart.draw(data, options);
}
})
}
$(function(){
// Callback that creates and populates a data table,
// instantiates the pie chart, passes in the data and
// draws it.
// Load the Visualization API and the corechart package.
google.charts.load('current', {'packages':['corechart', 'bar', 'line']});
//TODO: Set the start and end date and generate the data
// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawMarketingPieChart);
google.charts.setOnLoadCallback(drawIncomeChart);
google.charts.setOnLoadCallback(drawIncomeByCategoryChart);
google.charts.setOnLoadCallback(drawRevenueBySalesRepChart);
google.charts.setOnLoadCallback(drawRevenueByMarketingSourceChart);
google.charts.setOnLoadCallback(drawProductionRevenueByEmployee);
google.charts.setOnLoadCallback(drawTimeClockedByEmployee);
})
/**
* @param {data} data sets the invoice type label next to the invoice display number at top of invoice
*/
// function updateInvoiceTypeLabel(data) {
// $('.invoice_type').text(data.target.value);
// if (data.target.value == "Invoice") {
// $('#invoice-due').show();
// $('.invoice-only-section').show();
// }
// else {
// $('.invoice-only-section').hide();
// $('#invoice-due').hide();
// }
// }
/** Invoice date and due date section */
// setupContentEditableDisplay('#invoice-date');
// setupContentEditableDisplay('#invoice-due-date');
// function setupContentEditableDisplay(datePickerId) {
// flatpickr(datePickerId, {
// altInput: true,
// altFormat: "F j, Y",
// });
// }
/**
* Autofilling invoice from existing invoice
*/
$('#template_invoice_template_id').on('change', function (e) {
$.get({
url: `../../invoice_templates/${$('#template_invoice_template_id').val()}.json`,
success: function (data) { //TODO: Here is where we autofill and do a gritter notification
if (data != null) {
$('#service-notes')[0].innerText = data['description']
if (data.line_item_template.length > 0){
clearAllLineItems();
for (var i = data["line_item_template"].length-1; i >= 0; i--) { //doing this backwards to put them in the right order..
addLineItem(data["line_item_template"][i]["name"], data["line_item_template"][i]["description"], data["line_item_template"][i]["price"], data["line_item_template"][i]["product_id"], data["line_item_template"][i]["quantity"]);
}
}
}
sumLineItemTotal();
},
error: function () { //TODO: Here is where we alert user of any server-side errors
console.log("There was a server-side error");
}
});
});
function clearAllLineItems() {
var paras = document.getElementsByClassName('line-item');
while (paras[0]) {
paras[0].parentNode.removeChild(paras[0]);
}
}
/** Line Items: */
function addLineItem(name = null, description = null, price = null, product_id = null, quantity = null) {
var name = name == null ? "" : name
var description = description == null ? "Enter Description Here.." : description;
var price = price == null ? "0.00" : price;
var product_id = product_id == null ? "" : product_id;
var quantity = quantity == null ? 1 : quantity
var $lineItemRow = $(" ");
appendElement($lineItemRow, $(` ${name} `))
let descriptionElement = $(` ${description} `);
descriptionElement.keypress(maxCharacters);
appendElement($lineItemRow, descriptionElement, function(e){ return e.which != 13}); //Disable new lines
appendElement($lineItemRow, $(` ${product_id} ")`), onlyIntegersAllowed);
appendElement($lineItemRow, $(` ${quantity} ")`), onlyIntegersAllowed); //Line Item Quantity hard coded for now
appendElement($lineItemRow, $(`$ ${parseFloat(price).toFixed(2)} `), onlyNumbersAllowed);
appendElement($lineItemRow, $(" "));
$lineItemRow.insertBefore($('#line-item-anchor-row'));
lineItemEventHandlers();
calibrateLineItemIds();
setupProductQuerySelectForEachLineItem();
sumLineItemTotal();
}
function appendElement(element, elementToAppend, keyPressCallback){
if (keyPressCallback != null)
elementToAppend.keypress(keyPressCallback);
element.append(elementToAppend);
}
maxCharacters = function (evt) {
if(evt.target.textContent.length > 750)
{
evt.preventDefault();
}
}
tabOrBackspaceWasTyped = function(evt){
return (evt.which == 8 || evt.which == 9 || evt.which == 46)
}
onlyIntegersAllowed = function (evt) {
if (!tabOrBackspaceWasTyped(evt) && (evt.which < 48 || evt.which > 57 && evt.which < 96 || evt.which > 105))
evt.preventDefault();
}
onlyNumbersAllowed = function(evt) {
//left or right arrow key should be allowed.
//negative sign should be allowed.
const wasLeftOrRightArrow = evt.which == 39 || evt.which == 37;
const wasTabOrBackspace = tabOrBackspaceWasTyped(evt);
const decimalLimitReached = evt.currentTarget.innerText.includes(".");
const negativeSignLimitReached = evt.currentTarget.innerText.includes("-");
const wasNegativeSign = evt.which == 45 || evt.which == 189;
const wasDecimal = evt.which == 46 || evt.which == 190 || evt.which == 110;
const wasntNumber = (evt.which < 48 || evt.which > 57)
const wasRightNumpad = (evt.which >= 96 && evt.which <= 105)
if (wasDecimal && decimalLimitReached){ //They should be able to add a decimal if it's the only one.
evt.preventDefault();
}
if (wasNegativeSign && negativeSignLimitReached)
{
evt.preventDefault();
}
if (!wasDecimal && !wasNegativeSign && !wasTabOrBackspace && wasntNumber && !wasRightNumpad && !wasLeftOrRightArrow){
evt.preventDefault();
}
}
/**
* Deletes the associated line item that was clicked on
* @param {*} event click event
*/
// function deleteLineItem(event) {
// $('#' + event.target.parentElement.parentElement.parentElement.id).remove();
// sumLineItemTotal();
// calibrateLineItemIds();
// }
//These correspond to the positioning on the page of each column in a line item.
//Now I can just update this any time the ordering changes or we add antoher column
// lineItemAttributeIndices = {
// "name":0,
// "description":1,
// "product_id":2,
// "quantity":3,
// "price":4,
// "delete":5,
// }
/**
* Goes through and reassigns all the line item id's according to their order on the html page
*/
// function calibrateLineItemIds() {
// $('.line-item').each(function (i, obj) {
// obj.id = 'line-item-' + i;
// obj.children[lineItemAttributeIndices["name"]].children[0].id = 'product-line-item-select-' + i;
// obj.children[lineItemAttributeIndices["name"]].children[0]["data-position"] = i;
// obj.children[lineItemAttributeIndices["name"]].id = 'line-item-name-' + i;
// obj.children[lineItemAttributeIndices["description"]].id = 'line-item-description-' + i;
// obj.children[lineItemAttributeIndices["product_id"]].id = 'line-item-product-id-' + i;
// obj.children[lineItemAttributeIndices["quantity"]].id = 'line-item-quantity-' + i;
// obj.children[lineItemAttributeIndices["price"]].children[0].children[0].id = 'line-item-price-' + i;
// obj.children[lineItemAttributeIndices["delete"]].id = 'delete-line-item-' + i;
// });
// }
// function setupProductQuerySelectForEachLineItem(){
// $('.line-item').each(function (i, obj) {
// new QuerySelect(`product-line-item-select-${i}`,
// '/products.json',
// RansackSearch('name_cont'),
// function (data) {
// //https://select2.org/data-sources/ajax
// return { results: $.map(data, function(val) { return {"id": val["id"], "text": val["name"], "data-description": val["description"], "data-sku": val["SKU"], "data-price": val["price"]}})};
// });
// $(`#product-line-item-select-${i}`).on('change', function (evt) {
// data=$(`#product-line-item-select-${i}`).select2('data')[0]; //https://stackoverflow.com/questions/33940477/adding-data-attributes-with-select2
// $(`#line-item-description-${evt.currentTarget["data-position"]}`).text(data["data-description"]);
// $(`#line-item-product-id-${evt.currentTarget["data-position"]}`).text(data["id"])
// let lineItemPriceElement = $(`#line-item-price-${evt.currentTarget["data-position"]}`);
// let hasPriceBeenChangedByUser = lineItemPriceElement.text().trim() != "0.00" && lineItemPriceElement.text().trim() !== ""
// if (!hasPriceBeenChangedByUser) {// if the price has been manually changed by user, we don't want to overwrite it here, otherwise they will have to re-enter it.
// let nonNanPrice = parseFloat(data["data-price"] == null ? 0 : data["data-price"]);//We don't want an NaN in our system.
// $(`#line-item-price-${evt.currentTarget["data-position"]}`).text(nonNanPrice.toFixed(2));
// }
// });
// });
// }
// function lineItemEventHandlers() {
// $(".line-item-price").keyup(function (e) { // Only numbers can be entered in line item prices
// sumLineItemTotal();
// });
// $('.line-item-quantity').keyup(function(e){
// sumLineItemTotal();
// });
// }
// function sumLineItemTotal() {
// //Sum all the line items and put it into the total for the invoice:
// total = 0.0
// $('.line-item-price').each(function (i, obj) {
// thisLineItemPrice = parseFloat(obj.textContent.trim());
// thisLineItemQuantity = parseInt($('.line-item-quantity')[i].textContent.trim())
// if (!thisLineItemPrice) {
// thisLineItemPrice = 0;
// }
// var toAdd = convertNaNToZero(thisLineItemPrice * thisLineItemQuantity)
// total += toAdd;
// });
// var formatter = new Intl.NumberFormat('en-US', {
// style: 'currency',
// currency: 'USD',
// });
// $('#line-item-total').text(formatter.format(total));
// }
// function convertNaNToZero(a){ //https://stackoverflow.com/a/7540412/6374022
// a = a || 0;
// return a;
// }
// lineItemEventHandlers();
/** Generate Invoice Logic */
// function generateInvoice(basePath, generatingNewInvoiceTemplate = false, send = false) {
// const lineItems = generateLineItemsArray();
// invoiceData = null;
// if (generatingNewInvoiceTemplate) { //TODO: Pretty bad to add logic variable like this. Should probably use polymorphism somehow.
// invoiceData = {
// invoice_template: {
// name: $('#template_name').text(),
// description: $('#service-notes')[0] && $('#service-notes')[0].innerText,
// line_items: lineItems
// }
// };
// $.redirect(`/${basePath}`, invoiceData);
// //Does a POST to the basePath route with invoiceData
// }
// else {
// const totalfiles = $('#attach_images').prop("files").length;
// const formData = new FormData();
// for (let index = 0; index < totalfiles; index++) {
// formData.append("images[]", $('#attach_images').prop("files")[index]);
// }
// const invoice = {
// 'job_id': $('#job-id')[0].textContent.trim(),
// 'invoice_type': $('#type').find(":selected").text().trim(),
// 'invoice_display_number': $('#invoice-display-number')[0].textContent.trim(),
// 'issued_on': $('#invoice-date').val(),
// 'due_on': $('#invoice-due-date').val(),
// 'notes': $('#service-notes')[0].innerText,
// 'invoice_to_id': isBlank($('#select_different_customer_to_invoice').val()) ? '' : $('#select_different_customer_to_invoice').val(),
// 'line_items': lineItems,
// }
// formData.append('invoice', JSON.stringify(invoice))
// $.ajax({
// url: `/${basePath}`,
// type: "POST",
// dataType: 'json',
// data: formData,
// processData: false,
// contentType: false,
// success: function(data){
// console.log("Now being redirected to the created invoice");
// gritterSuccess("Success", "Invoice added Successfully");
// window.location.href = `/${basePath}/${data.id}`;
// },
// error: function(data){
// console.log("Error Ocurred");
// gritterError(`Error`, "Something went wrong");
// }
// });
// }
// }
// function updateInvoice(basePath, invoiceId, updatingInvoiceTemplate = false) {
// lineItems = generateLineItemsArray();
// invoiceData = null;
// if (updatingInvoiceTemplate) { //TODO: Pretty bad to add logic variable like this. Should probably use polymorphism somehow.
// invoiceData = {
// invoice_template: {
// name: $('#template_name').text(),
// description: $('#service-notes')[0] && $('#service-notes')[0].innerText,
// line_items: lineItems
// }
// };
// }
// else{
// invoiceData = {
// invoice: {
// id: invoiceId,
// job_id: $('#job-id')[0].textContent.trim(),
// invoice_type: $('#type').find(":selected").text().trim(), //TODO: This is a magic string for the invoices database table
// invoice_display_number: $('#invoice-display-number')[0].textContent.trim(),
// issued_on: $('#invoice-date').val(),
// due_on: $('#invoice-due-date').val(), //.val() is returning empty string for some reason
// notes: $('#service-notes')[0].innerText, //it seems $.ajax will encode these new lines with \n instead of \r\n
// // private_notes: GetQuillHtml('private-notes'),
// invoice_to_id: $('#select_different_customer_to_invoice').val(), //TODO:
// line_items: lineItems
// }
// };
// }
// $.ajax({
// url: `/${basePath}/${invoiceId}.json`,
// data: invoiceData,
// type: 'PUT',
// success: function (data) {
// console.log("Now being redirected to the updated invoice");
// window.location.href = `/${basePath}/${invoiceId}`; //Redirect to the edited invoice
// }
// });
// }
// function generateLineItemsArray(){
// lineItems = []
// $('.line-item').each(function (itemIndex) {
// name = $(`#product-line-item-select-${itemIndex}`).find(':selected').text();
// description = $('.line-item')[itemIndex].children[lineItemAttributeIndices["description"]].innerText;
// price = $('.line-item')[itemIndex].children[lineItemAttributeIndices["price"]].innerText.replace('$', '').trim();
// product_id = $(`#product-line-item-select-${itemIndex}`).val();
// quantity = $('.line-item')[itemIndex].children[lineItemAttributeIndices["quantity"]].innerText;
// lineItems[itemIndex] = { name: name, description: description, price: price, product_id: product_id, quantity: quantity}
// });
// return lineItems;
// }
function emailInvoiceToCustomer(invoiceId) {
$('.loader').show();
$.post({
url: `../../invoices/${invoiceId}/deliver_invoice`,
data: { id: invoiceId },
success: function () {
gritterSuccess("Successfully Sent Invoice to Customer",
`You've successfully sent invoice ${invoiceId} to the customer`);
$('.loader').hide();
},
error: function (data) {
$('.loader').hide();
var needToAuthenticateGmail = data.status == 511
if (needToAuthenticateGmail) {
gritterError("Need to authenticate Gmail", "Redirecting you to authenticate gmail", "10000");
var uri = data.responseJSON["response"]["google_redirect_uri"];
console.log(`Redirecting to ${uri}`);
window.location = uri;
}
else {
gritterError("Couldn't send email", "Does the customer have a valid e-mail address? Check the email and try again", "10000");
}
}
});
}
$(document).ready(function () {
String.prototype.isEmpty = function() { // https://stackoverflow.com/a/3261380
return (this.length === 0 || !this.trim());
};
$('#type').on('change', function() { //https://stackoverflow.com/questions/11179406/jquery-get-value-of-select-onchange
$('.invoice_type').innerHTML = this.value;
if (this.value == "Invoice" || this.value == "Estimate"){
$('#invoice-due-date-section').show();
}
else {
$('#invoice-due-date-section').hide();
}
if (this.value == "Invoice")//TODO: This is pretty bad that it's hardcoded, when we should be using the invoice_helper def invoice_due_text(invoice)
{
$('.invoice_type_due_label').text("Due On:");
}
else if (this.value == "Estimate"){
$('.invoice_type_due_label').text("Expires:");
}
$('.invoice-display-numberc').text(calculateDisplayNumber($('#job-id')[0].textContent.trim(), this.value, $('#job-invoice-index').text())); //How do we know the job number?
if (this.value == "Invoice" || this.value == "Change Order"){
$('#invoice-amount-due-label').text("Due:");
$('#invoice-amount-due-label').css("color", "darkred");
}
else {
$('#invoice-amount-due-label').text("Total:");
$('#invoice-amount-due-label').css("color", "");
}
});
});
function refundIbxPayment(invoiceId){
$.get({
url: `/invoices/${invoiceId}/amount_that_will_be_refunded_ibx`,
success: function(data){
let userAgreedToProcessRefund = confirm(`This will refund ${formatCurrency(data["amount"])}, are you sure you want to refund this full amount?`);
if (userAgreedToProcessRefund){
$('.loader').show();
$.post({
url: `/invoices/${invoiceId}/refund_ibx`,
success: function(data){
gritterSuccess("Successfully Refunded Card Payment", "Successfully Refunded Card Payment.", "10000");
$('.loader').hide();
location.reload();
return false; //https://stackoverflow.com/a/3715053/6374022
},
error: function(data){
$('.loader').hide();
gritterError("Couldn't refund this card payment", "Couldn't refund this card payment", "10000");
}
})
}
}
})
}
function markAsPaid(invoiceId){
$('.loader').show();
$.post({
url: `/invoices/${invoiceId}/mark_as_paid`,
success: function(data){
gritterSuccess("Successfully Marked as Paid", "Successfully marked as paid.", "10000");
$('.loader').hide();
location.reload();
return false; //https://stackoverflow.com/a/3715053/6374022
},
error: function(data){
$('.loader').hide();
gritterError("Couldn't mark as paid", "Couldn't mark this document as paid", "10000");
}
});
}
function openOptionsDropdown(){
console.log("Open options dropdown clicked");
document.getElementById("dropdown-menu").classList.toggle("show");
}
// Close the dropdown menu if the user clicks outside of it
window.onclick = function(event) {
if (!event.target.matches('.dropbtn')) {
var dropdowns = document.getElementsByClassName("dropdown-menu");;
for ( let i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}
Error reading file: U_STRING_NOT_TERMINATED_WARNING
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
/*!
* baguetteBox.js
* @author feimosi
* @version 1.11.0
* @url https://github.com/feimosi/baguetteBox.js
*/
/* global define, module */
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.baguetteBox = factory();
}
}(this, function () {
'use strict';
// SVG shapes used on the buttons
var leftArrow = '' +
'' +
'',
rightArrow = '' +
'' +
'',
closeX = '' +
'' +
'' +
'' +
'';
// Global options and their defaults
var options = {},
defaults = {
captions: true,
buttons: 'auto',
fullScreen: false,
noScrollbars: false,
bodyClass: 'baguetteBox-open',
titleTag: false,
async: false,
preload: 2,
animation: 'slideIn',
afterShow: null,
afterHide: null,
onChange: null,
overlayBackgroundColor: 'rgba(0,0,0,.8)'
};
// Object containing information about features compatibility
var supports = {};
// DOM Elements references
var overlay, slider, previousButton, nextButton, closeButton;
// An array with all images in the current gallery
var currentGallery = [];
// Current image index inside the slider
var currentIndex = 0;
// Visibility of the overlay
var isOverlayVisible = false;
// Touch event start position (for slide gesture)
var touch = {};
// If set to true ignore touch events because animation was already fired
var touchFlag = false;
// Regex pattern to match image files
var regex = /.+\.(gif|jpe?g|png|webp)/i;
// Object of all used galleries
var data = {};
// Array containing temporary images DOM elements
var imagesElements = [];
// The last focused element before opening the overlay
var documentLastFocus = null;
var overlayClickHandler = function(event) {
// Close the overlay when user clicks directly on the background
if (event.target.id.indexOf('baguette-img') !== -1) {
hideOverlay();
}
};
var previousButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
showPreviousImage();
};
var nextButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
showNextImage();
};
var closeButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
hideOverlay();
};
var touchstartHandler = function(event) {
touch.count++;
if (touch.count > 1) {
touch.multitouch = true;
}
// Save x and y axis position
touch.startX = event.changedTouches[0].pageX;
touch.startY = event.changedTouches[0].pageY;
};
var touchmoveHandler = function(event) {
// If action was already triggered or multitouch return
if (touchFlag || touch.multitouch) {
return;
}
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
var touchEvent = event.touches[0] || event.changedTouches[0];
// Move at least 40 pixels to trigger the action
if (touchEvent.pageX - touch.startX > 40) {
touchFlag = true;
showPreviousImage();
} else if (touchEvent.pageX - touch.startX < -40) {
touchFlag = true;
showNextImage();
// Move 100 pixels up to close the overlay
} else if (touch.startY - touchEvent.pageY > 100) {
hideOverlay();
}
};
var touchendHandler = function() {
touch.count--;
if (touch.count <= 0) {
touch.multitouch = false;
}
touchFlag = false;
};
var contextmenuHandler = function() {
touchendHandler();
};
var trapFocusInsideOverlay = function(event) {
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
event.stopPropagation();
initFocus();
}
};
// forEach polyfill for IE8
// http://stackoverflow.com/a/14827443/1077846
/* eslint-disable */
if (![].forEach) {
Array.prototype.forEach = function(callback, thisArg) {
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
// filter polyfill for IE8
// https://gist.github.com/eliperelman/1031656
if (![].filter) {
Array.prototype.filter = function(a, b, c, d, e) {
c = this;
d = [];
for (e = 0; e < c.length; e++)
a.call(b, c[e], e, c) && d.push(c[e]);
return d;
};
}
/* eslint-enable */
// Script entry point
function run(selector, userOptions) {
// Fill supports object
supports.transforms = testTransformsSupport();
supports.svg = testSvgSupport();
supports.passiveEvents = testPassiveEventsSupport();
buildOverlay();
removeFromCache(selector);
return bindImageClickListeners(selector, userOptions);
}
function bindImageClickListeners(selector, userOptions) {
// For each gallery bind a click event to every image inside it
var galleryNodeList = document.querySelectorAll(selector);
var selectorData = {
galleries: [],
nodeList: galleryNodeList
};
data[selector] = selectorData;
[].forEach.call(galleryNodeList, function(galleryElement) {
if (userOptions && userOptions.filter) {
regex = userOptions.filter;
}
// Get nodes from gallery elements or single-element galleries
var tagsNodeList = [];
if (galleryElement.tagName === 'A') {
tagsNodeList = [galleryElement];
} else {
tagsNodeList = galleryElement.getElementsByTagName('a');
}
// Filter 'a' elements from those not linking to images
tagsNodeList = [].filter.call(tagsNodeList, function(element) {
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
//Check if the className is 'lightbox' before testing if the file ends in jpeg, png, etc.
return element.className == "lightbox" || regex.test(element.href); //this filter is causing it to not display the images...??
// This way, if a photo is saved but the 'lightbox' class is added to it, we still know it's a photo and we can display it in the photo modal.
}
});
if (tagsNodeList.length === 0) {
return;
}
var gallery = [];
[].forEach.call(tagsNodeList, function(imageElement, imageIndex) {
var imageElementClickHandler = function(event) {
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
prepareOverlay(gallery, userOptions);
showOverlay(imageIndex);
};
var imageItem = {
eventHandler: imageElementClickHandler,
imageElement: imageElement
};
bind(imageElement, 'click', imageElementClickHandler);
gallery.push(imageItem);
console.log('binding image click listener for ' + imageElement);
});
selectorData.galleries.push(gallery);
});
return selectorData.galleries;
}
function clearCachedData() {
for (var selector in data) {
if (data.hasOwnProperty(selector)) {
removeFromCache(selector);
}
}
}
function removeFromCache(selector) {
if (!data.hasOwnProperty(selector)) {
return;
}
var galleries = data[selector].galleries;
[].forEach.call(galleries, function(gallery) {
[].forEach.call(gallery, function(imageItem) {
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
});
if (currentGallery === gallery) {
currentGallery = [];
}
});
delete data[selector];
}
function buildOverlay() {
overlay = getByID('baguetteBox-overlay');
// Check if the overlay already exists
if (overlay) {
slider = getByID('baguetteBox-slider');
previousButton = getByID('previous-button');
nextButton = getByID('next-button');
closeButton = getByID('close-button');
return;
}
// Create overlay element
overlay = create('div');
overlay.setAttribute('role', 'dialog');
overlay.id = 'baguetteBox-overlay';
document.getElementsByTagName('body')[0].appendChild(overlay);
// Create gallery slider element
slider = create('div');
slider.id = 'baguetteBox-slider';
overlay.appendChild(slider);
// Create all necessary buttons
previousButton = create('button');
previousButton.setAttribute('type', 'button');
previousButton.id = 'previous-button';
previousButton.setAttribute('aria-label', 'Previous');
previousButton.innerHTML = supports.svg ? leftArrow : '<';
overlay.appendChild(previousButton);
nextButton = create('button');
nextButton.setAttribute('type', 'button');
nextButton.id = 'next-button';
nextButton.setAttribute('aria-label', 'Next');
nextButton.innerHTML = supports.svg ? rightArrow : '>';
overlay.appendChild(nextButton);
closeButton = create('button');
closeButton.setAttribute('type', 'button');
closeButton.id = 'close-button';
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = supports.svg ? closeX : '×';
overlay.appendChild(closeButton);
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
bindEvents();
}
function keyDownHandler(event) {
switch (event.keyCode) {
case 37: // Left arrow
showPreviousImage();
break;
case 39: // Right arrow
showNextImage();
break;
case 27: // Esc
hideOverlay();
break;
case 36: // Home
showFirstImage(event);
break;
case 35: // End
showLastImage(event);
break;
}
}
function bindEvents() {
var options = supports.passiveEvents ? { passive: true } : null;
bind(overlay, 'click', overlayClickHandler);
bind(previousButton, 'click', previousButtonClickHandler);
bind(nextButton, 'click', nextButtonClickHandler);
bind(closeButton, 'click', closeButtonClickHandler);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, options);
bind(overlay, 'touchmove', touchmoveHandler, options);
bind(overlay, 'touchend', touchendHandler);
bind(document, 'focus', trapFocusInsideOverlay, true);
}
function unbindEvents() {
var options = supports.passiveEvents ? { passive: true } : null;
unbind(overlay, 'click', overlayClickHandler);
unbind(previousButton, 'click', previousButtonClickHandler);
unbind(nextButton, 'click', nextButtonClickHandler);
unbind(closeButton, 'click', closeButtonClickHandler);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, options);
unbind(overlay, 'touchmove', touchmoveHandler, options);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
}
function prepareOverlay(gallery, userOptions) {
// If the same gallery is being opened prevent from loading it once again
if (currentGallery === gallery) {
return;
}
currentGallery = gallery;
// Update gallery specific options
setOptions(userOptions);
// Empty slider of previous contents (more effective than .innerHTML = "")
while (slider.firstChild) {
slider.removeChild(slider.firstChild);
}
imagesElements.length = 0;
var imagesFiguresIds = [];
var imagesCaptionsIds = [];
// Prepare and append images containers and populate figure and captions IDs arrays
for (var i = 0, fullImage; i < gallery.length; i++) {
fullImage = create('div');
fullImage.className = 'full-image';
fullImage.id = 'baguette-img-' + i;
imagesElements.push(fullImage);
imagesFiguresIds.push('baguetteBox-figure-' + i);
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
slider.appendChild(imagesElements[i]);
}
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
}
function setOptions(newOptions) {
if (!newOptions) {
newOptions = {};
}
// Fill options object
for (var item in defaults) {
options[item] = defaults[item];
if (typeof newOptions[item] !== 'undefined') {
options[item] = newOptions[item];
}
}
/* Apply new options */
// Change transition for proper animation
slider.style.transition = slider.style.webkitTransition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
options.animation === 'slideIn' ? '' : 'none');
// Hide buttons if necessary
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
options.buttons = false;
}
// Set buttons style to hide or display them
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
// Set overlay color
try {
overlay.style.backgroundColor = options.overlayBackgroundColor;
} catch (e) {
// Silence the error and continue
}
}
function showOverlay(chosenImageIndex) {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'hidden';
document.body.style.overflowY = 'scroll';
}
if (overlay.style.display === 'block') {
return;
}
bind(document, 'keydown', keyDownHandler);
currentIndex = chosenImageIndex;
touch = {
count: 0,
startX: null,
startY: null
};
loadImage(currentIndex, function() {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
overlay.style.display = 'block';
if (options.fullScreen) {
enterFullScreen();
}
// Fade in overlay
setTimeout(function() {
overlay.className = 'visible';
if (options.bodyClass && document.body.classList) {
document.body.classList.add(options.bodyClass);
}
if (options.afterShow) {
options.afterShow();
}
}, 50);
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
documentLastFocus = document.activeElement;
initFocus();
isOverlayVisible = true;
}
function initFocus() {
if (options.buttons) {
previousButton.focus();
} else {
closeButton.focus();
}
}
function enterFullScreen() {
if (overlay.requestFullscreen) {
overlay.requestFullscreen();
} else if (overlay.webkitRequestFullscreen) {
overlay.webkitRequestFullscreen();
} else if (overlay.mozRequestFullScreen) {
overlay.mozRequestFullScreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function hideOverlay() {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'auto';
document.body.style.overflowY = 'auto';
}
if (overlay.style.display === 'none') {
return;
}
unbind(document, 'keydown', keyDownHandler);
// Fade out and hide the overlay
overlay.className = '';
setTimeout(function() {
overlay.style.display = 'none';
if (document.fullscreen) {
exitFullscreen();
}
if (options.bodyClass && document.body.classList) {
document.body.classList.remove(options.bodyClass);
}
if (options.afterHide) {
options.afterHide();
}
documentLastFocus && documentLastFocus.focus();
isOverlayVisible = false;
}, 500);
}
function loadImage(index, callback) {
var imageContainer = imagesElements[index];
var galleryItem = currentGallery[index];
// Return if the index exceeds prepared images in the overlay
// or if the current gallery has been changed / closed
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
return;
}
// If image is already loaded run callback and return
if (imageContainer.getElementsByTagName('img')[0]) {
if (callback) {
callback();
}
return;
}
// Get element reference, optional caption and source path
var imageElement = galleryItem.imageElement;
var thumbnailElement = imageElement.getElementsByTagName('img')[0];
var imageCaption = typeof options.captions === 'function' ?
options.captions.call(currentGallery, imageElement) :
imageElement.getAttribute('data-caption') || imageElement.title;
var imageSrc = getImageSrc(imageElement);
// Prepare figure element
var figure = create('figure');
figure.id = 'baguetteBox-figure-' + index;
figure.innerHTML = '
// Prepare gallery img element
var image = create('img');
image.onload = function() {
// Remove loader element
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
figure.removeChild(spinner);
if (!options.async && callback) {
callback();
}
};
image.setAttribute('src', imageSrc);
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption) {
image.title = imageCaption;
}
figure.appendChild(image);
// Run callback
if (options.async && callback) {
callback();
}
}
// Get image source location, mostly used for responsive images
function getImageSrc(image) {
// Set default image path from href
var result = image.href;
// If dataset is supported find the most suitable image
if (image.dataset) {
var srcs = [];
// Get all possible image versions depending on the resolution
for (var item in image.dataset) {
if (item.substring(0, 3) === 'at-' && !isNaN(item.substring(3))) {
srcs[item.replace('at-', '')] = image.dataset[item];
}
}
// Sort resolutions ascending
var keys = Object.keys(srcs).sort(function(a, b) {
return parseInt(a, 10) < parseInt(b, 10) ? -1 : 1;
});
// Get real screen resolution
var width = window.innerWidth * window.devicePixelRatio;
// Find the first image bigger than or equal to the current width
var i = 0;
while (i < keys.length - 1 && keys[i] < width) {
i++;
}
result = srcs[keys[i]] || result;
}
return result;
}
// Return false at the right end of the gallery
function showNextImage() {
return show(currentIndex + 1);
}
// Return false at the left end of the gallery
function showPreviousImage() {
return show(currentIndex - 1);
}
// Return false at the left end of the gallery
function showFirstImage(event) {
if (event) {
event.preventDefault();
}
return show(0);
}
// Return false at the right end of the gallery
function showLastImage(event) {
if (event) {
event.preventDefault();
}
return show(currentGallery.length - 1);
}
/**
* Move the gallery to a specific index
* @param `index` {number} - the position of the image
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
* @return {boolean} - true on success or false if the index is invalid
*/
function show(index, gallery) {
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
prepareOverlay(gallery, options);
showOverlay(index);
return true;
}
if (index < 0) {
if (options.animation) {
bounceAnimation('left');
}
return false;
}
if (index >= imagesElements.length) {
if (options.animation) {
bounceAnimation('right');
}
return false;
}
currentIndex = index;
loadImage(currentIndex, function() {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
return true;
}
/**
* Triggers the bounce animation
* @param {('left'|'right')} direction - Direction of the movement
*/
function bounceAnimation(direction) {
slider.className = 'bounce-from-' + direction;
setTimeout(function() {
slider.className = '';
}, 400);
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
setTimeout(function() {
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
slider.style.opacity = 1;
}, 400);
} else {
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
}
}
// CSS 3D Transforms test
function testTransformsSupport() {
var div = create('div');
return typeof div.style.perspective !== 'undefined' || typeof div.style.webkitPerspective !== 'undefined';
}
// Inline SVG test
function testSvgSupport() {
var div = create('div');
div.innerHTML = '';
return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
}
// Borrowed from https://github.com/seiyria/bootstrap-slider/pull/680/files
/* eslint-disable getter-return */
function testPassiveEventsSupport() {
var passiveEvents = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
passiveEvents = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) { /* Silence the error and continue */ }
return passiveEvents;
}
/* eslint-enable getter-return */
function preloadNext(index) {
if (index - currentIndex >= options.preload) {
return;
}
loadImage(index + 1, function() {
preloadNext(index + 1);
});
}
function preloadPrev(index) {
if (currentIndex - index >= options.preload) {
return;
}
loadImage(index - 1, function() {
preloadPrev(index - 1);
});
}
function bind(element, event, callback, options) {
if (element.addEventListener) {
element.addEventListener(event, callback, options);
} else {
// IE8 fallback
element.attachEvent('on' + event, function(event) {
// `event` and `event.target` are not provided in IE8
event = event || window.event;
event.target = event.target || event.srcElement;
callback(event);
});
}
}
function unbind(element, event, callback, options) {
if (element.removeEventListener) {
element.removeEventListener(event, callback, options);
} else {
// IE8 fallback
element.detachEvent('on' + event, callback);
}
}
function getByID(id) {
return document.getElementById(id);
}
function create(element) {
return document.createElement(element);
}
function destroyPlugin() {
unbindEvents();
clearCachedData();
unbind(document, 'keydown', keyDownHandler);
document.getElementsByTagName('body')[0].removeChild(document.getElementById('baguetteBox-overlay'));
data = {};
currentGallery = [];
currentIndex = 0;
}
return {
run: run,
show: show,
showNext: showNextImage,
showPrevious: showPreviousImage,
hide: hideOverlay,
destroy: destroyPlugin
};
}));
/*
jQuery Redirect v1.1.3
Copyright (c) 2013-2018 Miguel Galante
Copyright (c) 2011-2013 Nemanja Avramovic, www.avramovic.info
Licensed under CC BY-SA 4.0 License: http://creativecommons.org/licenses/by-sa/4.0/
This means everyone is allowed to:
Share - copy and redistribute the material in any medium or format
Adapt - remix, transform, and build upon the material for any purpose, even commercially.
Under following conditions:
Attribution - You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
ShareAlike - If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
*/
;(function ($) {
'use strict';
//Defaults configuration
var defaults = {
url: null,
values: null,
method: "POST",
target: null,
traditional: false,
redirectTop: false
};
/**
* jQuery Redirect
* @param {string} url - Url of the redirection
* @param {Object} values - (optional) An object with the data to send. If not present will look for values as QueryString in the target url.
* @param {string} method - (optional) The HTTP verb can be GET or POST (defaults to POST)
* @param {string} target - (optional) The target of the form. "_blank" will open the url in a new window.
* @param {boolean} traditional - (optional) This provides the same function as jquery's ajax function. The brackets are omitted on the field name if its an array. This allows arrays to work with MVC.net among others.
* @param {boolean} redirectTop - (optional) If its called from a iframe, force to navigate the top window.
*//**
* jQuery Redirect
* @param {string} opts - Options object
* @param {string} opts.url - Url of the redirection
* @param {Object} opts.values - (optional) An object with the data to send. If not present will look for values as QueryString in the target url.
* @param {string} opts.method - (optional) The HTTP verb can be GET or POST (defaults to POST)
* @param {string} opts.target - (optional) The target of the form. "_blank" will open the url in a new window.
* @param {boolean} opts.traditional - (optional) This provides the same function as jquery's ajax function. The brackets are omitted on the field name if its an array. This allows arrays to work with MVC.net among others.
* @param {boolean} opts.redirectTop - (optional) If its called from a iframe, force to navigate the top window.
*/
$.redirect = function (url, values, method, target, traditional, redirectTop) {
var opts = url;
if (typeof url !== "object") {
var opts = {
url: url,
values: values,
method: method,
target: target,
traditional: traditional,
redirectTop: redirectTop
};
}
var config = $.extend({}, defaults, opts);
var generatedForm = $.redirect.getForm(config.url, config.values, config.method, config.target, config.traditional);
$('body', config.redirectTop ? window.top.document : undefined).append(generatedForm.form);
generatedForm.submit();
generatedForm.form.remove();
};
$.redirect.getForm = function (url, values, method, target, traditional) {
method = (method && ["GET", "POST", "PUT", "DELETE"].indexOf(method.toUpperCase()) !== -1) ? method.toUpperCase() : 'POST';
url = url.split("#");
var hash = url[1] ? ("#" + url[1]) : "";
url = url[0];
if (!values) {
var obj = $.parseUrl(url);
url = obj.url;
values = obj.params;
}
values = removeNulls(values);
var form = $('')
.attr("method", method)
.attr("action", url + hash);
if (target) {
form.attr("target", target);
}
var submit = form[0].submit;
iterateValues(values, [], form, null, traditional);
return { form: form, submit: function () { submit.call(form[0]); } };
}
//Utility Functions
/**
* Url and QueryString Parser.
* @param {string} url - a Url to parse.
* @returns {object} an object with the parsed url with the following structure {url: URL, params:{ KEY: VALUE }}
*/
$.parseUrl = function (url) {
if (url.indexOf('?') === -1) {
return {
url: url,
params: {}
};
}
var parts = url.split('?'),
query_string = parts[1],
elems = query_string.split('&');
url = parts[0];
var i, pair, obj = {};
for (i = 0; i < elems.length; i += 1) {
pair = elems[i].split('=');
obj[pair[0]] = pair[1];
}
return {
url: url,
params: obj
};
};
//Private Functions
var getInput = function (name, value, parent, array, traditional) {
var parentString;
if (parent.length > 0) {
parentString = parent[0];
var i;
for (i = 1; i < parent.length; i += 1) {
parentString += "[" + parent[i] + "]";
}
if (array) {
if (traditional)
name = parentString;
else
name = parentString + "[" + name + "]";
} else {
name = parentString + "[" + name + "]";
}
}
return $("").attr("type", "hidden")
.attr("name", name)
.attr("value", value);
};
var iterateValues = function (values, parent, form, isArray, traditional) {
var i, iterateParent = [];
Object.keys(values).forEach(function (i) {
if (typeof values[i] === "object") {
iterateParent = parent.slice();
iterateParent.push(i);
iterateValues(values[i], iterateParent, form, Array.isArray(values[i]), traditional);
} else {
form.append(getInput(i, values[i], parent, isArray, traditional));
}
});
};
var removeNulls = function (values) {
var propNames = Object.getOwnPropertyNames(values);
for (var i = 0; i < propNames.length; i++) {
var propName = propNames[i];
if (values[propName] === null || values[propName] === undefined) {
delete values[propName];
} else if (typeof values[propName] === 'object') {
values[propName] = removeNulls(values[propName]);
} else if (values[propName].length < 1) {
delete values[propName];
}
}
return values;
};
}(window.jQuery || window.Zepto || window.jqlite));
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function() {
$('#existing-customer-radio').click(); //Have the existing customer fields show first.
$('#existing-customer-radio').click(() => {
$('#existing-customer-fields').show();
$('#new-customer-fields').hide();
});
$('#new-customer-radio').click(() => {
$('#existing-customer-fields').hide();
$('#new-customer-fields').show();
});
});
let formatPhoneNumber = (str) => {
//Filter only numbers from the input
let cleaned = ('' + str).replace(/\D/g, '');
//Check if the input is of correct length
let match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
if (match) {
return '(' + match[1] + ') ' + match[2] + '-' + match[3]
};
return null
};
let trimNonNumericValue = (str) => {
return str.replace(/[^.\d]/g, '');
}
let isValidEmail = (str) => {
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(str) //https://www.w3resource.com/javascript/form/email-validation.php
}
let isValidPhoneNumber = (str) => {
return /^((\+1)?[\s-]?)?\(?[1-9]\d\d\)?[\s-]?[1-9]\d\d[\s-]?\d\d\d\d/.test(str);
};
let isBlank = (str) => {
return (!str || /^\s*$/.test(str));
}
let formatCurrency = (str) => {
// Create our number formatter.
var formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
return formatter.format(str); /* $2,500.00 */
}
/*
Text Message Template Validators
**/
//used in evaluating the text message template variable: {{start_time}}
let evaluateStartTimeForTextMessage = (startDateTime) => {
return moment(startDateTime).utc().format('h:mm a');
}
//used in evaluating the text message template variable: {{readable_start_time}}
let evaluateReadableStartTimeFromAppointment = (startDateTime) => {
return moment(startDateTime).calendar({
sameDay: '[today at] h:mm a',
nextDay: '[tomorrow] h:mm a',
nextWeek: '[this upcoming] ddd [at] h:mm a',
lastDay: '[yesterday]',
lastWeek: '[last] ddd',
sameElse: 'ddd, MMM Do [at] h:mm a',
});
}
//used in evaluating the readable event time in the /events table
let evaluateReadableDateTime = (datetime) => {
return moment(datetime).calendar({
sameDay: '[today at] h:mm a',
nextDay: '[tomorrow] h:mm a',
nextWeek: '[this upcoming] ddd [at] h:mm a',
lastDay: '[yesterday at] h:mm a',
lastWeek: '[last] ddd [at] h:mm a',
sameElse: 'ddd, MMM Do [at] h:mm a',
});
}
//used in evaluating the text message template variable: {{end_time}}
let evaluateEndTimeForTextMessage = (endDateTime) => {
return moment(endDateTime).utc().format('h:mm a')
}
//used in evaluating the text message template variable: {{day}}
let evaluateDayForTextMessage = (startDateTime) => {
return moment(startDateTime).calendar({
sameDay: '[today]',
nextDay: '[tomorrow]',
nextWeek: '[this upcoming] ddd',
lastDay: '[yesterday]',
lastWeek: '[last] ddd',
sameElse: 'ddd',
})
}
//used in evaluating the text message template variable: {{date}}
let evaluateDateForTextMessage = (startDateTime) => {
return moment(startDateTime).utc().format('MMM Do');
}
//used in evaluating the text message template variable: {{date}}
let formatReadableDate = (startDateTime) => {
return moment(startDateTime).utc().format('MMM Do YYYY');
}
let formatDuration = (totalSeconds) => {
let seconds = totalSeconds;
const hours = Math.floor(seconds / 3600);
seconds %= 3600;
const minutes = Math.floor(seconds / 60);
seconds = parseInt(seconds % 60);
return `${hours}h ${minutes}m ${seconds}s`;
}
function addLineItem(description = null, price = null){
var description = description;
if (description == null){
description = "Enter Description Here..";
}
var price = price;
if (price == null){
price = "0.00";
}
var $lineItemRow = $("
var $lineItemPrice = $("
");var $lineItemDelete = $("
");# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function(){
new PublicMarketingSourceSelect('request_marketing_source_select', $('#job_request_business_id').text(), function(event){
$('#job_request_marketing_sources_id').val(event.currentTarget.value);
});
setPhoneNumberValidationCallback();
setEmailValidationCallback();
setPhoneNumberCustomerLookupCallback();
});
setEmailValidationCallback = function(){
$('#job_request_email').change(function(event){
//let's remove any whitespace at beginning or end of the value first.
const attemptedEmail = event.currentTarget.value;
const trimmedOfWhitespaceEmail = attemptedEmail.trim();
event.currentTarget.value = trimmedOfWhitespaceEmail;
if (!isValidEmail(event.currentTarget.value)){
$('#job_request_email').addClass('is-invalid');
$("#job_request_email").parent().append('Email wasn\'t in correct format.');
}
else{
$('#job_request_email').removeClass('is-invalid');
$('#job_request_email').addClass('is-valid');
$('.email-invalid-text').remove();
}
});
}
setPhoneNumberValidationCallback = function(){
$('#job_request_phone').change(function(event){
if (!isValidPhoneNumber(event.currentTarget.value)){
$('#job_request_phone').addClass('is-invalid');
$("#job_request_phone").parent().append('Phone wasn\'t in correct format');
}
else{
$('#job_request_phone').removeClass('is-invalid');
$('#job_request_phone').addClass('is-valid');
$('.phone-invalid-text').remove();
}
});
}
setPhoneNumberCustomerLookupCallback = function(){
$('#job_request_phone').change(function(event){
console.log("Searching for phone number to see if this customer is in our system.")
if (isValidPhoneNumber(event.currentTarget.value)){
let secretAuthKey = $('#job_request_secret_key').val();
let cleanPhoneNumber = trimNonNumericValue(event.currentTarget.value);
console.log(`${cleanPhoneNumber} is valid, searching now..`)
$.get({url: `/resolve_phone?secret_key=${secretAuthKey}&phone=${cleanPhoneNumber}`,
success: function(data){
if (data.customer){
console.log(`Found a customer record for ${cleanPhoneNumber}`)
$('#customer_information_card_name').text(data.customer.full_name);
$('#customer_information_card_phone').text(formatPhoneNumber(data.customer.phone));
$('#customer_information_card_email').text(data.customer.email);
$('#customer_information_card_address').text(data.customer.default_location?.display_address);
$('#location_information_card_address').text(`${data.customer.default_location?.street_address}, ${data.customer.default_location?.city} ${data.customer.default_location?.zip_code}`)
window.scrollTo(0,0); //scroll to top so user can see the modal.
$('#is_this_you_customer_lookup_modal_for_job_request').modal('show');
$('.modal-backdrop').remove(); //If we don't call this then the backdrop pops up in front of the modal and breaks the functionality
$('#yes_button_add_customer_information').on('click', function(event){
//Set all the information on the form with the customer information.
$('#job_request_first_name').val(data.customer.first_name);
$('#job_request_last_name').val(data.customer.last_name);
$('#job_request_email').val(data.customer.email);
// Leaving this commented out for now until I decide what to do with it. Basically, if the user says "Yes" but doesn't have a state saved, then it won't save the state even though it should.
// Right now, the default is to set the state based on the business state value. So if the customer's
if (data.customer.default_location?.state != null){ //Don't erase our state out if it was already set by our system.
$('#job_request_state').val(data.customer.default_location?.state);
}
$('#job_request_customer_id').val(data.customer.id);
$('#is_this_the_correct_address_lookup_modal').modal('show');
$('.modal-backdrop').remove(); //If we don't call this then the backdrop pops up in front of the modal and breaks the functionality
$('#yes_button_correct_address_information').on('click', function(event){
$('#job_request_street_address').val(data.customer.default_location?.street_address);
$('#job_request_location_id').val(data.customer.default_location?.id);
$('#job_request_city').val(data.customer.default_location?.city);
$('#job_request_zip').val(data.customer.default_location?.zip_code);
$('#job_request_create_new_location_for_job_request').val(false);
});
$('#no_button_correct_address_information').on('click', function(event){
$('#job_request_street_address').val('');
$('#job_request_city').val('');
$('#job_request_zip').val('');
//create new location for user =
$('#job_request_create_new_location_for_job_request').val(true);
});
});
//If Yes, autofill the fields with customer info and add customer ID
//If No, then close modal silently.
}
else {
console.log(`Didn't find any customer record for ${cleanPhoneNumber}`)
}
}
})
}
});
}
function GetQuillHtml(id){
return $(`#${id}`)[0].children[0].innerHTML;
}
function SetQuillHtml(id, givenHtml){
$(`#${id}`)[0].children[0].innerHTML = givenHtml;
}
/**
* Makes sure that the modal is completely closed.
* @param {Id of Modal to close} id
*/
function ForceCloseModal(id){
$(`#${id}`).toggle('modal');
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function(){
$('.edit_job_appointment_modal').click(function(evt){
setEmployeeJobModalToBeEditModal(evt);
});
});
$('#new_employee_job').on('ajax:success', function(event){
var data = event.detail[0];
var wasAnUpdate = event.detail[1] == "Accepted"; //Means was a PUT request to server
if (wasAnUpdate){
//Modifying the the table row and put new, updated info in.
setEmployeeJobRow(appointmentEditButtonClickedOn, data)
setModalBackToAdd();
}
});
//Important! This global is used by methods to determine what to do.
var appointmentEditButtonClickedOn = null; //Saving state like this.. might be a bad idea..
function setModalBackToAdd(firstTimeInitializingAddModal){
//We need to remove the attributes to cha nge it back to a POST instead of a PUT.
clearAppointmentModalForm();
$('#submitJobAppointmentButton').removeAttr('formaction');
$('#submitJobAppointmentButton').removeAttr('formmethod');
appointmentEditButtonClickedOn = null;
const start = moment();
const remainder = 30 - (start.minute() % 30); // rounding process.
const startDate = moment(start).add(remainder, "minutes")
SetFlatpickr('employee_job_time_start', startDate.toDate());
SetFlatpickr('employee_job_time_end', startDate.add(2, 'h').toDate());
//should set the default employee too..
SetSelect2Option('employee_select', currentEmployeeName, currentEmployeeId);
}
function setEmployeeJobRow(editButtonElement, data){
editButtonElement.parentElement.parentElement.children[0].innerHTML = data["employee_name"];
editButtonElement.parentElement.parentElement.children[1].innerHTML = data["appointment_type_name"];
editButtonElement.parentElement.parentElement.children[2].innerHTML = data["formatted_start"];
editButtonElement.parentElement.parentElement.children[3].innerHTML = data["formatted_end"];
}
function setEmployeeJobModalToBeEditModal(evt){
employeeJobId = evt.currentTarget.dataset.custom;
appointmentEditButtonClickedOn = evt.currentTarget;
$('#employee_job_id').val(employeeJobId);
// Make a get request for the employee job info, then fill in all the modal fields.
$.get({
url: `/employee_jobs/details/${employeeJobId}.json`,
success: function(data){
$('#new_job_appointment_modal').modal('show');
SetSelect2Option('employee_select', data["employee_name"], data["employee_id"])
SetSelect2Option('appointment_type', data["appointment_type_name"], data["appointment_type_id"])
SetFlatpickr('employee_job_time_start', data["time_start"]);
SetFlatpickr('employee_job_time_end', data["time_end"]);
// document.querySelector("#employee_job_time_start")._flatpickr.setDate(data["time_start"]);
// document.querySelector("#employee_job_time_end")._flatpickr.setDate(data["time_end"]);
$('#submitJobAppointmentButton').attr('formaction', `/employee_jobs/${employeeJobId}.json`);
$('#submitJobAppointmentButton').attr('formmethod', 'put');
//Change button to PUT a url that will update the employee job & reload the job page.
},
error: function(data){
console.log("error");
console.log(data);
}
});
}
/*
* bootstrap-tagsinput v0.8.0
*
*/
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'badge badge-info';
},
focusClass: 'focus',
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
itemTitle: function(item) {
return null;
},
freeInput: true,
addOnBlur: true,
maxTags: undefined,
maxChars: undefined,
confirmKeys: [13, 44],
delimiter: ',',
delimiterRegex: null,
cancelConfirmKeysOnEmpty: false,
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
},
trimValue: false,
allowDuplicates: false,
triggerChange: true,
editOnBackspace: false
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.isInit = true;
this.itemsArray = [];
this.$element = $(element);
this.$element.addClass('sr-only');
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 2011–2014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
/*!
* Flickity PACKAGED v2.3.0
* Touch, responsive, flickable carousels
*
* Licensed GPLv3 for open source use
* or Flickity Commercial License for commercial use
*
* https://flickity.metafizzy.co
* Copyright 2015-2021 Metafizzy
*/
(function(e,i){if(typeof define=="function"&&define.amd){define("jquery-bridget/jquery-bridget",["jquery"],function(t){return i(e,t)})}else if(typeof module=="object"&&module.exports){module.exports=i(e,require("jquery"))}else{e.jQueryBridget=i(e,e.jQuery)}})(window,function t(e,r){"use strict";var o=Array.prototype.slice;var i=e.console;var u=typeof i=="undefined"?function(){}:function(t){i.error(t)};function n(h,s,c){c=c||r||e.jQuery;if(!c){return}if(!s.prototype.option){s.prototype.option=function(t){if(!c.isPlainObject(t)){return}this.options=c.extend(true,this.options,t)}}c.fn[h]=function(t){if(typeof t=="string"){var e=o.call(arguments,1);return i(this,t,e)}n(this,t);return this};function i(t,r,o){var a;var l="$()."+h+'("'+r+'")';t.each(function(t,e){var i=c.data(e,h);if(!i){u(h+" not initialized. Cannot call methods, i.e. "+l);return}var n=i[r];if(!n||r.charAt(0)=="_"){u(l+" is not a valid method");return}var s=n.apply(i,o);a=a===undefined?s:a});return a!==undefined?a:t}function n(t,n){t.each(function(t,e){var i=c.data(e,h);if(i){i.option(n);i._init()}else{i=new s(e,n);c.data(e,h,i)}})}a(c)}function a(t){if(!t||t&&t.bridget){return}t.bridget=n}a(r||e.jQuery);return n});(function(t,e){if(typeof define=="function"&&define.amd){define("ev-emitter/ev-emitter",e)}else if(typeof module=="object"&&module.exports){module.exports=e()}else{t.EvEmitter=e()}})(typeof window!="undefined"?window:this,function(){function t(){}var e=t.prototype;e.on=function(t,e){if(!t||!e){return}var i=this._events=this._events||{};var n=i[t]=i[t]||[];if(n.indexOf(e)==-1){n.push(e)}return this};e.once=function(t,e){if(!t||!e){return}this.on(t,e);var i=this._onceEvents=this._onceEvents||{};var n=i[t]=i[t]||{};n[e]=true;return this};e.off=function(t,e){var i=this._events&&this._events[t];if(!i||!i.length){return}var n=i.indexOf(e);if(n!=-1){i.splice(n,1)}return this};e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(!i||!i.length){return}i=i.slice(0);e=e||[];var n=this._onceEvents&&this._onceEvents[t];for(var s=0;s1){t=i.modulo(t,this.slideableWidth);t-=this.slideableWidth;this.shiftWrapCells(t)}this.setTranslateX(t,this.isAnimating);this.dispatchScrollEvent()};n.setTranslateX=function(t,e){t+=this.cursorPosition;t=this.options.rightToLeft?-t:t;var i=this.getPositionValue(t);this.slider.style.transform=e?"translate3d("+i+",0,0)":"translateX("+i+")"};n.dispatchScrollEvent=function(){var t=this.slides[0];if(!t){return}var e=-this.x-t.target;var i=e/this.slidesWidth;this.dispatchEvent("scroll",null,[i,e])};n.positionSliderAtSelected=function(){if(!this.cells.length){return}this.x=-this.selectedSlide.target;this.velocity=0;this.positionSlider()};n.getPositionValue=function(t){if(this.options.percentPosition){return Math.round(t/this.size.innerWidth*1e4)*.01+"%"}else{return Math.round(t)+"px"}};n.settle=function(t){var e=!this.isPointerDown&&Math.round(this.x*100)==Math.round(t*100);if(e){this.restingFrames++}if(this.restingFrames>2){this.isAnimating=false;delete this.isFreeScrolling;this.positionSlider();this.dispatchEvent("settle",null,[this.selectedIndex])}};n.shiftWrapCells=function(t){var e=this.cursorPosition+t;this._shiftCells(this.beforeShiftCells,e,-1);var i=this.size.innerWidth-(t+this.slideableWidth+this.cursorPosition);this._shiftCells(this.afterShiftCells,i,1)};n._shiftCells=function(t,e,i){for(var n=0;n0?i:0;s.wrapShift(r);e-=s.size.outerWidth}};n._unshiftCells=function(t){if(!t||!t.length){return}for(var e=0;e0){var i=this.cells[t-1];e=i.x+i.size.outerWidth}var n=this.cells.length;for(var s=t;s0){var s=this.cells[e];if(!s){break}n.push(s);e+=i;t-=s.size.outerWidth}return n};v._containSlides=function(){if(!this.options.contain||this.options.wrapAround||!this.cells.length){return}var t=this.options.rightToLeft;var e=t?"marginRight":"marginLeft";var i=t?"marginLeft":"marginRight";var n=this.slideableWidth-this.getLastCell().size[i];var s=n1;if(!i){return t}var n=a.modulo(t,e);var s=Math.abs(n-this.selectedIndex);var r=Math.abs(n+e-this.selectedIndex);var o=Math.abs(n-e-this.selectedIndex);if(!this.isDragSelect&&r=e){this.x+=this.slideableWidth}};v.previous=function(t,e){this.select(this.selectedIndex-1,t,e)};v.next=function(t,e){this.select(this.selectedIndex+1,t,e)};v.updateSelectedSlide=function(){var t=this.slides[this.selectedIndex];if(!t){return}this.unselectSelectedSlide();this.selectedSlide=t;t.select();this.selectedCells=t.cells;this.selectedElements=t.getCellElements();this.selectedCell=t.cells[0];this.selectedElement=this.selectedElements[0]};v.unselectSelectedSlide=function(){if(this.selectedSlide){this.selectedSlide.unselect()}};v.selectInitialIndex=function(){var t=this.options.initialIndex;if(this.isInitActivated){this.select(this.selectedIndex,false,true);return}if(t&&typeof t=="string"){var e=this.queryCell(t);if(e){this.selectCell(t,false,true);return}}var i=0;if(t&&this.slides[t]){i=t}this.select(i,false,true)};v.selectCell=function(t,e,i){var n=this.queryCell(t);if(!n){return}var s=this.getCellSlideIndex(n);this.select(s,e,i)};v.getCellSlideIndex=function(t){for(var e=0;e *");return this.getCell(t)};v.getAdjacentCellElements=function(t,e){if(!t){return this.selectedSlide.getCellElements()}e=e===undefined?this.selectedIndex:e;var i=this.slides.length;if(1+t*2>=i){return this.getCellElements()}var n=[];for(var s=e-t;s<=e+t;s++){var r=this.options.wrapAround?a.modulo(s,i):s;var o=this.slides[r];if(o){n=n.concat(o.getCellElements())}}return n};v.queryCell=function(t){if(typeof t=="number"){return this.cells[t]}if(typeof t=="string"){if(t.match(/^[#.]?[\d/]/)){return}t=this.element.querySelector(t)}return this.getCell(t)};v.uiChange=function(){this.emitEvent("uiChange")};v.childUIPointerDown=function(t){if(t.type!="touchstart"){t.preventDefault()}this.focus()};v.onresize=function(){this.watchCSS();this.resize()};a.debounceMethod(p,"onresize",150);v.resize=function(){if(!this.isActive||this.isAnimating||this.isDragging){return}this.getSize();if(this.options.wrapAround){this.x=a.modulo(this.x,this.slideableWidth)}this.positionCells();this._getWrapShiftCells();this.setGallerySize();this.emitEvent("resize");var t=this.selectedElements&&this.selectedElements[0];this.selectCell(t,false,true)};v.watchCSS=function(){var t=this.options.watchCSS;if(!t){return}var e=h(this.element,":after").content;if(e.indexOf("flickity")!=-1){this.activate()}else{this.deactivate()}};v.onkeydown=function(t){var e=document.activeElement&&document.activeElement!=this.element;if(!this.options.accessibility||e){return}var i=p.keyboardHandlers[t.keyCode];if(i){i.call(this)}};p.keyboardHandlers={37:function(){var t=this.options.rightToLeft?"next":"previous";this.uiChange();this[t]()},39:function(){var t=this.options.rightToLeft?"previous":"next";this.uiChange();this[t]()}};v.focus=function(){var t=n.pageYOffset;this.element.focus({preventScroll:true});if(n.pageYOffset!=t){n.scrollTo(n.pageXOffset,t)}};v.deactivate=function(){if(!this.isActive){return}this.element.classList.remove("flickity-enabled");this.element.classList.remove("flickity-rtl");this.unselectSelectedSlide();this.cells.forEach(function(t){t.destroy()});this.element.removeChild(this.viewport);u(this.slider.children,this.element);if(this.options.accessibility){this.element.removeAttribute("tabIndex");this.element.removeEventListener("keydown",this)}this.isActive=false;this.emitEvent("deactivate")};v.destroy=function(){this.deactivate();n.removeEventListener("resize",this);this.allOff();this.emitEvent("destroy");if(l&&this.$element){l.removeData(this.element,"flickity")}delete this.element.flickityGUID;delete f[this.guid]};a.extend(v,r);p.data=function(t){t=a.getQueryElement(t);var e=t&&t.flickityGUID;return e&&f[e]};a.htmlInit(p,"flickity");if(l&&l.bridget){l.bridget("flickity",p)}p.setJQuery=function(t){l=t};p.Cell=s;p.Slide=o;return p});
/*!
* Unipointer v2.4.0
* base class for doing one thing with pointer event
* MIT license
*/
(function(e,i){if(typeof define=="function"&&define.amd){define("unipointer/unipointer",["ev-emitter/ev-emitter"],function(t){return i(e,t)})}else if(typeof module=="object"&&module.exports){module.exports=i(e,require("ev-emitter"))}else{e.Unipointer=i(e,e.EvEmitter)}})(window,function t(s,e){function i(){}function n(){}var r=n.prototype=Object.create(e.prototype);r.bindStartEvent=function(t){this._bindStartEvent(t,true)};r.unbindStartEvent=function(t){this._bindStartEvent(t,false)};r._bindStartEvent=function(t,e){e=e===undefined?true:e;var i=e?"addEventListener":"removeEventListener";var n="mousedown";if("ontouchstart"in s){n="touchstart"}else if(s.PointerEvent){n="pointerdown"}t[i](n,this)};r.handleEvent=function(t){var e="on"+t.type;if(this[e]){this[e](t)}};r.getTouch=function(t){for(var e=0;e3||Math.abs(t.y)>3};n.pointerUp=function(t,e){this.emitEvent("pointerUp",[t,e]);this._dragPointerUp(t,e)};n._dragPointerUp=function(t,e){if(this.isDragging){this._dragEnd(t,e)}else{this._staticClick(t,e)}};n._dragStart=function(t,e){this.isDragging=true;this.isPreventingClicks=true;this.dragStart(t,e)};n.dragStart=function(t,e){this.emitEvent("dragStart",[t,e])};n._dragMove=function(t,e,i){if(!this.isDragging){return}this.dragMove(t,e,i)};n.dragMove=function(t,e,i){t.preventDefault();this.emitEvent("dragMove",[t,e,i])};n._dragEnd=function(t,e){this.isDragging=false;setTimeout(function(){delete this.isPreventingClicks}.bind(this));this.dragEnd(t,e)};n.dragEnd=function(t,e){this.emitEvent("dragEnd",[t,e])};n.onclick=function(t){if(this.isPreventingClicks){t.preventDefault()}};n._staticClick=function(t,e){if(this.isIgnoringMouseUp&&t.type=="mouseup"){return}this.staticClick(t,e);if(t.type!="mouseup"){this.isIgnoringMouseUp=true;setTimeout(function(){delete this.isIgnoringMouseUp}.bind(this),400)}};n.staticClick=function(t,e){this.emitEvent("staticClick",[t,e])};i.getPointerPoint=e.getPointerPoint;return i});(function(n,s){if(typeof define=="function"&&define.amd){define("flickity/js/drag",["./flickity","unidragger/unidragger","fizzy-ui-utils/utils"],function(t,e,i){return s(n,t,e,i)})}else if(typeof module=="object"&&module.exports){module.exports=s(n,require("./flickity"),require("unidragger"),require("fizzy-ui-utils"))}else{n.Flickity=s(n,n.Flickity,n.Unidragger,n.fizzyUIUtils)}})(window,function t(n,e,i,a){a.extend(e.defaults,{draggable:">1",dragThreshold:3});e.createMethods.push("_createDrag");var s=e.prototype;a.extend(s,i.prototype);s._touchActionValue="pan-y";s._createDrag=function(){this.on("activate",this.onActivateDrag);this.on("uiChange",this._uiChangeDrag);this.on("deactivate",this.onDeactivateDrag);this.on("cellChange",this.updateDraggable)};s.onActivateDrag=function(){this.handles=[this.viewport];this.bindHandles();this.updateDraggable()};s.onDeactivateDrag=function(){this.unbindHandles();this.element.classList.remove("is-draggable")};s.updateDraggable=function(){if(this.options.draggable==">1"){this.isDraggable=this.slides.length>1}else{this.isDraggable=this.options.draggable}if(this.isDraggable){this.element.classList.add("is-draggable")}else{this.element.classList.remove("is-draggable")}};s.bindDrag=function(){this.options.draggable=true;this.updateDraggable()};s.unbindDrag=function(){this.options.draggable=false;this.updateDraggable()};s._uiChangeDrag=function(){delete this.isFreeScrolling};s.pointerDown=function(t,e){if(!this.isDraggable){this._pointerDownDefault(t,e);return}var i=this.okayPointerDown(t);if(!i){return}this._pointerDownPreventDefault(t);this.pointerDownFocus(t);if(document.activeElement!=this.element){this.pointerDownBlur()}this.dragX=this.x;this.viewport.classList.add("is-pointer-down");this.pointerDownScroll=o();n.addEventListener("scroll",this);this._pointerDownDefault(t,e)};s._pointerDownDefault=function(t,e){this.pointerDownPointer={pageX:e.pageX,pageY:e.pageY};this._bindPostStartEvents(t);this.dispatchEvent("pointerDown",t,[e])};var r={INPUT:true,TEXTAREA:true,SELECT:true};s.pointerDownFocus=function(t){var e=r[t.target.nodeName];if(!e){this.focus()}};s._pointerDownPreventDefault=function(t){var e=t.type=="touchstart";var i=t.pointerType=="touch";var n=r[t.target.nodeName];if(!e&&!i&&!n){t.preventDefault()}};s.hasDragStarted=function(t){return Math.abs(t.x)>this.options.dragThreshold};s.pointerUp=function(t,e){delete this.isTouchScrolling;this.viewport.classList.remove("is-pointer-down");this.dispatchEvent("pointerUp",t,[e]);this._dragPointerUp(t,e)};s.pointerDone=function(){n.removeEventListener("scroll",this);delete this.pointerDownScroll};s.dragStart=function(t,e){if(!this.isDraggable){return}this.dragStartPosition=this.x;this.startAnimation();n.removeEventListener("scroll",this);this.dispatchEvent("dragStart",t,[e])};s.pointerMove=function(t,e){var i=this._dragPointerMove(t,e);this.dispatchEvent("pointerMove",t,[e,i]);this._dragMove(t,e,i)};s.dragMove=function(t,e,i){if(!this.isDraggable){return}t.preventDefault();this.previousDragX=this.dragX;var n=this.options.rightToLeft?-1:1;if(this.options.wrapAround){i.x%=this.slideableWidth}var s=this.dragStartPosition+i.x*n;if(!this.options.wrapAround&&this.slides.length){var r=Math.max(-this.slides[0].target,this.dragStartPosition);s=s>r?(s+r)*.5:s;var o=Math.min(-this.getLastSlide().target,this.dragStartPosition);s=sthis.slides[0].target&&-n1;var s=n?a.modulo(e,i):e;var r=this.slides[s];if(!r){return null}var o=n?this.slideableWidth*Math.floor(e/i):0;return t-(r.target+o)};s.dragEndBoostSelect=function(){if(this.previousDragX===undefined||!this.dragMoveTime||new Date-this.dragMoveTime>100){return 0}var t=this.getSlideDistance(-this.dragX,this.selectedIndex);var e=this.previousDragX-this.dragX;if(t>0&&e>0){return 1}else if(t<0&&e<0){return-1}return 0};s.staticClick=function(t,e){var i=this.getParentCell(t.target);var n=i&&i.element;var s=i&&this.cells.indexOf(i);this.dispatchEvent("staticClick",t,[e,n,s])};s.onscroll=function(){var t=o();var e=this.pointerDownScroll.x-t.x;var i=this.pointerDownScroll.y-t.y;if(Math.abs(e)>3||Math.abs(i)>3){this._pointerDone()}};function o(){return{x:n.pageXOffset,y:n.pageYOffset}}return e});(function(n,s){if(typeof define=="function"&&define.amd){define("flickity/js/prev-next-button",["./flickity","unipointer/unipointer","fizzy-ui-utils/utils"],function(t,e,i){return s(n,t,e,i)})}else if(typeof module=="object"&&module.exports){module.exports=s(n,require("./flickity"),require("unipointer"),require("fizzy-ui-utils"))}else{s(n,n.Flickity,n.Unipointer,n.fizzyUIUtils)}})(window,function t(e,i,n,s){"use strict";var r="http://www.w3.org/2000/svg";function o(t,e){this.direction=t;this.parent=e;this._create()}o.prototype=Object.create(n.prototype);o.prototype._create=function(){this.isEnabled=true;this.isPrevious=this.direction==-1;var t=this.parent.options.rightToLeft?1:-1;this.isLeft=this.direction==t;var e=this.element=document.createElement("button");e.className="flickity-button flickity-prev-next-button";e.className+=this.isPrevious?" previous":" next";e.setAttribute("type","button");this.disable();e.setAttribute("aria-label",this.isPrevious?"Previous":"Next");var i=this.createSVG();e.appendChild(i);this.parent.on("select",this.update.bind(this));this.on("pointerDown",this.parent.childUIPointerDown.bind(this.parent))};o.prototype.activate=function(){this.bindStartEvent(this.element);this.element.addEventListener("click",this);this.parent.element.appendChild(this.element)};o.prototype.deactivate=function(){this.parent.element.removeChild(this.element);this.unbindStartEvent(this.element);this.element.removeEventListener("click",this)};o.prototype.createSVG=function(){var t=document.createElementNS(r,"svg");t.setAttribute("class","flickity-button-icon");t.setAttribute("viewBox","0 0 100 100");var e=document.createElementNS(r,"path");var i=a(this.parent.options.arrowShape);e.setAttribute("d",i);e.setAttribute("class","arrow");if(!this.isLeft){e.setAttribute("transform","translate(100, 100) rotate(180) ")}t.appendChild(e);return t};function a(t){if(typeof t=="string"){return t}return"M "+t.x0+",50"+" L "+t.x1+","+(t.y1+50)+" L "+t.x2+","+(t.y2+50)+" L "+t.x3+",50 "+" L "+t.x2+","+(50-t.y2)+" L "+t.x1+","+(50-t.y1)+" Z"}o.prototype.handleEvent=s.handleEvent;o.prototype.onclick=function(){if(!this.isEnabled){return}this.parent.uiChange();var t=this.isPrevious?"previous":"next";this.parent[t]()};o.prototype.enable=function(){if(this.isEnabled){return}this.element.disabled=false;this.isEnabled=true};o.prototype.disable=function(){if(!this.isEnabled){return}this.element.disabled=true;this.isEnabled=false};o.prototype.update=function(){var t=this.parent.slides;if(this.parent.options.wrapAround&&t.length>1){this.enable();return}var e=t.length?t.length-1:0;var i=this.isPrevious?0:e;var n=this.parent.selectedIndex==i?"disable":"enable";this[n]()};o.prototype.destroy=function(){this.deactivate();this.allOff()};s.extend(i.defaults,{prevNextButtons:true,arrowShape:{x0:10,x1:60,y1:50,x2:70,y2:40,x3:30}});i.createMethods.push("_createPrevNextButtons");var l=i.prototype;l._createPrevNextButtons=function(){if(!this.options.prevNextButtons){return}this.prevButton=new o(-1,this);this.nextButton=new o(1,this);this.on("activate",this.activatePrevNextButtons)};l.activatePrevNextButtons=function(){this.prevButton.activate();this.nextButton.activate();this.on("deactivate",this.deactivatePrevNextButtons)};l.deactivatePrevNextButtons=function(){this.prevButton.deactivate();this.nextButton.deactivate();this.off("deactivate",this.deactivatePrevNextButtons)};i.PrevNextButton=o;return i});(function(n,s){if(typeof define=="function"&&define.amd){define("flickity/js/page-dots",["./flickity","unipointer/unipointer","fizzy-ui-utils/utils"],function(t,e,i){return s(n,t,e,i)})}else if(typeof module=="object"&&module.exports){module.exports=s(n,require("./flickity"),require("unipointer"),require("fizzy-ui-utils"))}else{s(n,n.Flickity,n.Unipointer,n.fizzyUIUtils)}})(window,function t(e,i,n,s){function r(t){this.parent=t;this._create()}r.prototype=Object.create(n.prototype);r.prototype._create=function(){this.holder=document.createElement("ol");this.holder.className="flickity-page-dots";this.dots=[];this.handleClick=this.onClick.bind(this);this.on("pointerDown",this.parent.childUIPointerDown.bind(this.parent))};r.prototype.activate=function(){this.setDots();this.holder.addEventListener("click",this.handleClick);this.bindStartEvent(this.holder);this.parent.element.appendChild(this.holder)};r.prototype.deactivate=function(){this.holder.removeEventListener("click",this.handleClick);this.unbindStartEvent(this.holder);this.parent.element.removeChild(this.holder)};r.prototype.setDots=function(){var t=this.parent.slides.length-this.dots.length;if(t>0){this.addDots(t)}else if(t<0){this.removeDots(-t)}};r.prototype.addDots=function(t){var e=document.createDocumentFragment();var i=[];var n=this.dots.length;var s=n+t;for(var r=n;r=this.cells.length){return}var o=this.cells.slice(n,s+1);this.navSelectedElements=o.map(function(t){return t.element});this.changeNavSelectedClass("add")};function a(t,e,i){return(e-t)*i+t}e.changeNavSelectedClass=function(e){this.navSelectedElements.forEach(function(t){t.classList[e]("is-nav-selected")})};e.activateAsNavFor=function(){this.navCompanionSelect(true)};e.removeNavSelectedElements=function(){if(!this.navSelectedElements){return}this.changeNavSelectedClass("remove");delete this.navSelectedElements};e.onNavStaticClick=function(t,e,i,n){if(typeof n=="number"){this.navCompanion.selectCell(n)}};e.deactivateAsNavFor=function(){this.removeNavSelectedElements()};e.destroyAsNavFor=function(){if(!this.navCompanion){return}this.navCompanion.off("select",this.onNavCompanionSelect);this.off("staticClick",this.onNavStaticClick);delete this.navCompanion};return n});
/*!
* imagesLoaded v4.1.4
* JavaScript is all like "You images are done yet or what?"
* MIT License
*/
(function(e,i){"use strict";if(typeof define=="function"&&define.amd){define("imagesloaded/imagesloaded",["ev-emitter/ev-emitter"],function(t){return i(e,t)})}else if(typeof module=="object"&&module.exports){module.exports=i(e,require("ev-emitter"))}else{e.imagesLoaded=i(e,e.EvEmitter)}})(typeof window!=="undefined"?window:this,function t(e,i){var s=e.jQuery;var r=e.console;function o(t,e){for(var i in e){t[i]=e[i]}return t}var n=Array.prototype.slice;function a(t){if(Array.isArray(t)){return t}var e=typeof t=="object"&&typeof t.length=="number";if(e){return n.call(t)}return[t]}function l(t,e,i){if(!(this instanceof l)){return new l(t,e,i)}var n=t;if(typeof t=="string"){n=document.querySelectorAll(t)}if(!n){r.error("Bad element for imagesLoaded "+(n||t));return}this.elements=a(n);this.options=o({},this.options);if(typeof e=="function"){i=e}else{o(this.options,e)}if(i){this.on("always",i)}this.getImages();if(s){this.jqDeferred=new s.Deferred}setTimeout(this.check.bind(this))}l.prototype=Object.create(i.prototype);l.prototype.options={};l.prototype.getImages=function(){this.images=[];this.elements.forEach(this.addElementImages,this)};l.prototype.addElementImages=function(t){if(t.nodeName=="IMG"){this.addImage(t)}if(this.options.background===true){this.addElementBackgroundImages(t)}var e=t.nodeType;if(!e||!h[e]){return}var i=t.querySelectorAll("img");for(var n=0;n
/* =====================================================================
* longclick.js v0.0.1
* https://github.com/djreimer/jquery-longclick
* =====================================================================
* Copyright (c) 2015 Derrick Reimer
* ===================================================================== */
(function($) {
// LONGCLICK CLASS DEFINITION
// ==========================
var Longclick = function(el, options) {
var that = this;
this.el = el;
this.timer = null;
this.held = false
this.options = options;
if (el.innerText) {
this.originalText = el.innerText;
} else {
this.originalText = el.value;
}
$(el).on("mousedown", function(event) {
var $this = $(this);
that.reset(options.holdText);
that.timer = window.setTimeout(function() {
if (options.heldText) {
that.reset(options.heldText);
} else {
that.reset();
}
that.held = true;
$this.trigger("longclick");
// gritterNotification("longclick","longclick");
}, options.duration);
return false;
}).on("mouseup mouseleave click touchend", function(event) {
let letClickThrough = !that.held; //If it wasn't held down, we want the click to go through by returning true
//If it was held down, we want to prevent the click by returning falses
//if was held, return false
//if wasn't held, return true so click can go through
if (!that.held || !options.sticky){
that.reset();
}
if (!letClickThrough && event.type == "click") //have to reset that.held if we prevented the click going through, so that if we try to click it next time we can click it if it wasn't held.
that.held = false;
clearTimeout(that.timer);
return letClickThrough;
})
}
Longclick.prototype = {
constructor: Longclick
, reset: function(value) {
value = value || this.originalText;
if (this.el.innerText) {
this.el.innerText = value;
} else {
this.el.value = value;
}
}
}
Longclick.DEFAULTS = {
duration: 1000,
holdText: "Hold...",
heldText: null,
sticky: false,
}
// LONGCLICK PLUGIN DEFINITION
// ===========================
function Plugin(option) {
return this.each(function() {
var $this = $(this);
var data = $this.data('longclick');
var options = $.extend({}, Longclick.DEFAULTS, typeof option == 'object' && option);
if (!data) $this.data('longclick', (data = new Longclick(this, options)));
if (typeof option == 'string') data[option].call(data);
})
}
$.fn.longclick = Plugin;
})(jQuery);
/*!
* Signature Pad v3.0.0-beta.4 | https://github.com/szimek/signature_pad
* (c) 2020 Szymon Nowak | Released under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SignaturePad = factory());
}(this, (function () { 'use strict';
var Point = (function () {
function Point(x, y, time) {
this.x = x;
this.y = y;
this.time = time || Date.now();
}
Point.prototype.distanceTo = function (start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
};
Point.prototype.equals = function (other) {
return this.x === other.x && this.y === other.y && this.time === other.time;
};
Point.prototype.velocityFrom = function (start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
};
return Point;
}());
var Bezier = (function () {
function Bezier(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
Bezier.fromPoints = function (points, widths) {
var c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
var c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
};
Bezier.calculateControlPoints = function (s1, s2, s3) {
var dx1 = s1.x - s2.x;
var dy1 = s1.y - s2.y;
var dx2 = s2.x - s3.x;
var dy2 = s2.y - s3.y;
var m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
var m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
var l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
var l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
var dxm = m1.x - m2.x;
var dym = m1.y - m2.y;
var k = l2 / (l1 + l2);
var cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
var tx = s2.x - cm.x;
var ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty)
};
};
Bezier.prototype.length = function () {
var steps = 10;
var length = 0;
var px;
var py;
for (var i = 0; i <= steps; i += 1) {
var t = i / steps;
var cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
var cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
var xdiff = cx - px;
var ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
};
Bezier.prototype.point = function (t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
};
return Bezier;
}());
function throttle(fn, wait) {
if (wait === void 0) { wait = 250; }
var previous = 0;
var timeout = null;
var result;
var storedContext;
var storedArgs;
var later = function () {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var now = Date.now();
var remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
var SignaturePad = (function () {
function SignaturePad(canvas, options) {
var _this = this;
if (options === void 0) { options = {}; }
this.canvas = canvas;
this.options = options;
this._handleMouseDown = function (event) {
if (event.which === 1) {
_this._mouseButtonDown = true;
_this._strokeBegin(event);
}
};
this._handleMouseMove = function (event) {
if (_this._mouseButtonDown) {
_this._strokeMoveUpdate(event);
}
};
this._handleMouseUp = function (event) {
if (event.which === 1 && _this._mouseButtonDown) {
_this._mouseButtonDown = false;
_this._strokeEnd(event);
}
};
this._handleTouchStart = function (event) {
event.preventDefault();
if (event.targetTouches.length === 1) {
var touch = event.changedTouches[0];
_this._strokeBegin(touch);
}
};
this._handleTouchMove = function (event) {
event.preventDefault();
var touch = event.targetTouches[0];
_this._strokeMoveUpdate(touch);
};
this._handleTouchEnd = function (event) {
var wasCanvasTouched = event.target === _this.canvas;
if (wasCanvasTouched) {
event.preventDefault();
var touch = event.changedTouches[0];
_this._strokeEnd(touch);
}
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = ('throttle' in options ? options.throttle : 16);
this.minDistance = ('minDistance' in options
? options.minDistance
: 5);
this.dotSize =
options.dotSize ||
function dotSize() {
return (this.minWidth + this.maxWidth) / 2;
};
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this.onBegin = options.onBegin;
this.onEnd = options.onEnd;
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d');
this.clear();
this.on();
}
SignaturePad.prototype.clear = function () {
var _a = this, ctx = _a._ctx, canvas = _a.canvas;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset();
this._isEmpty = true;
};
SignaturePad.prototype.fromDataURL = function (dataUrl, options, callback) {
var _this = this;
if (options === void 0) { options = {}; }
var image = new Image();
var ratio = options.ratio || window.devicePixelRatio || 1;
var width = options.width || this.canvas.width / ratio;
var height = options.height || this.canvas.height / ratio;
this._reset();
image.onload = function () {
_this._ctx.drawImage(image, 0, 0, width, height);
if (callback) {
callback();
}
};
image.onerror = function (error) {
if (callback) {
callback(error);
}
};
image.src = dataUrl;
this._isEmpty = false;
};
SignaturePad.prototype.toDataURL = function (type, encoderOptions) {
if (type === void 0) { type = 'image/png'; }
switch (type) {
case 'image/svg+xml':
return this._toSVG();
default:
return this.canvas.toDataURL(type, encoderOptions);
}
};
SignaturePad.prototype.on = function () {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
if (window.PointerEvent) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
};
SignaturePad.prototype.off = function () {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.removeEventListener('pointerdown', this._handleMouseDown);
this.canvas.removeEventListener('pointermove', this._handleMouseMove);
document.removeEventListener('pointerup', this._handleMouseUp);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('mousemove', this._handleMouseMove);
document.removeEventListener('mouseup', this._handleMouseUp);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this.canvas.removeEventListener('touchend', this._handleTouchEnd);
};
SignaturePad.prototype.isEmpty = function () {
return this._isEmpty;
};
SignaturePad.prototype.fromData = function (pointGroups) {
var _this = this;
this.clear();
this._fromData(pointGroups, function (_a) {
var color = _a.color, curve = _a.curve;
return _this._drawCurve({ color: color, curve: curve });
}, function (_a) {
var color = _a.color, point = _a.point;
return _this._drawDot({ color: color, point: point });
});
this._data = pointGroups;
};
SignaturePad.prototype.toData = function () {
return this._data;
};
SignaturePad.prototype._strokeBegin = function (event) {
var newPointGroup = {
color: this.penColor,
points: []
};
if (typeof this.onBegin === 'function') {
this.onBegin(event);
}
this._data.push(newPointGroup);
this._reset();
this._strokeUpdate(event);
};
SignaturePad.prototype._strokeUpdate = function (event) {
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
var x = event.clientX;
var y = event.clientY;
var point = this._createPoint(x, y);
var lastPointGroup = this._data[this._data.length - 1];
var lastPoints = lastPointGroup.points;
var lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
var isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
var color = lastPointGroup.color;
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
var curve = this._addPoint(point);
if (!lastPoint) {
this._drawDot({ color: color, point: point });
}
else if (curve) {
this._drawCurve({ color: color, curve: curve });
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y
});
}
};
SignaturePad.prototype._strokeEnd = function (event) {
this._strokeUpdate(event);
if (typeof this.onEnd === 'function') {
this.onEnd(event);
}
};
SignaturePad.prototype._handlePointerEvents = function () {
this._mouseButtonDown = false;
this.canvas.addEventListener('pointerdown', this._handleMouseDown);
this.canvas.addEventListener('pointermove', this._handleMouseMove);
document.addEventListener('pointerup', this._handleMouseUp);
};
SignaturePad.prototype._handleMouseEvents = function () {
this._mouseButtonDown = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
this.canvas.addEventListener('mousemove', this._handleMouseMove);
document.addEventListener('mouseup', this._handleMouseUp);
};
SignaturePad.prototype._handleTouchEvents = function () {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
this.canvas.addEventListener('touchmove', this._handleTouchMove);
this.canvas.addEventListener('touchend', this._handleTouchEnd);
};
SignaturePad.prototype._reset = function () {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (this.minWidth + this.maxWidth) / 2;
this._ctx.fillStyle = this.penColor;
};
SignaturePad.prototype._createPoint = function (x, y) {
var rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, new Date().getTime());
};
SignaturePad.prototype._addPoint = function (point) {
var _lastPoints = this._lastPoints;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
var widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2]);
var curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
};
SignaturePad.prototype._calculateCurveWidths = function (startPoint, endPoint) {
var velocity = this.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - this.velocityFilterWeight) * this._lastVelocity;
var newWidth = this._strokeWidth(velocity);
var widths = {
end: newWidth,
start: this._lastWidth
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
};
SignaturePad.prototype._strokeWidth = function (velocity) {
return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
};
SignaturePad.prototype._drawCurveSegment = function (x, y, width) {
var ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
};
SignaturePad.prototype._drawCurve = function (_a) {
var color = _a.color, curve = _a.curve;
var ctx = this._ctx;
var widthDelta = curve.endWidth - curve.startWidth;
var drawSteps = Math.floor(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = color;
for (var i = 0; i < drawSteps; i += 1) {
var t = i / drawSteps;
var tt = t * t;
var ttt = tt * t;
var u = 1 - t;
var uu = u * u;
var uuu = uu * u;
var x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
var y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
var width = Math.min(curve.startWidth + ttt * widthDelta, this.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
};
SignaturePad.prototype._drawDot = function (_a) {
var color = _a.color, point = _a.point;
var ctx = this._ctx;
var width = typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
};
SignaturePad.prototype._fromData = function (pointGroups, drawCurve, drawDot) {
for (var _i = 0, pointGroups_1 = pointGroups; _i < pointGroups_1.length; _i++) {
var group = pointGroups_1[_i];
var color = group.color, points = group.points;
if (points.length > 1) {
for (var j = 0; j < points.length; j += 1) {
var basicPoint = points[j];
var point = new Point(basicPoint.x, basicPoint.y, basicPoint.time);
this.penColor = color;
if (j === 0) {
this._reset();
}
var curve = this._addPoint(point);
if (curve) {
drawCurve({ color: color, curve: curve });
}
}
}
else {
this._reset();
drawDot({
color: color,
point: points[0]
});
}
}
};
SignaturePad.prototype._toSVG = function () {
var _this = this;
var pointGroups = this._data;
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var minX = 0;
var minY = 0;
var maxX = this.canvas.width / ratio;
var maxY = this.canvas.height / ratio;
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', this.canvas.width.toString());
svg.setAttribute('height', this.canvas.height.toString());
this._fromData(pointGroups, function (_a) {
var color = _a.color, curve = _a.curve;
var path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
var attr = "M " + curve.startPoint.x.toFixed(3) + "," + curve.startPoint.y.toFixed(3) + " " +
("C " + curve.control1.x.toFixed(3) + "," + curve.control1.y.toFixed(3) + " ") +
(curve.control2.x.toFixed(3) + "," + curve.control2.y.toFixed(3) + " ") +
(curve.endPoint.x.toFixed(3) + "," + curve.endPoint.y.toFixed(3));
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', color);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, function (_a) {
var color = _a.color, point = _a.point;
var circle = document.createElement('circle');
var dotSize = typeof _this.dotSize === 'function' ? _this.dotSize() : _this.dotSize;
circle.setAttribute('r', dotSize.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', color);
svg.appendChild(circle);
});
var prefix = 'data:image/svg+xml;base64,';
var header = '';
var body = svg.innerHTML;
if (body === undefined) {
var dummy = document.createElement('dummy');
var nodes = svg.childNodes;
dummy.innerHTML = '';
for (var i = 0; i < nodes.length; i += 1) {
dummy.appendChild(nodes[i].cloneNode(true));
}
body = dummy.innerHTML;
}
var footer = '';
var data = header + body + footer;
return prefix + btoa(data);
};
return SignaturePad;
}());
return SignaturePad;
})));
function SetFlatpickr(id, time){
document.querySelector(`#${id}`)._flatpickr.setDate(time);
}
August 14 - 20th, 2022:
> Fix a bug that was making job requests not clickable after the latest chrome update
> Add downloadable calls CSV to pull out your call history data
> Added in Report for Time Sheets accumulated for employees in the reports page.
August 21st - 27th, 2022:
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
setSignatureToBeEdited = function(dataPoints){}; //declared globally so we can access outside this scope
disablePaymentIfNoSignature = function(){};
disableSignButtonIfNoSignature = function(){};
$(function(){
// var canvas = document.querySelector('signature-pad');
var canvas = document.getElementById("signature-pad");
console.log("Canvas is: " + canvas);
var signaturePad = new SignaturePad(canvas);
window.signaturePad = signaturePad; // Expose the signaturePad variable to the global scope
submittingForm = canvas.closest("form");
$('button[type="submit"]').click(function(e){//here's where I could check if the signature was present, if it's not, prevent default and alert that they need to sign.
document.getElementById("signature-pad-value").value = signaturePad.toDataURL();
})
setSignatureToBeEdited = function(dataPoints){ //sets the signature from an invoice that's already been signed
signaturePad.fromDataURL(dataPoints);
}
document.getElementById('clear-signature').addEventListener('click', function () {
signaturePad.clear();
document.getElementById("signature-pad-value").value = "";
disablePayment(); // we don't want them to be able to pay if they just cleared the signature, because there's no signature now haha.
disableSignButton();
});
disablePaymentIfNoSignature = function(){
if (signaturePad.isEmpty()){
disablePayment();
}
}
disableSignButtonIfNoSignature = function(){
if (signaturePad.isEmpty()){
disableSignButton();
}
}
canvas.addEventListener('mousedown', function() {
enablePayment();
enableSignButton();
}, false);
canvas.addEventListener('mouseup', function() {
document.getElementById("signature-pad-value").value = signaturePad.toDataURL();
}, false);
setInterval(checkForSignatureAndEnable, 2000);
function checkForSignatureAndEnable() {
if (signaturePad?.isEmpty() === false) {
console.log("Signature detected, allowing payment");
enablePayment();
enableSignButton();
} else {
console.log("Signature not detected, not allowing payment");
disablePayment();
disableSignButton();
}
}
function disablePayment() {
if (document.getElementById("payment_gateway_name")?.value === 'stripe') {
const stripeButton = document.getElementsByClassName("stripe-button-el")[0];
if (stripeButton) {
stripeButton.disabled = true;
}
} else {
const ibxPayButton = document.getElementsByClassName("ibx-pay-button")[0];
if (ibxPayButton) {
ibxPayButton.disabled = true;
}
}
$('#require-signature-notice')?.show?.();
}
function disableSignButton() {
const signButton = document.getElementById("submit-signature-button");
if (signButton) {
signButton.disabled = true;
}
$('#require-signature-notice')?.show?.();
}
function enableSignButton() {
const signButton = document.getElementById("submit-signature-button");
if (signButton) {
signButton.disabled = false;
}
$('#require-signature-notice')?.hide?.();
}
function enablePayment() {
if (document.getElementById("payment_gateway_name")?.value === 'stripe') {
const stripeButton = document.getElementsByClassName("stripe-button-el")[0];
if (stripeButton) {
stripeButton.disabled = false;
}
} else {
const ibxPayButton = document.getElementsByClassName("ibx-pay-button")[0];
if (ibxPayButton) {
ibxPayButton.disabled = false;
}
}
$('#require-signature-notice')?.hide?.();
}
})
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
//This document for toastr to notify us when a new text messages comes in, and to play our notification sound.
$(document).ready(function(){
console.log("Attempting to subscribe to MessagesChannel now")
if (typeof currentBusinessId !== "undefined"){
console.log("User signed in, attempt to subscribe");
App.messages = App.cable.subscriptions.create({channel: 'MessagesChannel', business_id: currentBusinessId}, { //TODO: We need to get the current business id
received: function(data){
toastr.options = {
"closeButton": true,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-right",
"preventDuplicates": false,
"onclick": null,
"showDuration": "30000",
"hideDuration": "1000",
"timeOut": "5000",
"extendedTimeOut": "1000",
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut"
}
document.getElementById("sms-notification-audio").play();
toastr["info"](` ${data['content']}`, ` ${data['sent_from']}`)
},
});
}
else {
console.log("User not signed in (employee & business not found), so not going to subscribe.");
}
})
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require jquery/dist/jquery.min.js
//= require jquery-ujs/src/rails.js
//= require jquery-ui
//= require jquery.mask
//= require popper
//= require bootstrap-sprockets
//= require stack
//= require js-cookie/src/js.cookie.js
//= require 3rdparty/signature_pad.js
//= require redirect.js
//= require moment
//= require 3rdparty/bootstrap-tagsinput.js
//= require 3rdparty/jquery-longclick.js
//= require d3
//= require_directory ./Select2Helpers
// Needs to load first because classes in ModalHelpers inherit from this
//= require ModalHelpers/_SubmitAndEditModalState.js
//= require_directory ./ModalHelpers
//= require gritter
//= require flatpickr
//= require gritter-notifications.js
//= require_directory ./fullcalendar_helpers
//= require ./fullcalendar_helpers/AppointmentModal.js
//= require invoices.js
//= require customers.js
//= require modal-helper.js
//= require quill-helpers.js
//= require flatpickr-helpers.js
//= require helpers.js
//= require sms-notification.js
//= require phone-mask.js
//= require context_menu_setter.js
//= require_tree ./channels
//= require flowbite.min.js
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(function(){ //Any elements with this phone_us class will now be masked
$('.phone_mask').mask('(000) 000-0000');
$('.phone_mask').bind('paste', (event) => { //I added this in so that you could copy and paste from iPhone phone numbers into the app.. the +1 was throwing off our phone_masking.
var pastedData = event.originalEvent.clipboardData.getData('text')
if (pastedData[0] == "+" && pastedData[1] == "1"){
let filteredPhoneNumber = pastedData.substring(2); //trim out the +1 from phone number if it exists..
console.log(`Modified Pasted phone number from: ${pastedData} to: ${filteredPhoneNumber}!`);
// event.originalEvent.clipboardData.setData('text/plain', filteredPhoneNumber);
event.target.value = filteredPhoneNumber
}
});
$('.area_code_mask').mask('(000)');
})
$(function() {
$('#business_logo').bind('change', function() {
var fileName = $(this).val();
$("#business-logo-file-field").html(fileName);
});
$("#business-logo-file-field").bind('click', function() {
$("#business_logo").click();
});
});
/**
* This function sets the context menu after a link element is clicked to display a context menu and
* allow for it to be clicked with actions depending on the callback.
* @param {*} idOfElement
* @param {*} idOfTextMenu
* @param {*} onClickCallback
*/
function registerContextMenu(idOfElement, idOfTextMenu, onClickCallback){
var toggleContextMenu = () => {
event.preventDefault();
$(idOfTextMenu).finish().toggle(100)
};
$(idOfElement).bind("contextmenu", toggleContextMenu);
$(idOfElement).on("click", toggleContextMenu);
// If the document is clicked somewhere
$(document).bind("mousedown", function (e) {
// If the clicked element is not the menu
if (!$(e.target).parents(idOfTextMenu).length > 0) {
// Hide it
$(idOfTextMenu).hide(100);
}
});
registerContextMenuClickForNewListElements(idOfTextMenu, onClickCallback);
}
/**
* This function needs to be called to set the click listener callback for any 'li' elements.
* If 'li' elements are added dynamically, like in the case of the location context menu and job select context menu,
* we will need to re-call this method to set the listeners for the new 'li' elements.
* @param {*} idOfTextMenu
* @param {*} onClickCallback
*/
function registerContextMenuClickForNewListElements(idOfTextMenu, onClickCallback){
$(`${idOfTextMenu} li`).click(function(){
var boundOnClickCallback = onClickCallback.bind($(this)); //bind $(this) to the callback so that the callbackcan access the correct scope and get the 'data' elements out of it.
boundOnClickCallback();
$(idOfTextMenu).hide(100);
});
}
function setLocationMenuItemsCallback() // If the location menu element is clicked
{
if ($(this).data().action == 'New Location'){
$('#manage_properties_modal').modal();
// $('#job_location_attributes_street_address').val('');
// $('#job_location_attributes_city').val('');
// $('#job_location_attributes_zip_code').val('');
// $('#job_location_attributes_state').val('');
// $('#job_location_attributes_default_location').prop('checked', false);
// $('#job_location_id').val('');
// $("#job_location_attributes_id").val('');
// $("#bill_location_checkbox").show();
}
else {
let street_address = $(this).data().street_address;
$('#job_location_attributes_street_address').val(street_address);
let city = $(this).data().city;
$('#job_location_attributes_city').val(city);
let zip_code = $(this).data().zip_code;
$('#job_location_attributes_zip_code').val(zip_code);
let state = $(this).data().state;
$('#job_location_attributes_state').val(state);
let is_default_location = $(this).data().is_default_location;
$('#job_location_attributes_default_location').prop('checked', is_default_location);
let location_id = $(this).data().id; //Sets the job_location_id field so that the appointment modal won't create a new location, but simply update this location (if there are changes).
//See the jobs controller to see how this works.
$('#job_location_id').val(location_id);
$("#job_location_attributes_id").val(location_id);
}
}
function setJobLinkMenuItemsCallback(){
// Should set the new job id we want this employee job to be transferred over to.
// It should set the job notes to match the old job
// It should set the invoices.
// When posting to the server, it should set this employee job id to the new job id.
var jobId = $(this).data().id;
modalAppointmentContext._jobId = jobId;
modalAppointmentContext._data["notes"] = JSON.parse($(this).data('notes')); //biggest problem is the job notes and the invoices section. Just need to set the job notes and the invoices section.
modalAppointmentContext._data["customer_id"] = $(this).data().customer_id;
modalAppointmentContext._data["invoices"] = JSON.parse($(this).data('invoices')); //Not quite in the right format..
modalAppointmentContext.fillInModalFormsWithCurrentVariables(false); //passing in false prevents the extra employee from getting added to the appointment when it's filled in.
setAptModalForEditExistingEmployeeJob(modalAppointmentContext._employeeJobId, jobId, modalAppointmentContext._data["customer_id"]);
// showCustomerSearchFields();
$("#apt-cal-job-number").html(`${$(this).text().trim()}`);
// removeExtraEmployeeFormData(); //Have duplicate employee problem.
// $('#job_notes').val($(this).data().job_notes)
// TODO: Set Invoices...???
}
function redirectToMessagesWithTextTemplate(unevaluatedTextTemplate){
$('#messages_modal').modal('show'); //here is where we'll set the text message templates.?
// and this is where we should call
if (isBlank(unevaluatedTextTemplate)){
$('#message_to_send').val('')
}
else {
let evaluatedTextTemplate = evaluateContentBasedOnCurrentAppointment(unevaluatedTextTemplate);
$('#message_to_send').val(evaluatedTextTemplate) //the value of the text message tempplate...
}
//We load these here for performance -- no need to load all the messages and the images until the user is ready to start viewing and sending messages.
FCTool.setMessagesModalFromPhoneNumber(modalAppointmentContext._data["contacts_phone"]);
FCTool.setUploadedImagesInMessageAttachmentModal(modalAppointmentContext._data["customer_id"]);
}
function evaluateContentBasedOnCurrentAppointment(unevaluatedTextTemplate){
//Customer and employee info
let evaluatedTextTemplate = unevaluatedTextTemplate.replace('{{employee}}', (modalAppointmentContext._data["employee_first"]))
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{user}}', (currentEmployeeName.includes(' ') ? currentEmployeeName.substr(0,currentEmployeeName.indexOf(' ')) : currentEmployeeName)); // Gets the first name of the user..
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{customer_first_name}}', (modalAppointmentContext._data["customer_first_name"])) //TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{contact_first_name}}', (modalAppointmentContext._data["customer_first_name"])) //TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
//Location
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{location}}', (modalAppointmentContext._data["display_address"]))
//Dates
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{start_time}}', (evaluateStartTimeForTextMessage(modalAppointmentContext._data["time_start"]))) //TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{readable_start_time}}', (evaluateReadableStartTimeFromAppointment(modalAppointmentContext._startDate)))
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{end_time}}', (evaluateEndTimeForTextMessage(modalAppointmentContext._data["time_end"])))
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{day}}', (evaluateDayForTextMessage(modalAppointmentContext._data["time_start"])))
evaluatedTextTemplate = evaluatedTextTemplate.replace('{{date}}', (evaluateDateForTextMessage(modalAppointmentContext._data["time_start"])))
return evaluatedTextTemplate;
};
//h
function setJobAppointmentColor(){
var employeeJobId = modalAppointmentContext._employeeJobId;
var colorId = $(this).data().color_id
if (colorId == null){
//clear out the field
$('#set-appointment-color').css('color', '');
$("#job_employee_jobs_attributes_0_appointment_color_id").val(null); //what is the id to set this color?
}
else{
var colorHex = $(this).data().color_hex
console.log(`Employee job id: ${employeeJobId}. color: ${colorId}`);
$("#job_employee_jobs_attributes_0_appointment_color_id").val(colorId); //what is the id to set this color?
$('#set-appointment-color').css('color', colorHex);
}
}
$(function() {
registerContextMenu('#link-job-from-customer', '#link-job-menu', setJobLinkMenuItemsCallback);
registerContextMenu('#location-context-menu-link', '#location-menu', setLocationMenuItemsCallback);
registerContextMenu('#text-customer-action-link-twilio', "#text-menu", function(){
// $('#messages_modal').modal('show'); //here is where we'll set the text message templates.?
redirectToMessagesWithTextTemplate($(this).attr("data-unevaluated-body"));
// let unevaluatedTextTemplate = $(this).attr("data-unevaluated-body");
//attr data-evaluated-url comes from..??
// let evaluatedUrl = modalAppointmentContext.evaluateUrlVariablesForTextTemplate($(this).attr("data-evaluated-url"));
// contactNumber = modalAppointmentContext._data['contacts_phone'];
// redirectToMessagesWithTextTemplate(evaluatedUrl, contactNumber);
})
registerContextMenu('#set-appointment-color', '#appointment-color-menu', setJobAppointmentColor);
registerContextMenu('#quickbooks-context-menu-click', '#quickbooks-context-menu', function(){
let qb_id = $(this).data().id;
$.post({
url: `/invoices/${qb_id}/resync_qb_token`,
success(data){
console.log("success server response: " + data);
gritterSuccess("Succesfully Resynced", "This invoice just synced it's refresh token with Quickbooks")
},
error(data){
console.log("error server response: " + data);
gritterError("Couldn't Resync", "This invoice couldn't sync it's refresh token with Quickbooks. Try again later.")
}
})
});
});
class FCTool{
constructor(){}
/**
* Takes the button with id: '#submitjobbutton' and removes all of it's on click actions
*/
static removeAllActionsFromModalButtons(){
// $("#submitnewjob").off("click");
$("#apt-cal-modal-customer").off("click");
$("#apt-cal-modal-title").off("click");
}
/**
* Sets the actions for the modal buttons, "View Customer", "View Job", "Navigate to Job", "Invoice Job", "Text Message", "Call"
* @param {Id of the Job, used for setting the View Job Button} jobId
* @param {Id of the customer, used for setting the Text Message/Call Buttons} customerId
* @param {The employee job data, we'll use this to figure out which employee is going on this job so we can autofill the employee's name in our text message link} employeeJobData
*/
static setActionsForModalButtons(jobId, customerId, employeeJobData){
$("#apt-cal-edit-title").click(function(event){ //Setting the "View Job" button to go to the correct job
// if (currentBusinessName === 'Tree Removal Company, LLC.'){
// window.location = "/jobs/" + jobId; //temporarily keeping this the same for Utah Treeco to avoid confusion to adding a new appointment
// }
// else { //Super bad and hacky. Lol. Doing this one off for a customer is not a good business practice, but oh well. I wnat o keep them happy.
window.location = "/customers/" + customerId;
// }
});
$("#apt-cal-modal-customer").click(function(event){ //Setting the "View Customer" button to go to the correct customer
window.location = "/customers/" + customerId;
});
$('#add_appointment_button').unbind().click(function(event){
$('#new_job_appointment_modal').modal('show'); //Show the modal for adding a new appointment
$('#employee_job_job_id').val(jobId);
});
let assignedEmployeeName = employeeJobData["employee_name"].split(' ')[0].trim();
if (employeeJobData["twilio_enabled"] == true){
$("#text-customer-action-link-twilio").attr("href", `/messages?conversation=${employeeJobData['contacts_phone']}&content=This%20is%20${assignedEmployeeName}%20from%20${$('#home-company-name').text().trim()},%20just%20wanted%20to%20let%20you%20know%20I%27m%20on%20my%20way`);
$("#call-customer-action-link").attr("href", `#`); //need to clear the href if it was set previous.
if (currentBusinessName != 'Tree Removal Company, LLC.'){ //How do know how to set this..? For now, we're going to make it the default behavior. Let's see how Mark likes it..
$("#call-customer-action-link").click(() => FCTool.makeOutboundCallToTwilio(modalAppointmentContext._data.customer_phone)); //it would be cool to track the recording here as well.
}
else {
$("#call-customer-action-link").attr("href", `tel:${modalAppointmentContext._data.customer_phone}`); //Temporary for Utah Tree Co. Because they want to be able to call on their own phone.
}
}
else {
$("#text-customer-action-link-no-twilio").attr("href", `sms:${employeeJobData['contacts_phone']}&body=This%20is%20${assignedEmployeeName}%20from%20${$('#home-company-name').text().trim()},%20just%20wanted%20to%20let%20you%20know%20I%27m%20on%20my%20way`);
$("#call-customer-action-link").attr("href", `tel:${employeeJobData['contacts_phone']}`);
}
//I think we already have the google_maps_link_data here
$("#google-maps-action-link").attr("href", `${employeeJobData['google_maps_link']}`);
$("#invoice-generation-action-link").attr("href", `/jobs/${jobId}/generate_invoice`);
}
static setUploadedImagesInMessageAttachmentModal(customerId) {
const noImage = $('
if(customerId){
$.get({
url: `/messages/${customerId}/customer_uploaded_images.json`,
success: function(data){
if(data.length > 0){
const imageContainer = $('
data.forEach((image)=>{
imageContainer.append(
$('', {
'src': `${image['url']}`,
'style': 'width: 150px !important; height: 150px;',
'class': 'm-1 btn p-0 modal-img',
'data-id': `${image['id']}`,
'onclick': 'imageClickHandler(event)'
})
)
})
$('#photos-to-attach-to-message').append(imageContainer);
}
},
error: function(data){
console.log("error");
console.log(data);
}
});
}
}
//Sets the locations for the given customer on the appointment modal for the arrow to select multiple locations.
static setLocationMenu(customerData){
$('#location-menu').empty(); //remove the location menu children
//Add the default 'New..' button.
$('#location-menu').append(`
static setJobLinkMenu(customerId){
$('#link-job-menu').empty();
//TODO: Implement for better linking.
// $.get({
// url: `/customers/${customerId}/jobs.json`,
// success: function(data){
// let jobs = data;
// //if length of jobs is >1, then we'll show the link field. Otherwise, we'll hide it. (Hide complexity for UI/UX experience)
// if (jobs.length > 1){
// // debugger;
// $.each(jobs, function(index, value) {
// $("#link-job-menu").append(
// $(`
/**
* Sets the messages modal on the schedule, with the customer's name and phone number, and the last 25 messages to and from this customer.
* @param {*} customerId
*/
static setMessagesModal(customerData){
//set the send text button to add a message to datatbase and on the UI when clicked.
$('#send_text_button').unbind().click(function(){ //have to use unbind() so that the send button doesn't send multiple times because click() sets a function to be ran for each time it's called.. so it will send 1, then 2, then 3 messages, etc. for as many times as the modal has been opened. By unbinding first, we remove the previous click() action. This would be better if this was only ran once perhaps when the schedule page is loaded. We could probably move this to the schedule javascript code so that it's only ran on the page load instead of each time a modal is loaded.
const imageSrc = $('#attached-image').attr('src')
const file = $('#image_avatar')[0].files[0] || {}
const uploadedImageId = $('#image_avatar_url_id').val();
const messagePayload = {
image: {
file: file,
imageUrl: imageSrc,
id: uploadedImageId
},
message: $('#message_to_send').val()
}
FCTool.addNewMessage(messagePayload);
});
//Clear all messages in the modal, so we have a clean slate:
const parent = document.getElementById("chat-window-inner");
while (parent.firstChild) {
parent.firstChild.remove()
}
$('#recipient-display-name').text(customerData["phone"]);
$('#convo-recipient-name').text(customerData["full_name"]);
// get the phone number for the customer, and let's get the messages for this customer (latest 25) and put it in this modal.
}
static setMessagesModalFromPhoneNumber(phoneNumber){
//Clear all messages in the modal, so we have a clean slate:
const parent = document.getElementById("chat-window-inner");
while (parent.firstChild) {
parent.firstChild.remove()
}
$.get({ //this should only be called when they click on the messaging button to message the customer.
url: `/messages/message_thread_from_number?customer_number=${phoneNumber}`,
success(data){
data['messages'].forEach(function(message, index){
let imageUrl = data['urls'][index]
if (message["is_inbound"]){
FCTool.addMessageFromCustomer(message["content"], message["name"], moment(message["created_at"]).calendar(null, {sameElse: 'MMMM DD, hh:mm a'}), imageUrl);
}
else{
FCTool.addMessageFromBusiness(message["content"], message["name"], moment(message["created_at"]).calendar(null, {sameElse: 'MMMM DD, hh:mm a'}), imageUrl);
}
});
baguetteBox.run('.tz-gallery',{ //We have to re-enable Baguette Box so that we can actually click on the images and get the gallery.
"buttons":true
});
}
});
}
/*
* All the message functions that will be moved to a new messages.js javascript class/file for better organization.
*/
/**
*
* @param {*} text
* @param {*} name
* @param {*} date
*/
//TODO: Move this into it's own messages.js class, shouldn't be in the FCTool class.
static addMessageFromCustomer(text, name, date, imageUrl){
var $elem = $('#chat-window-inner'); //https://amiyasahu.github.io/create-nested-html-elements-in-jquery.html
//We don't want to attach an image if this message didn't have an mms sent with it.
let $imageElement = isBlank(imageUrl) ? $('') : ($('', {'class': 'lightbox', 'href' : `${imageUrl}`})
.append($('', { 'style': 'width: 100px !important; height: 100px; object-fit: contain;', 'src': `${imageUrl}`, 'class': `${imageUrl ? 'd-block' : 'd-none'} rounded w-50` }))
);
$elem.append(
$('
//TODO: Move this into it's own messages.js class, shouldn't be in the FCTool class.
static addMessageFromBusiness(text, sentFromName, date, imageUrl){
var $elem = $('#chat-window-inner'); //https://amiyasahu.github.io/create-nested-html-elements-in-jquery.html
//We don't want to attach an image if this message didn't have an mms sent with it.
let $imageElement = isBlank(imageUrl) ? $('') : $('', { 'class': 'lightbox', 'href' : `${imageUrl}`})
.append( $('', { 'style': 'width: 100px !important; height: 100px; object-fit: contain;', 'src': `${imageUrl}`, 'class': `${imageUrl ? 'd-block' : 'd-none'} rounded w-50`}));
$elem.append(
$('
static getEmployeeJobDetailsAjax(employeeJobId, successCallback, errorCallback){
if (!employeeJobId) //Preventing GET http://localhost:3000/employee_jobs/details/.json 404 (Not Found) error
return;
$.get({
url: `/employee_jobs/details/${employeeJobId}.json`,
success(data){
successCallback(data);
},
error(data){
errorCallback(data);
}
});
}
//when sending a new message, this is the callback for adding the new message bubble in the conversation
static addNewMessage(messagePayload){
const { image: { imageUrl, file, id }, message } = messagePayload;
if (isBlank(message) && isBlank(imageUrl)){ //Don't want to send a message if it's blank
return;
}
const fd = new FormData();
let sendTo = modalAppointmentContext._data.customer_phone;
let businessPhoneNumber = modalAppointmentContext._data.business_twilio_number
fd.append('file', file)
fd.append('id', id)
fd.append('content', message)
fd.append('to', sendTo)
fd.append('sent_from', businessPhoneNumber)
fd.append('name', currentEmployeeName)
$.ajax({
url: '/messages.json',
type: "POST",
dataType: 'json',
data: fd,
processData: false,
contentType: false,
success:function(data){
FCTool.addMessageFromBusiness(message, currentEmployeeName, moment().calendar(null, {sameElse: 'MMMM DD, hh:mm a'}), imageUrl);
$('#message_to_send').val('');
resetImage();
$('#chat-window').scrollTop($('#chat-window')[0].scrollHeight);//Scroll to bottom of current convo page
}.bind(this), //binding to 'this' so we can access 'this' from within the success callback.
error: function(data){
showError("Couldn't Send Message", data); //shows gritter errors
}.bind(this)
});
}
/**
* KEEP
* https://stackoverflow.com/questions/21741841/detecting-ios-android-operating-system
*
* Determine the mobile operating system.
* This function returns one of 'iOS', 'Android', 'Windows Phone', or 'unknown'.
*
* @returns {String}
*/
static getMobileOperatingSystem() {
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
// Windows Phone must come first because its UA also contains "Android"
if (/windows phone/i.test(userAgent)) {
return "Windows Phone";
}
if (/android/i.test(userAgent)) {
return "Android";
}
// iOS detection from: http://stackoverflow.com/a/9039885/177710
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
return "iOS";
}
return "unknown";
}
/**
* Gets the aspect ratio of the width / height.
* Used for mobile scaling of the calendar.
*/
static getAspectRatio(){
return window.innerWidth/window.innerHeight
}
/**
* Gets the default calendar view. Display day if the window screen width is smaller than 800
* for best viewing results.
*/
static getDefaultView(){
if (Cookies.get("dispatch_view")){
return Cookies.get("dispatch_view");
}
if (window.innerWidth < 800){
return 'timeGridDay'
}
else {
return 'timeGridWeek'
}
}
//How to use:
// document.querySelector('.fc-event').append(FCTool.createBadgeElement(''));
static createBadgeElement(content){
let badge = document.createElement("span");
badge.className = "appointment-message-badge";
var textNode = document.createTextNode(content);
badge.appendChild(textNode);
return badge;
}
/**
* Used to scroll the calendar to the current day in our timeline resource view
* @param {*} calendar
*/
static scrollCalendarToToday(calendar){
calendar.scrollToTime((moment()._d.getDate() - calendar.state.currentDate.getDate() + 1) * parseInt(moment().format("HH")) + ":00:00");
}
static didUserConfirmNotification(){ //Problem.. we have to render this on the client side where we're currently rendering the notification message on the server side.
let notify = $('#job_notify').is(':checked'); //is the notify checkbox checked?
if (!notify){
return true; //if the user didn't select the notify checkbox, then we want to be able to save without notifying and sending to the customer.
}
let firstName = $('#job_customer_attributes_first_name').val();
let employeeName = modalAppointmentContext._employeeName;
let companyName = $('#home-company-name').text().trim();
let startTime = moment(modalAppointmentContext._startDate).format('h:mm a');
let endTime = moment(modalAppointmentContext._endDate).format('h:mm a');
let date = moment(modalAppointmentContext._endDate).format('ddd MMM Do');
let message = `Hi ${firstName}, this is ${employeeName} from ${companyName}, just wanted to let you know that I put you on our schedule for an appointment on ${date} between ${startTime} to ${endTime}`
return notify && confirm(`Send text message: "${message}"?`);
}
static makeOutboundCallToTwilio(toPhone){
$.post("/messages/connect_twilio_call", {
phone: toPhone
})
}
}
var className = FCTool;
//https://stackoverflow.com/questions/6998355/including-javascript-class-definition-from-another-file-in-node-js
try {
module.exports = className;
}
catch {
console.log("Couldn't export " + className.name + " as a seperate module.");
}
//= require_tree .
var modalAppointmentContext;
var calendar;
var clickDisabled = false;
/**
* Display the calendar with the draggable options and allow the user
* to modify the events on the calendar. Done through extending the
* default options before initializing the calendar.
*/
function showEditableCalendar() {
var eventBeingMoved = false;
calendar = new FullCalendar.Calendar(document.getElementById('calendar'), {
customButtons: {
addJobButton: {
text: 'new',
click: function() {
setNewAppointmentModal(currentEmployeeId,currentEmployeeName, null, null, null);
$('#invoice-section').hide();
}
},
filterEmployeesDropdown: {
class: 'fc-button-icon',
click: function(){
$('#appointment_filter_modal').modal('show');
}
}
},
plugins: ['interaction', 'dayGrid', 'timeGrid', 'resourceTimeline'],
allDaySlot: false, //don't show the all day slot on the week view..
now: new Date().toISOString(),
defaultDate: Cookies.get("dispatch_date") ? moment(Cookies.get("dispatch_date")).format() : new Date().toISOString(),
editable: true, // enable draggable events
aspectRatio: FCTool.getAspectRatio(),
droppable: true,
minTime: '8:00',
maxTime: '19:15',
// maxTime: '23:45',
header: {
left: 'prev,next, today, addJobButton',
center: 'title',
right: 'filterEmployeesDropdown, resourceTimelineWeekWithTime, dayGridMonth, timeGridWeek, timeGridDay'
},
footer: {
left: 'printSummariesButton'
},
defaultView: FCTool.getDefaultView(),
views: {
resourceTimelineWeekWithTime: {
slotLabelInterval: '2:00',
slotWidth: 15,
type: 'resourceTimeline',
duration: { days: 7 },
buttonText: 'all',
dateAlignment: 'week', //Makes left-most day day at start of week instead of current day of the week
nowIndicator: true,
}
},
datesRender: function(info){ //Callback for when dates render. Set cookie to remember where user was here.
Cookies.set("dispatch_date", info.view.activeStart)
},
viewSkeletonRender: function(info){
Cookies.set("dispatch_view", info.view.type);
},
eventRender: function(info) { //Used to set the hovering tooltips with the address or time of appointment.
$('.tooltip').remove();
if (info == null) return;
if (eventBeingMoved){
let range = FullCalendar.formatRange(info.event.start, info.event.end, {
weekday: 'short', hour: 'numeric', minute: 'numeric',
separator: ' to ',
})
new Tooltip(info.el, {
title: range,
placement: 'bottom',
trigger: 'hover',
container: 'body'
});
}
else {
//is eventHoverText is blank it will cause issues with the schedule, especially with job requests.
let title = isBlank(info.event.extendedProps.eventHoverText) ? info.event.extendedProps.summary : info.event.extendedProps.eventHoverText
new Tooltip(info.el, {
title: title,
placement: 'bottom',
trigger: 'hover',
container: 'body'
});
}
//Set the border width to be larger.
//https://stackoverflow.com/questions/36136056/how-set-width-border-events-with-full-calendar
info. el. style. borderWidth = '2.5px';
info. el. style. borderRadius = '0px';
},
eventResizeStart: function(info){
eventBeingMoved = true;
},
eventResizeStop: function(info){
eventBeingMoved = false;
$('.tooltip').remove();
},
eventDragStart: function(info){
eventBeingMoved = true;
},
eventDragStop: function(info){
eventBeingMoved = false;
$('.tooltip').remove();
},
eventReceive: function(info){ //after an event is dropped onto the calendar. Original Q: Not sure where 'info' gets set with all of the extra props. Answer: The event extended props that are in this info object get set in RequestsDraggable.jsx.
setNewAppointmentFromRequest(info);
info.event.remove(); //remove the event here because the above line opens the modal and sets it with all relveant info, so if they close modal witout saving, we don't want this event still on the calandar. If they were then to save the event modal again, then when the calendar refreshes (as it does every time it closes), then it will add a new event to the calendar, so if we kept this one here, then we'd have two events on there until the user refreshes.
},
resourcesInitiallyExpanded: false,
firstDay: 1, //Setting the first day to be monday on the calendar
resourceLabelText: 'Branches',
resourceGroupField: 'branch',
resources: {
url: '/employees/fullcalendar_resources', //Fetches the employee names for the left-side column and is how we set up full calendar info objects.
failure: function () {
document.getElementById('script-warning').style.display = 'block';
}
},
getTopLevelResources: {
},
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
events: { //Fetches the employee appointments. Also sets the params with a start and end key.
url: '/employees/schedule_json',
failure: function (data) {
console.log(data);
gritterError("Error", "Couldn't get events for calendar");
}
},
dateClick: function (info) { //When clicking on an empty part of the calendar, open the modal
setNewAppointment(info);
},
eventClick: function(info) {
if (clickDisabled) //Preventing multiple clicks on Full Calender Event.
return;
clickDisabled = true;
$('.tooltip').remove();
if (!info.event.id) //Preventing GET http://localhost:3000/employee_jobs/details/.json 404 (Not Found) error
return;
console.log("A calendar event was clicked, here is it's info!");
console.log(info);
$.get({
url: `/employee_jobs/details/${info.event.id}.json?breadcrumb=true`,
success(data){
console.log("Just got data response from employee_jobs/details after an appointment was clicked on, here is the data:")
console.log(data)
setAppointmentFromFullCalendarInfo(info, data);
modalAppointmentContext.toggleModal();
setAptModalForEditExistingEmployeeJob(data, info.event.id, modalAppointmentContext.getCurrentJobId(), modalAppointmentContext.getCurrentCustomerId());
clickDisabled = false;
}
})
},
eventDrop: function(info){ //after dropping an event onto the calendar.. Does this still have the information saved?
updateAppointmentDetailsFromFullCalendarInfo(info);
},
eventResize: function(info){ //after dragging an event
updateAppointmentDetailsFromFullCalendarInfo(info);
},
resourceAreaWidth: '20%',
slotDuration: '0:30',
});
calendar.render();
//This is how we set it to look like a font-awesome icon.
$(".fc-filterEmployeesDropdown-button").html('');
/* Pushes the calendar to the right so we can see the current day if we're loading our current day */
let lookingAtCurrentWeek = moment().isSame(calendar.state.currentDate, "week");
if (lookingAtCurrentWeek){
FCTool.scrollCalendarToToday(calendar);
}
}
//TODO: There is duplicate logic between this and setAppointmentFromEmployeeJobData
function setAppointmentFromFullCalendarInfo(fullCalendarEventInfo, employeeJobData){ //Gets called anytime we populate the job-form from a full calendar event, whether that's from dragging an existing event around, resizing an existind event, clicking on an event, or clicking on the calendar.
var employeeJobId = fullCalendarEventInfo.event.id;
var jobId = fullCalendarEventInfo.event.extendedProps.jobId;
var employeeId = fullCalendarEventInfo.event.getResources()[0]?.id;
if (employeeId == null){ //For employees that have been deactivated, getResources()[0] return [null]. If that happens, let's just rely on the employeeJobData.employee_id.
employeeId = employeeJobData.employee_id;
}
var employeeName = employeeJobData["employee_name"]
var eventStart = moment(fullCalendarEventInfo.event.start).format().substring(0,16);
var eventEnd = moment(fullCalendarEventInfo.event.end).format().substring(0,16);
modalAppointmentContext = new AppointmentModal(employeeId, employeeName, eventStart, eventEnd, employeeJobId, jobId, employeeJobData); //Automatically puts data in right forms.
modalAppointmentContext.fillInModalFormsWithCurrentVariables()
}
function setAppointmentFromEmployeeJobData(employeeJobData){
var employeeJobId = employeeJobData.id;
var jobId = employeeJobData.job_id;
var employeeId = employeeJobData.employee_id;
var employeeName = employeeJobData["employee_name"]
var eventStart = moment(employeeJobData.formatted_start).format().substring(0,16);
var eventEnd = moment(employeeJobData.formatted_end).format().substring(0,16);
modalAppointmentContext = new AppointmentModal(employeeId, employeeName, eventStart, eventEnd, employeeJobId, jobId, employeeJobData); //Automatically puts data in right forms.
modalAppointmentContext.fillInModalFormsWithCurrentVariables()
}
/**
* Finds and sets all the relevant appointment information from the fullcalendar 'info' object if the calendar was clicked or the "New" button was clicked
* @param {*} info
*/
function setNewAppointment(info){
startDate = info.dateStr.substring(0,16); //substring(0, 16) because form needs exactly in format: YYYY-MM-DDThh:mm
endDate = moment(info.dateStr).add(2, 'h').format().substring(0,16); //Add 2 hours to start date for end date (making appointments default of 2 hour long), truncate to YYYY-MM-DDThh:mm, .format() keeps moment.js in local time.
//info.resource.id is set when we're using the "all" view, or the vertical resource view where when you click on a row it's associated with an employee
employeeId = info.resource != null ? info.resource.id : currentEmployeeId; //employeeId and employeeName should be set based on whoever is signed in.
employeeName = info.resource != null ? info.resource.title : currentEmployeeName;
setNewAppointmentModal(employeeId, employeeName, startDate, endDate);
}
/**
* Finds and sets all the relevant appointment information from the fullcalendar 'info' object if a job_request was dragged onto the calendar
* @param {*} info
*/
function setNewAppointmentFromRequest(info){
startDate = moment(info.event.start).format().substring(0,16);
endDate = moment(info.event.end).format().substring(0,16);
if (info?.view?.type == 'dayGridMonth'){ //Doing this for Joe from Cincinatti Pool Divers -- he wants it to be 9AM to 3PM. Previously it was defaulting to 12AM-2AM if dragging a job request on to the month view
startDate = moment(info.event.start).set({hour: 9}).format().substring(0,16);
endDate = moment(info.event.start).set({hour: 15}).format().substring(0,16);
}
//info.resource.id is set when we're using the "all" view, or the vertical resource view where when you click on a row it's associated with an employee
employeeId = info.event.getResources().length > 0 ? info.event.getResources()[0]._resource.id : currentEmployeeId;
employeeName = info.event.getResources().length > 0 ? info.event.getResources()[0]._resource.title : currentEmployeeName;
let data = {"notes": info.event.extendedProps.notes,
"request_id": info.event.extendedProps.requestId,
"customer_id": info.event.extendedProps.customerId,
"location_id": info.event.extendedProps.locationId,
"customer_first_name": info.event.extendedProps.firstName,
"customer_last_name": info.event.extendedProps.lastName,
"customer_phone": info.event.extendedProps.phone,
"customer_email": info.event.extendedProps.email,
"street_address": info.event.extendedProps.streetAddress,
"city": info.event.extendedProps.city,
"zip_code": info.event.extendedProps.zip,
"state": info.event.extendedProps.state,
"marketing_source_id": info.event.extendedProps.marketingSourceId,
"marketing_source_name": info.event.extendedProps.marketingSourceName}
setNewAppointmentModal(employeeId, employeeName, startDate, endDate, data);
}
/**
* Sets a 'new appointment modal' used to submit a new job appointment
* @param {*} employeeId
* @param {*} employeeName
* @param {*} startDate
* @param {*} endDate
* @param {*} data
*/
function setNewAppointmentModal(employeeId, employeeName, startDate, endDate, data){ //if data["request_id"] is set in modal it will set it.
modalAppointmentContext = new AppointmentModal(employeeId, employeeName, startDate, endDate, null, null, data);
modalAppointmentContext.fillInModalFormsWithCurrentVariables();
modalAppointmentContext.toggleModal();
setAptModalForCreateNewEmployeeJob();
}
/**
* Sets the '#submitnewjob' button to post the contents of the
* form to the jobs/new when on click
*/
function setAptModalForCreateNewEmployeeJob() {
setModalHeader(edit = false);
FCTool.removeAllActionsFromModalButtons();
modalAppointmentContext._isNewJob = true;
// FCTool.bindSubmitModalButtonForNewJob();
$('#submitnewjob').unbind().click(function(event){ //https://stackoverflow.com/questions/14969960/jquery-click-events-firing-multiple-times
event.preventDefault();
checkForErrorsBeforeSubmitting();
removeExtraEmployeeFormData();
pushAllSelectedEmployeesIntoFormData();
var data = $("#job-form").serialize();
if (FCTool.didUserConfirmNotification()){
$.ajax({
type: "POST",
url: "/jobs.json",
data: data,
success: function () {
let jobRequestIdThatWasScheduled = modalAppointmentContext._data["request_id"]
if (jobRequestIdThatWasScheduled){ //if this is false, then the job wasn't scheduled from a job request
$(`div[data-requestId='${jobRequestIdThatWasScheduled}']`)[0].remove();
}
closeModal(doRefresh = true);// clear the modal fields after data serialization
gritterSuccess('Successfully added new appointment', "You've successfully added a new appointment", '2500');
},
error: function(data) {
// resetAppointmentModalSaveButton(); //reset it so the loader is no longer showing and it's activated again.
$("#submit_button_outer").removeClass("is-loading");
$('#submitnewjob').prop('disabled', false);
showErrorForCalendar(data)
},
});
removeExtraEmployeeFormData();
}
else {
// resetAppointmentModalSaveButton(); //reset it so the loader is no longer showing and it's activated again.
$("#submit_button_outer").removeClass("is-loading");
$('#submitnewjob').prop('disabled', false);
}
});
}
/**
* Sets the '#submitnewjob' button to update the associated
* employee job id.
*/
function setAptModalForEditExistingEmployeeJob(employeeJobData, employeeJobId, jobId, customerId) { //KEEP
setModalHeader(edit = true, customerId, jobId); //Is this jobId really the Job Id, or is it the EmployeeJob Id?
FCTool.setActionsForModalButtons(jobId, customerId, employeeJobData, modalAppointmentContext); // KEEP This gets hit again with employee job details
FCTool.setJobLinkMenu(customerId);
$.get({
url: `/customers/${customerId}/lite.json`,
success: function(customerData){
FCTool.setLocationMenu(customerData); //Sets the menu dropdown for selecting a different location that the customer has.
FCTool.setMessagesModal(customerData);
},
error: function(data){
gritterError()
}
});
modalAppointmentContext._isNewJob = false;
var handleSubmittingEmployeeJobUpdate = function(event){
$("#submitnewjob").unbind('click', handleSubmittingEmployeeJobUpdate);
event.preventDefault();
checkForErrorsBeforeSubmitting();
removeExtraEmployeeFormData();
pushAllSelectedEmployeesIntoFormData();
const data = $("#job-form").serialize() + "&id=" + jobId + "&employee_job_id=" + employeeJobId + "&customer_id=" + customerId;
$.ajax({
url: "/update_job_and_times",
type: "PUT",
data: data,
success: function () {
gritterSuccess('Successfully updated appointment', "You've successfully updated the appointment", '2500');
closeModal(doRefresh = true); //could improve performance if we didn't reload the entire schedule after changing just 1 job.. although it's good practice usually to refresh the whole calendar to fetch any recent updates. Could send a delta T to server? and server just sends updates since last? That would be pretty cool and high-performance.
},
error: showErrorForCalendar,
});
removeExtraEmployeeFormData();
};
$("#submitnewjob").bind('click', handleSubmittingEmployeeJobUpdate);
}
/**
* Sets the modal header depending on if it's the modal for a new appointment, (sends a POST to create new employee_job & job)
* or the modal for editing (sends a PUT to the server to edit existing employee_job) an existing appointment
* @param {*} edit
*/
function setModalHeader(edit = false, customerId, jobId){
if (edit) {
$('#apt-cal-new-title').hide();
$("#apt-cal-edit-title").show();
$('#apt-cal-job-number').show();
// $("#apt-cal-job-number").html(`${modalAppointmentContext._data["customer_full_name"]}, ${customerId}`); //index.. how to get the index number?
$("#apt-cal-modal-customer").show();
$("#apt-cal-modal-job").show();
$('.edit-job-modal').show();
$('.new-job-modal').hide();
$('.delete-current-job').show();
$('#add_appointment_button').show();
hideCustomerSearchFields();
}
else {
$('#add_appointment_button').hide();
$("#apt-cal-edit-title").hide();
$('#apt-cal-job-number').hide();
$('#apt-cal-new-title').show();
$('.edit-job-modal').hide();
$('.new-job-modal').show();
//DON'T DISPLAY AS LINK
$("#apt-cal-modal-customer").hide();
$("#apt-cal-modal-job").hide();
$('.delete-current-job').hide();
showCustomerSearchFields();
}
}
function hideCustomerSearchFields(){
//hide customer search bar by default if editing an appointment
$('#search-minus-icon').hide();
$('#search-icon').show();
$('#customer-search-fields').hide();
// $('#link-job-from-customer').hide();
}
function showCustomerSearchFields(){
//set customer search bar to visible
$('#search-minus-icon').show();
$('#search-icon').hide();
$('#customer-search-fields').show();
// $('#link-job-from-customer').show();
}
/**
* Closes the modal and refreshes the calendar events.
*/
async function closeModal(doRefresh) {
if (doRefresh) {
await sleep(50); //give the server time to insert the new record into the database before requesting it.
calendar.refetchEvents();
}
if (modalAppointmentContext != null){
modalAppointmentContext.clear();
modalAppointmentContext = null;
}
$.post({url: `/remove_appointment_breadcrumb`}); //TODO: This one isn't always working.
$("#job_notify").prop("checked", false); //uncheck the notify checkbox when closing..
document.getElementById('job_notify').value = 'on';
$('#delete_job_request').hide();
$('#set-appointment-color').css('color', "");
}
function deleteCurrentJobContext() {
$.ajax({
url: `/employee_jobs/${modalAppointmentContext._employeeJobId}.json`,
type: "DELETE",
success: function () { //TODO: Difference between deleting job & appointment?
gritterSuccess('Successfully Deleted the Appointment', "You've successfully deleted the appointment", '2500');
closeModal(doRefresh = true);
},
error: showErrorForCalendar,
});
}
function setFullCalendarBranchResource(branchName) {
setTimeout(function () {setBranch(branchName)}, 500);
}
/**
* Expands the "branch" (FullCalendar Resource) of whatever the given branch name was.
* This is so the employee that's logged in only has to look at his current branch's appointments.
* @param {*} branchName
*/
function setBranch(branchName){
$('#calendar .fc-divider .fc-cell-text').each(function (index, area_text_el) {
if (area_text_el.innerText === branchName) {
$(area_text_el).siblings('.fc-expander').click()
}
});
}
/**
* Sleep for the given time period
* @param {*} ms ms to sleep for.
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function updateAppointmentDetailsFromFullCalendarInfo(info){
let appointmentId = info.event.id;
let appointmentStart = moment(info.event.start).format().substring(0,16);
let appointmentEnd = moment(info.event.end).format().substring(0,16);
let employeeForAppointment = info.event.getResources()[0].id;
updateAppointmentDetails(appointmentId, appointmentStart, appointmentEnd, employeeForAppointment);
}
/**
* This function is used when dragging the appointments around on fullcalendar.
* This function hits the server with a "PUT" to update the appointments with the new employee and/or appointment times.
* @param {*} employeeJobId
* @param {*} startTime
* @param {*} endTime
* @param {*} employeeId
*/
function updateAppointmentDetails(employeeJobId, startTime, endTime, employeeId){ //How many times is this getting called? Twice.
//with this approach, we're requesting the appointment data from the server and just changing what we need. This is easier than implementing a sparse update route
FCTool.getEmployeeJobDetailsAjax(employeeJobId, //This does a GET, and then a PUT. Is there a way to do a PUT without a GET?
function(data){
data["time_start"] = startTime;
data["time_end"] = endTime;
delete data["id"]; //throws a 500 error if we keep this "id" in the data.
data["employee_id"] = employeeId //change the employee if it's dragged onto a different employee.
$.ajax({
method: "PUT",
url: `/employee_jobs/${employeeJobId}.json`,
data: {employee_job: data},
success(){
gritterSuccess("Success", "Successfully updated appointment")
},
error(){
gritterError("Error", "Couldn't update appointment")
}
})
});
}
/**
* This takes our select 2 input box, iterates over all entries, and adds input fields to the form
* so that an appointment gets created for each employee that was selected.
*/
function pushAllSelectedEmployeesIntoFormData(){
let numberOfEmployeesAssignedToThisJob = $('#employees_select').find(':selected').length;
$('#job_employee_jobs_attributes_0_employee_id').val($('#employees_select').find(':selected')[0].value); //Set the first selected employee in the first form value.
if (numberOfEmployeesAssignedToThisJob > 1){
for (var i = 1; i < numberOfEmployeesAssignedToThisJob; i++){
let currentEmployeeId = $('#employees_select').find(':selected')[i].value;
appendEmployeeAppointment(i, currentEmployeeId);
}
}
else {
removeExtraEmployeeFormData();
}
}
/**
* This removes any extra employees from our form data, to be called after the form has been submitted.
*/
function removeExtraEmployeeFormData(){
$('.added-employee-job-input').remove();
}
/**
* This is where we add the extra fields so we can create multiple appointments for different employees at the same time.
* @param {*} index
* @param {*} employeeId
*/
function appendEmployeeAppointment(index, employeeId){//https://stackoverflow.com/questions/2530635/jquery-add-additional-parameters-on-submit-not-ajax
var employeeIdInput = $("")
.attr("type", "hidden")
.attr("class", "added-employee-job-input")
.attr("name", `job[employee_jobs_attributes][${index}][employee_id]`).val(employeeId)
.attr("id", `job_employee_jobs_attributes_${index}_employee_id`);
var timeStartInput = $("")
.attr("type", "hidden")
.attr("class", "added-employee-job-input")
.attr("name", `job[employee_jobs_attributes][${index}][time_start]`).val($('#job_employee_jobs_attributes_0_time_start').val())
.attr("id", `job_employee_jobs_attributes_${index}_time_start`).val($('#job_employee_jobs_attributes_0_time_start').val());
var timeEndInput = $("")
.attr("type", "hidden")
.attr("class", "added-employee-job-input")
.attr("name", `job[employee_jobs_attributes][${index}][time_end]`).val($('#job_employee_jobs_attributes_0_time_end').val())
.attr("id", `job_employee_jobs_attributes_${index}_time_end`).val($('#job_employee_jobs_attributes_0_time_end').val());
$('#job-form').append(employeeIdInput);
$('#job-form').append(timeStartInput);
$('#job-form').append(timeEndInput);
}
/**
* Checks the job_form modal for errors
*/
function checkForErrorsBeforeSubmitting(){ //TODO: Add red borders around the fields that have issues.
//check number of employees
var error = false;
let numberOfEmployeesAssignedToThisJob = $('#employees_select').find(':selected').length;
if (numberOfEmployeesAssignedToThisJob == 0){
gritterError("Error", "Select employee(s) to schedule or modify appointments", 10000);
error = true;
}
//check dates are set
if ($('#job_employee_jobs_attributes_0_time_start').val() == ""){
$("#submit_button_outer").removeClass("is-loading");
$('#submitnewjob').prop('disabled', false);
gritterError("Error", "You must have an appointment start time", 10000);
error = true;
}
if ($('#job_employee_jobs_attributes_0_time_end').val() == ""){
$("#submit_button_outer").removeClass("is-loading");
$('#submitnewjob').prop('disabled', false);
gritterError("Error", "You must have an appointment end time", 10000);
error = true;
}
if (error){
throw ("User tried to submit a job & appintment with errors");
}
}
/**
* This is used when a job request is clicked rather than dragged onto the calendar
* @param {*} event
*/
function jobRequestClicked(event){
let eventEl = event.currentTarget;
let data = {"notes": eventEl.dataset.notes,
"request_id": eventEl.dataset.requestid,
"customer_id": eventEl.dataset.customerid,
"location_id": eventEl.dataset.customerid,
"customer_first_name": eventEl.dataset.firstname,
"customer_last_name": eventEl.dataset.lastname,
"customer_phone": eventEl.dataset.phone,
"customer_email": eventEl.dataset.email,
"street_address": eventEl.dataset.streetaddress,
"city": eventEl.dataset.city,
"zip_code": eventEl.dataset.zip,
"state": eventEl.dataset.state,
"marketing_source_id": eventEl.dataset.marketingsourceid,
"marketing_source_name" :eventEl.dataset.marketingsourcename}
$('#delete_job_request').show();
$('#delete_job_request').unbind().click(function(){
if (confirm("Are you sure you want to delete?")){
$.ajax({
url: `/job_requests/${eventEl.dataset.requestid}.json`,
type: 'DELETE',
success: function(result) {
gritterSuccess("Success", "Successfully Deleted Request");
closeModal();
eventEl.remove();
},
error: function(result){
gritterError("Error", "Sorry, we couldn't delete this job request right now.");
}
});
}
});
setNewAppointmentModal(currentEmployeeId,currentEmployeeName, null, null, data);
}
class @AppointmentModal2 # This javascript object represents the data in the job modal form. It maps logic between the visible UI state and data for a given appointment.
constructor: (employeeId, employeeName, startDate, endDate, employeeJobId, jobId, data) ->
@_data = {}
if (data?)
@_data = data
@_employeeJobId = employeeJobId
@_jobId = jobId
@_startDate = startDate
@_endDate = endDate
@_employeeId = employeeId
@_employeeName = employeeName
@_isNewJob = false
#Toggles the Modal with the 'job-modal' id.
toggleModal: () =>
$("#job-modal").modal("toggle");
$("#assign-roster-toggle").hide(); #TODO: We don't want to support rosters anymore. They're confusing and not used. We should gut all the roster code for the front-end and backend.
setInvoicesSection = ->
if @_data["invoices"] && @_data["invoices"][0] # If there's at least one invoice for this job, show invoice section
$('#invoice-section').show();
for q in [0, 1, 2] # Show the first three invoices from a job..
if @_data["invoices"][q]
$("#invoice-section-#{q}").show()
invoiceId = @_data["invoices"][q]["id"]
$("#invoice-#{q}-title").text(@_data["invoices"][q]["short_title"])
$("#invoice-#{q}-title").attr("href": "/invoices/#{invoiceId}")
$("#invoice-#{q}-total").text(formatCurrency(@_data["invoices"][q]["total_price"]))
if parseFloat(@_data["invoices"][q]["amount_due"]) > 0
$("#invoice-#{q}-total").attr("style": "color:red")
else
$("#invoice-#{q}-total").attr("style": "color:green")
$("#invoice-#{q}-send").attr("onclick": "emailInvoiceToCustomer(#{invoiceId})")
$("#invoice-#{q}-stripe").attr("href": "/invoices/#{invoiceId}/pay")
$("#invoice-#{q}-mark-as-paid").attr("onclick": "markAsPaid(#{invoiceId})")
else
$("#invoice-section-#{q}").hide()
else
$('#invoice-section').hide(); # If there are no invoices for this job, don't show the invoices section.
setInvoicesCustomerSection = ->
if @_data["invoices_for_customer"] && @_data["invoices_for_customer"][0] # If there's at least one invoice for this job, show invoice section
$('#invoice-section-customer-more-url').attr("href", "/customers/#{@_data["customer_id"]}")
$('#invoice-section-customer').show();
for q in [0,1,2] # Show the first three invoices from a job..
if @_data["invoices_for_customer"][q] && @_data["invoices_for_customer"][q]["job_id"] != @_jobId
$("#invoice-section-customer-#{q}").show()
invoiceId = @_data["invoices_for_customer"][q]["id"]
$("#invoice-customer-#{q}-title").text(@_data["invoices_for_customer"][q]["short_title"])
$("#invoice-customer-#{q}-title").attr("href": "/invoices/#{invoiceId}")
$("#invoice-customer-#{q}-total").text(formatCurrency(@_data["invoices_for_customer"][q]["total_price"]))
if parseFloat(@_data["invoices_for_customer"][q]["amount_due"]) > 0
$("#invoice-customer-#{q}-total").attr("style": "color:red")
else
$("#invoice-customer-#{q}-total").attr("style": "color:green")
$("#invoice-customer-#{q}-send").attr("onclick": "emailInvoiceToCustomer(#{invoiceId})")
$("#invoice-customer-#{q}-stripe").attr("href": "/invoices/#{invoiceId}/pay")
$("#invoice-customer-#{q}-mark-as-paid").attr("onclick": "markAsPaid(#{invoiceId})")
else
$("#invoice-section-customer-#{q}").hide()
else
$('#invoice-section-customer-more-url').attr("href", "/#")
$('#invoice-section-customer').hide(); # If there are no invoices for this job, don't show the invoices section.
setInvoicesSectionPublic: ->
setInvoicesSection.call(this)
#Sets all the modal's forms with our current internal variables
fillInModalFormsWithCurrentVariables: (setEmployeeSelect2Option = true) =>
#All involved models ID's
$('#job_id').val(@_jobId);
$('#job_customer_id').val(@_data["customer_id"]);
$('#job_location_id').val(@_data["location_id"]);
$('#job_customer_contact_id').val(@_data["customer_contact_id"]);
$('#job_job_request_id').val(@_data["request_id"]) #if request_id is set in our data, then we set that value on the form. This is so when it's submitted to the server, it can associate the job with the request, and we know that the request was turned into a job.
#Customer Name:
$('#job_customer_attributes_id').val(@_data["customer_id"])
$('#job_customer_attributes_first_name').val(@_data["customer_first_name"])
$('#job_customer_attributes_last_name').val(@_data["customer_last_name"])
$('#job_customer_attributes_phone').val(formatPhoneNumber(@_data["customer_phone"]))
$('#job_customer_attributes_email').val(@_data["customer_email"])
$('#job_location_attributes_id').val(@_data["location_id"])
$('#job_location_attributes_customer_id').val(@_data["customer_id"])
$('#job_location_attributes_street_address').val(@_data["street_address"])
$('#job_location_attributes_city').val(@_data["city"])
$('#job_location_attributes_zip_code').val(@_data["zip_code"])
$('#job_location_attributes_state').val(@_data["state"])
if (@_data["location"])
$("#job_location_attributes_default_location").prop("checked", @_data["location"]["is_default_location"]);
if (@_data["owner_id"]) #If the owner isn 't set, just set it to the given employee.
SetSelect2Option('owner_id', @_data["owner_full_name"], @_data["owner_id"]);
SetSelect2Option('appointment-type-search', @_data["appointment_type_name"], @_data["appointment_type_id"]);
#Appointment fields:
SetFlatpickr('job_employee_jobs_attributes_0_time_start', @_startDate);
SetFlatpickr('job_employee_jobs_attributes_0_time_end', @_endDate);
$("#job_employee_jobs_attributes_0_employee_id").val(@_employeeId);
$("#job_employee_jobs_attributes_0_id").val(@_employeeJobId);
#Job attributes:
$("#job_notes").val(@_data["notes"]);
$("#job_job_status").val("job_status");
SetSelect2Option('employees_select', @_employeeName, @_employeeId) if setEmployeeSelect2Option
SetSelect2Option('marketing_source_id', @_data["marketing_source_name"], @_data["marketing_source_id"])
$('#job_upload_form').attr("action", "/jobs/#{@_jobId}/image.json")
$("#apt-cal-job-number").html("#{@_data["job_title"]}")
$("#form-load-status").val("true")
#How to get the appointment color...???
if (@_data["color"] != null)
$('#set-appointment-color').css('color', @_data["color"]); #This is setting the paint brush button color to the right one
$("#job_employee_jobs_attributes_0_appointment_color_id").val(@_data["color_id"]);
setInvoicesSection.call(this)
setInvoicesCustomerSection.call(this)
evaluateUrlVariablesForTextTemplate: (url) ->
# { = '%7B', { is url encoded to '%7B', so we need to match on '%7B' because here we're evaluating the already encoded url
# } = '%7D' same as above
#Names
evaluatedUrl = url.replace('%7B%7Bemployee%7D%7D', encodeURIComponent(@_data["employee_first"]))
evaluatedUrl = evaluatedUrl.replace('%7B%7Bcustomer_first_name%7D%7D', encodeURIComponent(@_data["customer_first_name"])) #TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
evaluatedUrl = evaluatedUrl.replace('%7B%7Bcontact_first_name%7D%7D', encodeURIComponent(@_data["customer_first_name"])) #TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
#Location
evaluatedUrl = evaluatedUrl.replace('%7B%7Blocation%7D%7D', encodeURIComponent(@_data["display_address"]))
#Dates
evaluatedUrl = evaluatedUrl.replace('%7B%7Bstart_time%7D%7D', encodeURIComponent(evaluateStartTimeForTextMessage(@_data["time_start"]))) #TODO: Having to convert this to UTC to get the correct local time. Bad sign, appears we're storing the local time in our database as if it were UTC time.
evaluatedUrl = evaluatedUrl.replace('%7B%7Breadable_start_time%7D%7D', encodeURIComponent(evaluateReadableStartTimeFromAppointment(@_startDate)))
evaluatedUrl = evaluatedUrl.replace('%7B%7Bend_time%7D%7D', encodeURIComponent(evaluateEndTimeForTextMessage(@_data["time_end"])))
evaluatedUrl = evaluatedUrl.replace('%7B%7Bday%7D%7D', encodeURIComponent(evaluateDayForTextMessage(@_data["time_start"])))
evaluatedUrl = evaluatedUrl.replace('%7B%7Bdate%7D%7D', encodeURIComponent(evaluateDateForTextMessage(@_data["time_start"])))
return evaluatedUrl
clear: () ->
$("#job-modal").modal("hide");
$('#customer-name-search').val(null).trigger('change'); #wipe all fields (causes failed AJAX Customer GET request from the CustomerSearchSelect class, that's ok)
$('#marketing_sources_id').val(null).trigger('change');
$('#owner_id').val(null).trigger('change');
$('#appointment-type-search').val(null).trigger('change');
$('#employees_select').val(null).empty().trigger('change');
$(".form-control").val(""); # wipe all fields
$('.modal-backdrop').remove(); #Removes all remaining modal backdrops https://stackoverflow.com/a/11544860
# Remove all invoices from the table..
$('#invoice-section').hide(); # If there are no invoices for this job, don't show the invoices section.
$('#submitnewjob').off('click')
$("#submit_button_outer").removeClass("is-loading");
$('#job_appointment_close').removeClass('is-loading');
$('#submitnewjob').prop('disabled', false);
$('.cancel-with-autosave').prop('disabled', false);
getCurrentJobId: () ->
return @_jobId;
getCurrentCustomerId: () ->
return @_data["customer_id"];
getEmployeeId: () ->
return @_employeeId;
getData: () ->
return @_data
class AppointmentModal {
constructor(employeeId, employeeName, startDate, endDate, employeeJobId, jobId, data) {
this._data = data ? data : {};
this._employeeJobId = employeeJobId;
this._jobId = jobId;
this._startDate = startDate;
this._endDate = endDate;
this._employeeId = employeeId;
this._employeeName = employeeName;
this._isNewJob = false;
this.toggleModal = this.toggleModal.bind(this);
this.fillInModalFormsWithCurrentVariables = this.fillInModalFormsWithCurrentVariables.bind(this);
}
toggleModal() {
$("#job-modal").modal("toggle");
$("#assign-roster-toggle").hide();
}
setInvoicesSection() {
if (this._data["invoices"] && this._data["invoices"][0]) {
$('#invoice-section').show();
for (let q = 0; q < 3; q++) {
if (this._data["invoices"][q]) {
$("#invoice-section-" + q).show();
const invoiceId = this._data["invoices"][q]["id"];
$("#invoice-" + q + "-title").text(this._data["invoices"][q]["short_title"]);
$("#invoice-" + q + "-title").attr("href", "/invoices/" + invoiceId);
$("#invoice-" + q + "-total").text(formatCurrency(this._data["invoices"][q]["total_price"]));
if (parseFloat(this._data["invoices"][q]["amount_due"]) > 0) {
$("#invoice-" + q + "-total").attr("style", "color:red");
} else {
$("#invoice-" + q + "-total").attr("style", "color:green");
}
$("#invoice-" + q + "-send").attr("onclick", "emailInvoiceToCustomer(" + invoiceId + ")");
$("#invoice-" + q + "-stripe").attr("href", "/invoices/" + invoiceId + "/pay");
$("#invoice-" + q + "-mark-as-paid").attr("onclick", "markAsPaid(" + invoiceId + ")");
} else {
$("#invoice-section-" + q).hide();
}
}
} else {
$('#invoice-section').hide();
}
}
setInvoicesCustomerSection() {
if (this._data["invoices_for_customer"] && this._data["invoices_for_customer"][0]) {
$('#invoice-section-customer-more-url').attr("href", "/customers/" + this._data["customer_id"]);
$('#invoice-section-customer').show();
for (let q = 0; q < 3; q++) {
if (this._data["invoices_for_customer"][q] && this._data["invoices_for_customer"][q]["job_id"] !== this._jobId) {
$("#invoice-section-customer-" + q).show();
const invoiceId = this._data["invoices_for_customer"][q]["id"];
$("#invoice-customer-" + q + "-title").text(this._data["invoices_for_customer"][q]["short_title"]);
$("#invoice-customer-" + q + "-title").attr("href", "/invoices/" + invoiceId);
$("#invoice-customer-" + q + "-total").text(formatCurrency(this._data["invoices_for_customer"][q]["total_price"]));
if (parseFloat(this._data["invoices_for_customer"][q]["amount_due"]) > 0) {
$("#invoice-customer-" + q + "-total").attr("style", "color:red");
} else {
$("#invoice-customer-" + q + "-total").attr("style", "color:green");
}
$("#invoice-customer-" + q + "-send").attr("onclick", "emailInvoiceToCustomer(" + invoiceId + ")");
$("#invoice-customer-" + q + "-stripe").attr("href", "/invoices/" + invoiceId + "/pay");
$("#invoice-customer-" + q + "-mark-as-paid").attr("onclick", "markAsPaid(" + invoiceId + ")");
} else {
$("#invoice-section-customer-" + q).hide();
}
}
} else {
$('#invoice-section-customer-more-url').attr("href", "/#");
$('#invoice-section-customer').hide();
}
}
setInvoicesSectionPublic() {
this.setInvoicesSection();
}
fillInModalFormsWithCurrentVariables(setEmployeeSelect2Option = true) {
$('#job_id').val(this._jobId);
$('#job_customer_id').val(this._data["customer_id"]);
$('#job_location_id').val(this._data["location_id"]);
$('#job_customer_contact_id').val(this._data["customer_contact_id"]);
$('#job_job_request_id').val(this._data["request_id"]);
$('#job_customer_attributes_id').val(this._data["customer_id"]);
$('#job_customer_attributes_first_name').val(this._data["customer_first_name"]);
$('#job_customer_attributes_last_name').val(this._data["customer_last_name"]);
$('#job_customer_attributes_phone').val(formatPhoneNumber(this._data["customer_phone"]));
$('#job_customer_attributes_email').val(this._data["customer_email"]);
$('#job_location_attributes_id').val(this._data["location_id"]);
$('#job_location_attributes_customer_id').val(this._data["customer_id"]);
$('#job_location_attributes_street_address').val(this._data["street_address"]);
$('#job_location_attributes_city').val(this._data["city"]);
$('#job_location_attributes_zip_code').val(this._data["zip_code"]);
$('#job_location_attributes_state').val(this._data["state"]);
if (this._data["location"]) {
$("#job_location_attributes_default_location").prop("checked", this._data["location"]["is_default_location"]);
}
if (this._data["owner_id"]) {
SetSelect2Option('owner_id', this._data["owner_full_name"], this._data["owner_id"]);
}
SetSelect2Option('appointment-type-search', this._data["appointment_type_name"], this._data["appointment_type_id"]);
SetFlatpickr('job_employee_jobs_attributes_0_time_start', this._startDate);
SetFlatpickr('job_employee_jobs_attributes_0_time_end', this._endDate);
$("#job_employee_jobs_attributes_0_employee_id").val(this._employeeId);
$("#job_employee_jobs_attributes_0_id").val(this._employeeJobId);
$("#job_notes").val(this._data["notes"]);
$("#job_job_status").val("job_status");
if (setEmployeeSelect2Option) {
SetSelect2Option('employees_select', this._employeeName, this._employeeId);
}
SetSelect2Option('marketing_source_id', this._data["marketing_source_name"], this._data["marketing_source_id"]);
$('#job_upload_form').attr("action", "/jobs/" + this._jobId + "/image.json");
$("#apt-cal-job-number").html("" + this._data["job_title"]);
$("#form-load-status").val("true");
if (this._data["color"] !== null) {
$('#set-appointment-color').css('color', this._data["color"]);
}
$("#job_employee_jobs_attributes_0_appointment_color_id").val(this._data["color_id"]);
this.setInvoicesSection();
this.setInvoicesCustomerSection();
}
evaluateUrlVariablesForTextTemplate(url) {
let evaluatedUrl = url.replace('%7B%7Bemployee%7D%7D', encodeURIComponent(this._data["employee_first"]));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bcustomer_first_name%7D%7D', encodeURIComponent(this._data["customer_first_name"]));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bcontact_first_name%7D%7D', encodeURIComponent(this._data["customer_first_name"]));
evaluatedUrl = evaluatedUrl.replace('%7B%7Blocation%7D%7D', encodeURIComponent(this._data["display_address"]));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bstart_time%7D%7D', encodeURIComponent(evaluateStartTimeForTextMessage(this._data["time_start"])));
evaluatedUrl = evaluatedUrl.replace('%7B%7Breadable_start_time%7D%7D', encodeURIComponent(evaluateReadableStartTimeFromAppointment(this._startDate)));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bend_time%7D%7D', encodeURIComponent(evaluateEndTimeForTextMessage(this._data["time_end"])));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bday%7D%7D', encodeURIComponent(evaluateDayForTextMessage(this._data["time_start"])));
evaluatedUrl = evaluatedUrl.replace('%7B%7Bdate%7D%7D', encodeURIComponent(evaluateDateForTextMessage(this._data["time_start"])));
return evaluatedUrl;
}
clear() {
$("#job-modal").modal("hide");
$('#customer-name-search').val(null).trigger('change');
$('#marketing_sources_id').val(null).trigger('change');
$('#owner_id').val(null).trigger('change');
$('#appointment-type-search').val(null).trigger('change');
$('#employees_select').val(null).empty().trigger('change');
$(".form-control").val("");
$('.modal-backdrop').remove();
$('#invoice-section').hide();
$('#submitnewjob').off('click');
$("#submit_button_outer").removeClass("is-loading");
$('#job_appointment_close').removeClass('is-loading');
$('#submitnewjob').prop('disabled', false);
$('.cancel-with-autosave').prop('disabled', false);
}
getCurrentJobId() {
return this._jobId;
}
getCurrentCustomerId() {
return this._data["customer_id"];
}
getEmployeeId() {
return this._employeeId;
}
getData() {
return this._data;
}
}
class EmployeeJobTypeSelect {
constructor(idOfSelectElement, onSelectionCallback) {
//Setting values for the Job Modal's Contact 'Select2' form
$(`#${idOfSelectElement}`).val(null).trigger('change'); //Empty out the values in contact select dropdown.
return new QuerySelect(idOfSelectElement,
'/employee_job_types.json',
function (params) { return { q: { status_cont: params.term } } },
function (data) {
// Transforms the top-level key of the response object from 'items' to 'results'
return { results: $.map(data, function (val) { return { "id": val.id, "text": val.name } })}
},
onSelectionCallback,
);
}
}
class EmailSelect{
constructor(elementId, url, onSelectCallback){
new QuerySelect(elementId,
url,
null,
null,
onSelectCallback,
{tags:true}
);
}
}
class MarketingSourceSelect {
constructor(idOfSelectElement, onSelectionCallback) {
$(`#${idOfSelectElement}`).val(null).trigger('change'); //Empty out the values in contact select dropdown.
return new QuerySelect(idOfSelectElement, `/marketing_sources.json`, null, function (data) { return { results: $.map(data, function (val) { return { "id": val.id, "text": val.name }})}}, onSelectionCallback)
}
}
/**
* Responsible for setting up the customer search box. Depends on the select2 javascript library.
*/
class CustomerSearchSelect{
/**
* @param {Id of the HTML Select} idOfSelectElement
* @param {Callback function for when a customer is selected} onSelectionCallback
*/
constructor(idOfSelectElement, onSelectionCallback){
return new QuerySelect(idOfSelectElement, '/customer_search', function (params) { return { name: params.term }}, function (data) { return { results: $.map(data, function (val) { return { "id": val.id, "text": val.full_name }})}}, onSelectionCallback)
}
}
class EmployeeSelect { //Used to handle select2 boxes that are searching for Employees.
constructor(idOfSelectElement, onSelectionCallback, options = null) {
return new QuerySelect(idOfSelectElement, '/employees.json',
function(params) {
return {
q: {first_name_or_last_name_cont: params.term}
}
},
function (data) {
return {
results: $.map(data, function (val) {
return { "id": val.id, "text": val.full_name }
})
}
},
onSelectionCallback,
options);
}
}
function SetSelect2Option(select2Id, text, id){
$(`#${select2Id}`).append(new Option(text, id, false, false));
$(`#${select2Id}`).val(id);
$(`#${select2Id}`).trigger('change');
}
function SetSelect2OptionExtraEventArg(select2Id, text, id, extraEventArg){
$(`#${select2Id}`).append(new Option(text, id, false, false));
$(`#${select2Id}`).val(id);
$(`#${select2Id}`).trigger('change', extraEventArg);
}
//https://www.thatsoftwaredude.com/content/9025/creating-functions-dynamically-with-javascript
/**
* This is sort of some crazy, meta stuff -- we're dynamically making a function to avoid code reuse.
* @param {*} ransackAttrSearch
*/
function RansackSearch(ransackAttrSearch){
return new Function('params', 'return {q: {' + ransackAttrSearch + ': params.term}}');
}
function SelectResultsTransform(displayAttr){
return new Function('data',
`return {results: $.map(data, function (val) {
return { "id": val.id, "text": val["${displayAttr}"]}
})}`);
}
function InlineEditCallback(resource, resource_id, attribute, attribute_display_key = null, val = null){
attribute_display_key = attribute_display_key == null ? attribute : attribute_display_key
const isFormNotLoaded = $('#form-load-status').val() == "false";
var update_attr = new function(){
if(isFormNotLoaded || isBlank(resource_id) || ( !isBlank(resource_id) && isBlank(event || val))) {
$("#form-load-status").val("false")
return
}
const selectedId = val || event.target.value;
$.ajax({
method: 'PUT',
url: `/${resource}/${resource_id}/update_attribute.json`,
data: {attribute: `${attribute}`, value: selectedId},
success: function(data){
// gritterSuccess(`Updated ${titleizeAttribute(attribute)}`, `Successfully set ${titleizeAttribute(attribute)} to: ` + data[attribute_display_key]);
},
error: function(data){
for (var key in data.responseJSON) {
// gritterError(`Couldn't Set ${titleizeAttribute(attribute)}`, data.responseJSON[key], "10000");
}
}
});
}
return update_attr
}
function titleizeAttribute(attr){
if (typeof attr != "string")
return attr
return attr.split('_').map(function(val){return val.charAt(0).toUpperCase() + val.slice(1)}).join(' ');
}
class LocationSelect {
constructor(idOfSelectElement, customerId, onSelectionCallback) {
//Setting values for the Job Modal's Location 'Select2' form
$(`#${idOfSelectElement}`).val(null).trigger('change'); //Empty out the values in location select dropdown.
if (customerId == null){
$(`#${idOfSelectElement}`).select2({});
}
else {
return new QuerySelect(idOfSelectElement, `/customer_locations/${customerId}`, null, function (data) {return { results: $.map(data, function (val) { return { "id": val.id, "text": val.display_address }})}}, onSelectionCallback);
}
}
}
/**
* Used for setting up Select2 boxes that do AJAX searches to fetch data from the server, and then performs the selectonCallBack
* when the user selects one of those options from the server.
*/
class QuerySelect{
/**
* Create a QuerySelect for the given element.
* @param {The id of the select element on the page} elementId
* @param {The url that will return JSON for the searched object} urlForSearch
* @param {Transforms the select2 input to the paramaters that the server URL is expecting} searchParamaterCallback
* @param {Transorms the server's response to the select2's expected format} selectionOptionFormattingCallback
* @param {Action to be taken after a user has selected one of the options returned from the server} onSelectionCallback
* @param {Additional options for the select2 element} moreSelect2Options
*/
constructor(elementId, urlForSearch, searchParamaterCallback, selectionOptionFormattingCallback, onSelectionCallback, moreSelect2Options){
var defaultSelect2Options = {
ajax: {
url: urlForSearch,
data: searchParamaterCallback,
processResults: selectionOptionFormattingCallback,
delay: 500 //https://stackoverflow.com/questions/30805457/how-to-delay-select-2-so-that-it-waits-for-some-time-after-user-types-in-data
}
};
var select2Options = Object.assign({}, defaultSelect2Options, moreSelect2Options);
$(`#${elementId}`).select2(select2Options);
$(`#${elementId}`).on('change', onSelectionCallback);
}
}
class CustomerContactSelect {
constructor(idOfSelectElement, customerId, onSelectionCallback) {
//Setting values for the Job Modal's Contact 'Select2' form
$(`#${idOfSelectElement}`).val(null).trigger('change'); //Empty out the values in contact select dropdown.
if (customerId == null){
$(`#${idOfSelectElement}`).select2({});
}
else {
return new QuerySelect(idOfSelectElement, `/get_customer_contacts/${customerId}`, null, function (data) { return { results: $.map(data, function (val) { return { "id": val.id, "text": val.full_name }})}}, onSelectionCallback)
}
}
}
class PublicMarketingSourceSelect {
constructor(idOfSelectElement, businessId, onSelectionCallback) {
$(`#${idOfSelectElement}`).val(null).trigger('change'); //Empty out the values in contact select dropdown.
return new QuerySelect(idOfSelectElement, `/get_marketing_sources/${businessId}`, null, function (data) { return { results: $.map(data, function (val) { return { "id": val.id, "text": val.name }})}}, onSelectionCallback)
}
}
$(function() {
$('#csv-form-field').bind('change', function() {
var fileName = $(this).val();
$("#csv-dummy-field").html(fileName);
});
$("#csv-dummy-field").bind('click', function() {
$("#csv-form-field").click();
});
});
$(document).ready(function(){
$("#new_customer_form").on("ajax:success", function(event){
$('#new_customer_form')[0].reset(); //https://www.formget.com/jquery-reset-form/
$('#new_customer_modal').modal('hide');
gritterSuccess("Added Customer", "Successfully added the customer");
//Need to set customer, location, and contact after a new customer has been successfully created.
});
$("#new_customer_form").on("ajax:error", function(event){
// Was having an issue getting the data and everything out when submitting the form this way, found this page:
// https://stackoverflow.com/questions/43827441/how-to-get-data-from-response-of-event-ajaxerror-in-rails-5-1
var detail = event.detail;
var data = detail[0], status = detail[1], xhr = detail[2];
for (var key in data) {
if (data.hasOwnProperty(key)) {
gritterError(`${key} error`, data[key], 10000);
}
}
});
new QuerySelect('marketing_source_select',
'/marketing_sources.json',
function(params) {return {q: {name_cont: params.term}}},
function (data) { return { results: $.map(data, function (val) { return { "id": val.id, "text": val.name }})}},
function (e){
$('#customer_marketing_source_id').val(e.currentTarget.value);
}
);
$("#closeCustomerModal").bind("click", function(){
$("#new_customer_modal").modal("hide");
});
});
$(function(){
new ContactNewUpdateModalState(
'new_customer_contact_modal',
'new_customer_contact_form',
'submitCustomerContact',
'customer_contacts',
'edit_contact_modal',
'new_contact_modal');
});
$("#closeCustomerContactModal").bind("click", function(){
$("#new_customer_contact_modal").modal("hide");
});
$('.modal').css('overflow-y', 'auto'); //https://stackoverflow.com/a/28077674
function makeOutboundCall(contactId){
$.post({url: `/customer_contacts/${contactId}/call`});
}
function gritterSuccess(title, message, time){
gritterNotification(title, message, getTime(time), "success-notice");
}
function gritterError(title, message, time = null){
gritterNotification(title, message, getTime(time), "error-notice");
}
function gritterNotification(title, message, time, className){
$.gritter.add({
title: title,
text: message,
sticky:false,
class_name: className,
time: time
});
}
/**
* Displays an error to the user that there was a schedule conflict.
* @param {*} data
*/
function showErrorForCalendar(data) {
showError("Couldn't add/update appointment", data);
calendar.refetchEvents();
}
/**
* Displays all the errors from a responseJSON
*/
function showError(title, data){
for (var key in data.responseJSON) {
gritterError(title, data.responseJSON[key], "10000");
}
}
function showErrorForm(event){
var detail = event.detail;
var data = detail[0], status = detail[1], xhr = detail[2];
for (var key in data) {
if (data.hasOwnProperty(key)) {
gritterError(`${key} error`, data[key], 10000);
}
}
}
/**
* Used to set the default time to '2500' if the user doesn't provide time to gritterSuccess or gritterNotification
* @param {*} setTime if null returns '2500', else returns the not null value
*/
function getTime(setTime){
if (setTime == null)
return '2500';
else return setTime;
}
$(function(){
new LocationModalState('new_location_modal',
'new_location_form',
'submitLocation',
'locations',
'edit_location_modal',
'new_location_button');
});
$("#closeLocationModal").bind("click", function(){
$("#new_location_modal").modal("hide");
});
$('.modal').css('overflow-y', 'auto'); //https://stackoverflow.com/a/28077674
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
$(document).ready(function(){
$("#new_employee_job").on("ajax:success", function(event){
gritterSuccess("Added Appointment", "Successfully set the appointment");
clearAppointmentModalForm();
});
$("#new_employee_job").on("ajax:error", function(event){
// Was having an issue getting the data and everything out when submitting the form this way, found this page:
// https://stackoverflow.com/questions/43827441/how-to-get-data-from-response-of-event-ajaxerror-in-rails-5-1
var detail = event.detail;
var data = detail[0], status = detail[1], xhr = detail[2];
$(data).each((function(index){
gritterError(`Appointment Error ${index}`, this, 10000)
}));
});
});
function clearAppointmentModalForm(){
$('#new_job_appointment_modal').modal('hide');
$('#new_employee_job')[0].reset(); //https://www.formget.com/jquery-reset-form/
$('#employee_select').val(null).trigger('change'); //Clear select2 forms
$('#appointment_type').val(null).trigger('change');
}
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
$(document).ready(function () {
var locationSelect = new LocationSelect("customer-location-search", null, function (e) { $('#job_location_id').val(e.currentTarget.value)});
var contactSelect = new CustomerContactSelect("customer-contact-search", null, function (e) { $('#job_customer_contact_id').val(e.currentTarget.value)});
var customerSearch = new CustomerSearchSelect("customer-name-search", function (e, dontSetDefaults) {
contactSelect = new CustomerContactSelect("customer-contact-search", selectedCustomerId, function (e) { $('#job_customer_contact_id').val(e.currentTarget.value)});
locationSelect = new LocationSelect("customer-location-search", selectedCustomerId, function (e) { $('#job_location_id').val(e.currentTarget.value)});
var selectedCustomerId = e.currentTarget.value;
$('#job_customer_id').val($('#customer-name-search').val()); //<%# Controller is expecting this param, so when we set the search field, set the job_customer_id input field as well %>
$('#job_customer_attributes_id').val($('#customer-name-search').val());
if (!selectedCustomerId) //Preventing GET http://localhost:3000/customers/.json 404 (Not Found) error that happens if the selectedCustomerId is nil or empty.
return;
$.get({url: `/customers/${selectedCustomerId}.json`,
success: function(data){
$('#job_customer_attributes_id').val(data["id"])
$('#job_customer_attributes_first_name').val(data["first_name"])
$('#job_customer_attributes_last_name').val(data["last_name"])
$('#job_customer_attributes_email').val(data["email"])
$('#job_customer_attributes_phone').val(formatPhoneNumber(data["phone"]))
if (data["default_location"]){
$("#job_location_id").val(data["default_location"]["id"])
$('#job_location_attributes_id').val(data["default_location"]["id"])
$('#job_location_attributes_customer_id').val(data["id"])
$('#job_location_attributes_street_address').val(data["default_location"]["street_address"])
$('#job_location_attributes_city').val(data["default_location"]["city"])
$('#job_location_attributes_zip_code').val(data["default_location"]["zip_code"])
$('#job_location_attributes_state').val(data["default_location"]["state"])
}
if (data["marketing_source"] != "Unknown")
SetSelect2Option('marketing_source_id', data["marketing_source_name"], data["marketing_source_id"]);
if (typeof(FCTool) != "undefined"){
//After a search is complete, set the job link menu.
FCTool.setLocationMenu(data); //set the locations to choose from
// FCTool.setJobLinkMenu(data["id"]); //this is triggering when searching for a customer on a new job...
}
},
error: function(data){
console.log(`Couldn't fetch /customers/${selectedCustomerId}.json`);
}});
});
new MarketingSourceSelect("marketing_source_id", function(evt){
$("#job_customer_attributes_marketing_source_id").val(evt.currentTarget.value);
});
//Setup the Owner ID Select Box
var employeeSelect = new EmployeeSelect("owner_id", function(e){
$('#job_owner_id').val(e.currentTarget.value);
});
new EmployeeJobTypeSelect("appointment-type-search", function (evt){
$('#job_employee_jobs_attributes_0_employee_job_type_id').val(evt.currentTarget.value);
});
new MarketingSourceSelect("marketing_source_id", function(evt){
$("#job_customer_attributes_marketing_source_id").val(evt.currentTarget.value);
InlineEditCallback('customers', modalAppointmentContext.getCurrentCustomerId(), 'marketing_source_id', 'marketing_source');
});
const flatpickrOptions = {
enableTime: true,
altInput: true,
altFormat: 'l F j, h:i K',
minTime: "6:00",
maxTime: "22:00",
minuteIncrement: 15,
onChange: function(selectedDates, dateStr, instance){
// if end date is before start date, abort.
let employeeJobId = $('#job_employee_jobs_attributes_0_id').val();
let weAreEditingExistingAppointment = !(isBlank(employeeJobId)) //if employee job id is blank, then we're not editing an existing appointment
if (weAreEditingExistingAppointment){
InlineEditCallback('employee_jobs', $('#job_employee_jobs_attributes_0_id').val(), 'time_end', null, $('#job_employee_jobs_attributes_0_time_end').val());
}
}
}
const timeStartFlatpickrOptions = {
enableTime: true,
altInput: true,
altFormat: 'l F j, h:i K',
minTime: "6:00",
maxTime: "22:00",
minuteIncrement: 15,
onChange: function(selectedDates, dateStr, instance){
s = selectedDates[0];//set the end time to the same as start time except 2 hours ahead
s.setTime(s.getTime() + (2*60*60*1000))
SetFlatpickr('job_employee_jobs_attributes_0_time_end', s)
let endTimeDateStr = moment(s).format('YYYY-MM-DD HH:mm');
let employeeJobId = $('#job_employee_jobs_attributes_0_id').val();
let weAreEditingExistingAppointment = !(isBlank(employeeJobId)) //if employee job id is blank, then we're not editing an existing appointment
if (weAreEditingExistingAppointment){ //we don't want to try and update an the time if the appointment has been made yet (this happens when we're on a 'new' job modal and haven't saved it yet.)
$.ajax({
method: 'PUT',
url: `/employee_jobs/${employeeJobId}/update_start_time_end_time.json`,
data: {time_start: dateStr, time_end: endTimeDateStr},
success: function(data){
gritterSuccess(`Updated Appointment Time`, `Successfully set appointment time`);
},
error: function(data){
gritterError(`Couldn't Set Appointment Time`, "Couldn't set appointment time", "10000");
}
});
}
}
}
$('#job_employee_jobs_attributes_0_time_start').flatpickr(timeStartFlatpickrOptions);
$('#job_employee_jobs_attributes_0_time_end').flatpickr(flatpickrOptions);
});
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/
/*!
FullCalendar Google Calendar Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { createPlugin, refineProps, requestJson, addDays } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
// TODO: expose somehow
var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars';
var STANDARD_PROPS = {
url: String,
googleCalendarApiKey: String,
googleCalendarId: String,
data: null
};
var eventSourceDef = {
parseMeta: function (raw) {
if (typeof raw === 'string') {
raw = { url: raw };
}
if (typeof raw === 'object') {
var standardProps = refineProps(raw, STANDARD_PROPS);
if (!standardProps.googleCalendarId && standardProps.url) {
standardProps.googleCalendarId = parseGoogleCalendarId(standardProps.url);
}
delete standardProps.url;
if (standardProps.googleCalendarId) {
return standardProps;
}
}
return null;
},
fetch: function (arg, onSuccess, onFailure) {
var calendar = arg.calendar;
var meta = arg.eventSource.meta;
var apiKey = meta.googleCalendarApiKey || calendar.opt('googleCalendarApiKey');
if (!apiKey) {
onFailure({
message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/'
});
}
else {
var url = buildUrl(meta);
var requestParams_1 = buildRequestParams(arg.range, apiKey, meta.data, calendar.dateEnv);
requestJson('GET', url, requestParams_1, function (body, xhr) {
if (body.error) {
onFailure({
message: 'Google Calendar API: ' + body.error.message,
errors: body.error.errors,
xhr: xhr
});
}
else {
onSuccess({
rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone),
xhr: xhr
});
}
}, function (message, xhr) {
onFailure({ message: message, xhr: xhr });
});
}
}
};
function parseGoogleCalendarId(url) {
var match;
// detect if the ID was specified as a single string.
// will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars.
if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) {
return url;
}
else if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) ||
(match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))) {
return decodeURIComponent(match[1]);
}
}
function buildUrl(meta) {
return API_BASE + '/' + encodeURIComponent(meta.googleCalendarId) + '/events';
}
function buildRequestParams(range, apiKey, extraParams, dateEnv) {
var params;
var startStr;
var endStr;
if (dateEnv.canComputeOffset) {
// strings will naturally have offsets, which GCal needs
startStr = dateEnv.formatIso(range.start);
endStr = dateEnv.formatIso(range.end);
}
else {
// when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day
// from the UTC day-start to guarantee we're getting all the events
// (start/end will be UTC-coerced dates, so toISOString is okay)
startStr = addDays(range.start, -1).toISOString();
endStr = addDays(range.end, 1).toISOString();
}
params = __assign({}, (extraParams || {}), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 });
if (dateEnv.timeZone !== 'local') {
params.timeZone = dateEnv.timeZone;
}
return params;
}
function gcalItemsToRawEventDefs(items, gcalTimezone) {
return items.map(function (item) {
return gcalItemToRawEventDef(item, gcalTimezone);
});
}
function gcalItemToRawEventDef(item, gcalTimezone) {
var url = item.htmlLink || null;
// make the URLs for each event show times in the correct timezone
if (url && gcalTimezone) {
url = injectQsComponent(url, 'ctz=' + gcalTimezone);
}
return {
id: item.id,
title: item.summary,
start: item.start.dateTime || item.start.date,
end: item.end.dateTime || item.end.date,
url: url,
location: item.location,
description: item.description
};
}
// Injects a string like "arg=value" into the querystring of a URL
// TODO: move to a general util file?
function injectQsComponent(url, component) {
// inject it after the querystring but before the fragment
return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) {
return (qs ? qs + '&' : '?') + component + hash;
});
}
var main = createPlugin({
eventSourceDefs: [eventSourceDef]
});
export default main;
/*!
FullCalendar Google Calendar Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarGoogleCalendar = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
// TODO: expose somehow
var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars';
var STANDARD_PROPS = {
url: String,
googleCalendarApiKey: String,
googleCalendarId: String,
data: null
};
var eventSourceDef = {
parseMeta: function (raw) {
if (typeof raw === 'string') {
raw = { url: raw };
}
if (typeof raw === 'object') {
var standardProps = core.refineProps(raw, STANDARD_PROPS);
if (!standardProps.googleCalendarId && standardProps.url) {
standardProps.googleCalendarId = parseGoogleCalendarId(standardProps.url);
}
delete standardProps.url;
if (standardProps.googleCalendarId) {
return standardProps;
}
}
return null;
},
fetch: function (arg, onSuccess, onFailure) {
var calendar = arg.calendar;
var meta = arg.eventSource.meta;
var apiKey = meta.googleCalendarApiKey || calendar.opt('googleCalendarApiKey');
if (!apiKey) {
onFailure({
message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/'
});
}
else {
var url = buildUrl(meta);
var requestParams_1 = buildRequestParams(arg.range, apiKey, meta.data, calendar.dateEnv);
core.requestJson('GET', url, requestParams_1, function (body, xhr) {
if (body.error) {
onFailure({
message: 'Google Calendar API: ' + body.error.message,
errors: body.error.errors,
xhr: xhr
});
}
else {
onSuccess({
rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone),
xhr: xhr
});
}
}, function (message, xhr) {
onFailure({ message: message, xhr: xhr });
});
}
}
};
function parseGoogleCalendarId(url) {
var match;
// detect if the ID was specified as a single string.
// will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars.
if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) {
return url;
}
else if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) ||
(match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))) {
return decodeURIComponent(match[1]);
}
}
function buildUrl(meta) {
return API_BASE + '/' + encodeURIComponent(meta.googleCalendarId) + '/events';
}
function buildRequestParams(range, apiKey, extraParams, dateEnv) {
var params;
var startStr;
var endStr;
if (dateEnv.canComputeOffset) {
// strings will naturally have offsets, which GCal needs
startStr = dateEnv.formatIso(range.start);
endStr = dateEnv.formatIso(range.end);
}
else {
// when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day
// from the UTC day-start to guarantee we're getting all the events
// (start/end will be UTC-coerced dates, so toISOString is okay)
startStr = core.addDays(range.start, -1).toISOString();
endStr = core.addDays(range.end, 1).toISOString();
}
params = __assign({}, (extraParams || {}), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 });
if (dateEnv.timeZone !== 'local') {
params.timeZone = dateEnv.timeZone;
}
return params;
}
function gcalItemsToRawEventDefs(items, gcalTimezone) {
return items.map(function (item) {
return gcalItemToRawEventDef(item, gcalTimezone);
});
}
function gcalItemToRawEventDef(item, gcalTimezone) {
var url = item.htmlLink || null;
// make the URLs for each event show times in the correct timezone
if (url && gcalTimezone) {
url = injectQsComponent(url, 'ctz=' + gcalTimezone);
}
return {
id: item.id,
title: item.summary,
start: item.start.dateTime || item.start.date,
end: item.end.dateTime || item.end.date,
url: url,
location: item.location,
description: item.description
};
}
// Injects a string like "arg=value" into the querystring of a URL
// TODO: move to a general util file?
function injectQsComponent(url, component) {
// inject it after the querystring but before the fragment
return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) {
return (qs ? qs + '&' : '?') + component + hash;
});
}
var main = core.createPlugin({
eventSourceDefs: [eventSourceDef]
});
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// main.d.ts
declare module '@fullcalendar/google-calendar' {
module '@fullcalendar/core' {
interface OptionsInput {
googleCalendarApiKey?: string;
}
}
module '@fullcalendar/core/structs/event-source' {
interface ExtendedEventSourceInput {
googleCalendarApiKey?: string;
googleCalendarId?: string;
}
}
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar Google Calendar Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],r):r((e=e||self).FullCalendarGoogleCalendar={},e.FullCalendar)}(this,function(e,r){"use strict";var t=function(){return(t=Object.assign||function(e){for(var r,t=1,n=arguments.length;t
/*!
FullCalendar RRule Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { rrulestr, RRule } from 'rrule';
import { createPlugin, refineProps, createDuration } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var EVENT_DEF_PROPS = {
rrule: null,
duration: createDuration
};
var recurring = {
parse: function (rawEvent, leftoverProps, dateEnv) {
if (rawEvent.rrule != null) {
var props = refineProps(rawEvent, EVENT_DEF_PROPS, {}, leftoverProps);
var parsed = parseRRule(props.rrule, dateEnv);
if (parsed) {
return {
typeData: parsed.rrule,
allDayGuess: parsed.allDayGuess,
duration: props.duration
};
}
}
return null;
},
expand: function (rrule, framingRange) {
// we WANT an inclusive start and in exclusive end, but the js rrule lib will only do either BOTH
// inclusive or BOTH exclusive, which is stupid: https://github.com/jakubroztocil/rrule/issues/84
// Workaround: make inclusive, which will generate extra occurences, and then trim.
return rrule.between(framingRange.start, framingRange.end, true)
.filter(function (date) {
return date.valueOf() < framingRange.end.valueOf();
});
}
};
var main = createPlugin({
recurringTypes: [recurring]
});
function parseRRule(input, dateEnv) {
var allDayGuess = null;
var rrule;
if (typeof input === 'string') {
rrule = rrulestr(input);
}
else if (typeof input === 'object' && input) { // non-null object
var refined = __assign({}, input); // copy
if (typeof refined.dtstart === 'string') {
var dtstartMeta = dateEnv.createMarkerMeta(refined.dtstart);
if (dtstartMeta) {
refined.dtstart = dtstartMeta.marker;
allDayGuess = dtstartMeta.isTimeUnspecified;
}
else {
delete refined.dtstart;
}
}
if (typeof refined.until === 'string') {
refined.until = dateEnv.createMarker(refined.until);
}
if (refined.freq != null) {
refined.freq = convertConstant(refined.freq);
}
if (refined.wkst != null) {
refined.wkst = convertConstant(refined.wkst);
}
else {
refined.wkst = (dateEnv.weekDow - 1 + 7) % 7; // convert Sunday-first to Monday-first
}
if (refined.byweekday != null) {
refined.byweekday = convertConstants(refined.byweekday); // the plural version
}
rrule = new RRule(refined);
}
if (rrule) {
return { rrule: rrule, allDayGuess: allDayGuess };
}
return null;
}
function convertConstants(input) {
if (Array.isArray(input)) {
return input.map(convertConstant);
}
return convertConstant(input);
}
function convertConstant(input) {
if (typeof input === 'string') {
return RRule[input.toUpperCase()];
}
return input;
}
export default main;
/*!
FullCalendar RRule Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('rrule'), require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', 'rrule', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarRrule = {}, global.rrule, global.FullCalendar));
}(this, function (exports, rrule, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var EVENT_DEF_PROPS = {
rrule: null,
duration: core.createDuration
};
var recurring = {
parse: function (rawEvent, leftoverProps, dateEnv) {
if (rawEvent.rrule != null) {
var props = core.refineProps(rawEvent, EVENT_DEF_PROPS, {}, leftoverProps);
var parsed = parseRRule(props.rrule, dateEnv);
if (parsed) {
return {
typeData: parsed.rrule,
allDayGuess: parsed.allDayGuess,
duration: props.duration
};
}
}
return null;
},
expand: function (rrule, framingRange) {
// we WANT an inclusive start and in exclusive end, but the js rrule lib will only do either BOTH
// inclusive or BOTH exclusive, which is stupid: https://github.com/jakubroztocil/rrule/issues/84
// Workaround: make inclusive, which will generate extra occurences, and then trim.
return rrule.between(framingRange.start, framingRange.end, true)
.filter(function (date) {
return date.valueOf() < framingRange.end.valueOf();
});
}
};
var main = core.createPlugin({
recurringTypes: [recurring]
});
function parseRRule(input, dateEnv) {
var allDayGuess = null;
var rrule$1;
if (typeof input === 'string') {
rrule$1 = rrule.rrulestr(input);
}
else if (typeof input === 'object' && input) { // non-null object
var refined = __assign({}, input); // copy
if (typeof refined.dtstart === 'string') {
var dtstartMeta = dateEnv.createMarkerMeta(refined.dtstart);
if (dtstartMeta) {
refined.dtstart = dtstartMeta.marker;
allDayGuess = dtstartMeta.isTimeUnspecified;
}
else {
delete refined.dtstart;
}
}
if (typeof refined.until === 'string') {
refined.until = dateEnv.createMarker(refined.until);
}
if (refined.freq != null) {
refined.freq = convertConstant(refined.freq);
}
if (refined.wkst != null) {
refined.wkst = convertConstant(refined.wkst);
}
else {
refined.wkst = (dateEnv.weekDow - 1 + 7) % 7; // convert Sunday-first to Monday-first
}
if (refined.byweekday != null) {
refined.byweekday = convertConstants(refined.byweekday); // the plural version
}
rrule$1 = new rrule.RRule(refined);
}
if (rrule$1) {
return { rrule: rrule$1, allDayGuess: allDayGuess };
}
return null;
}
function convertConstants(input) {
if (Array.isArray(input)) {
return input.map(convertConstant);
}
return convertConstant(input);
}
function convertConstant(input) {
if (typeof input === 'string') {
return rrule.RRule[input.toUpperCase()];
}
return input;
}
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/rrule' {
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar RRule Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("rrule"),require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","rrule","@fullcalendar/core"],r):r((e=e||self).FullCalendarRrule={},e.rrule,e.FullCalendar)}(this,function(e,r,t){"use strict";var n=function(){return(n=Object.assign||function(e){for(var r,t=1,n=arguments.length;t
/*!
FullCalendar Interaction Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { config, elementClosest, EmitterMixin, applyStyle, whenTransitionDone, removeElement, ScrollController, ElementScrollController, computeInnerRect, WindowScrollController, preventSelection, preventContextMenu, allowSelection, allowContextMenu, ElementDragging, computeRect, getClippingParents, pointInsideRect, isDateSpansEqual, constrainPoint, intersectRects, getRectCenter, diffPoints, mapHash, rangeContainsRange, interactionSettingsToStore, Interaction, enableCursor, disableCursor, compareNumbers, getElSeg, getRelevantEvents, EventApi, createEmptyEventStore, applyMutationToEventStore, interactionSettingsStore, startOfDay, diffDates, createDuration, eventTupleToStore, isInteractionValid, parseDragMeta, elementMatches, parseEventDef, createEventInstance, globalDefaults, createPlugin } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
config.touchMouseIgnoreWait = 500;
var ignoreMouseDepth = 0;
var listenerCnt = 0;
var isWindowTouchMoveCancelled = false;
/*
Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
Tracks when the pointer "drags" on a certain element, meaning down+move+up.
Also, tracks if there was touch-scrolling.
Also, can prevent touch-scrolling from happening.
Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
emits:
- pointerdown
- pointermove
- pointerup
*/
var PointerDragging = /** @class */ (function () {
function PointerDragging(containerEl) {
var _this = this;
this.subjectEl = null;
this.downEl = null;
// options that can be directly assigned by caller
this.selector = ''; // will cause subjectEl in all emitted events to be this element
this.handleSelector = '';
this.shouldIgnoreMove = false;
this.shouldWatchScroll = true; // for simulating pointermove on scroll
// internal states
this.isDragging = false;
this.isTouchDragging = false;
this.wasTouchScroll = false;
// Mouse
// ----------------------------------------------------------------------------------------------------
this.handleMouseDown = function (ev) {
if (!_this.shouldIgnoreMouse() &&
isPrimaryMouseButton(ev) &&
_this.tryStart(ev)) {
var pev = _this.createEventFromMouse(ev, true);
_this.emitter.trigger('pointerdown', pev);
_this.initScrollWatch(pev);
if (!_this.shouldIgnoreMove) {
document.addEventListener('mousemove', _this.handleMouseMove);
}
document.addEventListener('mouseup', _this.handleMouseUp);
}
};
this.handleMouseMove = function (ev) {
var pev = _this.createEventFromMouse(ev);
_this.recordCoords(pev);
_this.emitter.trigger('pointermove', pev);
};
this.handleMouseUp = function (ev) {
document.removeEventListener('mousemove', _this.handleMouseMove);
document.removeEventListener('mouseup', _this.handleMouseUp);
_this.emitter.trigger('pointerup', _this.createEventFromMouse(ev));
_this.cleanup(); // call last so that pointerup has access to props
};
// Touch
// ----------------------------------------------------------------------------------------------------
this.handleTouchStart = function (ev) {
if (_this.tryStart(ev)) {
_this.isTouchDragging = true;
var pev = _this.createEventFromTouch(ev, true);
_this.emitter.trigger('pointerdown', pev);
_this.initScrollWatch(pev);
// unlike mouse, need to attach to target, not document
// https://stackoverflow.com/a/45760014
var target = ev.target;
if (!_this.shouldIgnoreMove) {
target.addEventListener('touchmove', _this.handleTouchMove);
}
target.addEventListener('touchend', _this.handleTouchEnd);
target.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end
// attach a handler to get called when ANY scroll action happens on the page.
// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
// http://stackoverflow.com/a/32954565/96342
window.addEventListener('scroll', _this.handleTouchScroll, true // useCapture
);
}
};
this.handleTouchMove = function (ev) {
var pev = _this.createEventFromTouch(ev);
_this.recordCoords(pev);
_this.emitter.trigger('pointermove', pev);
};
this.handleTouchEnd = function (ev) {
if (_this.isDragging) { // done to guard against touchend followed by touchcancel
var target = ev.target;
target.removeEventListener('touchmove', _this.handleTouchMove);
target.removeEventListener('touchend', _this.handleTouchEnd);
target.removeEventListener('touchcancel', _this.handleTouchEnd);
window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true
_this.emitter.trigger('pointerup', _this.createEventFromTouch(ev));
_this.cleanup(); // call last so that pointerup has access to props
_this.isTouchDragging = false;
startIgnoringMouse();
}
};
this.handleTouchScroll = function () {
_this.wasTouchScroll = true;
};
this.handleScroll = function (ev) {
if (!_this.shouldIgnoreMove) {
var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX;
var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY;
_this.emitter.trigger('pointermove', {
origEvent: ev,
isTouch: _this.isTouchDragging,
subjectEl: _this.subjectEl,
pageX: pageX,
pageY: pageY,
deltaX: pageX - _this.origPageX,
deltaY: pageY - _this.origPageY
});
}
};
this.containerEl = containerEl;
this.emitter = new EmitterMixin();
containerEl.addEventListener('mousedown', this.handleMouseDown);
containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
listenerCreated();
}
PointerDragging.prototype.destroy = function () {
this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
listenerDestroyed();
};
PointerDragging.prototype.tryStart = function (ev) {
var subjectEl = this.querySubjectEl(ev);
var downEl = ev.target;
if (subjectEl &&
(!this.handleSelector || elementClosest(downEl, this.handleSelector))) {
this.subjectEl = subjectEl;
this.downEl = downEl;
this.isDragging = true; // do this first so cancelTouchScroll will work
this.wasTouchScroll = false;
return true;
}
return false;
};
PointerDragging.prototype.cleanup = function () {
isWindowTouchMoveCancelled = false;
this.isDragging = false;
this.subjectEl = null;
this.downEl = null;
// keep wasTouchScroll around for later access
this.destroyScrollWatch();
};
PointerDragging.prototype.querySubjectEl = function (ev) {
if (this.selector) {
return elementClosest(ev.target, this.selector);
}
else {
return this.containerEl;
}
};
PointerDragging.prototype.shouldIgnoreMouse = function () {
return ignoreMouseDepth || this.isTouchDragging;
};
// can be called by user of this class, to cancel touch-based scrolling for the current drag
PointerDragging.prototype.cancelTouchScroll = function () {
if (this.isDragging) {
isWindowTouchMoveCancelled = true;
}
};
// Scrolling that simulates pointermoves
// ----------------------------------------------------------------------------------------------------
PointerDragging.prototype.initScrollWatch = function (ev) {
if (this.shouldWatchScroll) {
this.recordCoords(ev);
window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
}
};
PointerDragging.prototype.recordCoords = function (ev) {
if (this.shouldWatchScroll) {
this.prevPageX = ev.pageX;
this.prevPageY = ev.pageY;
this.prevScrollX = window.pageXOffset;
this.prevScrollY = window.pageYOffset;
}
};
PointerDragging.prototype.destroyScrollWatch = function () {
if (this.shouldWatchScroll) {
window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
}
};
// Event Normalization
// ----------------------------------------------------------------------------------------------------
PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) {
var deltaX = 0;
var deltaY = 0;
// TODO: repeat code
if (isFirst) {
this.origPageX = ev.pageX;
this.origPageY = ev.pageY;
}
else {
deltaX = ev.pageX - this.origPageX;
deltaY = ev.pageY - this.origPageY;
}
return {
origEvent: ev,
isTouch: false,
subjectEl: this.subjectEl,
pageX: ev.pageX,
pageY: ev.pageY,
deltaX: deltaX,
deltaY: deltaY
};
};
PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) {
var touches = ev.touches;
var pageX;
var pageY;
var deltaX = 0;
var deltaY = 0;
// if touch coords available, prefer,
// because FF would give bad ev.pageX ev.pageY
if (touches && touches.length) {
pageX = touches[0].pageX;
pageY = touches[0].pageY;
}
else {
pageX = ev.pageX;
pageY = ev.pageY;
}
// TODO: repeat code
if (isFirst) {
this.origPageX = pageX;
this.origPageY = pageY;
}
else {
deltaX = pageX - this.origPageX;
deltaY = pageY - this.origPageY;
}
return {
origEvent: ev,
isTouch: true,
subjectEl: this.subjectEl,
pageX: pageX,
pageY: pageY,
deltaX: deltaX,
deltaY: deltaY
};
};
return PointerDragging;
}());
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
function isPrimaryMouseButton(ev) {
return ev.button === 0 && !ev.ctrlKey;
}
// Ignoring fake mouse events generated by touch
// ----------------------------------------------------------------------------------------------------
function startIgnoringMouse() {
ignoreMouseDepth++;
setTimeout(function () {
ignoreMouseDepth--;
}, config.touchMouseIgnoreWait);
}
// We want to attach touchmove as early as possible for Safari
// ----------------------------------------------------------------------------------------------------
function listenerCreated() {
if (!(listenerCnt++)) {
window.addEventListener('touchmove', onWindowTouchMove, { passive: false });
}
}
function listenerDestroyed() {
if (!(--listenerCnt)) {
window.removeEventListener('touchmove', onWindowTouchMove, { passive: false });
}
}
function onWindowTouchMove(ev) {
if (isWindowTouchMoveCancelled) {
ev.preventDefault();
}
}
/*
An effect in which an element follows the movement of a pointer across the screen.
The moving element is a clone of some other element.
Must call start + handleMove + stop.
*/
var ElementMirror = /** @class */ (function () {
function ElementMirror() {
this.isVisible = false; // must be explicitly enabled
this.sourceEl = null;
this.mirrorEl = null;
this.sourceElRect = null; // screen coords relative to viewport
// options that can be set directly by caller
this.parentNode = document.body;
this.zIndex = 9999;
this.revertDuration = 0;
}
ElementMirror.prototype.start = function (sourceEl, pageX, pageY) {
this.sourceEl = sourceEl;
this.sourceElRect = this.sourceEl.getBoundingClientRect();
this.origScreenX = pageX - window.pageXOffset;
this.origScreenY = pageY - window.pageYOffset;
this.deltaX = 0;
this.deltaY = 0;
this.updateElPosition();
};
ElementMirror.prototype.handleMove = function (pageX, pageY) {
this.deltaX = (pageX - window.pageXOffset) - this.origScreenX;
this.deltaY = (pageY - window.pageYOffset) - this.origScreenY;
this.updateElPosition();
};
// can be called before start
ElementMirror.prototype.setIsVisible = function (bool) {
if (bool) {
if (!this.isVisible) {
if (this.mirrorEl) {
this.mirrorEl.style.display = '';
}
this.isVisible = bool; // needs to happen before updateElPosition
this.updateElPosition(); // because was not updating the position while invisible
}
}
else {
if (this.isVisible) {
if (this.mirrorEl) {
this.mirrorEl.style.display = 'none';
}
this.isVisible = bool;
}
}
};
// always async
ElementMirror.prototype.stop = function (needsRevertAnimation, callback) {
var _this = this;
var done = function () {
_this.cleanup();
callback();
};
if (needsRevertAnimation &&
this.mirrorEl &&
this.isVisible &&
this.revertDuration && // if 0, transition won't work
(this.deltaX || this.deltaY) // if same coords, transition won't work
) {
this.doRevertAnimation(done, this.revertDuration);
}
else {
setTimeout(done, 0);
}
};
ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) {
var mirrorEl = this.mirrorEl;
var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened
mirrorEl.style.transition =
'top ' + revertDuration + 'ms,' +
'left ' + revertDuration + 'ms';
applyStyle(mirrorEl, {
left: finalSourceElRect.left,
top: finalSourceElRect.top
});
whenTransitionDone(mirrorEl, function () {
mirrorEl.style.transition = '';
callback();
});
};
ElementMirror.prototype.cleanup = function () {
if (this.mirrorEl) {
removeElement(this.mirrorEl);
this.mirrorEl = null;
}
this.sourceEl = null;
};
ElementMirror.prototype.updateElPosition = function () {
if (this.sourceEl && this.isVisible) {
applyStyle(this.getMirrorEl(), {
left: this.sourceElRect.left + this.deltaX,
top: this.sourceElRect.top + this.deltaY
});
}
};
ElementMirror.prototype.getMirrorEl = function () {
var sourceElRect = this.sourceElRect;
var mirrorEl = this.mirrorEl;
if (!mirrorEl) {
mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true
// we don't want long taps or any mouse interaction causing selection/menus.
// would use preventSelection(), but that prevents selectstart, causing problems.
mirrorEl.classList.add('fc-unselectable');
mirrorEl.classList.add('fc-dragging');
applyStyle(mirrorEl, {
position: 'fixed',
zIndex: this.zIndex,
visibility: '',
boxSizing: 'border-box',
width: sourceElRect.right - sourceElRect.left,
height: sourceElRect.bottom - sourceElRect.top,
right: 'auto',
bottom: 'auto',
margin: 0
});
this.parentNode.appendChild(mirrorEl);
}
return mirrorEl;
};
return ElementMirror;
}());
/*
Is a cache for a given element's scroll information (all the info that ScrollController stores)
in addition the "client rectangle" of the element.. the area within the scrollbars.
The cache can be in one of two modes:
- doesListening:false - ignores when the container is scrolled by someone else
- doesListening:true - watch for scrolling and update the cache
*/
var ScrollGeomCache = /** @class */ (function (_super) {
__extends(ScrollGeomCache, _super);
function ScrollGeomCache(scrollController, doesListening) {
var _this = _super.call(this) || this;
_this.handleScroll = function () {
_this.scrollTop = _this.scrollController.getScrollTop();
_this.scrollLeft = _this.scrollController.getScrollLeft();
_this.handleScrollChange();
};
_this.scrollController = scrollController;
_this.doesListening = doesListening;
_this.scrollTop = _this.origScrollTop = scrollController.getScrollTop();
_this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft();
_this.scrollWidth = scrollController.getScrollWidth();
_this.scrollHeight = scrollController.getScrollHeight();
_this.clientWidth = scrollController.getClientWidth();
_this.clientHeight = scrollController.getClientHeight();
_this.clientRect = _this.computeClientRect(); // do last in case it needs cached values
if (_this.doesListening) {
_this.getEventTarget().addEventListener('scroll', _this.handleScroll);
}
return _this;
}
ScrollGeomCache.prototype.destroy = function () {
if (this.doesListening) {
this.getEventTarget().removeEventListener('scroll', this.handleScroll);
}
};
ScrollGeomCache.prototype.getScrollTop = function () {
return this.scrollTop;
};
ScrollGeomCache.prototype.getScrollLeft = function () {
return this.scrollLeft;
};
ScrollGeomCache.prototype.setScrollTop = function (top) {
this.scrollController.setScrollTop(top);
if (!this.doesListening) {
// we are not relying on the element to normalize out-of-bounds scroll values
// so we need to sanitize ourselves
this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0);
this.handleScrollChange();
}
};
ScrollGeomCache.prototype.setScrollLeft = function (top) {
this.scrollController.setScrollLeft(top);
if (!this.doesListening) {
// we are not relying on the element to normalize out-of-bounds scroll values
// so we need to sanitize ourselves
this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0);
this.handleScrollChange();
}
};
ScrollGeomCache.prototype.getClientWidth = function () {
return this.clientWidth;
};
ScrollGeomCache.prototype.getClientHeight = function () {
return this.clientHeight;
};
ScrollGeomCache.prototype.getScrollWidth = function () {
return this.scrollWidth;
};
ScrollGeomCache.prototype.getScrollHeight = function () {
return this.scrollHeight;
};
ScrollGeomCache.prototype.handleScrollChange = function () {
};
return ScrollGeomCache;
}(ScrollController));
var ElementScrollGeomCache = /** @class */ (function (_super) {
__extends(ElementScrollGeomCache, _super);
function ElementScrollGeomCache(el, doesListening) {
return _super.call(this, new ElementScrollController(el), doesListening) || this;
}
ElementScrollGeomCache.prototype.getEventTarget = function () {
return this.scrollController.el;
};
ElementScrollGeomCache.prototype.computeClientRect = function () {
return computeInnerRect(this.scrollController.el);
};
return ElementScrollGeomCache;
}(ScrollGeomCache));
var WindowScrollGeomCache = /** @class */ (function (_super) {
__extends(WindowScrollGeomCache, _super);
function WindowScrollGeomCache(doesListening) {
return _super.call(this, new WindowScrollController(), doesListening) || this;
}
WindowScrollGeomCache.prototype.getEventTarget = function () {
return window;
};
WindowScrollGeomCache.prototype.computeClientRect = function () {
return {
left: this.scrollLeft,
right: this.scrollLeft + this.clientWidth,
top: this.scrollTop,
bottom: this.scrollTop + this.clientHeight
};
};
// the window is the only scroll object that changes it's rectangle relative
// to the document's topleft as it scrolls
WindowScrollGeomCache.prototype.handleScrollChange = function () {
this.clientRect = this.computeClientRect();
};
return WindowScrollGeomCache;
}(ScrollGeomCache));
// If available we are using native "performance" API instead of "Date"
// Read more about it on MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
var getTime = typeof performance === 'function' ? performance.now : Date.now;
/*
For a pointer interaction, automatically scrolls certain scroll containers when the pointer
approaches the edge.
The caller must call start + handleMove + stop.
*/
var AutoScroller = /** @class */ (function () {
function AutoScroller() {
var _this = this;
// options that can be set by caller
this.isEnabled = true;
this.scrollQuery = [window, '.fc-scroller'];
this.edgeThreshold = 50; // pixels
this.maxVelocity = 300; // pixels per second
// internal state
this.pointerScreenX = null;
this.pointerScreenY = null;
this.isAnimating = false;
this.scrollCaches = null;
// protect against the initial pointerdown being too close to an edge and starting the scroll
this.everMovedUp = false;
this.everMovedDown = false;
this.everMovedLeft = false;
this.everMovedRight = false;
this.animate = function () {
if (_this.isAnimating) { // wasn't cancelled between animation calls
var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset);
if (edge) {
var now = getTime();
_this.handleSide(edge, (now - _this.msSinceRequest) / 1000);
_this.requestAnimation(now);
}
else {
_this.isAnimating = false; // will stop animation
}
}
};
}
AutoScroller.prototype.start = function (pageX, pageY) {
if (this.isEnabled) {
this.scrollCaches = this.buildCaches();
this.pointerScreenX = null;
this.pointerScreenY = null;
this.everMovedUp = false;
this.everMovedDown = false;
this.everMovedLeft = false;
this.everMovedRight = false;
this.handleMove(pageX, pageY);
}
};
AutoScroller.prototype.handleMove = function (pageX, pageY) {
if (this.isEnabled) {
var pointerScreenX = pageX - window.pageXOffset;
var pointerScreenY = pageY - window.pageYOffset;
var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY;
var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX;
if (yDelta < 0) {
this.everMovedUp = true;
}
else if (yDelta > 0) {
this.everMovedDown = true;
}
if (xDelta < 0) {
this.everMovedLeft = true;
}
else if (xDelta > 0) {
this.everMovedRight = true;
}
this.pointerScreenX = pointerScreenX;
this.pointerScreenY = pointerScreenY;
if (!this.isAnimating) {
this.isAnimating = true;
this.requestAnimation(getTime());
}
}
};
AutoScroller.prototype.stop = function () {
if (this.isEnabled) {
this.isAnimating = false; // will stop animation
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
scrollCache.destroy();
}
this.scrollCaches = null;
}
};
AutoScroller.prototype.requestAnimation = function (now) {
this.msSinceRequest = now;
requestAnimationFrame(this.animate);
};
AutoScroller.prototype.handleSide = function (edge, seconds) {
var scrollCache = edge.scrollCache;
var edgeThreshold = this.edgeThreshold;
var invDistance = edgeThreshold - edge.distance;
var velocity = // the closer to the edge, the faster we scroll
(invDistance * invDistance) / (edgeThreshold * edgeThreshold) * // quadratic
this.maxVelocity * seconds;
var sign = 1;
switch (edge.name) {
case 'left':
sign = -1;
// falls through
case 'right':
scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign);
break;
case 'top':
sign = -1;
// falls through
case 'bottom':
scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign);
break;
}
};
// left/top are relative to document topleft
AutoScroller.prototype.computeBestEdge = function (left, top) {
var edgeThreshold = this.edgeThreshold;
var bestSide = null;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
var rect = scrollCache.clientRect;
var leftDist = left - rect.left;
var rightDist = rect.right - left;
var topDist = top - rect.top;
var bottomDist = rect.bottom - top;
// completely within the rect?
if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
(!bestSide || bestSide.distance > topDist)) {
bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist };
}
if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
(!bestSide || bestSide.distance > bottomDist)) {
bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist };
}
if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
(!bestSide || bestSide.distance > leftDist)) {
bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist };
}
if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
(!bestSide || bestSide.distance > rightDist)) {
bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist };
}
}
}
return bestSide;
};
AutoScroller.prototype.buildCaches = function () {
return this.queryScrollEls().map(function (el) {
if (el === window) {
return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls
}
else {
return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls
}
});
};
AutoScroller.prototype.queryScrollEls = function () {
var els = [];
for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) {
var query = _a[_i];
if (typeof query === 'object') {
els.push(query);
}
else {
els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query)));
}
}
return els;
};
return AutoScroller;
}());
/*
Monitors dragging on an element. Has a number of high-level features:
- minimum distance required before dragging
- minimum wait time ("delay") before dragging
- a mirror element that follows the pointer
*/
var FeaturefulElementDragging = /** @class */ (function (_super) {
__extends(FeaturefulElementDragging, _super);
function FeaturefulElementDragging(containerEl) {
var _this = _super.call(this, containerEl) || this;
// options that can be directly set by caller
// the caller can also set the PointerDragging's options as well
_this.delay = null;
_this.minDistance = 0;
_this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
_this.mirrorNeedsRevert = false;
_this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
_this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
_this.isDelayEnded = false;
_this.isDistanceSurpassed = false;
_this.delayTimeoutId = null;
_this.onPointerDown = function (ev) {
if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going
_this.isInteracting = true;
_this.isDelayEnded = false;
_this.isDistanceSurpassed = false;
preventSelection(document.body);
preventContextMenu(document.body);
// prevent links from being visited if there's an eventual drag.
// also prevents selection in older browsers (maybe?).
// not necessary for touch, besides, browser would complain about passiveness.
if (!ev.isTouch) {
ev.origEvent.preventDefault();
}
_this.emitter.trigger('pointerdown', ev);
if (!_this.pointer.shouldIgnoreMove) {
// actions related to initiating dragstart+dragmove+dragend...
_this.mirror.setIsVisible(false); // reset. caller must set-visible
_this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down
_this.startDelay(ev);
if (!_this.minDistance) {
_this.handleDistanceSurpassed(ev);
}
}
}
};
_this.onPointerMove = function (ev) {
if (_this.isInteracting) { // if false, still waiting for previous drag's revert
_this.emitter.trigger('pointermove', ev);
if (!_this.isDistanceSurpassed) {
var minDistance = _this.minDistance;
var distanceSq = void 0; // current distance from the origin, squared
var deltaX = ev.deltaX, deltaY = ev.deltaY;
distanceSq = deltaX * deltaX + deltaY * deltaY;
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
_this.handleDistanceSurpassed(ev);
}
}
if (_this.isDragging) {
// a real pointer move? (not one simulated by scrolling)
if (ev.origEvent.type !== 'scroll') {
_this.mirror.handleMove(ev.pageX, ev.pageY);
_this.autoScroller.handleMove(ev.pageX, ev.pageY);
}
_this.emitter.trigger('dragmove', ev);
}
}
};
_this.onPointerUp = function (ev) {
if (_this.isInteracting) { // if false, still waiting for previous drag's revert
_this.isInteracting = false;
allowSelection(document.body);
allowContextMenu(document.body);
_this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert
if (_this.isDragging) {
_this.autoScroller.stop();
_this.tryStopDrag(ev); // which will stop the mirror
}
if (_this.delayTimeoutId) {
clearTimeout(_this.delayTimeoutId);
_this.delayTimeoutId = null;
}
}
};
var pointer = _this.pointer = new PointerDragging(containerEl);
pointer.emitter.on('pointerdown', _this.onPointerDown);
pointer.emitter.on('pointermove', _this.onPointerMove);
pointer.emitter.on('pointerup', _this.onPointerUp);
_this.mirror = new ElementMirror();
_this.autoScroller = new AutoScroller();
return _this;
}
FeaturefulElementDragging.prototype.destroy = function () {
this.pointer.destroy();
};
FeaturefulElementDragging.prototype.startDelay = function (ev) {
var _this = this;
if (typeof this.delay === 'number') {
this.delayTimeoutId = setTimeout(function () {
_this.delayTimeoutId = null;
_this.handleDelayEnd(ev);
}, this.delay); // not assignable to number!
}
else {
this.handleDelayEnd(ev);
}
};
FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) {
this.isDelayEnded = true;
this.tryStartDrag(ev);
};
FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) {
this.isDistanceSurpassed = true;
this.tryStartDrag(ev);
};
FeaturefulElementDragging.prototype.tryStartDrag = function (ev) {
if (this.isDelayEnded && this.isDistanceSurpassed) {
if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
this.isDragging = true;
this.mirrorNeedsRevert = false;
this.autoScroller.start(ev.pageX, ev.pageY);
this.emitter.trigger('dragstart', ev);
if (this.touchScrollAllowed === false) {
this.pointer.cancelTouchScroll();
}
}
}
};
FeaturefulElementDragging.prototype.tryStopDrag = function (ev) {
// .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
// that come from the document to fire beforehand. much more convenient this way.
this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev) // bound with args
);
};
FeaturefulElementDragging.prototype.stopDrag = function (ev) {
this.isDragging = false;
this.emitter.trigger('dragend', ev);
};
// fill in the implementations...
FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) {
this.pointer.shouldIgnoreMove = bool;
};
FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) {
this.mirror.setIsVisible(bool);
};
FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) {
this.mirrorNeedsRevert = bool;
};
FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) {
this.autoScroller.isEnabled = bool;
};
return FeaturefulElementDragging;
}(ElementDragging));
/*
When this class is instantiated, it records the offset of an element (relative to the document topleft),
and continues to monitor scrolling, updating the cached coordinates if it needs to.
Does not access the DOM after instantiation, so highly performant.
Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
and an determine if a given point is inside the combined clipping rectangle.
*/
var OffsetTracker = /** @class */ (function () {
function OffsetTracker(el) {
this.origRect = computeRect(el);
// will work fine for divs that have overflow:hidden
this.scrollCaches = getClippingParents(el).map(function (el) {
return new ElementScrollGeomCache(el, true); // listen=true
});
}
OffsetTracker.prototype.destroy = function () {
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
scrollCache.destroy();
}
};
OffsetTracker.prototype.computeLeft = function () {
var left = this.origRect.left;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
left += scrollCache.origScrollLeft - scrollCache.getScrollLeft();
}
return left;
};
OffsetTracker.prototype.computeTop = function () {
var top = this.origRect.top;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
top += scrollCache.origScrollTop - scrollCache.getScrollTop();
}
return top;
};
OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) {
var point = { left: pageX, top: pageY };
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
if (!isIgnoredClipping(scrollCache.getEventTarget()) &&
!pointInsideRect(point, scrollCache.clientRect)) {
return false;
}
}
return true;
};
return OffsetTracker;
}());
// certain clipping containers should never constrain interactions, like and
// https://github.com/fullcalendar/fullcalendar/issues/3615
function isIgnoredClipping(node) {
var tagName = node.tagName;
return tagName === 'HTML' || tagName === 'BODY';
}
/*
Tracks movement over multiple droppable areas (aka "hits")
that exist in one or more DateComponents.
Relies on an existing draggable.
emits:
- pointerdown
- dragstart
- hitchange - fires initially, even if not over a hit
- pointerup
- (hitchange - again, to null, if ended over a hit)
- dragend
*/
var HitDragging = /** @class */ (function () {
function HitDragging(dragging, droppableStore) {
var _this = this;
// options that can be set by caller
this.useSubjectCenter = false;
this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
this.initialHit = null;
this.movingHit = null;
this.finalHit = null; // won't ever be populated if shouldIgnoreMove
this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
_this.initialHit = null;
_this.movingHit = null;
_this.finalHit = null;
_this.prepareHits();
_this.processFirstCoord(ev);
if (_this.initialHit || !_this.requireInitial) {
dragging.setIgnoreMove(false);
_this.emitter.trigger('pointerdown', ev); // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
}
else {
dragging.setIgnoreMove(true);
}
};
this.handleDragStart = function (ev) {
_this.emitter.trigger('dragstart', ev);
_this.handleMove(ev, true); // force = fire even if initially null
};
this.handleDragMove = function (ev) {
_this.emitter.trigger('dragmove', ev);
_this.handleMove(ev);
};
this.handlePointerUp = function (ev) {
_this.releaseHits();
_this.emitter.trigger('pointerup', ev);
};
this.handleDragEnd = function (ev) {
if (_this.movingHit) {
_this.emitter.trigger('hitupdate', null, true, ev);
}
_this.finalHit = _this.movingHit;
_this.movingHit = null;
_this.emitter.trigger('dragend', ev);
};
this.droppableStore = droppableStore;
dragging.emitter.on('pointerdown', this.handlePointerDown);
dragging.emitter.on('dragstart', this.handleDragStart);
dragging.emitter.on('dragmove', this.handleDragMove);
dragging.emitter.on('pointerup', this.handlePointerUp);
dragging.emitter.on('dragend', this.handleDragEnd);
this.dragging = dragging;
this.emitter = new EmitterMixin();
}
// sets initialHit
// sets coordAdjust
HitDragging.prototype.processFirstCoord = function (ev) {
var origPoint = { left: ev.pageX, top: ev.pageY };
var adjustedPoint = origPoint;
var subjectEl = ev.subjectEl;
var subjectRect;
if (subjectEl !== document) {
subjectRect = computeRect(subjectEl);
adjustedPoint = constrainPoint(adjustedPoint, subjectRect);
}
var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top);
if (initialHit) {
if (this.useSubjectCenter && subjectRect) {
var slicedSubjectRect = intersectRects(subjectRect, initialHit.rect);
if (slicedSubjectRect) {
adjustedPoint = getRectCenter(slicedSubjectRect);
}
}
this.coordAdjust = diffPoints(adjustedPoint, origPoint);
}
else {
this.coordAdjust = { left: 0, top: 0 };
}
};
HitDragging.prototype.handleMove = function (ev, forceHandle) {
var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top);
if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
this.movingHit = hit;
this.emitter.trigger('hitupdate', hit, false, ev);
}
};
HitDragging.prototype.prepareHits = function () {
this.offsetTrackers = mapHash(this.droppableStore, function (interactionSettings) {
interactionSettings.component.buildPositionCaches();
return new OffsetTracker(interactionSettings.el);
});
};
HitDragging.prototype.releaseHits = function () {
var offsetTrackers = this.offsetTrackers;
for (var id in offsetTrackers) {
offsetTrackers[id].destroy();
}
this.offsetTrackers = {};
};
HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) {
var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers;
var bestHit = null;
for (var id in droppableStore) {
var component = droppableStore[id].component;
var offsetTracker = offsetTrackers[id];
if (offsetTracker.isWithinClipping(offsetLeft, offsetTop)) {
var originLeft = offsetTracker.computeLeft();
var originTop = offsetTracker.computeTop();
var positionLeft = offsetLeft - originLeft;
var positionTop = offsetTop - originTop;
var origRect = offsetTracker.origRect;
var width = origRect.right - origRect.left;
var height = origRect.bottom - origRect.top;
if (
// must be within the element's bounds
positionLeft >= 0 && positionLeft < width &&
positionTop >= 0 && positionTop < height) {
var hit = component.queryHit(positionLeft, positionTop, width, height);
if (hit &&
(
// make sure the hit is within activeRange, meaning it's not a deal cell
!component.props.dateProfile || // hack for DayTile
rangeContainsRange(component.props.dateProfile.activeRange, hit.dateSpan.range)) &&
(!bestHit || hit.layer > bestHit.layer)) {
// TODO: better way to re-orient rectangle
hit.rect.left += originLeft;
hit.rect.right += originLeft;
hit.rect.top += originTop;
hit.rect.bottom += originTop;
bestHit = hit;
}
}
}
}
return bestHit;
};
return HitDragging;
}());
function isHitsEqual(hit0, hit1) {
if (!hit0 && !hit1) {
return true;
}
if (Boolean(hit0) !== Boolean(hit1)) {
return false;
}
return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
}
/*
Monitors when the user clicks on a specific date/time of a component.
A pointerdown+pointerup on the same "hit" constitutes a click.
*/
var DateClicking = /** @class */ (function (_super) {
__extends(DateClicking, _super);
function DateClicking(settings) {
var _this = _super.call(this, settings) || this;
_this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
// do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired
dragging.setIgnoreMove(!_this.component.isValidDateDownEl(dragging.pointer.downEl));
};
// won't even fire if moving was ignored
_this.handleDragEnd = function (ev) {
var component = _this.component;
var pointer = _this.dragging.pointer;
if (!pointer.wasTouchScroll) {
var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit;
if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
component.calendar.triggerDateClick(initialHit.dateSpan, initialHit.dayEl, component.view, ev.origEvent);
}
}
};
var component = settings.component;
// we DO want to watch pointer moves because otherwise finalHit won't get populated
_this.dragging = new FeaturefulElementDragging(component.el);
_this.dragging.autoScroller.isEnabled = false;
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
DateClicking.prototype.destroy = function () {
this.dragging.destroy();
};
return DateClicking;
}(Interaction));
/*
Tracks when the user selects a portion of time of a component,
constituted by a drag over date cells, with a possible delay at the beginning of the drag.
*/
var DateSelecting = /** @class */ (function (_super) {
__extends(DateSelecting, _super);
function DateSelecting(settings) {
var _this = _super.call(this, settings) || this;
_this.dragSelection = null;
_this.handlePointerDown = function (ev) {
var _a = _this, component = _a.component, dragging = _a.dragging;
var canSelect = component.opt('selectable') &&
component.isValidDateDownEl(ev.origEvent.target);
// don't bother to watch expensive moves if component won't do selection
dragging.setIgnoreMove(!canSelect);
// if touch, require user to hold down
dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null;
};
_this.handleDragStart = function (ev) {
_this.component.calendar.unselect(ev); // unselect previous selections
};
_this.handleHitUpdate = function (hit, isFinal) {
var calendar = _this.component.calendar;
var dragSelection = null;
var isInvalid = false;
if (hit) {
dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, calendar.pluginSystem.hooks.dateSelectionTransformers);
if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) {
isInvalid = true;
dragSelection = null;
}
}
if (dragSelection) {
calendar.dispatch({ type: 'SELECT_DATES', selection: dragSelection });
}
else if (!isFinal) { // only unselect if moved away while dragging
calendar.dispatch({ type: 'UNSELECT_DATES' });
}
if (!isInvalid) {
enableCursor();
}
else {
disableCursor();
}
if (!isFinal) {
_this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging
}
};
_this.handlePointerUp = function (pev) {
if (_this.dragSelection) {
// selection is already rendered, so just need to report selection
_this.component.calendar.triggerDateSelect(_this.dragSelection, pev);
_this.dragSelection = null;
}
};
var component = settings.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.touchScrollAllowed = false;
dragging.minDistance = component.opt('selectMinDistance') || 0;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('pointerup', _this.handlePointerUp);
return _this;
}
DateSelecting.prototype.destroy = function () {
this.dragging.destroy();
};
return DateSelecting;
}(Interaction));
function getComponentTouchDelay(component) {
var delay = component.opt('selectLongPressDelay');
if (delay == null) {
delay = component.opt('longPressDelay');
}
return delay;
}
function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
var dateSpan0 = hit0.dateSpan;
var dateSpan1 = hit1.dateSpan;
var ms = [
dateSpan0.range.start,
dateSpan0.range.end,
dateSpan1.range.start,
dateSpan1.range.end
];
ms.sort(compareNumbers);
var props = {};
for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) {
var transformer = dateSelectionTransformers_1[_i];
var res = transformer(hit0, hit1);
if (res === false) {
return null;
}
else if (res) {
__assign(props, res);
}
}
props.range = { start: ms[0], end: ms[3] };
props.allDay = dateSpan0.allDay;
return props;
}
var EventDragging = /** @class */ (function (_super) {
__extends(EventDragging, _super);
function EventDragging(settings) {
var _this = _super.call(this, settings) || this;
// internal state
_this.subjectSeg = null; // the seg being selected/dragged
_this.isDragging = false;
_this.eventRange = null;
_this.relevantEvents = null; // the events being dragged
_this.receivingCalendar = null;
_this.validMutation = null;
_this.mutatedRelevantEvents = null;
_this.handlePointerDown = function (ev) {
var origTarget = ev.origEvent.target;
var _a = _this, component = _a.component, dragging = _a.dragging;
var mirror = dragging.mirror;
var initialCalendar = component.calendar;
var subjectSeg = _this.subjectSeg = getElSeg(ev.subjectEl);
var eventRange = _this.eventRange = subjectSeg.eventRange;
var eventInstanceId = eventRange.instance.instanceId;
_this.relevantEvents = getRelevantEvents(initialCalendar.state.eventStore, eventInstanceId);
dragging.minDistance = ev.isTouch ? 0 : component.opt('eventDragMinDistance');
dragging.delay =
// only do a touch delay if touch and this event hasn't been selected yet
(ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
getComponentTouchDelay$1(component) :
null;
mirror.parentNode = initialCalendar.el;
mirror.revertDuration = component.opt('dragRevertDuration');
var isValid = component.isValidSegDownEl(origTarget) &&
!elementClosest(origTarget, '.fc-resizer'); // NOT on a resizer
dragging.setIgnoreMove(!isValid);
// disable dragging for elements that are resizable (ie, selectable)
// but are not draggable
_this.isDragging = isValid &&
ev.subjectEl.classList.contains('fc-draggable');
};
_this.handleDragStart = function (ev) {
var initialCalendar = _this.component.calendar;
var eventRange = _this.eventRange;
var eventInstanceId = eventRange.instance.instanceId;
if (ev.isTouch) {
// need to select a different event?
if (eventInstanceId !== _this.component.props.eventSelection) {
initialCalendar.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId });
}
}
else {
// if now using mouse, but was previous touch interaction, clear selected event
initialCalendar.dispatch({ type: 'UNSELECT_EVENT' });
}
if (_this.isDragging) {
initialCalendar.unselect(ev); // unselect *date* selection
initialCalendar.publiclyTrigger('eventDragStart', [
{
el: _this.subjectSeg.el,
event: new EventApi(initialCalendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
view: _this.component.view
}
]);
}
};
_this.handleHitUpdate = function (hit, isFinal) {
if (!_this.isDragging) {
return;
}
var relevantEvents = _this.relevantEvents;
var initialHit = _this.hitDragging.initialHit;
var initialCalendar = _this.component.calendar;
// states based on new hit
var receivingCalendar = null;
var mutation = null;
var mutatedRelevantEvents = null;
var isInvalid = false;
var interaction = {
affectedEvents: relevantEvents,
mutatedEvents: createEmptyEventStore(),
isEvent: true,
origSeg: _this.subjectSeg
};
if (hit) {
var receivingComponent = hit.component;
receivingCalendar = receivingComponent.calendar;
if (initialCalendar === receivingCalendar ||
receivingComponent.opt('editable') && receivingComponent.opt('droppable')) {
mutation = computeEventMutation(initialHit, hit, receivingCalendar.pluginSystem.hooks.eventDragMutationMassagers);
if (mutation) {
mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar);
interaction.mutatedEvents = mutatedRelevantEvents;
if (!receivingComponent.isInteractionValid(interaction)) {
isInvalid = true;
mutation = null;
mutatedRelevantEvents = null;
interaction.mutatedEvents = createEmptyEventStore();
}
}
}
else {
receivingCalendar = null;
}
}
_this.displayDrag(receivingCalendar, interaction);
if (!isInvalid) {
enableCursor();
}
else {
disableCursor();
}
if (!isFinal) {
if (initialCalendar === receivingCalendar && // TODO: write test for this
isHitsEqual(initialHit, hit)) {
mutation = null;
}
_this.dragging.setMirrorNeedsRevert(!mutation);
// render the mirror if no already-rendered mirror
// TODO: wish we could somehow wait for dispatch to guarantee render
_this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-mirror'));
// assign states based on new hit
_this.receivingCalendar = receivingCalendar;
_this.validMutation = mutation;
_this.mutatedRelevantEvents = mutatedRelevantEvents;
}
};
_this.handlePointerUp = function () {
if (!_this.isDragging) {
_this.cleanup(); // because handleDragEnd won't fire
}
};
_this.handleDragEnd = function (ev) {
if (_this.isDragging) {
var initialCalendar_1 = _this.component.calendar;
var initialView = _this.component.view;
var _a = _this, receivingCalendar = _a.receivingCalendar, validMutation = _a.validMutation;
var eventDef = _this.eventRange.def;
var eventInstance = _this.eventRange.instance;
var eventApi = new EventApi(initialCalendar_1, eventDef, eventInstance);
var relevantEvents_1 = _this.relevantEvents;
var mutatedRelevantEvents = _this.mutatedRelevantEvents;
var finalHit = _this.hitDragging.finalHit;
_this.clearDrag(); // must happen after revert animation
initialCalendar_1.publiclyTrigger('eventDragStop', [
{
el: _this.subjectSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
view: initialView
}
]);
if (validMutation) {
// dropped within same calendar
if (receivingCalendar === initialCalendar_1) {
initialCalendar_1.dispatch({
type: 'MERGE_EVENTS',
eventStore: mutatedRelevantEvents
});
var transformed = {};
for (var _i = 0, _b = initialCalendar_1.pluginSystem.hooks.eventDropTransformers; _i < _b.length; _i++) {
var transformer = _b[_i];
__assign(transformed, transformer(validMutation, initialCalendar_1));
}
var eventDropArg = __assign({}, transformed, { el: ev.subjectEl, delta: validMutation.datesDelta, oldEvent: eventApi, event: new EventApi(// the data AFTER the mutation
initialCalendar_1, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), revert: function () {
initialCalendar_1.dispatch({
type: 'MERGE_EVENTS',
eventStore: relevantEvents_1
});
}, jsEvent: ev.origEvent, view: initialView });
initialCalendar_1.publiclyTrigger('eventDrop', [eventDropArg]);
// dropped in different calendar
}
else if (receivingCalendar) {
initialCalendar_1.publiclyTrigger('eventLeave', [
{
draggedEl: ev.subjectEl,
event: eventApi,
view: initialView
}
]);
initialCalendar_1.dispatch({
type: 'REMOVE_EVENT_INSTANCES',
instances: _this.mutatedRelevantEvents.instances
});
receivingCalendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: _this.mutatedRelevantEvents
});
if (ev.isTouch) {
receivingCalendar.dispatch({
type: 'SELECT_EVENT',
eventInstanceId: eventInstance.instanceId
});
}
var dropArg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.component // should this be finalHit.component.view? See #4644
});
receivingCalendar.publiclyTrigger('drop', [dropArg]);
receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: ev.subjectEl,
event: new EventApi(// the data AFTER the mutation
receivingCalendar, mutatedRelevantEvents.defs[eventDef.defId], mutatedRelevantEvents.instances[eventInstance.instanceId]),
view: finalHit.component // should this be finalHit.component.view? See #4644
}
]);
}
}
else {
initialCalendar_1.publiclyTrigger('_noEventDrop');
}
}
_this.cleanup();
};
var component = _this.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.pointer.selector = EventDragging.SELECTOR;
dragging.touchScrollAllowed = false;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsStore);
hitDragging.useSubjectCenter = settings.useEventCenter;
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('pointerup', _this.handlePointerUp);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
EventDragging.prototype.destroy = function () {
this.dragging.destroy();
};
// render a drag state on the next receivingCalendar
EventDragging.prototype.displayDrag = function (nextCalendar, state) {
var initialCalendar = this.component.calendar;
var prevCalendar = this.receivingCalendar;
// does the previous calendar need to be cleared?
if (prevCalendar && prevCalendar !== nextCalendar) {
// does the initial calendar need to be cleared?
// if so, don't clear all the way. we still need to to hide the affectedEvents
if (prevCalendar === initialCalendar) {
prevCalendar.dispatch({
type: 'SET_EVENT_DRAG',
state: {
affectedEvents: state.affectedEvents,
mutatedEvents: createEmptyEventStore(),
isEvent: true,
origSeg: state.origSeg
}
});
// completely clear the old calendar if it wasn't the initial
}
else {
prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
}
if (nextCalendar) {
nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state });
}
};
EventDragging.prototype.clearDrag = function () {
var initialCalendar = this.component.calendar;
var receivingCalendar = this.receivingCalendar;
if (receivingCalendar) {
receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
// the initial calendar might have an dummy drag state from displayDrag
if (initialCalendar !== receivingCalendar) {
initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
};
EventDragging.prototype.cleanup = function () {
this.subjectSeg = null;
this.isDragging = false;
this.eventRange = null;
this.relevantEvents = null;
this.receivingCalendar = null;
this.validMutation = null;
this.mutatedRelevantEvents = null;
};
EventDragging.SELECTOR = '.fc-draggable, .fc-resizable'; // TODO: test this in IE11
return EventDragging;
}(Interaction));
function computeEventMutation(hit0, hit1, massagers) {
var dateSpan0 = hit0.dateSpan;
var dateSpan1 = hit1.dateSpan;
var date0 = dateSpan0.range.start;
var date1 = dateSpan1.range.start;
var standardProps = {};
if (dateSpan0.allDay !== dateSpan1.allDay) {
standardProps.allDay = dateSpan1.allDay;
standardProps.hasEnd = hit1.component.opt('allDayMaintainDuration');
if (dateSpan1.allDay) {
// means date1 is already start-of-day,
// but date0 needs to be converted
date0 = startOfDay(date0);
}
}
var delta = diffDates(date0, date1, hit0.component.dateEnv, hit0.component === hit1.component ?
hit0.component.largeUnit :
null);
if (delta.milliseconds) { // has hours/minutes/seconds
standardProps.allDay = false;
}
var mutation = {
datesDelta: delta,
standardProps: standardProps
};
for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) {
var massager = massagers_1[_i];
massager(mutation, hit0, hit1);
}
return mutation;
}
function getComponentTouchDelay$1(component) {
var delay = component.opt('eventLongPressDelay');
if (delay == null) {
delay = component.opt('longPressDelay');
}
return delay;
}
var EventDragging$1 = /** @class */ (function (_super) {
__extends(EventDragging, _super);
function EventDragging(settings) {
var _this = _super.call(this, settings) || this;
// internal state
_this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg?
_this.eventRange = null;
_this.relevantEvents = null;
_this.validMutation = null;
_this.mutatedRelevantEvents = null;
_this.handlePointerDown = function (ev) {
var component = _this.component;
var seg = _this.querySeg(ev);
var eventRange = _this.eventRange = seg.eventRange;
_this.dragging.minDistance = component.opt('eventDragMinDistance');
// if touch, need to be working with a selected event
_this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) ||
(ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId));
};
_this.handleDragStart = function (ev) {
var calendar = _this.component.calendar;
var eventRange = _this.eventRange;
_this.relevantEvents = getRelevantEvents(calendar.state.eventStore, _this.eventRange.instance.instanceId);
_this.draggingSeg = _this.querySeg(ev);
calendar.unselect();
calendar.publiclyTrigger('eventResizeStart', [
{
el: _this.draggingSeg.el,
event: new EventApi(calendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
view: _this.component.view
}
]);
};
_this.handleHitUpdate = function (hit, isFinal, ev) {
var calendar = _this.component.calendar;
var relevantEvents = _this.relevantEvents;
var initialHit = _this.hitDragging.initialHit;
var eventInstance = _this.eventRange.instance;
var mutation = null;
var mutatedRelevantEvents = null;
var isInvalid = false;
var interaction = {
affectedEvents: relevantEvents,
mutatedEvents: createEmptyEventStore(),
isEvent: true,
origSeg: _this.draggingSeg
};
if (hit) {
mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-start-resizer'), eventInstance.range, calendar.pluginSystem.hooks.eventResizeJoinTransforms);
}
if (mutation) {
mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, calendar.eventUiBases, mutation, calendar);
interaction.mutatedEvents = mutatedRelevantEvents;
if (!_this.component.isInteractionValid(interaction)) {
isInvalid = true;
mutation = null;
mutatedRelevantEvents = null;
interaction.mutatedEvents = null;
}
}
if (mutatedRelevantEvents) {
calendar.dispatch({
type: 'SET_EVENT_RESIZE',
state: interaction
});
}
else {
calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' });
}
if (!isInvalid) {
enableCursor();
}
else {
disableCursor();
}
if (!isFinal) {
if (mutation && isHitsEqual(initialHit, hit)) {
mutation = null;
}
_this.validMutation = mutation;
_this.mutatedRelevantEvents = mutatedRelevantEvents;
}
};
_this.handleDragEnd = function (ev) {
var calendar = _this.component.calendar;
var view = _this.component.view;
var eventDef = _this.eventRange.def;
var eventInstance = _this.eventRange.instance;
var eventApi = new EventApi(calendar, eventDef, eventInstance);
var relevantEvents = _this.relevantEvents;
var mutatedRelevantEvents = _this.mutatedRelevantEvents;
calendar.publiclyTrigger('eventResizeStop', [
{
el: _this.draggingSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
view: view
}
]);
if (_this.validMutation) {
calendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: mutatedRelevantEvents
});
calendar.publiclyTrigger('eventResize', [
{
el: _this.draggingSeg.el,
startDelta: _this.validMutation.startDelta || createDuration(0),
endDelta: _this.validMutation.endDelta || createDuration(0),
prevEvent: eventApi,
event: new EventApi(// the data AFTER the mutation
calendar, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null),
revert: function () {
calendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: relevantEvents
});
},
jsEvent: ev.origEvent,
view: view
}
]);
}
else {
calendar.publiclyTrigger('_noEventResize');
}
// reset all internal state
_this.draggingSeg = null;
_this.relevantEvents = null;
_this.validMutation = null;
// okay to keep eventInstance around. useful to set it in handlePointerDown
};
var component = settings.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.pointer.selector = '.fc-resizer';
dragging.touchScrollAllowed = false;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
EventDragging.prototype.destroy = function () {
this.dragging.destroy();
};
EventDragging.prototype.querySeg = function (ev) {
return getElSeg(elementClosest(ev.subjectEl, this.component.fgSegSelector));
};
return EventDragging;
}(Interaction));
function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) {
var dateEnv = hit0.component.dateEnv;
var date0 = hit0.dateSpan.range.start;
var date1 = hit1.dateSpan.range.start;
var delta = diffDates(date0, date1, dateEnv, hit0.component.largeUnit);
var props = {};
for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) {
var transform = transforms_1[_i];
var res = transform(hit0, hit1);
if (res === false) {
return null;
}
else if (res) {
__assign(props, res);
}
}
if (isFromStart) {
if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
props.startDelta = delta;
return props;
}
}
else {
if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
props.endDelta = delta;
return props;
}
}
return null;
}
var UnselectAuto = /** @class */ (function () {
function UnselectAuto(calendar) {
var _this = this;
this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
this.onSelect = function (selectInfo) {
if (selectInfo.jsEvent) {
_this.isRecentPointerDateSelect = true;
}
};
this.onDocumentPointerUp = function (pev) {
var _a = _this, calendar = _a.calendar, documentPointer = _a.documentPointer;
var state = calendar.state;
// touch-scrolling should never unfocus any type of selection
if (!documentPointer.wasTouchScroll) {
if (state.dateSelection && // an existing date selection?
!_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
) {
var unselectAuto = calendar.viewOpt('unselectAuto');
var unselectCancel = calendar.viewOpt('unselectCancel');
if (unselectAuto && (!unselectAuto || !elementClosest(documentPointer.downEl, unselectCancel))) {
calendar.unselect(pev);
}
}
if (state.eventSelection && // an existing event selected?
!elementClosest(documentPointer.downEl, EventDragging.SELECTOR) // interaction DIDN'T start on an event
) {
calendar.dispatch({ type: 'UNSELECT_EVENT' });
}
}
_this.isRecentPointerDateSelect = false;
};
this.calendar = calendar;
var documentPointer = this.documentPointer = new PointerDragging(document);
documentPointer.shouldIgnoreMove = true;
documentPointer.shouldWatchScroll = false;
documentPointer.emitter.on('pointerup', this.onDocumentPointerUp);
/*
TODO: better way to know about whether there was a selection with the pointer
*/
calendar.on('select', this.onSelect);
}
UnselectAuto.prototype.destroy = function () {
this.calendar.off('select', this.onSelect);
this.documentPointer.destroy();
};
return UnselectAuto;
}());
/*
Given an already instantiated draggable object for one-or-more elements,
Interprets any dragging as an attempt to drag an events that lives outside
of a calendar onto a calendar.
*/
var ExternalElementDragging = /** @class */ (function () {
function ExternalElementDragging(dragging, suppliedDragMeta) {
var _this = this;
this.receivingCalendar = null;
this.droppableEvent = null; // will exist for all drags, even if create:false
this.suppliedDragMeta = null;
this.dragMeta = null;
this.handleDragStart = function (ev) {
_this.dragMeta = _this.buildDragMeta(ev.subjectEl);
};
this.handleHitUpdate = function (hit, isFinal, ev) {
var dragging = _this.hitDragging.dragging;
var receivingCalendar = null;
var droppableEvent = null;
var isInvalid = false;
var interaction = {
affectedEvents: createEmptyEventStore(),
mutatedEvents: createEmptyEventStore(),
isEvent: _this.dragMeta.create,
origSeg: null
};
if (hit) {
receivingCalendar = hit.component.calendar;
if (_this.canDropElOnCalendar(ev.subjectEl, receivingCalendar)) {
droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingCalendar);
interaction.mutatedEvents = eventTupleToStore(droppableEvent);
isInvalid = !isInteractionValid(interaction, receivingCalendar);
if (isInvalid) {
interaction.mutatedEvents = createEmptyEventStore();
droppableEvent = null;
}
}
}
_this.displayDrag(receivingCalendar, interaction);
// show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
// TODO: wish we could somehow wait for dispatch to guarantee render
dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-mirror'));
if (!isInvalid) {
enableCursor();
}
else {
disableCursor();
}
if (!isFinal) {
dragging.setMirrorNeedsRevert(!droppableEvent);
_this.receivingCalendar = receivingCalendar;
_this.droppableEvent = droppableEvent;
}
};
this.handleDragEnd = function (pev) {
var _a = _this, receivingCalendar = _a.receivingCalendar, droppableEvent = _a.droppableEvent;
_this.clearDrag();
if (receivingCalendar && droppableEvent) {
var finalHit = _this.hitDragging.finalHit;
var finalView = finalHit.component.view;
var dragMeta = _this.dragMeta;
var arg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView });
receivingCalendar.publiclyTrigger('drop', [arg]);
if (dragMeta.create) {
receivingCalendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: eventTupleToStore(droppableEvent)
});
if (pev.isTouch) {
receivingCalendar.dispatch({
type: 'SELECT_EVENT',
eventInstanceId: droppableEvent.instance.instanceId
});
}
// signal that an external event landed
receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: pev.subjectEl,
event: new EventApi(receivingCalendar, droppableEvent.def, droppableEvent.instance),
view: finalView
}
]);
}
}
_this.receivingCalendar = null;
_this.droppableEvent = null;
};
var hitDragging = this.hitDragging = new HitDragging(dragging, interactionSettingsStore);
hitDragging.requireInitial = false; // will start outside of a component
hitDragging.emitter.on('dragstart', this.handleDragStart);
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
hitDragging.emitter.on('dragend', this.handleDragEnd);
this.suppliedDragMeta = suppliedDragMeta;
}
ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) {
if (typeof this.suppliedDragMeta === 'object') {
return parseDragMeta(this.suppliedDragMeta);
}
else if (typeof this.suppliedDragMeta === 'function') {
return parseDragMeta(this.suppliedDragMeta(subjectEl));
}
else {
return getDragMetaFromEl(subjectEl);
}
};
ExternalElementDragging.prototype.displayDrag = function (nextCalendar, state) {
var prevCalendar = this.receivingCalendar;
if (prevCalendar && prevCalendar !== nextCalendar) {
prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
if (nextCalendar) {
nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state });
}
};
ExternalElementDragging.prototype.clearDrag = function () {
if (this.receivingCalendar) {
this.receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
};
ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingCalendar) {
var dropAccept = receivingCalendar.opt('dropAccept');
if (typeof dropAccept === 'function') {
return dropAccept(el);
}
else if (typeof dropAccept === 'string' && dropAccept) {
return Boolean(elementMatches(el, dropAccept));
}
return true;
};
return ExternalElementDragging;
}());
// Utils for computing event store from the DragMeta
// ----------------------------------------------------------------------------------------------------
function computeEventForDateSpan(dateSpan, dragMeta, calendar) {
var defProps = __assign({}, dragMeta.leftoverProps);
for (var _i = 0, _a = calendar.pluginSystem.hooks.externalDefTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(defProps, transform(dateSpan, dragMeta));
}
var def = parseEventDef(defProps, dragMeta.sourceId, dateSpan.allDay, calendar.opt('forceEventDuration') || Boolean(dragMeta.duration), // hasEnd
calendar);
var start = dateSpan.range.start;
// only rely on time info if drop zone is all-day,
// otherwise, we already know the time
if (dateSpan.allDay && dragMeta.startTime) {
start = calendar.dateEnv.add(start, dragMeta.startTime);
}
var end = dragMeta.duration ?
calendar.dateEnv.add(start, dragMeta.duration) :
calendar.getDefaultEventEnd(dateSpan.allDay, start);
var instance = createEventInstance(def.defId, { start: start, end: end });
return { def: def, instance: instance };
}
// Utils for extracting data from element
// ----------------------------------------------------------------------------------------------------
function getDragMetaFromEl(el) {
var str = getEmbeddedElData(el, 'event');
var obj = str ?
JSON.parse(str) :
{ create: false }; // if no embedded data, assume no event creation
return parseDragMeta(obj);
}
config.dataAttrPrefix = '';
function getEmbeddedElData(el, name) {
var prefix = config.dataAttrPrefix;
var prefixedName = (prefix ? prefix + '-' : '') + name;
return el.getAttribute('data-' + prefixedName) || '';
}
/*
Makes an element (that is *external* to any calendar) draggable.
Can pass in data that determines how an event will be created when dropped onto a calendar.
Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
*/
var ExternalDraggable = /** @class */ (function () {
function ExternalDraggable(el, settings) {
var _this = this;
if (settings === void 0) { settings = {}; }
this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay;
dragging.minDistance =
minDistance != null ?
minDistance :
(ev.isTouch ? 0 : globalDefaults.eventDragMinDistance);
dragging.delay =
ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
(longPressDelay != null ? longPressDelay : globalDefaults.longPressDelay) :
0;
};
this.handleDragStart = function (ev) {
if (ev.isTouch &&
_this.dragging.delay &&
ev.subjectEl.classList.contains('fc-event')) {
_this.dragging.mirror.getMirrorEl().classList.add('fc-selected');
}
};
this.settings = settings;
var dragging = this.dragging = new FeaturefulElementDragging(el);
dragging.touchScrollAllowed = false;
if (settings.itemSelector != null) {
dragging.pointer.selector = settings.itemSelector;
}
if (settings.appendTo != null) {
dragging.mirror.parentNode = settings.appendTo; // TODO: write tests
}
dragging.emitter.on('pointerdown', this.handlePointerDown);
dragging.emitter.on('dragstart', this.handleDragStart);
new ExternalElementDragging(dragging, settings.eventData);
}
ExternalDraggable.prototype.destroy = function () {
this.dragging.destroy();
};
return ExternalDraggable;
}());
/*
Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
The third-party system is responsible for drawing the visuals effects of the drag.
This class simply monitors for pointer movements and fires events.
It also has the ability to hide the moving element (the "mirror") during the drag.
*/
var InferredElementDragging = /** @class */ (function (_super) {
__extends(InferredElementDragging, _super);
function InferredElementDragging(containerEl) {
var _this = _super.call(this, containerEl) || this;
_this.shouldIgnoreMove = false;
_this.mirrorSelector = '';
_this.currentMirrorEl = null;
_this.handlePointerDown = function (ev) {
_this.emitter.trigger('pointerdown', ev);
if (!_this.shouldIgnoreMove) {
// fire dragstart right away. does not support delay or min-distance
_this.emitter.trigger('dragstart', ev);
}
};
_this.handlePointerMove = function (ev) {
if (!_this.shouldIgnoreMove) {
_this.emitter.trigger('dragmove', ev);
}
};
_this.handlePointerUp = function (ev) {
_this.emitter.trigger('pointerup', ev);
if (!_this.shouldIgnoreMove) {
// fire dragend right away. does not support a revert animation
_this.emitter.trigger('dragend', ev);
}
};
var pointer = _this.pointer = new PointerDragging(containerEl);
pointer.emitter.on('pointerdown', _this.handlePointerDown);
pointer.emitter.on('pointermove', _this.handlePointerMove);
pointer.emitter.on('pointerup', _this.handlePointerUp);
return _this;
}
InferredElementDragging.prototype.destroy = function () {
this.pointer.destroy();
};
InferredElementDragging.prototype.setIgnoreMove = function (bool) {
this.shouldIgnoreMove = bool;
};
InferredElementDragging.prototype.setMirrorIsVisible = function (bool) {
if (bool) {
// restore a previously hidden element.
// use the reference in case the selector class has already been removed.
if (this.currentMirrorEl) {
this.currentMirrorEl.style.visibility = '';
this.currentMirrorEl = null;
}
}
else {
var mirrorEl = this.mirrorSelector ?
document.querySelector(this.mirrorSelector) :
null;
if (mirrorEl) {
this.currentMirrorEl = mirrorEl;
mirrorEl.style.visibility = 'hidden';
}
}
};
return InferredElementDragging;
}(ElementDragging));
/*
Bridges third-party drag-n-drop systems with FullCalendar.
Must be instantiated and destroyed by caller.
*/
var ThirdPartyDraggable = /** @class */ (function () {
function ThirdPartyDraggable(containerOrSettings, settings) {
var containerEl = document;
if (
// wish we could just test instanceof EventTarget, but doesn't work in IE11
containerOrSettings === document ||
containerOrSettings instanceof Element) {
containerEl = containerOrSettings;
settings = settings || {};
}
else {
settings = (containerOrSettings || {});
}
var dragging = this.dragging = new InferredElementDragging(containerEl);
if (typeof settings.itemSelector === 'string') {
dragging.pointer.selector = settings.itemSelector;
}
else if (containerEl === document) {
dragging.pointer.selector = '[data-event]';
}
if (typeof settings.mirrorSelector === 'string') {
dragging.mirrorSelector = settings.mirrorSelector;
}
new ExternalElementDragging(dragging, settings.eventData);
}
ThirdPartyDraggable.prototype.destroy = function () {
this.dragging.destroy();
};
return ThirdPartyDraggable;
}());
var main = createPlugin({
componentInteractions: [DateClicking, DateSelecting, EventDragging, EventDragging$1],
calendarInteractions: [UnselectAuto],
elementDraggingImpl: FeaturefulElementDragging
});
export default main;
export { ExternalDraggable as Draggable, FeaturefulElementDragging, PointerDragging, ThirdPartyDraggable };
/*!
FullCalendar Interaction Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarInteraction = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
core.config.touchMouseIgnoreWait = 500;
var ignoreMouseDepth = 0;
var listenerCnt = 0;
var isWindowTouchMoveCancelled = false;
/*
Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
Tracks when the pointer "drags" on a certain element, meaning down+move+up.
Also, tracks if there was touch-scrolling.
Also, can prevent touch-scrolling from happening.
Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
emits:
- pointerdown
- pointermove
- pointerup
*/
var PointerDragging = /** @class */ (function () {
function PointerDragging(containerEl) {
var _this = this;
this.subjectEl = null;
this.downEl = null;
// options that can be directly assigned by caller
this.selector = ''; // will cause subjectEl in all emitted events to be this element
this.handleSelector = '';
this.shouldIgnoreMove = false;
this.shouldWatchScroll = true; // for simulating pointermove on scroll
// internal states
this.isDragging = false;
this.isTouchDragging = false;
this.wasTouchScroll = false;
// Mouse
// ----------------------------------------------------------------------------------------------------
this.handleMouseDown = function (ev) {
if (!_this.shouldIgnoreMouse() &&
isPrimaryMouseButton(ev) &&
_this.tryStart(ev)) {
var pev = _this.createEventFromMouse(ev, true);
_this.emitter.trigger('pointerdown', pev);
_this.initScrollWatch(pev);
if (!_this.shouldIgnoreMove) {
document.addEventListener('mousemove', _this.handleMouseMove);
}
document.addEventListener('mouseup', _this.handleMouseUp);
}
};
this.handleMouseMove = function (ev) {
var pev = _this.createEventFromMouse(ev);
_this.recordCoords(pev);
_this.emitter.trigger('pointermove', pev);
};
this.handleMouseUp = function (ev) {
document.removeEventListener('mousemove', _this.handleMouseMove);
document.removeEventListener('mouseup', _this.handleMouseUp);
_this.emitter.trigger('pointerup', _this.createEventFromMouse(ev));
_this.cleanup(); // call last so that pointerup has access to props
};
// Touch
// ----------------------------------------------------------------------------------------------------
this.handleTouchStart = function (ev) {
if (_this.tryStart(ev)) {
_this.isTouchDragging = true;
var pev = _this.createEventFromTouch(ev, true);
_this.emitter.trigger('pointerdown', pev);
_this.initScrollWatch(pev);
// unlike mouse, need to attach to target, not document
// https://stackoverflow.com/a/45760014
var target = ev.target;
if (!_this.shouldIgnoreMove) {
target.addEventListener('touchmove', _this.handleTouchMove);
}
target.addEventListener('touchend', _this.handleTouchEnd);
target.addEventListener('touchcancel', _this.handleTouchEnd); // treat it as a touch end
// attach a handler to get called when ANY scroll action happens on the page.
// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
// http://stackoverflow.com/a/32954565/96342
window.addEventListener('scroll', _this.handleTouchScroll, true // useCapture
);
}
};
this.handleTouchMove = function (ev) {
var pev = _this.createEventFromTouch(ev);
_this.recordCoords(pev);
_this.emitter.trigger('pointermove', pev);
};
this.handleTouchEnd = function (ev) {
if (_this.isDragging) { // done to guard against touchend followed by touchcancel
var target = ev.target;
target.removeEventListener('touchmove', _this.handleTouchMove);
target.removeEventListener('touchend', _this.handleTouchEnd);
target.removeEventListener('touchcancel', _this.handleTouchEnd);
window.removeEventListener('scroll', _this.handleTouchScroll, true); // useCaptured=true
_this.emitter.trigger('pointerup', _this.createEventFromTouch(ev));
_this.cleanup(); // call last so that pointerup has access to props
_this.isTouchDragging = false;
startIgnoringMouse();
}
};
this.handleTouchScroll = function () {
_this.wasTouchScroll = true;
};
this.handleScroll = function (ev) {
if (!_this.shouldIgnoreMove) {
var pageX = (window.pageXOffset - _this.prevScrollX) + _this.prevPageX;
var pageY = (window.pageYOffset - _this.prevScrollY) + _this.prevPageY;
_this.emitter.trigger('pointermove', {
origEvent: ev,
isTouch: _this.isTouchDragging,
subjectEl: _this.subjectEl,
pageX: pageX,
pageY: pageY,
deltaX: pageX - _this.origPageX,
deltaY: pageY - _this.origPageY
});
}
};
this.containerEl = containerEl;
this.emitter = new core.EmitterMixin();
containerEl.addEventListener('mousedown', this.handleMouseDown);
containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
listenerCreated();
}
PointerDragging.prototype.destroy = function () {
this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
listenerDestroyed();
};
PointerDragging.prototype.tryStart = function (ev) {
var subjectEl = this.querySubjectEl(ev);
var downEl = ev.target;
if (subjectEl &&
(!this.handleSelector || core.elementClosest(downEl, this.handleSelector))) {
this.subjectEl = subjectEl;
this.downEl = downEl;
this.isDragging = true; // do this first so cancelTouchScroll will work
this.wasTouchScroll = false;
return true;
}
return false;
};
PointerDragging.prototype.cleanup = function () {
isWindowTouchMoveCancelled = false;
this.isDragging = false;
this.subjectEl = null;
this.downEl = null;
// keep wasTouchScroll around for later access
this.destroyScrollWatch();
};
PointerDragging.prototype.querySubjectEl = function (ev) {
if (this.selector) {
return core.elementClosest(ev.target, this.selector);
}
else {
return this.containerEl;
}
};
PointerDragging.prototype.shouldIgnoreMouse = function () {
return ignoreMouseDepth || this.isTouchDragging;
};
// can be called by user of this class, to cancel touch-based scrolling for the current drag
PointerDragging.prototype.cancelTouchScroll = function () {
if (this.isDragging) {
isWindowTouchMoveCancelled = true;
}
};
// Scrolling that simulates pointermoves
// ----------------------------------------------------------------------------------------------------
PointerDragging.prototype.initScrollWatch = function (ev) {
if (this.shouldWatchScroll) {
this.recordCoords(ev);
window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
}
};
PointerDragging.prototype.recordCoords = function (ev) {
if (this.shouldWatchScroll) {
this.prevPageX = ev.pageX;
this.prevPageY = ev.pageY;
this.prevScrollX = window.pageXOffset;
this.prevScrollY = window.pageYOffset;
}
};
PointerDragging.prototype.destroyScrollWatch = function () {
if (this.shouldWatchScroll) {
window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
}
};
// Event Normalization
// ----------------------------------------------------------------------------------------------------
PointerDragging.prototype.createEventFromMouse = function (ev, isFirst) {
var deltaX = 0;
var deltaY = 0;
// TODO: repeat code
if (isFirst) {
this.origPageX = ev.pageX;
this.origPageY = ev.pageY;
}
else {
deltaX = ev.pageX - this.origPageX;
deltaY = ev.pageY - this.origPageY;
}
return {
origEvent: ev,
isTouch: false,
subjectEl: this.subjectEl,
pageX: ev.pageX,
pageY: ev.pageY,
deltaX: deltaX,
deltaY: deltaY
};
};
PointerDragging.prototype.createEventFromTouch = function (ev, isFirst) {
var touches = ev.touches;
var pageX;
var pageY;
var deltaX = 0;
var deltaY = 0;
// if touch coords available, prefer,
// because FF would give bad ev.pageX ev.pageY
if (touches && touches.length) {
pageX = touches[0].pageX;
pageY = touches[0].pageY;
}
else {
pageX = ev.pageX;
pageY = ev.pageY;
}
// TODO: repeat code
if (isFirst) {
this.origPageX = pageX;
this.origPageY = pageY;
}
else {
deltaX = pageX - this.origPageX;
deltaY = pageY - this.origPageY;
}
return {
origEvent: ev,
isTouch: true,
subjectEl: this.subjectEl,
pageX: pageX,
pageY: pageY,
deltaX: deltaX,
deltaY: deltaY
};
};
return PointerDragging;
}());
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
function isPrimaryMouseButton(ev) {
return ev.button === 0 && !ev.ctrlKey;
}
// Ignoring fake mouse events generated by touch
// ----------------------------------------------------------------------------------------------------
function startIgnoringMouse() {
ignoreMouseDepth++;
setTimeout(function () {
ignoreMouseDepth--;
}, core.config.touchMouseIgnoreWait);
}
// We want to attach touchmove as early as possible for Safari
// ----------------------------------------------------------------------------------------------------
function listenerCreated() {
if (!(listenerCnt++)) {
window.addEventListener('touchmove', onWindowTouchMove, { passive: false });
}
}
function listenerDestroyed() {
if (!(--listenerCnt)) {
window.removeEventListener('touchmove', onWindowTouchMove, { passive: false });
}
}
function onWindowTouchMove(ev) {
if (isWindowTouchMoveCancelled) {
ev.preventDefault();
}
}
/*
An effect in which an element follows the movement of a pointer across the screen.
The moving element is a clone of some other element.
Must call start + handleMove + stop.
*/
var ElementMirror = /** @class */ (function () {
function ElementMirror() {
this.isVisible = false; // must be explicitly enabled
this.sourceEl = null;
this.mirrorEl = null;
this.sourceElRect = null; // screen coords relative to viewport
// options that can be set directly by caller
this.parentNode = document.body;
this.zIndex = 9999;
this.revertDuration = 0;
}
ElementMirror.prototype.start = function (sourceEl, pageX, pageY) {
this.sourceEl = sourceEl;
this.sourceElRect = this.sourceEl.getBoundingClientRect();
this.origScreenX = pageX - window.pageXOffset;
this.origScreenY = pageY - window.pageYOffset;
this.deltaX = 0;
this.deltaY = 0;
this.updateElPosition();
};
ElementMirror.prototype.handleMove = function (pageX, pageY) {
this.deltaX = (pageX - window.pageXOffset) - this.origScreenX;
this.deltaY = (pageY - window.pageYOffset) - this.origScreenY;
this.updateElPosition();
};
// can be called before start
ElementMirror.prototype.setIsVisible = function (bool) {
if (bool) {
if (!this.isVisible) {
if (this.mirrorEl) {
this.mirrorEl.style.display = '';
}
this.isVisible = bool; // needs to happen before updateElPosition
this.updateElPosition(); // because was not updating the position while invisible
}
}
else {
if (this.isVisible) {
if (this.mirrorEl) {
this.mirrorEl.style.display = 'none';
}
this.isVisible = bool;
}
}
};
// always async
ElementMirror.prototype.stop = function (needsRevertAnimation, callback) {
var _this = this;
var done = function () {
_this.cleanup();
callback();
};
if (needsRevertAnimation &&
this.mirrorEl &&
this.isVisible &&
this.revertDuration && // if 0, transition won't work
(this.deltaX || this.deltaY) // if same coords, transition won't work
) {
this.doRevertAnimation(done, this.revertDuration);
}
else {
setTimeout(done, 0);
}
};
ElementMirror.prototype.doRevertAnimation = function (callback, revertDuration) {
var mirrorEl = this.mirrorEl;
var finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened
mirrorEl.style.transition =
'top ' + revertDuration + 'ms,' +
'left ' + revertDuration + 'ms';
core.applyStyle(mirrorEl, {
left: finalSourceElRect.left,
top: finalSourceElRect.top
});
core.whenTransitionDone(mirrorEl, function () {
mirrorEl.style.transition = '';
callback();
});
};
ElementMirror.prototype.cleanup = function () {
if (this.mirrorEl) {
core.removeElement(this.mirrorEl);
this.mirrorEl = null;
}
this.sourceEl = null;
};
ElementMirror.prototype.updateElPosition = function () {
if (this.sourceEl && this.isVisible) {
core.applyStyle(this.getMirrorEl(), {
left: this.sourceElRect.left + this.deltaX,
top: this.sourceElRect.top + this.deltaY
});
}
};
ElementMirror.prototype.getMirrorEl = function () {
var sourceElRect = this.sourceElRect;
var mirrorEl = this.mirrorEl;
if (!mirrorEl) {
mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true
// we don't want long taps or any mouse interaction causing selection/menus.
// would use preventSelection(), but that prevents selectstart, causing problems.
mirrorEl.classList.add('fc-unselectable');
mirrorEl.classList.add('fc-dragging');
core.applyStyle(mirrorEl, {
position: 'fixed',
zIndex: this.zIndex,
visibility: '',
boxSizing: 'border-box',
width: sourceElRect.right - sourceElRect.left,
height: sourceElRect.bottom - sourceElRect.top,
right: 'auto',
bottom: 'auto',
margin: 0
});
this.parentNode.appendChild(mirrorEl);
}
return mirrorEl;
};
return ElementMirror;
}());
/*
Is a cache for a given element's scroll information (all the info that ScrollController stores)
in addition the "client rectangle" of the element.. the area within the scrollbars.
The cache can be in one of two modes:
- doesListening:false - ignores when the container is scrolled by someone else
- doesListening:true - watch for scrolling and update the cache
*/
var ScrollGeomCache = /** @class */ (function (_super) {
__extends(ScrollGeomCache, _super);
function ScrollGeomCache(scrollController, doesListening) {
var _this = _super.call(this) || this;
_this.handleScroll = function () {
_this.scrollTop = _this.scrollController.getScrollTop();
_this.scrollLeft = _this.scrollController.getScrollLeft();
_this.handleScrollChange();
};
_this.scrollController = scrollController;
_this.doesListening = doesListening;
_this.scrollTop = _this.origScrollTop = scrollController.getScrollTop();
_this.scrollLeft = _this.origScrollLeft = scrollController.getScrollLeft();
_this.scrollWidth = scrollController.getScrollWidth();
_this.scrollHeight = scrollController.getScrollHeight();
_this.clientWidth = scrollController.getClientWidth();
_this.clientHeight = scrollController.getClientHeight();
_this.clientRect = _this.computeClientRect(); // do last in case it needs cached values
if (_this.doesListening) {
_this.getEventTarget().addEventListener('scroll', _this.handleScroll);
}
return _this;
}
ScrollGeomCache.prototype.destroy = function () {
if (this.doesListening) {
this.getEventTarget().removeEventListener('scroll', this.handleScroll);
}
};
ScrollGeomCache.prototype.getScrollTop = function () {
return this.scrollTop;
};
ScrollGeomCache.prototype.getScrollLeft = function () {
return this.scrollLeft;
};
ScrollGeomCache.prototype.setScrollTop = function (top) {
this.scrollController.setScrollTop(top);
if (!this.doesListening) {
// we are not relying on the element to normalize out-of-bounds scroll values
// so we need to sanitize ourselves
this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0);
this.handleScrollChange();
}
};
ScrollGeomCache.prototype.setScrollLeft = function (top) {
this.scrollController.setScrollLeft(top);
if (!this.doesListening) {
// we are not relying on the element to normalize out-of-bounds scroll values
// so we need to sanitize ourselves
this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0);
this.handleScrollChange();
}
};
ScrollGeomCache.prototype.getClientWidth = function () {
return this.clientWidth;
};
ScrollGeomCache.prototype.getClientHeight = function () {
return this.clientHeight;
};
ScrollGeomCache.prototype.getScrollWidth = function () {
return this.scrollWidth;
};
ScrollGeomCache.prototype.getScrollHeight = function () {
return this.scrollHeight;
};
ScrollGeomCache.prototype.handleScrollChange = function () {
};
return ScrollGeomCache;
}(core.ScrollController));
var ElementScrollGeomCache = /** @class */ (function (_super) {
__extends(ElementScrollGeomCache, _super);
function ElementScrollGeomCache(el, doesListening) {
return _super.call(this, new core.ElementScrollController(el), doesListening) || this;
}
ElementScrollGeomCache.prototype.getEventTarget = function () {
return this.scrollController.el;
};
ElementScrollGeomCache.prototype.computeClientRect = function () {
return core.computeInnerRect(this.scrollController.el);
};
return ElementScrollGeomCache;
}(ScrollGeomCache));
var WindowScrollGeomCache = /** @class */ (function (_super) {
__extends(WindowScrollGeomCache, _super);
function WindowScrollGeomCache(doesListening) {
return _super.call(this, new core.WindowScrollController(), doesListening) || this;
}
WindowScrollGeomCache.prototype.getEventTarget = function () {
return window;
};
WindowScrollGeomCache.prototype.computeClientRect = function () {
return {
left: this.scrollLeft,
right: this.scrollLeft + this.clientWidth,
top: this.scrollTop,
bottom: this.scrollTop + this.clientHeight
};
};
// the window is the only scroll object that changes it's rectangle relative
// to the document's topleft as it scrolls
WindowScrollGeomCache.prototype.handleScrollChange = function () {
this.clientRect = this.computeClientRect();
};
return WindowScrollGeomCache;
}(ScrollGeomCache));
// If available we are using native "performance" API instead of "Date"
// Read more about it on MDN:
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
var getTime = typeof performance === 'function' ? performance.now : Date.now;
/*
For a pointer interaction, automatically scrolls certain scroll containers when the pointer
approaches the edge.
The caller must call start + handleMove + stop.
*/
var AutoScroller = /** @class */ (function () {
function AutoScroller() {
var _this = this;
// options that can be set by caller
this.isEnabled = true;
this.scrollQuery = [window, '.fc-scroller'];
this.edgeThreshold = 50; // pixels
this.maxVelocity = 300; // pixels per second
// internal state
this.pointerScreenX = null;
this.pointerScreenY = null;
this.isAnimating = false;
this.scrollCaches = null;
// protect against the initial pointerdown being too close to an edge and starting the scroll
this.everMovedUp = false;
this.everMovedDown = false;
this.everMovedLeft = false;
this.everMovedRight = false;
this.animate = function () {
if (_this.isAnimating) { // wasn't cancelled between animation calls
var edge = _this.computeBestEdge(_this.pointerScreenX + window.pageXOffset, _this.pointerScreenY + window.pageYOffset);
if (edge) {
var now = getTime();
_this.handleSide(edge, (now - _this.msSinceRequest) / 1000);
_this.requestAnimation(now);
}
else {
_this.isAnimating = false; // will stop animation
}
}
};
}
AutoScroller.prototype.start = function (pageX, pageY) {
if (this.isEnabled) {
this.scrollCaches = this.buildCaches();
this.pointerScreenX = null;
this.pointerScreenY = null;
this.everMovedUp = false;
this.everMovedDown = false;
this.everMovedLeft = false;
this.everMovedRight = false;
this.handleMove(pageX, pageY);
}
};
AutoScroller.prototype.handleMove = function (pageX, pageY) {
if (this.isEnabled) {
var pointerScreenX = pageX - window.pageXOffset;
var pointerScreenY = pageY - window.pageYOffset;
var yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY;
var xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX;
if (yDelta < 0) {
this.everMovedUp = true;
}
else if (yDelta > 0) {
this.everMovedDown = true;
}
if (xDelta < 0) {
this.everMovedLeft = true;
}
else if (xDelta > 0) {
this.everMovedRight = true;
}
this.pointerScreenX = pointerScreenX;
this.pointerScreenY = pointerScreenY;
if (!this.isAnimating) {
this.isAnimating = true;
this.requestAnimation(getTime());
}
}
};
AutoScroller.prototype.stop = function () {
if (this.isEnabled) {
this.isAnimating = false; // will stop animation
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
scrollCache.destroy();
}
this.scrollCaches = null;
}
};
AutoScroller.prototype.requestAnimation = function (now) {
this.msSinceRequest = now;
requestAnimationFrame(this.animate);
};
AutoScroller.prototype.handleSide = function (edge, seconds) {
var scrollCache = edge.scrollCache;
var edgeThreshold = this.edgeThreshold;
var invDistance = edgeThreshold - edge.distance;
var velocity = // the closer to the edge, the faster we scroll
(invDistance * invDistance) / (edgeThreshold * edgeThreshold) * // quadratic
this.maxVelocity * seconds;
var sign = 1;
switch (edge.name) {
case 'left':
sign = -1;
// falls through
case 'right':
scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign);
break;
case 'top':
sign = -1;
// falls through
case 'bottom':
scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign);
break;
}
};
// left/top are relative to document topleft
AutoScroller.prototype.computeBestEdge = function (left, top) {
var edgeThreshold = this.edgeThreshold;
var bestSide = null;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
var rect = scrollCache.clientRect;
var leftDist = left - rect.left;
var rightDist = rect.right - left;
var topDist = top - rect.top;
var bottomDist = rect.bottom - top;
// completely within the rect?
if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
(!bestSide || bestSide.distance > topDist)) {
bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist };
}
if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
(!bestSide || bestSide.distance > bottomDist)) {
bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist };
}
if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
(!bestSide || bestSide.distance > leftDist)) {
bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist };
}
if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
(!bestSide || bestSide.distance > rightDist)) {
bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist };
}
}
}
return bestSide;
};
AutoScroller.prototype.buildCaches = function () {
return this.queryScrollEls().map(function (el) {
if (el === window) {
return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls
}
else {
return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls
}
});
};
AutoScroller.prototype.queryScrollEls = function () {
var els = [];
for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) {
var query = _a[_i];
if (typeof query === 'object') {
els.push(query);
}
else {
els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query)));
}
}
return els;
};
return AutoScroller;
}());
/*
Monitors dragging on an element. Has a number of high-level features:
- minimum distance required before dragging
- minimum wait time ("delay") before dragging
- a mirror element that follows the pointer
*/
var FeaturefulElementDragging = /** @class */ (function (_super) {
__extends(FeaturefulElementDragging, _super);
function FeaturefulElementDragging(containerEl) {
var _this = _super.call(this, containerEl) || this;
// options that can be directly set by caller
// the caller can also set the PointerDragging's options as well
_this.delay = null;
_this.minDistance = 0;
_this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
_this.mirrorNeedsRevert = false;
_this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
_this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
_this.isDelayEnded = false;
_this.isDistanceSurpassed = false;
_this.delayTimeoutId = null;
_this.onPointerDown = function (ev) {
if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going
_this.isInteracting = true;
_this.isDelayEnded = false;
_this.isDistanceSurpassed = false;
core.preventSelection(document.body);
core.preventContextMenu(document.body);
// prevent links from being visited if there's an eventual drag.
// also prevents selection in older browsers (maybe?).
// not necessary for touch, besides, browser would complain about passiveness.
if (!ev.isTouch) {
ev.origEvent.preventDefault();
}
_this.emitter.trigger('pointerdown', ev);
if (!_this.pointer.shouldIgnoreMove) {
// actions related to initiating dragstart+dragmove+dragend...
_this.mirror.setIsVisible(false); // reset. caller must set-visible
_this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down
_this.startDelay(ev);
if (!_this.minDistance) {
_this.handleDistanceSurpassed(ev);
}
}
}
};
_this.onPointerMove = function (ev) {
if (_this.isInteracting) { // if false, still waiting for previous drag's revert
_this.emitter.trigger('pointermove', ev);
if (!_this.isDistanceSurpassed) {
var minDistance = _this.minDistance;
var distanceSq = void 0; // current distance from the origin, squared
var deltaX = ev.deltaX, deltaY = ev.deltaY;
distanceSq = deltaX * deltaX + deltaY * deltaY;
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
_this.handleDistanceSurpassed(ev);
}
}
if (_this.isDragging) {
// a real pointer move? (not one simulated by scrolling)
if (ev.origEvent.type !== 'scroll') {
_this.mirror.handleMove(ev.pageX, ev.pageY);
_this.autoScroller.handleMove(ev.pageX, ev.pageY);
}
_this.emitter.trigger('dragmove', ev);
}
}
};
_this.onPointerUp = function (ev) {
if (_this.isInteracting) { // if false, still waiting for previous drag's revert
_this.isInteracting = false;
core.allowSelection(document.body);
core.allowContextMenu(document.body);
_this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert
if (_this.isDragging) {
_this.autoScroller.stop();
_this.tryStopDrag(ev); // which will stop the mirror
}
if (_this.delayTimeoutId) {
clearTimeout(_this.delayTimeoutId);
_this.delayTimeoutId = null;
}
}
};
var pointer = _this.pointer = new PointerDragging(containerEl);
pointer.emitter.on('pointerdown', _this.onPointerDown);
pointer.emitter.on('pointermove', _this.onPointerMove);
pointer.emitter.on('pointerup', _this.onPointerUp);
_this.mirror = new ElementMirror();
_this.autoScroller = new AutoScroller();
return _this;
}
FeaturefulElementDragging.prototype.destroy = function () {
this.pointer.destroy();
};
FeaturefulElementDragging.prototype.startDelay = function (ev) {
var _this = this;
if (typeof this.delay === 'number') {
this.delayTimeoutId = setTimeout(function () {
_this.delayTimeoutId = null;
_this.handleDelayEnd(ev);
}, this.delay); // not assignable to number!
}
else {
this.handleDelayEnd(ev);
}
};
FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) {
this.isDelayEnded = true;
this.tryStartDrag(ev);
};
FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) {
this.isDistanceSurpassed = true;
this.tryStartDrag(ev);
};
FeaturefulElementDragging.prototype.tryStartDrag = function (ev) {
if (this.isDelayEnded && this.isDistanceSurpassed) {
if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
this.isDragging = true;
this.mirrorNeedsRevert = false;
this.autoScroller.start(ev.pageX, ev.pageY);
this.emitter.trigger('dragstart', ev);
if (this.touchScrollAllowed === false) {
this.pointer.cancelTouchScroll();
}
}
}
};
FeaturefulElementDragging.prototype.tryStopDrag = function (ev) {
// .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
// that come from the document to fire beforehand. much more convenient this way.
this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev) // bound with args
);
};
FeaturefulElementDragging.prototype.stopDrag = function (ev) {
this.isDragging = false;
this.emitter.trigger('dragend', ev);
};
// fill in the implementations...
FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) {
this.pointer.shouldIgnoreMove = bool;
};
FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) {
this.mirror.setIsVisible(bool);
};
FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) {
this.mirrorNeedsRevert = bool;
};
FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) {
this.autoScroller.isEnabled = bool;
};
return FeaturefulElementDragging;
}(core.ElementDragging));
/*
When this class is instantiated, it records the offset of an element (relative to the document topleft),
and continues to monitor scrolling, updating the cached coordinates if it needs to.
Does not access the DOM after instantiation, so highly performant.
Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
and an determine if a given point is inside the combined clipping rectangle.
*/
var OffsetTracker = /** @class */ (function () {
function OffsetTracker(el) {
this.origRect = core.computeRect(el);
// will work fine for divs that have overflow:hidden
this.scrollCaches = core.getClippingParents(el).map(function (el) {
return new ElementScrollGeomCache(el, true); // listen=true
});
}
OffsetTracker.prototype.destroy = function () {
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
scrollCache.destroy();
}
};
OffsetTracker.prototype.computeLeft = function () {
var left = this.origRect.left;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
left += scrollCache.origScrollLeft - scrollCache.getScrollLeft();
}
return left;
};
OffsetTracker.prototype.computeTop = function () {
var top = this.origRect.top;
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
top += scrollCache.origScrollTop - scrollCache.getScrollTop();
}
return top;
};
OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) {
var point = { left: pageX, top: pageY };
for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) {
var scrollCache = _a[_i];
if (!isIgnoredClipping(scrollCache.getEventTarget()) &&
!core.pointInsideRect(point, scrollCache.clientRect)) {
return false;
}
}
return true;
};
return OffsetTracker;
}());
// certain clipping containers should never constrain interactions, like and
// https://github.com/fullcalendar/fullcalendar/issues/3615
function isIgnoredClipping(node) {
var tagName = node.tagName;
return tagName === 'HTML' || tagName === 'BODY';
}
/*
Tracks movement over multiple droppable areas (aka "hits")
that exist in one or more DateComponents.
Relies on an existing draggable.
emits:
- pointerdown
- dragstart
- hitchange - fires initially, even if not over a hit
- pointerup
- (hitchange - again, to null, if ended over a hit)
- dragend
*/
var HitDragging = /** @class */ (function () {
function HitDragging(dragging, droppableStore) {
var _this = this;
// options that can be set by caller
this.useSubjectCenter = false;
this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
this.initialHit = null;
this.movingHit = null;
this.finalHit = null; // won't ever be populated if shouldIgnoreMove
this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
_this.initialHit = null;
_this.movingHit = null;
_this.finalHit = null;
_this.prepareHits();
_this.processFirstCoord(ev);
if (_this.initialHit || !_this.requireInitial) {
dragging.setIgnoreMove(false);
_this.emitter.trigger('pointerdown', ev); // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
}
else {
dragging.setIgnoreMove(true);
}
};
this.handleDragStart = function (ev) {
_this.emitter.trigger('dragstart', ev);
_this.handleMove(ev, true); // force = fire even if initially null
};
this.handleDragMove = function (ev) {
_this.emitter.trigger('dragmove', ev);
_this.handleMove(ev);
};
this.handlePointerUp = function (ev) {
_this.releaseHits();
_this.emitter.trigger('pointerup', ev);
};
this.handleDragEnd = function (ev) {
if (_this.movingHit) {
_this.emitter.trigger('hitupdate', null, true, ev);
}
_this.finalHit = _this.movingHit;
_this.movingHit = null;
_this.emitter.trigger('dragend', ev);
};
this.droppableStore = droppableStore;
dragging.emitter.on('pointerdown', this.handlePointerDown);
dragging.emitter.on('dragstart', this.handleDragStart);
dragging.emitter.on('dragmove', this.handleDragMove);
dragging.emitter.on('pointerup', this.handlePointerUp);
dragging.emitter.on('dragend', this.handleDragEnd);
this.dragging = dragging;
this.emitter = new core.EmitterMixin();
}
// sets initialHit
// sets coordAdjust
HitDragging.prototype.processFirstCoord = function (ev) {
var origPoint = { left: ev.pageX, top: ev.pageY };
var adjustedPoint = origPoint;
var subjectEl = ev.subjectEl;
var subjectRect;
if (subjectEl !== document) {
subjectRect = core.computeRect(subjectEl);
adjustedPoint = core.constrainPoint(adjustedPoint, subjectRect);
}
var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top);
if (initialHit) {
if (this.useSubjectCenter && subjectRect) {
var slicedSubjectRect = core.intersectRects(subjectRect, initialHit.rect);
if (slicedSubjectRect) {
adjustedPoint = core.getRectCenter(slicedSubjectRect);
}
}
this.coordAdjust = core.diffPoints(adjustedPoint, origPoint);
}
else {
this.coordAdjust = { left: 0, top: 0 };
}
};
HitDragging.prototype.handleMove = function (ev, forceHandle) {
var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top);
if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
this.movingHit = hit;
this.emitter.trigger('hitupdate', hit, false, ev);
}
};
HitDragging.prototype.prepareHits = function () {
this.offsetTrackers = core.mapHash(this.droppableStore, function (interactionSettings) {
interactionSettings.component.buildPositionCaches();
return new OffsetTracker(interactionSettings.el);
});
};
HitDragging.prototype.releaseHits = function () {
var offsetTrackers = this.offsetTrackers;
for (var id in offsetTrackers) {
offsetTrackers[id].destroy();
}
this.offsetTrackers = {};
};
HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) {
var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers;
var bestHit = null;
for (var id in droppableStore) {
var component = droppableStore[id].component;
var offsetTracker = offsetTrackers[id];
if (offsetTracker.isWithinClipping(offsetLeft, offsetTop)) {
var originLeft = offsetTracker.computeLeft();
var originTop = offsetTracker.computeTop();
var positionLeft = offsetLeft - originLeft;
var positionTop = offsetTop - originTop;
var origRect = offsetTracker.origRect;
var width = origRect.right - origRect.left;
var height = origRect.bottom - origRect.top;
if (
// must be within the element's bounds
positionLeft >= 0 && positionLeft < width &&
positionTop >= 0 && positionTop < height) {
var hit = component.queryHit(positionLeft, positionTop, width, height);
if (hit &&
(
// make sure the hit is within activeRange, meaning it's not a deal cell
!component.props.dateProfile || // hack for DayTile
core.rangeContainsRange(component.props.dateProfile.activeRange, hit.dateSpan.range)) &&
(!bestHit || hit.layer > bestHit.layer)) {
// TODO: better way to re-orient rectangle
hit.rect.left += originLeft;
hit.rect.right += originLeft;
hit.rect.top += originTop;
hit.rect.bottom += originTop;
bestHit = hit;
}
}
}
}
return bestHit;
};
return HitDragging;
}());
function isHitsEqual(hit0, hit1) {
if (!hit0 && !hit1) {
return true;
}
if (Boolean(hit0) !== Boolean(hit1)) {
return false;
}
return core.isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
}
/*
Monitors when the user clicks on a specific date/time of a component.
A pointerdown+pointerup on the same "hit" constitutes a click.
*/
var DateClicking = /** @class */ (function (_super) {
__extends(DateClicking, _super);
function DateClicking(settings) {
var _this = _super.call(this, settings) || this;
_this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
// do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired
dragging.setIgnoreMove(!_this.component.isValidDateDownEl(dragging.pointer.downEl));
};
// won't even fire if moving was ignored
_this.handleDragEnd = function (ev) {
var component = _this.component;
var pointer = _this.dragging.pointer;
if (!pointer.wasTouchScroll) {
var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit;
if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
component.calendar.triggerDateClick(initialHit.dateSpan, initialHit.dayEl, component.view, ev.origEvent);
}
}
};
var component = settings.component;
// we DO want to watch pointer moves because otherwise finalHit won't get populated
_this.dragging = new FeaturefulElementDragging(component.el);
_this.dragging.autoScroller.isEnabled = false;
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
DateClicking.prototype.destroy = function () {
this.dragging.destroy();
};
return DateClicking;
}(core.Interaction));
/*
Tracks when the user selects a portion of time of a component,
constituted by a drag over date cells, with a possible delay at the beginning of the drag.
*/
var DateSelecting = /** @class */ (function (_super) {
__extends(DateSelecting, _super);
function DateSelecting(settings) {
var _this = _super.call(this, settings) || this;
_this.dragSelection = null;
_this.handlePointerDown = function (ev) {
var _a = _this, component = _a.component, dragging = _a.dragging;
var canSelect = component.opt('selectable') &&
component.isValidDateDownEl(ev.origEvent.target);
// don't bother to watch expensive moves if component won't do selection
dragging.setIgnoreMove(!canSelect);
// if touch, require user to hold down
dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null;
};
_this.handleDragStart = function (ev) {
_this.component.calendar.unselect(ev); // unselect previous selections
};
_this.handleHitUpdate = function (hit, isFinal) {
var calendar = _this.component.calendar;
var dragSelection = null;
var isInvalid = false;
if (hit) {
dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, calendar.pluginSystem.hooks.dateSelectionTransformers);
if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) {
isInvalid = true;
dragSelection = null;
}
}
if (dragSelection) {
calendar.dispatch({ type: 'SELECT_DATES', selection: dragSelection });
}
else if (!isFinal) { // only unselect if moved away while dragging
calendar.dispatch({ type: 'UNSELECT_DATES' });
}
if (!isInvalid) {
core.enableCursor();
}
else {
core.disableCursor();
}
if (!isFinal) {
_this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging
}
};
_this.handlePointerUp = function (pev) {
if (_this.dragSelection) {
// selection is already rendered, so just need to report selection
_this.component.calendar.triggerDateSelect(_this.dragSelection, pev);
_this.dragSelection = null;
}
};
var component = settings.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.touchScrollAllowed = false;
dragging.minDistance = component.opt('selectMinDistance') || 0;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('pointerup', _this.handlePointerUp);
return _this;
}
DateSelecting.prototype.destroy = function () {
this.dragging.destroy();
};
return DateSelecting;
}(core.Interaction));
function getComponentTouchDelay(component) {
var delay = component.opt('selectLongPressDelay');
if (delay == null) {
delay = component.opt('longPressDelay');
}
return delay;
}
function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
var dateSpan0 = hit0.dateSpan;
var dateSpan1 = hit1.dateSpan;
var ms = [
dateSpan0.range.start,
dateSpan0.range.end,
dateSpan1.range.start,
dateSpan1.range.end
];
ms.sort(core.compareNumbers);
var props = {};
for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) {
var transformer = dateSelectionTransformers_1[_i];
var res = transformer(hit0, hit1);
if (res === false) {
return null;
}
else if (res) {
__assign(props, res);
}
}
props.range = { start: ms[0], end: ms[3] };
props.allDay = dateSpan0.allDay;
return props;
}
var EventDragging = /** @class */ (function (_super) {
__extends(EventDragging, _super);
function EventDragging(settings) {
var _this = _super.call(this, settings) || this;
// internal state
_this.subjectSeg = null; // the seg being selected/dragged
_this.isDragging = false;
_this.eventRange = null;
_this.relevantEvents = null; // the events being dragged
_this.receivingCalendar = null;
_this.validMutation = null;
_this.mutatedRelevantEvents = null;
_this.handlePointerDown = function (ev) {
var origTarget = ev.origEvent.target;
var _a = _this, component = _a.component, dragging = _a.dragging;
var mirror = dragging.mirror;
var initialCalendar = component.calendar;
var subjectSeg = _this.subjectSeg = core.getElSeg(ev.subjectEl);
var eventRange = _this.eventRange = subjectSeg.eventRange;
var eventInstanceId = eventRange.instance.instanceId;
_this.relevantEvents = core.getRelevantEvents(initialCalendar.state.eventStore, eventInstanceId);
dragging.minDistance = ev.isTouch ? 0 : component.opt('eventDragMinDistance');
dragging.delay =
// only do a touch delay if touch and this event hasn't been selected yet
(ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
getComponentTouchDelay$1(component) :
null;
mirror.parentNode = initialCalendar.el;
mirror.revertDuration = component.opt('dragRevertDuration');
var isValid = component.isValidSegDownEl(origTarget) &&
!core.elementClosest(origTarget, '.fc-resizer'); // NOT on a resizer
dragging.setIgnoreMove(!isValid);
// disable dragging for elements that are resizable (ie, selectable)
// but are not draggable
_this.isDragging = isValid &&
ev.subjectEl.classList.contains('fc-draggable');
};
_this.handleDragStart = function (ev) {
var initialCalendar = _this.component.calendar;
var eventRange = _this.eventRange;
var eventInstanceId = eventRange.instance.instanceId;
if (ev.isTouch) {
// need to select a different event?
if (eventInstanceId !== _this.component.props.eventSelection) {
initialCalendar.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId });
}
}
else {
// if now using mouse, but was previous touch interaction, clear selected event
initialCalendar.dispatch({ type: 'UNSELECT_EVENT' });
}
if (_this.isDragging) {
initialCalendar.unselect(ev); // unselect *date* selection
initialCalendar.publiclyTrigger('eventDragStart', [
{
el: _this.subjectSeg.el,
event: new core.EventApi(initialCalendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
view: _this.component.view
}
]);
}
};
_this.handleHitUpdate = function (hit, isFinal) {
if (!_this.isDragging) {
return;
}
var relevantEvents = _this.relevantEvents;
var initialHit = _this.hitDragging.initialHit;
var initialCalendar = _this.component.calendar;
// states based on new hit
var receivingCalendar = null;
var mutation = null;
var mutatedRelevantEvents = null;
var isInvalid = false;
var interaction = {
affectedEvents: relevantEvents,
mutatedEvents: core.createEmptyEventStore(),
isEvent: true,
origSeg: _this.subjectSeg
};
if (hit) {
var receivingComponent = hit.component;
receivingCalendar = receivingComponent.calendar;
if (initialCalendar === receivingCalendar ||
receivingComponent.opt('editable') && receivingComponent.opt('droppable')) {
mutation = computeEventMutation(initialHit, hit, receivingCalendar.pluginSystem.hooks.eventDragMutationMassagers);
if (mutation) {
mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar);
interaction.mutatedEvents = mutatedRelevantEvents;
if (!receivingComponent.isInteractionValid(interaction)) {
isInvalid = true;
mutation = null;
mutatedRelevantEvents = null;
interaction.mutatedEvents = core.createEmptyEventStore();
}
}
}
else {
receivingCalendar = null;
}
}
_this.displayDrag(receivingCalendar, interaction);
if (!isInvalid) {
core.enableCursor();
}
else {
core.disableCursor();
}
if (!isFinal) {
if (initialCalendar === receivingCalendar && // TODO: write test for this
isHitsEqual(initialHit, hit)) {
mutation = null;
}
_this.dragging.setMirrorNeedsRevert(!mutation);
// render the mirror if no already-rendered mirror
// TODO: wish we could somehow wait for dispatch to guarantee render
_this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-mirror'));
// assign states based on new hit
_this.receivingCalendar = receivingCalendar;
_this.validMutation = mutation;
_this.mutatedRelevantEvents = mutatedRelevantEvents;
}
};
_this.handlePointerUp = function () {
if (!_this.isDragging) {
_this.cleanup(); // because handleDragEnd won't fire
}
};
_this.handleDragEnd = function (ev) {
if (_this.isDragging) {
var initialCalendar_1 = _this.component.calendar;
var initialView = _this.component.view;
var _a = _this, receivingCalendar = _a.receivingCalendar, validMutation = _a.validMutation;
var eventDef = _this.eventRange.def;
var eventInstance = _this.eventRange.instance;
var eventApi = new core.EventApi(initialCalendar_1, eventDef, eventInstance);
var relevantEvents_1 = _this.relevantEvents;
var mutatedRelevantEvents = _this.mutatedRelevantEvents;
var finalHit = _this.hitDragging.finalHit;
_this.clearDrag(); // must happen after revert animation
initialCalendar_1.publiclyTrigger('eventDragStop', [
{
el: _this.subjectSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
view: initialView
}
]);
if (validMutation) {
// dropped within same calendar
if (receivingCalendar === initialCalendar_1) {
initialCalendar_1.dispatch({
type: 'MERGE_EVENTS',
eventStore: mutatedRelevantEvents
});
var transformed = {};
for (var _i = 0, _b = initialCalendar_1.pluginSystem.hooks.eventDropTransformers; _i < _b.length; _i++) {
var transformer = _b[_i];
__assign(transformed, transformer(validMutation, initialCalendar_1));
}
var eventDropArg = __assign({}, transformed, { el: ev.subjectEl, delta: validMutation.datesDelta, oldEvent: eventApi, event: new core.EventApi(// the data AFTER the mutation
initialCalendar_1, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), revert: function () {
initialCalendar_1.dispatch({
type: 'MERGE_EVENTS',
eventStore: relevantEvents_1
});
}, jsEvent: ev.origEvent, view: initialView });
initialCalendar_1.publiclyTrigger('eventDrop', [eventDropArg]);
// dropped in different calendar
}
else if (receivingCalendar) {
initialCalendar_1.publiclyTrigger('eventLeave', [
{
draggedEl: ev.subjectEl,
event: eventApi,
view: initialView
}
]);
initialCalendar_1.dispatch({
type: 'REMOVE_EVENT_INSTANCES',
instances: _this.mutatedRelevantEvents.instances
});
receivingCalendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: _this.mutatedRelevantEvents
});
if (ev.isTouch) {
receivingCalendar.dispatch({
type: 'SELECT_EVENT',
eventInstanceId: eventInstance.instanceId
});
}
var dropArg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.component // should this be finalHit.component.view? See #4644
});
receivingCalendar.publiclyTrigger('drop', [dropArg]);
receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: ev.subjectEl,
event: new core.EventApi(// the data AFTER the mutation
receivingCalendar, mutatedRelevantEvents.defs[eventDef.defId], mutatedRelevantEvents.instances[eventInstance.instanceId]),
view: finalHit.component // should this be finalHit.component.view? See #4644
}
]);
}
}
else {
initialCalendar_1.publiclyTrigger('_noEventDrop');
}
}
_this.cleanup();
};
var component = _this.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.pointer.selector = EventDragging.SELECTOR;
dragging.touchScrollAllowed = false;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsStore);
hitDragging.useSubjectCenter = settings.useEventCenter;
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('pointerup', _this.handlePointerUp);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
EventDragging.prototype.destroy = function () {
this.dragging.destroy();
};
// render a drag state on the next receivingCalendar
EventDragging.prototype.displayDrag = function (nextCalendar, state) {
var initialCalendar = this.component.calendar;
var prevCalendar = this.receivingCalendar;
// does the previous calendar need to be cleared?
if (prevCalendar && prevCalendar !== nextCalendar) {
// does the initial calendar need to be cleared?
// if so, don't clear all the way. we still need to to hide the affectedEvents
if (prevCalendar === initialCalendar) {
prevCalendar.dispatch({
type: 'SET_EVENT_DRAG',
state: {
affectedEvents: state.affectedEvents,
mutatedEvents: core.createEmptyEventStore(),
isEvent: true,
origSeg: state.origSeg
}
});
// completely clear the old calendar if it wasn't the initial
}
else {
prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
}
if (nextCalendar) {
nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state });
}
};
EventDragging.prototype.clearDrag = function () {
var initialCalendar = this.component.calendar;
var receivingCalendar = this.receivingCalendar;
if (receivingCalendar) {
receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
// the initial calendar might have an dummy drag state from displayDrag
if (initialCalendar !== receivingCalendar) {
initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
};
EventDragging.prototype.cleanup = function () {
this.subjectSeg = null;
this.isDragging = false;
this.eventRange = null;
this.relevantEvents = null;
this.receivingCalendar = null;
this.validMutation = null;
this.mutatedRelevantEvents = null;
};
EventDragging.SELECTOR = '.fc-draggable, .fc-resizable'; // TODO: test this in IE11
return EventDragging;
}(core.Interaction));
function computeEventMutation(hit0, hit1, massagers) {
var dateSpan0 = hit0.dateSpan;
var dateSpan1 = hit1.dateSpan;
var date0 = dateSpan0.range.start;
var date1 = dateSpan1.range.start;
var standardProps = {};
if (dateSpan0.allDay !== dateSpan1.allDay) {
standardProps.allDay = dateSpan1.allDay;
standardProps.hasEnd = hit1.component.opt('allDayMaintainDuration');
if (dateSpan1.allDay) {
// means date1 is already start-of-day,
// but date0 needs to be converted
date0 = core.startOfDay(date0);
}
}
var delta = core.diffDates(date0, date1, hit0.component.dateEnv, hit0.component === hit1.component ?
hit0.component.largeUnit :
null);
if (delta.milliseconds) { // has hours/minutes/seconds
standardProps.allDay = false;
}
var mutation = {
datesDelta: delta,
standardProps: standardProps
};
for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) {
var massager = massagers_1[_i];
massager(mutation, hit0, hit1);
}
return mutation;
}
function getComponentTouchDelay$1(component) {
var delay = component.opt('eventLongPressDelay');
if (delay == null) {
delay = component.opt('longPressDelay');
}
return delay;
}
var EventDragging$1 = /** @class */ (function (_super) {
__extends(EventDragging, _super);
function EventDragging(settings) {
var _this = _super.call(this, settings) || this;
// internal state
_this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg?
_this.eventRange = null;
_this.relevantEvents = null;
_this.validMutation = null;
_this.mutatedRelevantEvents = null;
_this.handlePointerDown = function (ev) {
var component = _this.component;
var seg = _this.querySeg(ev);
var eventRange = _this.eventRange = seg.eventRange;
_this.dragging.minDistance = component.opt('eventDragMinDistance');
// if touch, need to be working with a selected event
_this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) ||
(ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId));
};
_this.handleDragStart = function (ev) {
var calendar = _this.component.calendar;
var eventRange = _this.eventRange;
_this.relevantEvents = core.getRelevantEvents(calendar.state.eventStore, _this.eventRange.instance.instanceId);
_this.draggingSeg = _this.querySeg(ev);
calendar.unselect();
calendar.publiclyTrigger('eventResizeStart', [
{
el: _this.draggingSeg.el,
event: new core.EventApi(calendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
view: _this.component.view
}
]);
};
_this.handleHitUpdate = function (hit, isFinal, ev) {
var calendar = _this.component.calendar;
var relevantEvents = _this.relevantEvents;
var initialHit = _this.hitDragging.initialHit;
var eventInstance = _this.eventRange.instance;
var mutation = null;
var mutatedRelevantEvents = null;
var isInvalid = false;
var interaction = {
affectedEvents: relevantEvents,
mutatedEvents: core.createEmptyEventStore(),
isEvent: true,
origSeg: _this.draggingSeg
};
if (hit) {
mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-start-resizer'), eventInstance.range, calendar.pluginSystem.hooks.eventResizeJoinTransforms);
}
if (mutation) {
mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, calendar.eventUiBases, mutation, calendar);
interaction.mutatedEvents = mutatedRelevantEvents;
if (!_this.component.isInteractionValid(interaction)) {
isInvalid = true;
mutation = null;
mutatedRelevantEvents = null;
interaction.mutatedEvents = null;
}
}
if (mutatedRelevantEvents) {
calendar.dispatch({
type: 'SET_EVENT_RESIZE',
state: interaction
});
}
else {
calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' });
}
if (!isInvalid) {
core.enableCursor();
}
else {
core.disableCursor();
}
if (!isFinal) {
if (mutation && isHitsEqual(initialHit, hit)) {
mutation = null;
}
_this.validMutation = mutation;
_this.mutatedRelevantEvents = mutatedRelevantEvents;
}
};
_this.handleDragEnd = function (ev) {
var calendar = _this.component.calendar;
var view = _this.component.view;
var eventDef = _this.eventRange.def;
var eventInstance = _this.eventRange.instance;
var eventApi = new core.EventApi(calendar, eventDef, eventInstance);
var relevantEvents = _this.relevantEvents;
var mutatedRelevantEvents = _this.mutatedRelevantEvents;
calendar.publiclyTrigger('eventResizeStop', [
{
el: _this.draggingSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
view: view
}
]);
if (_this.validMutation) {
calendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: mutatedRelevantEvents
});
calendar.publiclyTrigger('eventResize', [
{
el: _this.draggingSeg.el,
startDelta: _this.validMutation.startDelta || core.createDuration(0),
endDelta: _this.validMutation.endDelta || core.createDuration(0),
prevEvent: eventApi,
event: new core.EventApi(// the data AFTER the mutation
calendar, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null),
revert: function () {
calendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: relevantEvents
});
},
jsEvent: ev.origEvent,
view: view
}
]);
}
else {
calendar.publiclyTrigger('_noEventResize');
}
// reset all internal state
_this.draggingSeg = null;
_this.relevantEvents = null;
_this.validMutation = null;
// okay to keep eventInstance around. useful to set it in handlePointerDown
};
var component = settings.component;
var dragging = _this.dragging = new FeaturefulElementDragging(component.el);
dragging.pointer.selector = '.fc-resizer';
dragging.touchScrollAllowed = false;
dragging.autoScroller.isEnabled = component.opt('dragScroll');
var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings));
hitDragging.emitter.on('pointerdown', _this.handlePointerDown);
hitDragging.emitter.on('dragstart', _this.handleDragStart);
hitDragging.emitter.on('hitupdate', _this.handleHitUpdate);
hitDragging.emitter.on('dragend', _this.handleDragEnd);
return _this;
}
EventDragging.prototype.destroy = function () {
this.dragging.destroy();
};
EventDragging.prototype.querySeg = function (ev) {
return core.getElSeg(core.elementClosest(ev.subjectEl, this.component.fgSegSelector));
};
return EventDragging;
}(core.Interaction));
function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) {
var dateEnv = hit0.component.dateEnv;
var date0 = hit0.dateSpan.range.start;
var date1 = hit1.dateSpan.range.start;
var delta = core.diffDates(date0, date1, dateEnv, hit0.component.largeUnit);
var props = {};
for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) {
var transform = transforms_1[_i];
var res = transform(hit0, hit1);
if (res === false) {
return null;
}
else if (res) {
__assign(props, res);
}
}
if (isFromStart) {
if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
props.startDelta = delta;
return props;
}
}
else {
if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
props.endDelta = delta;
return props;
}
}
return null;
}
var UnselectAuto = /** @class */ (function () {
function UnselectAuto(calendar) {
var _this = this;
this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
this.onSelect = function (selectInfo) {
if (selectInfo.jsEvent) {
_this.isRecentPointerDateSelect = true;
}
};
this.onDocumentPointerUp = function (pev) {
var _a = _this, calendar = _a.calendar, documentPointer = _a.documentPointer;
var state = calendar.state;
// touch-scrolling should never unfocus any type of selection
if (!documentPointer.wasTouchScroll) {
if (state.dateSelection && // an existing date selection?
!_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
) {
var unselectAuto = calendar.viewOpt('unselectAuto');
var unselectCancel = calendar.viewOpt('unselectCancel');
if (unselectAuto && (!unselectAuto || !core.elementClosest(documentPointer.downEl, unselectCancel))) {
calendar.unselect(pev);
}
}
if (state.eventSelection && // an existing event selected?
!core.elementClosest(documentPointer.downEl, EventDragging.SELECTOR) // interaction DIDN'T start on an event
) {
calendar.dispatch({ type: 'UNSELECT_EVENT' });
}
}
_this.isRecentPointerDateSelect = false;
};
this.calendar = calendar;
var documentPointer = this.documentPointer = new PointerDragging(document);
documentPointer.shouldIgnoreMove = true;
documentPointer.shouldWatchScroll = false;
documentPointer.emitter.on('pointerup', this.onDocumentPointerUp);
/*
TODO: better way to know about whether there was a selection with the pointer
*/
calendar.on('select', this.onSelect);
}
UnselectAuto.prototype.destroy = function () {
this.calendar.off('select', this.onSelect);
this.documentPointer.destroy();
};
return UnselectAuto;
}());
/*
Given an already instantiated draggable object for one-or-more elements,
Interprets any dragging as an attempt to drag an events that lives outside
of a calendar onto a calendar.
*/
var ExternalElementDragging = /** @class */ (function () {
function ExternalElementDragging(dragging, suppliedDragMeta) {
var _this = this;
this.receivingCalendar = null;
this.droppableEvent = null; // will exist for all drags, even if create:false
this.suppliedDragMeta = null;
this.dragMeta = null;
this.handleDragStart = function (ev) {
_this.dragMeta = _this.buildDragMeta(ev.subjectEl);
};
this.handleHitUpdate = function (hit, isFinal, ev) {
var dragging = _this.hitDragging.dragging;
var receivingCalendar = null;
var droppableEvent = null;
var isInvalid = false;
var interaction = {
affectedEvents: core.createEmptyEventStore(),
mutatedEvents: core.createEmptyEventStore(),
isEvent: _this.dragMeta.create,
origSeg: null
};
if (hit) {
receivingCalendar = hit.component.calendar;
if (_this.canDropElOnCalendar(ev.subjectEl, receivingCalendar)) {
droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingCalendar);
interaction.mutatedEvents = core.eventTupleToStore(droppableEvent);
isInvalid = !core.isInteractionValid(interaction, receivingCalendar);
if (isInvalid) {
interaction.mutatedEvents = core.createEmptyEventStore();
droppableEvent = null;
}
}
}
_this.displayDrag(receivingCalendar, interaction);
// show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
// TODO: wish we could somehow wait for dispatch to guarantee render
dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-mirror'));
if (!isInvalid) {
core.enableCursor();
}
else {
core.disableCursor();
}
if (!isFinal) {
dragging.setMirrorNeedsRevert(!droppableEvent);
_this.receivingCalendar = receivingCalendar;
_this.droppableEvent = droppableEvent;
}
};
this.handleDragEnd = function (pev) {
var _a = _this, receivingCalendar = _a.receivingCalendar, droppableEvent = _a.droppableEvent;
_this.clearDrag();
if (receivingCalendar && droppableEvent) {
var finalHit = _this.hitDragging.finalHit;
var finalView = finalHit.component.view;
var dragMeta = _this.dragMeta;
var arg = __assign({}, receivingCalendar.buildDatePointApi(finalHit.dateSpan), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView });
receivingCalendar.publiclyTrigger('drop', [arg]);
if (dragMeta.create) {
receivingCalendar.dispatch({
type: 'MERGE_EVENTS',
eventStore: core.eventTupleToStore(droppableEvent)
});
if (pev.isTouch) {
receivingCalendar.dispatch({
type: 'SELECT_EVENT',
eventInstanceId: droppableEvent.instance.instanceId
});
}
// signal that an external event landed
receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: pev.subjectEl,
event: new core.EventApi(receivingCalendar, droppableEvent.def, droppableEvent.instance),
view: finalView
}
]);
}
}
_this.receivingCalendar = null;
_this.droppableEvent = null;
};
var hitDragging = this.hitDragging = new HitDragging(dragging, core.interactionSettingsStore);
hitDragging.requireInitial = false; // will start outside of a component
hitDragging.emitter.on('dragstart', this.handleDragStart);
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
hitDragging.emitter.on('dragend', this.handleDragEnd);
this.suppliedDragMeta = suppliedDragMeta;
}
ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) {
if (typeof this.suppliedDragMeta === 'object') {
return core.parseDragMeta(this.suppliedDragMeta);
}
else if (typeof this.suppliedDragMeta === 'function') {
return core.parseDragMeta(this.suppliedDragMeta(subjectEl));
}
else {
return getDragMetaFromEl(subjectEl);
}
};
ExternalElementDragging.prototype.displayDrag = function (nextCalendar, state) {
var prevCalendar = this.receivingCalendar;
if (prevCalendar && prevCalendar !== nextCalendar) {
prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
if (nextCalendar) {
nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state });
}
};
ExternalElementDragging.prototype.clearDrag = function () {
if (this.receivingCalendar) {
this.receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
}
};
ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingCalendar) {
var dropAccept = receivingCalendar.opt('dropAccept');
if (typeof dropAccept === 'function') {
return dropAccept(el);
}
else if (typeof dropAccept === 'string' && dropAccept) {
return Boolean(core.elementMatches(el, dropAccept));
}
return true;
};
return ExternalElementDragging;
}());
// Utils for computing event store from the DragMeta
// ----------------------------------------------------------------------------------------------------
function computeEventForDateSpan(dateSpan, dragMeta, calendar) {
var defProps = __assign({}, dragMeta.leftoverProps);
for (var _i = 0, _a = calendar.pluginSystem.hooks.externalDefTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(defProps, transform(dateSpan, dragMeta));
}
var def = core.parseEventDef(defProps, dragMeta.sourceId, dateSpan.allDay, calendar.opt('forceEventDuration') || Boolean(dragMeta.duration), // hasEnd
calendar);
var start = dateSpan.range.start;
// only rely on time info if drop zone is all-day,
// otherwise, we already know the time
if (dateSpan.allDay && dragMeta.startTime) {
start = calendar.dateEnv.add(start, dragMeta.startTime);
}
var end = dragMeta.duration ?
calendar.dateEnv.add(start, dragMeta.duration) :
calendar.getDefaultEventEnd(dateSpan.allDay, start);
var instance = core.createEventInstance(def.defId, { start: start, end: end });
return { def: def, instance: instance };
}
// Utils for extracting data from element
// ----------------------------------------------------------------------------------------------------
function getDragMetaFromEl(el) {
var str = getEmbeddedElData(el, 'event');
var obj = str ?
JSON.parse(str) :
{ create: false }; // if no embedded data, assume no event creation
return core.parseDragMeta(obj);
}
core.config.dataAttrPrefix = '';
function getEmbeddedElData(el, name) {
var prefix = core.config.dataAttrPrefix;
var prefixedName = (prefix ? prefix + '-' : '') + name;
return el.getAttribute('data-' + prefixedName) || '';
}
/*
Makes an element (that is *external* to any calendar) draggable.
Can pass in data that determines how an event will be created when dropped onto a calendar.
Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
*/
var ExternalDraggable = /** @class */ (function () {
function ExternalDraggable(el, settings) {
var _this = this;
if (settings === void 0) { settings = {}; }
this.handlePointerDown = function (ev) {
var dragging = _this.dragging;
var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay;
dragging.minDistance =
minDistance != null ?
minDistance :
(ev.isTouch ? 0 : core.globalDefaults.eventDragMinDistance);
dragging.delay =
ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
(longPressDelay != null ? longPressDelay : core.globalDefaults.longPressDelay) :
0;
};
this.handleDragStart = function (ev) {
if (ev.isTouch &&
_this.dragging.delay &&
ev.subjectEl.classList.contains('fc-event')) {
_this.dragging.mirror.getMirrorEl().classList.add('fc-selected');
}
};
this.settings = settings;
var dragging = this.dragging = new FeaturefulElementDragging(el);
dragging.touchScrollAllowed = false;
if (settings.itemSelector != null) {
dragging.pointer.selector = settings.itemSelector;
}
if (settings.appendTo != null) {
dragging.mirror.parentNode = settings.appendTo; // TODO: write tests
}
dragging.emitter.on('pointerdown', this.handlePointerDown);
dragging.emitter.on('dragstart', this.handleDragStart);
new ExternalElementDragging(dragging, settings.eventData);
}
ExternalDraggable.prototype.destroy = function () {
this.dragging.destroy();
};
return ExternalDraggable;
}());
/*
Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
The third-party system is responsible for drawing the visuals effects of the drag.
This class simply monitors for pointer movements and fires events.
It also has the ability to hide the moving element (the "mirror") during the drag.
*/
var InferredElementDragging = /** @class */ (function (_super) {
__extends(InferredElementDragging, _super);
function InferredElementDragging(containerEl) {
var _this = _super.call(this, containerEl) || this;
_this.shouldIgnoreMove = false;
_this.mirrorSelector = '';
_this.currentMirrorEl = null;
_this.handlePointerDown = function (ev) {
_this.emitter.trigger('pointerdown', ev);
if (!_this.shouldIgnoreMove) {
// fire dragstart right away. does not support delay or min-distance
_this.emitter.trigger('dragstart', ev);
}
};
_this.handlePointerMove = function (ev) {
if (!_this.shouldIgnoreMove) {
_this.emitter.trigger('dragmove', ev);
}
};
_this.handlePointerUp = function (ev) {
_this.emitter.trigger('pointerup', ev);
if (!_this.shouldIgnoreMove) {
// fire dragend right away. does not support a revert animation
_this.emitter.trigger('dragend', ev);
}
};
var pointer = _this.pointer = new PointerDragging(containerEl);
pointer.emitter.on('pointerdown', _this.handlePointerDown);
pointer.emitter.on('pointermove', _this.handlePointerMove);
pointer.emitter.on('pointerup', _this.handlePointerUp);
return _this;
}
InferredElementDragging.prototype.destroy = function () {
this.pointer.destroy();
};
InferredElementDragging.prototype.setIgnoreMove = function (bool) {
this.shouldIgnoreMove = bool;
};
InferredElementDragging.prototype.setMirrorIsVisible = function (bool) {
if (bool) {
// restore a previously hidden element.
// use the reference in case the selector class has already been removed.
if (this.currentMirrorEl) {
this.currentMirrorEl.style.visibility = '';
this.currentMirrorEl = null;
}
}
else {
var mirrorEl = this.mirrorSelector ?
document.querySelector(this.mirrorSelector) :
null;
if (mirrorEl) {
this.currentMirrorEl = mirrorEl;
mirrorEl.style.visibility = 'hidden';
}
}
};
return InferredElementDragging;
}(core.ElementDragging));
/*
Bridges third-party drag-n-drop systems with FullCalendar.
Must be instantiated and destroyed by caller.
*/
var ThirdPartyDraggable = /** @class */ (function () {
function ThirdPartyDraggable(containerOrSettings, settings) {
var containerEl = document;
if (
// wish we could just test instanceof EventTarget, but doesn't work in IE11
containerOrSettings === document ||
containerOrSettings instanceof Element) {
containerEl = containerOrSettings;
settings = settings || {};
}
else {
settings = (containerOrSettings || {});
}
var dragging = this.dragging = new InferredElementDragging(containerEl);
if (typeof settings.itemSelector === 'string') {
dragging.pointer.selector = settings.itemSelector;
}
else if (containerEl === document) {
dragging.pointer.selector = '[data-event]';
}
if (typeof settings.mirrorSelector === 'string') {
dragging.mirrorSelector = settings.mirrorSelector;
}
new ExternalElementDragging(dragging, settings.eventData);
}
ThirdPartyDraggable.prototype.destroy = function () {
this.dragging.destroy();
};
return ThirdPartyDraggable;
}());
var main = core.createPlugin({
componentInteractions: [DateClicking, DateSelecting, EventDragging, EventDragging$1],
calendarInteractions: [UnselectAuto],
elementDraggingImpl: FeaturefulElementDragging
});
exports.Draggable = ExternalDraggable;
exports.FeaturefulElementDragging = FeaturefulElementDragging;
exports.PointerDragging = PointerDragging;
exports.ThirdPartyDraggable = ThirdPartyDraggable;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/interaction' {
import FeaturefulElementDragging from '@fullcalendar/interaction/dnd/FeaturefulElementDragging';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
export { FeaturefulElementDragging };
export { default as PointerDragging } from '@fullcalendar/interaction/dnd/PointerDragging';
export { default as Draggable } from '@fullcalendar/interaction/interactions-external/ExternalDraggable';
export { default as ThirdPartyDraggable } from '@fullcalendar/interaction/interactions-external/ThirdPartyDraggable';
}
declare module '@fullcalendar/interaction/dnd/FeaturefulElementDragging' {
import { PointerDragEvent, ElementDragging } from '@fullcalendar/core';
import PointerDragging from '@fullcalendar/interaction/dnd/PointerDragging';
import ElementMirror from '@fullcalendar/interaction/dnd/ElementMirror';
import AutoScroller from '@fullcalendar/interaction/dnd/AutoScroller';
export { FeaturefulElementDragging as default, FeaturefulElementDragging };
class FeaturefulElementDragging extends ElementDragging {
pointer: PointerDragging;
mirror: ElementMirror;
autoScroller: AutoScroller;
delay: number | null;
minDistance: number;
touchScrollAllowed: boolean;
mirrorNeedsRevert: boolean;
isInteracting: boolean;
isDragging: boolean;
isDelayEnded: boolean;
isDistanceSurpassed: boolean;
delayTimeoutId: number | null;
constructor(containerEl: HTMLElement);
destroy(): void;
onPointerDown: (ev: PointerDragEvent) => void;
onPointerMove: (ev: PointerDragEvent) => void;
onPointerUp: (ev: PointerDragEvent) => void;
startDelay(ev: PointerDragEvent): void;
handleDelayEnd(ev: PointerDragEvent): void;
handleDistanceSurpassed(ev: PointerDragEvent): void;
tryStartDrag(ev: PointerDragEvent): void;
tryStopDrag(ev: PointerDragEvent): void;
stopDrag(ev: PointerDragEvent): void;
setIgnoreMove(bool: boolean): void;
setMirrorIsVisible(bool: boolean): void;
setMirrorNeedsRevert(bool: boolean): void;
setAutoScrollEnabled(bool: boolean): void;
}
}
declare module '@fullcalendar/interaction/dnd/PointerDragging' {
import { EmitterMixin, PointerDragEvent } from '@fullcalendar/core';
export { PointerDragging as default, PointerDragging };
class PointerDragging {
containerEl: EventTarget;
subjectEl: HTMLElement | null;
downEl: HTMLElement | null;
emitter: EmitterMixin;
selector: string;
handleSelector: string;
shouldIgnoreMove: boolean;
shouldWatchScroll: boolean;
isDragging: boolean;
isTouchDragging: boolean;
wasTouchScroll: boolean;
origPageX: number;
origPageY: number;
prevPageX: number;
prevPageY: number;
prevScrollX: number;
prevScrollY: number;
constructor(containerEl: EventTarget);
destroy(): void;
tryStart(ev: UIEvent): boolean;
cleanup(): void;
querySubjectEl(ev: UIEvent): HTMLElement;
handleMouseDown: (ev: MouseEvent) => void;
handleMouseMove: (ev: MouseEvent) => void;
handleMouseUp: (ev: MouseEvent) => void;
shouldIgnoreMouse(): number | boolean;
handleTouchStart: (ev: TouchEvent) => void;
handleTouchMove: (ev: TouchEvent) => void;
handleTouchEnd: (ev: TouchEvent) => void;
handleTouchScroll: () => void;
cancelTouchScroll(): void;
initScrollWatch(ev: PointerDragEvent): void;
recordCoords(ev: PointerDragEvent): void;
handleScroll: (ev: UIEvent) => void;
destroyScrollWatch(): void;
createEventFromMouse(ev: MouseEvent, isFirst?: boolean): PointerDragEvent;
createEventFromTouch(ev: TouchEvent, isFirst?: boolean): PointerDragEvent;
}
}
declare module '@fullcalendar/interaction/interactions-external/ExternalDraggable' {
import { PointerDragEvent } from '@fullcalendar/core';
import FeaturefulElementDragging from '@fullcalendar/interaction/dnd/FeaturefulElementDragging';
import { DragMetaGenerator } from '@fullcalendar/interaction/interactions-external/ExternalElementDragging';
export interface ExternalDraggableSettings {
eventData?: DragMetaGenerator;
itemSelector?: string;
minDistance?: number;
longPressDelay?: number;
appendTo?: HTMLElement;
}
export { ExternalDraggable as default, ExternalDraggable };
class ExternalDraggable {
dragging: FeaturefulElementDragging;
settings: ExternalDraggableSettings;
constructor(el: HTMLElement, settings?: ExternalDraggableSettings);
handlePointerDown: (ev: PointerDragEvent) => void;
handleDragStart: (ev: PointerDragEvent) => void;
destroy(): void;
}
}
declare module '@fullcalendar/interaction/interactions-external/ThirdPartyDraggable' {
import { DragMetaGenerator } from '@fullcalendar/interaction/interactions-external/ExternalElementDragging';
import InferredElementDragging from '@fullcalendar/interaction/interactions-external/InferredElementDragging';
export interface ThirdPartyDraggableSettings {
eventData?: DragMetaGenerator;
itemSelector?: string;
mirrorSelector?: string;
}
export { ThirdPartyDraggable as default, ThirdPartyDraggable };
class ThirdPartyDraggable {
dragging: InferredElementDragging;
constructor(containerOrSettings?: EventTarget | ThirdPartyDraggableSettings, settings?: ThirdPartyDraggableSettings);
destroy(): void;
}
}
declare module '@fullcalendar/interaction/dnd/ElementMirror' {
import { Rect } from '@fullcalendar/core';
export { ElementMirror as default, ElementMirror };
class ElementMirror {
isVisible: boolean;
origScreenX?: number;
origScreenY?: number;
deltaX?: number;
deltaY?: number;
sourceEl: HTMLElement | null;
mirrorEl: HTMLElement | null;
sourceElRect: Rect | null;
parentNode: HTMLElement;
zIndex: number;
revertDuration: number;
start(sourceEl: HTMLElement, pageX: number, pageY: number): void;
handleMove(pageX: number, pageY: number): void;
setIsVisible(bool: boolean): void;
stop(needsRevertAnimation: boolean, callback: () => void): void;
doRevertAnimation(callback: () => void, revertDuration: number): void;
cleanup(): void;
updateElPosition(): void;
getMirrorEl(): HTMLElement;
}
}
declare module '@fullcalendar/interaction/dnd/AutoScroller' {
import { ScrollGeomCache } from '@fullcalendar/interaction/scroll-geom-cache';
export { AutoScroller as default, AutoScroller };
class AutoScroller {
isEnabled: boolean;
scrollQuery: (Window | string)[];
edgeThreshold: number;
maxVelocity: number;
pointerScreenX: number | null;
pointerScreenY: number | null;
isAnimating: boolean;
scrollCaches: ScrollGeomCache[] | null;
msSinceRequest?: number;
everMovedUp: boolean;
everMovedDown: boolean;
everMovedLeft: boolean;
everMovedRight: boolean;
start(pageX: number, pageY: number): void;
handleMove(pageX: number, pageY: number): void;
stop(): void;
requestAnimation(now: number): void;
}
}
declare module '@fullcalendar/interaction/interactions-external/ExternalElementDragging' {
import { Hit, PointerDragEvent, EventTuple, DatePointApi, Calendar, EventInteractionState, DragMetaInput, DragMeta, View, ElementDragging } from '@fullcalendar/core';
import HitDragging from '@fullcalendar/interaction/interactions/HitDragging';
export type DragMetaGenerator = DragMetaInput | ((el: HTMLElement) => DragMetaInput);
export interface ExternalDropApi extends DatePointApi {
draggedEl: HTMLElement;
jsEvent: UIEvent;
view: View;
}
export { ExternalElementDragging as default, ExternalElementDragging };
class ExternalElementDragging {
hitDragging: HitDragging;
receivingCalendar: Calendar | null;
droppableEvent: EventTuple | null;
suppliedDragMeta: DragMetaGenerator | null;
dragMeta: DragMeta | null;
constructor(dragging: ElementDragging, suppliedDragMeta?: DragMetaGenerator);
handleDragStart: (ev: PointerDragEvent) => void;
buildDragMeta(subjectEl: HTMLElement): DragMeta;
handleHitUpdate: (hit: Hit, isFinal: boolean, ev: PointerDragEvent) => void;
handleDragEnd: (pev: PointerDragEvent) => void;
displayDrag(nextCalendar: Calendar | null, state: EventInteractionState): void;
clearDrag(): void;
canDropElOnCalendar(el: HTMLElement, receivingCalendar: Calendar): boolean;
}
}
declare module '@fullcalendar/interaction/interactions-external/InferredElementDragging' {
import { PointerDragEvent, ElementDragging } from '@fullcalendar/core';
import PointerDragging from '@fullcalendar/interaction/dnd/PointerDragging';
export { InferredElementDragging as default, InferredElementDragging };
class InferredElementDragging extends ElementDragging {
pointer: PointerDragging;
shouldIgnoreMove: boolean;
mirrorSelector: string;
currentMirrorEl: HTMLElement | null;
constructor(containerEl: HTMLElement);
destroy(): void;
handlePointerDown: (ev: PointerDragEvent) => void;
handlePointerMove: (ev: PointerDragEvent) => void;
handlePointerUp: (ev: PointerDragEvent) => void;
setIgnoreMove(bool: boolean): void;
setMirrorIsVisible(bool: boolean): void;
}
}
declare module '@fullcalendar/interaction/scroll-geom-cache' {
import { Rect, ScrollController } from '@fullcalendar/core';
export abstract class ScrollGeomCache extends ScrollController {
clientRect: Rect;
origScrollTop: number;
origScrollLeft: number;
protected scrollController: ScrollController;
protected doesListening: boolean;
protected scrollTop: number;
protected scrollLeft: number;
protected scrollWidth: number;
protected scrollHeight: number;
protected clientWidth: number;
protected clientHeight: number;
constructor(scrollController: ScrollController, doesListening: boolean);
abstract getEventTarget(): EventTarget;
abstract computeClientRect(): Rect;
destroy(): void;
handleScroll: () => void;
getScrollTop(): number;
getScrollLeft(): number;
setScrollTop(top: number): void;
setScrollLeft(top: number): void;
getClientWidth(): number;
getClientHeight(): number;
getScrollWidth(): number;
getScrollHeight(): number;
handleScrollChange(): void;
}
export class ElementScrollGeomCache extends ScrollGeomCache {
constructor(el: HTMLElement, doesListening: boolean);
getEventTarget(): EventTarget;
computeClientRect(): {
left: number;
right: number;
top: number;
bottom: number;
};
}
export class WindowScrollGeomCache extends ScrollGeomCache {
constructor(doesListening: boolean);
getEventTarget(): EventTarget;
computeClientRect(): Rect;
handleScrollChange(): void;
}
}
declare module '@fullcalendar/interaction/interactions/HitDragging' {
import { EmitterMixin, PointerDragEvent, Point, Hit, InteractionSettingsStore, ElementDragging } from '@fullcalendar/core';
import OffsetTracker from '@fullcalendar/interaction/OffsetTracker';
export { HitDragging as default, HitDragging };
class HitDragging {
droppableStore: InteractionSettingsStore;
dragging: ElementDragging;
emitter: EmitterMixin;
useSubjectCenter: boolean;
requireInitial: boolean;
offsetTrackers: {
[componentUid: string]: OffsetTracker;
};
initialHit: Hit | null;
movingHit: Hit | null;
finalHit: Hit | null;
coordAdjust?: Point;
constructor(dragging: ElementDragging, droppableStore: InteractionSettingsStore);
handlePointerDown: (ev: PointerDragEvent) => void;
processFirstCoord(ev: PointerDragEvent): void;
handleDragStart: (ev: PointerDragEvent) => void;
handleDragMove: (ev: PointerDragEvent) => void;
handlePointerUp: (ev: PointerDragEvent) => void;
handleDragEnd: (ev: PointerDragEvent) => void;
handleMove(ev: PointerDragEvent, forceHandle?: boolean): void;
prepareHits(): void;
releaseHits(): void;
queryHitForOffset(offsetLeft: number, offsetTop: number): Hit | null;
}
export function isHitsEqual(hit0: Hit | null, hit1: Hit | null): boolean;
}
declare module '@fullcalendar/interaction/OffsetTracker' {
import { Rect } from '@fullcalendar/core';
import { ElementScrollGeomCache } from '@fullcalendar/interaction/scroll-geom-cache';
export { OffsetTracker as default, OffsetTracker };
class OffsetTracker {
scrollCaches: ElementScrollGeomCache[];
origRect: Rect;
constructor(el: HTMLElement);
destroy(): void;
computeLeft(): number;
computeTop(): number;
isWithinClipping(pageX: number, pageY: number): boolean;
}
}
/*!
FullCalendar Interaction Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):t((e=e||self).FullCalendarInteraction={},e.FullCalendar)}(this,function(e,t){"use strict";var n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)};function r(e,t){function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}var i=function(){return(i=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0&&(this.everMovedDown=!0),o<0?this.everMovedLeft=!0:o>0&&(this.everMovedRight=!0),this.pointerScreenX=n,this.pointerScreenY=r,this.isAnimating||(this.isAnimating=!0,this.requestAnimation(p()))}},e.prototype.stop=function(){if(this.isEnabled){this.isAnimating=!1;for(var e=0,t=this.scrollCaches;e=0&&c>=0&&d>=0&&g>=0&&(d<=n&&this.everMovedUp&&a.canScrollUp()&&(!r||r.distance>d)&&(r={scrollCache:a,name:"top",distance:d}),g<=n&&this.everMovedDown&&a.canScrollDown()&&(!r||r.distance>g)&&(r={scrollCache:a,name:"bottom",distance:g}),s<=n&&this.everMovedLeft&&a.canScrollLeft()&&(!r||r.distance>s)&&(r={scrollCache:a,name:"left",distance:s}),c<=n&&this.everMovedRight&&a.canScrollRight()&&(!r||r.distance>c)&&(r={scrollCache:a,name:"right",distance:c}))}return r},e.prototype.buildCaches=function(){return this.queryScrollEls().map(function(e){return e===window?new h(!1):new u(e,!1)})},e.prototype.queryScrollEls=function(){for(var e=[],t=0,n=this.scrollQuery;t=t*t&&r.handleDistanceSurpassed(e)}r.isDragging&&("scroll"!==e.origEvent.type&&(r.mirror.handleMove(e.pageX,e.pageY),r.autoScroller.handleMove(e.pageX,e.pageY)),r.emitter.trigger("dragmove",e))}},r.onPointerUp=function(e){r.isInteracting&&(r.isInteracting=!1,t.allowSelection(document.body),t.allowContextMenu(document.body),r.emitter.trigger("pointerup",e),r.isDragging&&(r.autoScroller.stop(),r.tryStopDrag(e)),r.delayTimeoutId&&(clearTimeout(r.delayTimeoutId),r.delayTimeoutId=null))};var i=r.pointer=new s(n);return i.emitter.on("pointerdown",r.onPointerDown),i.emitter.on("pointermove",r.onPointerMove),i.emitter.on("pointerup",r.onPointerUp),r.mirror=new d,r.autoScroller=new v,r}return r(n,e),n.prototype.destroy=function(){this.pointer.destroy()},n.prototype.startDelay=function(e){var t=this;"number"==typeof this.delay?this.delayTimeoutId=setTimeout(function(){t.delayTimeoutId=null,t.handleDelayEnd(e)},this.delay):this.handleDelayEnd(e)},n.prototype.handleDelayEnd=function(e){this.isDelayEnded=!0,this.tryStartDrag(e)},n.prototype.handleDistanceSurpassed=function(e){this.isDistanceSurpassed=!0,this.tryStartDrag(e)},n.prototype.tryStartDrag=function(e){this.isDelayEnded&&this.isDistanceSurpassed&&(this.pointer.wasTouchScroll&&!this.touchScrollAllowed||(this.isDragging=!0,this.mirrorNeedsRevert=!1,this.autoScroller.start(e.pageX,e.pageY),this.emitter.trigger("dragstart",e),!1===this.touchScrollAllowed&&this.pointer.cancelTouchScroll()))},n.prototype.tryStopDrag=function(e){this.mirror.stop(this.mirrorNeedsRevert,this.stopDrag.bind(this,e))},n.prototype.stopDrag=function(e){this.isDragging=!1,this.emitter.trigger("dragend",e)},n.prototype.setIgnoreMove=function(e){this.pointer.shouldIgnoreMove=e},n.prototype.setMirrorIsVisible=function(e){this.mirror.setIsVisible(e)},n.prototype.setMirrorNeedsRevert=function(e){this.mirrorNeedsRevert=e},n.prototype.setAutoScrollEnabled=function(e){this.autoScroller.isEnabled=e},n}(t.ElementDragging),E=function(){function e(e){this.origRect=t.computeRect(e),this.scrollCaches=t.getClippingParents(e).map(function(e){return new u(e,!0)})}return e.prototype.destroy=function(){for(var e=0,t=this.scrollCaches;e=0&&g
=0&&uo.layer)||(f.rect.left+=c,f.rect.right+=c,f.rect.top+=d,f.rect.bottom+=d,o=f)}}}return o},e}();function S(e,n){return!e&&!n||Boolean(e)===Boolean(n)&&t.isDateSpansEqual(e.dateSpan,n.dateSpan)}var y=function(e){function n(n){var r=e.call(this,n)||this;r.handlePointerDown=function(e){var t=r.dragging;t.setIgnoreMove(!r.component.isValidDateDownEl(t.pointer.downEl))},r.handleDragEnd=function(e){var t=r.component;if(!r.dragging.pointer.wasTouchScroll){var n=r.hitDragging,i=n.initialHit,o=n.finalHit;i&&o&&S(i,o)&&t.calendar.triggerDateClick(i.dateSpan,i.dayEl,t.view,e.origEvent)}};var i=n.component;r.dragging=new f(i.el),r.dragging.autoScroller.isEnabled=!1;var o=r.hitDragging=new m(r.dragging,t.interactionSettingsToStore(n));return o.emitter.on("pointerdown",r.handlePointerDown),o.emitter.on("dragend",r.handleDragEnd),r}return r(n,e),n.prototype.destroy=function(){this.dragging.destroy()},n}(t.Interaction),D=function(e){function n(n){var r=e.call(this,n)||this;r.dragSelection=null,r.handlePointerDown=function(e){var t=r,n=t.component,i=t.dragging,o=n.opt("selectable")&&n.isValidDateDownEl(e.origEvent.target);i.setIgnoreMove(!o),i.delay=e.isTouch?function(e){var t=e.opt("selectLongPressDelay");null==t&&(t=e.opt("longPressDelay"));return t}(n):null},r.handleDragStart=function(e){r.component.calendar.unselect(e)},r.handleHitUpdate=function(e,n){var o=r.component.calendar,a=null,l=!1;e&&((a=function(e,n,r){var o=e.dateSpan,a=n.dateSpan,l=[o.range.start,o.range.end,a.range.start,a.range.end];l.sort(t.compareNumbers);for(var s={},c=0,d=r;co.start)return g.endDelta=d,g;return null}(s,e,o.subjectEl.classList.contains("fc-start-resizer"),c.range,a.pluginSystem.hooks.eventResizeJoinTransforms)),d&&(g=t.applyMutationToEventStore(l,a.eventUiBases,d,a),h.mutatedEvents=g,r.component.isInteractionValid(h)||(u=!0,d=null,g=null,h.mutatedEvents=null)),g?a.dispatch({type:"SET_EVENT_RESIZE",state:h}):a.dispatch({type:"UNSET_EVENT_RESIZE"}),u?t.disableCursor():t.enableCursor(),n||(d&&S(s,e)&&(d=null),r.validMutation=d,r.mutatedRelevantEvents=g)},r.handleDragEnd=function(e){var n=r.component.calendar,i=r.component.view,o=r.eventRange.def,a=r.eventRange.instance,l=new t.EventApi(n,o,a),s=r.relevantEvents,c=r.mutatedRelevantEvents;n.publiclyTrigger("eventResizeStop",[{el:r.draggingSeg.el,event:l,jsEvent:e.origEvent,view:i}]),r.validMutation?(n.dispatch({type:"MERGE_EVENTS",eventStore:c}),n.publiclyTrigger("eventResize",[{el:r.draggingSeg.el,startDelta:r.validMutation.startDelta||t.createDuration(0),endDelta:r.validMutation.endDelta||t.createDuration(0),prevEvent:l,event:new t.EventApi(n,c.defs[o.defId],a?c.instances[a.instanceId]:null),revert:function(){n.dispatch({type:"MERGE_EVENTS",eventStore:s})},jsEvent:e.origEvent,view:i}])):n.publiclyTrigger("_noEventResize"),r.draggingSeg=null,r.relevantEvents=null,r.validMutation=null};var o=n.component,a=r.dragging=new f(o.el);a.pointer.selector=".fc-resizer",a.touchScrollAllowed=!1,a.autoScroller.isEnabled=o.opt("dragScroll");var l=r.hitDragging=new m(r.dragging,t.interactionSettingsToStore(n));return l.emitter.on("pointerdown",r.handlePointerDown),l.emitter.on("dragstart",r.handleDragStart),l.emitter.on("hitupdate",r.handleHitUpdate),l.emitter.on("dragend",r.handleDragEnd),r}return r(n,e),n.prototype.destroy=function(){this.dragging.destroy()},n.prototype.querySeg=function(e){return t.getElSeg(t.elementClosest(e.subjectEl,this.component.fgSegSelector))},n}(t.Interaction);var M=function(){function e(e){var n=this;this.isRecentPointerDateSelect=!1,this.onSelect=function(e){e.jsEvent&&(n.isRecentPointerDateSelect=!0)},this.onDocumentPointerUp=function(e){var r=n,i=r.calendar,o=r.documentPointer,a=i.state;if(!o.wasTouchScroll){if(a.dateSelection&&!n.isRecentPointerDateSelect){var l=i.viewOpt("unselectAuto"),s=i.viewOpt("unselectCancel");!l||l&&t.elementClosest(o.downEl,s)||i.unselect(e)}a.eventSelection&&!t.elementClosest(o.downEl,w.SELECTOR)&&i.dispatch({type:"UNSELECT_EVENT"})}n.isRecentPointerDateSelect=!1},this.calendar=e;var r=this.documentPointer=new s(document);r.shouldIgnoreMove=!0,r.shouldWatchScroll=!1,r.emitter.on("pointerup",this.onDocumentPointerUp),e.on("select",this.onSelect)}return e.prototype.destroy=function(){this.calendar.off("select",this.onSelect),this.documentPointer.destroy()},e}(),b=function(){function e(e,n){var r=this;this.receivingCalendar=null,this.droppableEvent=null,this.suppliedDragMeta=null,this.dragMeta=null,this.handleDragStart=function(e){r.dragMeta=r.buildDragMeta(e.subjectEl)},this.handleHitUpdate=function(e,n,o){var a=r.hitDragging.dragging,l=null,s=null,c=!1,d={affectedEvents:t.createEmptyEventStore(),mutatedEvents:t.createEmptyEventStore(),isEvent:r.dragMeta.create,origSeg:null};e&&(l=e.component.calendar,r.canDropElOnCalendar(o.subjectEl,l)&&(s=function(e,n,r){for(var o=i({},n.leftoverProps),a=0,l=r.pluginSystem.hooks.externalDefTransforms;a
/*!
FullCalendar Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { createFormatter, removeElement, cssToStr, isMultiDayRange, htmlEscape, compareByFieldSpecs, applyStyle, FgEventRenderer, buildSegCompareObj, FillRenderer, memoizeRendering, createDuration, wholeDivideDurations, findElements, PositionCache, startOfDay, asRoughMs, formatIsoTimeString, addDurations, htmlToElement, createElement, multiplyDuration, DateComponent, hasBgRendering, Splitter, diffDays, buildGotoAnchorHtml, getAllDayHtml, ScrollComponent, matchCellWidths, uncompensateScroll, compensateScroll, subtractInnerElHeight, View, memoize, intersectRanges, Slicer, DayHeader, DaySeries, DayTable, createPlugin } from '@fullcalendar/core';
import { DayBgRow, DayGrid, SimpleDayGrid } from '@fullcalendar/daygrid';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/*
Only handles foreground segs.
Does not own rendering. Use for low-level util methods by TimeGrid.
*/
var TimeGridEventRenderer = /** @class */ (function (_super) {
__extends(TimeGridEventRenderer, _super);
function TimeGridEventRenderer(timeGrid) {
var _this = _super.call(this, timeGrid.context) || this;
_this.timeGrid = timeGrid;
_this.fullTimeFormat = createFormatter({
hour: 'numeric',
minute: '2-digit',
separator: _this.context.options.defaultRangeSeparator
});
return _this;
}
// Given an array of foreground segments, render a DOM element for each, computes position,
// and attaches to the column inner-container elements.
TimeGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var segsByCol = this.timeGrid.groupSegsByCol(segs);
// order the segs within each column
// TODO: have groupSegsByCol do this?
for (var col = 0; col < segsByCol.length; col++) {
segsByCol[col] = this.sortEventSegs(segsByCol[col]);
}
this.segsByCol = segsByCol;
this.timeGrid.attachSegsByCol(segsByCol, this.timeGrid.fgContainerEls);
};
TimeGridEventRenderer.prototype.detachSegs = function (segs) {
segs.forEach(function (seg) {
removeElement(seg.el);
});
this.segsByCol = null;
};
TimeGridEventRenderer.prototype.computeSegSizes = function (allSegs) {
var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol;
var colCnt = timeGrid.colCnt;
timeGrid.computeSegVerticals(allSegs); // horizontals relies on this
if (segsByCol) {
for (var col = 0; col < colCnt; col++) {
this.computeSegHorizontals(segsByCol[col]); // compute horizontal coordinates, z-index's, and reorder the array
}
}
};
TimeGridEventRenderer.prototype.assignSegSizes = function (allSegs) {
var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol;
var colCnt = timeGrid.colCnt;
timeGrid.assignSegVerticals(allSegs); // horizontals relies on this
if (segsByCol) {
for (var col = 0; col < colCnt; col++) {
this.assignSegCss(segsByCol[col]);
}
}
};
// Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined
TimeGridEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
meridiem: false
};
};
// Computes a default `displayEventEnd` value if one is not expliclty defined
TimeGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return true;
};
// Renders the HTML for a single event segment's default rendering
TimeGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var view = this.context.view;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var allDay = eventDef.allDay;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
var skinCss = cssToStr(this.getSkinCss(eventUi));
var timeText;
var fullTimeText; // more verbose time text. for the print stylesheet
var startTimeText; // just the start time text
classes.unshift('fc-time-grid-event');
// if the event appears to span more than one day...
if (isMultiDayRange(eventRange.range)) {
// Don't display time text on segments that run entirely through a day.
// That would appear as midnight-midnight and would look dumb.
// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
if (seg.isStart || seg.isEnd) {
var unzonedStart = seg.start;
var unzonedEnd = seg.end;
timeText = this._getTimeText(unzonedStart, unzonedEnd, allDay); // TODO: give the timezones
fullTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, this.fullTimeFormat);
startTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, null, false); // displayEnd=false
}
}
else {
// Display the normal time text for the *event's* times
timeText = this.getTimeText(eventRange);
fullTimeText = this.getTimeText(eventRange, this.fullTimeFormat);
startTimeText = this.getTimeText(eventRange, null, false); // displayEnd=false
}
return '' +
'
(timeText ?
'
'' + htmlEscape(timeText) + '' +
'
'') +
(eventDef.title ?
'
htmlEscape(eventDef.title) +
'
'') +
'
/* TODO: write CSS for this
(isResizableFromStart ?
'' :
''
) +
*/
(isResizableFromEnd ?
'' :
'') +
'';
};
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
// Assumed the segs are already ordered.
// NOTE: Also reorders the given array by date!
TimeGridEventRenderer.prototype.computeSegHorizontals = function (segs) {
var levels;
var level0;
var i;
levels = buildSlotSegLevels(segs);
computeForwardSlotSegs(levels);
if ((level0 = levels[0])) {
for (i = 0; i < level0.length; i++) {
computeSlotSegPressures(level0[i]);
}
for (i = 0; i < level0.length; i++) {
this.computeSegForwardBack(level0[i], 0, 0);
}
}
};
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
//
// The segment might be part of a "series", which means consecutive segments with the same pressure
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
// coordinate of the first segment in the series.
TimeGridEventRenderer.prototype.computeSegForwardBack = function (seg, seriesBackwardPressure, seriesBackwardCoord) {
var forwardSegs = seg.forwardSegs;
var i;
if (seg.forwardCoord === undefined) { // not already computed
if (!forwardSegs.length) {
// if there are no forward segments, this segment should butt up against the edge
seg.forwardCoord = 1;
}
else {
// sort highest pressure first
this.sortForwardSegs(forwardSegs);
// this segment's forwardCoord will be calculated from the backwardCoord of the
// highest-pressure forward segment.
this.computeSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
seg.forwardCoord = forwardSegs[0].backwardCoord;
}
// calculate the backwardCoord from the forwardCoord. consider the series
seg.backwardCoord = seg.forwardCoord -
(seg.forwardCoord - seriesBackwardCoord) / // available width for series
(seriesBackwardPressure + 1); // # of segments in the series
// use this segment's coordinates to computed the coordinates of the less-pressurized
// forward segments
for (i = 0; i < forwardSegs.length; i++) {
this.computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
}
}
};
TimeGridEventRenderer.prototype.sortForwardSegs = function (forwardSegs) {
var objs = forwardSegs.map(buildTimeGridSegCompareObj);
var specs = [
// put higher-pressure first
{ field: 'forwardPressure', order: -1 },
// put segments that are closer to initial edge first (and favor ones with no coords yet)
{ field: 'backwardCoord', order: 1 }
].concat(this.context.view.eventOrderSpecs);
objs.sort(function (obj0, obj1) {
return compareByFieldSpecs(obj0, obj1, specs);
});
return objs.map(function (c) {
return c._seg;
});
};
// Given foreground event segments that have already had their position coordinates computed,
// assigns position-related CSS values to their elements.
TimeGridEventRenderer.prototype.assignSegCss = function (segs) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
applyStyle(seg.el, this.generateSegCss(seg));
if (seg.level > 0) {
seg.el.classList.add('fc-time-grid-event-inset');
}
// if the event is short that the title will be cut off,
// attach a className that condenses the title into the time area.
if (seg.eventRange.def.title && seg.bottom - seg.top < 30) {
seg.el.classList.add('fc-short'); // TODO: "condensed" is a better name
}
}
};
// Generates an object with CSS properties/values that should be applied to an event segment element.
// Contains important positioning-related properties that should be applied to any event element, customized or not.
TimeGridEventRenderer.prototype.generateSegCss = function (seg) {
var shouldOverlap = this.context.options.slotEventOverlap;
var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
var isRtl = this.timeGrid.isRtl;
var left; // amount of space from left edge, a fraction of the total width
var right; // amount of space from right edge, a fraction of the total width
if (shouldOverlap) {
// double the width, but don't go beyond the maximum forward coordinate (1.0)
forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
}
if (isRtl) {
left = 1 - forwardCoord;
right = backwardCoord;
}
else {
left = backwardCoord;
right = 1 - forwardCoord;
}
props.zIndex = seg.level + 1; // convert from 0-base to 1-based
props.left = left * 100 + '%';
props.right = right * 100 + '%';
if (shouldOverlap && seg.forwardPressure) {
// add padding to the edge so that forward stacked events don't cover the resizer's icon
props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
}
return props;
};
return TimeGridEventRenderer;
}(FgEventRenderer));
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
function buildSlotSegLevels(segs) {
var levels = [];
var i;
var seg;
var j;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
// go through all the levels and stop on the first level where there are no collisions
for (j = 0; j < levels.length; j++) {
if (!computeSlotSegCollisions(seg, levels[j]).length) {
break;
}
}
seg.level = j;
(levels[j] || (levels[j] = [])).push(seg);
}
return levels;
}
// For every segment, figure out the other segments that are in subsequent
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
function computeForwardSlotSegs(levels) {
var i;
var level;
var j;
var seg;
var k;
for (i = 0; i < levels.length; i++) {
level = levels[i];
for (j = 0; j < level.length; j++) {
seg = level[j];
seg.forwardSegs = [];
for (k = i + 1; k < levels.length; k++) {
computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
}
}
}
}
// Figure out which path forward (via seg.forwardSegs) results in the longest path until
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
function computeSlotSegPressures(seg) {
var forwardSegs = seg.forwardSegs;
var forwardPressure = 0;
var i;
var forwardSeg;
if (seg.forwardPressure === undefined) { // not already computed
for (i = 0; i < forwardSegs.length; i++) {
forwardSeg = forwardSegs[i];
// figure out the child's maximum forward path
computeSlotSegPressures(forwardSeg);
// either use the existing maximum, or use the child's forward pressure
// plus one (for the forwardSeg itself)
forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure);
}
seg.forwardPressure = forwardPressure;
}
}
// Find all the segments in `otherSegs` that vertically collide with `seg`.
// Append into an optionally-supplied `results` array and return.
function computeSlotSegCollisions(seg, otherSegs, results) {
if (results === void 0) { results = []; }
for (var i = 0; i < otherSegs.length; i++) {
if (isSlotSegCollision(seg, otherSegs[i])) {
results.push(otherSegs[i]);
}
}
return results;
}
// Do these segments occupy the same vertical space?
function isSlotSegCollision(seg1, seg2) {
return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
}
function buildTimeGridSegCompareObj(seg) {
var obj = buildSegCompareObj(seg);
obj.forwardPressure = seg.forwardPressure;
obj.backwardCoord = seg.backwardCoord;
return obj;
}
var TimeGridMirrorRenderer = /** @class */ (function (_super) {
__extends(TimeGridMirrorRenderer, _super);
function TimeGridMirrorRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimeGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
this.segsByCol = this.timeGrid.groupSegsByCol(segs);
this.timeGrid.attachSegsByCol(this.segsByCol, this.timeGrid.mirrorContainerEls);
this.sourceSeg = mirrorInfo.sourceSeg;
};
TimeGridMirrorRenderer.prototype.generateSegCss = function (seg) {
var props = _super.prototype.generateSegCss.call(this, seg);
var sourceSeg = this.sourceSeg;
if (sourceSeg && sourceSeg.col === seg.col) {
var sourceSegProps = _super.prototype.generateSegCss.call(this, sourceSeg);
props.left = sourceSegProps.left;
props.right = sourceSegProps.right;
props.marginLeft = sourceSegProps.marginLeft;
props.marginRight = sourceSegProps.marginRight;
}
return props;
};
return TimeGridMirrorRenderer;
}(TimeGridEventRenderer));
var TimeGridFillRenderer = /** @class */ (function (_super) {
__extends(TimeGridFillRenderer, _super);
function TimeGridFillRenderer(timeGrid) {
var _this = _super.call(this, timeGrid.context) || this;
_this.timeGrid = timeGrid;
return _this;
}
TimeGridFillRenderer.prototype.attachSegs = function (type, segs) {
var timeGrid = this.timeGrid;
var containerEls;
// TODO: more efficient lookup
if (type === 'bgEvent') {
containerEls = timeGrid.bgContainerEls;
}
else if (type === 'businessHours') {
containerEls = timeGrid.businessContainerEls;
}
else if (type === 'highlight') {
containerEls = timeGrid.highlightContainerEls;
}
timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls);
return segs.map(function (seg) {
return seg.el;
});
};
TimeGridFillRenderer.prototype.computeSegSizes = function (segs) {
this.timeGrid.computeSegVerticals(segs);
};
TimeGridFillRenderer.prototype.assignSegSizes = function (segs) {
this.timeGrid.assignSegVerticals(segs);
};
return TimeGridFillRenderer;
}(FillRenderer));
/* A component that renders one or more columns of vertical time slots
----------------------------------------------------------------------------------------------------------------------*/
// potential nice values for the slot-duration and interval-duration
// from largest to smallest
var AGENDA_STOCK_SUB_DURATIONS = [
{ hours: 1 },
{ minutes: 30 },
{ minutes: 15 },
{ seconds: 30 },
{ seconds: 15 }
];
var TimeGrid = /** @class */ (function (_super) {
__extends(TimeGrid, _super);
function TimeGrid(context, el, renderProps) {
var _this = _super.call(this, context, el) || this;
_this.isSlatSizesDirty = false;
_this.isColSizesDirty = false;
_this.renderSlats = memoizeRendering(_this._renderSlats);
var eventRenderer = _this.eventRenderer = new TimeGridEventRenderer(_this);
var fillRenderer = _this.fillRenderer = new TimeGridFillRenderer(_this);
_this.mirrorRenderer = new TimeGridMirrorRenderer(_this);
var renderColumns = _this.renderColumns = memoizeRendering(_this._renderColumns, _this._unrenderColumns);
_this.renderBusinessHours = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderColumns]);
_this.renderDateSelection = memoizeRendering(_this._renderDateSelection, _this._unrenderDateSelection, [renderColumns]);
_this.renderFgEvents = memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderColumns]);
_this.renderBgEvents = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderColumns]);
_this.renderEventSelection = memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderColumns]);
_this.renderEventResize = memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderColumns]);
_this.processOptions();
el.innerHTML =
'
var AllDaySplitter = /** @class */ (function (_super) {
__extends(AllDaySplitter, _super);
function AllDaySplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
AllDaySplitter.prototype.getKeyInfo = function () {
return {
allDay: {},
timed: {}
};
};
AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) {
if (dateSpan.allDay) {
return ['allDay'];
}
else {
return ['timed'];
}
};
AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) {
if (!eventDef.allDay) {
return ['timed'];
}
else if (hasBgRendering(eventDef)) {
return ['timed', 'allDay'];
}
else {
return ['allDay'];
}
};
return AllDaySplitter;
}(Splitter));
var TIMEGRID_ALL_DAY_EVENT_LIMIT = 5;
var WEEK_HEADER_FORMAT = createFormatter({ week: 'short' });
/* An abstract class for all timegrid-related views. Displays one more columns with time slots running vertically.
----------------------------------------------------------------------------------------------------------------------*/
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
// Responsible for managing width/height.
var TimeGridView = /** @class */ (function (_super) {
__extends(TimeGridView, _super);
function TimeGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.splitter = new AllDaySplitter();
/* Header Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before the day-of week header cells
_this.renderHeadIntroHtml = function () {
var _a = _this, theme = _a.theme, dateEnv = _a.dateEnv;
var range = _this.props.dateProfile.renderRange;
var dayCnt = diffDays(range.start, range.end);
var weekText;
if (_this.opt('weekNumbers')) {
weekText = dateEnv.format(range.start, WEEK_HEADER_FORMAT);
return '' +
'' +
buildGotoAnchorHtml(// aside from link, important for matchCellWidths
_this, { date: range.start, type: 'week', forceOff: dayCnt > 1 }, htmlEscape(weekText) // inner HTML
) +
'';
}
else {
return '';
}
};
/* Time Grid Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
_this.renderTimeGridBgIntroHtml = function () {
var theme = _this.theme;
return '';
};
// Generates the HTML that goes before all other types of cells.
// Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
_this.renderTimeGridIntroHtml = function () {
return '';
};
/* Day Grid Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that goes before the all-day cells
_this.renderDayGridBgIntroHtml = function () {
var theme = _this.theme;
return '' +
'' +
'' + // needed for matchCellWidths
getAllDayHtml(_this) +
'' +
'';
};
// Generates the HTML that goes before all other types of cells.
// Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
_this.renderDayGridIntroHtml = function () {
return '';
};
_this.el.classList.add('fc-timeGrid-view');
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.scroller = new ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
var timeGridWrapEl = _this.scroller.el;
_this.el.querySelector('.fc-body > tr > td').appendChild(timeGridWrapEl);
timeGridWrapEl.classList.add('fc-time-grid-container');
var timeGridEl = createElement('div', { className: 'fc-time-grid' });
timeGridWrapEl.appendChild(timeGridEl);
_this.timeGrid = new TimeGrid(_this.context, timeGridEl, {
renderBgIntroHtml: _this.renderTimeGridBgIntroHtml,
renderIntroHtml: _this.renderTimeGridIntroHtml
});
if (_this.opt('allDaySlot')) { // should we display the "all-day" area?
_this.dayGrid = new DayGrid(// the all-day subcomponent of this view
_this.context, _this.el.querySelector('.fc-day-grid'), {
renderNumberIntroHtml: _this.renderDayGridIntroHtml,
renderBgIntroHtml: _this.renderDayGridBgIntroHtml,
renderIntroHtml: _this.renderDayGridIntroHtml,
colWeekNumbersVisible: false,
cellWeekNumbersVisible: false
});
// have the day-grid extend it's coordinate area over the
var SimpleTimeGrid = /** @class */ (function (_super) {
__extends(SimpleTimeGrid, _super);
function SimpleTimeGrid(context, timeGrid) {
var _this = _super.call(this, context, timeGrid.el) || this;
_this.buildDayRanges = memoize(buildDayRanges);
_this.slicer = new TimeGridSlicer();
_this.timeGrid = timeGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeGrid.el
});
return _this;
}
SimpleTimeGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
SimpleTimeGrid.prototype.render = function (props) {
var dateProfile = props.dateProfile, dayTable = props.dayTable;
var dayRanges = this.dayRanges = this.buildDayRanges(dayTable, dateProfile, this.dateEnv);
this.timeGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, null, this.timeGrid, dayRanges), { dateProfile: dateProfile, cells: dayTable.cells[0] }));
};
SimpleTimeGrid.prototype.renderNowIndicator = function (date) {
this.timeGrid.renderNowIndicator(this.slicer.sliceNowDate(date, this.timeGrid, this.dayRanges), date);
};
SimpleTimeGrid.prototype.buildPositionCaches = function () {
this.timeGrid.buildPositionCaches();
};
SimpleTimeGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.timeGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.timeGrid,
dateSpan: rawHit.dateSpan,
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return SimpleTimeGrid;
}(DateComponent));
function buildDayRanges(dayTable, dateProfile, dateEnv) {
var ranges = [];
for (var _i = 0, _a = dayTable.headerDates; _i < _a.length; _i++) {
var date = _a[_i];
ranges.push({
start: dateEnv.add(date, dateProfile.minTime),
end: dateEnv.add(date, dateProfile.maxTime)
});
}
return ranges;
}
var TimeGridSlicer = /** @class */ (function (_super) {
__extends(TimeGridSlicer, _super);
function TimeGridSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimeGridSlicer.prototype.sliceRange = function (range, dayRanges) {
var segs = [];
for (var col = 0; col < dayRanges.length; col++) {
var segRange = intersectRanges(range, dayRanges[col]);
if (segRange) {
segs.push({
start: segRange.start,
end: segRange.end,
isStart: segRange.start.valueOf() === range.start.valueOf(),
isEnd: segRange.end.valueOf() === range.end.valueOf(),
col: col
});
}
}
return segs;
};
return TimeGridSlicer;
}(Slicer));
var TimeGridView$1 = /** @class */ (function (_super) {
__extends(TimeGridView, _super);
function TimeGridView(_context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, _context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.buildDayTable = memoize(buildDayTable);
if (_this.opt('columnHeader')) {
_this.header = new DayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.simpleTimeGrid = new SimpleTimeGrid(_this.context, _this.timeGrid);
if (_this.dayGrid) {
_this.simpleDayGrid = new SimpleDayGrid(_this.context, _this.dayGrid);
}
return _this;
}
TimeGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.simpleTimeGrid.destroy();
if (this.simpleDayGrid) {
this.simpleDayGrid.destroy();
}
};
TimeGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var dateProfile = this.props.dateProfile;
var dayTable = this.buildDayTable(dateProfile, this.dateProfileGenerator);
var splitProps = this.splitter.splitProps(props);
if (this.header) {
this.header.receiveProps({
dateProfile: dateProfile,
dates: dayTable.headerDates,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.simpleTimeGrid.receiveProps(__assign({}, splitProps['timed'], { dateProfile: dateProfile,
dayTable: dayTable }));
if (this.simpleDayGrid) {
this.simpleDayGrid.receiveProps(__assign({}, splitProps['allDay'], { dateProfile: dateProfile,
dayTable: dayTable, nextDayThreshold: this.nextDayThreshold, isRigid: false }));
}
};
TimeGridView.prototype.renderNowIndicator = function (date) {
this.simpleTimeGrid.renderNowIndicator(date);
};
return TimeGridView;
}(TimeGridView));
function buildDayTable(dateProfile, dateProfileGenerator) {
var daySeries = new DaySeries(dateProfile.renderRange, dateProfileGenerator);
return new DayTable(daySeries, false);
}
var main = createPlugin({
defaultView: 'timeGridWeek',
views: {
timeGrid: {
class: TimeGridView$1,
allDaySlot: true,
slotDuration: '00:30:00',
slotEventOverlap: true // a bad name. confused with overlap/constraint system
},
timeGridDay: {
type: 'timeGrid',
duration: { days: 1 }
},
timeGridWeek: {
type: 'timeGrid',
duration: { weeks: 1 }
}
}
});
export default main;
export { TimeGridView as AbstractTimeGridView, TimeGrid, TimeGridSlicer, TimeGridView$1 as TimeGridView, buildDayRanges, buildDayTable };
/*!
FullCalendar Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core'), require('@fullcalendar/daygrid')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core', '@fullcalendar/daygrid'], factory) :
(global = global || self, factory(global.FullCalendarTimeGrid = {}, global.FullCalendar, global.FullCalendarDayGrid));
}(this, function (exports, core, daygrid) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/*
Only handles foreground segs.
Does not own rendering. Use for low-level util methods by TimeGrid.
*/
var TimeGridEventRenderer = /** @class */ (function (_super) {
__extends(TimeGridEventRenderer, _super);
function TimeGridEventRenderer(timeGrid) {
var _this = _super.call(this, timeGrid.context) || this;
_this.timeGrid = timeGrid;
_this.fullTimeFormat = core.createFormatter({
hour: 'numeric',
minute: '2-digit',
separator: _this.context.options.defaultRangeSeparator
});
return _this;
}
// Given an array of foreground segments, render a DOM element for each, computes position,
// and attaches to the column inner-container elements.
TimeGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var segsByCol = this.timeGrid.groupSegsByCol(segs);
// order the segs within each column
// TODO: have groupSegsByCol do this?
for (var col = 0; col < segsByCol.length; col++) {
segsByCol[col] = this.sortEventSegs(segsByCol[col]);
}
this.segsByCol = segsByCol;
this.timeGrid.attachSegsByCol(segsByCol, this.timeGrid.fgContainerEls);
};
TimeGridEventRenderer.prototype.detachSegs = function (segs) {
segs.forEach(function (seg) {
core.removeElement(seg.el);
});
this.segsByCol = null;
};
TimeGridEventRenderer.prototype.computeSegSizes = function (allSegs) {
var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol;
var colCnt = timeGrid.colCnt;
timeGrid.computeSegVerticals(allSegs); // horizontals relies on this
if (segsByCol) {
for (var col = 0; col < colCnt; col++) {
this.computeSegHorizontals(segsByCol[col]); // compute horizontal coordinates, z-index's, and reorder the array
}
}
};
TimeGridEventRenderer.prototype.assignSegSizes = function (allSegs) {
var _a = this, timeGrid = _a.timeGrid, segsByCol = _a.segsByCol;
var colCnt = timeGrid.colCnt;
timeGrid.assignSegVerticals(allSegs); // horizontals relies on this
if (segsByCol) {
for (var col = 0; col < colCnt; col++) {
this.assignSegCss(segsByCol[col]);
}
}
};
// Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined
TimeGridEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
meridiem: false
};
};
// Computes a default `displayEventEnd` value if one is not expliclty defined
TimeGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return true;
};
// Renders the HTML for a single event segment's default rendering
TimeGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var view = this.context.view;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var allDay = eventDef.allDay;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
var skinCss = core.cssToStr(this.getSkinCss(eventUi));
var timeText;
var fullTimeText; // more verbose time text. for the print stylesheet
var startTimeText; // just the start time text
classes.unshift('fc-time-grid-event');
// if the event appears to span more than one day...
if (core.isMultiDayRange(eventRange.range)) {
// Don't display time text on segments that run entirely through a day.
// That would appear as midnight-midnight and would look dumb.
// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
if (seg.isStart || seg.isEnd) {
var unzonedStart = seg.start;
var unzonedEnd = seg.end;
timeText = this._getTimeText(unzonedStart, unzonedEnd, allDay); // TODO: give the timezones
fullTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, this.fullTimeFormat);
startTimeText = this._getTimeText(unzonedStart, unzonedEnd, allDay, null, false); // displayEnd=false
}
}
else {
// Display the normal time text for the *event's* times
timeText = this.getTimeText(eventRange);
fullTimeText = this.getTimeText(eventRange, this.fullTimeFormat);
startTimeText = this.getTimeText(eventRange, null, false); // displayEnd=false
}
return '' +
'
(timeText ?
'
'' + core.htmlEscape(timeText) + '' +
'
'') +
(eventDef.title ?
'
core.htmlEscape(eventDef.title) +
'
'') +
'
/* TODO: write CSS for this
(isResizableFromStart ?
'' :
''
) +
*/
(isResizableFromEnd ?
'' :
'') +
'';
};
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
// Assumed the segs are already ordered.
// NOTE: Also reorders the given array by date!
TimeGridEventRenderer.prototype.computeSegHorizontals = function (segs) {
var levels;
var level0;
var i;
levels = buildSlotSegLevels(segs);
computeForwardSlotSegs(levels);
if ((level0 = levels[0])) {
for (i = 0; i < level0.length; i++) {
computeSlotSegPressures(level0[i]);
}
for (i = 0; i < level0.length; i++) {
this.computeSegForwardBack(level0[i], 0, 0);
}
}
};
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
//
// The segment might be part of a "series", which means consecutive segments with the same pressure
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
// coordinate of the first segment in the series.
TimeGridEventRenderer.prototype.computeSegForwardBack = function (seg, seriesBackwardPressure, seriesBackwardCoord) {
var forwardSegs = seg.forwardSegs;
var i;
if (seg.forwardCoord === undefined) { // not already computed
if (!forwardSegs.length) {
// if there are no forward segments, this segment should butt up against the edge
seg.forwardCoord = 1;
}
else {
// sort highest pressure first
this.sortForwardSegs(forwardSegs);
// this segment's forwardCoord will be calculated from the backwardCoord of the
// highest-pressure forward segment.
this.computeSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
seg.forwardCoord = forwardSegs[0].backwardCoord;
}
// calculate the backwardCoord from the forwardCoord. consider the series
seg.backwardCoord = seg.forwardCoord -
(seg.forwardCoord - seriesBackwardCoord) / // available width for series
(seriesBackwardPressure + 1); // # of segments in the series
// use this segment's coordinates to computed the coordinates of the less-pressurized
// forward segments
for (i = 0; i < forwardSegs.length; i++) {
this.computeSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
}
}
};
TimeGridEventRenderer.prototype.sortForwardSegs = function (forwardSegs) {
var objs = forwardSegs.map(buildTimeGridSegCompareObj);
var specs = [
// put higher-pressure first
{ field: 'forwardPressure', order: -1 },
// put segments that are closer to initial edge first (and favor ones with no coords yet)
{ field: 'backwardCoord', order: 1 }
].concat(this.context.view.eventOrderSpecs);
objs.sort(function (obj0, obj1) {
return core.compareByFieldSpecs(obj0, obj1, specs);
});
return objs.map(function (c) {
return c._seg;
});
};
// Given foreground event segments that have already had their position coordinates computed,
// assigns position-related CSS values to their elements.
TimeGridEventRenderer.prototype.assignSegCss = function (segs) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
core.applyStyle(seg.el, this.generateSegCss(seg));
if (seg.level > 0) {
seg.el.classList.add('fc-time-grid-event-inset');
}
// if the event is short that the title will be cut off,
// attach a className that condenses the title into the time area.
if (seg.eventRange.def.title && seg.bottom - seg.top < 30) {
seg.el.classList.add('fc-short'); // TODO: "condensed" is a better name
}
}
};
// Generates an object with CSS properties/values that should be applied to an event segment element.
// Contains important positioning-related properties that should be applied to any event element, customized or not.
TimeGridEventRenderer.prototype.generateSegCss = function (seg) {
var shouldOverlap = this.context.options.slotEventOverlap;
var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
var props = this.timeGrid.generateSegVerticalCss(seg); // get top/bottom first
var isRtl = this.timeGrid.isRtl;
var left; // amount of space from left edge, a fraction of the total width
var right; // amount of space from right edge, a fraction of the total width
if (shouldOverlap) {
// double the width, but don't go beyond the maximum forward coordinate (1.0)
forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
}
if (isRtl) {
left = 1 - forwardCoord;
right = backwardCoord;
}
else {
left = backwardCoord;
right = 1 - forwardCoord;
}
props.zIndex = seg.level + 1; // convert from 0-base to 1-based
props.left = left * 100 + '%';
props.right = right * 100 + '%';
if (shouldOverlap && seg.forwardPressure) {
// add padding to the edge so that forward stacked events don't cover the resizer's icon
props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
}
return props;
};
return TimeGridEventRenderer;
}(core.FgEventRenderer));
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
function buildSlotSegLevels(segs) {
var levels = [];
var i;
var seg;
var j;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
// go through all the levels and stop on the first level where there are no collisions
for (j = 0; j < levels.length; j++) {
if (!computeSlotSegCollisions(seg, levels[j]).length) {
break;
}
}
seg.level = j;
(levels[j] || (levels[j] = [])).push(seg);
}
return levels;
}
// For every segment, figure out the other segments that are in subsequent
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
function computeForwardSlotSegs(levels) {
var i;
var level;
var j;
var seg;
var k;
for (i = 0; i < levels.length; i++) {
level = levels[i];
for (j = 0; j < level.length; j++) {
seg = level[j];
seg.forwardSegs = [];
for (k = i + 1; k < levels.length; k++) {
computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
}
}
}
}
// Figure out which path forward (via seg.forwardSegs) results in the longest path until
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
function computeSlotSegPressures(seg) {
var forwardSegs = seg.forwardSegs;
var forwardPressure = 0;
var i;
var forwardSeg;
if (seg.forwardPressure === undefined) { // not already computed
for (i = 0; i < forwardSegs.length; i++) {
forwardSeg = forwardSegs[i];
// figure out the child's maximum forward path
computeSlotSegPressures(forwardSeg);
// either use the existing maximum, or use the child's forward pressure
// plus one (for the forwardSeg itself)
forwardPressure = Math.max(forwardPressure, 1 + forwardSeg.forwardPressure);
}
seg.forwardPressure = forwardPressure;
}
}
// Find all the segments in `otherSegs` that vertically collide with `seg`.
// Append into an optionally-supplied `results` array and return.
function computeSlotSegCollisions(seg, otherSegs, results) {
if (results === void 0) { results = []; }
for (var i = 0; i < otherSegs.length; i++) {
if (isSlotSegCollision(seg, otherSegs[i])) {
results.push(otherSegs[i]);
}
}
return results;
}
// Do these segments occupy the same vertical space?
function isSlotSegCollision(seg1, seg2) {
return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
}
function buildTimeGridSegCompareObj(seg) {
var obj = core.buildSegCompareObj(seg);
obj.forwardPressure = seg.forwardPressure;
obj.backwardCoord = seg.backwardCoord;
return obj;
}
var TimeGridMirrorRenderer = /** @class */ (function (_super) {
__extends(TimeGridMirrorRenderer, _super);
function TimeGridMirrorRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimeGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
this.segsByCol = this.timeGrid.groupSegsByCol(segs);
this.timeGrid.attachSegsByCol(this.segsByCol, this.timeGrid.mirrorContainerEls);
this.sourceSeg = mirrorInfo.sourceSeg;
};
TimeGridMirrorRenderer.prototype.generateSegCss = function (seg) {
var props = _super.prototype.generateSegCss.call(this, seg);
var sourceSeg = this.sourceSeg;
if (sourceSeg && sourceSeg.col === seg.col) {
var sourceSegProps = _super.prototype.generateSegCss.call(this, sourceSeg);
props.left = sourceSegProps.left;
props.right = sourceSegProps.right;
props.marginLeft = sourceSegProps.marginLeft;
props.marginRight = sourceSegProps.marginRight;
}
return props;
};
return TimeGridMirrorRenderer;
}(TimeGridEventRenderer));
var TimeGridFillRenderer = /** @class */ (function (_super) {
__extends(TimeGridFillRenderer, _super);
function TimeGridFillRenderer(timeGrid) {
var _this = _super.call(this, timeGrid.context) || this;
_this.timeGrid = timeGrid;
return _this;
}
TimeGridFillRenderer.prototype.attachSegs = function (type, segs) {
var timeGrid = this.timeGrid;
var containerEls;
// TODO: more efficient lookup
if (type === 'bgEvent') {
containerEls = timeGrid.bgContainerEls;
}
else if (type === 'businessHours') {
containerEls = timeGrid.businessContainerEls;
}
else if (type === 'highlight') {
containerEls = timeGrid.highlightContainerEls;
}
timeGrid.attachSegsByCol(timeGrid.groupSegsByCol(segs), containerEls);
return segs.map(function (seg) {
return seg.el;
});
};
TimeGridFillRenderer.prototype.computeSegSizes = function (segs) {
this.timeGrid.computeSegVerticals(segs);
};
TimeGridFillRenderer.prototype.assignSegSizes = function (segs) {
this.timeGrid.assignSegVerticals(segs);
};
return TimeGridFillRenderer;
}(core.FillRenderer));
/* A component that renders one or more columns of vertical time slots
----------------------------------------------------------------------------------------------------------------------*/
// potential nice values for the slot-duration and interval-duration
// from largest to smallest
var AGENDA_STOCK_SUB_DURATIONS = [
{ hours: 1 },
{ minutes: 30 },
{ minutes: 15 },
{ seconds: 30 },
{ seconds: 15 }
];
var TimeGrid = /** @class */ (function (_super) {
__extends(TimeGrid, _super);
function TimeGrid(context, el, renderProps) {
var _this = _super.call(this, context, el) || this;
_this.isSlatSizesDirty = false;
_this.isColSizesDirty = false;
_this.renderSlats = core.memoizeRendering(_this._renderSlats);
var eventRenderer = _this.eventRenderer = new TimeGridEventRenderer(_this);
var fillRenderer = _this.fillRenderer = new TimeGridFillRenderer(_this);
_this.mirrorRenderer = new TimeGridMirrorRenderer(_this);
var renderColumns = _this.renderColumns = core.memoizeRendering(_this._renderColumns, _this._unrenderColumns);
_this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderColumns]);
_this.renderDateSelection = core.memoizeRendering(_this._renderDateSelection, _this._unrenderDateSelection, [renderColumns]);
_this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderColumns]);
_this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderColumns]);
_this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderColumns]);
_this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderColumns]);
_this.processOptions();
el.innerHTML =
'
var AllDaySplitter = /** @class */ (function (_super) {
__extends(AllDaySplitter, _super);
function AllDaySplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
AllDaySplitter.prototype.getKeyInfo = function () {
return {
allDay: {},
timed: {}
};
};
AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) {
if (dateSpan.allDay) {
return ['allDay'];
}
else {
return ['timed'];
}
};
AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) {
if (!eventDef.allDay) {
return ['timed'];
}
else if (core.hasBgRendering(eventDef)) {
return ['timed', 'allDay'];
}
else {
return ['allDay'];
}
};
return AllDaySplitter;
}(core.Splitter));
var TIMEGRID_ALL_DAY_EVENT_LIMIT = 5;
var WEEK_HEADER_FORMAT = core.createFormatter({ week: 'short' });
/* An abstract class for all timegrid-related views. Displays one more columns with time slots running vertically.
----------------------------------------------------------------------------------------------------------------------*/
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
// Responsible for managing width/height.
var TimeGridView = /** @class */ (function (_super) {
__extends(TimeGridView, _super);
function TimeGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.splitter = new AllDaySplitter();
/* Header Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before the day-of week header cells
_this.renderHeadIntroHtml = function () {
var _a = _this, theme = _a.theme, dateEnv = _a.dateEnv;
var range = _this.props.dateProfile.renderRange;
var dayCnt = core.diffDays(range.start, range.end);
var weekText;
if (_this.opt('weekNumbers')) {
weekText = dateEnv.format(range.start, WEEK_HEADER_FORMAT);
return '' +
'' +
core.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
_this, { date: range.start, type: 'week', forceOff: dayCnt > 1 }, core.htmlEscape(weekText) // inner HTML
) +
'';
}
else {
return '';
}
};
/* Time Grid Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
_this.renderTimeGridBgIntroHtml = function () {
var theme = _this.theme;
return '';
};
// Generates the HTML that goes before all other types of cells.
// Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
_this.renderTimeGridIntroHtml = function () {
return '';
};
/* Day Grid Render Methods
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that goes before the all-day cells
_this.renderDayGridBgIntroHtml = function () {
var theme = _this.theme;
return '' +
'' +
'' + // needed for matchCellWidths
core.getAllDayHtml(_this) +
'' +
'';
};
// Generates the HTML that goes before all other types of cells.
// Affects content-skeleton, mirror-skeleton, highlight-skeleton for both the time-grid and day-grid.
_this.renderDayGridIntroHtml = function () {
return '';
};
_this.el.classList.add('fc-timeGrid-view');
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.scroller = new core.ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
var timeGridWrapEl = _this.scroller.el;
_this.el.querySelector('.fc-body > tr > td').appendChild(timeGridWrapEl);
timeGridWrapEl.classList.add('fc-time-grid-container');
var timeGridEl = core.createElement('div', { className: 'fc-time-grid' });
timeGridWrapEl.appendChild(timeGridEl);
_this.timeGrid = new TimeGrid(_this.context, timeGridEl, {
renderBgIntroHtml: _this.renderTimeGridBgIntroHtml,
renderIntroHtml: _this.renderTimeGridIntroHtml
});
if (_this.opt('allDaySlot')) { // should we display the "all-day" area?
_this.dayGrid = new daygrid.DayGrid(// the all-day subcomponent of this view
_this.context, _this.el.querySelector('.fc-day-grid'), {
renderNumberIntroHtml: _this.renderDayGridIntroHtml,
renderBgIntroHtml: _this.renderDayGridBgIntroHtml,
renderIntroHtml: _this.renderDayGridIntroHtml,
colWeekNumbersVisible: false,
cellWeekNumbersVisible: false
});
// have the day-grid extend it's coordinate area over the
var SimpleTimeGrid = /** @class */ (function (_super) {
__extends(SimpleTimeGrid, _super);
function SimpleTimeGrid(context, timeGrid) {
var _this = _super.call(this, context, timeGrid.el) || this;
_this.buildDayRanges = core.memoize(buildDayRanges);
_this.slicer = new TimeGridSlicer();
_this.timeGrid = timeGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeGrid.el
});
return _this;
}
SimpleTimeGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
SimpleTimeGrid.prototype.render = function (props) {
var dateProfile = props.dateProfile, dayTable = props.dayTable;
var dayRanges = this.dayRanges = this.buildDayRanges(dayTable, dateProfile, this.dateEnv);
this.timeGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, null, this.timeGrid, dayRanges), { dateProfile: dateProfile, cells: dayTable.cells[0] }));
};
SimpleTimeGrid.prototype.renderNowIndicator = function (date) {
this.timeGrid.renderNowIndicator(this.slicer.sliceNowDate(date, this.timeGrid, this.dayRanges), date);
};
SimpleTimeGrid.prototype.buildPositionCaches = function () {
this.timeGrid.buildPositionCaches();
};
SimpleTimeGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.timeGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.timeGrid,
dateSpan: rawHit.dateSpan,
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return SimpleTimeGrid;
}(core.DateComponent));
function buildDayRanges(dayTable, dateProfile, dateEnv) {
var ranges = [];
for (var _i = 0, _a = dayTable.headerDates; _i < _a.length; _i++) {
var date = _a[_i];
ranges.push({
start: dateEnv.add(date, dateProfile.minTime),
end: dateEnv.add(date, dateProfile.maxTime)
});
}
return ranges;
}
var TimeGridSlicer = /** @class */ (function (_super) {
__extends(TimeGridSlicer, _super);
function TimeGridSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimeGridSlicer.prototype.sliceRange = function (range, dayRanges) {
var segs = [];
for (var col = 0; col < dayRanges.length; col++) {
var segRange = core.intersectRanges(range, dayRanges[col]);
if (segRange) {
segs.push({
start: segRange.start,
end: segRange.end,
isStart: segRange.start.valueOf() === range.start.valueOf(),
isEnd: segRange.end.valueOf() === range.end.valueOf(),
col: col
});
}
}
return segs;
};
return TimeGridSlicer;
}(core.Slicer));
var TimeGridView$1 = /** @class */ (function (_super) {
__extends(TimeGridView, _super);
function TimeGridView(_context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, _context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.buildDayTable = core.memoize(buildDayTable);
if (_this.opt('columnHeader')) {
_this.header = new core.DayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.simpleTimeGrid = new SimpleTimeGrid(_this.context, _this.timeGrid);
if (_this.dayGrid) {
_this.simpleDayGrid = new daygrid.SimpleDayGrid(_this.context, _this.dayGrid);
}
return _this;
}
TimeGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.simpleTimeGrid.destroy();
if (this.simpleDayGrid) {
this.simpleDayGrid.destroy();
}
};
TimeGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var dateProfile = this.props.dateProfile;
var dayTable = this.buildDayTable(dateProfile, this.dateProfileGenerator);
var splitProps = this.splitter.splitProps(props);
if (this.header) {
this.header.receiveProps({
dateProfile: dateProfile,
dates: dayTable.headerDates,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.simpleTimeGrid.receiveProps(__assign({}, splitProps['timed'], { dateProfile: dateProfile,
dayTable: dayTable }));
if (this.simpleDayGrid) {
this.simpleDayGrid.receiveProps(__assign({}, splitProps['allDay'], { dateProfile: dateProfile,
dayTable: dayTable, nextDayThreshold: this.nextDayThreshold, isRigid: false }));
}
};
TimeGridView.prototype.renderNowIndicator = function (date) {
this.simpleTimeGrid.renderNowIndicator(date);
};
return TimeGridView;
}(TimeGridView));
function buildDayTable(dateProfile, dateProfileGenerator) {
var daySeries = new core.DaySeries(dateProfile.renderRange, dateProfileGenerator);
return new core.DayTable(daySeries, false);
}
var main = core.createPlugin({
defaultView: 'timeGridWeek',
views: {
timeGrid: {
class: TimeGridView$1,
allDaySlot: true,
slotDuration: '00:30:00',
slotEventOverlap: true // a bad name. confused with overlap/constraint system
},
timeGridDay: {
type: 'timeGrid',
duration: { days: 1 }
},
timeGridWeek: {
type: 'timeGrid',
duration: { weeks: 1 }
}
}
});
exports.AbstractTimeGridView = TimeGridView;
exports.TimeGrid = TimeGrid;
exports.TimeGridSlicer = TimeGridSlicer;
exports.TimeGridView = TimeGridView$1;
exports.buildDayRanges = buildDayRanges;
exports.buildDayTable = buildDayTable;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
// ../../../../../@fullcalendar/daygrid
declare module '@fullcalendar/timegrid' {
import AbstractTimeGridView from '@fullcalendar/timegrid/AbstractTimeGridView';
import TimeGridView, { buildDayTable } from '@fullcalendar/timegrid/TimeGridView';
import { TimeGridSeg } from '@fullcalendar/timegrid/TimeGrid';
import { TimeGridSlicer, buildDayRanges } from '@fullcalendar/timegrid/SimpleTimeGrid';
export { TimeGridView, AbstractTimeGridView, buildDayTable, buildDayRanges, TimeGridSlicer, TimeGridSeg };
export { default as TimeGrid } from '@fullcalendar/timegrid/TimeGrid';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/timegrid/AbstractTimeGridView' {
import { ScrollComponent, View, ViewSpec, DateProfileGenerator, ComponentContext, Duration } from '@fullcalendar/core';
import { DayGrid } from '@fullcalendar/daygrid';
import TimeGrid from '@fullcalendar/timegrid/TimeGrid';
import AllDaySplitter from '@fullcalendar/timegrid/AllDaySplitter';
export { TimeGridView as default, TimeGridView };
abstract class TimeGridView extends View {
timeGrid: TimeGrid;
dayGrid: DayGrid;
scroller: ScrollComponent;
axisWidth: any;
protected splitter: AllDaySplitter;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
destroy(): void;
renderSkeletonHtml(): string;
getNowIndicatorUnit(): string;
unrenderNowIndicator(): void;
updateSize(isResize: boolean, viewHeight: number, isAuto: boolean): void;
updateBaseSize(isResize: any, viewHeight: any, isAuto: any): void;
computeScrollerHeight(viewHeight: any): number;
computeDateScroll(duration: Duration): {
top: any;
};
queryDateScroll(): {
top: number;
};
applyDateScroll(scroll: any): void;
renderHeadIntroHtml: () => string;
axisStyleAttr(): string;
renderTimeGridBgIntroHtml: () => string;
renderTimeGridIntroHtml: () => string;
renderDayGridBgIntroHtml: () => string;
renderDayGridIntroHtml: () => string;
}
}
declare module '@fullcalendar/timegrid/TimeGridView' {
import { DateProfileGenerator, DateProfile, ComponentContext, ViewSpec, DayHeader, DayTable, ViewProps } from '@fullcalendar/core';
import { SimpleDayGrid } from '@fullcalendar/daygrid';
import SimpleTimeGrid from '@fullcalendar/timegrid/SimpleTimeGrid';
import AbstractTimeGridView from '@fullcalendar/timegrid/AbstractTimeGridView';
export { TimeGridView as default, TimeGridView };
class TimeGridView extends AbstractTimeGridView {
header: DayHeader;
simpleDayGrid: SimpleDayGrid;
simpleTimeGrid: SimpleTimeGrid;
constructor(_context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
destroy(): void;
render(props: ViewProps): void;
renderNowIndicator(date: any): void;
}
export function buildDayTable(dateProfile: DateProfile, dateProfileGenerator: DateProfileGenerator): DayTable;
}
declare module '@fullcalendar/timegrid/TimeGrid' {
import { PositionCache, Duration, DateMarker, DateFormatter, ComponentContext, DateComponent, Seg, EventSegUiInteractionState, DateProfile } from '@fullcalendar/core';
export interface RenderProps {
renderBgIntroHtml: () => string;
renderIntroHtml: () => string;
}
export interface TimeGridSeg extends Seg {
col: number;
start: DateMarker;
end: DateMarker;
}
export interface TimeGridCell {
date: DateMarker;
htmlAttrs?: string;
}
export interface TimeGridProps {
dateProfile: DateProfile;
cells: TimeGridCell[];
businessHourSegs: TimeGridSeg[];
bgEventSegs: TimeGridSeg[];
fgEventSegs: TimeGridSeg[];
dateSelectionSegs: TimeGridSeg[];
eventSelection: string;
eventDrag: EventSegUiInteractionState | null;
eventResize: EventSegUiInteractionState | null;
}
export { TimeGrid as default, TimeGrid };
class TimeGrid extends DateComponent {
renderProps: RenderProps;
slotDuration: Duration;
snapDuration: Duration;
snapsPerSlot: any;
labelFormat: DateFormatter;
labelInterval: Duration;
colCnt: number;
colEls: HTMLElement[];
slatContainerEl: HTMLElement;
slatEls: HTMLElement[];
nowIndicatorEls: HTMLElement[];
colPositions: PositionCache;
slatPositions: PositionCache;
isSlatSizesDirty: boolean;
isColSizesDirty: boolean;
rootBgContainerEl: HTMLElement;
bottomRuleEl: HTMLElement;
contentSkeletonEl: HTMLElement;
colContainerEls: HTMLElement[];
fgContainerEls: HTMLElement[];
bgContainerEls: HTMLElement[];
mirrorContainerEls: HTMLElement[];
highlightContainerEls: HTMLElement[];
businessContainerEls: HTMLElement[];
constructor(context: ComponentContext, el: HTMLElement, renderProps: RenderProps);
processOptions(): void;
computeLabelInterval(slotDuration: any): any;
render(props: TimeGridProps): void;
destroy(): void;
updateSize(isResize: boolean): void;
_renderSlats(dateProfile: DateProfile): void;
renderSlatRowHtml(dateProfile: DateProfile): string;
_renderColumns(cells: TimeGridCell[], dateProfile: DateProfile): void;
_unrenderColumns(): void;
renderContentSkeleton(): void;
unrenderContentSkeleton(): void;
groupSegsByCol(segs: any): any[];
attachSegsByCol(segsByCol: any, containerEls: HTMLElement[]): void;
getNowIndicatorUnit(): string;
renderNowIndicator(segs: TimeGridSeg[], date: any): void;
unrenderNowIndicator(): void;
getTotalSlatHeight(): number;
computeDateTop(when: DateMarker, startOfDayDate?: DateMarker): any;
computeTimeTop(duration: Duration): any;
computeSegVerticals(segs: any): void;
assignSegVerticals(segs: any): void;
generateSegVerticalCss(seg: any): {
top: any;
bottom: number;
};
buildPositionCaches(): void;
buildColPositions(): void;
buildSlatPositions(): void;
positionToHit(positionLeft: any, positionTop: any): {
col: any;
dateSpan: {
range: {
start: Date;
end: Date;
};
allDay: boolean;
};
dayEl: HTMLElement;
relativeRect: {
left: any;
right: any;
top: any;
bottom: any;
};
};
_renderEventDrag(state: EventSegUiInteractionState): void;
_unrenderEventDrag(state: EventSegUiInteractionState): void;
_renderEventResize(state: EventSegUiInteractionState): void;
_unrenderEventResize(state: EventSegUiInteractionState): void;
_renderDateSelection(segs: Seg[]): void;
_unrenderDateSelection(segs: Seg[]): void;
}
}
declare module '@fullcalendar/timegrid/SimpleTimeGrid' {
import { DateComponent, DateProfile, EventStore, EventUiHash, EventInteractionState, DateSpan, DateRange, DayTable, DateEnv, DateMarker, Slicer, Hit, ComponentContext } from '@fullcalendar/core';
import TimeGrid, { TimeGridSeg } from '@fullcalendar/timegrid/TimeGrid';
export interface SimpleTimeGridProps {
dateProfile: DateProfile | null;
dayTable: DayTable;
businessHours: EventStore;
eventStore: EventStore;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export { SimpleTimeGrid as default, SimpleTimeGrid };
class SimpleTimeGrid extends DateComponent {
timeGrid: TimeGrid;
constructor(context: ComponentContext, timeGrid: TimeGrid);
destroy(): void;
render(props: SimpleTimeGridProps): void;
renderNowIndicator(date: DateMarker): void;
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number): Hit;
}
export function buildDayRanges(dayTable: DayTable, dateProfile: DateProfile, dateEnv: DateEnv): DateRange[];
export class TimeGridSlicer extends Slicer {
sliceRange(range: DateRange, dayRanges: DateRange[]): TimeGridSeg[];
}
}
declare module '@fullcalendar/timegrid/AllDaySplitter' {
import { Splitter, EventDef, DateSpan } from '@fullcalendar/core';
export { AllDaySplitter as default, AllDaySplitter };
class AllDaySplitter extends Splitter {
getKeyInfo(): {
allDay: {};
timed: {};
};
getKeysForDateSpan(dateSpan: DateSpan): string[];
getKeysForEventDef(eventDef: EventDef): string[];
}
}
/*!
FullCalendar Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core"),require("@fullcalendar/daygrid")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core","@fullcalendar/daygrid"],t):t((e=e||self).FullCalendarTimeGrid={},e.FullCalendar,e.FullCalendarDayGrid)}(this,function(e,t,r){"use strict";var i=function(e,t){return(i=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)};function n(e,t){function r(){this.constructor=e}i(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}var o=function(){return(o=Object.assign||function(e){for(var t,r=1,i=arguments.length;r
',o.rootBgContainerEl=i.querySelector(".fc-bg"),o.slatContainerEl=i.querySelector(".fc-slats"),o.bottomRuleEl=i.querySelector(".fc-divider"),o.renderProps=n,o}return n(i,e),i.prototype.processOptions=function(){var e,r,i=this.opt("slotDuration"),n=this.opt("snapDuration");i=t.createDuration(i),n=n?t.createDuration(n):i,null===(e=t.wholeDivideDurations(i,n))&&(n=i,e=1),this.slotDuration=i,this.snapDuration=n,this.snapsPerSlot=e,r=this.opt("slotLabelFormat"),Array.isArray(r)&&(r=r[r.length-1]),this.labelFormat=t.createFormatter(r||{hour:"numeric",minute:"2-digit",omitZeroMinute:!0,meridiem:"short"}),r=this.opt("slotLabelInterval"),this.labelInterval=r?t.createDuration(r):this.computeLabelInterval(i)},i.prototype.computeLabelInterval=function(e){var r,i,n;for(r=u.length-1;r>=0;r--)if(i=t.createDuration(u[r]),null!==(n=t.wholeDivideDurations(i,e))&&n>1)return i;return e},i.prototype.render=function(e){var t=e.cells;this.colCnt=t.length,this.renderSlats(e.dateProfile),this.renderColumns(e.cells,e.dateProfile),this.renderBusinessHours(e.businessHourSegs),this.renderDateSelection(e.dateSelectionSegs),this.renderFgEvents(e.fgEventSegs),this.renderBgEvents(e.bgEventSegs),this.renderEventSelection(e.eventSelection),this.renderEventDrag(e.eventDrag),this.renderEventResize(e.eventResize)},i.prototype.destroy=function(){e.prototype.destroy.call(this),this.renderSlats.unrender(),this.renderColumns.unrender()},i.prototype.updateSize=function(e){var t=this.fillRenderer,r=this.eventRenderer,i=this.mirrorRenderer;(e||this.isSlatSizesDirty)&&(this.buildSlatPositions(),this.isSlatSizesDirty=!1),(e||this.isColSizesDirty)&&(this.buildColPositions(),this.isColSizesDirty=!1),t.computeSizes(e),r.computeSizes(e),i.computeSizes(e),t.assignSizes(e),r.assignSizes(e),i.assignSizes(e)},i.prototype._renderSlats=function(e){var r=this.theme;this.slatContainerEl.innerHTML=''+this.renderSlatRowHtml(e)+"",this.slatEls=t.findElements(this.slatContainerEl,"tr"),this.slatPositions=new t.PositionCache(this.el,this.slatEls,!1,!0),this.isSlatSizesDirty=!0},i.prototype.renderSlatRowHtml=function(e){for(var r,i,n,o=this.dateEnv,s=this.theme,a=this.isRtl,l="",d=t.startOfDay(e.renderRange.start),c=e.minTime,h=t.createDuration(0);t.asRoughMs(c)'+(i?""+t.htmlEscape(o.format(r,this.labelFormat))+"":"")+"",l+='"+(a?"":n)+''+(a?n:"")+"",c=t.addDurations(c,this.slotDuration),h=t.addDurations(h,this.slotDuration);return l},i.prototype._renderColumns=function(e,i){var n=this.theme,o=this.dateEnv,s=this.view,a=new r.DayBgRow(this.context);this.rootBgContainerEl.innerHTML=''+a.renderHtml({cells:e,dateProfile:i,renderIntroHtml:this.renderProps.renderBgIntroHtml})+"",this.colEls=t.findElements(this.el,".fc-day, .fc-disabled-day");for(var l=0;l
':"")+""},i.prototype.getNowIndicatorUnit=function(){return this.timeGrid.getNowIndicatorUnit()},i.prototype.unrenderNowIndicator=function(){this.timeGrid.unrenderNowIndicator()},i.prototype.updateSize=function(t,r,i){e.prototype.updateSize.call(this,t,r,i),this.timeGrid.updateSize(t),this.dayGrid&&this.dayGrid.updateSize(t)},i.prototype.updateBaseSize=function(e,r,i){var n,o,s,a=this;if(this.axisWidth=t.matchCellWidths(t.findElements(this.el,".fc-axis")),this.timeGrid.colEls){var l=t.findElements(this.el,".fc-row").filter(function(e){return!a.scroller.el.contains(e)});this.timeGrid.bottomRuleEl.style.display="none",this.scroller.clear(),l.forEach(t.uncompensateScroll),this.dayGrid&&(this.dayGrid.removeSegPopover(),(n=this.opt("eventLimit"))&&"number"!=typeof n&&(n=5),n&&this.dayGrid.limitRows(n)),i||(o=this.computeScrollerHeight(r),this.scroller.setHeight(o),((s=this.scroller.getScrollbarWidths()).left||s.right)&&(l.forEach(function(e){t.compensateScroll(e,s)}),o=this.computeScrollerHeight(r),this.scroller.setHeight(o)),this.scroller.lockOverflow(s),this.timeGrid.getTotalSlatHeight()
/*!
FullCalendar Moment Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import * as momentNs from 'moment';
import { createPlugin, Calendar } from '@fullcalendar/core';
var moment = momentNs; // the directly callable function
function toMoment(date, calendar) {
if (!(calendar instanceof Calendar)) {
throw new Error('must supply a Calendar instance');
}
return convertToMoment(date, calendar.dateEnv.timeZone, null, calendar.dateEnv.locale.codes[0]);
}
function toDuration(fcDuration) {
return moment.duration(fcDuration); // moment accepts all the props that fc.Duration already has!
}
function formatWithCmdStr(cmdStr, arg) {
var cmd = parseCmdStr(cmdStr);
if (arg.end) {
var startMom = convertToMoment(arg.start.array, arg.timeZone, arg.start.timeZoneOffset, arg.localeCodes[0]);
var endMom = convertToMoment(arg.end.array, arg.timeZone, arg.end.timeZoneOffset, arg.localeCodes[0]);
return formatRange(cmd, createMomentFormatFunc(startMom), createMomentFormatFunc(endMom), arg.separator);
}
return convertToMoment(arg.date.array, arg.timeZone, arg.date.timeZoneOffset, arg.localeCodes[0]).format(cmd.whole); // TODO: test for this
}
var main = createPlugin({
cmdFormatter: formatWithCmdStr
});
function createMomentFormatFunc(mom) {
return function (cmdStr) {
return cmdStr ? mom.format(cmdStr) : ''; // because calling with blank string results in ISO8601 :(
};
}
function convertToMoment(input, timeZone, timeZoneOffset, locale) {
var mom;
if (timeZone === 'local') {
mom = moment(input);
}
else if (timeZone === 'UTC') {
mom = moment.utc(input);
}
else if (moment.tz) {
mom = moment.tz(input, timeZone);
}
else {
mom = moment.utc(input);
if (timeZoneOffset != null) {
mom.utcOffset(timeZoneOffset);
}
}
mom.locale(locale);
return mom;
}
function parseCmdStr(cmdStr) {
var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters
if (parts) {
var middle = parseCmdStr(parts[2]);
return {
head: parts[1],
middle: middle,
tail: parts[3],
whole: parts[1] + middle.whole + parts[3]
};
}
else {
return {
head: null,
middle: null,
tail: null,
whole: cmdStr
};
}
}
function formatRange(cmd, formatStart, formatEnd, separator) {
if (cmd.middle) {
var startHead = formatStart(cmd.head);
var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var startTail = formatStart(cmd.tail);
var endHead = formatEnd(cmd.head);
var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var endTail = formatEnd(cmd.tail);
if (startHead === endHead && startTail === endTail) {
return startHead +
(startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) +
startTail;
}
}
var startWhole = formatStart(cmd.whole);
var endWhole = formatEnd(cmd.whole);
if (startWhole === endWhole) {
return startWhole;
}
else {
return startWhole + separator + endWhole;
}
}
export default main;
export { toDuration, toMoment };
/*!
FullCalendar Moment Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('moment'), require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', 'moment', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarMoment = {}, global.moment, global.FullCalendar));
}(this, function (exports, momentNs, core) { 'use strict';
var moment = momentNs; // the directly callable function
function toMoment(date, calendar) {
if (!(calendar instanceof core.Calendar)) {
throw new Error('must supply a Calendar instance');
}
return convertToMoment(date, calendar.dateEnv.timeZone, null, calendar.dateEnv.locale.codes[0]);
}
function toDuration(fcDuration) {
return moment.duration(fcDuration); // moment accepts all the props that fc.Duration already has!
}
function formatWithCmdStr(cmdStr, arg) {
var cmd = parseCmdStr(cmdStr);
if (arg.end) {
var startMom = convertToMoment(arg.start.array, arg.timeZone, arg.start.timeZoneOffset, arg.localeCodes[0]);
var endMom = convertToMoment(arg.end.array, arg.timeZone, arg.end.timeZoneOffset, arg.localeCodes[0]);
return formatRange(cmd, createMomentFormatFunc(startMom), createMomentFormatFunc(endMom), arg.separator);
}
return convertToMoment(arg.date.array, arg.timeZone, arg.date.timeZoneOffset, arg.localeCodes[0]).format(cmd.whole); // TODO: test for this
}
var main = core.createPlugin({
cmdFormatter: formatWithCmdStr
});
function createMomentFormatFunc(mom) {
return function (cmdStr) {
return cmdStr ? mom.format(cmdStr) : ''; // because calling with blank string results in ISO8601 :(
};
}
function convertToMoment(input, timeZone, timeZoneOffset, locale) {
var mom;
if (timeZone === 'local') {
mom = moment(input);
}
else if (timeZone === 'UTC') {
mom = moment.utc(input);
}
else if (moment.tz) {
mom = moment.tz(input, timeZone);
}
else {
mom = moment.utc(input);
if (timeZoneOffset != null) {
mom.utcOffset(timeZoneOffset);
}
}
mom.locale(locale);
return mom;
}
function parseCmdStr(cmdStr) {
var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters
if (parts) {
var middle = parseCmdStr(parts[2]);
return {
head: parts[1],
middle: middle,
tail: parts[3],
whole: parts[1] + middle.whole + parts[3]
};
}
else {
return {
head: null,
middle: null,
tail: null,
whole: cmdStr
};
}
}
function formatRange(cmd, formatStart, formatEnd, separator) {
if (cmd.middle) {
var startHead = formatStart(cmd.head);
var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var startTail = formatStart(cmd.tail);
var endHead = formatEnd(cmd.head);
var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var endTail = formatEnd(cmd.tail);
if (startHead === endHead && startTail === endTail) {
return startHead +
(startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) +
startTail;
}
}
var startWhole = formatStart(cmd.whole);
var endWhole = formatEnd(cmd.whole);
if (startWhole === endWhole) {
return startWhole;
}
else {
return startWhole + separator + endWhole;
}
}
exports.default = main;
exports.toDuration = toDuration;
exports.toMoment = toMoment;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../moment
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/moment' {
import * as momentNs from 'moment';
import { Calendar, Duration } from '@fullcalendar/core';
export function toMoment(date: Date, calendar: Calendar): momentNs.Moment;
export function toDuration(fcDuration: Duration): momentNs.Duration;
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar Moment Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("moment"),require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","moment","@fullcalendar/core"],t):t((e=e||self).FullCalendarMoment={},e.moment,e.FullCalendar)}(this,function(e,t,n){"use strict";var r=t;var a=n.createPlugin({cmdFormatter:function(e,t){var n=function e(t){var n=t.match(/^(.*?)\{(.*)\}(.*)$/);if(n){var r=e(n[2]);return{head:n[1],middle:r,tail:n[3],whole:n[1]+r.whole+n[3]}}return{head:null,middle:null,tail:null,whole:t}}(e);if(t.end){var r=l(t.start.array,t.timeZone,t.start.timeZoneOffset,t.localeCodes[0]),a=l(t.end.array,t.timeZone,t.end.timeZoneOffset,t.localeCodes[0]);return function e(t,n,r,a){if(t.middle){var o=n(t.head),l=e(t.middle,n,r,a),u=n(t.tail),i=r(t.head),d=e(t.middle,n,r,a),f=r(t.tail);if(o===i&&u===f)return o+(l===d?l:l+a+d)+u}var c=n(t.whole),m=r(t.whole);return c===m?c:c+a+m}(n,o(r),o(a),t.separator)}return l(t.date.array,t.timeZone,t.date.timeZoneOffset,t.localeCodes[0]).format(n.whole)}});function o(e){return function(t){return t?e.format(t):""}}function l(e,t,n,a){var o;return"local"===t?o=r(e):"UTC"===t?o=r.utc(e):r.tz?o=r.tz(e,t):(o=r.utc(e),null!=n&&o.utcOffset(n)),o.locale(a),o}e.default=a,e.toDuration=function(e){return r.duration(e)},e.toMoment=function(e,t){if(!(t instanceof n.Calendar))throw new Error("must supply a Calendar instance");return l(e,t.dateEnv.timeZone,null,t.dateEnv.locale.codes[0])},Object.defineProperty(e,"__esModule",{value:!0})});
/*!
FullCalendar Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { addWeeks, diffWeeks, DateProfileGenerator, createElement, listenBySelector, removeElement, computeRect, computeClippingRect, applyStyle, cssToStr, htmlEscape, FgEventRenderer, appendToElement, prependToElement, htmlToElement, FillRenderer, memoizeRendering, createFormatter, addDays, DateComponent, rangeContainsMarker, getDayClasses, findElements, PositionCache, buildGotoAnchorHtml, findChildren, insertAfterElement, intersectRanges, ScrollComponent, matchCellWidths, uncompensateScroll, compensateScroll, subtractInnerElHeight, distributeHeight, undistributeHeight, View, Slicer, memoize, DayHeader, DaySeries, DayTable, createPlugin } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var DayGridDateProfileGenerator = /** @class */ (function (_super) {
__extends(DayGridDateProfileGenerator, _super);
function DayGridDateProfileGenerator() {
return _super !== null && _super.apply(this, arguments) || this;
}
// Computes the date range that will be rendered.
DayGridDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
var dateEnv = this.dateEnv;
var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay);
var start = renderRange.start;
var end = renderRange.end;
var endOfWeek;
// year and month views should be aligned with weeks. this is already done for week
if (/^(year|month)$/.test(currentRangeUnit)) {
start = dateEnv.startOfWeek(start);
// make end-of-week if not already
endOfWeek = dateEnv.startOfWeek(end);
if (endOfWeek.valueOf() !== end.valueOf()) {
end = addWeeks(endOfWeek, 1);
}
}
// ensure 6 weeks
if (this.options.monthMode &&
this.options.fixedWeekCount) {
var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
diffWeeks(start, end));
end = addWeeks(end, 6 - rowCnt);
}
return { start: start, end: end };
};
return DayGridDateProfileGenerator;
}(DateProfileGenerator));
/* A rectangular panel that is absolutely positioned over other content
------------------------------------------------------------------------------------------------------------------------
Options:
- className (string)
- content (HTML string, element, or element array)
- parentEl
- top
- left
- right (the x coord of where the right edge should be. not a "CSS" right)
- autoHide (boolean)
- show (callback)
- hide (callback)
*/
var Popover = /** @class */ (function () {
function Popover(options) {
var _this = this;
this.isHidden = true;
this.margin = 10; // the space required between the popover and the edges of the scroll container
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
this.documentMousedown = function (ev) {
// only hide the popover if the click happened outside the popover
if (_this.el && !_this.el.contains(ev.target)) {
_this.hide();
}
};
this.options = options;
}
// Shows the popover on the specified position. Renders it if not already
Popover.prototype.show = function () {
if (this.isHidden) {
if (!this.el) {
this.render();
}
this.el.style.display = '';
this.position();
this.isHidden = false;
this.trigger('show');
}
};
// Hides the popover, through CSS, but does not remove it from the DOM
Popover.prototype.hide = function () {
if (!this.isHidden) {
this.el.style.display = 'none';
this.isHidden = true;
this.trigger('hide');
}
};
// Creates `this.el` and renders content inside of it
Popover.prototype.render = function () {
var _this = this;
var options = this.options;
var el = this.el = createElement('div', {
className: 'fc-popover ' + (options.className || ''),
style: {
top: '0',
left: '0'
}
});
if (typeof options.content === 'function') {
options.content(el);
}
options.parentEl.appendChild(el);
// when a click happens on anything inside with a 'fc-close' className, hide the popover
listenBySelector(el, 'click', '.fc-close', function (ev) {
_this.hide();
});
if (options.autoHide) {
document.addEventListener('mousedown', this.documentMousedown);
}
};
// Hides and unregisters any handlers
Popover.prototype.destroy = function () {
this.hide();
if (this.el) {
removeElement(this.el);
this.el = null;
}
document.removeEventListener('mousedown', this.documentMousedown);
};
// Positions the popover optimally, using the top/left/right options
Popover.prototype.position = function () {
var options = this.options;
var el = this.el;
var elDims = el.getBoundingClientRect(); // only used for width,height
var origin = computeRect(el.offsetParent);
var clippingRect = computeClippingRect(options.parentEl);
var top; // the "position" (not "offset") values for the popover
var left; //
// compute top and left
top = options.top || 0;
if (options.left !== undefined) {
left = options.left;
}
else if (options.right !== undefined) {
left = options.right - elDims.width; // derive the left value from the right value
}
else {
left = 0;
}
// constrain to the view port. if constrained by two edges, give precedence to top/left
top = Math.min(top, clippingRect.bottom - elDims.height - this.margin);
top = Math.max(top, clippingRect.top + this.margin);
left = Math.min(left, clippingRect.right - elDims.width - this.margin);
left = Math.max(left, clippingRect.left + this.margin);
applyStyle(el, {
top: top - origin.top,
left: left - origin.left
});
};
// Triggers a callback. Calls a function in the option hash of the same name.
// Arguments beyond the first `name` are forwarded on.
// TODO: better code reuse for this. Repeat code
// can kill this???
Popover.prototype.trigger = function (name) {
if (this.options[name]) {
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
}
};
return Popover;
}());
/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/
// "Simple" is bad a name. has nothing to do with SimpleDayGrid
var SimpleDayGridEventRenderer = /** @class */ (function (_super) {
__extends(SimpleDayGridEventRenderer, _super);
function SimpleDayGridEventRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
// Builds the HTML to be used for the default element for an individual segment
SimpleDayGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var _a = this.context, view = _a.view, options = _a.options;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var allDay = eventDef.allDay;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = allDay && seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = allDay && seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
var skinCss = cssToStr(this.getSkinCss(eventUi));
var timeHtml = '';
var timeText;
var titleHtml;
classes.unshift('fc-day-grid-event', 'fc-h-event');
// Only display a timed events time if it is the starting segment
if (seg.isStart) {
timeText = this.getTimeText(eventRange);
if (timeText) {
timeHtml = '' + htmlEscape(timeText) + '';
}
}
titleHtml =
'' +
(htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height
'';
return '' +
'
(options.dir === 'rtl' ?
titleHtml + ' ' + timeHtml : // put a natural space in between
timeHtml + ' ' + titleHtml //
) +
'
(isResizableFromStart ?
'' :
'') +
(isResizableFromEnd ?
'' :
'') +
'';
};
// Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined
SimpleDayGridEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'narrow'
};
};
SimpleDayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return false; // TODO: somehow consider the originating DayGrid's column count
};
return SimpleDayGridEventRenderer;
}(FgEventRenderer));
/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/
var DayGridEventRenderer = /** @class */ (function (_super) {
__extends(DayGridEventRenderer, _super);
function DayGridEventRenderer(dayGrid) {
var _this = _super.call(this, dayGrid.context) || this;
_this.dayGrid = dayGrid;
return _this;
}
// Renders the given foreground event segments onto the grid
DayGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var rowStructs = this.rowStructs = this.renderSegRows(segs);
// append to each row's content skeleton
this.dayGrid.rowEls.forEach(function (rowNode, i) {
rowNode.querySelector('.fc-content-skeleton > table').appendChild(rowStructs[i].tbodyEl);
});
// removes the "more.." events popover
if (!mirrorInfo) {
this.dayGrid.removeSegPopover();
}
};
// Unrenders all currently rendered foreground event segments
DayGridEventRenderer.prototype.detachSegs = function () {
var rowStructs = this.rowStructs || [];
var rowStruct;
while ((rowStruct = rowStructs.pop())) {
removeElement(rowStruct.tbodyEl);
}
this.rowStructs = null;
};
// Uses the given events array to generate elements that should be appended to each row's content skeleton.
// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
DayGridEventRenderer.prototype.renderSegRows = function (segs) {
var rowStructs = [];
var segRows;
var row;
segRows = this.groupSegRows(segs); // group into nested arrays
// iterate each row of segment groupings
for (row = 0; row < segRows.length; row++) {
rowStructs.push(this.renderSegRow(row, segRows[row]));
}
return rowStructs;
};
// Given a row # and an array of segments all in the same row, render a element, a skeleton that contains
// the segments. Returns object with a bunch of internal data about how the render was calculated.
// NOTE: modifies rowSegs
DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) {
var dayGrid = this.dayGrid;
var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl;
var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
var tbody = document.createElement('tbody');
var segMatrix = []; // lookup for which segments are rendered into which level+col cells
var cellMatrix = []; // lookup for all elements of the level+col matrix
var loneCellMatrix = []; // lookup for elements that only take up a single column
var i;
var levelSegs;
var col;
var tr;
var j;
var seg;
var td;
// populates empty cells from the current column (`col`) to `endCol`
function emptyCellsUntil(endCol) {
while (col < endCol) {
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
td = (loneCellMatrix[i - 1] || [])[col];
if (td) {
td.rowSpan = (td.rowSpan || 1) + 1;
}
else {
td = document.createElement('td');
tr.appendChild(td);
}
cellMatrix[i][col] = td;
loneCellMatrix[i][col] = td;
col++;
}
}
for (i = 0; i < levelCnt; i++) { // iterate through all levels
levelSegs = segLevels[i];
col = 0;
tr = document.createElement('tr');
segMatrix.push([]);
cellMatrix.push([]);
loneCellMatrix.push([]);
// levelCnt might be 1 even though there are no actual levels. protect against this.
// this single empty row is useful for styling.
if (levelSegs) {
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
seg = levelSegs[j];
var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol;
var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol;
emptyCellsUntil(leftCol);
// create a container that occupies or more columns. append the event element.
td = createElement('td', { className: 'fc-event-container' }, seg.el);
if (leftCol !== rightCol) {
td.colSpan = rightCol - leftCol + 1;
}
else { // a single-column segment
loneCellMatrix[i][col] = td;
}
while (col <= rightCol) {
cellMatrix[i][col] = td;
segMatrix[i][col] = seg;
col++;
}
tr.appendChild(td);
}
}
emptyCellsUntil(colCnt); // finish off the row
var introHtml = dayGrid.renderProps.renderIntroHtml();
if (introHtml) {
if (dayGrid.isRtl) {
appendToElement(tr, introHtml);
}
else {
prependToElement(tr, introHtml);
}
}
tbody.appendChild(tr);
}
return {
row: row,
tbodyEl: tbody,
cellMatrix: cellMatrix,
segMatrix: segMatrix,
segLevels: segLevels,
segs: rowSegs
};
};
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
// NOTE: modifies segs
DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
var _a = this.dayGrid, isRtl = _a.isRtl, colCnt = _a.colCnt;
var levels = [];
var i;
var seg;
var j;
// Give preference to elements with certain criteria, so they have
// a chance to be closer to the top.
segs = this.sortEventSegs(segs);
for (i = 0; i < segs.length; i++) {
seg = segs[i];
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
for (j = 0; j < levels.length; j++) {
if (!isDaySegCollision(seg, levels[j])) {
break;
}
}
// `j` now holds the desired subrow index
seg.level = j;
seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only
seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol // for sorting only
;
(levels[j] || (levels[j] = [])).push(seg);
}
// order segments left-to-right. very important if calendar is RTL
for (j = 0; j < levels.length; j++) {
levels[j].sort(compareDaySegCols);
}
return levels;
};
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
DayGridEventRenderer.prototype.groupSegRows = function (segs) {
var segRows = [];
var i;
for (i = 0; i < this.dayGrid.rowCnt; i++) {
segRows.push([]);
}
for (i = 0; i < segs.length; i++) {
segRows[segs[i].row].push(segs[i]);
}
return segRows;
};
// Computes a default `displayEventEnd` value if one is not expliclty defined
DayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
};
return DayGridEventRenderer;
}(SimpleDayGridEventRenderer));
// Computes whether two segments' columns collide. They are assumed to be in the same row.
function isDaySegCollision(seg, otherSegs) {
var i;
var otherSeg;
for (i = 0; i < otherSegs.length; i++) {
otherSeg = otherSegs[i];
if (otherSeg.firstCol <= seg.lastCol &&
otherSeg.lastCol >= seg.firstCol) {
return true;
}
}
return false;
}
// A cmp function for determining the leftmost event
function compareDaySegCols(a, b) {
return a.leftCol - b.leftCol;
}
var DayGridMirrorRenderer = /** @class */ (function (_super) {
__extends(DayGridMirrorRenderer, _super);
function DayGridMirrorRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var sourceSeg = mirrorInfo.sourceSeg;
var rowStructs = this.rowStructs = this.renderSegRows(segs);
// inject each new event skeleton into each associated row
this.dayGrid.rowEls.forEach(function (rowNode, row) {
var skeletonEl = htmlToElement('
var EMPTY_CELL_HTML = '';
var DayGridFillRenderer = /** @class */ (function (_super) {
__extends(DayGridFillRenderer, _super);
function DayGridFillRenderer(dayGrid) {
var _this = _super.call(this, dayGrid.context) || this;
_this.fillSegTag = 'td'; // override the default tag name
_this.dayGrid = dayGrid;
return _this;
}
DayGridFillRenderer.prototype.renderSegs = function (type, segs) {
// don't render timed background events
if (type === 'bgEvent') {
segs = segs.filter(function (seg) {
return seg.eventRange.def.allDay;
});
}
_super.prototype.renderSegs.call(this, type, segs);
};
DayGridFillRenderer.prototype.attachSegs = function (type, segs) {
var els = [];
var i;
var seg;
var skeletonEl;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
skeletonEl = this.renderFillRow(type, seg);
this.dayGrid.rowEls[seg.row].appendChild(skeletonEl);
els.push(skeletonEl);
}
return els;
};
// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
DayGridFillRenderer.prototype.renderFillRow = function (type, seg) {
var dayGrid = this.dayGrid;
var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl;
var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol;
var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol;
var startCol = leftCol;
var endCol = rightCol + 1;
var className;
var skeletonEl;
var trEl;
if (type === 'businessHours') {
className = 'bgevent';
}
else {
className = type.toLowerCase();
}
skeletonEl = htmlToElement('
var DayTile = /** @class */ (function (_super) {
__extends(DayTile, _super);
function DayTile(context, el) {
var _this = _super.call(this, context, el) || this;
var eventRenderer = _this.eventRenderer = new DayTileEventRenderer(_this);
var renderFrame = _this.renderFrame = memoizeRendering(_this._renderFrame);
_this.renderFgEvents = memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderFrame]);
_this.renderEventSelection = memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]);
_this.renderEventResize = memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]);
context.calendar.registerInteractiveComponent(_this, {
el: _this.el,
useEventCenter: false
});
return _this;
}
DayTile.prototype.render = function (props) {
this.renderFrame(props.date);
this.renderFgEvents(props.fgSegs);
this.renderEventSelection(props.eventSelection);
this.renderEventDrag(props.eventDragInstances);
this.renderEventResize(props.eventResizeInstances);
};
DayTile.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderFrame.unrender(); // should unrender everything else
this.calendar.unregisterInteractiveComponent(this);
};
DayTile.prototype._renderFrame = function (date) {
var _a = this, theme = _a.theme, dateEnv = _a.dateEnv;
var title = dateEnv.format(date, createFormatter(this.opt('dayPopoverFormat')) // TODO: cache
);
this.el.innerHTML =
'
var DayBgRow = /** @class */ (function () {
function DayBgRow(context) {
this.context = context;
}
DayBgRow.prototype.renderHtml = function (props) {
var parts = [];
if (props.renderIntroHtml) {
parts.push(props.renderIntroHtml());
}
for (var _i = 0, _a = props.cells; _i < _a.length; _i++) {
var cell = _a[_i];
parts.push(renderCellHtml(cell.date, props.dateProfile, this.context, cell.htmlAttrs));
}
if (!props.cells.length) {
parts.push('');
}
if (this.context.options.dir === 'rtl') {
parts.reverse();
}
return '' + parts.join('') + '';
};
return DayBgRow;
}());
function renderCellHtml(date, dateProfile, context, otherAttrs) {
var dateEnv = context.dateEnv, theme = context.theme;
var isDateValid = rangeContainsMarker(dateProfile.activeRange, date); // TODO: called too frequently. cache somehow.
var classes = getDayClasses(date, dateProfile, context);
classes.unshift('fc-day', theme.getClass('widgetContent'));
return '';
}
var DAY_NUM_FORMAT = createFormatter({ day: 'numeric' });
var WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' });
var DayGrid = /** @class */ (function (_super) {
__extends(DayGrid, _super);
function DayGrid(context, el, renderProps) {
var _this = _super.call(this, context, el) || this;
_this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid
_this.isCellSizesDirty = false;
var eventRenderer = _this.eventRenderer = new DayGridEventRenderer(_this);
var fillRenderer = _this.fillRenderer = new DayGridFillRenderer(_this);
_this.mirrorRenderer = new DayGridMirrorRenderer(_this);
var renderCells = _this.renderCells = memoizeRendering(_this._renderCells, _this._unrenderCells);
_this.renderBusinessHours = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderCells]);
_this.renderDateSelection = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'), [renderCells]);
_this.renderBgEvents = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderCells]);
_this.renderFgEvents = memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderCells]);
_this.renderEventSelection = memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderCells]);
_this.renderEventResize = memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderCells]);
_this.renderProps = renderProps;
return _this;
}
DayGrid.prototype.render = function (props) {
var cells = props.cells;
this.rowCnt = cells.length;
this.colCnt = cells[0].length;
this.renderCells(cells, props.isRigid);
this.renderBusinessHours(props.businessHourSegs);
this.renderDateSelection(props.dateSelectionSegs);
this.renderBgEvents(props.bgEventSegs);
this.renderFgEvents(props.fgEventSegs);
this.renderEventSelection(props.eventSelection);
this.renderEventDrag(props.eventDrag);
this.renderEventResize(props.eventResize);
if (this.segPopoverTile) {
this.updateSegPopoverTile();
}
};
DayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderCells.unrender(); // will unrender everything else
};
DayGrid.prototype.getCellRange = function (row, col) {
var start = this.props.cells[row][col].date;
var end = addDays(start, 1);
return { start: start, end: end };
};
DayGrid.prototype.updateSegPopoverTile = function (date, segs) {
var ownProps = this.props;
this.segPopoverTile.receiveProps({
date: date || this.segPopoverTile.props.date,
fgSegs: segs || this.segPopoverTile.props.fgSegs,
eventSelection: ownProps.eventSelection,
eventDragInstances: ownProps.eventDrag ? ownProps.eventDrag.affectedInstances : null,
eventResizeInstances: ownProps.eventResize ? ownProps.eventResize.affectedInstances : null
});
};
/* Date Rendering
------------------------------------------------------------------------------------------------------------------*/
DayGrid.prototype._renderCells = function (cells, isRigid) {
var _a = this, view = _a.view, dateEnv = _a.dateEnv;
var _b = this, rowCnt = _b.rowCnt, colCnt = _b.colCnt;
var html = '';
var row;
var col;
for (row = 0; row < rowCnt; row++) {
html += this.renderDayRowHtml(row, isRigid);
}
this.el.innerHTML = html;
this.rowEls = findElements(this.el, '.fc-row');
this.cellEls = findElements(this.el, '.fc-day, .fc-disabled-day');
if (this.isRtl) {
this.cellEls.reverse();
}
this.rowPositions = new PositionCache(this.el, this.rowEls, false, true // vertical
);
this.colPositions = new PositionCache(this.el, this.cellEls.slice(0, colCnt), // only the first row
true, false // horizontal
);
// trigger dayRender with each cell's element
for (row = 0; row < rowCnt; row++) {
for (col = 0; col < colCnt; col++) {
this.publiclyTrigger('dayRender', [
{
date: dateEnv.toDate(cells[row][col].date),
el: this.getCellEl(row, col),
view: view
}
]);
}
}
this.isCellSizesDirty = true;
};
DayGrid.prototype._unrenderCells = function () {
this.removeSegPopover();
};
// Generates the HTML for a single row, which is a div that wraps a table.
// `row` is the row number.
DayGrid.prototype.renderDayRowHtml = function (row, isRigid) {
var theme = this.theme;
var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')];
if (isRigid) {
classes.push('fc-rigid');
}
var bgRow = new DayBgRow(this.context);
return '' +
'
var WEEK_NUM_FORMAT$1 = createFormatter({ week: 'numeric' });
/* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells.
----------------------------------------------------------------------------------------------------------------------*/
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
// It is responsible for managing width/height.
var DayGridView = /** @class */ (function (_super) {
__extends(DayGridView, _super);
function DayGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
/* Header Rendering
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before the day-of week header cells
_this.renderHeadIntroHtml = function () {
var theme = _this.theme;
if (_this.colWeekNumbersVisible) {
return '' +
'' +
'' + // needed for matchCellWidths
htmlEscape(_this.opt('weekLabel')) +
'' +
'';
}
return '';
};
/* Day Grid Rendering
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
_this.renderDayGridNumberIntroHtml = function (row, dayGrid) {
var dateEnv = _this.dateEnv;
var weekStart = dayGrid.props.cells[row][0].date;
if (_this.colWeekNumbersVisible) {
return '' +
'' +
buildGotoAnchorHtml(// aside from link, important for matchCellWidths
_this, { date: weekStart, type: 'week', forceOff: dayGrid.colCnt === 1 }, dateEnv.format(weekStart, WEEK_NUM_FORMAT$1) // inner HTML
) +
'';
}
return '';
};
// Generates the HTML that goes before the day bg cells for each day-row
_this.renderDayGridBgIntroHtml = function () {
var theme = _this.theme;
if (_this.colWeekNumbersVisible) {
return '';
}
return '';
};
// Generates the HTML that goes before every other type of row generated by DayGrid.
// Affects mirror-skeleton and highlight-skeleton rows.
_this.renderDayGridIntroHtml = function () {
if (_this.colWeekNumbersVisible) {
return '';
}
return '';
};
_this.el.classList.add('fc-dayGrid-view');
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.scroller = new ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
var dayGridContainerEl = _this.scroller.el;
_this.el.querySelector('.fc-body > tr > td').appendChild(dayGridContainerEl);
dayGridContainerEl.classList.add('fc-day-grid-container');
var dayGridEl = createElement('div', { className: 'fc-day-grid' });
dayGridContainerEl.appendChild(dayGridEl);
var cellWeekNumbersVisible;
if (_this.opt('weekNumbers')) {
if (_this.opt('weekNumbersWithinDays')) {
cellWeekNumbersVisible = true;
_this.colWeekNumbersVisible = false;
}
else {
cellWeekNumbersVisible = false;
_this.colWeekNumbersVisible = true;
}
}
else {
_this.colWeekNumbersVisible = false;
cellWeekNumbersVisible = false;
}
_this.dayGrid = new DayGrid(_this.context, dayGridEl, {
renderNumberIntroHtml: _this.renderDayGridNumberIntroHtml,
renderBgIntroHtml: _this.renderDayGridBgIntroHtml,
renderIntroHtml: _this.renderDayGridIntroHtml,
colWeekNumbersVisible: _this.colWeekNumbersVisible,
cellWeekNumbersVisible: cellWeekNumbersVisible
});
return _this;
}
DayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.dayGrid.destroy();
this.scroller.destroy();
};
// Builds the HTML skeleton for the view.
// The day-grid component will render inside of a container defined by this HTML.
DayGridView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return '' +
'' +
(this.opt('columnHeader') ?
'' +
'' +
' ' +
'' +
'' :
'') +
'' +
'' +
'' +
'' +
'' +
'';
};
// Generates an HTML attribute string for setting the width of the week number column, if it is known
DayGridView.prototype.weekNumberStyleAttr = function () {
if (this.weekNumberWidth != null) {
return 'style="width:' + this.weekNumberWidth + 'px"';
}
return '';
};
// Determines whether each row should have a constant height
DayGridView.prototype.hasRigidRows = function () {
var eventLimit = this.opt('eventLimit');
return eventLimit && typeof eventLimit !== 'number';
};
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
DayGridView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
_super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); // will call updateBaseSize. important that executes first
this.dayGrid.updateSize(isResize);
};
// Refreshes the horizontal dimensions of the view
DayGridView.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) {
var dayGrid = this.dayGrid;
var eventLimit = this.opt('eventLimit');
var headRowEl = this.header ? this.header.el : null; // HACK
var scrollerHeight;
var scrollbarWidths;
// hack to give the view some height prior to dayGrid's columns being rendered
// TODO: separate setting height from scroller VS dayGrid.
if (!dayGrid.rowEls) {
if (!isAuto) {
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.scroller.setHeight(scrollerHeight);
}
return;
}
if (this.colWeekNumbersVisible) {
// Make sure all week number cells running down the side have the same width.
this.weekNumberWidth = matchCellWidths(findElements(this.el, '.fc-week-number'));
}
// reset all heights to be natural
this.scroller.clear();
if (headRowEl) {
uncompensateScroll(headRowEl);
}
dayGrid.removeSegPopover(); // kill the "more" popover if displayed
// is the event limit a constant level number?
if (eventLimit && typeof eventLimit === 'number') {
dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
}
// distribute the height to the rows
// (viewHeight is a "recommended" value if isAuto)
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.setGridHeight(scrollerHeight, isAuto);
// is the event limit dynamically calculated?
if (eventLimit && typeof eventLimit !== 'number') {
dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
}
if (!isAuto) { // should we force dimensions of the scroll container?
this.scroller.setHeight(scrollerHeight);
scrollbarWidths = this.scroller.getScrollbarWidths();
if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
if (headRowEl) {
compensateScroll(headRowEl, scrollbarWidths);
}
// doing the scrollbar compensation might have created text overflow which created more height. redo
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.scroller.setHeight(scrollerHeight);
}
// guarantees the same scrollbar widths
this.scroller.lockOverflow(scrollbarWidths);
}
};
// given a desired total height of the view, returns what the height of the scroller should be
DayGridView.prototype.computeScrollerHeight = function (viewHeight) {
return viewHeight -
subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
};
// Sets the height of just the DayGrid component in this view
DayGridView.prototype.setGridHeight = function (height, isAuto) {
if (this.opt('monthMode')) {
// if auto, make the height of each row the height that it would be if there were 6 weeks
if (isAuto) {
height *= this.dayGrid.rowCnt / 6;
}
distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
}
else {
if (isAuto) {
undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
}
else {
distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
}
}
};
/* Scroll
------------------------------------------------------------------------------------------------------------------*/
DayGridView.prototype.computeDateScroll = function (duration) {
return { top: 0 };
};
DayGridView.prototype.queryDateScroll = function () {
return { top: this.scroller.getScrollTop() };
};
DayGridView.prototype.applyDateScroll = function (scroll) {
if (scroll.top !== undefined) {
this.scroller.setScrollTop(scroll.top);
}
};
return DayGridView;
}(View));
DayGridView.prototype.dateProfileGeneratorClass = DayGridDateProfileGenerator;
var SimpleDayGrid = /** @class */ (function (_super) {
__extends(SimpleDayGrid, _super);
function SimpleDayGrid(context, dayGrid) {
var _this = _super.call(this, context, dayGrid.el) || this;
_this.slicer = new DayGridSlicer();
_this.dayGrid = dayGrid;
context.calendar.registerInteractiveComponent(_this, { el: _this.dayGrid.el });
return _this;
}
SimpleDayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
SimpleDayGrid.prototype.render = function (props) {
var dayGrid = this.dayGrid;
var dateProfile = props.dateProfile, dayTable = props.dayTable;
dayGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, dayGrid, dayTable), { dateProfile: dateProfile, cells: dayTable.cells, isRigid: props.isRigid }));
};
SimpleDayGrid.prototype.buildPositionCaches = function () {
this.dayGrid.buildPositionCaches();
};
SimpleDayGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.dayGrid,
dateSpan: rawHit.dateSpan,
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return SimpleDayGrid;
}(DateComponent));
var DayGridSlicer = /** @class */ (function (_super) {
__extends(DayGridSlicer, _super);
function DayGridSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayGridSlicer.prototype.sliceRange = function (dateRange, dayTable) {
return dayTable.sliceRange(dateRange);
};
return DayGridSlicer;
}(Slicer));
var DayGridView$1 = /** @class */ (function (_super) {
__extends(DayGridView, _super);
function DayGridView(_context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, _context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.buildDayTable = memoize(buildDayTable);
if (_this.opt('columnHeader')) {
_this.header = new DayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.simpleDayGrid = new SimpleDayGrid(_this.context, _this.dayGrid);
return _this;
}
DayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.simpleDayGrid.destroy();
};
DayGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props);
var dateProfile = this.props.dateProfile;
var dayTable = this.dayTable =
this.buildDayTable(dateProfile, this.dateProfileGenerator);
if (this.header) {
this.header.receiveProps({
dateProfile: dateProfile,
dates: dayTable.headerDates,
datesRepDistinctDays: dayTable.rowCnt === 1,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.simpleDayGrid.receiveProps({
dateProfile: dateProfile,
dayTable: dayTable,
businessHours: props.businessHours,
dateSelection: props.dateSelection,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
isRigid: this.hasRigidRows(),
nextDayThreshold: this.nextDayThreshold
});
};
return DayGridView;
}(DayGridView));
function buildDayTable(dateProfile, dateProfileGenerator) {
var daySeries = new DaySeries(dateProfile.renderRange, dateProfileGenerator);
return new DayTable(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit));
}
var main = createPlugin({
defaultView: 'dayGridMonth',
views: {
dayGrid: DayGridView$1,
dayGridDay: {
type: 'dayGrid',
duration: { days: 1 }
},
dayGridWeek: {
type: 'dayGrid',
duration: { weeks: 1 }
},
dayGridMonth: {
type: 'dayGrid',
duration: { months: 1 },
monthMode: true,
fixedWeekCount: true
}
}
});
export default main;
export { DayGridView as AbstractDayGridView, DayBgRow, DayGrid, DayGridSlicer, DayGridView$1 as DayGridView, SimpleDayGrid, buildDayTable as buildBasicDayTable };
/*!
FullCalendar Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarDayGrid = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var DayGridDateProfileGenerator = /** @class */ (function (_super) {
__extends(DayGridDateProfileGenerator, _super);
function DayGridDateProfileGenerator() {
return _super !== null && _super.apply(this, arguments) || this;
}
// Computes the date range that will be rendered.
DayGridDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
var dateEnv = this.dateEnv;
var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay);
var start = renderRange.start;
var end = renderRange.end;
var endOfWeek;
// year and month views should be aligned with weeks. this is already done for week
if (/^(year|month)$/.test(currentRangeUnit)) {
start = dateEnv.startOfWeek(start);
// make end-of-week if not already
endOfWeek = dateEnv.startOfWeek(end);
if (endOfWeek.valueOf() !== end.valueOf()) {
end = core.addWeeks(endOfWeek, 1);
}
}
// ensure 6 weeks
if (this.options.monthMode &&
this.options.fixedWeekCount) {
var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
core.diffWeeks(start, end));
end = core.addWeeks(end, 6 - rowCnt);
}
return { start: start, end: end };
};
return DayGridDateProfileGenerator;
}(core.DateProfileGenerator));
/* A rectangular panel that is absolutely positioned over other content
------------------------------------------------------------------------------------------------------------------------
Options:
- className (string)
- content (HTML string, element, or element array)
- parentEl
- top
- left
- right (the x coord of where the right edge should be. not a "CSS" right)
- autoHide (boolean)
- show (callback)
- hide (callback)
*/
var Popover = /** @class */ (function () {
function Popover(options) {
var _this = this;
this.isHidden = true;
this.margin = 10; // the space required between the popover and the edges of the scroll container
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
this.documentMousedown = function (ev) {
// only hide the popover if the click happened outside the popover
if (_this.el && !_this.el.contains(ev.target)) {
_this.hide();
}
};
this.options = options;
}
// Shows the popover on the specified position. Renders it if not already
Popover.prototype.show = function () {
if (this.isHidden) {
if (!this.el) {
this.render();
}
this.el.style.display = '';
this.position();
this.isHidden = false;
this.trigger('show');
}
};
// Hides the popover, through CSS, but does not remove it from the DOM
Popover.prototype.hide = function () {
if (!this.isHidden) {
this.el.style.display = 'none';
this.isHidden = true;
this.trigger('hide');
}
};
// Creates `this.el` and renders content inside of it
Popover.prototype.render = function () {
var _this = this;
var options = this.options;
var el = this.el = core.createElement('div', {
className: 'fc-popover ' + (options.className || ''),
style: {
top: '0',
left: '0'
}
});
if (typeof options.content === 'function') {
options.content(el);
}
options.parentEl.appendChild(el);
// when a click happens on anything inside with a 'fc-close' className, hide the popover
core.listenBySelector(el, 'click', '.fc-close', function (ev) {
_this.hide();
});
if (options.autoHide) {
document.addEventListener('mousedown', this.documentMousedown);
}
};
// Hides and unregisters any handlers
Popover.prototype.destroy = function () {
this.hide();
if (this.el) {
core.removeElement(this.el);
this.el = null;
}
document.removeEventListener('mousedown', this.documentMousedown);
};
// Positions the popover optimally, using the top/left/right options
Popover.prototype.position = function () {
var options = this.options;
var el = this.el;
var elDims = el.getBoundingClientRect(); // only used for width,height
var origin = core.computeRect(el.offsetParent);
var clippingRect = core.computeClippingRect(options.parentEl);
var top; // the "position" (not "offset") values for the popover
var left; //
// compute top and left
top = options.top || 0;
if (options.left !== undefined) {
left = options.left;
}
else if (options.right !== undefined) {
left = options.right - elDims.width; // derive the left value from the right value
}
else {
left = 0;
}
// constrain to the view port. if constrained by two edges, give precedence to top/left
top = Math.min(top, clippingRect.bottom - elDims.height - this.margin);
top = Math.max(top, clippingRect.top + this.margin);
left = Math.min(left, clippingRect.right - elDims.width - this.margin);
left = Math.max(left, clippingRect.left + this.margin);
core.applyStyle(el, {
top: top - origin.top,
left: left - origin.left
});
};
// Triggers a callback. Calls a function in the option hash of the same name.
// Arguments beyond the first `name` are forwarded on.
// TODO: better code reuse for this. Repeat code
// can kill this???
Popover.prototype.trigger = function (name) {
if (this.options[name]) {
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
}
};
return Popover;
}());
/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/
// "Simple" is bad a name. has nothing to do with SimpleDayGrid
var SimpleDayGridEventRenderer = /** @class */ (function (_super) {
__extends(SimpleDayGridEventRenderer, _super);
function SimpleDayGridEventRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
// Builds the HTML to be used for the default element for an individual segment
SimpleDayGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var _a = this.context, view = _a.view, options = _a.options;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var allDay = eventDef.allDay;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = allDay && seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = allDay && seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
var skinCss = core.cssToStr(this.getSkinCss(eventUi));
var timeHtml = '';
var timeText;
var titleHtml;
classes.unshift('fc-day-grid-event', 'fc-h-event');
// Only display a timed events time if it is the starting segment
if (seg.isStart) {
timeText = this.getTimeText(eventRange);
if (timeText) {
timeHtml = '' + core.htmlEscape(timeText) + '';
}
}
titleHtml =
'' +
(core.htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height
'';
return '' +
'
(options.dir === 'rtl' ?
titleHtml + ' ' + timeHtml : // put a natural space in between
timeHtml + ' ' + titleHtml //
) +
'
(isResizableFromStart ?
'' :
'') +
(isResizableFromEnd ?
'' :
'') +
'';
};
// Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined
SimpleDayGridEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'narrow'
};
};
SimpleDayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return false; // TODO: somehow consider the originating DayGrid's column count
};
return SimpleDayGridEventRenderer;
}(core.FgEventRenderer));
/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/
var DayGridEventRenderer = /** @class */ (function (_super) {
__extends(DayGridEventRenderer, _super);
function DayGridEventRenderer(dayGrid) {
var _this = _super.call(this, dayGrid.context) || this;
_this.dayGrid = dayGrid;
return _this;
}
// Renders the given foreground event segments onto the grid
DayGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var rowStructs = this.rowStructs = this.renderSegRows(segs);
// append to each row's content skeleton
this.dayGrid.rowEls.forEach(function (rowNode, i) {
rowNode.querySelector('.fc-content-skeleton > table').appendChild(rowStructs[i].tbodyEl);
});
// removes the "more.." events popover
if (!mirrorInfo) {
this.dayGrid.removeSegPopover();
}
};
// Unrenders all currently rendered foreground event segments
DayGridEventRenderer.prototype.detachSegs = function () {
var rowStructs = this.rowStructs || [];
var rowStruct;
while ((rowStruct = rowStructs.pop())) {
core.removeElement(rowStruct.tbodyEl);
}
this.rowStructs = null;
};
// Uses the given events array to generate elements that should be appended to each row's content skeleton.
// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
DayGridEventRenderer.prototype.renderSegRows = function (segs) {
var rowStructs = [];
var segRows;
var row;
segRows = this.groupSegRows(segs); // group into nested arrays
// iterate each row of segment groupings
for (row = 0; row < segRows.length; row++) {
rowStructs.push(this.renderSegRow(row, segRows[row]));
}
return rowStructs;
};
// Given a row # and an array of segments all in the same row, render a element, a skeleton that contains
// the segments. Returns object with a bunch of internal data about how the render was calculated.
// NOTE: modifies rowSegs
DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) {
var dayGrid = this.dayGrid;
var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl;
var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
var tbody = document.createElement('tbody');
var segMatrix = []; // lookup for which segments are rendered into which level+col cells
var cellMatrix = []; // lookup for all elements of the level+col matrix
var loneCellMatrix = []; // lookup for elements that only take up a single column
var i;
var levelSegs;
var col;
var tr;
var j;
var seg;
var td;
// populates empty cells from the current column (`col`) to `endCol`
function emptyCellsUntil(endCol) {
while (col < endCol) {
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
td = (loneCellMatrix[i - 1] || [])[col];
if (td) {
td.rowSpan = (td.rowSpan || 1) + 1;
}
else {
td = document.createElement('td');
tr.appendChild(td);
}
cellMatrix[i][col] = td;
loneCellMatrix[i][col] = td;
col++;
}
}
for (i = 0; i < levelCnt; i++) { // iterate through all levels
levelSegs = segLevels[i];
col = 0;
tr = document.createElement('tr');
segMatrix.push([]);
cellMatrix.push([]);
loneCellMatrix.push([]);
// levelCnt might be 1 even though there are no actual levels. protect against this.
// this single empty row is useful for styling.
if (levelSegs) {
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
seg = levelSegs[j];
var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol;
var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol;
emptyCellsUntil(leftCol);
// create a container that occupies or more columns. append the event element.
td = core.createElement('td', { className: 'fc-event-container' }, seg.el);
if (leftCol !== rightCol) {
td.colSpan = rightCol - leftCol + 1;
}
else { // a single-column segment
loneCellMatrix[i][col] = td;
}
while (col <= rightCol) {
cellMatrix[i][col] = td;
segMatrix[i][col] = seg;
col++;
}
tr.appendChild(td);
}
}
emptyCellsUntil(colCnt); // finish off the row
var introHtml = dayGrid.renderProps.renderIntroHtml();
if (introHtml) {
if (dayGrid.isRtl) {
core.appendToElement(tr, introHtml);
}
else {
core.prependToElement(tr, introHtml);
}
}
tbody.appendChild(tr);
}
return {
row: row,
tbodyEl: tbody,
cellMatrix: cellMatrix,
segMatrix: segMatrix,
segLevels: segLevels,
segs: rowSegs
};
};
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
// NOTE: modifies segs
DayGridEventRenderer.prototype.buildSegLevels = function (segs) {
var _a = this.dayGrid, isRtl = _a.isRtl, colCnt = _a.colCnt;
var levels = [];
var i;
var seg;
var j;
// Give preference to elements with certain criteria, so they have
// a chance to be closer to the top.
segs = this.sortEventSegs(segs);
for (i = 0; i < segs.length; i++) {
seg = segs[i];
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
for (j = 0; j < levels.length; j++) {
if (!isDaySegCollision(seg, levels[j])) {
break;
}
}
// `j` now holds the desired subrow index
seg.level = j;
seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only
seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol // for sorting only
;
(levels[j] || (levels[j] = [])).push(seg);
}
// order segments left-to-right. very important if calendar is RTL
for (j = 0; j < levels.length; j++) {
levels[j].sort(compareDaySegCols);
}
return levels;
};
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
DayGridEventRenderer.prototype.groupSegRows = function (segs) {
var segRows = [];
var i;
for (i = 0; i < this.dayGrid.rowCnt; i++) {
segRows.push([]);
}
for (i = 0; i < segs.length; i++) {
segRows[segs[i].row].push(segs[i]);
}
return segRows;
};
// Computes a default `displayEventEnd` value if one is not expliclty defined
DayGridEventRenderer.prototype.computeDisplayEventEnd = function () {
return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day
};
return DayGridEventRenderer;
}(SimpleDayGridEventRenderer));
// Computes whether two segments' columns collide. They are assumed to be in the same row.
function isDaySegCollision(seg, otherSegs) {
var i;
var otherSeg;
for (i = 0; i < otherSegs.length; i++) {
otherSeg = otherSegs[i];
if (otherSeg.firstCol <= seg.lastCol &&
otherSeg.lastCol >= seg.firstCol) {
return true;
}
}
return false;
}
// A cmp function for determining the leftmost event
function compareDaySegCols(a, b) {
return a.leftCol - b.leftCol;
}
var DayGridMirrorRenderer = /** @class */ (function (_super) {
__extends(DayGridMirrorRenderer, _super);
function DayGridMirrorRenderer() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
var sourceSeg = mirrorInfo.sourceSeg;
var rowStructs = this.rowStructs = this.renderSegRows(segs);
// inject each new event skeleton into each associated row
this.dayGrid.rowEls.forEach(function (rowNode, row) {
var skeletonEl = core.htmlToElement('
var EMPTY_CELL_HTML = '';
var DayGridFillRenderer = /** @class */ (function (_super) {
__extends(DayGridFillRenderer, _super);
function DayGridFillRenderer(dayGrid) {
var _this = _super.call(this, dayGrid.context) || this;
_this.fillSegTag = 'td'; // override the default tag name
_this.dayGrid = dayGrid;
return _this;
}
DayGridFillRenderer.prototype.renderSegs = function (type, segs) {
// don't render timed background events
if (type === 'bgEvent') {
segs = segs.filter(function (seg) {
return seg.eventRange.def.allDay;
});
}
_super.prototype.renderSegs.call(this, type, segs);
};
DayGridFillRenderer.prototype.attachSegs = function (type, segs) {
var els = [];
var i;
var seg;
var skeletonEl;
for (i = 0; i < segs.length; i++) {
seg = segs[i];
skeletonEl = this.renderFillRow(type, seg);
this.dayGrid.rowEls[seg.row].appendChild(skeletonEl);
els.push(skeletonEl);
}
return els;
};
// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
DayGridFillRenderer.prototype.renderFillRow = function (type, seg) {
var dayGrid = this.dayGrid;
var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl;
var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol;
var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol;
var startCol = leftCol;
var endCol = rightCol + 1;
var className;
var skeletonEl;
var trEl;
if (type === 'businessHours') {
className = 'bgevent';
}
else {
className = type.toLowerCase();
}
skeletonEl = core.htmlToElement('
var DayTile = /** @class */ (function (_super) {
__extends(DayTile, _super);
function DayTile(context, el) {
var _this = _super.call(this, context, el) || this;
var eventRenderer = _this.eventRenderer = new DayTileEventRenderer(_this);
var renderFrame = _this.renderFrame = core.memoizeRendering(_this._renderFrame);
_this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderFrame]);
_this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]);
_this.renderEventResize = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]);
context.calendar.registerInteractiveComponent(_this, {
el: _this.el,
useEventCenter: false
});
return _this;
}
DayTile.prototype.render = function (props) {
this.renderFrame(props.date);
this.renderFgEvents(props.fgSegs);
this.renderEventSelection(props.eventSelection);
this.renderEventDrag(props.eventDragInstances);
this.renderEventResize(props.eventResizeInstances);
};
DayTile.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderFrame.unrender(); // should unrender everything else
this.calendar.unregisterInteractiveComponent(this);
};
DayTile.prototype._renderFrame = function (date) {
var _a = this, theme = _a.theme, dateEnv = _a.dateEnv;
var title = dateEnv.format(date, core.createFormatter(this.opt('dayPopoverFormat')) // TODO: cache
);
this.el.innerHTML =
'
var DayBgRow = /** @class */ (function () {
function DayBgRow(context) {
this.context = context;
}
DayBgRow.prototype.renderHtml = function (props) {
var parts = [];
if (props.renderIntroHtml) {
parts.push(props.renderIntroHtml());
}
for (var _i = 0, _a = props.cells; _i < _a.length; _i++) {
var cell = _a[_i];
parts.push(renderCellHtml(cell.date, props.dateProfile, this.context, cell.htmlAttrs));
}
if (!props.cells.length) {
parts.push('');
}
if (this.context.options.dir === 'rtl') {
parts.reverse();
}
return '' + parts.join('') + '';
};
return DayBgRow;
}());
function renderCellHtml(date, dateProfile, context, otherAttrs) {
var dateEnv = context.dateEnv, theme = context.theme;
var isDateValid = core.rangeContainsMarker(dateProfile.activeRange, date); // TODO: called too frequently. cache somehow.
var classes = core.getDayClasses(date, dateProfile, context);
classes.unshift('fc-day', theme.getClass('widgetContent'));
return '';
}
var DAY_NUM_FORMAT = core.createFormatter({ day: 'numeric' });
var WEEK_NUM_FORMAT = core.createFormatter({ week: 'numeric' });
var DayGrid = /** @class */ (function (_super) {
__extends(DayGrid, _super);
function DayGrid(context, el, renderProps) {
var _this = _super.call(this, context, el) || this;
_this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid
_this.isCellSizesDirty = false;
var eventRenderer = _this.eventRenderer = new DayGridEventRenderer(_this);
var fillRenderer = _this.fillRenderer = new DayGridFillRenderer(_this);
_this.mirrorRenderer = new DayGridMirrorRenderer(_this);
var renderCells = _this.renderCells = core.memoizeRendering(_this._renderCells, _this._unrenderCells);
_this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderCells]);
_this.renderDateSelection = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'), [renderCells]);
_this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderCells]);
_this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderCells]);
_this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderCells]);
_this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderCells]);
_this.renderProps = renderProps;
return _this;
}
DayGrid.prototype.render = function (props) {
var cells = props.cells;
this.rowCnt = cells.length;
this.colCnt = cells[0].length;
this.renderCells(cells, props.isRigid);
this.renderBusinessHours(props.businessHourSegs);
this.renderDateSelection(props.dateSelectionSegs);
this.renderBgEvents(props.bgEventSegs);
this.renderFgEvents(props.fgEventSegs);
this.renderEventSelection(props.eventSelection);
this.renderEventDrag(props.eventDrag);
this.renderEventResize(props.eventResize);
if (this.segPopoverTile) {
this.updateSegPopoverTile();
}
};
DayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderCells.unrender(); // will unrender everything else
};
DayGrid.prototype.getCellRange = function (row, col) {
var start = this.props.cells[row][col].date;
var end = core.addDays(start, 1);
return { start: start, end: end };
};
DayGrid.prototype.updateSegPopoverTile = function (date, segs) {
var ownProps = this.props;
this.segPopoverTile.receiveProps({
date: date || this.segPopoverTile.props.date,
fgSegs: segs || this.segPopoverTile.props.fgSegs,
eventSelection: ownProps.eventSelection,
eventDragInstances: ownProps.eventDrag ? ownProps.eventDrag.affectedInstances : null,
eventResizeInstances: ownProps.eventResize ? ownProps.eventResize.affectedInstances : null
});
};
/* Date Rendering
------------------------------------------------------------------------------------------------------------------*/
DayGrid.prototype._renderCells = function (cells, isRigid) {
var _a = this, view = _a.view, dateEnv = _a.dateEnv;
var _b = this, rowCnt = _b.rowCnt, colCnt = _b.colCnt;
var html = '';
var row;
var col;
for (row = 0; row < rowCnt; row++) {
html += this.renderDayRowHtml(row, isRigid);
}
this.el.innerHTML = html;
this.rowEls = core.findElements(this.el, '.fc-row');
this.cellEls = core.findElements(this.el, '.fc-day, .fc-disabled-day');
if (this.isRtl) {
this.cellEls.reverse();
}
this.rowPositions = new core.PositionCache(this.el, this.rowEls, false, true // vertical
);
this.colPositions = new core.PositionCache(this.el, this.cellEls.slice(0, colCnt), // only the first row
true, false // horizontal
);
// trigger dayRender with each cell's element
for (row = 0; row < rowCnt; row++) {
for (col = 0; col < colCnt; col++) {
this.publiclyTrigger('dayRender', [
{
date: dateEnv.toDate(cells[row][col].date),
el: this.getCellEl(row, col),
view: view
}
]);
}
}
this.isCellSizesDirty = true;
};
DayGrid.prototype._unrenderCells = function () {
this.removeSegPopover();
};
// Generates the HTML for a single row, which is a div that wraps a table.
// `row` is the row number.
DayGrid.prototype.renderDayRowHtml = function (row, isRigid) {
var theme = this.theme;
var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')];
if (isRigid) {
classes.push('fc-rigid');
}
var bgRow = new DayBgRow(this.context);
return '' +
'
var WEEK_NUM_FORMAT$1 = core.createFormatter({ week: 'numeric' });
/* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells.
----------------------------------------------------------------------------------------------------------------------*/
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
// It is responsible for managing width/height.
var DayGridView = /** @class */ (function (_super) {
__extends(DayGridView, _super);
function DayGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
/* Header Rendering
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before the day-of week header cells
_this.renderHeadIntroHtml = function () {
var theme = _this.theme;
if (_this.colWeekNumbersVisible) {
return '' +
'' +
'' + // needed for matchCellWidths
core.htmlEscape(_this.opt('weekLabel')) +
'' +
'';
}
return '';
};
/* Day Grid Rendering
------------------------------------------------------------------------------------------------------------------*/
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers
_this.renderDayGridNumberIntroHtml = function (row, dayGrid) {
var dateEnv = _this.dateEnv;
var weekStart = dayGrid.props.cells[row][0].date;
if (_this.colWeekNumbersVisible) {
return '' +
'' +
core.buildGotoAnchorHtml(// aside from link, important for matchCellWidths
_this, { date: weekStart, type: 'week', forceOff: dayGrid.colCnt === 1 }, dateEnv.format(weekStart, WEEK_NUM_FORMAT$1) // inner HTML
) +
'';
}
return '';
};
// Generates the HTML that goes before the day bg cells for each day-row
_this.renderDayGridBgIntroHtml = function () {
var theme = _this.theme;
if (_this.colWeekNumbersVisible) {
return '';
}
return '';
};
// Generates the HTML that goes before every other type of row generated by DayGrid.
// Affects mirror-skeleton and highlight-skeleton rows.
_this.renderDayGridIntroHtml = function () {
if (_this.colWeekNumbersVisible) {
return '';
}
return '';
};
_this.el.classList.add('fc-dayGrid-view');
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.scroller = new core.ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
var dayGridContainerEl = _this.scroller.el;
_this.el.querySelector('.fc-body > tr > td').appendChild(dayGridContainerEl);
dayGridContainerEl.classList.add('fc-day-grid-container');
var dayGridEl = core.createElement('div', { className: 'fc-day-grid' });
dayGridContainerEl.appendChild(dayGridEl);
var cellWeekNumbersVisible;
if (_this.opt('weekNumbers')) {
if (_this.opt('weekNumbersWithinDays')) {
cellWeekNumbersVisible = true;
_this.colWeekNumbersVisible = false;
}
else {
cellWeekNumbersVisible = false;
_this.colWeekNumbersVisible = true;
}
}
else {
_this.colWeekNumbersVisible = false;
cellWeekNumbersVisible = false;
}
_this.dayGrid = new DayGrid(_this.context, dayGridEl, {
renderNumberIntroHtml: _this.renderDayGridNumberIntroHtml,
renderBgIntroHtml: _this.renderDayGridBgIntroHtml,
renderIntroHtml: _this.renderDayGridIntroHtml,
colWeekNumbersVisible: _this.colWeekNumbersVisible,
cellWeekNumbersVisible: cellWeekNumbersVisible
});
return _this;
}
DayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.dayGrid.destroy();
this.scroller.destroy();
};
// Builds the HTML skeleton for the view.
// The day-grid component will render inside of a container defined by this HTML.
DayGridView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return '' +
'' +
(this.opt('columnHeader') ?
'' +
'' +
' ' +
'' +
'' :
'') +
'' +
'' +
'' +
'' +
'' +
'';
};
// Generates an HTML attribute string for setting the width of the week number column, if it is known
DayGridView.prototype.weekNumberStyleAttr = function () {
if (this.weekNumberWidth != null) {
return 'style="width:' + this.weekNumberWidth + 'px"';
}
return '';
};
// Determines whether each row should have a constant height
DayGridView.prototype.hasRigidRows = function () {
var eventLimit = this.opt('eventLimit');
return eventLimit && typeof eventLimit !== 'number';
};
/* Dimensions
------------------------------------------------------------------------------------------------------------------*/
DayGridView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
_super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); // will call updateBaseSize. important that executes first
this.dayGrid.updateSize(isResize);
};
// Refreshes the horizontal dimensions of the view
DayGridView.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) {
var dayGrid = this.dayGrid;
var eventLimit = this.opt('eventLimit');
var headRowEl = this.header ? this.header.el : null; // HACK
var scrollerHeight;
var scrollbarWidths;
// hack to give the view some height prior to dayGrid's columns being rendered
// TODO: separate setting height from scroller VS dayGrid.
if (!dayGrid.rowEls) {
if (!isAuto) {
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.scroller.setHeight(scrollerHeight);
}
return;
}
if (this.colWeekNumbersVisible) {
// Make sure all week number cells running down the side have the same width.
this.weekNumberWidth = core.matchCellWidths(core.findElements(this.el, '.fc-week-number'));
}
// reset all heights to be natural
this.scroller.clear();
if (headRowEl) {
core.uncompensateScroll(headRowEl);
}
dayGrid.removeSegPopover(); // kill the "more" popover if displayed
// is the event limit a constant level number?
if (eventLimit && typeof eventLimit === 'number') {
dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
}
// distribute the height to the rows
// (viewHeight is a "recommended" value if isAuto)
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.setGridHeight(scrollerHeight, isAuto);
// is the event limit dynamically calculated?
if (eventLimit && typeof eventLimit !== 'number') {
dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
}
if (!isAuto) { // should we force dimensions of the scroll container?
this.scroller.setHeight(scrollerHeight);
scrollbarWidths = this.scroller.getScrollbarWidths();
if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
if (headRowEl) {
core.compensateScroll(headRowEl, scrollbarWidths);
}
// doing the scrollbar compensation might have created text overflow which created more height. redo
scrollerHeight = this.computeScrollerHeight(viewHeight);
this.scroller.setHeight(scrollerHeight);
}
// guarantees the same scrollbar widths
this.scroller.lockOverflow(scrollbarWidths);
}
};
// given a desired total height of the view, returns what the height of the scroller should be
DayGridView.prototype.computeScrollerHeight = function (viewHeight) {
return viewHeight -
core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
};
// Sets the height of just the DayGrid component in this view
DayGridView.prototype.setGridHeight = function (height, isAuto) {
if (this.opt('monthMode')) {
// if auto, make the height of each row the height that it would be if there were 6 weeks
if (isAuto) {
height *= this.dayGrid.rowCnt / 6;
}
core.distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
}
else {
if (isAuto) {
core.undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
}
else {
core.distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
}
}
};
/* Scroll
------------------------------------------------------------------------------------------------------------------*/
DayGridView.prototype.computeDateScroll = function (duration) {
return { top: 0 };
};
DayGridView.prototype.queryDateScroll = function () {
return { top: this.scroller.getScrollTop() };
};
DayGridView.prototype.applyDateScroll = function (scroll) {
if (scroll.top !== undefined) {
this.scroller.setScrollTop(scroll.top);
}
};
return DayGridView;
}(core.View));
DayGridView.prototype.dateProfileGeneratorClass = DayGridDateProfileGenerator;
var SimpleDayGrid = /** @class */ (function (_super) {
__extends(SimpleDayGrid, _super);
function SimpleDayGrid(context, dayGrid) {
var _this = _super.call(this, context, dayGrid.el) || this;
_this.slicer = new DayGridSlicer();
_this.dayGrid = dayGrid;
context.calendar.registerInteractiveComponent(_this, { el: _this.dayGrid.el });
return _this;
}
SimpleDayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
SimpleDayGrid.prototype.render = function (props) {
var dayGrid = this.dayGrid;
var dateProfile = props.dateProfile, dayTable = props.dayTable;
dayGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, dayGrid, dayTable), { dateProfile: dateProfile, cells: dayTable.cells, isRigid: props.isRigid }));
};
SimpleDayGrid.prototype.buildPositionCaches = function () {
this.dayGrid.buildPositionCaches();
};
SimpleDayGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.dayGrid,
dateSpan: rawHit.dateSpan,
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return SimpleDayGrid;
}(core.DateComponent));
var DayGridSlicer = /** @class */ (function (_super) {
__extends(DayGridSlicer, _super);
function DayGridSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayGridSlicer.prototype.sliceRange = function (dateRange, dayTable) {
return dayTable.sliceRange(dateRange);
};
return DayGridSlicer;
}(core.Slicer));
var DayGridView$1 = /** @class */ (function (_super) {
__extends(DayGridView, _super);
function DayGridView(_context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, _context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.buildDayTable = core.memoize(buildDayTable);
if (_this.opt('columnHeader')) {
_this.header = new core.DayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.simpleDayGrid = new SimpleDayGrid(_this.context, _this.dayGrid);
return _this;
}
DayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.simpleDayGrid.destroy();
};
DayGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props);
var dateProfile = this.props.dateProfile;
var dayTable = this.dayTable =
this.buildDayTable(dateProfile, this.dateProfileGenerator);
if (this.header) {
this.header.receiveProps({
dateProfile: dateProfile,
dates: dayTable.headerDates,
datesRepDistinctDays: dayTable.rowCnt === 1,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.simpleDayGrid.receiveProps({
dateProfile: dateProfile,
dayTable: dayTable,
businessHours: props.businessHours,
dateSelection: props.dateSelection,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
isRigid: this.hasRigidRows(),
nextDayThreshold: this.nextDayThreshold
});
};
return DayGridView;
}(DayGridView));
function buildDayTable(dateProfile, dateProfileGenerator) {
var daySeries = new core.DaySeries(dateProfile.renderRange, dateProfileGenerator);
return new core.DayTable(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit));
}
var main = core.createPlugin({
defaultView: 'dayGridMonth',
views: {
dayGrid: DayGridView$1,
dayGridDay: {
type: 'dayGrid',
duration: { days: 1 }
},
dayGridWeek: {
type: 'dayGrid',
duration: { weeks: 1 }
},
dayGridMonth: {
type: 'dayGrid',
duration: { months: 1 },
monthMode: true,
fixedWeekCount: true
}
}
});
exports.AbstractDayGridView = DayGridView;
exports.DayBgRow = DayBgRow;
exports.DayGrid = DayGrid;
exports.DayGridSlicer = DayGridSlicer;
exports.DayGridView = DayGridView$1;
exports.SimpleDayGrid = SimpleDayGrid;
exports.buildBasicDayTable = buildDayTable;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
/*!
FullCalendar Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):t((e=e||self).FullCalendarDayGrid={},e.FullCalendar)}(this,function(e,t){"use strict";var r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)};function n(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var i=function(){return(i=Object.assign||function(e){for(var t,r=1,n=arguments.length;r'+t.htmlEscape(n)+""),i=''+(t.htmlEscape(d.title||"")||" ")+"",'
/*!
FullCalendar Luxon Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { DateTime, Duration } from 'luxon';
import { createPlugin, Calendar, NamedTimeZoneImpl } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function toDateTime(date, calendar) {
if (!(calendar instanceof Calendar)) {
throw new Error('must supply a Calendar instance');
}
return DateTime.fromJSDate(date, {
zone: calendar.dateEnv.timeZone,
locale: calendar.dateEnv.locale.codes[0]
});
}
function toDuration(duration, calendar) {
if (!(calendar instanceof Calendar)) {
throw new Error('must supply a Calendar instance');
}
return Duration.fromObject(__assign({}, duration, { locale: calendar.dateEnv.locale.codes[0] }));
}
var LuxonNamedTimeZone = /** @class */ (function (_super) {
__extends(LuxonNamedTimeZone, _super);
function LuxonNamedTimeZone() {
return _super !== null && _super.apply(this, arguments) || this;
}
LuxonNamedTimeZone.prototype.offsetForArray = function (a) {
return arrayToLuxon(a, this.timeZoneName).offset;
};
LuxonNamedTimeZone.prototype.timestampToArray = function (ms) {
return luxonToArray(DateTime.fromMillis(ms, {
zone: this.timeZoneName
}));
};
return LuxonNamedTimeZone;
}(NamedTimeZoneImpl));
function formatWithCmdStr(cmdStr, arg) {
var cmd = parseCmdStr(cmdStr);
if (arg.end) {
var start = arrayToLuxon(arg.start.array, arg.timeZone, arg.localeCodes[0]);
var end = arrayToLuxon(arg.end.array, arg.timeZone, arg.localeCodes[0]);
return formatRange(cmd, start.toFormat.bind(start), end.toFormat.bind(end), arg.separator);
}
return arrayToLuxon(arg.date.array, arg.timeZone, arg.localeCodes[0]).toFormat(cmd.whole);
}
var main = createPlugin({
cmdFormatter: formatWithCmdStr,
namedTimeZonedImpl: LuxonNamedTimeZone
});
function luxonToArray(datetime) {
return [
datetime.year,
datetime.month - 1,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
datetime.millisecond
];
}
function arrayToLuxon(arr, timeZone, locale) {
return DateTime.fromObject({
zone: timeZone,
locale: locale,
year: arr[0],
month: arr[1] + 1,
day: arr[2],
hour: arr[3],
minute: arr[4],
second: arr[5],
millisecond: arr[6]
});
}
function parseCmdStr(cmdStr) {
var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters
if (parts) {
var middle = parseCmdStr(parts[2]);
return {
head: parts[1],
middle: middle,
tail: parts[3],
whole: parts[1] + middle.whole + parts[3]
};
}
else {
return {
head: null,
middle: null,
tail: null,
whole: cmdStr
};
}
}
function formatRange(cmd, formatStart, formatEnd, separator) {
if (cmd.middle) {
var startHead = formatStart(cmd.head);
var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var startTail = formatStart(cmd.tail);
var endHead = formatEnd(cmd.head);
var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var endTail = formatEnd(cmd.tail);
if (startHead === endHead && startTail === endTail) {
return startHead +
(startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) +
startTail;
}
}
var startWhole = formatStart(cmd.whole);
var endWhole = formatEnd(cmd.whole);
if (startWhole === endWhole) {
return startWhole;
}
else {
return startWhole + separator + endWhole;
}
}
export default main;
export { toDateTime, toDuration };
/*!
FullCalendar Luxon Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('luxon'), require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', 'luxon', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarLuxon = {}, global.luxon, global.FullCalendar));
}(this, function (exports, luxon, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function toDateTime(date, calendar) {
if (!(calendar instanceof core.Calendar)) {
throw new Error('must supply a Calendar instance');
}
return luxon.DateTime.fromJSDate(date, {
zone: calendar.dateEnv.timeZone,
locale: calendar.dateEnv.locale.codes[0]
});
}
function toDuration(duration, calendar) {
if (!(calendar instanceof core.Calendar)) {
throw new Error('must supply a Calendar instance');
}
return luxon.Duration.fromObject(__assign({}, duration, { locale: calendar.dateEnv.locale.codes[0] }));
}
var LuxonNamedTimeZone = /** @class */ (function (_super) {
__extends(LuxonNamedTimeZone, _super);
function LuxonNamedTimeZone() {
return _super !== null && _super.apply(this, arguments) || this;
}
LuxonNamedTimeZone.prototype.offsetForArray = function (a) {
return arrayToLuxon(a, this.timeZoneName).offset;
};
LuxonNamedTimeZone.prototype.timestampToArray = function (ms) {
return luxonToArray(luxon.DateTime.fromMillis(ms, {
zone: this.timeZoneName
}));
};
return LuxonNamedTimeZone;
}(core.NamedTimeZoneImpl));
function formatWithCmdStr(cmdStr, arg) {
var cmd = parseCmdStr(cmdStr);
if (arg.end) {
var start = arrayToLuxon(arg.start.array, arg.timeZone, arg.localeCodes[0]);
var end = arrayToLuxon(arg.end.array, arg.timeZone, arg.localeCodes[0]);
return formatRange(cmd, start.toFormat.bind(start), end.toFormat.bind(end), arg.separator);
}
return arrayToLuxon(arg.date.array, arg.timeZone, arg.localeCodes[0]).toFormat(cmd.whole);
}
var main = core.createPlugin({
cmdFormatter: formatWithCmdStr,
namedTimeZonedImpl: LuxonNamedTimeZone
});
function luxonToArray(datetime) {
return [
datetime.year,
datetime.month - 1,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second,
datetime.millisecond
];
}
function arrayToLuxon(arr, timeZone, locale) {
return luxon.DateTime.fromObject({
zone: timeZone,
locale: locale,
year: arr[0],
month: arr[1] + 1,
day: arr[2],
hour: arr[3],
minute: arr[4],
second: arr[5],
millisecond: arr[6]
});
}
function parseCmdStr(cmdStr) {
var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters
if (parts) {
var middle = parseCmdStr(parts[2]);
return {
head: parts[1],
middle: middle,
tail: parts[3],
whole: parts[1] + middle.whole + parts[3]
};
}
else {
return {
head: null,
middle: null,
tail: null,
whole: cmdStr
};
}
}
function formatRange(cmd, formatStart, formatEnd, separator) {
if (cmd.middle) {
var startHead = formatStart(cmd.head);
var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var startTail = formatStart(cmd.tail);
var endHead = formatEnd(cmd.head);
var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator);
var endTail = formatEnd(cmd.tail);
if (startHead === endHead && startTail === endTail) {
return startHead +
(startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) +
startTail;
}
}
var startWhole = formatStart(cmd.whole);
var endWhole = formatEnd(cmd.whole);
if (startWhole === endWhole) {
return startWhole;
}
else {
return startWhole + separator + endWhole;
}
}
exports.default = main;
exports.toDateTime = toDateTime;
exports.toDuration = toDuration;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../luxon
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/luxon' {
import { DateTime as LuxonDateTime, Duration as LuxonDuration } from 'luxon';
import { Calendar, Duration } from '@fullcalendar/core';
export function toDateTime(date: Date, calendar: Calendar): LuxonDateTime;
export function toDuration(duration: Duration, calendar: Calendar): LuxonDuration;
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar Luxon Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("luxon"),require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","luxon","@fullcalendar/core"],t):t((e=e||self).FullCalendarLuxon={},e.luxon,e.FullCalendar)}(this,function(e,t,n){"use strict";var o=function(e,t){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)};var r=function(){return(r=Object.assign||function(e){for(var t,n=1,o=arguments.length;n
/*!
FullCalendar List View Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { getAllDayHtml, isMultiDayRange, htmlEscape, FgEventRenderer, memoize, memoizeRendering, ScrollComponent, subtractInnerElHeight, sliceEventStore, intersectRanges, htmlToElement, createFormatter, createElement, buildGotoAnchorHtml, View, startOfDay, addDays, createPlugin } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var ListEventRenderer = /** @class */ (function (_super) {
__extends(ListEventRenderer, _super);
function ListEventRenderer(listView) {
var _this = _super.call(this, listView.context) || this;
_this.listView = listView;
return _this;
}
ListEventRenderer.prototype.attachSegs = function (segs) {
if (!segs.length) {
this.listView.renderEmptyMessage();
}
else {
this.listView.renderSegList(segs);
}
};
ListEventRenderer.prototype.detachSegs = function () {
};
// generates the HTML for a single event row
ListEventRenderer.prototype.renderSegHtml = function (seg) {
var _a = this.context, view = _a.view, theme = _a.theme;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventInstance = eventRange.instance;
var eventUi = eventRange.ui;
var url = eventDef.url;
var classes = ['fc-list-item'].concat(eventUi.classNames);
var bgColor = eventUi.backgroundColor;
var timeHtml;
if (eventDef.allDay) {
timeHtml = getAllDayHtml(view);
}
else if (isMultiDayRange(eventRange.range)) {
if (seg.isStart) {
timeHtml = htmlEscape(this._getTimeText(eventInstance.range.start, seg.end, false // allDay
));
}
else if (seg.isEnd) {
timeHtml = htmlEscape(this._getTimeText(seg.start, eventInstance.range.end, false // allDay
));
}
else { // inner segment that lasts the whole day
timeHtml = getAllDayHtml(view);
}
}
else {
// Display the normal time text for the *event's* times
timeHtml = htmlEscape(this.getTimeText(eventRange));
}
if (url) {
classes.push('fc-has-url');
}
return '' +
(this.displayEventTime ?
'' +
(timeHtml || '') +
'' :
'') +
'' +
'' +
'' +
'' +
'' +
htmlEscape(eventDef.title || '') +
'' +
'' +
'';
};
// like "4:00am"
ListEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short'
};
};
return ListEventRenderer;
}(FgEventRenderer));
/*
Responsible for the scroller, and forwarding event-related actions into the "grid".
*/
var ListView = /** @class */ (function (_super) {
__extends(ListView, _super);
function ListView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.computeDateVars = memoize(computeDateVars);
_this.eventStoreToSegs = memoize(_this._eventStoreToSegs);
var eventRenderer = _this.eventRenderer = new ListEventRenderer(_this);
_this.renderContent = memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer));
_this.el.classList.add('fc-list-view');
var listViewClassNames = (_this.theme.getClass('listView') || '').split(' '); // wish we didn't have to do this
for (var _i = 0, listViewClassNames_1 = listViewClassNames; _i < listViewClassNames_1.length; _i++) {
var listViewClassName = listViewClassNames_1[_i];
if (listViewClassName) { // in case input was empty string
_this.el.classList.add(listViewClassName);
}
}
_this.scroller = new ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
_this.el.appendChild(_this.scroller.el);
_this.contentEl = _this.scroller.el; // shortcut
context.calendar.registerInteractiveComponent(_this, {
el: _this.el
// TODO: make aware that it doesn't do Hits
});
return _this;
}
ListView.prototype.render = function (props) {
var _a = this.computeDateVars(props.dateProfile), dayDates = _a.dayDates, dayRanges = _a.dayRanges;
this.dayDates = dayDates;
this.renderContent(this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges));
};
ListView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderContent.unrender();
this.scroller.destroy(); // will remove the Grid too
this.calendar.unregisterInteractiveComponent(this);
};
ListView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
_super.prototype.updateSize.call(this, isResize, viewHeight, isAuto);
this.eventRenderer.computeSizes(isResize);
this.eventRenderer.assignSizes(isResize);
this.scroller.clear(); // sets height to 'auto' and clears overflow
if (!isAuto) {
this.scroller.setHeight(this.computeScrollerHeight(viewHeight));
}
};
ListView.prototype.computeScrollerHeight = function (viewHeight) {
return viewHeight -
subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
};
ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) {
return this.eventRangesToSegs(sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.nextDayThreshold).fg, dayRanges);
};
ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) {
var segs = [];
for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) {
var eventRange = eventRanges_1[_i];
segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges));
}
return segs;
};
ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) {
var _a = this, dateEnv = _a.dateEnv, nextDayThreshold = _a.nextDayThreshold;
var range = eventRange.range;
var allDay = eventRange.def.allDay;
var dayIndex;
var segRange;
var seg;
var segs = [];
for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) {
segRange = intersectRanges(range, dayRanges[dayIndex]);
if (segRange) {
seg = {
component: this,
eventRange: eventRange,
start: segRange.start,
end: segRange.end,
isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(),
isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(),
dayIndex: dayIndex
};
segs.push(seg);
// detect when range won't go fully into the next day,
// and mutate the latest seg to the be the end.
if (!seg.isEnd && !allDay &&
dayIndex + 1 < dayRanges.length &&
range.end <
dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) {
seg.end = range.end;
seg.isEnd = true;
break;
}
}
}
return segs;
};
ListView.prototype.renderEmptyMessage = function () {
this.contentEl.innerHTML =
'
var main = createPlugin({
views: {
list: {
class: ListView,
buttonTextKey: 'list',
listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016"
},
listDay: {
type: 'list',
duration: { days: 1 },
listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header
},
listWeek: {
type: 'list',
duration: { weeks: 1 },
listDayFormat: { weekday: 'long' },
listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' }
},
listMonth: {
type: 'list',
duration: { month: 1 },
listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
},
listYear: {
type: 'list',
duration: { year: 1 },
listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
}
}
});
export default main;
export { ListView };
/*!
FullCalendar List View Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarList = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var ListEventRenderer = /** @class */ (function (_super) {
__extends(ListEventRenderer, _super);
function ListEventRenderer(listView) {
var _this = _super.call(this, listView.context) || this;
_this.listView = listView;
return _this;
}
ListEventRenderer.prototype.attachSegs = function (segs) {
if (!segs.length) {
this.listView.renderEmptyMessage();
}
else {
this.listView.renderSegList(segs);
}
};
ListEventRenderer.prototype.detachSegs = function () {
};
// generates the HTML for a single event row
ListEventRenderer.prototype.renderSegHtml = function (seg) {
var _a = this.context, view = _a.view, theme = _a.theme;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventInstance = eventRange.instance;
var eventUi = eventRange.ui;
var url = eventDef.url;
var classes = ['fc-list-item'].concat(eventUi.classNames);
var bgColor = eventUi.backgroundColor;
var timeHtml;
if (eventDef.allDay) {
timeHtml = core.getAllDayHtml(view);
}
else if (core.isMultiDayRange(eventRange.range)) {
if (seg.isStart) {
timeHtml = core.htmlEscape(this._getTimeText(eventInstance.range.start, seg.end, false // allDay
));
}
else if (seg.isEnd) {
timeHtml = core.htmlEscape(this._getTimeText(seg.start, eventInstance.range.end, false // allDay
));
}
else { // inner segment that lasts the whole day
timeHtml = core.getAllDayHtml(view);
}
}
else {
// Display the normal time text for the *event's* times
timeHtml = core.htmlEscape(this.getTimeText(eventRange));
}
if (url) {
classes.push('fc-has-url');
}
return '' +
(this.displayEventTime ?
'' +
(timeHtml || '') +
'' :
'') +
'' +
'' +
'' +
'' +
'' +
core.htmlEscape(eventDef.title || '') +
'' +
'' +
'';
};
// like "4:00am"
ListEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short'
};
};
return ListEventRenderer;
}(core.FgEventRenderer));
/*
Responsible for the scroller, and forwarding event-related actions into the "grid".
*/
var ListView = /** @class */ (function (_super) {
__extends(ListView, _super);
function ListView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.computeDateVars = core.memoize(computeDateVars);
_this.eventStoreToSegs = core.memoize(_this._eventStoreToSegs);
var eventRenderer = _this.eventRenderer = new ListEventRenderer(_this);
_this.renderContent = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer));
_this.el.classList.add('fc-list-view');
var listViewClassNames = (_this.theme.getClass('listView') || '').split(' '); // wish we didn't have to do this
for (var _i = 0, listViewClassNames_1 = listViewClassNames; _i < listViewClassNames_1.length; _i++) {
var listViewClassName = listViewClassNames_1[_i];
if (listViewClassName) { // in case input was empty string
_this.el.classList.add(listViewClassName);
}
}
_this.scroller = new core.ScrollComponent('hidden', // overflow x
'auto' // overflow y
);
_this.el.appendChild(_this.scroller.el);
_this.contentEl = _this.scroller.el; // shortcut
context.calendar.registerInteractiveComponent(_this, {
el: _this.el
// TODO: make aware that it doesn't do Hits
});
return _this;
}
ListView.prototype.render = function (props) {
var _a = this.computeDateVars(props.dateProfile), dayDates = _a.dayDates, dayRanges = _a.dayRanges;
this.dayDates = dayDates;
this.renderContent(this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges));
};
ListView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderContent.unrender();
this.scroller.destroy(); // will remove the Grid too
this.calendar.unregisterInteractiveComponent(this);
};
ListView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
_super.prototype.updateSize.call(this, isResize, viewHeight, isAuto);
this.eventRenderer.computeSizes(isResize);
this.eventRenderer.assignSizes(isResize);
this.scroller.clear(); // sets height to 'auto' and clears overflow
if (!isAuto) {
this.scroller.setHeight(this.computeScrollerHeight(viewHeight));
}
};
ListView.prototype.computeScrollerHeight = function (viewHeight) {
return viewHeight -
core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
};
ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) {
return this.eventRangesToSegs(core.sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.nextDayThreshold).fg, dayRanges);
};
ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) {
var segs = [];
for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) {
var eventRange = eventRanges_1[_i];
segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges));
}
return segs;
};
ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) {
var _a = this, dateEnv = _a.dateEnv, nextDayThreshold = _a.nextDayThreshold;
var range = eventRange.range;
var allDay = eventRange.def.allDay;
var dayIndex;
var segRange;
var seg;
var segs = [];
for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) {
segRange = core.intersectRanges(range, dayRanges[dayIndex]);
if (segRange) {
seg = {
component: this,
eventRange: eventRange,
start: segRange.start,
end: segRange.end,
isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(),
isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(),
dayIndex: dayIndex
};
segs.push(seg);
// detect when range won't go fully into the next day,
// and mutate the latest seg to the be the end.
if (!seg.isEnd && !allDay &&
dayIndex + 1 < dayRanges.length &&
range.end <
dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) {
seg.end = range.end;
seg.isEnd = true;
break;
}
}
}
return segs;
};
ListView.prototype.renderEmptyMessage = function () {
this.contentEl.innerHTML =
'
var main = core.createPlugin({
views: {
list: {
class: ListView,
buttonTextKey: 'list',
listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016"
},
listDay: {
type: 'list',
duration: { days: 1 },
listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header
},
listWeek: {
type: 'list',
duration: { weeks: 1 },
listDayFormat: { weekday: 'long' },
listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' }
},
listMonth: {
type: 'list',
duration: { month: 1 },
listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
},
listYear: {
type: 'list',
duration: { year: 1 },
listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have
}
}
});
exports.ListView = ListView;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/list' {
import ListView from '@fullcalendar/list/ListView';
export { ListView };
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/list/ListView' {
import { View, ViewProps, ScrollComponent, DateMarker, DateRange, DateProfileGenerator, ComponentContext, ViewSpec, EventUiHash, EventRenderRange, EventStore, Seg } from '@fullcalendar/core';
export { ListView as default, ListView };
class ListView extends View {
scroller: ScrollComponent;
contentEl: HTMLElement;
dayDates: DateMarker[];
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
render(props: ViewProps): void;
destroy(): void;
updateSize(isResize: any, viewHeight: any, isAuto: any): void;
computeScrollerHeight(viewHeight: any): number;
_eventStoreToSegs(eventStore: EventStore, eventUiBases: EventUiHash, dayRanges: DateRange[]): Seg[];
eventRangesToSegs(eventRanges: EventRenderRange[], dayRanges: DateRange[]): any[];
eventRangeToSegs(eventRange: EventRenderRange, dayRanges: DateRange[]): any[];
renderEmptyMessage(): void;
renderSegList(allSegs: any): void;
groupSegsByDay(segs: any): any[];
buildDayHeaderRow(dayDate: any): HTMLTableRowElement;
}
}
/*!
FullCalendar List View Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):t((e=e||self).FullCalendarList={},e.FullCalendar)}(this,function(e,t){"use strict";var n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)};function r(e,t){function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}var s=function(e){function n(t){var n=e.call(this,t.context)||this;return n.listView=t,n}return r(n,e),n.prototype.attachSegs=function(e){e.length?this.listView.renderSegList(e):this.listView.renderEmptyMessage()},n.prototype.detachSegs=function(){},n.prototype.renderSegHtml=function(e){var n,r=this.context,s=r.view,a=r.theme,i=e.eventRange,o=i.def,l=i.instance,d=i.ui,c=o.url,p=["fc-list-item"].concat(d.classNames),h=d.backgroundColor;return n=o.allDay?t.getAllDayHtml(s):t.isMultiDayRange(i.range)?e.isStart?t.htmlEscape(this._getTimeText(l.range.start,e.end,!1)):e.isEnd?t.htmlEscape(this._getTimeText(e.start,l.range.end,!1)):t.getAllDayHtml(s):t.htmlEscape(this.getTimeText(i)),c&&p.push("fc-has-url"),''+(this.displayEventTime?''+(n||"")+"":"")+'"+t.htmlEscape(o.title||"")+""},n.prototype.computeEventTimeFormat=function(){return{hour:"numeric",minute:"2-digit",meridiem:"short"}},n}(t.FgEventRenderer),a=function(e){function n(n,r,a,o){var l=e.call(this,n,r,a,o)||this;l.computeDateVars=t.memoize(i),l.eventStoreToSegs=t.memoize(l._eventStoreToSegs);var d=l.eventRenderer=new s(l);l.renderContent=t.memoizeRendering(d.renderSegs.bind(d),d.unrender.bind(d)),l.el.classList.add("fc-list-view");for(var c=0,p=(l.theme.getClass("listView")||"").split(" ");c
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.FullCalendarLocalesAll = factory());
}(this, function () { 'use strict';
var _m0 = {
code: "af",
week: {
dow: 1,
doy: 4 // Die week wat die 4de Januarie bevat is die eerste week van die jaar.
},
buttonText: {
prev: "Vorige",
next: "Volgende",
today: "Vandag",
year: "Jaar",
month: "Maand",
week: "Week",
day: "Dag",
list: "Agenda"
},
allDayHtml: "Heeldag",
eventLimitText: "Addisionele",
noEventsMessage: "Daar is geen gebeurtenisse nie"
};
var _m1 = {
code: "ar-dz",
week: {
dow: 0,
doy: 4 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m2 = {
code: "ar-kw",
week: {
dow: 0,
doy: 12 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m3 = {
code: "ar-ly",
week: {
dow: 6,
doy: 12 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m4 = {
code: "ar-ma",
week: {
dow: 6,
doy: 12 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m5 = {
code: "ar-sa",
week: {
dow: 0,
doy: 6 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m6 = {
code: "ar-tn",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m7 = {
code: "ar",
week: {
dow: 6,
doy: 12 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "السابق",
next: "التالي",
today: "اليوم",
month: "شهر",
week: "أسبوع",
day: "يوم",
list: "أجندة"
},
weekLabel: "أسبوع",
allDayText: "اليوم كله",
eventLimitText: "أخرى",
noEventsMessage: "أي أحداث لعرض"
};
var _m8 = {
code: "bg",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "назад",
next: "напред",
today: "днес",
month: "Месец",
week: "Седмица",
day: "Ден",
list: "График"
},
allDayText: "Цял ден",
eventLimitText: function (n) {
return "+още " + n;
},
noEventsMessage: "Няма събития за показване"
};
var _m9 = {
code: "bs",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Prošli",
next: "Sljedeći",
today: "Danas",
month: "Mjesec",
week: "Sedmica",
day: "Dan",
list: "Raspored"
},
weekLabel: "Sed",
allDayText: "Cijeli dan",
eventLimitText: function (n) {
return "+ još " + n;
},
noEventsMessage: "Nema događaja za prikazivanje"
};
var _m10 = {
code: "ca",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Anterior",
next: "Següent",
today: "Avui",
month: "Mes",
week: "Setmana",
day: "Dia",
list: "Agenda"
},
weekLabel: "Set",
allDayText: "Tot el dia",
eventLimitText: "més",
noEventsMessage: "No hi ha esdeveniments per mostrar"
};
var _m11 = {
code: "cs",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Dříve",
next: "Později",
today: "Nyní",
month: "Měsíc",
week: "Týden",
day: "Den",
list: "Agenda"
},
weekLabel: "Týd",
allDayText: "Celý den",
eventLimitText: function (n) {
return "+další: " + n;
},
noEventsMessage: "Žádné akce k zobrazení"
};
var _m12 = {
code: "da",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Forrige",
next: "Næste",
today: "I dag",
month: "Måned",
week: "Uge",
day: "Dag",
list: "Agenda"
},
weekLabel: "Uge",
allDayText: "Hele dagen",
eventLimitText: "flere",
noEventsMessage: "Ingen arrangementer at vise"
};
var _m13 = {
code: "de",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Zurück",
next: "Vor",
today: "Heute",
year: "Jahr",
month: "Monat",
week: "Woche",
day: "Tag",
list: "Terminübersicht"
},
weekLabel: "KW",
allDayText: "Ganztägig",
eventLimitText: function (n) {
return "+ weitere " + n;
},
noEventsMessage: "Keine Ereignisse anzuzeigen"
};
var _m14 = {
code: "el",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4st is the first week of the year.
},
buttonText: {
prev: "Προηγούμενος",
next: "Επόμενος",
today: "Σήμερα",
month: "Μήνας",
week: "Εβδομάδα",
day: "Ημέρα",
list: "Ατζέντα"
},
weekLabel: "Εβδ",
allDayText: "Ολοήμερο",
eventLimitText: "περισσότερα",
noEventsMessage: "Δεν υπάρχουν γεγονότα για να εμφανιστεί"
};
var _m15 = {
code: "en-au",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
}
};
var _m16 = {
code: "en-gb",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
}
};
var _m17 = {
code: "en-nz",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
}
};
var _m18 = {
code: "es",
week: {
dow: 0,
doy: 6 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Ant",
next: "Sig",
today: "Hoy",
month: "Mes",
week: "Semana",
day: "Día",
list: "Agenda"
},
weekLabel: "Sm",
allDayHtml: "Todo
el día",
eventLimitText: "más",
noEventsMessage: "No hay eventos para mostrar"
};
var _m19 = {
code: "es",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Ant",
next: "Sig",
today: "Hoy",
month: "Mes",
week: "Semana",
day: "Día",
list: "Agenda"
},
weekLabel: "Sm",
allDayHtml: "Todo
el día",
eventLimitText: "más",
noEventsMessage: "No hay eventos para mostrar"
};
var _m20 = {
code: "et",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Eelnev",
next: "Järgnev",
today: "Täna",
month: "Kuu",
week: "Nädal",
day: "Päev",
list: "Päevakord"
},
weekLabel: "näd",
allDayText: "Kogu päev",
eventLimitText: function (n) {
return "+ veel " + n;
},
noEventsMessage: "Kuvamiseks puuduvad sündmused"
};
var _m21 = {
code: "eu",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Aur",
next: "Hur",
today: "Gaur",
month: "Hilabetea",
week: "Astea",
day: "Eguna",
list: "Agenda"
},
weekLabel: "As",
allDayHtml: "Egun
osoa",
eventLimitText: "gehiago",
noEventsMessage: "Ez dago ekitaldirik erakusteko"
};
var _m22 = {
code: "fa",
week: {
dow: 6,
doy: 12 // The week that contains Jan 1st is the first week of the year.
},
dir: 'rtl',
buttonText: {
prev: "قبلی",
next: "بعدی",
today: "امروز",
month: "ماه",
week: "هفته",
day: "روز",
list: "برنامه"
},
weekLabel: "هف",
allDayText: "تمام روز",
eventLimitText: function (n) {
return "بیش از " + n;
},
noEventsMessage: "هیچ رویدادی به نمایش"
};
var _m23 = {
code: "fi",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Edellinen",
next: "Seuraava",
today: "Tänään",
month: "Kuukausi",
week: "Viikko",
day: "Päivä",
list: "Tapahtumat"
},
weekLabel: "Vk",
allDayText: "Koko päivä",
eventLimitText: "lisää",
noEventsMessage: "Ei näytettäviä tapahtumia"
};
var _m24 = {
code: "fr",
buttonText: {
prev: "Précédent",
next: "Suivant",
today: "Aujourd'hui",
year: "Année",
month: "Mois",
week: "Semaine",
day: "Jour",
list: "Mon planning"
},
weekLabel: "Sem.",
allDayHtml: "Toute la
journée",
eventLimitText: "en plus",
noEventsMessage: "Aucun événement à afficher"
};
var _m25 = {
code: "fr-ch",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Précédent",
next: "Suivant",
today: "Courant",
year: "Année",
month: "Mois",
week: "Semaine",
day: "Jour",
list: "Mon planning"
},
weekLabel: "Sm",
allDayHtml: "Toute la
journée",
eventLimitText: "en plus",
noEventsMessage: "Aucun événement à afficher"
};
var _m26 = {
code: "fr",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Précédent",
next: "Suivant",
today: "Aujourd'hui",
year: "Année",
month: "Mois",
week: "Semaine",
day: "Jour",
list: "Mon planning"
},
weekLabel: "Sem.",
allDayHtml: "Toute la
journée",
eventLimitText: "en plus",
noEventsMessage: "Aucun événement à afficher"
};
var _m27 = {
code: "gl",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Ant",
next: "Seg",
today: "Hoxe",
month: "Mes",
week: "Semana",
day: "Día",
list: "Axenda"
},
weekLabel: "Sm",
allDayHtml: "Todo
o día",
eventLimitText: "máis",
noEventsMessage: "Non hai eventos para amosar"
};
var _m28 = {
code: "he",
dir: 'rtl',
buttonText: {
prev: "הקודם",
next: "הבא",
today: "היום",
month: "חודש",
week: "שבוע",
day: "יום",
list: "סדר יום"
},
allDayText: "כל היום",
eventLimitText: "אחר",
noEventsMessage: "אין אירועים להצגה",
weekLabel: "שבוע"
};
var _m29 = {
code: "hi",
week: {
dow: 0,
doy: 6 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "पिछला",
next: "अगला",
today: "आज",
month: "महीना",
week: "सप्ताह",
day: "दिन",
list: "कार्यसूची"
},
weekLabel: "हफ्ता",
allDayText: "सभी दिन",
eventLimitText: function (n) {
return "+अधिक " + n;
},
noEventsMessage: "कोई घटनाओं को प्रदर्शित करने के लिए"
};
var _m30 = {
code: "hr",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Prijašnji",
next: "Sljedeći",
today: "Danas",
month: "Mjesec",
week: "Tjedan",
day: "Dan",
list: "Raspored"
},
weekLabel: "Tje",
allDayText: "Cijeli dan",
eventLimitText: function (n) {
return "+ još " + n;
},
noEventsMessage: "Nema događaja za prikaz"
};
var _m31 = {
code: "hu",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "vissza",
next: "előre",
today: "ma",
month: "Hónap",
week: "Hét",
day: "Nap",
list: "Napló"
},
weekLabel: "Hét",
allDayText: "Egész nap",
eventLimitText: "további",
noEventsMessage: "Nincs megjeleníthető esemény"
};
var _m32 = {
code: "id",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "mundur",
next: "maju",
today: "hari ini",
month: "Bulan",
week: "Minggu",
day: "Hari",
list: "Agenda"
},
weekLabel: "Mg",
allDayHtml: "Sehari
penuh",
eventLimitText: "lebih",
noEventsMessage: "Tidak ada acara untuk ditampilkan"
};
var _m33 = {
code: "is",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Fyrri",
next: "Næsti",
today: "Í dag",
month: "Mánuður",
week: "Vika",
day: "Dagur",
list: "Dagskrá"
},
weekLabel: "Vika",
allDayHtml: "Allan
daginn",
eventLimitText: "meira",
noEventsMessage: "Engir viðburðir til að sýna"
};
var _m34 = {
code: "it",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Prec",
next: "Succ",
today: "Oggi",
month: "Mese",
week: "Settimana",
day: "Giorno",
list: "Agenda"
},
weekLabel: "Sm",
allDayHtml: "Tutto il
giorno",
eventLimitText: function (n) {
return "+altri " + n;
},
noEventsMessage: "Non ci sono eventi da visualizzare"
};
var _m35 = {
code: "ja",
buttonText: {
prev: "前",
next: "次",
today: "今日",
month: "月",
week: "週",
day: "日",
list: "予定リスト"
},
weekLabel: "週",
allDayText: "終日",
eventLimitText: function (n) {
return "他 " + n + " 件";
},
noEventsMessage: "表示する予定はありません"
};
var _m36 = {
code: "ka",
week: {
dow: 1,
doy: 7
},
buttonText: {
prev: "წინა",
next: "შემდეგი",
today: "დღეს",
month: "თვე",
week: "კვირა",
day: "დღე",
list: "დღის წესრიგი"
},
weekLabel: "კვ",
allDayText: "მთელი დღე",
eventLimitText: function (n) {
return "+ კიდევ " + n;
},
noEventsMessage: "ღონისძიებები არ არის"
};
var _m37 = {
code: "kk",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Алдыңғы",
next: "Келесі",
today: "Бүгін",
month: "Ай",
week: "Апта",
day: "Күн",
list: "Күн тәртібі"
},
weekLabel: "Не",
allDayText: "Күні бойы",
eventLimitText: function (n) {
return "+ тағы " + n;
},
noEventsMessage: "Көрсету үшін оқиғалар жоқ"
};
var _m38 = {
code: "ko",
buttonText: {
prev: "이전달",
next: "다음달",
today: "오늘",
month: "월",
week: "주",
day: "일",
list: "일정목록"
},
weekLabel: "주",
allDayText: "종일",
eventLimitText: "개",
noEventsMessage: "일정이 없습니다"
};
var _m39 = {
code: "lb",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Zréck",
next: "Weider",
today: "Haut",
month: "Mount",
week: "Woch",
day: "Dag",
list: "Terminiwwersiicht"
},
weekLabel: "W",
allDayText: "Ganzen Dag",
eventLimitText: "méi",
noEventsMessage: "Nee Evenementer ze affichéieren"
};
var _m40 = {
code: "lt",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Atgal",
next: "Pirmyn",
today: "Šiandien",
month: "Mėnuo",
week: "Savaitė",
day: "Diena",
list: "Darbotvarkė"
},
weekLabel: "SAV",
allDayText: "Visą dieną",
eventLimitText: "daugiau",
noEventsMessage: "Nėra įvykių rodyti"
};
var _m41 = {
code: "lv",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Iepr.",
next: "Nāk.",
today: "Šodien",
month: "Mēnesis",
week: "Nedēļa",
day: "Diena",
list: "Dienas kārtība"
},
weekLabel: "Ned.",
allDayText: "Visu dienu",
eventLimitText: function (n) {
return "+vēl " + n;
},
noEventsMessage: "Nav notikumu"
};
var _m42 = {
code: "mk",
buttonText: {
prev: "претходно",
next: "следно",
today: "Денес",
month: "Месец",
week: "Недела",
day: "Ден",
list: "График"
},
weekLabel: "Сед",
allDayText: "Цел ден",
eventLimitText: function (n) {
return "+повеќе " + n;
},
noEventsMessage: "Нема настани за прикажување"
};
var _m43 = {
code: "ms",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Sebelum",
next: "Selepas",
today: "hari ini",
month: "Bulan",
week: "Minggu",
day: "Hari",
list: "Agenda"
},
weekLabel: "Mg",
allDayText: "Sepanjang hari",
eventLimitText: function (n) {
return "masih ada " + n + " acara";
},
noEventsMessage: "Tiada peristiwa untuk dipaparkan"
};
var _m44 = {
code: "nb",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Forrige",
next: "Neste",
today: "I dag",
month: "Måned",
week: "Uke",
day: "Dag",
list: "Agenda"
},
weekLabel: "Uke",
allDayText: "Hele dagen",
eventLimitText: "til",
noEventsMessage: "Ingen hendelser å vise"
};
var _m45 = {
code: "nl",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Voorgaand",
next: "Volgende",
today: "Vandaag",
year: "Jaar",
month: "Maand",
week: "Week",
day: "Dag",
list: "Agenda"
},
allDayText: "Hele dag",
eventLimitText: "extra",
noEventsMessage: "Geen evenementen om te laten zien"
};
var _m46 = {
code: "nn",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Førre",
next: "Neste",
today: "I dag",
month: "Månad",
week: "Veke",
day: "Dag",
list: "Agenda"
},
weekLabel: "Veke",
allDayText: "Heile dagen",
eventLimitText: "til",
noEventsMessage: "Ingen hendelser å vise"
};
var _m47 = {
code: "pl",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Poprzedni",
next: "Następny",
today: "Dziś",
month: "Miesiąc",
week: "Tydzień",
day: "Dzień",
list: "Plan dnia"
},
weekLabel: "Tydz",
allDayText: "Cały dzień",
eventLimitText: "więcej",
noEventsMessage: "Brak wydarzeń do wyświetlenia"
};
var _m48 = {
code: "pt-br",
buttonText: {
prev: "Anterior",
next: "Próximo",
today: "Hoje",
month: "Mês",
week: "Semana",
day: "Dia",
list: "Compromissos"
},
weekLabel: "Sm",
allDayText: "dia inteiro",
eventLimitText: function (n) {
return "mais +" + n;
},
noEventsMessage: "Não há eventos para mostrar"
};
var _m49 = {
code: "pt",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Anterior",
next: "Seguinte",
today: "Hoje",
month: "Mês",
week: "Semana",
day: "Dia",
list: "Agenda"
},
weekLabel: "Sem",
allDayText: "Todo o dia",
eventLimitText: "mais",
noEventsMessage: "Não há eventos para mostrar"
};
var _m50 = {
code: "ro",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "precedentă",
next: "următoare",
today: "Azi",
month: "Lună",
week: "Săptămână",
day: "Zi",
list: "Agendă"
},
weekLabel: "Săpt",
allDayText: "Toată ziua",
eventLimitText: function (n) {
return "+alte " + n;
},
noEventsMessage: "Nu există evenimente de afișat"
};
var _m51 = {
code: "ru",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Пред",
next: "След",
today: "Сегодня",
month: "Месяц",
week: "Неделя",
day: "День",
list: "Повестка дня"
},
weekLabel: "Нед",
allDayText: "Весь день",
eventLimitText: function (n) {
return "+ ещё " + n;
},
noEventsMessage: "Нет событий для отображения"
};
var _m52 = {
code: "sk",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Predchádzajúci",
next: "Nasledujúci",
today: "Dnes",
month: "Mesiac",
week: "Týždeň",
day: "Deň",
list: "Rozvrh"
},
weekLabel: "Ty",
allDayText: "Celý deň",
eventLimitText: function (n) {
return "+ďalšie: " + n;
},
noEventsMessage: "Žiadne akcie na zobrazenie"
};
var _m53 = {
code: "sl",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Prejšnji",
next: "Naslednji",
today: "Trenutni",
month: "Mesec",
week: "Teden",
day: "Dan",
list: "Dnevni red"
},
weekLabel: "Teden",
allDayText: "Ves dan",
eventLimitText: "več",
noEventsMessage: "Ni dogodkov za prikaz"
};
var _m54 = {
code: "sq",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "mbrapa",
next: "Përpara",
today: "sot",
month: "Muaj",
week: "Javë",
day: "Ditë",
list: "Listë"
},
weekLabel: "Ja",
allDayHtml: "Gjithë
ditën",
eventLimitText: function (n) {
return "+më tepër " + n;
},
noEventsMessage: "Nuk ka evente për të shfaqur"
};
var _m55 = {
code: "sr-cyrl",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Претходна",
next: "следећи",
today: "Данас",
month: "Месец",
week: "Недеља",
day: "Дан",
list: "Планер"
},
weekLabel: "Сед",
allDayText: "Цео дан",
eventLimitText: function (n) {
return "+ још " + n;
},
noEventsMessage: "Нема догађаја за приказ"
};
var _m56 = {
code: "sr",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Prethodna",
next: "Sledeći",
today: "Danas",
month: "Mеsеc",
week: "Nеdеlja",
day: "Dan",
list: "Planеr"
},
weekLabel: "Sed",
allDayText: "Cеo dan",
eventLimitText: function (n) {
return "+ još " + n;
},
noEventsMessage: "Nеma događaja za prikaz"
};
var _m57 = {
code: "sv",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Förra",
next: "Nästa",
today: "Idag",
month: "Månad",
week: "Vecka",
day: "Dag",
list: "Program"
},
weekLabel: "v.",
allDayText: "Heldag",
eventLimitText: "till",
noEventsMessage: "Inga händelser att visa"
};
var _m58 = {
code: "th",
buttonText: {
prev: "ย้อน",
next: "ถัดไป",
today: "วันนี้",
month: "เดือน",
week: "สัปดาห์",
day: "วัน",
list: "แผนงาน"
},
allDayText: "ตลอดวัน",
eventLimitText: "เพิ่มเติม",
noEventsMessage: "ไม่มีกิจกรรมที่จะแสดง"
};
var _m59 = {
code: "tr",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "geri",
next: "ileri",
today: "bugün",
month: "Ay",
week: "Hafta",
day: "Gün",
list: "Ajanda"
},
weekLabel: "Hf",
allDayText: "Tüm gün",
eventLimitText: "daha fazla",
noEventsMessage: "Gösterilecek etkinlik yok"
};
var _m60 = {
code: "uk",
week: {
dow: 1,
doy: 7 // The week that contains Jan 1st is the first week of the year.
},
buttonText: {
prev: "Попередній",
next: "далі",
today: "Сьогодні",
month: "Місяць",
week: "Тиждень",
day: "День",
list: "Порядок денний"
},
weekLabel: "Тиж",
allDayText: "Увесь день",
eventLimitText: function (n) {
return "+ще " + n + "...";
},
noEventsMessage: "Немає подій для відображення"
};
var _m61 = {
code: "vi",
week: {
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "Trước",
next: "Tiếp",
today: "Hôm nay",
month: "Tháng",
week: "Tuần",
day: "Ngày",
list: "Lịch biểu"
},
weekLabel: "Tu",
allDayText: "Cả ngày",
eventLimitText: function (n) {
return "+ thêm " + n;
},
noEventsMessage: "Không có sự kiện để hiển thị"
};
var _m62 = {
code: "zh-cn",
week: {
// GB/T 7408-1994《数据元和交换格式·信息交换·日期和时间表示法》与ISO 8601:1988等效
dow: 1,
doy: 4 // The week that contains Jan 4th is the first week of the year.
},
buttonText: {
prev: "上月",
next: "下月",
today: "今天",
month: "月",
week: "周",
day: "日",
list: "日程"
},
weekLabel: "周",
allDayText: "全天",
eventLimitText: function (n) {
return "另外 " + n + " 个";
},
noEventsMessage: "没有事件显示"
};
var _m63 = {
code: "zh-tw",
buttonText: {
prev: "上月",
next: "下月",
today: "今天",
month: "月",
week: "週",
day: "天",
list: "活動列表"
},
weekLabel: "周",
allDayText: "整天",
eventLimitText: '顯示更多',
noEventsMessage: "没有任何活動"
};
var _rollupPluginMultiEntry_entryPoint = [
_m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14, _m15, _m16, _m17, _m18, _m19, _m20, _m21, _m22, _m23, _m24, _m25, _m26, _m27, _m28, _m29, _m30, _m31, _m32, _m33, _m34, _m35, _m36, _m37, _m38, _m39, _m40, _m41, _m42, _m43, _m44, _m45, _m46, _m47, _m48, _m49, _m50, _m51, _m52, _m53, _m54, _m55, _m56, _m57, _m58, _m59, _m60, _m61, _m62, _m63
];
return _rollupPluginMultiEntry_entryPoint;
}));
/*!
FullCalendar Core Package v4.3.1
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
// Creating
// ----------------------------------------------------------------------------------------------------------------
var elementPropHash = {
className: true,
colSpan: true,
rowSpan: true
};
var containerTagHash = {
'= rect.left &&
point.left < rect.right &&
point.top >= rect.top &&
point.top < rect.bottom;
}
// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
function intersectRects(rect1, rect2) {
var res = {
left: Math.max(rect1.left, rect2.left),
right: Math.min(rect1.right, rect2.right),
top: Math.max(rect1.top, rect2.top),
bottom: Math.min(rect1.bottom, rect2.bottom)
};
if (res.left < res.right && res.top < res.bottom) {
return res;
}
return false;
}
function translateRect(rect, deltaX, deltaY) {
return {
left: rect.left + deltaX,
right: rect.right + deltaX,
top: rect.top + deltaY,
bottom: rect.bottom + deltaY
};
}
// Returns a new point that will have been moved to reside within the given rectangle
function constrainPoint(point, rect) {
return {
left: Math.min(Math.max(point.left, rect.left), rect.right),
top: Math.min(Math.max(point.top, rect.top), rect.bottom)
};
}
// Returns a point that is the center of the given rectangle
function getRectCenter(rect) {
return {
left: (rect.left + rect.right) / 2,
top: (rect.top + rect.bottom) / 2
};
}
// Subtracts point2's coordinates from point1's coordinates, returning a delta
function diffPoints(point1, point2) {
return {
left: point1.left - point2.left,
top: point1.top - point2.top
};
}
// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
var isRtlScrollbarOnLeft = null;
function getIsRtlScrollbarOnLeft() {
if (isRtlScrollbarOnLeft === null) {
isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft();
}
return isRtlScrollbarOnLeft;
}
function computeIsRtlScrollbarOnLeft() {
var outerEl = createElement('div', {
style: {
position: 'absolute',
top: -1000,
left: 0,
border: 0,
padding: 0,
overflow: 'scroll',
direction: 'rtl'
}
}, '
function computeEdges(el, getPadding) {
if (getPadding === void 0) { getPadding = false; }
var computedStyle = window.getComputedStyle(el);
var borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0;
var borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0;
var borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0;
var borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
// must use offset(Width|Height) because compatible with client(Width|Height)
var scrollbarLeftRight = sanitizeScrollbarWidth(el.offsetWidth - el.clientWidth - borderLeft - borderRight);
var scrollbarBottom = sanitizeScrollbarWidth(el.offsetHeight - el.clientHeight - borderTop - borderBottom);
var res = {
borderLeft: borderLeft,
borderRight: borderRight,
borderTop: borderTop,
borderBottom: borderBottom,
scrollbarBottom: scrollbarBottom,
scrollbarLeft: 0,
scrollbarRight: 0
};
if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side?
res.scrollbarLeft = scrollbarLeftRight;
}
else {
res.scrollbarRight = scrollbarLeftRight;
}
if (getPadding) {
res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
}
return res;
}
function computeInnerRect(el, goWithinPadding) {
if (goWithinPadding === void 0) { goWithinPadding = false; }
var outerRect = computeRect(el);
var edges = computeEdges(el, goWithinPadding);
var res = {
left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,
right: outerRect.right - edges.borderRight - edges.scrollbarRight,
top: outerRect.top + edges.borderTop,
bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom
};
if (goWithinPadding) {
res.left += edges.paddingLeft;
res.right -= edges.paddingRight;
res.top += edges.paddingTop;
res.bottom -= edges.paddingBottom;
}
return res;
}
function computeRect(el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
right: rect.right + window.pageXOffset,
bottom: rect.bottom + window.pageYOffset
};
}
function computeViewportRect() {
return {
left: window.pageXOffset,
right: window.pageXOffset + document.documentElement.clientWidth,
top: window.pageYOffset,
bottom: window.pageYOffset + document.documentElement.clientHeight
};
}
function computeHeightAndMargins(el) {
return el.getBoundingClientRect().height + computeVMargins(el);
}
function computeVMargins(el) {
var computed = window.getComputedStyle(el);
return parseInt(computed.marginTop, 10) +
parseInt(computed.marginBottom, 10);
}
// does not return window
function getClippingParents(el) {
var parents = [];
while (el instanceof HTMLElement) { // will stop when gets to document or null
var computedStyle = window.getComputedStyle(el);
if (computedStyle.position === 'fixed') {
break;
}
if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) {
parents.push(el);
}
el = el.parentNode;
}
return parents;
}
function computeClippingRect(el) {
return getClippingParents(el)
.map(function (el) {
return computeInnerRect(el);
})
.concat(computeViewportRect())
.reduce(function (rect0, rect1) {
return intersectRects(rect0, rect1) || rect1; // should always intersect
});
}
// Stops a mouse/touch event from doing it's native browser action
function preventDefault(ev) {
ev.preventDefault();
}
// Event Delegation
// ----------------------------------------------------------------------------------------------------------------
function listenBySelector(container, eventType, selector, handler) {
function realHandler(ev) {
var matchedChild = elementClosest(ev.target, selector);
if (matchedChild) {
handler.call(matchedChild, ev, matchedChild);
}
}
container.addEventListener(eventType, realHandler);
return function () {
container.removeEventListener(eventType, realHandler);
};
}
function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) {
var currentMatchedChild;
return listenBySelector(container, 'mouseover', selector, function (ev, matchedChild) {
if (matchedChild !== currentMatchedChild) {
currentMatchedChild = matchedChild;
onMouseEnter(ev, matchedChild);
var realOnMouseLeave_1 = function (ev) {
currentMatchedChild = null;
onMouseLeave(ev, matchedChild);
matchedChild.removeEventListener('mouseleave', realOnMouseLeave_1);
};
// listen to the next mouseleave, and then unattach
matchedChild.addEventListener('mouseleave', realOnMouseLeave_1);
}
});
}
// Animation
// ----------------------------------------------------------------------------------------------------------------
var transitionEventNames = [
'webkitTransitionEnd',
'otransitionend',
'oTransitionEnd',
'msTransitionEnd',
'transitionend'
];
// triggered only when the next single subsequent transition finishes
function whenTransitionDone(el, callback) {
var realCallback = function (ev) {
callback(ev);
transitionEventNames.forEach(function (eventName) {
el.removeEventListener(eventName, realCallback);
});
};
transitionEventNames.forEach(function (eventName) {
el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes
});
}
var DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
// Adding
function addWeeks(m, n) {
var a = dateToUtcArray(m);
a[2] += n * 7;
return arrayToUtcDate(a);
}
function addDays(m, n) {
var a = dateToUtcArray(m);
a[2] += n;
return arrayToUtcDate(a);
}
function addMs(m, n) {
var a = dateToUtcArray(m);
a[6] += n;
return arrayToUtcDate(a);
}
// Diffing (all return floats)
function diffWeeks(m0, m1) {
return diffDays(m0, m1) / 7;
}
function diffDays(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24);
}
function diffHours(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60);
}
function diffMinutes(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60);
}
function diffSeconds(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / 1000;
}
function diffDayAndTime(m0, m1) {
var m0day = startOfDay(m0);
var m1day = startOfDay(m1);
return {
years: 0,
months: 0,
days: Math.round(diffDays(m0day, m1day)),
milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf())
};
}
// Diffing Whole Units
function diffWholeWeeks(m0, m1) {
var d = diffWholeDays(m0, m1);
if (d !== null && d % 7 === 0) {
return d / 7;
}
return null;
}
function diffWholeDays(m0, m1) {
if (timeAsMs(m0) === timeAsMs(m1)) {
return Math.round(diffDays(m0, m1));
}
return null;
}
// Start-Of
function startOfDay(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate()
]);
}
function startOfHour(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours()
]);
}
function startOfMinute(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours(),
m.getUTCMinutes()
]);
}
function startOfSecond(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours(),
m.getUTCMinutes(),
m.getUTCSeconds()
]);
}
// Week Computation
function weekOfYear(marker, dow, doy) {
var y = marker.getUTCFullYear();
var w = weekOfGivenYear(marker, y, dow, doy);
if (w < 1) {
return weekOfGivenYear(marker, y - 1, dow, doy);
}
var nextW = weekOfGivenYear(marker, y + 1, dow, doy);
if (nextW >= 1) {
return Math.min(w, nextW);
}
return w;
}
function weekOfGivenYear(marker, year, dow, doy) {
var firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]);
var dayStart = startOfDay(marker);
var days = Math.round(diffDays(firstWeekStart, dayStart));
return Math.floor(days / 7) + 1; // zero-indexed
}
// start-of-first-week - start-of-year
function firstWeekOffset(year, dow, doy) {
// first-week day -- which january is always in the first week (4 for iso, 1 for other)
var fwd = 7 + dow - doy;
// first-week day local weekday -- which local weekday is fwd
var fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7;
return -fwdlw + fwd - 1;
}
// Array Conversion
function dateToLocalArray(date) {
return [
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
];
}
function arrayToLocalDate(a) {
return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month
a[3] || 0, a[4] || 0, a[5] || 0);
}
function dateToUtcArray(date) {
return [
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
];
}
function arrayToUtcDate(a) {
// according to web standards (and Safari), a month index is required.
// massage if only given a year.
if (a.length === 1) {
a = a.concat([0]);
}
return new Date(Date.UTC.apply(Date, a));
}
// Other Utils
function isValidDate(m) {
return !isNaN(m.valueOf());
}
function timeAsMs(m) {
return m.getUTCHours() * 1000 * 60 * 60 +
m.getUTCMinutes() * 1000 * 60 +
m.getUTCSeconds() * 1000 +
m.getUTCMilliseconds();
}
var INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds'];
var PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/;
// Parsing and Creation
function createDuration(input, unit) {
var _a;
if (typeof input === 'string') {
return parseString(input);
}
else if (typeof input === 'object' && input) { // non-null object
return normalizeObject(input);
}
else if (typeof input === 'number') {
return normalizeObject((_a = {}, _a[unit || 'milliseconds'] = input, _a));
}
else {
return null;
}
}
function parseString(s) {
var m = PARSE_RE.exec(s);
if (m) {
var sign = m[1] ? -1 : 1;
return {
years: 0,
months: 0,
days: sign * (m[2] ? parseInt(m[2], 10) : 0),
milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours
(m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes
(m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds
(m[6] ? parseInt(m[6], 10) : 0) // ms
)
};
}
return null;
}
function normalizeObject(obj) {
return {
years: obj.years || obj.year || 0,
months: obj.months || obj.month || 0,
days: (obj.days || obj.day || 0) +
getWeeksFromInput(obj) * 7,
milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours
(obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes
(obj.seconds || obj.second || 0) * 1000 + // seconds
(obj.milliseconds || obj.millisecond || obj.ms || 0) // ms
};
}
function getWeeksFromInput(obj) {
return obj.weeks || obj.week || 0;
}
// Equality
function durationsEqual(d0, d1) {
return d0.years === d1.years &&
d0.months === d1.months &&
d0.days === d1.days &&
d0.milliseconds === d1.milliseconds;
}
function isSingleDay(dur) {
return dur.years === 0 && dur.months === 0 && dur.days === 1 && dur.milliseconds === 0;
}
// Simple Math
function addDurations(d0, d1) {
return {
years: d0.years + d1.years,
months: d0.months + d1.months,
days: d0.days + d1.days,
milliseconds: d0.milliseconds + d1.milliseconds
};
}
function subtractDurations(d1, d0) {
return {
years: d1.years - d0.years,
months: d1.months - d0.months,
days: d1.days - d0.days,
milliseconds: d1.milliseconds - d0.milliseconds
};
}
function multiplyDuration(d, n) {
return {
years: d.years * n,
months: d.months * n,
days: d.days * n,
milliseconds: d.milliseconds * n
};
}
// Conversions
// "Rough" because they are based on average-case Gregorian months/years
function asRoughYears(dur) {
return asRoughDays(dur) / 365;
}
function asRoughMonths(dur) {
return asRoughDays(dur) / 30;
}
function asRoughDays(dur) {
return asRoughMs(dur) / 864e5;
}
function asRoughMinutes(dur) {
return asRoughMs(dur) / (1000 * 60);
}
function asRoughSeconds(dur) {
return asRoughMs(dur) / 1000;
}
function asRoughMs(dur) {
return dur.years * (365 * 864e5) +
dur.months * (30 * 864e5) +
dur.days * 864e5 +
dur.milliseconds;
}
// Advanced Math
function wholeDivideDurations(numerator, denominator) {
var res = null;
for (var i = 0; i < INTERNAL_UNITS.length; i++) {
var unit = INTERNAL_UNITS[i];
if (denominator[unit]) {
var localRes = numerator[unit] / denominator[unit];
if (!isInt(localRes) || (res !== null && res !== localRes)) {
return null;
}
res = localRes;
}
else if (numerator[unit]) {
// needs to divide by something but can't!
return null;
}
}
return res;
}
function greatestDurationDenominator(dur, dontReturnWeeks) {
var ms = dur.milliseconds;
if (ms) {
if (ms % 1000 !== 0) {
return { unit: 'millisecond', value: ms };
}
if (ms % (1000 * 60) !== 0) {
return { unit: 'second', value: ms / 1000 };
}
if (ms % (1000 * 60 * 60) !== 0) {
return { unit: 'minute', value: ms / (1000 * 60) };
}
if (ms) {
return { unit: 'hour', value: ms / (1000 * 60 * 60) };
}
}
if (dur.days) {
if (!dontReturnWeeks && dur.days % 7 === 0) {
return { unit: 'week', value: dur.days / 7 };
}
return { unit: 'day', value: dur.days };
}
if (dur.months) {
return { unit: 'month', value: dur.months };
}
if (dur.years) {
return { unit: 'year', value: dur.years };
}
return { unit: 'millisecond', value: 0 };
}
/* FullCalendar-specific DOM Utilities
----------------------------------------------------------------------------------------------------------------------*/
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
function compensateScroll(rowEl, scrollbarWidths) {
if (scrollbarWidths.left) {
applyStyle(rowEl, {
borderLeftWidth: 1,
marginLeft: scrollbarWidths.left - 1
});
}
if (scrollbarWidths.right) {
applyStyle(rowEl, {
borderRightWidth: 1,
marginRight: scrollbarWidths.right - 1
});
}
}
// Undoes compensateScroll and restores all borders/margins
function uncompensateScroll(rowEl) {
applyStyle(rowEl, {
marginLeft: '',
marginRight: '',
borderLeftWidth: '',
borderRightWidth: ''
});
}
// Make the mouse cursor express that an event is not allowed in the current area
function disableCursor() {
document.body.classList.add('fc-not-allowed');
}
// Returns the mouse cursor to its original look
function enableCursor() {
document.body.classList.remove('fc-not-allowed');
}
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
// reduces the available height.
function distributeHeight(els, availableHeight, shouldRedistribute) {
// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
var flexEls = []; // elements that are allowed to expand. array of DOM nodes
var flexOffsets = []; // amount of vertical space it takes up
var flexHeights = []; // actual css height
var usedHeight = 0;
undistributeHeight(els); // give all elements their natural height
// find elements that are below the recommended height (expandable).
// important to query for heights in a single first pass (to avoid reflow oscillation).
els.forEach(function (el, i) {
var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
var naturalHeight = el.getBoundingClientRect().height;
var naturalOffset = naturalHeight + computeVMargins(el);
if (naturalOffset < minOffset) {
flexEls.push(el);
flexOffsets.push(naturalOffset);
flexHeights.push(naturalHeight);
}
else {
// this element stretches past recommended height (non-expandable). mark the space as occupied.
usedHeight += naturalOffset;
}
});
// readjust the recommended height to only consider the height available to non-maxed-out rows.
if (shouldRedistribute) {
availableHeight -= usedHeight;
minOffset1 = Math.floor(availableHeight / flexEls.length);
minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
}
// assign heights to all expandable elements
flexEls.forEach(function (el, i) {
var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
var naturalOffset = flexOffsets[i];
var naturalHeight = flexHeights[i];
var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
el.style.height = newHeight + 'px';
}
});
}
// Undoes distrubuteHeight, restoring all els to their natural height
function undistributeHeight(els) {
els.forEach(function (el) {
el.style.height = '';
});
}
// Given `els`, a set of cells, find the cell with the largest natural width and set the widths of all the
// cells to be that width.
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
function matchCellWidths(els) {
var maxInnerWidth = 0;
els.forEach(function (el) {
var innerEl = el.firstChild; // hopefully an element
if (innerEl instanceof HTMLElement) {
var innerWidth_1 = innerEl.getBoundingClientRect().width;
if (innerWidth_1 > maxInnerWidth) {
maxInnerWidth = innerWidth_1;
}
}
});
maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
els.forEach(function (el) {
el.style.width = maxInnerWidth + 'px';
});
return maxInnerWidth;
}
// Given one element that resides inside another,
// Subtracts the height of the inner element from the outer element.
function subtractInnerElHeight(outerEl, innerEl) {
// effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
var reflowStyleProps = {
position: 'relative',
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
};
applyStyle(outerEl, reflowStyleProps);
applyStyle(innerEl, reflowStyleProps);
var diff = // grab the dimensions
outerEl.getBoundingClientRect().height -
innerEl.getBoundingClientRect().height;
// undo hack
var resetStyleProps = { position: '', left: '' };
applyStyle(outerEl, resetStyleProps);
applyStyle(innerEl, resetStyleProps);
return diff;
}
/* Selection
----------------------------------------------------------------------------------------------------------------------*/
function preventSelection(el) {
el.classList.add('fc-unselectable');
el.addEventListener('selectstart', preventDefault);
}
function allowSelection(el) {
el.classList.remove('fc-unselectable');
el.removeEventListener('selectstart', preventDefault);
}
/* Context Menu
----------------------------------------------------------------------------------------------------------------------*/
function preventContextMenu(el) {
el.addEventListener('contextmenu', preventDefault);
}
function allowContextMenu(el) {
el.removeEventListener('contextmenu', preventDefault);
}
/* Object Ordering by Field
----------------------------------------------------------------------------------------------------------------------*/
function parseFieldSpecs(input) {
var specs = [];
var tokens = [];
var i;
var token;
if (typeof input === 'string') {
tokens = input.split(/\s*,\s*/);
}
else if (typeof input === 'function') {
tokens = [input];
}
else if (Array.isArray(input)) {
tokens = input;
}
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
if (typeof token === 'string') {
specs.push(token.charAt(0) === '-' ?
{ field: token.substring(1), order: -1 } :
{ field: token, order: 1 });
}
else if (typeof token === 'function') {
specs.push({ func: token });
}
}
return specs;
}
function compareByFieldSpecs(obj0, obj1, fieldSpecs) {
var i;
var cmp;
for (i = 0; i < fieldSpecs.length; i++) {
cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]);
if (cmp) {
return cmp;
}
}
return 0;
}
function compareByFieldSpec(obj0, obj1, fieldSpec) {
if (fieldSpec.func) {
return fieldSpec.func(obj0, obj1);
}
return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field])
* (fieldSpec.order || 1);
}
function flexibleCompare(a, b) {
if (!a && !b) {
return 0;
}
if (b == null) {
return -1;
}
if (a == null) {
return 1;
}
if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b));
}
return a - b;
}
/* String Utilities
----------------------------------------------------------------------------------------------------------------------*/
function capitaliseFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function padStart(val, len) {
var s = String(val);
return '000'.substr(0, len - s.length) + s;
}
/* Number Utilities
----------------------------------------------------------------------------------------------------------------------*/
function compareNumbers(a, b) {
return a - b;
}
function isInt(n) {
return n % 1 === 0;
}
/* Weird Utilities
----------------------------------------------------------------------------------------------------------------------*/
function applyAll(functions, thisObj, args) {
if (typeof functions === 'function') { // supplied a single function
functions = [functions];
}
if (functions) {
var i = void 0;
var ret = void 0;
for (i = 0; i < functions.length; i++) {
ret = functions[i].apply(thisObj, args) || ret;
}
return ret;
}
}
function firstDefined() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
if (args[i] !== undefined) {
return args[i];
}
}
}
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
function debounce(func, wait) {
var timeout;
var args;
var context;
var timestamp;
var result;
var later = function () {
var last = new Date().valueOf() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
}
else {
timeout = null;
result = func.apply(context, args);
context = args = null;
}
};
return function () {
context = this;
args = arguments;
timestamp = new Date().valueOf();
if (!timeout) {
timeout = setTimeout(later, wait);
}
return result;
};
}
// Number and Boolean are only types that defaults or not computed for
// TODO: write more comments
function refineProps(rawProps, processors, defaults, leftoverProps) {
if (defaults === void 0) { defaults = {}; }
var refined = {};
for (var key in processors) {
var processor = processors[key];
if (rawProps[key] !== undefined) {
// found
if (processor === Function) {
refined[key] = typeof rawProps[key] === 'function' ? rawProps[key] : null;
}
else if (processor) { // a refining function?
refined[key] = processor(rawProps[key]);
}
else {
refined[key] = rawProps[key];
}
}
else if (defaults[key] !== undefined) {
// there's an explicit default
refined[key] = defaults[key];
}
else {
// must compute a default
if (processor === String) {
refined[key] = ''; // empty string is default for String
}
else if (!processor || processor === Number || processor === Boolean || processor === Function) {
refined[key] = null; // assign null for other non-custom processor funcs
}
else {
refined[key] = processor(null); // run the custom processor func
}
}
}
if (leftoverProps) {
for (var key in rawProps) {
if (processors[key] === undefined) {
leftoverProps[key] = rawProps[key];
}
}
}
return refined;
}
/* Date stuff that doesn't belong in datelib core
----------------------------------------------------------------------------------------------------------------------*/
// given a timed range, computes an all-day range that has the same exact duration,
// but whose start time is aligned with the start of the day.
function computeAlignedDayRange(timedRange) {
var dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1;
var start = startOfDay(timedRange.start);
var end = addDays(start, dayCnt);
return { start: start, end: end };
}
// given a timed range, computes an all-day range based on how for the end date bleeds into the next day
// TODO: give nextDayThreshold a default arg
function computeVisibleDayRange(timedRange, nextDayThreshold) {
if (nextDayThreshold === void 0) { nextDayThreshold = createDuration(0); }
var startDay = null;
var endDay = null;
if (timedRange.end) {
endDay = startOfDay(timedRange.end);
var endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay`
// If the end time is actually inclusively part of the next day and is equal to or
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
endDay = addDays(endDay, 1);
}
}
if (timedRange.start) {
startDay = startOfDay(timedRange.start); // the beginning of the day the range starts
// If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
if (endDay && endDay <= startDay) {
endDay = addDays(startDay, 1);
}
}
return { start: startDay, end: endDay };
}
// spans from one day into another?
function isMultiDayRange(range) {
var visibleRange = computeVisibleDayRange(range);
return diffDays(visibleRange.start, visibleRange.end) > 1;
}
function diffDates(date0, date1, dateEnv, largeUnit) {
if (largeUnit === 'year') {
return createDuration(dateEnv.diffWholeYears(date0, date1), 'year');
}
else if (largeUnit === 'month') {
return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month');
}
else {
return diffDayAndTime(date0, date1); // returns a duration
}
}
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function parseRecurring(eventInput, allDayDefault, dateEnv, recurringTypes, leftovers) {
for (var i = 0; i < recurringTypes.length; i++) {
var localLeftovers = {};
var parsed = recurringTypes[i].parse(eventInput, localLeftovers, dateEnv);
if (parsed) {
var allDay = localLeftovers.allDay;
delete localLeftovers.allDay; // remove from leftovers
if (allDay == null) {
allDay = allDayDefault;
if (allDay == null) {
allDay = parsed.allDayGuess;
if (allDay == null) {
allDay = false;
}
}
}
__assign(leftovers, localLeftovers);
return {
allDay: allDay,
duration: parsed.duration,
typeData: parsed.typeData,
typeId: i
};
}
}
return null;
}
/*
Event MUST have a recurringDef
*/
function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) {
var typeDef = recurringTypes[eventDef.recurringDef.typeId];
var markers = typeDef.expand(eventDef.recurringDef.typeData, {
start: dateEnv.subtract(framingRange.start, duration),
end: framingRange.end
}, dateEnv);
// the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to
if (eventDef.allDay) {
markers = markers.map(startOfDay);
}
return markers;
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
// Merges an array of objects into a single object.
// The second argument allows for an array of property names who's object values will be merged together.
function mergeProps(propObjs, complexProps) {
var dest = {};
var i;
var name;
var complexObjs;
var j;
var val;
var props;
if (complexProps) {
for (i = 0; i < complexProps.length; i++) {
name = complexProps[i];
complexObjs = [];
// collect the trailing object values, stopping when a non-object is discovered
for (j = propObjs.length - 1; j >= 0; j--) {
val = propObjs[j][name];
if (typeof val === 'object' && val) { // non-null object
complexObjs.unshift(val);
}
else if (val !== undefined) {
dest[name] = val; // if there were no objects, this value will be used
break;
}
}
// if the trailing values were objects, use the merged value
if (complexObjs.length) {
dest[name] = mergeProps(complexObjs);
}
}
}
// copy values into the destination, going from last to first
for (i = propObjs.length - 1; i >= 0; i--) {
props = propObjs[i];
for (name in props) {
if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
dest[name] = props[name];
}
}
}
return dest;
}
function filterHash(hash, func) {
var filtered = {};
for (var key in hash) {
if (func(hash[key], key)) {
filtered[key] = hash[key];
}
}
return filtered;
}
function mapHash(hash, func) {
var newHash = {};
for (var key in hash) {
newHash[key] = func(hash[key], key);
}
return newHash;
}
function arrayToHash(a) {
var hash = {};
for (var _i = 0, a_1 = a; _i < a_1.length; _i++) {
var item = a_1[_i];
hash[item] = true;
}
return hash;
}
function hashValuesToArray(obj) {
var a = [];
for (var key in obj) {
a.push(obj[key]);
}
return a;
}
function isPropsEqual(obj0, obj1) {
for (var key in obj0) {
if (hasOwnProperty.call(obj0, key)) {
if (!(key in obj1)) {
return false;
}
}
}
for (var key in obj1) {
if (hasOwnProperty.call(obj1, key)) {
if (obj0[key] !== obj1[key]) {
return false;
}
}
}
return true;
}
function parseEvents(rawEvents, sourceId, calendar, allowOpenRange) {
var eventStore = createEmptyEventStore();
for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) {
var rawEvent = rawEvents_1[_i];
var tuple = parseEvent(rawEvent, sourceId, calendar, allowOpenRange);
if (tuple) {
eventTupleToStore(tuple, eventStore);
}
}
return eventStore;
}
function eventTupleToStore(tuple, eventStore) {
if (eventStore === void 0) { eventStore = createEmptyEventStore(); }
eventStore.defs[tuple.def.defId] = tuple.def;
if (tuple.instance) {
eventStore.instances[tuple.instance.instanceId] = tuple.instance;
}
return eventStore;
}
function expandRecurring(eventStore, framingRange, calendar) {
var dateEnv = calendar.dateEnv;
var defs = eventStore.defs, instances = eventStore.instances;
// remove existing recurring instances
instances = filterHash(instances, function (instance) {
return !defs[instance.defId].recurringDef;
});
for (var defId in defs) {
var def = defs[defId];
if (def.recurringDef) {
var duration = def.recurringDef.duration;
if (!duration) {
duration = def.allDay ?
calendar.defaultAllDayEventDuration :
calendar.defaultTimedEventDuration;
}
var starts = expandRecurringRanges(def, duration, framingRange, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes);
for (var _i = 0, starts_1 = starts; _i < starts_1.length; _i++) {
var start = starts_1[_i];
var instance = createEventInstance(defId, {
start: start,
end: dateEnv.add(start, duration)
});
instances[instance.instanceId] = instance;
}
}
}
return { defs: defs, instances: instances };
}
// retrieves events that have the same groupId as the instance specified by `instanceId`
// or they are the same as the instance.
// why might instanceId not be in the store? an event from another calendar?
function getRelevantEvents(eventStore, instanceId) {
var instance = eventStore.instances[instanceId];
if (instance) {
var def_1 = eventStore.defs[instance.defId];
// get events/instances with same group
var newStore = filterEventStoreDefs(eventStore, function (lookDef) {
return isEventDefsGrouped(def_1, lookDef);
});
// add the original
// TODO: wish we could use eventTupleToStore or something like it
newStore.defs[def_1.defId] = def_1;
newStore.instances[instance.instanceId] = instance;
return newStore;
}
return createEmptyEventStore();
}
function isEventDefsGrouped(def0, def1) {
return Boolean(def0.groupId && def0.groupId === def1.groupId);
}
function transformRawEvents(rawEvents, eventSource, calendar) {
var calEachTransform = calendar.opt('eventDataTransform');
var sourceEachTransform = eventSource ? eventSource.eventDataTransform : null;
if (sourceEachTransform) {
rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform);
}
if (calEachTransform) {
rawEvents = transformEachRawEvent(rawEvents, calEachTransform);
}
return rawEvents;
}
function transformEachRawEvent(rawEvents, func) {
var refinedEvents;
if (!func) {
refinedEvents = rawEvents;
}
else {
refinedEvents = [];
for (var _i = 0, rawEvents_2 = rawEvents; _i < rawEvents_2.length; _i++) {
var rawEvent = rawEvents_2[_i];
var refinedEvent = func(rawEvent);
if (refinedEvent) {
refinedEvents.push(refinedEvent);
}
else if (refinedEvent == null) {
refinedEvents.push(rawEvent);
} // if a different falsy value, do nothing
}
}
return refinedEvents;
}
function createEmptyEventStore() {
return { defs: {}, instances: {} };
}
function mergeEventStores(store0, store1) {
return {
defs: __assign({}, store0.defs, store1.defs),
instances: __assign({}, store0.instances, store1.instances)
};
}
function filterEventStoreDefs(eventStore, filterFunc) {
var defs = filterHash(eventStore.defs, filterFunc);
var instances = filterHash(eventStore.instances, function (instance) {
return defs[instance.defId]; // still exists?
});
return { defs: defs, instances: instances };
}
function parseRange(input, dateEnv) {
var start = null;
var end = null;
if (input.start) {
start = dateEnv.createMarker(input.start);
}
if (input.end) {
end = dateEnv.createMarker(input.end);
}
if (!start && !end) {
return null;
}
if (start && end && end < start) {
return null;
}
return { start: start, end: end };
}
// SIDE-EFFECT: will mutate ranges.
// Will return a new array result.
function invertRanges(ranges, constraintRange) {
var invertedRanges = [];
var start = constraintRange.start; // the end of the previous range. the start of the new range
var i;
var dateRange;
// ranges need to be in order. required for our date-walking algorithm
ranges.sort(compareRanges);
for (i = 0; i < ranges.length; i++) {
dateRange = ranges[i];
// add the span of time before the event (if there is any)
if (dateRange.start > start) { // compare millisecond time (skip any ambig logic)
invertedRanges.push({ start: start, end: dateRange.start });
}
if (dateRange.end > start) {
start = dateRange.end;
}
}
// add the span of time after the last event (if there is any)
if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic)
invertedRanges.push({ start: start, end: constraintRange.end });
}
return invertedRanges;
}
function compareRanges(range0, range1) {
return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first
}
function intersectRanges(range0, range1) {
var start = range0.start;
var end = range0.end;
var newRange = null;
if (range1.start !== null) {
if (start === null) {
start = range1.start;
}
else {
start = new Date(Math.max(start.valueOf(), range1.start.valueOf()));
}
}
if (range1.end != null) {
if (end === null) {
end = range1.end;
}
else {
end = new Date(Math.min(end.valueOf(), range1.end.valueOf()));
}
}
if (start === null || end === null || start < end) {
newRange = { start: start, end: end };
}
return newRange;
}
function rangesEqual(range0, range1) {
return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) &&
(range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf());
}
function rangesIntersect(range0, range1) {
return (range0.end === null || range1.start === null || range0.end > range1.start) &&
(range0.start === null || range1.end === null || range0.start < range1.end);
}
function rangeContainsRange(outerRange, innerRange) {
return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) &&
(outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end));
}
function rangeContainsMarker(range, date) {
return (range.start === null || date >= range.start) &&
(range.end === null || date < range.end);
}
// If the given date is not within the given range, move it inside.
// (If it's past the end, make it one millisecond before the end).
function constrainMarkerToRange(date, range) {
if (range.start != null && date < range.start) {
return range.start;
}
if (range.end != null && date >= range.end) {
return new Date(range.end.valueOf() - 1);
}
return date;
}
function removeExact(array, exactVal) {
var removeCnt = 0;
var i = 0;
while (i < array.length) {
if (array[i] === exactVal) {
array.splice(i, 1);
removeCnt++;
}
else {
i++;
}
}
return removeCnt;
}
function isArraysEqual(a0, a1) {
var len = a0.length;
var i;
if (len !== a1.length) { // not array? or not same length?
return false;
}
for (i = 0; i < len; i++) {
if (a0[i] !== a1[i]) {
return false;
}
}
return true;
}
function memoize(workerFunc) {
var args;
var res;
return function () {
if (!args || !isArraysEqual(args, arguments)) {
args = arguments;
res = workerFunc.apply(this, arguments);
}
return res;
};
}
/*
always executes the workerFunc, but if the result is equal to the previous result,
return the previous result instead.
*/
function memoizeOutput(workerFunc, equalityFunc) {
var cachedRes = null;
return function () {
var newRes = workerFunc.apply(this, arguments);
if (cachedRes === null || !(cachedRes === newRes || equalityFunc(cachedRes, newRes))) {
cachedRes = newRes;
}
return cachedRes;
};
}
var EXTENDED_SETTINGS_AND_SEVERITIES = {
week: 3,
separator: 0,
omitZeroMinute: 0,
meridiem: 0,
omitCommas: 0
};
var STANDARD_DATE_PROP_SEVERITIES = {
timeZoneName: 7,
era: 6,
year: 5,
month: 4,
day: 2,
weekday: 2,
hour: 1,
minute: 1,
second: 1
};
var MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too
var COMMA_RE = /,/g; // we need re for globalness
var MULTI_SPACE_RE = /\s+/g;
var LTR_RE = /\u200e/g; // control character
var UTC_RE = /UTC|GMT/;
var NativeFormatter = /** @class */ (function () {
function NativeFormatter(formatSettings) {
var standardDateProps = {};
var extendedSettings = {};
var severity = 0;
for (var name_1 in formatSettings) {
if (name_1 in EXTENDED_SETTINGS_AND_SEVERITIES) {
extendedSettings[name_1] = formatSettings[name_1];
severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name_1], severity);
}
else {
standardDateProps[name_1] = formatSettings[name_1];
if (name_1 in STANDARD_DATE_PROP_SEVERITIES) {
severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name_1], severity);
}
}
}
this.standardDateProps = standardDateProps;
this.extendedSettings = extendedSettings;
this.severity = severity;
this.buildFormattingFunc = memoize(buildFormattingFunc);
}
NativeFormatter.prototype.format = function (date, context) {
return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date);
};
NativeFormatter.prototype.formatRange = function (start, end, context) {
var _a = this, standardDateProps = _a.standardDateProps, extendedSettings = _a.extendedSettings;
var diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem);
if (!diffSeverity) {
return this.format(start, context);
}
var biggestUnitForPartial = diffSeverity;
if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time
(standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') &&
(standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') &&
(standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) {
biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time
}
var full0 = this.format(start, context);
var full1 = this.format(end, context);
if (full0 === full1) {
return full0;
}
var partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial);
var partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context);
var partial0 = partialFormattingFunc(start);
var partial1 = partialFormattingFunc(end);
var insertion = findCommonInsertion(full0, partial0, full1, partial1);
var separator = extendedSettings.separator || '';
if (insertion) {
return insertion.before + partial0 + separator + partial1 + insertion.after;
}
return full0 + separator + full1;
};
NativeFormatter.prototype.getLargestUnit = function () {
switch (this.severity) {
case 7:
case 6:
case 5:
return 'year';
case 4:
return 'month';
case 3:
return 'week';
default:
return 'day';
}
};
return NativeFormatter;
}());
function buildFormattingFunc(standardDateProps, extendedSettings, context) {
var standardDatePropCnt = Object.keys(standardDateProps).length;
if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') {
return function (date) {
return formatTimeZoneOffset(date.timeZoneOffset);
};
}
if (standardDatePropCnt === 0 && extendedSettings.week) {
return function (date) {
return formatWeekNumber(context.computeWeekNumber(date.marker), context.weekLabel, context.locale, extendedSettings.week);
};
}
return buildNativeFormattingFunc(standardDateProps, extendedSettings, context);
}
function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) {
standardDateProps = __assign({}, standardDateProps); // copy
extendedSettings = __assign({}, extendedSettings); // copy
sanitizeSettings(standardDateProps, extendedSettings);
standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers
var normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps);
var zeroFormat; // needed?
if (extendedSettings.omitZeroMinute) {
var zeroProps = __assign({}, standardDateProps);
delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings
zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps);
}
return function (date) {
var marker = date.marker;
var format;
if (zeroFormat && !marker.getUTCMinutes()) {
format = zeroFormat;
}
else {
format = normalFormat;
}
var s = format.format(marker);
return postProcess(s, date, standardDateProps, extendedSettings, context);
};
}
function sanitizeSettings(standardDateProps, extendedSettings) {
// deal with a browser inconsistency where formatting the timezone
// requires that the hour/minute be present.
if (standardDateProps.timeZoneName) {
if (!standardDateProps.hour) {
standardDateProps.hour = '2-digit';
}
if (!standardDateProps.minute) {
standardDateProps.minute = '2-digit';
}
}
// only support short timezone names
if (standardDateProps.timeZoneName === 'long') {
standardDateProps.timeZoneName = 'short';
}
// if requesting to display seconds, MUST display minutes
if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) {
delete extendedSettings.omitZeroMinute;
}
}
function postProcess(s, date, standardDateProps, extendedSettings, context) {
s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes
if (standardDateProps.timeZoneName === 'short') {
s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ?
'UTC' : // important to normalize for IE, which does "GMT"
formatTimeZoneOffset(date.timeZoneOffset));
}
if (extendedSettings.omitCommas) {
s = s.replace(COMMA_RE, '').trim();
}
if (extendedSettings.omitZeroMinute) {
s = s.replace(':00', ''); // zeroFormat doesn't always achieve this
}
// ^ do anything that might create adjacent spaces before this point,
// because MERIDIEM_RE likes to eat up loading spaces
if (extendedSettings.meridiem === false) {
s = s.replace(MERIDIEM_RE, '').trim();
}
else if (extendedSettings.meridiem === 'narrow') { // a/p
s = s.replace(MERIDIEM_RE, function (m0, m1) {
return m1.toLocaleLowerCase();
});
}
else if (extendedSettings.meridiem === 'short') { // am/pm
s = s.replace(MERIDIEM_RE, function (m0, m1) {
return m1.toLocaleLowerCase() + 'm';
});
}
else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase
s = s.replace(MERIDIEM_RE, function (m0) {
return m0.toLocaleLowerCase();
});
}
s = s.replace(MULTI_SPACE_RE, ' ');
s = s.trim();
return s;
}
function injectTzoStr(s, tzoStr) {
var replaced = false;
s = s.replace(UTC_RE, function () {
replaced = true;
return tzoStr;
});
// IE11 doesn't include UTC/GMT in the original string, so append to end
if (!replaced) {
s += ' ' + tzoStr;
}
return s;
}
function formatWeekNumber(num, weekLabel, locale, display) {
var parts = [];
if (display === 'narrow') {
parts.push(weekLabel);
}
else if (display === 'short') {
parts.push(weekLabel, ' ');
}
// otherwise, considered 'numeric'
parts.push(locale.simpleNumberFormat.format(num));
if (locale.options.isRtl) { // TODO: use control characters instead?
parts.reverse();
}
return parts.join('');
}
// Range Formatting Utils
// 0 = exactly the same
// 1 = different by time
// and bigger
function computeMarkerDiffSeverity(d0, d1, ca) {
if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) {
return 5;
}
if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) {
return 4;
}
if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) {
return 2;
}
if (timeAsMs(d0) !== timeAsMs(d1)) {
return 1;
}
return 0;
}
function computePartialFormattingOptions(options, biggestUnit) {
var partialOptions = {};
for (var name_2 in options) {
if (!(name_2 in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone)
STANDARD_DATE_PROP_SEVERITIES[name_2] <= biggestUnit) {
partialOptions[name_2] = options[name_2];
}
}
return partialOptions;
}
function findCommonInsertion(full0, partial0, full1, partial1) {
var i0 = 0;
while (i0 < full0.length) {
var found0 = full0.indexOf(partial0, i0);
if (found0 === -1) {
break;
}
var before0 = full0.substr(0, found0);
i0 = found0 + partial0.length;
var after0 = full0.substr(i0);
var i1 = 0;
while (i1 < full1.length) {
var found1 = full1.indexOf(partial1, i1);
if (found1 === -1) {
break;
}
var before1 = full1.substr(0, found1);
i1 = found1 + partial1.length;
var after1 = full1.substr(i1);
if (before0 === before1 && after0 === after1) {
return {
before: before0,
after: after0
};
}
}
}
return null;
}
/*
TODO: fix the terminology of "formatter" vs "formatting func"
*/
/*
At the time of instantiation, this object does not know which cmd-formatting system it will use.
It receives this at the time of formatting, as a setting.
*/
var CmdFormatter = /** @class */ (function () {
function CmdFormatter(cmdStr, separator) {
this.cmdStr = cmdStr;
this.separator = separator;
}
CmdFormatter.prototype.format = function (date, context) {
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, this.separator));
};
CmdFormatter.prototype.formatRange = function (start, end, context) {
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, this.separator));
};
return CmdFormatter;
}());
var FuncFormatter = /** @class */ (function () {
function FuncFormatter(func) {
this.func = func;
}
FuncFormatter.prototype.format = function (date, context) {
return this.func(createVerboseFormattingArg(date, null, context));
};
FuncFormatter.prototype.formatRange = function (start, end, context) {
return this.func(createVerboseFormattingArg(start, end, context));
};
return FuncFormatter;
}());
// Formatter Object Creation
function createFormatter(input, defaultSeparator) {
if (typeof input === 'object' && input) { // non-null object
if (typeof defaultSeparator === 'string') {
input = __assign({ separator: defaultSeparator }, input);
}
return new NativeFormatter(input);
}
else if (typeof input === 'string') {
return new CmdFormatter(input, defaultSeparator);
}
else if (typeof input === 'function') {
return new FuncFormatter(input);
}
}
// String Utils
// timeZoneOffset is in minutes
function buildIsoString(marker, timeZoneOffset, stripZeroTime) {
if (stripZeroTime === void 0) { stripZeroTime = false; }
var s = marker.toISOString();
s = s.replace('.000', '');
if (stripZeroTime) {
s = s.replace('T00:00:00Z', '');
}
if (s.length > 10) { // time part wasn't stripped, can add timezone info
if (timeZoneOffset == null) {
s = s.replace('Z', '');
}
else if (timeZoneOffset !== 0) {
s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true));
}
// otherwise, its UTC-0 and we want to keep the Z
}
return s;
}
function formatIsoTimeString(marker) {
return padStart(marker.getUTCHours(), 2) + ':' +
padStart(marker.getUTCMinutes(), 2) + ':' +
padStart(marker.getUTCSeconds(), 2);
}
function formatTimeZoneOffset(minutes, doIso) {
if (doIso === void 0) { doIso = false; }
var sign = minutes < 0 ? '-' : '+';
var abs = Math.abs(minutes);
var hours = Math.floor(abs / 60);
var mins = Math.round(abs % 60);
if (doIso) {
return sign + padStart(hours, 2) + ':' + padStart(mins, 2);
}
else {
return 'GMT' + sign + hours + (mins ? ':' + padStart(mins, 2) : '');
}
}
// Arg Utils
function createVerboseFormattingArg(start, end, context, separator) {
var startInfo = expandZonedMarker(start, context.calendarSystem);
var endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null;
return {
date: startInfo,
start: startInfo,
end: endInfo,
timeZone: context.timeZone,
localeCodes: context.locale.codes,
separator: separator
};
}
function expandZonedMarker(dateInfo, calendarSystem) {
var a = calendarSystem.markerToArray(dateInfo.marker);
return {
marker: dateInfo.marker,
timeZoneOffset: dateInfo.timeZoneOffset,
array: a,
year: a[0],
month: a[1],
day: a[2],
hour: a[3],
minute: a[4],
second: a[5],
millisecond: a[6]
};
}
var EventSourceApi = /** @class */ (function () {
function EventSourceApi(calendar, internalEventSource) {
this.calendar = calendar;
this.internalEventSource = internalEventSource;
}
EventSourceApi.prototype.remove = function () {
this.calendar.dispatch({
type: 'REMOVE_EVENT_SOURCE',
sourceId: this.internalEventSource.sourceId
});
};
EventSourceApi.prototype.refetch = function () {
this.calendar.dispatch({
type: 'FETCH_EVENT_SOURCES',
sourceIds: [this.internalEventSource.sourceId]
});
};
Object.defineProperty(EventSourceApi.prototype, "id", {
get: function () {
return this.internalEventSource.publicId;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventSourceApi.prototype, "url", {
// only relevant to json-feed event sources
get: function () {
return this.internalEventSource.meta.url;
},
enumerable: true,
configurable: true
});
return EventSourceApi;
}());
var EventApi = /** @class */ (function () {
function EventApi(calendar, def, instance) {
this._calendar = calendar;
this._def = def;
this._instance = instance || null;
}
/*
TODO: make event struct more responsible for this
*/
EventApi.prototype.setProp = function (name, val) {
var _a, _b;
if (name in DATE_PROPS) ;
else if (name in NON_DATE_PROPS) {
if (typeof NON_DATE_PROPS[name] === 'function') {
val = NON_DATE_PROPS[name](val);
}
this.mutate({
standardProps: (_a = {}, _a[name] = val, _a)
});
}
else if (name in UNSCOPED_EVENT_UI_PROPS) {
var ui = void 0;
if (typeof UNSCOPED_EVENT_UI_PROPS[name] === 'function') {
val = UNSCOPED_EVENT_UI_PROPS[name](val);
}
if (name === 'color') {
ui = { backgroundColor: val, borderColor: val };
}
else if (name === 'editable') {
ui = { startEditable: val, durationEditable: val };
}
else {
ui = (_b = {}, _b[name] = val, _b);
}
this.mutate({
standardProps: { ui: ui }
});
}
};
EventApi.prototype.setExtendedProp = function (name, val) {
var _a;
this.mutate({
extendedProps: (_a = {}, _a[name] = val, _a)
});
};
EventApi.prototype.setStart = function (startInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var start = dateEnv.createMarker(startInput);
if (start && this._instance) { // TODO: warning if parsed bad
var instanceRange = this._instance.range;
var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!?
if (options.maintainDuration) {
this.mutate({ datesDelta: startDelta });
}
else {
this.mutate({ startDelta: startDelta });
}
}
};
EventApi.prototype.setEnd = function (endInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var end;
if (endInput != null) {
end = dateEnv.createMarker(endInput);
if (!end) {
return; // TODO: warning if parsed bad
}
}
if (this._instance) {
if (end) {
var endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity);
this.mutate({ endDelta: endDelta });
}
else {
this.mutate({ standardProps: { hasEnd: false } });
}
}
};
EventApi.prototype.setDates = function (startInput, endInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var standardProps = { allDay: options.allDay };
var start = dateEnv.createMarker(startInput);
var end;
if (!start) {
return; // TODO: warning if parsed bad
}
if (endInput != null) {
end = dateEnv.createMarker(endInput);
if (!end) { // TODO: warning if parsed bad
return;
}
}
if (this._instance) {
var instanceRange = this._instance.range;
// when computing the diff for an event being converted to all-day,
// compute diff off of the all-day values the way event-mutation does.
if (options.allDay === true) {
instanceRange = computeAlignedDayRange(instanceRange);
}
var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity);
if (end) {
var endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity);
if (durationsEqual(startDelta, endDelta)) {
this.mutate({ datesDelta: startDelta, standardProps: standardProps });
}
else {
this.mutate({ startDelta: startDelta, endDelta: endDelta, standardProps: standardProps });
}
}
else { // means "clear the end"
standardProps.hasEnd = false;
this.mutate({ datesDelta: startDelta, standardProps: standardProps });
}
}
};
EventApi.prototype.moveStart = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ startDelta: delta });
}
};
EventApi.prototype.moveEnd = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ endDelta: delta });
}
};
EventApi.prototype.moveDates = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ datesDelta: delta });
}
};
EventApi.prototype.setAllDay = function (allDay, options) {
if (options === void 0) { options = {}; }
var standardProps = { allDay: allDay };
var maintainDuration = options.maintainDuration;
if (maintainDuration == null) {
maintainDuration = this._calendar.opt('allDayMaintainDuration');
}
if (this._def.allDay !== allDay) {
standardProps.hasEnd = maintainDuration;
}
this.mutate({ standardProps: standardProps });
};
EventApi.prototype.formatRange = function (formatInput) {
var dateEnv = this._calendar.dateEnv;
var instance = this._instance;
var formatter = createFormatter(formatInput, this._calendar.opt('defaultRangeSeparator'));
if (this._def.hasEnd) {
return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, {
forcedStartTzo: instance.forcedStartTzo,
forcedEndTzo: instance.forcedEndTzo
});
}
else {
return dateEnv.format(instance.range.start, formatter, {
forcedTzo: instance.forcedStartTzo
});
}
};
EventApi.prototype.mutate = function (mutation) {
var def = this._def;
var instance = this._instance;
if (instance) {
this._calendar.dispatch({
type: 'MUTATE_EVENTS',
instanceId: instance.instanceId,
mutation: mutation,
fromApi: true
});
var eventStore = this._calendar.state.eventStore;
this._def = eventStore.defs[def.defId];
this._instance = eventStore.instances[instance.instanceId];
}
};
EventApi.prototype.remove = function () {
this._calendar.dispatch({
type: 'REMOVE_EVENT_DEF',
defId: this._def.defId
});
};
Object.defineProperty(EventApi.prototype, "source", {
get: function () {
var sourceId = this._def.sourceId;
if (sourceId) {
return new EventSourceApi(this._calendar, this._calendar.state.eventSources[sourceId]);
}
return null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "start", {
get: function () {
return this._instance ?
this._calendar.dateEnv.toDate(this._instance.range.start) :
null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "end", {
get: function () {
return (this._instance && this._def.hasEnd) ?
this._calendar.dateEnv.toDate(this._instance.range.end) :
null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "id", {
// computable props that all access the def
// TODO: find a TypeScript-compatible way to do this at scale
get: function () { return this._def.publicId; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "groupId", {
get: function () { return this._def.groupId; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "allDay", {
get: function () { return this._def.allDay; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "title", {
get: function () { return this._def.title; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "url", {
get: function () { return this._def.url; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "rendering", {
get: function () { return this._def.rendering; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "startEditable", {
get: function () { return this._def.ui.startEditable; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "durationEditable", {
get: function () { return this._def.ui.durationEditable; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "constraint", {
get: function () { return this._def.ui.constraints[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "overlap", {
get: function () { return this._def.ui.overlap; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "allow", {
get: function () { return this._def.ui.allows[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "backgroundColor", {
get: function () { return this._def.ui.backgroundColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "borderColor", {
get: function () { return this._def.ui.borderColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "textColor", {
get: function () { return this._def.ui.textColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "classNames", {
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
get: function () { return this._def.ui.classNames; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "extendedProps", {
get: function () { return this._def.extendedProps; },
enumerable: true,
configurable: true
});
return EventApi;
}());
/*
Specifying nextDayThreshold signals that all-day ranges should be sliced.
*/
function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) {
var inverseBgByGroupId = {};
var inverseBgByDefId = {};
var defByGroupId = {};
var bgRanges = [];
var fgRanges = [];
var eventUis = compileEventUis(eventStore.defs, eventUiBases);
for (var defId in eventStore.defs) {
var def = eventStore.defs[defId];
if (def.rendering === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId] = [];
if (!defByGroupId[def.groupId]) {
defByGroupId[def.groupId] = def;
}
}
else {
inverseBgByDefId[defId] = [];
}
}
}
for (var instanceId in eventStore.instances) {
var instance = eventStore.instances[instanceId];
var def = eventStore.defs[instance.defId];
var ui = eventUis[def.defId];
var origRange = instance.range;
var normalRange = (!def.allDay && nextDayThreshold) ?
computeVisibleDayRange(origRange, nextDayThreshold) :
origRange;
var slicedRange = intersectRanges(normalRange, framingRange);
if (slicedRange) {
if (def.rendering === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId].push(slicedRange);
}
else {
inverseBgByDefId[instance.defId].push(slicedRange);
}
}
else {
(def.rendering === 'background' ? bgRanges : fgRanges).push({
def: def,
ui: ui,
instance: instance,
range: slicedRange,
isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(),
isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf()
});
}
}
}
for (var groupId in inverseBgByGroupId) { // BY GROUP
var ranges = inverseBgByGroupId[groupId];
var invertedRanges = invertRanges(ranges, framingRange);
for (var _i = 0, invertedRanges_1 = invertedRanges; _i < invertedRanges_1.length; _i++) {
var invertedRange = invertedRanges_1[_i];
var def = defByGroupId[groupId];
var ui = eventUis[def.defId];
bgRanges.push({
def: def,
ui: ui,
instance: null,
range: invertedRange,
isStart: false,
isEnd: false
});
}
}
for (var defId in inverseBgByDefId) {
var ranges = inverseBgByDefId[defId];
var invertedRanges = invertRanges(ranges, framingRange);
for (var _a = 0, invertedRanges_2 = invertedRanges; _a < invertedRanges_2.length; _a++) {
var invertedRange = invertedRanges_2[_a];
bgRanges.push({
def: eventStore.defs[defId],
ui: eventUis[defId],
instance: null,
range: invertedRange,
isStart: false,
isEnd: false
});
}
}
return { bg: bgRanges, fg: fgRanges };
}
function hasBgRendering(def) {
return def.rendering === 'background' || def.rendering === 'inverse-background';
}
function filterSegsViaEls(view, segs, isMirror) {
if (view.hasPublicHandlers('eventRender')) {
segs = segs.filter(function (seg) {
var custom = view.publiclyTrigger('eventRender', [
{
event: new EventApi(view.calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirror,
isStart: seg.isStart,
isEnd: seg.isEnd,
// TODO: include seg.range once all components consistently generate it
el: seg.el,
view: view
}
]);
if (custom === false) { // means don't render at all
return false;
}
else if (custom && custom !== true) {
seg.el = custom;
}
return true;
});
}
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
setElSeg(seg.el, seg);
}
return segs;
}
function setElSeg(el, seg) {
el.fcSeg = seg;
}
function getElSeg(el) {
return el.fcSeg || null;
}
// event ui computation
function compileEventUis(eventDefs, eventUiBases) {
return mapHash(eventDefs, function (eventDef) {
return compileEventUi(eventDef, eventUiBases);
});
}
function compileEventUi(eventDef, eventUiBases) {
var uis = [];
if (eventUiBases['']) {
uis.push(eventUiBases['']);
}
if (eventUiBases[eventDef.defId]) {
uis.push(eventUiBases[eventDef.defId]);
}
uis.push(eventDef.ui);
return combineEventUis(uis);
}
// applies the mutation to ALL defs/instances within the event store
function applyMutationToEventStore(eventStore, eventConfigBase, mutation, calendar) {
var eventConfigs = compileEventUis(eventStore.defs, eventConfigBase);
var dest = createEmptyEventStore();
for (var defId in eventStore.defs) {
var def = eventStore.defs[defId];
dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, calendar.pluginSystem.hooks.eventDefMutationAppliers, calendar);
}
for (var instanceId in eventStore.instances) {
var instance = eventStore.instances[instanceId];
var def = dest.defs[instance.defId]; // important to grab the newly modified def
dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, calendar);
}
return dest;
}
function applyMutationToEventDef(eventDef, eventConfig, mutation, appliers, calendar) {
var standardProps = mutation.standardProps || {};
// if hasEnd has not been specified, guess a good value based on deltas.
// if duration will change, there's no way the default duration will persist,
// and thus, we need to mark the event as having a real end
if (standardProps.hasEnd == null &&
eventConfig.durationEditable &&
(mutation.startDelta || mutation.endDelta)) {
standardProps.hasEnd = true; // TODO: is this mutation okay?
}
var copy = __assign({}, eventDef, standardProps, { ui: __assign({}, eventDef.ui, standardProps.ui) });
if (mutation.extendedProps) {
copy.extendedProps = __assign({}, copy.extendedProps, mutation.extendedProps);
}
for (var _i = 0, appliers_1 = appliers; _i < appliers_1.length; _i++) {
var applier = appliers_1[_i];
applier(copy, mutation, calendar);
}
if (!copy.hasEnd && calendar.opt('forceEventDuration')) {
copy.hasEnd = true;
}
return copy;
}
function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef
eventConfig, mutation, calendar) {
var dateEnv = calendar.dateEnv;
var forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true;
var clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false;
var copy = __assign({}, eventInstance);
if (forceAllDay) {
copy.range = computeAlignedDayRange(copy.range);
}
if (mutation.datesDelta && eventConfig.startEditable) {
copy.range = {
start: dateEnv.add(copy.range.start, mutation.datesDelta),
end: dateEnv.add(copy.range.end, mutation.datesDelta)
};
}
if (mutation.startDelta && eventConfig.durationEditable) {
copy.range = {
start: dateEnv.add(copy.range.start, mutation.startDelta),
end: copy.range.end
};
}
if (mutation.endDelta && eventConfig.durationEditable) {
copy.range = {
start: copy.range.start,
end: dateEnv.add(copy.range.end, mutation.endDelta)
};
}
if (clearEnd) {
copy.range = {
start: copy.range.start,
end: calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start)
};
}
// in case event was all-day but the supplied deltas were not
// better util for this?
if (eventDef.allDay) {
copy.range = {
start: startOfDay(copy.range.start),
end: startOfDay(copy.range.end)
};
}
// handle invalid durations
if (copy.range.end < copy.range.start) {
copy.range.end = calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start);
}
return copy;
}
function reduceEventStore (eventStore, action, eventSources, dateProfile, calendar) {
switch (action.type) {
case 'RECEIVE_EVENTS': // raw
return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, calendar);
case 'ADD_EVENTS': // already parsed, but not expanded
return addEvent(eventStore, action.eventStore, // new ones
dateProfile ? dateProfile.activeRange : null, calendar);
case 'MERGE_EVENTS': // already parsed and expanded
return mergeEventStores(eventStore, action.eventStore);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
if (dateProfile) {
return expandRecurring(eventStore, dateProfile.activeRange, calendar);
}
else {
return eventStore;
}
case 'CHANGE_TIMEZONE':
return rezoneDates(eventStore, action.oldDateEnv, calendar.dateEnv);
case 'MUTATE_EVENTS':
return applyMutationToRelated(eventStore, action.instanceId, action.mutation, action.fromApi, calendar);
case 'REMOVE_EVENT_INSTANCES':
return excludeInstances(eventStore, action.instances);
case 'REMOVE_EVENT_DEF':
return filterEventStoreDefs(eventStore, function (eventDef) {
return eventDef.defId !== action.defId;
});
case 'REMOVE_EVENT_SOURCE':
return excludeEventsBySourceId(eventStore, action.sourceId);
case 'REMOVE_ALL_EVENT_SOURCES':
return filterEventStoreDefs(eventStore, function (eventDef) {
return !eventDef.sourceId; // only keep events with no source id
});
case 'REMOVE_ALL_EVENTS':
return createEmptyEventStore();
case 'RESET_EVENTS':
return {
defs: eventStore.defs,
instances: eventStore.instances
};
default:
return eventStore;
}
}
function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, calendar) {
if (eventSource && // not already removed
fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
) {
var subset = parseEvents(transformRawEvents(rawEvents, eventSource, calendar), eventSource.sourceId, calendar);
if (fetchRange) {
subset = expandRecurring(subset, fetchRange, calendar);
}
return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset);
}
return eventStore;
}
function addEvent(eventStore, subset, expandRange, calendar) {
if (expandRange) {
subset = expandRecurring(subset, expandRange, calendar);
}
return mergeEventStores(eventStore, subset);
}
function rezoneDates(eventStore, oldDateEnv, newDateEnv) {
var defs = eventStore.defs;
var instances = mapHash(eventStore.instances, function (instance) {
var def = defs[instance.defId];
if (def.allDay || def.recurringDef) {
return instance; // isn't dependent on timezone
}
else {
return __assign({}, instance, { range: {
start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)),
end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo))
}, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo });
}
});
return { defs: defs, instances: instances };
}
function applyMutationToRelated(eventStore, instanceId, mutation, fromApi, calendar) {
var relevant = getRelevantEvents(eventStore, instanceId);
var eventConfigBase = fromApi ?
{ '': {
startEditable: true,
durationEditable: true,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: []
} } :
calendar.eventUiBases;
relevant = applyMutationToEventStore(relevant, eventConfigBase, mutation, calendar);
return mergeEventStores(eventStore, relevant);
}
function excludeEventsBySourceId(eventStore, sourceId) {
return filterEventStoreDefs(eventStore, function (eventDef) {
return eventDef.sourceId !== sourceId;
});
}
// QUESTION: why not just return instances? do a general object-property-exclusion util
function excludeInstances(eventStore, removals) {
return {
defs: eventStore.defs,
instances: filterHash(eventStore.instances, function (instance) {
return !removals[instance.instanceId];
})
};
}
// high-level segmenting-aware tester functions
// ------------------------------------------------------------------------------------------------------------------------
function isInteractionValid(interaction, calendar) {
return isNewPropsValid({ eventDrag: interaction }, calendar); // HACK: the eventDrag props is used for ALL interactions
}
function isDateSelectionValid(dateSelection, calendar) {
return isNewPropsValid({ dateSelection: dateSelection }, calendar);
}
function isNewPropsValid(newProps, calendar) {
var view = calendar.view;
var props = __assign({ businessHours: view ? view.props.businessHours : createEmptyEventStore(), dateSelection: '', eventStore: calendar.state.eventStore, eventUiBases: calendar.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps);
return (calendar.pluginSystem.hooks.isPropsValid || isPropsValid)(props, calendar);
}
function isPropsValid(state, calendar, dateSpanMeta, filterConfig) {
if (dateSpanMeta === void 0) { dateSpanMeta = {}; }
if (state.eventDrag && !isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
return false;
}
if (state.dateSelection && !isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
return false;
}
return true;
}
// Moving Event Validation
// ------------------------------------------------------------------------------------------------------------------------
function isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig) {
var interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions
var subjectEventStore = interaction.mutatedEvents;
var subjectDefs = subjectEventStore.defs;
var subjectInstances = subjectEventStore.instances;
var subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ?
state.eventUiBases :
{ '': calendar.selectionConfig } // if not a real event, validate as a selection
);
if (filterConfig) {
subjectConfigs = mapHash(subjectConfigs, filterConfig);
}
var otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances); // exclude the subject events. TODO: exclude defs too?
var otherDefs = otherEventStore.defs;
var otherInstances = otherEventStore.instances;
var otherConfigs = compileEventUis(otherDefs, state.eventUiBases);
for (var subjectInstanceId in subjectInstances) {
var subjectInstance = subjectInstances[subjectInstanceId];
var subjectRange = subjectInstance.range;
var subjectConfig = subjectConfigs[subjectInstance.defId];
var subjectDef = subjectDefs[subjectInstance.defId];
// constraint
if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, calendar)) {
return false;
}
// overlap
var overlapFunc = calendar.opt('eventOverlap');
if (typeof overlapFunc !== 'function') {
overlapFunc = null;
}
for (var otherInstanceId in otherInstances) {
var otherInstance = otherInstances[otherInstanceId];
// intersect! evaluate
if (rangesIntersect(subjectRange, otherInstance.range)) {
var otherOverlap = otherConfigs[otherInstance.defId].overlap;
// consider the other event's overlap. only do this if the subject event is a "real" event
if (otherOverlap === false && interaction.isEvent) {
return false;
}
if (subjectConfig.overlap === false) {
return false;
}
if (overlapFunc && !overlapFunc(new EventApi(calendar, otherDefs[otherInstance.defId], otherInstance), // still event
new EventApi(calendar, subjectDef, subjectInstance) // moving event
)) {
return false;
}
}
}
// allow (a function)
var calendarEventStore = calendar.state.eventStore; // need global-to-calendar, not local to component (splittable)state
for (var _i = 0, _a = subjectConfig.allows; _i < _a.length; _i++) {
var subjectAllow = _a[_i];
var subjectDateSpan = __assign({}, dateSpanMeta, { range: subjectInstance.range, allDay: subjectDef.allDay });
var origDef = calendarEventStore.defs[subjectDef.defId];
var origInstance = calendarEventStore.instances[subjectInstanceId];
var eventApi = void 0;
if (origDef) { // was previously in the calendar
eventApi = new EventApi(calendar, origDef, origInstance);
}
else { // was an external event
eventApi = new EventApi(calendar, subjectDef); // no instance, because had no dates
}
if (!subjectAllow(calendar.buildDateSpanApi(subjectDateSpan), eventApi)) {
return false;
}
}
}
return true;
}
// Date Selection Validation
// ------------------------------------------------------------------------------------------------------------------------
function isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig) {
var relevantEventStore = state.eventStore;
var relevantDefs = relevantEventStore.defs;
var relevantInstances = relevantEventStore.instances;
var selection = state.dateSelection;
var selectionRange = selection.range;
var selectionConfig = calendar.selectionConfig;
if (filterConfig) {
selectionConfig = filterConfig(selectionConfig);
}
// constraint
if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, calendar)) {
return false;
}
// overlap
var overlapFunc = calendar.opt('selectOverlap');
if (typeof overlapFunc !== 'function') {
overlapFunc = null;
}
for (var relevantInstanceId in relevantInstances) {
var relevantInstance = relevantInstances[relevantInstanceId];
// intersect! evaluate
if (rangesIntersect(selectionRange, relevantInstance.range)) {
if (selectionConfig.overlap === false) {
return false;
}
if (overlapFunc && !overlapFunc(new EventApi(calendar, relevantDefs[relevantInstance.defId], relevantInstance))) {
return false;
}
}
}
// allow (a function)
for (var _i = 0, _a = selectionConfig.allows; _i < _a.length; _i++) {
var selectionAllow = _a[_i];
var fullDateSpan = __assign({}, dateSpanMeta, selection);
if (!selectionAllow(calendar.buildDateSpanApi(fullDateSpan), null)) {
return false;
}
}
return true;
}
// Constraint Utils
// ------------------------------------------------------------------------------------------------------------------------
function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, calendar) {
for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) {
var constraint = constraints_1[_i];
if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar), subjectRange)) {
return false;
}
}
return true;
}
function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours
otherEventStore, // for if constraint is an even group ID
businessHoursUnexpanded, // for if constraint is 'businessHours'
calendar // for expanding businesshours
) {
if (constraint === 'businessHours') {
return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, calendar));
}
else if (typeof constraint === 'string') { // an group ID
return eventStoreToRanges(filterEventStoreDefs(otherEventStore, function (eventDef) {
return eventDef.groupId === constraint;
}));
}
else if (typeof constraint === 'object' && constraint) { // non-null object
return eventStoreToRanges(expandRecurring(constraint, subjectRange, calendar));
}
return []; // if it's false
}
// TODO: move to event-store file?
function eventStoreToRanges(eventStore) {
var instances = eventStore.instances;
var ranges = [];
for (var instanceId in instances) {
ranges.push(instances[instanceId].range);
}
return ranges;
}
// TODO: move to geom file?
function anyRangesContainRange(outerRanges, innerRange) {
for (var _i = 0, outerRanges_1 = outerRanges; _i < outerRanges_1.length; _i++) {
var outerRange = outerRanges_1[_i];
if (rangeContainsRange(outerRange, innerRange)) {
return true;
}
}
return false;
}
// Parsing
// ------------------------------------------------------------------------------------------------------------------------
function normalizeConstraint(input, calendar) {
if (Array.isArray(input)) {
return parseEvents(input, '', calendar, true); // allowOpenRange=true
}
else if (typeof input === 'object' && input) { // non-null object
return parseEvents([input], '', calendar, true); // allowOpenRange=true
}
else if (input != null) {
return String(input);
}
else {
return null;
}
}
function htmlEscape(s) {
return (s + '').replace(/&/g, '&')
.replace(//g, '>')
.replace(/'/g, ''')
.replace(/"/g, '"')
.replace(/\n/g, '
');
}
// Given a hash of CSS properties, returns a string of CSS.
// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
function cssToStr(cssProps) {
var statements = [];
for (var name_1 in cssProps) {
var val = cssProps[name_1];
if (val != null && val !== '') {
statements.push(name_1 + ':' + val);
}
}
return statements.join(';');
}
// Given an object hash of HTML attribute names to values,
// generates a string that can be injected between < > in HTML
function attrsToStr(attrs) {
var parts = [];
for (var name_2 in attrs) {
var val = attrs[name_2];
if (val != null) {
parts.push(name_2 + '="' + htmlEscape(val) + '"');
}
}
return parts.join(' ');
}
function parseClassName(raw) {
if (Array.isArray(raw)) {
return raw;
}
else if (typeof raw === 'string') {
return raw.split(/\s+/);
}
else {
return [];
}
}
var UNSCOPED_EVENT_UI_PROPS = {
editable: Boolean,
startEditable: Boolean,
durationEditable: Boolean,
constraint: null,
overlap: null,
allow: null,
className: parseClassName,
classNames: parseClassName,
color: String,
backgroundColor: String,
borderColor: String,
textColor: String
};
function processUnscopedUiProps(rawProps, calendar, leftovers) {
var props = refineProps(rawProps, UNSCOPED_EVENT_UI_PROPS, {}, leftovers);
var constraint = normalizeConstraint(props.constraint, calendar);
return {
startEditable: props.startEditable != null ? props.startEditable : props.editable,
durationEditable: props.durationEditable != null ? props.durationEditable : props.editable,
constraints: constraint != null ? [constraint] : [],
overlap: props.overlap,
allows: props.allow != null ? [props.allow] : [],
backgroundColor: props.backgroundColor || props.color,
borderColor: props.borderColor || props.color,
textColor: props.textColor,
classNames: props.classNames.concat(props.className)
};
}
function processScopedUiProps(prefix, rawScoped, calendar, leftovers) {
var rawUnscoped = {};
var wasFound = {};
for (var key in UNSCOPED_EVENT_UI_PROPS) {
var scopedKey = prefix + capitaliseFirstLetter(key);
rawUnscoped[key] = rawScoped[scopedKey];
wasFound[scopedKey] = true;
}
if (prefix === 'event') {
rawUnscoped.editable = rawScoped.editable; // special case. there is no 'eventEditable', just 'editable'
}
if (leftovers) {
for (var key in rawScoped) {
if (!wasFound[key]) {
leftovers[key] = rawScoped[key];
}
}
}
return processUnscopedUiProps(rawUnscoped, calendar);
}
var EMPTY_EVENT_UI = {
startEditable: null,
durationEditable: null,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: []
};
// prevent against problems with <2 args!
function combineEventUis(uis) {
return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI);
}
function combineTwoEventUis(item0, item1) {
return {
startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable,
durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable,
constraints: item0.constraints.concat(item1.constraints),
overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap,
allows: item0.allows.concat(item1.allows),
backgroundColor: item1.backgroundColor || item0.backgroundColor,
borderColor: item1.borderColor || item0.borderColor,
textColor: item1.textColor || item0.textColor,
classNames: item0.classNames.concat(item1.classNames)
};
}
var NON_DATE_PROPS = {
id: String,
groupId: String,
title: String,
url: String,
rendering: String,
extendedProps: null
};
var DATE_PROPS = {
start: null,
date: null,
end: null,
allDay: null
};
var uid = 0;
function parseEvent(raw, sourceId, calendar, allowOpenRange) {
var allDayDefault = computeIsAllDayDefault(sourceId, calendar);
var leftovers0 = {};
var recurringRes = parseRecurring(raw, // raw, but with single-event stuff stripped out
allDayDefault, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes, leftovers0 // will populate with non-recurring props
);
if (recurringRes) {
var def = parseEventDef(leftovers0, sourceId, recurringRes.allDay, Boolean(recurringRes.duration), calendar);
def.recurringDef = {
typeId: recurringRes.typeId,
typeData: recurringRes.typeData,
duration: recurringRes.duration
};
return { def: def, instance: null };
}
else {
var leftovers1 = {};
var singleRes = parseSingle(raw, allDayDefault, calendar, leftovers1, allowOpenRange);
if (singleRes) {
var def = parseEventDef(leftovers1, sourceId, singleRes.allDay, singleRes.hasEnd, calendar);
var instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo);
return { def: def, instance: instance };
}
}
return null;
}
/*
Will NOT populate extendedProps with the leftover properties.
Will NOT populate date-related props.
The EventNonDateInput has been normalized (id => publicId, etc).
*/
function parseEventDef(raw, sourceId, allDay, hasEnd, calendar) {
var leftovers = {};
var def = pluckNonDateProps(raw, calendar, leftovers);
def.defId = String(uid++);
def.sourceId = sourceId;
def.allDay = allDay;
def.hasEnd = hasEnd;
for (var _i = 0, _a = calendar.pluginSystem.hooks.eventDefParsers; _i < _a.length; _i++) {
var eventDefParser = _a[_i];
var newLeftovers = {};
eventDefParser(def, leftovers, newLeftovers);
leftovers = newLeftovers;
}
def.extendedProps = __assign(leftovers, def.extendedProps || {});
// help out EventApi from having user modify props
Object.freeze(def.ui.classNames);
Object.freeze(def.extendedProps);
return def;
}
function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) {
return {
instanceId: String(uid++),
defId: defId,
range: range,
forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo,
forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo
};
}
function parseSingle(raw, allDayDefault, calendar, leftovers, allowOpenRange) {
var props = pluckDateProps(raw, leftovers);
var allDay = props.allDay;
var startMeta;
var startMarker = null;
var hasEnd = false;
var endMeta;
var endMarker = null;
startMeta = calendar.dateEnv.createMarkerMeta(props.start);
if (startMeta) {
startMarker = startMeta.marker;
}
else if (!allowOpenRange) {
return null;
}
if (props.end != null) {
endMeta = calendar.dateEnv.createMarkerMeta(props.end);
}
if (allDay == null) {
if (allDayDefault != null) {
allDay = allDayDefault;
}
else {
// fall back to the date props LAST
allDay = (!startMeta || startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
}
if (allDay && startMarker) {
startMarker = startOfDay(startMarker);
}
if (endMeta) {
endMarker = endMeta.marker;
if (allDay) {
endMarker = startOfDay(endMarker);
}
if (startMarker && endMarker <= startMarker) {
endMarker = null;
}
}
if (endMarker) {
hasEnd = true;
}
else if (!allowOpenRange) {
hasEnd = calendar.opt('forceEventDuration') || false;
endMarker = calendar.dateEnv.add(startMarker, allDay ?
calendar.defaultAllDayEventDuration :
calendar.defaultTimedEventDuration);
}
return {
allDay: allDay,
hasEnd: hasEnd,
range: { start: startMarker, end: endMarker },
forcedStartTzo: startMeta ? startMeta.forcedTzo : null,
forcedEndTzo: endMeta ? endMeta.forcedTzo : null
};
}
function pluckDateProps(raw, leftovers) {
var props = refineProps(raw, DATE_PROPS, {}, leftovers);
props.start = (props.start !== null) ? props.start : props.date;
delete props.date;
return props;
}
function pluckNonDateProps(raw, calendar, leftovers) {
var preLeftovers = {};
var props = refineProps(raw, NON_DATE_PROPS, {}, preLeftovers);
var ui = processUnscopedUiProps(preLeftovers, calendar, leftovers);
props.publicId = props.id;
delete props.id;
props.ui = ui;
return props;
}
function computeIsAllDayDefault(sourceId, calendar) {
var res = null;
if (sourceId) {
var source = calendar.state.eventSources[sourceId];
res = source.allDayDefault;
}
if (res == null) {
res = calendar.opt('allDayDefault');
}
return res;
}
var DEF_DEFAULTS = {
startTime: '09:00',
endTime: '17:00',
daysOfWeek: [1, 2, 3, 4, 5],
rendering: 'inverse-background',
classNames: 'fc-nonbusiness',
groupId: '_businessHours' // so multiple defs get grouped
};
/*
TODO: pass around as EventDefHash!!!
*/
function parseBusinessHours(input, calendar) {
return parseEvents(refineInputs(input), '', calendar);
}
function refineInputs(input) {
var rawDefs;
if (input === true) {
rawDefs = [{}]; // will get DEF_DEFAULTS verbatim
}
else if (Array.isArray(input)) {
// if specifying an array, every sub-definition NEEDS a day-of-week
rawDefs = input.filter(function (rawDef) {
return rawDef.daysOfWeek;
});
}
else if (typeof input === 'object' && input) { // non-null object
rawDefs = [input];
}
else { // is probably false
rawDefs = [];
}
rawDefs = rawDefs.map(function (rawDef) {
return __assign({}, DEF_DEFAULTS, rawDef);
});
return rawDefs;
}
function memoizeRendering(renderFunc, unrenderFunc, dependencies) {
if (dependencies === void 0) { dependencies = []; }
var dependents = [];
var thisContext;
var prevArgs;
function unrender() {
if (prevArgs) {
for (var _i = 0, dependents_1 = dependents; _i < dependents_1.length; _i++) {
var dependent = dependents_1[_i];
dependent.unrender();
}
if (unrenderFunc) {
unrenderFunc.apply(thisContext, prevArgs);
}
prevArgs = null;
}
}
function res() {
if (!prevArgs || !isArraysEqual(prevArgs, arguments)) {
unrender();
thisContext = this;
prevArgs = arguments;
renderFunc.apply(this, arguments);
}
}
res.dependents = dependents;
res.unrender = unrender;
for (var _i = 0, dependencies_1 = dependencies; _i < dependencies_1.length; _i++) {
var dependency = dependencies_1[_i];
dependency.dependents.push(res);
}
return res;
}
var EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere
var Splitter = /** @class */ (function () {
function Splitter() {
this.getKeysForEventDefs = memoize(this._getKeysForEventDefs);
this.splitDateSelection = memoize(this._splitDateSpan);
this.splitEventStore = memoize(this._splitEventStore);
this.splitIndividualUi = memoize(this._splitIndividualUi);
this.splitEventDrag = memoize(this._splitInteraction);
this.splitEventResize = memoize(this._splitInteraction);
this.eventUiBuilders = {}; // TODO: typescript protection
}
Splitter.prototype.splitProps = function (props) {
var _this = this;
var keyInfos = this.getKeyInfo(props);
var defKeys = this.getKeysForEventDefs(props.eventStore);
var dateSelections = this.splitDateSelection(props.dateSelection);
var individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases*
var eventStores = this.splitEventStore(props.eventStore, defKeys);
var eventDrags = this.splitEventDrag(props.eventDrag);
var eventResizes = this.splitEventResize(props.eventResize);
var splitProps = {};
this.eventUiBuilders = mapHash(keyInfos, function (info, key) {
return _this.eventUiBuilders[key] || memoize(buildEventUiForKey);
});
for (var key in keyInfos) {
var keyInfo = keyInfos[key];
var eventStore = eventStores[key] || EMPTY_EVENT_STORE;
var buildEventUi = this.eventUiBuilders[key];
splitProps[key] = {
businessHours: keyInfo.businessHours || props.businessHours,
dateSelection: dateSelections[key] || null,
eventStore: eventStore,
eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
eventDrag: eventDrags[key] || null,
eventResize: eventResizes[key] || null
};
}
return splitProps;
};
Splitter.prototype._splitDateSpan = function (dateSpan) {
var dateSpans = {};
if (dateSpan) {
var keys = this.getKeysForDateSpan(dateSpan);
for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
var key = keys_1[_i];
dateSpans[key] = dateSpan;
}
}
return dateSpans;
};
Splitter.prototype._getKeysForEventDefs = function (eventStore) {
var _this = this;
return mapHash(eventStore.defs, function (eventDef) {
return _this.getKeysForEventDef(eventDef);
});
};
Splitter.prototype._splitEventStore = function (eventStore, defKeys) {
var defs = eventStore.defs, instances = eventStore.instances;
var splitStores = {};
for (var defId in defs) {
for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
var key = _a[_i];
if (!splitStores[key]) {
splitStores[key] = createEmptyEventStore();
}
splitStores[key].defs[defId] = defs[defId];
}
}
for (var instanceId in instances) {
var instance = instances[instanceId];
for (var _b = 0, _c = defKeys[instance.defId]; _b < _c.length; _b++) {
var key = _c[_b];
if (splitStores[key]) { // must have already been created
splitStores[key].instances[instanceId] = instance;
}
}
}
return splitStores;
};
Splitter.prototype._splitIndividualUi = function (eventUiBases, defKeys) {
var splitHashes = {};
for (var defId in eventUiBases) {
if (defId) { // not the '' key
for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
var key = _a[_i];
if (!splitHashes[key]) {
splitHashes[key] = {};
}
splitHashes[key][defId] = eventUiBases[defId];
}
}
}
return splitHashes;
};
Splitter.prototype._splitInteraction = function (interaction) {
var splitStates = {};
if (interaction) {
var affectedStores_1 = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents) // can't use cached. might be events from other calendar
);
// can't rely on defKeys because event data is mutated
var mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents);
var mutatedStores_1 = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId);
var populate = function (key) {
if (!splitStates[key]) {
splitStates[key] = {
affectedEvents: affectedStores_1[key] || EMPTY_EVENT_STORE,
mutatedEvents: mutatedStores_1[key] || EMPTY_EVENT_STORE,
isEvent: interaction.isEvent,
origSeg: interaction.origSeg
};
}
};
for (var key in affectedStores_1) {
populate(key);
}
for (var key in mutatedStores_1) {
populate(key);
}
}
return splitStates;
};
return Splitter;
}());
function buildEventUiForKey(allUi, eventUiForKey, individualUi) {
var baseParts = [];
if (allUi) {
baseParts.push(allUi);
}
if (eventUiForKey) {
baseParts.push(eventUiForKey);
}
var stuff = {
'': combineEventUis(baseParts)
};
if (individualUi) {
__assign(stuff, individualUi);
}
return stuff;
}
// Generates HTML for an anchor to another view into the calendar.
// Will either generate an tag or a non-clickable tag, depending on enabled settings.
// `gotoOptions` can either be a DateMarker, or an object with the form:
// { date, type, forceOff }
// `type` is a view-type like "day" or "week". default value is "day".
// `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
function buildGotoAnchorHtml(component, gotoOptions, attrs, innerHtml) {
var dateEnv = component.dateEnv;
var date;
var type;
var forceOff;
var finalOptions;
if (gotoOptions instanceof Date) {
date = gotoOptions; // a single date-like input
}
else {
date = gotoOptions.date;
type = gotoOptions.type;
forceOff = gotoOptions.forceOff;
}
finalOptions = {
date: dateEnv.formatIso(date, { omitTime: true }),
type: type || 'day'
};
if (typeof attrs === 'string') {
innerHtml = attrs;
attrs = null;
}
attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
innerHtml = innerHtml || '';
if (!forceOff && component.opt('navLinks')) {
return '' +
innerHtml +
'';
}
else {
return '' +
innerHtml +
'';
}
}
function getAllDayHtml(component) {
return component.opt('allDayHtml') || htmlEscape(component.opt('allDayText'));
}
// Computes HTML classNames for a single-day element
function getDayClasses(date, dateProfile, context, noThemeHighlight) {
var calendar = context.calendar, view = context.view, theme = context.theme, dateEnv = context.dateEnv;
var classes = [];
var todayStart;
var todayEnd;
if (!rangeContainsMarker(dateProfile.activeRange, date)) {
classes.push('fc-disabled-day');
}
else {
classes.push('fc-' + DAY_IDS[date.getUTCDay()]);
if (view.opt('monthMode') &&
dateEnv.getMonth(date) !== dateEnv.getMonth(dateProfile.currentRange.start)) {
classes.push('fc-other-month');
}
todayStart = startOfDay(calendar.getNow());
todayEnd = addDays(todayStart, 1);
if (date < todayStart) {
classes.push('fc-past');
}
else if (date >= todayEnd) {
classes.push('fc-future');
}
else {
classes.push('fc-today');
if (noThemeHighlight !== true) {
classes.push(theme.getClass('today'));
}
}
}
return classes;
}
// given a function that resolves a result asynchronously.
// the function can either call passed-in success and failure callbacks,
// or it can return a promise.
// if you need to pass additional params to func, bind them first.
function unpromisify(func, success, failure) {
// guard against success/failure callbacks being called more than once
// and guard against a promise AND callback being used together.
var isResolved = false;
var wrappedSuccess = function () {
if (!isResolved) {
isResolved = true;
success.apply(this, arguments);
}
};
var wrappedFailure = function () {
if (!isResolved) {
isResolved = true;
if (failure) {
failure.apply(this, arguments);
}
}
};
var res = func(wrappedSuccess, wrappedFailure);
if (res && typeof res.then === 'function') {
res.then(wrappedSuccess, wrappedFailure);
}
}
var Mixin = /** @class */ (function () {
function Mixin() {
}
// mix into a CLASS
Mixin.mixInto = function (destClass) {
this.mixIntoObj(destClass.prototype);
};
// mix into ANY object
Mixin.mixIntoObj = function (destObj) {
var _this = this;
Object.getOwnPropertyNames(this.prototype).forEach(function (name) {
if (!destObj[name]) { // if destination doesn't already define it
destObj[name] = _this.prototype[name];
}
});
};
/*
will override existing methods
TODO: remove! not used anymore
*/
Mixin.mixOver = function (destClass) {
var _this = this;
Object.getOwnPropertyNames(this.prototype).forEach(function (name) {
destClass.prototype[name] = _this.prototype[name];
});
};
return Mixin;
}());
/*
USAGE:
import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
in class:
on: EmitterInterface['on']
one: EmitterInterface['one']
off: EmitterInterface['off']
trigger: EmitterInterface['trigger']
triggerWith: EmitterInterface['triggerWith']
hasHandlers: EmitterInterface['hasHandlers']
after class:
EmitterMixin.mixInto(TheClass)
*/
var EmitterMixin = /** @class */ (function (_super) {
__extends(EmitterMixin, _super);
function EmitterMixin() {
return _super !== null && _super.apply(this, arguments) || this;
}
EmitterMixin.prototype.on = function (type, handler) {
addToHash(this._handlers || (this._handlers = {}), type, handler);
return this; // for chaining
};
// todo: add comments
EmitterMixin.prototype.one = function (type, handler) {
addToHash(this._oneHandlers || (this._oneHandlers = {}), type, handler);
return this; // for chaining
};
EmitterMixin.prototype.off = function (type, handler) {
if (this._handlers) {
removeFromHash(this._handlers, type, handler);
}
if (this._oneHandlers) {
removeFromHash(this._oneHandlers, type, handler);
}
return this; // for chaining
};
EmitterMixin.prototype.trigger = function (type) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
this.triggerWith(type, this, args);
return this; // for chaining
};
EmitterMixin.prototype.triggerWith = function (type, context, args) {
if (this._handlers) {
applyAll(this._handlers[type], context, args);
}
if (this._oneHandlers) {
applyAll(this._oneHandlers[type], context, args);
delete this._oneHandlers[type]; // will never fire again
}
return this; // for chaining
};
EmitterMixin.prototype.hasHandlers = function (type) {
return (this._handlers && this._handlers[type] && this._handlers[type].length) ||
(this._oneHandlers && this._oneHandlers[type] && this._oneHandlers[type].length);
};
return EmitterMixin;
}(Mixin));
function addToHash(hash, type, handler) {
(hash[type] || (hash[type] = []))
.push(handler);
}
function removeFromHash(hash, type, handler) {
if (handler) {
if (hash[type]) {
hash[type] = hash[type].filter(function (func) {
return func !== handler;
});
}
}
else {
delete hash[type]; // remove all handler funcs for this type
}
}
/*
Records offset information for a set of elements, relative to an origin element.
Can record the left/right OR the top/bottom OR both.
Provides methods for querying the cache by position.
*/
var PositionCache = /** @class */ (function () {
function PositionCache(originEl, els, isHorizontal, isVertical) {
this.originEl = originEl;
this.els = els;
this.isHorizontal = isHorizontal;
this.isVertical = isVertical;
}
// Queries the els for coordinates and stores them.
// Call this method before using and of the get* methods below.
PositionCache.prototype.build = function () {
var originEl = this.originEl;
var originClientRect = this.originClientRect =
originEl.getBoundingClientRect(); // relative to viewport top-left
if (this.isHorizontal) {
this.buildElHorizontals(originClientRect.left);
}
if (this.isVertical) {
this.buildElVerticals(originClientRect.top);
}
};
// Populates the left/right internal coordinate arrays
PositionCache.prototype.buildElHorizontals = function (originClientLeft) {
var lefts = [];
var rights = [];
for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
var el = _a[_i];
var rect = el.getBoundingClientRect();
lefts.push(rect.left - originClientLeft);
rights.push(rect.right - originClientLeft);
}
this.lefts = lefts;
this.rights = rights;
};
// Populates the top/bottom internal coordinate arrays
PositionCache.prototype.buildElVerticals = function (originClientTop) {
var tops = [];
var bottoms = [];
for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
var el = _a[_i];
var rect = el.getBoundingClientRect();
tops.push(rect.top - originClientTop);
bottoms.push(rect.bottom - originClientTop);
}
this.tops = tops;
this.bottoms = bottoms;
};
// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
// If no intersection is made, returns undefined.
PositionCache.prototype.leftToIndex = function (leftPosition) {
var lefts = this.lefts;
var rights = this.rights;
var len = lefts.length;
var i;
for (i = 0; i < len; i++) {
if (leftPosition >= lefts[i] && leftPosition < rights[i]) {
return i;
}
}
};
// Given a top offset (from document top), returns the index of the el that it vertically intersects.
// If no intersection is made, returns undefined.
PositionCache.prototype.topToIndex = function (topPosition) {
var tops = this.tops;
var bottoms = this.bottoms;
var len = tops.length;
var i;
for (i = 0; i < len; i++) {
if (topPosition >= tops[i] && topPosition < bottoms[i]) {
return i;
}
}
};
// Gets the width of the element at the given index
PositionCache.prototype.getWidth = function (leftIndex) {
return this.rights[leftIndex] - this.lefts[leftIndex];
};
// Gets the height of the element at the given index
PositionCache.prototype.getHeight = function (topIndex) {
return this.bottoms[topIndex] - this.tops[topIndex];
};
return PositionCache;
}());
/*
An object for getting/setting scroll-related information for an element.
Internally, this is done very differently for window versus DOM element,
so this object serves as a common interface.
*/
var ScrollController = /** @class */ (function () {
function ScrollController() {
}
ScrollController.prototype.getMaxScrollTop = function () {
return this.getScrollHeight() - this.getClientHeight();
};
ScrollController.prototype.getMaxScrollLeft = function () {
return this.getScrollWidth() - this.getClientWidth();
};
ScrollController.prototype.canScrollVertically = function () {
return this.getMaxScrollTop() > 0;
};
ScrollController.prototype.canScrollHorizontally = function () {
return this.getMaxScrollLeft() > 0;
};
ScrollController.prototype.canScrollUp = function () {
return this.getScrollTop() > 0;
};
ScrollController.prototype.canScrollDown = function () {
return this.getScrollTop() < this.getMaxScrollTop();
};
ScrollController.prototype.canScrollLeft = function () {
return this.getScrollLeft() > 0;
};
ScrollController.prototype.canScrollRight = function () {
return this.getScrollLeft() < this.getMaxScrollLeft();
};
return ScrollController;
}());
var ElementScrollController = /** @class */ (function (_super) {
__extends(ElementScrollController, _super);
function ElementScrollController(el) {
var _this = _super.call(this) || this;
_this.el = el;
return _this;
}
ElementScrollController.prototype.getScrollTop = function () {
return this.el.scrollTop;
};
ElementScrollController.prototype.getScrollLeft = function () {
return this.el.scrollLeft;
};
ElementScrollController.prototype.setScrollTop = function (top) {
this.el.scrollTop = top;
};
ElementScrollController.prototype.setScrollLeft = function (left) {
this.el.scrollLeft = left;
};
ElementScrollController.prototype.getScrollWidth = function () {
return this.el.scrollWidth;
};
ElementScrollController.prototype.getScrollHeight = function () {
return this.el.scrollHeight;
};
ElementScrollController.prototype.getClientHeight = function () {
return this.el.clientHeight;
};
ElementScrollController.prototype.getClientWidth = function () {
return this.el.clientWidth;
};
return ElementScrollController;
}(ScrollController));
var WindowScrollController = /** @class */ (function (_super) {
__extends(WindowScrollController, _super);
function WindowScrollController() {
return _super !== null && _super.apply(this, arguments) || this;
}
WindowScrollController.prototype.getScrollTop = function () {
return window.pageYOffset;
};
WindowScrollController.prototype.getScrollLeft = function () {
return window.pageXOffset;
};
WindowScrollController.prototype.setScrollTop = function (n) {
window.scroll(window.pageXOffset, n);
};
WindowScrollController.prototype.setScrollLeft = function (n) {
window.scroll(n, window.pageYOffset);
};
WindowScrollController.prototype.getScrollWidth = function () {
return document.documentElement.scrollWidth;
};
WindowScrollController.prototype.getScrollHeight = function () {
return document.documentElement.scrollHeight;
};
WindowScrollController.prototype.getClientHeight = function () {
return document.documentElement.clientHeight;
};
WindowScrollController.prototype.getClientWidth = function () {
return document.documentElement.clientWidth;
};
return WindowScrollController;
}(ScrollController));
/*
Embodies a div that has potential scrollbars
*/
var ScrollComponent = /** @class */ (function (_super) {
__extends(ScrollComponent, _super);
function ScrollComponent(overflowX, overflowY) {
var _this = _super.call(this, createElement('div', {
className: 'fc-scroller'
})) || this;
_this.overflowX = overflowX;
_this.overflowY = overflowY;
_this.applyOverflow();
return _this;
}
// sets to natural height, unlocks overflow
ScrollComponent.prototype.clear = function () {
this.setHeight('auto');
this.applyOverflow();
};
ScrollComponent.prototype.destroy = function () {
removeElement(this.el);
};
// Overflow
// -----------------------------------------------------------------------------------------------------------------
ScrollComponent.prototype.applyOverflow = function () {
applyStyle(this.el, {
overflowX: this.overflowX,
overflowY: this.overflowY
});
};
// Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
// Useful for preserving scrollbar widths regardless of future resizes.
// Can pass in scrollbarWidths for optimization.
ScrollComponent.prototype.lockOverflow = function (scrollbarWidths) {
var overflowX = this.overflowX;
var overflowY = this.overflowY;
scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
if (overflowX === 'auto') {
overflowX = (scrollbarWidths.bottom || // horizontal scrollbars?
this.canScrollHorizontally() // OR scrolling pane with massless scrollbars?
) ? 'scroll' : 'hidden';
}
if (overflowY === 'auto') {
overflowY = (scrollbarWidths.left || scrollbarWidths.right || // horizontal scrollbars?
this.canScrollVertically() // OR scrolling pane with massless scrollbars?
) ? 'scroll' : 'hidden';
}
applyStyle(this.el, { overflowX: overflowX, overflowY: overflowY });
};
ScrollComponent.prototype.setHeight = function (height) {
applyStyleProp(this.el, 'height', height);
};
ScrollComponent.prototype.getScrollbarWidths = function () {
var edges = computeEdges(this.el);
return {
left: edges.scrollbarLeft,
right: edges.scrollbarRight,
bottom: edges.scrollbarBottom
};
};
return ScrollComponent;
}(ElementScrollController));
var Theme = /** @class */ (function () {
function Theme(calendarOptions) {
this.calendarOptions = calendarOptions;
this.processIconOverride();
}
Theme.prototype.processIconOverride = function () {
if (this.iconOverrideOption) {
this.setIconOverride(this.calendarOptions[this.iconOverrideOption]);
}
};
Theme.prototype.setIconOverride = function (iconOverrideHash) {
var iconClassesCopy;
var buttonName;
if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object
iconClassesCopy = __assign({}, this.iconClasses);
for (buttonName in iconOverrideHash) {
iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
}
this.iconClasses = iconClassesCopy;
}
else if (iconOverrideHash === false) {
this.iconClasses = {};
}
};
Theme.prototype.applyIconOverridePrefix = function (className) {
var prefix = this.iconOverridePrefix;
if (prefix && className.indexOf(prefix) !== 0) { // if not already present
className = prefix + className;
}
return className;
};
Theme.prototype.getClass = function (key) {
return this.classes[key] || '';
};
Theme.prototype.getIconClass = function (buttonName) {
var className = this.iconClasses[buttonName];
if (className) {
return this.baseIconClass + ' ' + className;
}
return '';
};
Theme.prototype.getCustomButtonIconClass = function (customButtonProps) {
var className;
if (this.iconOverrideCustomButtonOption) {
className = customButtonProps[this.iconOverrideCustomButtonOption];
if (className) {
return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className);
}
}
return '';
};
return Theme;
}());
Theme.prototype.classes = {};
Theme.prototype.iconClasses = {};
Theme.prototype.baseIconClass = '';
Theme.prototype.iconOverridePrefix = '';
var guid = 0;
var Component = /** @class */ (function () {
function Component(context, isView) {
// HACK to populate view at top of component instantiation call chain
if (isView) {
context.view = this;
}
this.uid = String(guid++);
this.context = context;
this.dateEnv = context.dateEnv;
this.theme = context.theme;
this.view = context.view;
this.calendar = context.calendar;
this.isRtl = this.opt('dir') === 'rtl';
}
Component.addEqualityFuncs = function (newFuncs) {
this.prototype.equalityFuncs = __assign({}, this.prototype.equalityFuncs, newFuncs);
};
Component.prototype.opt = function (name) {
return this.context.options[name];
};
Component.prototype.receiveProps = function (props) {
var _a = recycleProps(this.props || {}, props, this.equalityFuncs), anyChanges = _a.anyChanges, comboProps = _a.comboProps;
this.props = comboProps;
if (anyChanges) {
this.render(comboProps);
}
};
Component.prototype.render = function (props) {
};
// after destroy is called, this component won't ever be used again
Component.prototype.destroy = function () {
};
return Component;
}());
Component.prototype.equalityFuncs = {};
/*
Reuses old values when equal. If anything is unequal, returns newProps as-is.
Great for PureComponent, but won't be feasible with React, so just eliminate and use React's DOM diffing.
*/
function recycleProps(oldProps, newProps, equalityFuncs) {
var comboProps = {}; // some old, some new
var anyChanges = false;
for (var key in newProps) {
if (key in oldProps && (oldProps[key] === newProps[key] ||
(equalityFuncs[key] && equalityFuncs[key](oldProps[key], newProps[key])))) {
// equal to old? use old prop
comboProps[key] = oldProps[key];
}
else {
comboProps[key] = newProps[key];
anyChanges = true;
}
}
for (var key in oldProps) {
if (!(key in newProps)) {
anyChanges = true;
break;
}
}
return { anyChanges: anyChanges, comboProps: comboProps };
}
/*
PURPOSES:
- hook up to fg, fill, and mirror renderers
- interface for dragging and hits
*/
var DateComponent = /** @class */ (function (_super) {
__extends(DateComponent, _super);
function DateComponent(context, el, isView) {
var _this = _super.call(this, context, isView) || this;
_this.el = el;
return _this;
}
DateComponent.prototype.destroy = function () {
_super.prototype.destroy.call(this);
removeElement(this.el);
};
// TODO: WHAT ABOUT (sourceSeg && sourceSeg.component.doesDragMirror)
//
// Event Drag-n-Drop Rendering (for both events and external elements)
// ---------------------------------------------------------------------------------------------------------------
/*
renderEventDragSegs(state: EventSegUiInteractionState) {
if (state) {
let { isEvent, segs, sourceSeg } = state
if (this.eventRenderer) {
this.eventRenderer.hideByHash(state.affectedInstances)
}
// if the user is dragging something that is considered an event with real event data,
// and this component likes to do drag mirrors OR the component where the seg came from
// likes to do drag mirrors, then render a drag mirror.
if (isEvent && (this.doesDragMirror || sourceSeg && sourceSeg.component.doesDragMirror)) {
if (this.mirrorRenderer) {
this.mirrorRenderer.renderSegs(segs, { isDragging: true, sourceSeg })
}
}
// if it would be impossible to render a drag mirror OR this component likes to render
// highlights, then render a highlight.
if (!isEvent || this.doesDragHighlight) {
if (this.fillRenderer) {
this.fillRenderer.renderSegs('highlight', segs)
}
}
}
}
*/
// Hit System
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.buildPositionCaches = function () {
};
DateComponent.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) {
return null; // this should be abstract
};
// Validation
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.isInteractionValid = function (interaction) {
var calendar = this.calendar;
var dateProfile = this.props.dateProfile; // HACK
var instances = interaction.mutatedEvents.instances;
if (dateProfile) { // HACK for DayTile
for (var instanceId in instances) {
if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
return false;
}
}
}
return isInteractionValid(interaction, calendar);
};
DateComponent.prototype.isDateSelectionValid = function (selection) {
var dateProfile = this.props.dateProfile; // HACK
if (dateProfile && // HACK for DayTile
!rangeContainsRange(dateProfile.validRange, selection.range)) {
return false;
}
return isDateSelectionValid(selection, this.calendar);
};
// Triggering
// -----------------------------------------------------------------------------------------------------------------
// TODO: move to Calendar
DateComponent.prototype.publiclyTrigger = function (name, args) {
var calendar = this.calendar;
return calendar.publiclyTrigger(name, args);
};
DateComponent.prototype.publiclyTriggerAfterSizing = function (name, args) {
var calendar = this.calendar;
return calendar.publiclyTriggerAfterSizing(name, args);
};
DateComponent.prototype.hasPublicHandlers = function (name) {
var calendar = this.calendar;
return calendar.hasPublicHandlers(name);
};
DateComponent.prototype.triggerRenderedSegs = function (segs, isMirrors) {
var calendar = this.calendar;
if (this.hasPublicHandlers('eventPositioned')) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
this.publiclyTriggerAfterSizing('eventPositioned', [
{
event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirrors,
isStart: seg.isStart,
isEnd: seg.isEnd,
el: seg.el,
view: this // safe to cast because this method is only called on context.view
}
]);
}
}
if (!calendar.state.loadingLevel) { // avoid initial empty state while pending
calendar.afterSizingTriggers._eventsPositioned = [null]; // fire once
}
};
DateComponent.prototype.triggerWillRemoveSegs = function (segs, isMirrors) {
var calendar = this.calendar;
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
calendar.trigger('eventElRemove', seg.el);
}
if (this.hasPublicHandlers('eventDestroy')) {
for (var _a = 0, segs_3 = segs; _a < segs_3.length; _a++) {
var seg = segs_3[_a];
this.publiclyTrigger('eventDestroy', [
{
event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirrors,
el: seg.el,
view: this // safe to cast because this method is only called on context.view
}
]);
}
}
};
// Pointer Interaction Utils
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.isValidSegDownEl = function (el) {
return !this.props.eventDrag && // HACK
!this.props.eventResize && // HACK
!elementClosest(el, '.fc-mirror') &&
(this.isPopover() || !this.isInPopover(el));
// ^above line ensures we don't detect a seg interaction within a nested component.
// it's a HACK because it only supports a popover as the nested component.
};
DateComponent.prototype.isValidDateDownEl = function (el) {
var segEl = elementClosest(el, this.fgSegSelector);
return (!segEl || segEl.classList.contains('fc-mirror')) &&
!elementClosest(el, '.fc-more') && // a "more.." link
!elementClosest(el, 'a[data-goto]') && // a clickable nav link
!this.isInPopover(el);
};
DateComponent.prototype.isPopover = function () {
return this.el.classList.contains('fc-popover');
};
DateComponent.prototype.isInPopover = function (el) {
return Boolean(elementClosest(el, '.fc-popover'));
};
return DateComponent;
}(Component));
DateComponent.prototype.fgSegSelector = '.fc-event-container > *';
DateComponent.prototype.bgSegSelector = '.fc-bgevent:not(.fc-nonbusiness)';
var uid$1 = 0;
function createPlugin(input) {
return {
id: String(uid$1++),
deps: input.deps || [],
reducers: input.reducers || [],
eventDefParsers: input.eventDefParsers || [],
isDraggableTransformers: input.isDraggableTransformers || [],
eventDragMutationMassagers: input.eventDragMutationMassagers || [],
eventDefMutationAppliers: input.eventDefMutationAppliers || [],
dateSelectionTransformers: input.dateSelectionTransformers || [],
datePointTransforms: input.datePointTransforms || [],
dateSpanTransforms: input.dateSpanTransforms || [],
views: input.views || {},
viewPropsTransformers: input.viewPropsTransformers || [],
isPropsValid: input.isPropsValid || null,
externalDefTransforms: input.externalDefTransforms || [],
eventResizeJoinTransforms: input.eventResizeJoinTransforms || [],
viewContainerModifiers: input.viewContainerModifiers || [],
eventDropTransformers: input.eventDropTransformers || [],
componentInteractions: input.componentInteractions || [],
calendarInteractions: input.calendarInteractions || [],
themeClasses: input.themeClasses || {},
eventSourceDefs: input.eventSourceDefs || [],
cmdFormatter: input.cmdFormatter,
recurringTypes: input.recurringTypes || [],
namedTimeZonedImpl: input.namedTimeZonedImpl,
defaultView: input.defaultView || '',
elementDraggingImpl: input.elementDraggingImpl,
optionChangeHandlers: input.optionChangeHandlers || {}
};
}
var PluginSystem = /** @class */ (function () {
function PluginSystem() {
this.hooks = {
reducers: [],
eventDefParsers: [],
isDraggableTransformers: [],
eventDragMutationMassagers: [],
eventDefMutationAppliers: [],
dateSelectionTransformers: [],
datePointTransforms: [],
dateSpanTransforms: [],
views: {},
viewPropsTransformers: [],
isPropsValid: null,
externalDefTransforms: [],
eventResizeJoinTransforms: [],
viewContainerModifiers: [],
eventDropTransformers: [],
componentInteractions: [],
calendarInteractions: [],
themeClasses: {},
eventSourceDefs: [],
cmdFormatter: null,
recurringTypes: [],
namedTimeZonedImpl: null,
defaultView: '',
elementDraggingImpl: null,
optionChangeHandlers: {}
};
this.addedHash = {};
}
PluginSystem.prototype.add = function (plugin) {
if (!this.addedHash[plugin.id]) {
this.addedHash[plugin.id] = true;
for (var _i = 0, _a = plugin.deps; _i < _a.length; _i++) {
var dep = _a[_i];
this.add(dep);
}
this.hooks = combineHooks(this.hooks, plugin);
}
};
return PluginSystem;
}());
function combineHooks(hooks0, hooks1) {
return {
reducers: hooks0.reducers.concat(hooks1.reducers),
eventDefParsers: hooks0.eventDefParsers.concat(hooks1.eventDefParsers),
isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
views: __assign({}, hooks0.views, hooks1.views),
viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
eventResizeJoinTransforms: hooks0.eventResizeJoinTransforms.concat(hooks1.eventResizeJoinTransforms),
viewContainerModifiers: hooks0.viewContainerModifiers.concat(hooks1.viewContainerModifiers),
eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
themeClasses: __assign({}, hooks0.themeClasses, hooks1.themeClasses),
eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
defaultView: hooks0.defaultView || hooks1.defaultView,
elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
optionChangeHandlers: __assign({}, hooks0.optionChangeHandlers, hooks1.optionChangeHandlers)
};
}
var eventSourceDef = {
ignoreRange: true,
parseMeta: function (raw) {
if (Array.isArray(raw)) { // short form
return raw;
}
else if (Array.isArray(raw.events)) {
return raw.events;
}
return null;
},
fetch: function (arg, success) {
success({
rawEvents: arg.eventSource.meta
});
}
};
var ArrayEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef]
});
var eventSourceDef$1 = {
parseMeta: function (raw) {
if (typeof raw === 'function') { // short form
return raw;
}
else if (typeof raw.events === 'function') {
return raw.events;
}
return null;
},
fetch: function (arg, success, failure) {
var dateEnv = arg.calendar.dateEnv;
var func = arg.eventSource.meta;
unpromisify(func.bind(null, {
start: dateEnv.toDate(arg.range.start),
end: dateEnv.toDate(arg.range.end),
startStr: dateEnv.formatIso(arg.range.start),
endStr: dateEnv.formatIso(arg.range.end),
timeZone: dateEnv.timeZone
}), function (rawEvents) {
success({ rawEvents: rawEvents }); // needs an object response
}, failure // send errorObj directly to failure callback
);
}
};
var FuncEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef$1]
});
function requestJson(method, url, params, successCallback, failureCallback) {
method = method.toUpperCase();
var body = null;
if (method === 'GET') {
url = injectQueryStringParams(url, params);
}
else {
body = encodeParams(params);
}
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (method !== 'GET') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 400) {
try {
var res = JSON.parse(xhr.responseText);
successCallback(res, xhr);
}
catch (err) {
failureCallback('Failure parsing JSON', xhr);
}
}
else {
failureCallback('Request failed', xhr);
}
};
xhr.onerror = function () {
failureCallback('Request failed', xhr);
};
xhr.send(body);
}
function injectQueryStringParams(url, params) {
return url +
(url.indexOf('?') === -1 ? '?' : '&') +
encodeParams(params);
}
function encodeParams(params) {
var parts = [];
for (var key in params) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
}
return parts.join('&');
}
var eventSourceDef$2 = {
parseMeta: function (raw) {
if (typeof raw === 'string') { // short form
raw = { url: raw };
}
else if (!raw || typeof raw !== 'object' || !raw.url) {
return null;
}
return {
url: raw.url,
method: (raw.method || 'GET').toUpperCase(),
extraParams: raw.extraParams,
startParam: raw.startParam,
endParam: raw.endParam,
timeZoneParam: raw.timeZoneParam
};
},
fetch: function (arg, success, failure) {
var meta = arg.eventSource.meta;
var requestParams = buildRequestParams(meta, arg.range, arg.calendar);
requestJson(meta.method, meta.url, requestParams, function (rawEvents, xhr) {
success({ rawEvents: rawEvents, xhr: xhr });
}, function (errorMessage, xhr) {
failure({ message: errorMessage, xhr: xhr });
});
}
};
var JsonFeedEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef$2]
});
function buildRequestParams(meta, range, calendar) {
var dateEnv = calendar.dateEnv;
var startParam;
var endParam;
var timeZoneParam;
var customRequestParams;
var params = {};
startParam = meta.startParam;
if (startParam == null) {
startParam = calendar.opt('startParam');
}
endParam = meta.endParam;
if (endParam == null) {
endParam = calendar.opt('endParam');
}
timeZoneParam = meta.timeZoneParam;
if (timeZoneParam == null) {
timeZoneParam = calendar.opt('timeZoneParam');
}
// retrieve any outbound GET/POST data from the options
if (typeof meta.extraParams === 'function') {
// supplied as a function that returns a key/value object
customRequestParams = meta.extraParams();
}
else {
// probably supplied as a straight key/value object
customRequestParams = meta.extraParams || {};
}
__assign(params, customRequestParams);
params[startParam] = dateEnv.formatIso(range.start);
params[endParam] = dateEnv.formatIso(range.end);
if (dateEnv.timeZone !== 'local') {
params[timeZoneParam] = dateEnv.timeZone;
}
return params;
}
var recurring = {
parse: function (rawEvent, leftoverProps, dateEnv) {
var createMarker = dateEnv.createMarker.bind(dateEnv);
var processors = {
daysOfWeek: null,
startTime: createDuration,
endTime: createDuration,
startRecur: createMarker,
endRecur: createMarker
};
var props = refineProps(rawEvent, processors, {}, leftoverProps);
var anyValid = false;
for (var propName in props) {
if (props[propName] != null) {
anyValid = true;
break;
}
}
if (anyValid) {
var duration = null;
if ('duration' in leftoverProps) {
duration = createDuration(leftoverProps.duration);
delete leftoverProps.duration;
}
if (!duration && props.startTime && props.endTime) {
duration = subtractDurations(props.endTime, props.startTime);
}
return {
allDayGuess: Boolean(!props.startTime && !props.endTime),
duration: duration,
typeData: props // doesn't need endTime anymore but oh well
};
}
return null;
},
expand: function (typeData, framingRange, dateEnv) {
var clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
if (clippedFramingRange) {
return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv);
}
else {
return [];
}
}
};
var SimpleRecurrencePlugin = createPlugin({
recurringTypes: [recurring]
});
function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) {
var dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null;
var dayMarker = startOfDay(framingRange.start);
var endMarker = framingRange.end;
var instanceStarts = [];
while (dayMarker < endMarker) {
var instanceStart
// if everyday, or this particular day-of-week
= void 0;
// if everyday, or this particular day-of-week
if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
if (startTime) {
instanceStart = dateEnv.add(dayMarker, startTime);
}
else {
instanceStart = dayMarker;
}
instanceStarts.push(instanceStart);
}
dayMarker = addDays(dayMarker, 1);
}
return instanceStarts;
}
var DefaultOptionChangeHandlers = createPlugin({
optionChangeHandlers: {
events: function (events, calendar, deepEqual) {
handleEventSources([events], calendar, deepEqual);
},
eventSources: handleEventSources,
plugins: handlePlugins
}
});
function handleEventSources(inputs, calendar, deepEqual) {
var unfoundSources = hashValuesToArray(calendar.state.eventSources);
var newInputs = [];
for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) {
var input = inputs_1[_i];
var inputFound = false;
for (var i = 0; i < unfoundSources.length; i++) {
if (deepEqual(unfoundSources[i]._raw, input)) {
unfoundSources.splice(i, 1); // delete
inputFound = true;
break;
}
}
if (!inputFound) {
newInputs.push(input);
}
}
for (var _a = 0, unfoundSources_1 = unfoundSources; _a < unfoundSources_1.length; _a++) {
var unfoundSource = unfoundSources_1[_a];
calendar.dispatch({
type: 'REMOVE_EVENT_SOURCE',
sourceId: unfoundSource.sourceId
});
}
for (var _b = 0, newInputs_1 = newInputs; _b < newInputs_1.length; _b++) {
var newInput = newInputs_1[_b];
calendar.addEventSource(newInput);
}
}
// shortcoming: won't remove plugins
function handlePlugins(inputs, calendar) {
calendar.addPluginInputs(inputs); // will gracefully handle duplicates
}
var config = {}; // TODO: make these options
var globalDefaults = {
defaultRangeSeparator: ' - ',
titleRangeSeparator: ' \u2013 ',
defaultTimedEventDuration: '01:00:00',
defaultAllDayEventDuration: { day: 1 },
forceEventDuration: false,
nextDayThreshold: '00:00:00',
// display
columnHeader: true,
defaultView: '',
aspectRatio: 1.35,
header: {
left: 'title',
center: '',
right: 'today prev,next'
},
weekends: true,
weekNumbers: false,
weekNumberCalculation: 'local',
editable: false,
// nowIndicator: false,
scrollTime: '06:00:00',
minTime: '00:00:00',
maxTime: '24:00:00',
showNonCurrentDates: true,
// event ajax
lazyFetching: true,
startParam: 'start',
endParam: 'end',
timeZoneParam: 'timeZone',
timeZone: 'local',
// allDayDefault: undefined,
// locale
locales: [],
locale: '',
// dir: will get this from the default locale
// buttonIcons: null,
// allows setting a min-height to the event segment to prevent short events overlapping each other
timeGridEventMinHeight: 0,
themeSystem: 'standard',
// eventResizableFromStart: false,
dragRevertDuration: 500,
dragScroll: true,
allDayMaintainDuration: false,
// selectable: false,
unselectAuto: true,
// selectMinDistance: 0,
dropAccept: '*',
eventOrder: 'start,-duration,allDay,title',
// ^ if start tie, longer events go before shorter. final tie-breaker is title text
// rerenderDelay: null,
eventLimit: false,
eventLimitClick: 'popover',
dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
handleWindowResize: true,
windowResizeDelay: 100,
longPressDelay: 1000,
eventDragMinDistance: 5 // only applies to mouse
};
var rtlDefaults = {
header: {
left: 'next,prev today',
center: '',
right: 'title'
},
buttonIcons: {
// TODO: make RTL support the responibility of the theme
prev: 'fc-icon-chevron-right',
next: 'fc-icon-chevron-left',
prevYear: 'fc-icon-chevrons-right',
nextYear: 'fc-icon-chevrons-left'
}
};
var complexOptions = [
'header',
'footer',
'buttonText',
'buttonIcons'
];
// Merges an array of option objects into a single object
function mergeOptions(optionObjs) {
return mergeProps(optionObjs, complexOptions);
}
// TODO: move this stuff to a "plugin"-related file...
var INTERNAL_PLUGINS = [
ArrayEventSourcePlugin,
FuncEventSourcePlugin,
JsonFeedEventSourcePlugin,
SimpleRecurrencePlugin,
DefaultOptionChangeHandlers
];
function refinePluginDefs(pluginInputs) {
var plugins = [];
for (var _i = 0, pluginInputs_1 = pluginInputs; _i < pluginInputs_1.length; _i++) {
var pluginInput = pluginInputs_1[_i];
if (typeof pluginInput === 'string') {
var globalName = 'FullCalendar' + capitaliseFirstLetter(pluginInput);
if (!window[globalName]) {
console.warn('Plugin file not loaded for ' + pluginInput);
}
else {
plugins.push(window[globalName].default); // is an ES6 module
}
}
else {
plugins.push(pluginInput);
}
}
return INTERNAL_PLUGINS.concat(plugins);
}
var RAW_EN_LOCALE = {
code: 'en',
week: {
dow: 0,
doy: 4 // 4 days need to be within the year to be considered the first week
},
dir: 'ltr',
buttonText: {
prev: 'prev',
next: 'next',
prevYear: 'prev year',
nextYear: 'next year',
year: 'year',
today: 'today',
month: 'month',
week: 'week',
day: 'day',
list: 'list'
},
weekLabel: 'W',
allDayText: 'all-day',
eventLimitText: 'more',
noEventsMessage: 'No events to display'
};
function parseRawLocales(explicitRawLocales) {
var defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en';
var globalArray = window['FullCalendarLocalesAll'] || []; // from locales-all.js
var globalObject = window['FullCalendarLocales'] || {}; // from locales/*.js. keys are meaningless
var allRawLocales = globalArray.concat(// globalArray is low prio
hashValuesToArray(globalObject), // medium prio
explicitRawLocales // highest prio
);
var rawLocaleMap = {
en: RAW_EN_LOCALE // necessary?
};
for (var _i = 0, allRawLocales_1 = allRawLocales; _i < allRawLocales_1.length; _i++) {
var rawLocale = allRawLocales_1[_i];
rawLocaleMap[rawLocale.code] = rawLocale;
}
return {
map: rawLocaleMap,
defaultCode: defaultCode
};
}
function buildLocale(inputSingular, available) {
if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
}
else {
return queryLocale(inputSingular, available);
}
}
function queryLocale(codeArg, available) {
var codes = [].concat(codeArg || []); // will convert to array
var raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
return parseLocale(codeArg, codes, raw);
}
function queryRawLocale(codes, available) {
for (var i = 0; i < codes.length; i++) {
var parts = codes[i].toLocaleLowerCase().split('-');
for (var j = parts.length; j > 0; j--) {
var simpleId = parts.slice(0, j).join('-');
if (available[simpleId]) {
return available[simpleId];
}
}
}
return null;
}
function parseLocale(codeArg, codes, raw) {
var merged = mergeProps([RAW_EN_LOCALE, raw], ['buttonText']);
delete merged.code; // don't want this part of the options
var week = merged.week;
delete merged.week;
return {
codeArg: codeArg,
codes: codes,
week: week,
simpleNumberFormat: new Intl.NumberFormat(codeArg),
options: merged
};
}
var OptionsManager = /** @class */ (function () {
function OptionsManager(overrides) {
this.overrides = __assign({}, overrides); // make a copy
this.dynamicOverrides = {};
this.compute();
}
OptionsManager.prototype.mutate = function (updates, removals, isDynamic) {
var overrideHash = isDynamic ? this.dynamicOverrides : this.overrides;
__assign(overrideHash, updates);
for (var _i = 0, removals_1 = removals; _i < removals_1.length; _i++) {
var propName = removals_1[_i];
delete overrideHash[propName];
}
this.compute();
};
// Computes the flattened options hash for the calendar and assigns to `this.options`.
// Assumes this.overrides and this.dynamicOverrides have already been initialized.
OptionsManager.prototype.compute = function () {
// TODO: not a very efficient system
var locales = firstDefined(// explicit locale option given?
this.dynamicOverrides.locales, this.overrides.locales, globalDefaults.locales);
var locale = firstDefined(// explicit locales option given?
this.dynamicOverrides.locale, this.overrides.locale, globalDefaults.locale);
var available = parseRawLocales(locales);
var localeDefaults = buildLocale(locale || available.defaultCode, available.map).options;
var dir = firstDefined(// based on options computed so far, is direction RTL?
this.dynamicOverrides.dir, this.overrides.dir, localeDefaults.dir);
var dirDefaults = dir === 'rtl' ? rtlDefaults : {};
this.dirDefaults = dirDefaults;
this.localeDefaults = localeDefaults;
this.computed = mergeOptions([
globalDefaults,
dirDefaults,
localeDefaults,
this.overrides,
this.dynamicOverrides
]);
};
return OptionsManager;
}());
var calendarSystemClassMap = {};
function registerCalendarSystem(name, theClass) {
calendarSystemClassMap[name] = theClass;
}
function createCalendarSystem(name) {
return new calendarSystemClassMap[name]();
}
var GregorianCalendarSystem = /** @class */ (function () {
function GregorianCalendarSystem() {
}
GregorianCalendarSystem.prototype.getMarkerYear = function (d) {
return d.getUTCFullYear();
};
GregorianCalendarSystem.prototype.getMarkerMonth = function (d) {
return d.getUTCMonth();
};
GregorianCalendarSystem.prototype.getMarkerDay = function (d) {
return d.getUTCDate();
};
GregorianCalendarSystem.prototype.arrayToMarker = function (arr) {
return arrayToUtcDate(arr);
};
GregorianCalendarSystem.prototype.markerToArray = function (marker) {
return dateToUtcArray(marker);
};
return GregorianCalendarSystem;
}());
registerCalendarSystem('gregory', GregorianCalendarSystem);
var ISO_RE = /^\s*(\d{4})(-(\d{2})(-(\d{2})([T ](\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/;
function parse(str) {
var m = ISO_RE.exec(str);
if (m) {
var marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number('0.' + m[12]) * 1000 : 0));
if (isValidDate(marker)) {
var timeZoneOffset = null;
if (m[13]) {
timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 +
Number(m[18] || 0));
}
return {
marker: marker,
isTimeUnspecified: !m[6],
timeZoneOffset: timeZoneOffset
};
}
}
return null;
}
var DateEnv = /** @class */ (function () {
function DateEnv(settings) {
var timeZone = this.timeZone = settings.timeZone;
var isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC';
if (settings.namedTimeZoneImpl && isNamedTimeZone) {
this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone);
}
this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl);
this.calendarSystem = createCalendarSystem(settings.calendarSystem);
this.locale = settings.locale;
this.weekDow = settings.locale.week.dow;
this.weekDoy = settings.locale.week.doy;
if (settings.weekNumberCalculation === 'ISO') {
this.weekDow = 1;
this.weekDoy = 4;
}
if (typeof settings.firstDay === 'number') {
this.weekDow = settings.firstDay;
}
if (typeof settings.weekNumberCalculation === 'function') {
this.weekNumberFunc = settings.weekNumberCalculation;
}
this.weekLabel = settings.weekLabel != null ? settings.weekLabel : settings.locale.options.weekLabel;
this.cmdFormatter = settings.cmdFormatter;
}
// Creating / Parsing
DateEnv.prototype.createMarker = function (input) {
var meta = this.createMarkerMeta(input);
if (meta === null) {
return null;
}
return meta.marker;
};
DateEnv.prototype.createNowMarker = function () {
if (this.canComputeOffset) {
return this.timestampToMarker(new Date().valueOf());
}
else {
// if we can't compute the current date val for a timezone,
// better to give the current local date vals than UTC
return arrayToUtcDate(dateToLocalArray(new Date()));
}
};
DateEnv.prototype.createMarkerMeta = function (input) {
if (typeof input === 'string') {
return this.parse(input);
}
var marker = null;
if (typeof input === 'number') {
marker = this.timestampToMarker(input);
}
else if (input instanceof Date) {
input = input.valueOf();
if (!isNaN(input)) {
marker = this.timestampToMarker(input);
}
}
else if (Array.isArray(input)) {
marker = arrayToUtcDate(input);
}
if (marker === null || !isValidDate(marker)) {
return null;
}
return { marker: marker, isTimeUnspecified: false, forcedTzo: null };
};
DateEnv.prototype.parse = function (s) {
var parts = parse(s);
if (parts === null) {
return null;
}
var marker = parts.marker;
var forcedTzo = null;
if (parts.timeZoneOffset !== null) {
if (this.canComputeOffset) {
marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000);
}
else {
forcedTzo = parts.timeZoneOffset;
}
}
return { marker: marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo: forcedTzo };
};
// Accessors
DateEnv.prototype.getYear = function (marker) {
return this.calendarSystem.getMarkerYear(marker);
};
DateEnv.prototype.getMonth = function (marker) {
return this.calendarSystem.getMarkerMonth(marker);
};
// Adding / Subtracting
DateEnv.prototype.add = function (marker, dur) {
var a = this.calendarSystem.markerToArray(marker);
a[0] += dur.years;
a[1] += dur.months;
a[2] += dur.days;
a[6] += dur.milliseconds;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.subtract = function (marker, dur) {
var a = this.calendarSystem.markerToArray(marker);
a[0] -= dur.years;
a[1] -= dur.months;
a[2] -= dur.days;
a[6] -= dur.milliseconds;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.addYears = function (marker, n) {
var a = this.calendarSystem.markerToArray(marker);
a[0] += n;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.addMonths = function (marker, n) {
var a = this.calendarSystem.markerToArray(marker);
a[1] += n;
return this.calendarSystem.arrayToMarker(a);
};
// Diffing Whole Units
DateEnv.prototype.diffWholeYears = function (m0, m1) {
var calendarSystem = this.calendarSystem;
if (timeAsMs(m0) === timeAsMs(m1) &&
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) &&
calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) {
return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0);
}
return null;
};
DateEnv.prototype.diffWholeMonths = function (m0, m1) {
var calendarSystem = this.calendarSystem;
if (timeAsMs(m0) === timeAsMs(m1) &&
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) {
return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
(calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12;
}
return null;
};
// Range / Duration
DateEnv.prototype.greatestWholeUnit = function (m0, m1) {
var n = this.diffWholeYears(m0, m1);
if (n !== null) {
return { unit: 'year', value: n };
}
n = this.diffWholeMonths(m0, m1);
if (n !== null) {
return { unit: 'month', value: n };
}
n = diffWholeWeeks(m0, m1);
if (n !== null) {
return { unit: 'week', value: n };
}
n = diffWholeDays(m0, m1);
if (n !== null) {
return { unit: 'day', value: n };
}
n = diffHours(m0, m1);
if (isInt(n)) {
return { unit: 'hour', value: n };
}
n = diffMinutes(m0, m1);
if (isInt(n)) {
return { unit: 'minute', value: n };
}
n = diffSeconds(m0, m1);
if (isInt(n)) {
return { unit: 'second', value: n };
}
return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() };
};
DateEnv.prototype.countDurationsBetween = function (m0, m1, d) {
// TODO: can use greatestWholeUnit
var diff;
if (d.years) {
diff = this.diffWholeYears(m0, m1);
if (diff !== null) {
return diff / asRoughYears(d);
}
}
if (d.months) {
diff = this.diffWholeMonths(m0, m1);
if (diff !== null) {
return diff / asRoughMonths(d);
}
}
if (d.days) {
diff = diffWholeDays(m0, m1);
if (diff !== null) {
return diff / asRoughDays(d);
}
}
return (m1.valueOf() - m0.valueOf()) / asRoughMs(d);
};
// Start-Of
DateEnv.prototype.startOf = function (m, unit) {
if (unit === 'year') {
return this.startOfYear(m);
}
else if (unit === 'month') {
return this.startOfMonth(m);
}
else if (unit === 'week') {
return this.startOfWeek(m);
}
else if (unit === 'day') {
return startOfDay(m);
}
else if (unit === 'hour') {
return startOfHour(m);
}
else if (unit === 'minute') {
return startOfMinute(m);
}
else if (unit === 'second') {
return startOfSecond(m);
}
};
DateEnv.prototype.startOfYear = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m)
]);
};
DateEnv.prototype.startOfMonth = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m),
this.calendarSystem.getMarkerMonth(m)
]);
};
DateEnv.prototype.startOfWeek = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m),
this.calendarSystem.getMarkerMonth(m),
m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7)
]);
};
// Week Number
DateEnv.prototype.computeWeekNumber = function (marker) {
if (this.weekNumberFunc) {
return this.weekNumberFunc(this.toDate(marker));
}
else {
return weekOfYear(marker, this.weekDow, this.weekDoy);
}
};
// TODO: choke on timeZoneName: long
DateEnv.prototype.format = function (marker, formatter, dateOptions) {
if (dateOptions === void 0) { dateOptions = {}; }
return formatter.format({
marker: marker,
timeZoneOffset: dateOptions.forcedTzo != null ?
dateOptions.forcedTzo :
this.offsetForMarker(marker)
}, this);
};
DateEnv.prototype.formatRange = function (start, end, formatter, dateOptions) {
if (dateOptions === void 0) { dateOptions = {}; }
if (dateOptions.isEndExclusive) {
end = addMs(end, -1);
}
return formatter.formatRange({
marker: start,
timeZoneOffset: dateOptions.forcedStartTzo != null ?
dateOptions.forcedStartTzo :
this.offsetForMarker(start)
}, {
marker: end,
timeZoneOffset: dateOptions.forcedEndTzo != null ?
dateOptions.forcedEndTzo :
this.offsetForMarker(end)
}, this);
};
DateEnv.prototype.formatIso = function (marker, extraOptions) {
if (extraOptions === void 0) { extraOptions = {}; }
var timeZoneOffset = null;
if (!extraOptions.omitTimeZoneOffset) {
if (extraOptions.forcedTzo != null) {
timeZoneOffset = extraOptions.forcedTzo;
}
else {
timeZoneOffset = this.offsetForMarker(marker);
}
}
return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime);
};
// TimeZone
DateEnv.prototype.timestampToMarker = function (ms) {
if (this.timeZone === 'local') {
return arrayToUtcDate(dateToLocalArray(new Date(ms)));
}
else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
return new Date(ms);
}
else {
return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms));
}
};
DateEnv.prototype.offsetForMarker = function (m) {
if (this.timeZone === 'local') {
return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset
}
else if (this.timeZone === 'UTC') {
return 0;
}
else if (this.namedTimeZoneImpl) {
return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m));
}
return null;
};
// Conversion
DateEnv.prototype.toDate = function (m, forcedTzo) {
if (this.timeZone === 'local') {
return arrayToLocalDate(dateToUtcArray(m));
}
else if (this.timeZone === 'UTC') {
return new Date(m.valueOf()); // make sure it's a copy
}
else if (!this.namedTimeZoneImpl) {
return new Date(m.valueOf() - (forcedTzo || 0));
}
else {
return new Date(m.valueOf() -
this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60 // convert minutes -> ms
);
}
};
return DateEnv;
}());
var SIMPLE_SOURCE_PROPS = {
id: String,
allDayDefault: Boolean,
eventDataTransform: Function,
success: Function,
failure: Function
};
var uid$2 = 0;
function doesSourceNeedRange(eventSource, calendar) {
var defs = calendar.pluginSystem.hooks.eventSourceDefs;
return !defs[eventSource.sourceDefId].ignoreRange;
}
function parseEventSource(raw, calendar) {
var defs = calendar.pluginSystem.hooks.eventSourceDefs;
for (var i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
var def = defs[i];
var meta = def.parseMeta(raw);
if (meta) {
var res = parseEventSourceProps(typeof raw === 'object' ? raw : {}, meta, i, calendar);
res._raw = raw;
return res;
}
}
return null;
}
function parseEventSourceProps(raw, meta, sourceDefId, calendar) {
var leftovers0 = {};
var props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0);
var leftovers1 = {};
var ui = processUnscopedUiProps(leftovers0, calendar, leftovers1);
props.isFetching = false;
props.latestFetchId = '';
props.fetchRange = null;
props.publicId = String(raw.id || '');
props.sourceId = String(uid$2++);
props.sourceDefId = sourceDefId;
props.meta = meta;
props.ui = ui;
props.extendedProps = leftovers1;
return props;
}
function reduceEventSources (eventSources, action, dateProfile, calendar) {
switch (action.type) {
case 'ADD_EVENT_SOURCES': // already parsed
return addSources(eventSources, action.sources, dateProfile ? dateProfile.activeRange : null, calendar);
case 'REMOVE_EVENT_SOURCE':
return removeSource(eventSources, action.sourceId);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
if (dateProfile) {
return fetchDirtySources(eventSources, dateProfile.activeRange, calendar);
}
else {
return eventSources;
}
case 'FETCH_EVENT_SOURCES':
case 'CHANGE_TIMEZONE':
return fetchSourcesByIds(eventSources, action.sourceIds ?
arrayToHash(action.sourceIds) :
excludeStaticSources(eventSources, calendar), dateProfile ? dateProfile.activeRange : null, calendar);
case 'RECEIVE_EVENTS':
case 'RECEIVE_EVENT_ERROR':
return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
case 'REMOVE_ALL_EVENT_SOURCES':
return {};
default:
return eventSources;
}
}
var uid$3 = 0;
function addSources(eventSourceHash, sources, fetchRange, calendar) {
var hash = {};
for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) {
var source = sources_1[_i];
hash[source.sourceId] = source;
}
if (fetchRange) {
hash = fetchDirtySources(hash, fetchRange, calendar);
}
return __assign({}, eventSourceHash, hash);
}
function removeSource(eventSourceHash, sourceId) {
return filterHash(eventSourceHash, function (eventSource) {
return eventSource.sourceId !== sourceId;
});
}
function fetchDirtySources(sourceHash, fetchRange, calendar) {
return fetchSourcesByIds(sourceHash, filterHash(sourceHash, function (eventSource) {
return isSourceDirty(eventSource, fetchRange, calendar);
}), fetchRange, calendar);
}
function isSourceDirty(eventSource, fetchRange, calendar) {
if (!doesSourceNeedRange(eventSource, calendar)) {
return !eventSource.latestFetchId;
}
else {
return !calendar.opt('lazyFetching') ||
!eventSource.fetchRange ||
fetchRange.start < eventSource.fetchRange.start ||
fetchRange.end > eventSource.fetchRange.end;
}
}
function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, calendar) {
var nextSources = {};
for (var sourceId in prevSources) {
var source = prevSources[sourceId];
if (sourceIdHash[sourceId]) {
nextSources[sourceId] = fetchSource(source, fetchRange, calendar);
}
else {
nextSources[sourceId] = source;
}
}
return nextSources;
}
function fetchSource(eventSource, fetchRange, calendar) {
var sourceDef = calendar.pluginSystem.hooks.eventSourceDefs[eventSource.sourceDefId];
var fetchId = String(uid$3++);
sourceDef.fetch({
eventSource: eventSource,
calendar: calendar,
range: fetchRange
}, function (res) {
var rawEvents = res.rawEvents;
var calSuccess = calendar.opt('eventSourceSuccess');
var calSuccessRes;
var sourceSuccessRes;
if (eventSource.success) {
sourceSuccessRes = eventSource.success(rawEvents, res.xhr);
}
if (calSuccess) {
calSuccessRes = calSuccess(rawEvents, res.xhr);
}
rawEvents = sourceSuccessRes || calSuccessRes || rawEvents;
calendar.dispatch({
type: 'RECEIVE_EVENTS',
sourceId: eventSource.sourceId,
fetchId: fetchId,
fetchRange: fetchRange,
rawEvents: rawEvents
});
}, function (error) {
var callFailure = calendar.opt('eventSourceFailure');
console.warn(error.message, error);
if (eventSource.failure) {
eventSource.failure(error);
}
if (callFailure) {
callFailure(error);
}
calendar.dispatch({
type: 'RECEIVE_EVENT_ERROR',
sourceId: eventSource.sourceId,
fetchId: fetchId,
fetchRange: fetchRange,
error: error
});
});
return __assign({}, eventSource, { isFetching: true, latestFetchId: fetchId });
}
function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
var _a;
var eventSource = sourceHash[sourceId];
if (eventSource && // not already removed
fetchId === eventSource.latestFetchId) {
return __assign({}, sourceHash, (_a = {}, _a[sourceId] = __assign({}, eventSource, { isFetching: false, fetchRange: fetchRange }), _a));
}
return sourceHash;
}
function excludeStaticSources(eventSources, calendar) {
return filterHash(eventSources, function (eventSource) {
return doesSourceNeedRange(eventSource, calendar);
});
}
var DateProfileGenerator = /** @class */ (function () {
function DateProfileGenerator(viewSpec, calendar) {
this.viewSpec = viewSpec;
this.options = viewSpec.options;
this.dateEnv = calendar.dateEnv;
this.calendar = calendar;
this.initHiddenDays();
}
/* Date Range Computation
------------------------------------------------------------------------------------------------------------------*/
// Builds a structure with info about what the dates/ranges will be for the "prev" view.
DateProfileGenerator.prototype.buildPrev = function (currentDateProfile, currentDate) {
var dateEnv = this.dateEnv;
var prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(prevDate, -1);
};
// Builds a structure with info about what the dates/ranges will be for the "next" view.
DateProfileGenerator.prototype.buildNext = function (currentDateProfile, currentDate) {
var dateEnv = this.dateEnv;
var nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(nextDate, 1);
};
// Builds a structure holding dates/ranges for rendering around the given date.
// Optional direction param indicates whether the date is being incremented/decremented
// from its previous value. decremented = -1, incremented = 1 (default).
DateProfileGenerator.prototype.build = function (currentDate, direction, forceToValid) {
if (forceToValid === void 0) { forceToValid = false; }
var validRange;
var minTime = null;
var maxTime = null;
var currentInfo;
var isRangeAllDay;
var renderRange;
var activeRange;
var isValid;
validRange = this.buildValidRange();
validRange = this.trimHiddenDays(validRange);
if (forceToValid) {
currentDate = constrainMarkerToRange(currentDate, validRange);
}
currentInfo = this.buildCurrentRangeInfo(currentDate, direction);
isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay);
renderRange = this.trimHiddenDays(renderRange);
activeRange = renderRange;
if (!this.options.showNonCurrentDates) {
activeRange = intersectRanges(activeRange, currentInfo.range);
}
minTime = createDuration(this.options.minTime);
maxTime = createDuration(this.options.maxTime);
activeRange = this.adjustActiveRange(activeRange, minTime, maxTime);
activeRange = intersectRanges(activeRange, validRange); // might return null
// it's invalid if the originally requested date is not contained,
// or if the range is completely outside of the valid range.
isValid = rangesIntersect(currentInfo.range, validRange);
return {
// constraint for where prev/next operations can go and where events can be dragged/resized to.
// an object with optional start and end properties.
validRange: validRange,
// range the view is formally responsible for.
// for example, a month view might have 1st-31st, excluding padded dates
currentRange: currentInfo.range,
// name of largest unit being displayed, like "month" or "week"
currentRangeUnit: currentInfo.unit,
isRangeAllDay: isRangeAllDay,
// dates that display events and accept drag-n-drop
// will be `null` if no dates accept events
activeRange: activeRange,
// date range with a rendered skeleton
// includes not-active days that need some sort of DOM
renderRange: renderRange,
// Duration object that denotes the first visible time of any given day
minTime: minTime,
// Duration object that denotes the exclusive visible end time of any given day
maxTime: maxTime,
isValid: isValid,
// how far the current date will move for a prev/next operation
dateIncrement: this.buildDateIncrement(currentInfo.duration)
// pass a fallback (might be null) ^
};
};
// Builds an object with optional start/end properties.
// Indicates the minimum/maximum dates to display.
// not responsible for trimming hidden days.
DateProfileGenerator.prototype.buildValidRange = function () {
return this.getRangeOption('validRange', this.calendar.getNow()) ||
{ start: null, end: null }; // completely open-ended
};
// Builds a structure with info about the "current" range, the range that is
// highlighted as being the current month for example.
// See build() for a description of `direction`.
// Guaranteed to have `range` and `unit` properties. `duration` is optional.
DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) {
var _a = this, viewSpec = _a.viewSpec, dateEnv = _a.dateEnv;
var duration = null;
var unit = null;
var range = null;
var dayCount;
if (viewSpec.duration) {
duration = viewSpec.duration;
unit = viewSpec.durationUnit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
else if ((dayCount = this.options.dayCount)) {
unit = 'day';
range = this.buildRangeFromDayCount(date, direction, dayCount);
}
else if ((range = this.buildCustomVisibleRange(date))) {
unit = dateEnv.greatestWholeUnit(range.start, range.end).unit;
}
else {
duration = this.getFallbackDuration();
unit = greatestDurationDenominator(duration).unit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
return { duration: duration, unit: unit, range: range };
};
DateProfileGenerator.prototype.getFallbackDuration = function () {
return createDuration({ day: 1 });
};
// Returns a new activeRange to have time values (un-ambiguate)
// minTime or maxTime causes the range to expand.
DateProfileGenerator.prototype.adjustActiveRange = function (range, minTime, maxTime) {
var dateEnv = this.dateEnv;
var start = range.start;
var end = range.end;
if (this.viewSpec.class.prototype.usesMinMaxTime) {
// expand active range if minTime is negative (why not when positive?)
if (asRoughDays(minTime) < 0) {
start = startOfDay(start); // necessary?
start = dateEnv.add(start, minTime);
}
// expand active range if maxTime is beyond one day (why not when positive?)
if (asRoughDays(maxTime) > 1) {
end = startOfDay(end); // necessary?
end = addDays(end, -1);
end = dateEnv.add(end, maxTime);
}
}
return { start: start, end: end };
};
// Builds the "current" range when it is specified as an explicit duration.
// `unit` is the already-computed greatestDurationDenominator unit of duration.
DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) {
var dateEnv = this.dateEnv;
var alignment = this.options.dateAlignment;
var dateIncrementInput;
var dateIncrementDuration;
var start;
var end;
var res;
// compute what the alignment should be
if (!alignment) {
dateIncrementInput = this.options.dateIncrement;
if (dateIncrementInput) {
dateIncrementDuration = createDuration(dateIncrementInput);
// use the smaller of the two units
if (asRoughMs(dateIncrementDuration) < asRoughMs(duration)) {
alignment = greatestDurationDenominator(dateIncrementDuration, !getWeeksFromInput(dateIncrementInput)).unit;
}
else {
alignment = unit;
}
}
else {
alignment = unit;
}
}
// if the view displays a single day or smaller
if (asRoughDays(duration) <= 1) {
if (this.isHiddenDay(start)) {
start = this.skipHiddenDays(start, direction);
start = startOfDay(start);
}
}
function computeRes() {
start = dateEnv.startOf(date, alignment);
end = dateEnv.add(start, duration);
res = { start: start, end: end };
}
computeRes();
// if range is completely enveloped by hidden days, go past the hidden days
if (!this.trimHiddenDays(res)) {
date = this.skipHiddenDays(date, direction);
computeRes();
}
return res;
};
// Builds the "current" range when a dayCount is specified.
DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) {
var dateEnv = this.dateEnv;
var customAlignment = this.options.dateAlignment;
var runningCount = 0;
var start = date;
var end;
if (customAlignment) {
start = dateEnv.startOf(start, customAlignment);
}
start = startOfDay(start);
start = this.skipHiddenDays(start, direction);
end = start;
do {
end = addDays(end, 1);
if (!this.isHiddenDay(end)) {
runningCount++;
}
} while (runningCount < dayCount);
return { start: start, end: end };
};
// Builds a normalized range object for the "visible" range,
// which is a way to define the currentRange and activeRange at the same time.
DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) {
var dateEnv = this.dateEnv;
var visibleRange = this.getRangeOption('visibleRange', dateEnv.toDate(date));
if (visibleRange && (visibleRange.start == null || visibleRange.end == null)) {
return null;
}
return visibleRange;
};
// Computes the range that will represent the element/cells for *rendering*,
// but which may have voided days/times.
// not responsible for trimming hidden days.
DateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
return currentRange;
};
// Compute the duration value that should be added/substracted to the current date
// when a prev/next operation happens.
DateProfileGenerator.prototype.buildDateIncrement = function (fallback) {
var dateIncrementInput = this.options.dateIncrement;
var customAlignment;
if (dateIncrementInput) {
return createDuration(dateIncrementInput);
}
else if ((customAlignment = this.options.dateAlignment)) {
return createDuration(1, customAlignment);
}
else if (fallback) {
return fallback;
}
else {
return createDuration({ days: 1 });
}
};
// Arguments after name will be forwarded to a hypothetical function value
// WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
// Always clone your objects if you fear mutation.
DateProfileGenerator.prototype.getRangeOption = function (name) {
var otherArgs = [];
for (var _i = 1; _i < arguments.length; _i++) {
otherArgs[_i - 1] = arguments[_i];
}
var val = this.options[name];
if (typeof val === 'function') {
val = val.apply(null, otherArgs);
}
if (val) {
val = parseRange(val, this.dateEnv);
}
if (val) {
val = computeVisibleDayRange(val);
}
return val;
};
/* Hidden Days
------------------------------------------------------------------------------------------------------------------*/
// Initializes internal variables related to calculating hidden days-of-week
DateProfileGenerator.prototype.initHiddenDays = function () {
var hiddenDays = this.options.hiddenDays || []; // array of day-of-week indices that are hidden
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
var dayCnt = 0;
var i;
if (this.options.weekends === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
for (i = 0; i < 7; i++) {
if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) {
dayCnt++;
}
}
if (!dayCnt) {
throw new Error('invalid hiddenDays'); // all days were hidden? bad.
}
this.isHiddenDayHash = isHiddenDayHash;
};
// Remove days from the beginning and end of the range that are computed as hidden.
// If the whole range is trimmed off, returns null
DateProfileGenerator.prototype.trimHiddenDays = function (range) {
var start = range.start;
var end = range.end;
if (start) {
start = this.skipHiddenDays(start);
}
if (end) {
end = this.skipHiddenDays(end, -1, true);
}
if (start == null || end == null || start < end) {
return { start: start, end: end };
}
return null;
};
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Date (used for UTC)
DateProfileGenerator.prototype.isHiddenDay = function (day) {
if (day instanceof Date) {
day = day.getUTCDay();
}
return this.isHiddenDayHash[day];
};
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// DOES NOT CONSIDER validRange!
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
DateProfileGenerator.prototype.skipHiddenDays = function (date, inc, isExclusive) {
if (inc === void 0) { inc = 1; }
if (isExclusive === void 0) { isExclusive = false; }
while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) {
date = addDays(date, inc);
}
return date;
};
return DateProfileGenerator;
}());
// TODO: find a way to avoid comparing DateProfiles. it's tedious
function isDateProfilesEqual(p0, p1) {
return rangesEqual(p0.validRange, p1.validRange) &&
rangesEqual(p0.activeRange, p1.activeRange) &&
rangesEqual(p0.renderRange, p1.renderRange) &&
durationsEqual(p0.minTime, p1.minTime) &&
durationsEqual(p0.maxTime, p1.maxTime);
/*
TODO: compare more?
currentRange: DateRange
currentRangeUnit: string
isRangeAllDay: boolean
isValid: boolean
dateIncrement: Duration
*/
}
function reduce (state, action, calendar) {
var viewType = reduceViewType(state.viewType, action);
var dateProfile = reduceDateProfile(state.dateProfile, action, state.currentDate, viewType, calendar);
var eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar);
var nextState = __assign({}, state, { viewType: viewType,
dateProfile: dateProfile, currentDate: reduceCurrentDate(state.currentDate, action, dateProfile), eventSources: eventSources, eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendar), dateSelection: reduceDateSelection(state.dateSelection, action, calendar), eventSelection: reduceSelectedEvent(state.eventSelection, action), eventDrag: reduceEventDrag(state.eventDrag, action, eventSources, calendar), eventResize: reduceEventResize(state.eventResize, action, eventSources, calendar), eventSourceLoadingLevel: computeLoadingLevel(eventSources), loadingLevel: computeLoadingLevel(eventSources) });
for (var _i = 0, _a = calendar.pluginSystem.hooks.reducers; _i < _a.length; _i++) {
var reducerFunc = _a[_i];
nextState = reducerFunc(nextState, action, calendar);
}
// console.log(action.type, nextState)
return nextState;
}
function reduceViewType(currentViewType, action) {
switch (action.type) {
case 'SET_VIEW_TYPE':
return action.viewType;
default:
return currentViewType;
}
}
function reduceDateProfile(currentDateProfile, action, currentDate, viewType, calendar) {
var newDateProfile;
switch (action.type) {
case 'PREV':
newDateProfile = calendar.dateProfileGenerators[viewType].buildPrev(currentDateProfile, currentDate);
break;
case 'NEXT':
newDateProfile = calendar.dateProfileGenerators[viewType].buildNext(currentDateProfile, currentDate);
break;
case 'SET_DATE':
if (!currentDateProfile.activeRange ||
!rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker)) {
newDateProfile = calendar.dateProfileGenerators[viewType].build(action.dateMarker, undefined, true // forceToValid
);
}
break;
case 'SET_VIEW_TYPE':
var generator = calendar.dateProfileGenerators[viewType];
if (!generator) {
throw new Error(viewType ?
'The FullCalendar view "' + viewType + '" does not exist. Make sure your plugins are loaded correctly.' :
'No available FullCalendar view plugins.');
}
newDateProfile = generator.build(action.dateMarker || currentDate, undefined, true // forceToValid
);
break;
}
if (newDateProfile &&
newDateProfile.isValid &&
!(currentDateProfile && isDateProfilesEqual(currentDateProfile, newDateProfile))) {
return newDateProfile;
}
else {
return currentDateProfile;
}
}
function reduceCurrentDate(currentDate, action, dateProfile) {
switch (action.type) {
case 'PREV':
case 'NEXT':
if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
return dateProfile.currentRange.start;
}
else {
return currentDate;
}
case 'SET_DATE':
case 'SET_VIEW_TYPE':
var newDate = action.dateMarker || currentDate;
if (dateProfile.activeRange && !rangeContainsMarker(dateProfile.activeRange, newDate)) {
return dateProfile.currentRange.start;
}
else {
return newDate;
}
default:
return currentDate;
}
}
function reduceDateSelection(currentSelection, action, calendar) {
switch (action.type) {
case 'SELECT_DATES':
return action.selection;
case 'UNSELECT_DATES':
return null;
default:
return currentSelection;
}
}
function reduceSelectedEvent(currentInstanceId, action) {
switch (action.type) {
case 'SELECT_EVENT':
return action.eventInstanceId;
case 'UNSELECT_EVENT':
return '';
default:
return currentInstanceId;
}
}
function reduceEventDrag(currentDrag, action, sources, calendar) {
switch (action.type) {
case 'SET_EVENT_DRAG':
var newDrag = action.state;
return {
affectedEvents: newDrag.affectedEvents,
mutatedEvents: newDrag.mutatedEvents,
isEvent: newDrag.isEvent,
origSeg: newDrag.origSeg
};
case 'UNSET_EVENT_DRAG':
return null;
default:
return currentDrag;
}
}
function reduceEventResize(currentResize, action, sources, calendar) {
switch (action.type) {
case 'SET_EVENT_RESIZE':
var newResize = action.state;
return {
affectedEvents: newResize.affectedEvents,
mutatedEvents: newResize.mutatedEvents,
isEvent: newResize.isEvent,
origSeg: newResize.origSeg
};
case 'UNSET_EVENT_RESIZE':
return null;
default:
return currentResize;
}
}
function computeLoadingLevel(eventSources) {
var cnt = 0;
for (var sourceId in eventSources) {
if (eventSources[sourceId].isFetching) {
cnt++;
}
}
return cnt;
}
var STANDARD_PROPS = {
start: null,
end: null,
allDay: Boolean
};
function parseDateSpan(raw, dateEnv, defaultDuration) {
var span = parseOpenDateSpan(raw, dateEnv);
var range = span.range;
if (!range.start) {
return null;
}
if (!range.end) {
if (defaultDuration == null) {
return null;
}
else {
range.end = dateEnv.add(range.start, defaultDuration);
}
}
return span;
}
/*
TODO: somehow combine with parseRange?
Will return null if the start/end props were present but parsed invalidly.
*/
function parseOpenDateSpan(raw, dateEnv) {
var leftovers = {};
var standardProps = refineProps(raw, STANDARD_PROPS, {}, leftovers);
var startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null;
var endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null;
var allDay = standardProps.allDay;
if (allDay == null) {
allDay = (startMeta && startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
// use this leftover object as the selection object
leftovers.range = {
start: startMeta ? startMeta.marker : null,
end: endMeta ? endMeta.marker : null
};
leftovers.allDay = allDay;
return leftovers;
}
function isDateSpansEqual(span0, span1) {
return rangesEqual(span0.range, span1.range) &&
span0.allDay === span1.allDay &&
isSpanPropsEqual(span0, span1);
}
// the NON-DATE-RELATED props
function isSpanPropsEqual(span0, span1) {
for (var propName in span1) {
if (propName !== 'range' && propName !== 'allDay') {
if (span0[propName] !== span1[propName]) {
return false;
}
}
}
// are there any props that span0 has that span1 DOESN'T have?
// both have range/allDay, so no need to special-case.
for (var propName in span0) {
if (!(propName in span1)) {
return false;
}
}
return true;
}
function buildDateSpanApi(span, dateEnv) {
return {
start: dateEnv.toDate(span.range.start),
end: dateEnv.toDate(span.range.end),
startStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
endStr: dateEnv.formatIso(span.range.end, { omitTime: span.allDay }),
allDay: span.allDay
};
}
function buildDatePointApi(span, dateEnv) {
return {
date: dateEnv.toDate(span.range.start),
dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
allDay: span.allDay
};
}
function fabricateEventRange(dateSpan, eventUiBases, calendar) {
var def = parseEventDef({ editable: false }, '', // sourceId
dateSpan.allDay, true, // hasEnd
calendar);
return {
def: def,
ui: compileEventUi(def, eventUiBases),
instance: createEventInstance(def.defId, dateSpan.range),
range: dateSpan.range,
isStart: true,
isEnd: true
};
}
function compileViewDefs(defaultConfigs, overrideConfigs) {
var hash = {};
var viewType;
for (viewType in defaultConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
for (viewType in overrideConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
return hash;
}
function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
if (hash[viewType]) {
return hash[viewType];
}
var viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
if (viewDef) {
hash[viewType] = viewDef;
}
return viewDef;
}
function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
var defaultConfig = defaultConfigs[viewType];
var overrideConfig = overrideConfigs[viewType];
var queryProp = function (name) {
return (defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null);
};
var theClass = queryProp('class');
var superType = queryProp('superType');
if (!superType && theClass) {
superType =
findViewNameBySubclass(theClass, overrideConfigs) ||
findViewNameBySubclass(theClass, defaultConfigs);
}
var superDef = null;
if (superType) {
if (superType === viewType) {
throw new Error('Can\'t have a custom view type that references itself');
}
superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
}
if (!theClass && superDef) {
theClass = superDef.class;
}
if (!theClass) {
return null; // don't throw a warning, might be settings for a single-unit view
}
return {
type: viewType,
class: theClass,
defaults: __assign({}, (superDef ? superDef.defaults : {}), (defaultConfig ? defaultConfig.options : {})),
overrides: __assign({}, (superDef ? superDef.overrides : {}), (overrideConfig ? overrideConfig.options : {}))
};
}
function findViewNameBySubclass(viewSubclass, configs) {
var superProto = Object.getPrototypeOf(viewSubclass.prototype);
for (var viewType in configs) {
var parsed = configs[viewType];
// need DIRECT subclass, so instanceof won't do it
if (parsed.class && parsed.class.prototype === superProto) {
return viewType;
}
}
return '';
}
function parseViewConfigs(inputs) {
return mapHash(inputs, parseViewConfig);
}
var VIEW_DEF_PROPS = {
type: String,
class: null
};
function parseViewConfig(input) {
if (typeof input === 'function') {
input = { class: input };
}
var options = {};
var props = refineProps(input, VIEW_DEF_PROPS, {}, options);
return {
superType: props.type,
class: props.class,
options: options
};
}
function buildViewSpecs(defaultInputs, optionsManager) {
var defaultConfigs = parseViewConfigs(defaultInputs);
var overrideConfigs = parseViewConfigs(optionsManager.overrides.views);
var viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
return mapHash(viewDefs, function (viewDef) {
return buildViewSpec(viewDef, overrideConfigs, optionsManager);
});
}
function buildViewSpec(viewDef, overrideConfigs, optionsManager) {
var durationInput = viewDef.overrides.duration ||
viewDef.defaults.duration ||
optionsManager.dynamicOverrides.duration ||
optionsManager.overrides.duration;
var duration = null;
var durationUnit = '';
var singleUnit = '';
var singleUnitOverrides = {};
if (durationInput) {
duration = createDuration(durationInput);
if (duration) { // valid?
var denom = greatestDurationDenominator(duration, !getWeeksFromInput(durationInput));
durationUnit = denom.unit;
if (denom.value === 1) {
singleUnit = durationUnit;
singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].options : {};
}
}
}
var queryButtonText = function (options) {
var buttonTextMap = options.buttonText || {};
var buttonTextKey = viewDef.defaults.buttonTextKey;
if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
return buttonTextMap[buttonTextKey];
}
if (buttonTextMap[viewDef.type] != null) {
return buttonTextMap[viewDef.type];
}
if (buttonTextMap[singleUnit] != null) {
return buttonTextMap[singleUnit];
}
};
return {
type: viewDef.type,
class: viewDef.class,
duration: duration,
durationUnit: durationUnit,
singleUnit: singleUnit,
options: __assign({}, globalDefaults, viewDef.defaults, optionsManager.dirDefaults, optionsManager.localeDefaults, optionsManager.overrides, singleUnitOverrides, viewDef.overrides, optionsManager.dynamicOverrides),
buttonTextOverride: queryButtonText(optionsManager.dynamicOverrides) ||
queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
viewDef.overrides.buttonText,
buttonTextDefault: queryButtonText(optionsManager.localeDefaults) ||
queryButtonText(optionsManager.dirDefaults) ||
viewDef.defaults.buttonText ||
queryButtonText(globalDefaults) ||
viewDef.type // fall back to given view name
};
}
var Toolbar = /** @class */ (function (_super) {
__extends(Toolbar, _super);
function Toolbar(context, extraClassName) {
var _this = _super.call(this, context) || this;
_this._renderLayout = memoizeRendering(_this.renderLayout, _this.unrenderLayout);
_this._updateTitle = memoizeRendering(_this.updateTitle, null, [_this._renderLayout]);
_this._updateActiveButton = memoizeRendering(_this.updateActiveButton, null, [_this._renderLayout]);
_this._updateToday = memoizeRendering(_this.updateToday, null, [_this._renderLayout]);
_this._updatePrev = memoizeRendering(_this.updatePrev, null, [_this._renderLayout]);
_this._updateNext = memoizeRendering(_this.updateNext, null, [_this._renderLayout]);
_this.el = createElement('div', { className: 'fc-toolbar ' + extraClassName });
return _this;
}
Toolbar.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderLayout.unrender(); // should unrender everything else
removeElement(this.el);
};
Toolbar.prototype.render = function (props) {
this._renderLayout(props.layout);
this._updateTitle(props.title);
this._updateActiveButton(props.activeButton);
this._updateToday(props.isTodayEnabled);
this._updatePrev(props.isPrevEnabled);
this._updateNext(props.isNextEnabled);
};
Toolbar.prototype.renderLayout = function (layout) {
var el = this.el;
this.viewsWithButtons = [];
appendToElement(el, this.renderSection('left', layout.left));
appendToElement(el, this.renderSection('center', layout.center));
appendToElement(el, this.renderSection('right', layout.right));
};
Toolbar.prototype.unrenderLayout = function () {
this.el.innerHTML = '';
};
Toolbar.prototype.renderSection = function (position, buttonStr) {
var _this = this;
var _a = this, theme = _a.theme, calendar = _a.calendar;
var optionsManager = calendar.optionsManager;
var viewSpecs = calendar.viewSpecs;
var sectionEl = createElement('div', { className: 'fc-' + position });
var calendarCustomButtons = optionsManager.computed.customButtons || {};
var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {};
var calendarButtonText = optionsManager.computed.buttonText || {};
if (buttonStr) {
buttonStr.split(' ').forEach(function (buttonGroupStr, i) {
var groupChildren = [];
var isOnlyButtons = true;
var groupEl;
buttonGroupStr.split(',').forEach(function (buttonName, j) {
var customButtonProps;
var viewSpec;
var buttonClick;
var buttonIcon; // only one of these will be set
var buttonText; // "
var buttonInnerHtml;
var buttonClasses;
var buttonEl;
var buttonAriaAttr;
if (buttonName === 'title') {
groupChildren.push(htmlToElement('
var CalendarComponent = /** @class */ (function (_super) {
__extends(CalendarComponent, _super);
function CalendarComponent(context, el) {
var _this = _super.call(this, context) || this;
_this._renderToolbars = memoizeRendering(_this.renderToolbars);
_this.buildViewPropTransformers = memoize(buildViewPropTransformers);
_this.el = el;
prependToElement(el, _this.contentEl = createElement('div', { className: 'fc-view-container' }));
var calendar = _this.calendar;
for (var _i = 0, _a = calendar.pluginSystem.hooks.viewContainerModifiers; _i < _a.length; _i++) {
var modifyViewContainer = _a[_i];
modifyViewContainer(_this.contentEl, calendar);
}
_this.toggleElClassNames(true);
_this.computeTitle = memoize(computeTitle);
_this.parseBusinessHours = memoize(function (input) {
return parseBusinessHours(input, _this.calendar);
});
return _this;
}
CalendarComponent.prototype.destroy = function () {
if (this.header) {
this.header.destroy();
}
if (this.footer) {
this.footer.destroy();
}
if (this.view) {
this.view.destroy();
}
removeElement(this.contentEl);
this.toggleElClassNames(false);
_super.prototype.destroy.call(this);
};
CalendarComponent.prototype.toggleElClassNames = function (bool) {
var classList = this.el.classList;
var dirClassName = 'fc-' + this.opt('dir');
var themeClassName = this.theme.getClass('widget');
if (bool) {
classList.add('fc');
classList.add(dirClassName);
classList.add(themeClassName);
}
else {
classList.remove('fc');
classList.remove(dirClassName);
classList.remove(themeClassName);
}
};
CalendarComponent.prototype.render = function (props) {
this.freezeHeight();
var title = this.computeTitle(props.dateProfile, props.viewSpec.options);
this._renderToolbars(props.viewSpec, props.dateProfile, props.currentDate, props.dateProfileGenerator, title);
this.renderView(props, title);
this.updateSize();
this.thawHeight();
};
CalendarComponent.prototype.renderToolbars = function (viewSpec, dateProfile, currentDate, dateProfileGenerator, title) {
var headerLayout = this.opt('header');
var footerLayout = this.opt('footer');
var now = this.calendar.getNow();
var todayInfo = dateProfileGenerator.build(now);
var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate);
var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate);
var toolbarProps = {
title: title,
activeButton: viewSpec.type,
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
isPrevEnabled: prevInfo.isValid,
isNextEnabled: nextInfo.isValid
};
if (headerLayout) {
if (!this.header) {
this.header = new Toolbar(this.context, 'fc-header-toolbar');
prependToElement(this.el, this.header.el);
}
this.header.receiveProps(__assign({ layout: headerLayout }, toolbarProps));
}
else if (this.header) {
this.header.destroy();
this.header = null;
}
if (footerLayout) {
if (!this.footer) {
this.footer = new Toolbar(this.context, 'fc-footer-toolbar');
appendToElement(this.el, this.footer.el);
}
this.footer.receiveProps(__assign({ layout: footerLayout }, toolbarProps));
}
else if (this.footer) {
this.footer.destroy();
this.footer = null;
}
};
CalendarComponent.prototype.renderView = function (props, title) {
var view = this.view;
var viewSpec = props.viewSpec, dateProfileGenerator = props.dateProfileGenerator;
if (!view || view.viewSpec !== viewSpec) {
if (view) {
view.destroy();
}
view = this.view = new viewSpec['class']({
calendar: this.calendar,
view: null,
dateEnv: this.dateEnv,
theme: this.theme,
options: viewSpec.options
}, viewSpec, dateProfileGenerator, this.contentEl);
}
else {
view.addScroll(view.queryScroll());
}
view.title = title; // for the API
var viewProps = {
dateProfile: props.dateProfile,
businessHours: this.parseBusinessHours(viewSpec.options.businessHours),
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize
};
var transformers = this.buildViewPropTransformers(this.calendar.pluginSystem.hooks.viewPropsTransformers);
for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) {
var transformer = transformers_1[_i];
__assign(viewProps, transformer.transform(viewProps, viewSpec, props, view));
}
view.receiveProps(viewProps);
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
CalendarComponent.prototype.updateSize = function (isResize) {
if (isResize === void 0) { isResize = false; }
var view = this.view;
if (isResize) {
view.addScroll(view.queryScroll());
}
if (isResize || this.isHeightAuto == null) {
this.computeHeightVars();
}
view.updateSize(isResize, this.viewHeight, this.isHeightAuto);
view.updateNowIndicator(); // we need to guarantee this will run after updateSize
view.popScroll(isResize);
};
CalendarComponent.prototype.computeHeightVars = function () {
var calendar = this.calendar; // yuck. need to handle dynamic options
var heightInput = calendar.opt('height');
var contentHeightInput = calendar.opt('contentHeight');
this.isHeightAuto = heightInput === 'auto' || contentHeightInput === 'auto';
if (typeof contentHeightInput === 'number') { // exists and not 'auto'
this.viewHeight = contentHeightInput;
}
else if (typeof contentHeightInput === 'function') { // exists and is a function
this.viewHeight = contentHeightInput();
}
else if (typeof heightInput === 'number') { // exists and not 'auto'
this.viewHeight = heightInput - this.queryToolbarsHeight();
}
else if (typeof heightInput === 'function') { // exists and is a function
this.viewHeight = heightInput() - this.queryToolbarsHeight();
}
else if (heightInput === 'parent') { // set to height of parent element
var parentEl = this.el.parentNode;
this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight();
}
else {
this.viewHeight = Math.round(this.contentEl.getBoundingClientRect().width /
Math.max(calendar.opt('aspectRatio'), .5));
}
};
CalendarComponent.prototype.queryToolbarsHeight = function () {
var height = 0;
if (this.header) {
height += computeHeightAndMargins(this.header.el);
}
if (this.footer) {
height += computeHeightAndMargins(this.footer.el);
}
return height;
};
// Height "Freezing"
// -----------------------------------------------------------------------------------------------------------------
CalendarComponent.prototype.freezeHeight = function () {
applyStyle(this.el, {
height: this.el.getBoundingClientRect().height,
overflow: 'hidden'
});
};
CalendarComponent.prototype.thawHeight = function () {
applyStyle(this.el, {
height: '',
overflow: ''
});
};
return CalendarComponent;
}(Component));
// Title and Date Formatting
// -----------------------------------------------------------------------------------------------------------------
// Computes what the title at the top of the calendar should be for this view
function computeTitle(dateProfile, viewOptions) {
var range;
// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
range = dateProfile.currentRange;
}
else { // for day units or smaller, use the actual day range
range = dateProfile.activeRange;
}
return this.dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || computeTitleFormat(dateProfile), viewOptions.titleRangeSeparator), { isEndExclusive: dateProfile.isRangeAllDay });
}
// Generates the format string that should be used to generate the title for the current date range.
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
function computeTitleFormat(dateProfile) {
var currentRangeUnit = dateProfile.currentRangeUnit;
if (currentRangeUnit === 'year') {
return { year: 'numeric' };
}
else if (currentRangeUnit === 'month') {
return { year: 'numeric', month: 'long' }; // like "September 2014"
}
else {
var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
if (days !== null && days > 1) {
// multi-day range. shorter, like "Sep 9 - 10 2014"
return { year: 'numeric', month: 'short', day: 'numeric' };
}
else {
// one day. longer, like "September 9 2014"
return { year: 'numeric', month: 'long', day: 'numeric' };
}
}
}
// Plugin
// -----------------------------------------------------------------------------------------------------------------
function buildViewPropTransformers(theClasses) {
return theClasses.map(function (theClass) {
return new theClass();
});
}
var Interaction = /** @class */ (function () {
function Interaction(settings) {
this.component = settings.component;
}
Interaction.prototype.destroy = function () {
};
return Interaction;
}());
function parseInteractionSettings(component, input) {
return {
component: component,
el: input.el,
useEventCenter: input.useEventCenter != null ? input.useEventCenter : true
};
}
function interactionSettingsToStore(settings) {
var _a;
return _a = {},
_a[settings.component.uid] = settings,
_a;
}
// global state
var interactionSettingsStore = {};
/*
Detects when the user clicks on an event within a DateComponent
*/
var EventClicking = /** @class */ (function (_super) {
__extends(EventClicking, _super);
function EventClicking(settings) {
var _this = _super.call(this, settings) || this;
_this.handleSegClick = function (ev, segEl) {
var component = _this.component;
var seg = getElSeg(segEl);
if (seg && // might be the
/*
Triggers events and adds/removes core classNames when the user's pointer
enters/leaves event-elements of a component.
*/
var EventHovering = /** @class */ (function (_super) {
__extends(EventHovering, _super);
function EventHovering(settings) {
var _this = _super.call(this, settings) || this;
// for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
_this.handleEventElRemove = function (el) {
if (el === _this.currentSegEl) {
_this.handleSegLeave(null, _this.currentSegEl);
}
};
_this.handleSegEnter = function (ev, segEl) {
if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
segEl.classList.add('fc-allow-mouse-resize');
_this.currentSegEl = segEl;
_this.triggerEvent('eventMouseEnter', ev, segEl);
}
};
_this.handleSegLeave = function (ev, segEl) {
if (_this.currentSegEl) {
segEl.classList.remove('fc-allow-mouse-resize');
_this.currentSegEl = null;
_this.triggerEvent('eventMouseLeave', ev, segEl);
}
};
var component = settings.component;
_this.removeHoverListeners = listenToHoverBySelector(component.el, component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegEnter, _this.handleSegLeave);
component.calendar.on('eventElRemove', _this.handleEventElRemove);
return _this;
}
EventHovering.prototype.destroy = function () {
this.removeHoverListeners();
this.component.calendar.off('eventElRemove', this.handleEventElRemove);
};
EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) {
var component = this.component;
var seg = getElSeg(segEl);
if (!ev || component.isValidSegDownEl(ev.target)) {
component.publiclyTrigger(publicEvName, [
{
el: segEl,
event: new EventApi(this.component.calendar, seg.eventRange.def, seg.eventRange.instance),
jsEvent: ev,
view: component.view
}
]);
}
};
return EventHovering;
}(Interaction));
var StandardTheme = /** @class */ (function (_super) {
__extends(StandardTheme, _super);
function StandardTheme() {
return _super !== null && _super.apply(this, arguments) || this;
}
return StandardTheme;
}(Theme));
StandardTheme.prototype.classes = {
widget: 'fc-unthemed',
widgetHeader: 'fc-widget-header',
widgetContent: 'fc-widget-content',
buttonGroup: 'fc-button-group',
button: 'fc-button fc-button-primary',
buttonActive: 'fc-button-active',
popoverHeader: 'fc-widget-header',
popoverContent: 'fc-widget-content',
// day grid
headerRow: 'fc-widget-header',
dayRow: 'fc-widget-content',
// list view
listView: 'fc-widget-content'
};
StandardTheme.prototype.baseIconClass = 'fc-icon';
StandardTheme.prototype.iconClasses = {
close: 'fc-icon-x',
prev: 'fc-icon-chevron-left',
next: 'fc-icon-chevron-right',
prevYear: 'fc-icon-chevrons-left',
nextYear: 'fc-icon-chevrons-right'
};
StandardTheme.prototype.iconOverrideOption = 'buttonIcons';
StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
var Calendar = /** @class */ (function () {
function Calendar(el, overrides) {
var _this = this;
this.parseRawLocales = memoize(parseRawLocales);
this.buildLocale = memoize(buildLocale);
this.buildDateEnv = memoize(buildDateEnv);
this.buildTheme = memoize(buildTheme);
this.buildEventUiSingleBase = memoize(this._buildEventUiSingleBase);
this.buildSelectionConfig = memoize(this._buildSelectionConfig);
this.buildEventUiBySource = memoizeOutput(buildEventUiBySource, isPropsEqual);
this.buildEventUiBases = memoize(buildEventUiBases);
this.interactionsStore = {};
this.actionQueue = [];
this.isReducing = false;
// isDisplaying: boolean = false // installed in DOM? accepting renders?
this.needsRerender = false; // needs a render?
this.needsFullRerender = false;
this.isRendering = false; // currently in the executeRender function?
this.renderingPauseDepth = 0;
this.buildDelayedRerender = memoize(buildDelayedRerender);
this.afterSizingTriggers = {};
this.isViewUpdated = false;
this.isDatesUpdated = false;
this.isEventsUpdated = false;
this.el = el;
this.optionsManager = new OptionsManager(overrides || {});
this.pluginSystem = new PluginSystem();
// only do once. don't do in handleOptions. because can't remove plugins
this.addPluginInputs(this.optionsManager.computed.plugins || []);
this.handleOptions(this.optionsManager.computed);
this.publiclyTrigger('_init'); // for tests
this.hydrate();
this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions
.map(function (calendarInteractionClass) {
return new calendarInteractionClass(_this);
});
}
Calendar.prototype.addPluginInputs = function (pluginInputs) {
var pluginDefs = refinePluginDefs(pluginInputs);
for (var _i = 0, pluginDefs_1 = pluginDefs; _i < pluginDefs_1.length; _i++) {
var pluginDef = pluginDefs_1[_i];
this.pluginSystem.add(pluginDef);
}
};
Object.defineProperty(Calendar.prototype, "view", {
// public API
get: function () {
return this.component ? this.component.view : null;
},
enumerable: true,
configurable: true
});
// Public API for rendering
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.render = function () {
if (!this.component) {
this.renderableEventStore = createEmptyEventStore();
this.bindHandlers();
this.executeRender();
}
else {
this.requestRerender(true);
}
};
Calendar.prototype.destroy = function () {
if (this.component) {
this.unbindHandlers();
this.component.destroy(); // don't null-out. in case API needs access
this.component = null; // umm ???
for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) {
var interaction = _a[_i];
interaction.destroy();
}
this.publiclyTrigger('_destroyed');
}
};
// Handlers
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.bindHandlers = function () {
var _this = this;
// event delegation for nav links
this.removeNavLinkListener = listenBySelector(this.el, 'click', 'a[data-goto]', function (ev, anchorEl) {
var gotoOptions = anchorEl.getAttribute('data-goto');
gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {};
var dateEnv = _this.dateEnv;
var dateMarker = dateEnv.createMarker(gotoOptions.date);
var viewType = gotoOptions.type;
// property like "navLinkDayClick". might be a string or a function
var customAction = _this.viewOpt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
if (typeof customAction === 'function') {
customAction(dateEnv.toDate(dateMarker), ev);
}
else {
if (typeof customAction === 'string') {
viewType = customAction;
}
_this.zoomTo(dateMarker, viewType);
}
});
if (this.opt('handleWindowResize')) {
window.addEventListener('resize', this.windowResizeProxy = debounce(// prevents rapid calls
this.windowResize.bind(this), this.opt('windowResizeDelay')));
}
};
Calendar.prototype.unbindHandlers = function () {
this.removeNavLinkListener();
if (this.windowResizeProxy) {
window.removeEventListener('resize', this.windowResizeProxy);
this.windowResizeProxy = null;
}
};
// Dispatcher
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.hydrate = function () {
var _this = this;
this.state = this.buildInitialState();
var rawSources = this.opt('eventSources') || [];
var singleRawSource = this.opt('events');
var sources = []; // parsed
if (singleRawSource) {
rawSources.unshift(singleRawSource);
}
for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) {
var rawSource = rawSources_1[_i];
var source = parseEventSource(rawSource, this);
if (source) {
sources.push(source);
}
}
this.batchRendering(function () {
_this.dispatch({ type: 'INIT' }); // pass in sources here?
_this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: sources });
_this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: _this.opt('defaultView') || _this.pluginSystem.hooks.defaultView
});
});
};
Calendar.prototype.buildInitialState = function () {
return {
viewType: null,
loadingLevel: 0,
eventSourceLoadingLevel: 0,
currentDate: this.getInitialDate(),
dateProfile: null,
eventSources: {},
eventStore: createEmptyEventStore(),
dateSelection: null,
eventSelection: '',
eventDrag: null,
eventResize: null
};
};
Calendar.prototype.dispatch = function (action) {
this.actionQueue.push(action);
if (!this.isReducing) {
this.isReducing = true;
var oldState = this.state;
while (this.actionQueue.length) {
this.state = this.reduce(this.state, this.actionQueue.shift(), this);
}
var newState = this.state;
this.isReducing = false;
if (!oldState.loadingLevel && newState.loadingLevel) {
this.publiclyTrigger('loading', [true]);
}
else if (oldState.loadingLevel && !newState.loadingLevel) {
this.publiclyTrigger('loading', [false]);
}
var view = this.component && this.component.view;
if (oldState.eventStore !== newState.eventStore || this.needsFullRerender) {
if (oldState.eventStore) {
this.isEventsUpdated = true;
}
}
if (oldState.dateProfile !== newState.dateProfile || this.needsFullRerender) {
if (oldState.dateProfile && view) { // why would view be null!?
this.publiclyTrigger('datesDestroy', [
{
view: view,
el: view.el
}
]);
}
this.isDatesUpdated = true;
}
if (oldState.viewType !== newState.viewType || this.needsFullRerender) {
if (oldState.viewType && view) { // why would view be null!?
this.publiclyTrigger('viewSkeletonDestroy', [
{
view: view,
el: view.el
}
]);
}
this.isViewUpdated = true;
}
this.requestRerender();
}
};
Calendar.prototype.reduce = function (state, action, calendar) {
return reduce(state, action, calendar);
};
// Render Queue
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.requestRerender = function (needsFull) {
if (needsFull === void 0) { needsFull = false; }
this.needsRerender = true;
this.needsFullRerender = this.needsFullRerender || needsFull;
this.delayedRerender(); // will call a debounced-version of tryRerender
};
Calendar.prototype.tryRerender = function () {
if (this.component && // must be accepting renders
this.needsRerender && // indicates that a rerender was requested
!this.renderingPauseDepth && // not paused
!this.isRendering // not currently in the render loop
) {
this.executeRender();
}
};
Calendar.prototype.batchRendering = function (func) {
this.renderingPauseDepth++;
func();
this.renderingPauseDepth--;
if (this.needsRerender) {
this.requestRerender();
}
};
// Rendering
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.executeRender = function () {
var needsFullRerender = this.needsFullRerender; // save before clearing
// clear these BEFORE the render so that new values will accumulate during render
this.needsRerender = false;
this.needsFullRerender = false;
this.isRendering = true;
this.renderComponent(needsFullRerender);
this.isRendering = false;
// received a rerender request while rendering
if (this.needsRerender) {
this.delayedRerender();
}
};
/*
don't call this directly. use executeRender instead
*/
Calendar.prototype.renderComponent = function (needsFull) {
var _a = this, state = _a.state, component = _a.component;
var viewType = state.viewType;
var viewSpec = this.viewSpecs[viewType];
var savedScroll = (needsFull && component) ? component.view.queryScroll() : null;
if (!viewSpec) {
throw new Error("View type \"" + viewType + "\" is not valid");
}
// if event sources are still loading and progressive rendering hasn't been enabled,
// keep rendering the last fully loaded set of events
var renderableEventStore = this.renderableEventStore =
(state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ?
this.renderableEventStore :
state.eventStore;
var eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options);
var eventUiBySource = this.buildEventUiBySource(state.eventSources);
var eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
if (needsFull || !component) {
if (component) {
component.freezeHeight(); // next component will unfreeze it
component.destroy();
}
component = this.component = new CalendarComponent({
calendar: this,
view: null,
dateEnv: this.dateEnv,
theme: this.theme,
options: this.optionsManager.computed
}, this.el);
this.isViewUpdated = true;
this.isDatesUpdated = true;
this.isEventsUpdated = true;
}
component.receiveProps(__assign({}, state, { viewSpec: viewSpec, dateProfile: state.dateProfile, dateProfileGenerator: this.dateProfileGenerators[viewType], eventStore: renderableEventStore, eventUiBases: eventUiBases, dateSelection: state.dateSelection, eventSelection: state.eventSelection, eventDrag: state.eventDrag, eventResize: state.eventResize }));
if (savedScroll) {
component.view.applyScroll(savedScroll, false);
}
if (this.isViewUpdated) {
this.isViewUpdated = false;
this.publiclyTrigger('viewSkeletonRender', [
{
view: component.view,
el: component.view.el
}
]);
}
if (this.isDatesUpdated) {
this.isDatesUpdated = false;
this.publiclyTrigger('datesRender', [
{
view: component.view,
el: component.view.el
}
]);
}
if (this.isEventsUpdated) {
this.isEventsUpdated = false;
}
this.releaseAfterSizingTriggers();
};
// Options
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.setOption = function (name, val) {
var _a;
this.mutateOptions((_a = {}, _a[name] = val, _a), [], true);
};
Calendar.prototype.getOption = function (name) {
return this.optionsManager.computed[name];
};
Calendar.prototype.opt = function (name) {
return this.optionsManager.computed[name];
};
Calendar.prototype.viewOpt = function (name) {
return this.viewOpts()[name];
};
Calendar.prototype.viewOpts = function () {
return this.viewSpecs[this.state.viewType].options;
};
/*
handles option changes (like a diff)
*/
Calendar.prototype.mutateOptions = function (updates, removals, isDynamic, deepEqual) {
var _this = this;
var changeHandlers = this.pluginSystem.hooks.optionChangeHandlers;
var normalUpdates = {};
var specialUpdates = {};
var oldDateEnv = this.dateEnv; // do this before handleOptions
var isTimeZoneDirty = false;
var isSizeDirty = false;
var anyDifficultOptions = Boolean(removals.length);
for (var name_1 in updates) {
if (changeHandlers[name_1]) {
specialUpdates[name_1] = updates[name_1];
}
else {
normalUpdates[name_1] = updates[name_1];
}
}
for (var name_2 in normalUpdates) {
if (/^(height|contentHeight|aspectRatio)$/.test(name_2)) {
isSizeDirty = true;
}
else if (/^(defaultDate|defaultView)$/.test(name_2)) ;
else {
anyDifficultOptions = true;
if (name_2 === 'timeZone') {
isTimeZoneDirty = true;
}
}
}
this.optionsManager.mutate(normalUpdates, removals, isDynamic);
if (anyDifficultOptions) {
this.handleOptions(this.optionsManager.computed);
this.needsFullRerender = true;
}
this.batchRendering(function () {
if (anyDifficultOptions) {
if (isTimeZoneDirty) {
_this.dispatch({
type: 'CHANGE_TIMEZONE',
oldDateEnv: oldDateEnv
});
}
/* HACK
has the same effect as calling this.requestRerender(true)
but recomputes the state's dateProfile
*/
_this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: _this.state.viewType
});
}
else if (isSizeDirty) {
_this.updateSize();
}
// special updates
if (deepEqual) {
for (var name_3 in specialUpdates) {
changeHandlers[name_3](specialUpdates[name_3], _this, deepEqual);
}
}
});
};
/*
rebuilds things based off of a complete set of refined options
*/
Calendar.prototype.handleOptions = function (options) {
var _this = this;
var pluginHooks = this.pluginSystem.hooks;
this.defaultAllDayEventDuration = createDuration(options.defaultAllDayEventDuration);
this.defaultTimedEventDuration = createDuration(options.defaultTimedEventDuration);
this.delayedRerender = this.buildDelayedRerender(options.rerenderDelay);
this.theme = this.buildTheme(options);
var available = this.parseRawLocales(options.locales);
this.availableRawLocales = available.map;
var locale = this.buildLocale(options.locale || available.defaultCode, available.map);
this.dateEnv = this.buildDateEnv(locale, options.timeZone, pluginHooks.namedTimeZonedImpl, options.firstDay, options.weekNumberCalculation, options.weekLabel, pluginHooks.cmdFormatter);
this.selectionConfig = this.buildSelectionConfig(options); // needs dateEnv. do after :(
// ineffecient to do every time?
this.viewSpecs = buildViewSpecs(pluginHooks.views, this.optionsManager);
// ineffecient to do every time?
this.dateProfileGenerators = mapHash(this.viewSpecs, function (viewSpec) {
return new viewSpec.class.prototype.dateProfileGeneratorClass(viewSpec, _this);
});
};
Calendar.prototype.getAvailableLocaleCodes = function () {
return Object.keys(this.availableRawLocales);
};
Calendar.prototype._buildSelectionConfig = function (rawOpts) {
return processScopedUiProps('select', rawOpts, this);
};
Calendar.prototype._buildEventUiSingleBase = function (rawOpts) {
if (rawOpts.editable) { // so 'editable' affected events
rawOpts = __assign({}, rawOpts, { eventEditable: true });
}
return processScopedUiProps('event', rawOpts, this);
};
// Trigger
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.hasPublicHandlers = function (name) {
return this.hasHandlers(name) ||
this.opt(name); // handler specified in options
};
Calendar.prototype.publiclyTrigger = function (name, args) {
var optHandler = this.opt(name);
this.triggerWith(name, this, args);
if (optHandler) {
return optHandler.apply(this, args);
}
};
Calendar.prototype.publiclyTriggerAfterSizing = function (name, args) {
var afterSizingTriggers = this.afterSizingTriggers;
(afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args);
};
Calendar.prototype.releaseAfterSizingTriggers = function () {
var afterSizingTriggers = this.afterSizingTriggers;
for (var name_4 in afterSizingTriggers) {
for (var _i = 0, _a = afterSizingTriggers[name_4]; _i < _a.length; _i++) {
var args = _a[_i];
this.publiclyTrigger(name_4, args);
}
}
this.afterSizingTriggers = {};
};
// View
// -----------------------------------------------------------------------------------------------------------------
// Returns a boolean about whether the view is okay to instantiate at some point
Calendar.prototype.isValidViewType = function (viewType) {
return Boolean(this.viewSpecs[viewType]);
};
Calendar.prototype.changeView = function (viewType, dateOrRange) {
var dateMarker = null;
if (dateOrRange) {
if (dateOrRange.start && dateOrRange.end) { // a range
this.optionsManager.mutate({ visibleRange: dateOrRange }, []); // will not rerender
this.handleOptions(this.optionsManager.computed); // ...but yuck
}
else { // a date
dateMarker = this.dateEnv.createMarker(dateOrRange); // just like gotoDate
}
}
this.unselect();
this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: viewType,
dateMarker: dateMarker
});
};
// Forces navigation to a view for the given date.
// `viewType` can be a specific view name or a generic one like "week" or "day".
// needs to change
Calendar.prototype.zoomTo = function (dateMarker, viewType) {
var spec;
viewType = viewType || 'day'; // day is default zoom
spec = this.viewSpecs[viewType] ||
this.getUnitViewSpec(viewType);
this.unselect();
if (spec) {
this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: spec.type,
dateMarker: dateMarker
});
}
else {
this.dispatch({
type: 'SET_DATE',
dateMarker: dateMarker
});
}
};
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
// Preference is given to views that have corresponding buttons.
Calendar.prototype.getUnitViewSpec = function (unit) {
var component = this.component;
var viewTypes = [];
var i;
var spec;
// put views that have buttons first. there will be duplicates, but oh
if (component.header) {
viewTypes.push.apply(viewTypes, component.header.viewsWithButtons);
}
if (component.footer) {
viewTypes.push.apply(viewTypes, component.footer.viewsWithButtons);
}
for (var viewType in this.viewSpecs) {
viewTypes.push(viewType);
}
for (i = 0; i < viewTypes.length; i++) {
spec = this.viewSpecs[viewTypes[i]];
if (spec) {
if (spec.singleUnit === unit) {
return spec;
}
}
}
};
// Current Date
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.getInitialDate = function () {
var defaultDateInput = this.opt('defaultDate');
// compute the initial ambig-timezone date
if (defaultDateInput != null) {
return this.dateEnv.createMarker(defaultDateInput);
}
else {
return this.getNow(); // getNow already returns unzoned
}
};
Calendar.prototype.prev = function () {
this.unselect();
this.dispatch({ type: 'PREV' });
};
Calendar.prototype.next = function () {
this.unselect();
this.dispatch({ type: 'NEXT' });
};
Calendar.prototype.prevYear = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.addYears(this.state.currentDate, -1)
});
};
Calendar.prototype.nextYear = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.addYears(this.state.currentDate, 1)
});
};
Calendar.prototype.today = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.getNow()
});
};
Calendar.prototype.gotoDate = function (zonedDateInput) {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.createMarker(zonedDateInput)
});
};
Calendar.prototype.incrementDate = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // else, warn about invalid input?
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.add(this.state.currentDate, delta)
});
}
};
// for external API
Calendar.prototype.getDate = function () {
return this.dateEnv.toDate(this.state.currentDate);
};
// Date Formatting Utils
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.formatDate = function (d, formatter) {
var dateEnv = this.dateEnv;
return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter));
};
// `settings` is for formatter AND isEndExclusive
Calendar.prototype.formatRange = function (d0, d1, settings) {
var dateEnv = this.dateEnv;
return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings, this.opt('defaultRangeSeparator')), settings);
};
Calendar.prototype.formatIso = function (d, omitTime) {
var dateEnv = this.dateEnv;
return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime });
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.windowResize = function (ev) {
if (!this.isHandlingWindowResize &&
this.component && // why?
ev.target === window // not a jqui resize event
) {
this.isHandlingWindowResize = true;
this.updateSize();
this.publiclyTrigger('windowResize', [this.view]);
this.isHandlingWindowResize = false;
}
};
Calendar.prototype.updateSize = function () {
if (this.component) { // when?
this.component.updateSize(true);
}
};
// Component Registration
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.registerInteractiveComponent = function (component, settingsInput) {
var settings = parseInteractionSettings(component, settingsInput);
var DEFAULT_INTERACTIONS = [
EventClicking,
EventHovering
];
var interactionClasses = DEFAULT_INTERACTIONS.concat(this.pluginSystem.hooks.componentInteractions);
var interactions = interactionClasses.map(function (interactionClass) {
return new interactionClass(settings);
});
this.interactionsStore[component.uid] = interactions;
interactionSettingsStore[component.uid] = settings;
};
Calendar.prototype.unregisterInteractiveComponent = function (component) {
for (var _i = 0, _a = this.interactionsStore[component.uid]; _i < _a.length; _i++) {
var listener = _a[_i];
listener.destroy();
}
delete this.interactionsStore[component.uid];
delete interactionSettingsStore[component.uid];
};
// Date Selection / Event Selection / DayClick
// -----------------------------------------------------------------------------------------------------------------
// this public method receives start/end dates in any format, with any timezone
// NOTE: args were changed from v3
Calendar.prototype.select = function (dateOrObj, endDate) {
var selectionInput;
if (endDate == null) {
if (dateOrObj.start != null) {
selectionInput = dateOrObj;
}
else {
selectionInput = {
start: dateOrObj,
end: null
};
}
}
else {
selectionInput = {
start: dateOrObj,
end: endDate
};
}
var selection = parseDateSpan(selectionInput, this.dateEnv, createDuration({ days: 1 }) // TODO: cache this?
);
if (selection) { // throw parse error otherwise?
this.dispatch({ type: 'SELECT_DATES', selection: selection });
this.triggerDateSelect(selection);
}
};
// public method
Calendar.prototype.unselect = function (pev) {
if (this.state.dateSelection) {
this.dispatch({ type: 'UNSELECT_DATES' });
this.triggerDateUnselect(pev);
}
};
Calendar.prototype.triggerDateSelect = function (selection, pev) {
var arg = __assign({}, this.buildDateSpanApi(selection), { jsEvent: pev ? pev.origEvent : null, view: this.view });
this.publiclyTrigger('select', [arg]);
};
Calendar.prototype.triggerDateUnselect = function (pev) {
this.publiclyTrigger('unselect', [
{
jsEvent: pev ? pev.origEvent : null,
view: this.view
}
]);
};
// TODO: receive pev?
Calendar.prototype.triggerDateClick = function (dateSpan, dayEl, view, ev) {
var arg = __assign({}, this.buildDatePointApi(dateSpan), { dayEl: dayEl, jsEvent: ev, // Is this always a mouse event? See #4655
view: view });
this.publiclyTrigger('dateClick', [arg]);
};
Calendar.prototype.buildDatePointApi = function (dateSpan) {
var props = {};
for (var _i = 0, _a = this.pluginSystem.hooks.datePointTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(props, transform(dateSpan, this));
}
__assign(props, buildDatePointApi(dateSpan, this.dateEnv));
return props;
};
Calendar.prototype.buildDateSpanApi = function (dateSpan) {
var props = {};
for (var _i = 0, _a = this.pluginSystem.hooks.dateSpanTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(props, transform(dateSpan, this));
}
__assign(props, buildDateSpanApi(dateSpan, this.dateEnv));
return props;
};
// Date Utils
// -----------------------------------------------------------------------------------------------------------------
// Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
Calendar.prototype.getNow = function () {
var now = this.opt('now');
if (typeof now === 'function') {
now = now();
}
if (now == null) {
return this.dateEnv.createNowMarker();
}
return this.dateEnv.createMarker(now);
};
// Event-Date Utilities
// -----------------------------------------------------------------------------------------------------------------
// Given an event's allDay status and start date, return what its fallback end date should be.
// TODO: rename to computeDefaultEventEnd
Calendar.prototype.getDefaultEventEnd = function (allDay, marker) {
var end = marker;
if (allDay) {
end = startOfDay(end);
end = this.dateEnv.add(end, this.defaultAllDayEventDuration);
}
else {
end = this.dateEnv.add(end, this.defaultTimedEventDuration);
}
return end;
};
// Public Events API
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.addEvent = function (eventInput, sourceInput) {
if (eventInput instanceof EventApi) {
var def = eventInput._def;
var instance = eventInput._instance;
// not already present? don't want to add an old snapshot
if (!this.state.eventStore.defs[def.defId]) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore({ def: def, instance: instance }) // TODO: better util for two args?
});
}
return eventInput;
}
var sourceId;
if (sourceInput instanceof EventSourceApi) {
sourceId = sourceInput.internalEventSource.sourceId;
}
else if (sourceInput != null) {
var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function
if (!sourceApi) {
console.warn('Could not find an event source with ID "' + sourceInput + '"'); // TODO: test
return null;
}
else {
sourceId = sourceApi.internalEventSource.sourceId;
}
}
var tuple = parseEvent(eventInput, sourceId, this);
if (tuple) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore(tuple)
});
return new EventApi(this, tuple.def, tuple.def.recurringDef ? null : tuple.instance);
}
return null;
};
// TODO: optimize
Calendar.prototype.getEventById = function (id) {
var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances;
id = String(id);
for (var defId in defs) {
var def = defs[defId];
if (def.publicId === id) {
if (def.recurringDef) {
return new EventApi(this, def, null);
}
else {
for (var instanceId in instances) {
var instance = instances[instanceId];
if (instance.defId === def.defId) {
return new EventApi(this, def, instance);
}
}
}
}
}
return null;
};
Calendar.prototype.getEvents = function () {
var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances;
var eventApis = [];
for (var id in instances) {
var instance = instances[id];
var def = defs[instance.defId];
eventApis.push(new EventApi(this, def, instance));
}
return eventApis;
};
Calendar.prototype.removeAllEvents = function () {
this.dispatch({ type: 'REMOVE_ALL_EVENTS' });
};
Calendar.prototype.rerenderEvents = function () {
this.dispatch({ type: 'RESET_EVENTS' });
};
// Public Event Sources API
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.getEventSources = function () {
var sourceHash = this.state.eventSources;
var sourceApis = [];
for (var internalId in sourceHash) {
sourceApis.push(new EventSourceApi(this, sourceHash[internalId]));
}
return sourceApis;
};
Calendar.prototype.getEventSourceById = function (id) {
var sourceHash = this.state.eventSources;
id = String(id);
for (var sourceId in sourceHash) {
if (sourceHash[sourceId].publicId === id) {
return new EventSourceApi(this, sourceHash[sourceId]);
}
}
return null;
};
Calendar.prototype.addEventSource = function (sourceInput) {
if (sourceInput instanceof EventSourceApi) {
// not already present? don't want to add an old snapshot
if (!this.state.eventSources[sourceInput.internalEventSource.sourceId]) {
this.dispatch({
type: 'ADD_EVENT_SOURCES',
sources: [sourceInput.internalEventSource]
});
}
return sourceInput;
}
var eventSource = parseEventSource(sourceInput, this);
if (eventSource) { // TODO: error otherwise?
this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] });
return new EventSourceApi(this, eventSource);
}
return null;
};
Calendar.prototype.removeAllEventSources = function () {
this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' });
};
Calendar.prototype.refetchEvents = function () {
this.dispatch({ type: 'FETCH_EVENT_SOURCES' });
};
// Scroll
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.scrollToTime = function (timeInput) {
var duration = createDuration(timeInput);
if (duration) {
this.component.view.scrollToDuration(duration);
}
};
return Calendar;
}());
EmitterMixin.mixInto(Calendar);
// for memoizers
// -----------------------------------------------------------------------------------------------------------------
function buildDateEnv(locale, timeZone, namedTimeZoneImpl, firstDay, weekNumberCalculation, weekLabel, cmdFormatter) {
return new DateEnv({
calendarSystem: 'gregory',
timeZone: timeZone,
namedTimeZoneImpl: namedTimeZoneImpl,
locale: locale,
weekNumberCalculation: weekNumberCalculation,
firstDay: firstDay,
weekLabel: weekLabel,
cmdFormatter: cmdFormatter
});
}
function buildTheme(calendarOptions) {
var themeClass = this.pluginSystem.hooks.themeClasses[calendarOptions.themeSystem] || StandardTheme;
return new themeClass(calendarOptions);
}
function buildDelayedRerender(wait) {
var func = this.tryRerender.bind(this);
if (wait != null) {
func = debounce(func, wait);
}
return func;
}
function buildEventUiBySource(eventSources) {
return mapHash(eventSources, function (eventSource) {
return eventSource.ui;
});
}
function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
var eventUiBases = { '': eventUiSingleBase };
for (var defId in eventDefs) {
var def = eventDefs[defId];
if (def.sourceId && eventUiBySource[def.sourceId]) {
eventUiBases[defId] = eventUiBySource[def.sourceId];
}
}
return eventUiBases;
}
var View = /** @class */ (function (_super) {
__extends(View, _super);
function View(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' }), true // isView (HACK)
) || this;
_this.renderDatesMem = memoizeRendering(_this.renderDatesWrap, _this.unrenderDatesWrap);
_this.renderBusinessHoursMem = memoizeRendering(_this.renderBusinessHours, _this.unrenderBusinessHours, [_this.renderDatesMem]);
_this.renderDateSelectionMem = memoizeRendering(_this.renderDateSelectionWrap, _this.unrenderDateSelectionWrap, [_this.renderDatesMem]);
_this.renderEventsMem = memoizeRendering(_this.renderEvents, _this.unrenderEvents, [_this.renderDatesMem]);
_this.renderEventSelectionMem = memoizeRendering(_this.renderEventSelectionWrap, _this.unrenderEventSelectionWrap, [_this.renderEventsMem]);
_this.renderEventDragMem = memoizeRendering(_this.renderEventDragWrap, _this.unrenderEventDragWrap, [_this.renderDatesMem]);
_this.renderEventResizeMem = memoizeRendering(_this.renderEventResizeWrap, _this.unrenderEventResizeWrap, [_this.renderDatesMem]);
_this.viewSpec = viewSpec;
_this.dateProfileGenerator = dateProfileGenerator;
_this.type = viewSpec.type;
_this.eventOrderSpecs = parseFieldSpecs(_this.opt('eventOrder'));
_this.nextDayThreshold = createDuration(_this.opt('nextDayThreshold'));
parentEl.appendChild(_this.el);
_this.initialize();
return _this;
}
View.prototype.initialize = function () {
};
Object.defineProperty(View.prototype, "activeStart", {
// Date Setting/Unsetting
// -----------------------------------------------------------------------------------------------------------------
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.activeRange.start);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "activeEnd", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.activeRange.end);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "currentStart", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.currentRange.start);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "currentEnd", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.currentRange.end);
},
enumerable: true,
configurable: true
});
// General Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.render = function (props) {
this.renderDatesMem(props.dateProfile);
this.renderBusinessHoursMem(props.businessHours);
this.renderDateSelectionMem(props.dateSelection);
this.renderEventsMem(props.eventStore);
this.renderEventSelectionMem(props.eventSelection);
this.renderEventDragMem(props.eventDrag);
this.renderEventResizeMem(props.eventResize);
};
View.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderDatesMem.unrender(); // should unrender everything else
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
View.prototype.updateSize = function (isResize, viewHeight, isAuto) {
var calendar = this.calendar;
if (isResize || // HACKS...
calendar.isViewUpdated ||
calendar.isDatesUpdated ||
calendar.isEventsUpdated) {
// sort of the catch-all sizing
// anything that might cause dimension changes
this.updateBaseSize(isResize, viewHeight, isAuto);
}
};
View.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) {
};
// Date Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderDatesWrap = function (dateProfile) {
this.renderDates(dateProfile);
this.addScroll({
duration: createDuration(this.opt('scrollTime'))
});
this.startNowIndicator(dateProfile); // shouldn't render yet because updateSize will be called soon
};
View.prototype.unrenderDatesWrap = function () {
this.stopNowIndicator();
this.unrenderDates();
};
View.prototype.renderDates = function (dateProfile) { };
View.prototype.unrenderDates = function () { };
// Business Hours
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderBusinessHours = function (businessHours) { };
View.prototype.unrenderBusinessHours = function () { };
// Date Selection
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderDateSelectionWrap = function (selection) {
if (selection) {
this.renderDateSelection(selection);
}
};
View.prototype.unrenderDateSelectionWrap = function (selection) {
if (selection) {
this.unrenderDateSelection(selection);
}
};
View.prototype.renderDateSelection = function (selection) { };
View.prototype.unrenderDateSelection = function (selection) { };
// Event Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEvents = function (eventStore) { };
View.prototype.unrenderEvents = function () { };
// util for subclasses
View.prototype.sliceEvents = function (eventStore, allDay) {
var props = this.props;
return sliceEventStore(eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? this.nextDayThreshold : null).fg;
};
View.prototype.computeEventDraggable = function (eventDef, eventUi) {
var transformers = this.calendar.pluginSystem.hooks.isDraggableTransformers;
var val = eventUi.startEditable;
for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) {
var transformer = transformers_1[_i];
val = transformer(val, eventDef, eventUi, this);
}
return val;
};
View.prototype.computeEventStartResizable = function (eventDef, eventUi) {
return eventUi.durationEditable && this.opt('eventResizableFromStart');
};
View.prototype.computeEventEndResizable = function (eventDef, eventUi) {
return eventUi.durationEditable;
};
// Event Selection
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventSelectionWrap = function (instanceId) {
if (instanceId) {
this.renderEventSelection(instanceId);
}
};
View.prototype.unrenderEventSelectionWrap = function (instanceId) {
if (instanceId) {
this.unrenderEventSelection(instanceId);
}
};
View.prototype.renderEventSelection = function (instanceId) { };
View.prototype.unrenderEventSelection = function (instanceId) { };
// Event Drag
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventDragWrap = function (state) {
if (state) {
this.renderEventDrag(state);
}
};
View.prototype.unrenderEventDragWrap = function (state) {
if (state) {
this.unrenderEventDrag(state);
}
};
View.prototype.renderEventDrag = function (state) { };
View.prototype.unrenderEventDrag = function (state) { };
// Event Resize
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventResizeWrap = function (state) {
if (state) {
this.renderEventResize(state);
}
};
View.prototype.unrenderEventResizeWrap = function (state) {
if (state) {
this.unrenderEventResize(state);
}
};
View.prototype.renderEventResize = function (state) { };
View.prototype.unrenderEventResize = function (state) { };
/* Now Indicator
------------------------------------------------------------------------------------------------------------------*/
// Immediately render the current time indicator and begins re-rendering it at an interval,
// which is defined by this.getNowIndicatorUnit().
// TODO: somehow do this for the current whole day's background too
View.prototype.startNowIndicator = function (dateProfile) {
var _this = this;
var dateEnv = this.dateEnv;
var unit;
var update;
var delay; // ms wait value
if (this.opt('nowIndicator')) {
unit = this.getNowIndicatorUnit(dateProfile);
if (unit) {
update = this.updateNowIndicator.bind(this);
this.initialNowDate = this.calendar.getNow();
this.initialNowQueriedMs = new Date().valueOf();
// wait until the beginning of the next interval
delay = dateEnv.add(dateEnv.startOf(this.initialNowDate, unit), createDuration(1, unit)).valueOf() - this.initialNowDate.valueOf();
// TODO: maybe always use setTimeout, waiting until start of next unit
this.nowIndicatorTimeoutID = setTimeout(function () {
_this.nowIndicatorTimeoutID = null;
update();
if (unit === 'second') {
delay = 1000; // every second
}
else {
delay = 1000 * 60; // otherwise, every minute
}
_this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
}, delay);
}
// rendering will be initiated in updateSize
}
};
// rerenders the now indicator, computing the new current time from the amount of time that has passed
// since the initial getNow call.
View.prototype.updateNowIndicator = function () {
if (this.props.dateProfile && // a way to determine if dates were rendered yet
this.initialNowDate // activated before?
) {
this.unrenderNowIndicator(); // won't unrender if unnecessary
this.renderNowIndicator(addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs));
this.isNowIndicatorRendered = true;
}
};
// Immediately unrenders the view's current time indicator and stops any re-rendering timers.
// Won't cause side effects if indicator isn't rendered.
View.prototype.stopNowIndicator = function () {
if (this.isNowIndicatorRendered) {
if (this.nowIndicatorTimeoutID) {
clearTimeout(this.nowIndicatorTimeoutID);
this.nowIndicatorTimeoutID = null;
}
if (this.nowIndicatorIntervalID) {
clearInterval(this.nowIndicatorIntervalID);
this.nowIndicatorIntervalID = null;
}
this.unrenderNowIndicator();
this.isNowIndicatorRendered = false;
}
};
View.prototype.getNowIndicatorUnit = function (dateProfile) {
// subclasses should implement
};
// Renders a current time indicator at the given datetime
View.prototype.renderNowIndicator = function (date) {
// SUBCLASSES MUST PASS TO CHILDREN!
};
// Undoes the rendering actions from renderNowIndicator
View.prototype.unrenderNowIndicator = function () {
// SUBCLASSES MUST PASS TO CHILDREN!
};
/* Scroller
------------------------------------------------------------------------------------------------------------------*/
View.prototype.addScroll = function (scroll) {
var queuedScroll = this.queuedScroll || (this.queuedScroll = {});
__assign(queuedScroll, scroll);
};
View.prototype.popScroll = function (isResize) {
this.applyQueuedScroll(isResize);
this.queuedScroll = null;
};
View.prototype.applyQueuedScroll = function (isResize) {
this.applyScroll(this.queuedScroll || {}, isResize);
};
View.prototype.queryScroll = function () {
var scroll = {};
if (this.props.dateProfile) { // dates rendered yet?
__assign(scroll, this.queryDateScroll());
}
return scroll;
};
View.prototype.applyScroll = function (scroll, isResize) {
var duration = scroll.duration;
if (duration != null) {
delete scroll.duration;
if (this.props.dateProfile) { // dates rendered yet?
__assign(scroll, this.computeDateScroll(duration));
}
}
if (this.props.dateProfile) { // dates rendered yet?
this.applyDateScroll(scroll);
}
};
View.prototype.computeDateScroll = function (duration) {
return {}; // subclasses must implement
};
View.prototype.queryDateScroll = function () {
return {}; // subclasses must implement
};
View.prototype.applyDateScroll = function (scroll) {
// subclasses must implement
};
// for API
View.prototype.scrollToDuration = function (duration) {
this.applyScroll({ duration: duration }, false);
};
return View;
}(DateComponent));
EmitterMixin.mixInto(View);
View.prototype.usesMinMaxTime = false;
View.prototype.dateProfileGeneratorClass = DateProfileGenerator;
var FgEventRenderer = /** @class */ (function () {
function FgEventRenderer(context) {
this.segs = [];
this.isSizeDirty = false;
this.context = context;
}
FgEventRenderer.prototype.renderSegs = function (segs, mirrorInfo) {
this.rangeUpdated(); // called too frequently :(
// render an `.el` on each seg
// returns a subset of the segs. segs that were actually rendered
segs = this.renderSegEls(segs, mirrorInfo);
this.segs = segs;
this.attachSegs(segs, mirrorInfo);
this.isSizeDirty = true;
this.context.view.triggerRenderedSegs(this.segs, Boolean(mirrorInfo));
};
FgEventRenderer.prototype.unrender = function (_segs, mirrorInfo) {
this.context.view.triggerWillRemoveSegs(this.segs, Boolean(mirrorInfo));
this.detachSegs(this.segs);
this.segs = [];
};
// Updates values that rely on options and also relate to range
FgEventRenderer.prototype.rangeUpdated = function () {
var options = this.context.options;
var displayEventTime;
var displayEventEnd;
this.eventTimeFormat = createFormatter(options.eventTimeFormat || this.computeEventTimeFormat(), options.defaultRangeSeparator);
displayEventTime = options.displayEventTime;
if (displayEventTime == null) {
displayEventTime = this.computeDisplayEventTime(); // might be based off of range
}
displayEventEnd = options.displayEventEnd;
if (displayEventEnd == null) {
displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
}
this.displayEventTime = displayEventTime;
this.displayEventEnd = displayEventEnd;
};
// Renders and assigns an `el` property for each foreground event segment.
// Only returns segments that successfully rendered.
FgEventRenderer.prototype.renderSegEls = function (segs, mirrorInfo) {
var html = '';
var i;
if (segs.length) { // don't build an empty html string
// build a large concatenation of event segment HTML
for (i = 0; i < segs.length; i++) {
html += this.renderSegHtml(segs[i], mirrorInfo);
}
// Grab individual elements from the combined HTML string. Use each as the default rendering.
// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
htmlToElements(html).forEach(function (el, i) {
var seg = segs[i];
if (el) {
seg.el = el;
}
});
segs = filterSegsViaEls(this.context.view, segs, Boolean(mirrorInfo));
}
return segs;
};
// Generic utility for generating the HTML classNames for an event segment's element
FgEventRenderer.prototype.getSegClasses = function (seg, isDraggable, isResizable, mirrorInfo) {
var classes = [
'fc-event',
seg.isStart ? 'fc-start' : 'fc-not-start',
seg.isEnd ? 'fc-end' : 'fc-not-end'
].concat(seg.eventRange.ui.classNames);
if (isDraggable) {
classes.push('fc-draggable');
}
if (isResizable) {
classes.push('fc-resizable');
}
if (mirrorInfo) {
classes.push('fc-mirror');
if (mirrorInfo.isDragging) {
classes.push('fc-dragging');
}
if (mirrorInfo.isResizing) {
classes.push('fc-resizing');
}
}
return classes;
};
// Compute the text that should be displayed on an event's element.
// `range` can be the Event object itself, or something range-like, with at least a `start`.
// If event times are disabled, or the event has no time, will return a blank string.
// If not specified, formatter will default to the eventTimeFormat setting,
// and displayEnd will default to the displayEventEnd setting.
FgEventRenderer.prototype.getTimeText = function (eventRange, formatter, displayEnd) {
var def = eventRange.def, instance = eventRange.instance;
return this._getTimeText(instance.range.start, def.hasEnd ? instance.range.end : null, def.allDay, formatter, displayEnd, instance.forcedStartTzo, instance.forcedEndTzo);
};
FgEventRenderer.prototype._getTimeText = function (start, end, allDay, formatter, displayEnd, forcedStartTzo, forcedEndTzo) {
var dateEnv = this.context.dateEnv;
if (formatter == null) {
formatter = this.eventTimeFormat;
}
if (displayEnd == null) {
displayEnd = this.displayEventEnd;
}
if (this.displayEventTime && !allDay) {
if (displayEnd && end) {
return dateEnv.formatRange(start, end, formatter, {
forcedStartTzo: forcedStartTzo,
forcedEndTzo: forcedEndTzo
});
}
else {
return dateEnv.format(start, formatter, {
forcedTzo: forcedStartTzo
});
}
}
return '';
};
FgEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true
};
};
FgEventRenderer.prototype.computeDisplayEventTime = function () {
return true;
};
FgEventRenderer.prototype.computeDisplayEventEnd = function () {
return true;
};
// Utility for generating event skin-related CSS properties
FgEventRenderer.prototype.getSkinCss = function (ui) {
return {
'background-color': ui.backgroundColor,
'border-color': ui.borderColor,
color: ui.textColor
};
};
FgEventRenderer.prototype.sortEventSegs = function (segs) {
var specs = this.context.view.eventOrderSpecs;
var objs = segs.map(buildSegCompareObj);
objs.sort(function (obj0, obj1) {
return compareByFieldSpecs(obj0, obj1, specs);
});
return objs.map(function (c) {
return c._seg;
});
};
FgEventRenderer.prototype.computeSizes = function (force) {
if (force || this.isSizeDirty) {
this.computeSegSizes(this.segs);
}
};
FgEventRenderer.prototype.assignSizes = function (force) {
if (force || this.isSizeDirty) {
this.assignSegSizes(this.segs);
this.isSizeDirty = false;
}
};
FgEventRenderer.prototype.computeSegSizes = function (segs) {
};
FgEventRenderer.prototype.assignSegSizes = function (segs) {
};
// Manipulation on rendered segs
FgEventRenderer.prototype.hideByHash = function (hash) {
if (hash) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (hash[seg.eventRange.instance.instanceId]) {
seg.el.style.visibility = 'hidden';
}
}
}
};
FgEventRenderer.prototype.showByHash = function (hash) {
if (hash) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (hash[seg.eventRange.instance.instanceId]) {
seg.el.style.visibility = '';
}
}
}
};
FgEventRenderer.prototype.selectByInstanceId = function (instanceId) {
if (instanceId) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
var eventInstance = seg.eventRange.instance;
if (eventInstance && eventInstance.instanceId === instanceId &&
seg.el // necessary?
) {
seg.el.classList.add('fc-selected');
}
}
}
};
FgEventRenderer.prototype.unselectByInstanceId = function (instanceId) {
if (instanceId) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (seg.el) { // necessary?
seg.el.classList.remove('fc-selected');
}
}
}
};
return FgEventRenderer;
}());
// returns a object with all primitive props that can be compared
function buildSegCompareObj(seg) {
var eventDef = seg.eventRange.def;
var range = seg.eventRange.instance.range;
var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events
var end = range.end ? range.end.valueOf() : 0; // "
return __assign({}, eventDef.extendedProps, eventDef, { id: eventDef.publicId, start: start,
end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg // for later retrieval
});
}
var FillRenderer = /** @class */ (function () {
function FillRenderer(context) {
this.fillSegTag = 'div';
this.dirtySizeFlags = {};
this.context = context;
this.containerElsByType = {};
this.segsByType = {};
}
FillRenderer.prototype.getSegsByType = function (type) {
return this.segsByType[type] || [];
};
FillRenderer.prototype.renderSegs = function (type, segs) {
var _a;
var renderedSegs = this.renderSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
var containerEls = this.attachSegs(type, renderedSegs);
if (containerEls) {
(_a = (this.containerElsByType[type] || (this.containerElsByType[type] = []))).push.apply(_a, containerEls);
}
this.segsByType[type] = renderedSegs;
if (type === 'bgEvent') {
this.context.view.triggerRenderedSegs(renderedSegs, false); // isMirror=false
}
this.dirtySizeFlags[type] = true;
};
// Unrenders a specific type of fill that is currently rendered on the grid
FillRenderer.prototype.unrender = function (type) {
var segs = this.segsByType[type];
if (segs) {
if (type === 'bgEvent') {
this.context.view.triggerWillRemoveSegs(segs, false); // isMirror=false
}
this.detachSegs(type, segs);
}
};
// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
// Only returns segments that successfully rendered.
FillRenderer.prototype.renderSegEls = function (type, segs) {
var _this = this;
var html = '';
var i;
if (segs.length) {
// build a large concatenation of segment HTML
for (i = 0; i < segs.length; i++) {
html += this.renderSegHtml(type, segs[i]);
}
// Grab individual elements from the combined HTML string. Use each as the default rendering.
// Then, compute the 'el' for each segment.
htmlToElements(html).forEach(function (el, i) {
var seg = segs[i];
if (el) {
seg.el = el;
}
});
if (type === 'bgEvent') {
segs = filterSegsViaEls(this.context.view, segs, false // isMirror. background events can never be mirror elements
);
}
// correct element type? (would be bad if a non-TD were inserted into a table for example)
segs = segs.filter(function (seg) {
return elementMatches(seg.el, _this.fillSegTag);
});
}
return segs;
};
// Builds the HTML needed for one fill segment. Generic enough to work with different types.
FillRenderer.prototype.renderSegHtml = function (type, seg) {
var css = null;
var classNames = [];
if (type !== 'highlight' && type !== 'businessHours') {
css = {
'background-color': seg.eventRange.ui.backgroundColor
};
}
if (type !== 'highlight') {
classNames = classNames.concat(seg.eventRange.ui.classNames);
}
if (type === 'businessHours') {
classNames.push('fc-bgevent');
}
else {
classNames.push('fc-' + type.toLowerCase());
}
return '<' + this.fillSegTag +
(classNames.length ? ' class="' + classNames.join(' ') + '"' : '') +
(css ? ' style="' + cssToStr(css) + '"' : '') +
'>' + this.fillSegTag + '>';
};
FillRenderer.prototype.detachSegs = function (type, segs) {
var containerEls = this.containerElsByType[type];
if (containerEls) {
containerEls.forEach(removeElement);
delete this.containerElsByType[type];
}
};
FillRenderer.prototype.computeSizes = function (force) {
for (var type in this.segsByType) {
if (force || this.dirtySizeFlags[type]) {
this.computeSegSizes(this.segsByType[type]);
}
}
};
FillRenderer.prototype.assignSizes = function (force) {
for (var type in this.segsByType) {
if (force || this.dirtySizeFlags[type]) {
this.assignSegSizes(this.segsByType[type]);
}
}
this.dirtySizeFlags = {};
};
FillRenderer.prototype.computeSegSizes = function (segs) {
};
FillRenderer.prototype.assignSegSizes = function (segs) {
};
return FillRenderer;
}());
var NamedTimeZoneImpl = /** @class */ (function () {
function NamedTimeZoneImpl(timeZoneName) {
this.timeZoneName = timeZoneName;
}
return NamedTimeZoneImpl;
}());
/*
An abstraction for a dragging interaction originating on an event.
Does higher-level things than PointerDragger, such as possibly:
- a "mirror" that moves with the pointer
- a minimum number of pixels or other criteria for a true drag to begin
subclasses must emit:
- pointerdown
- dragstart
- dragmove
- pointerup
- dragend
*/
var ElementDragging = /** @class */ (function () {
function ElementDragging(el) {
this.emitter = new EmitterMixin();
}
ElementDragging.prototype.destroy = function () {
};
ElementDragging.prototype.setMirrorIsVisible = function (bool) {
// optional if subclass doesn't want to support a mirror
};
ElementDragging.prototype.setMirrorNeedsRevert = function (bool) {
// optional if subclass doesn't want to support a mirror
};
ElementDragging.prototype.setAutoScrollEnabled = function (bool) {
// optional
};
return ElementDragging;
}());
function formatDate(dateInput, settings) {
if (settings === void 0) { settings = {}; }
var dateEnv = buildDateEnv$1(settings);
var formatter = createFormatter(settings);
var dateMeta = dateEnv.createMarkerMeta(dateInput);
if (!dateMeta) { // TODO: warning?
return '';
}
return dateEnv.format(dateMeta.marker, formatter, {
forcedTzo: dateMeta.forcedTzo
});
}
function formatRange(startInput, endInput, settings // mixture of env and formatter settings
) {
var dateEnv = buildDateEnv$1(typeof settings === 'object' && settings ? settings : {}); // pass in if non-null object
var formatter = createFormatter(settings, globalDefaults.defaultRangeSeparator);
var startMeta = dateEnv.createMarkerMeta(startInput);
var endMeta = dateEnv.createMarkerMeta(endInput);
if (!startMeta || !endMeta) { // TODO: warning?
return '';
}
return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
forcedStartTzo: startMeta.forcedTzo,
forcedEndTzo: endMeta.forcedTzo,
isEndExclusive: settings.isEndExclusive
});
}
// TODO: more DRY and optimized
function buildDateEnv$1(settings) {
var locale = buildLocale(settings.locale || 'en', parseRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
// ensure required settings
settings = __assign({ timeZone: globalDefaults.timeZone, calendarSystem: 'gregory' }, settings, { locale: locale });
return new DateEnv(settings);
}
var DRAG_META_PROPS = {
startTime: createDuration,
duration: createDuration,
create: Boolean,
sourceId: String
};
var DRAG_META_DEFAULTS = {
create: true
};
function parseDragMeta(raw) {
var leftoverProps = {};
var refined = refineProps(raw, DRAG_META_PROPS, DRAG_META_DEFAULTS, leftoverProps);
refined.leftoverProps = leftoverProps;
return refined;
}
// Computes a default column header formatting string if `colFormat` is not explicitly defined
function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) {
// if more than one week row, or if there are a lot of columns with not much space,
// put just the day numbers will be in each cell
if (!datesRepDistinctDays || dayCnt > 10) {
return { weekday: 'short' }; // "Sat"
}
else if (dayCnt > 1) {
return { weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }; // "Sat 11/12"
}
else {
return { weekday: 'long' }; // "Saturday"
}
}
function renderDateCell(dateMarker, dateProfile, datesRepDistinctDays, colCnt, colHeadFormat, context, colspan, otherAttrs) {
var view = context.view, dateEnv = context.dateEnv, theme = context.theme, options = context.options;
var isDateValid = rangeContainsMarker(dateProfile.activeRange, dateMarker); // TODO: called too frequently. cache somehow.
var classNames = [
'fc-day-header',
theme.getClass('widgetHeader')
];
var innerHtml;
if (typeof options.columnHeaderHtml === 'function') {
innerHtml = options.columnHeaderHtml(dateEnv.toDate(dateMarker));
}
else if (typeof options.columnHeaderText === 'function') {
innerHtml = htmlEscape(options.columnHeaderText(dateEnv.toDate(dateMarker)));
}
else {
innerHtml = htmlEscape(dateEnv.format(dateMarker, colHeadFormat));
}
// if only one row of days, the classNames on the header can represent the specific days beneath
if (datesRepDistinctDays) {
classNames = classNames.concat(
// includes the day-of-week class
// noThemeHighlight=true (don't highlight the header)
getDayClasses(dateMarker, dateProfile, context, true));
}
else {
classNames.push('fc-' + DAY_IDS[dateMarker.getUTCDay()]); // only add the day-of-week class
}
return '' +
'
var DayHeader = /** @class */ (function (_super) {
__extends(DayHeader, _super);
function DayHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.innerHTML = ''; // because might be nbsp
parentEl.appendChild(_this.el = htmlToElement('
var DaySeries = /** @class */ (function () {
function DaySeries(range, dateProfileGenerator) {
var date = range.start;
var end = range.end;
var indices = [];
var dates = [];
var dayIndex = -1;
while (date < end) { // loop each day from start to end
if (dateProfileGenerator.isHiddenDay(date)) {
indices.push(dayIndex + 0.5); // mark that it's between indices
}
else {
dayIndex++;
indices.push(dayIndex);
dates.push(date);
}
date = addDays(date, 1);
}
this.dates = dates;
this.indices = indices;
this.cnt = dates.length;
}
DaySeries.prototype.sliceRange = function (range) {
var firstIndex = this.getDateDayIndex(range.start); // inclusive first index
var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index
var clippedFirstIndex = Math.max(0, firstIndex);
var clippedLastIndex = Math.min(this.cnt - 1, lastIndex);
// deal with in-between indices
clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell
clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell
if (clippedFirstIndex <= clippedLastIndex) {
return {
firstIndex: clippedFirstIndex,
lastIndex: clippedLastIndex,
isStart: firstIndex === clippedFirstIndex,
isEnd: lastIndex === clippedLastIndex
};
}
else {
return null;
}
};
// Given a date, returns its chronolocial cell-index from the first cell of the grid.
// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
// If before the first offset, returns a negative number.
// If after the last offset, returns an offset past the last cell offset.
// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
DaySeries.prototype.getDateDayIndex = function (date) {
var indices = this.indices;
var dayOffset = Math.floor(diffDays(this.dates[0], date));
if (dayOffset < 0) {
return indices[0] - 1;
}
else if (dayOffset >= indices.length) {
return indices[indices.length - 1] + 1;
}
else {
return indices[dayOffset];
}
};
return DaySeries;
}());
var DayTable = /** @class */ (function () {
function DayTable(daySeries, breakOnWeeks) {
var dates = daySeries.dates;
var daysPerRow;
var firstDay;
var rowCnt;
if (breakOnWeeks) {
// count columns until the day-of-week repeats
firstDay = dates[0].getUTCDay();
for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow++) {
if (dates[daysPerRow].getUTCDay() === firstDay) {
break;
}
}
rowCnt = Math.ceil(dates.length / daysPerRow);
}
else {
rowCnt = 1;
daysPerRow = dates.length;
}
this.rowCnt = rowCnt;
this.colCnt = daysPerRow;
this.daySeries = daySeries;
this.cells = this.buildCells();
this.headerDates = this.buildHeaderDates();
}
DayTable.prototype.buildCells = function () {
var rows = [];
for (var row = 0; row < this.rowCnt; row++) {
var cells = [];
for (var col = 0; col < this.colCnt; col++) {
cells.push(this.buildCell(row, col));
}
rows.push(cells);
}
return rows;
};
DayTable.prototype.buildCell = function (row, col) {
return {
date: this.daySeries.dates[row * this.colCnt + col]
};
};
DayTable.prototype.buildHeaderDates = function () {
var dates = [];
for (var col = 0; col < this.colCnt; col++) {
dates.push(this.cells[0][col].date);
}
return dates;
};
DayTable.prototype.sliceRange = function (range) {
var colCnt = this.colCnt;
var seriesSeg = this.daySeries.sliceRange(range);
var segs = [];
if (seriesSeg) {
var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex;
var index = firstIndex;
while (index <= lastIndex) {
var row = Math.floor(index / colCnt);
var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1);
segs.push({
row: row,
firstCol: index % colCnt,
lastCol: (nextIndex - 1) % colCnt,
isStart: seriesSeg.isStart && index === firstIndex,
isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex
});
index = nextIndex;
}
}
return segs;
};
return DayTable;
}());
var Slicer = /** @class */ (function () {
function Slicer() {
this.sliceBusinessHours = memoize(this._sliceBusinessHours);
this.sliceDateSelection = memoize(this._sliceDateSpan);
this.sliceEventStore = memoize(this._sliceEventStore);
this.sliceEventDrag = memoize(this._sliceInteraction);
this.sliceEventResize = memoize(this._sliceInteraction);
}
Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 4; _i < arguments.length; _i++) {
extraArgs[_i - 4] = arguments[_i];
}
var eventUiBases = props.eventUiBases;
var eventSegs = this.sliceEventStore.apply(this, [props.eventStore, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs));
return {
dateSelectionSegs: this.sliceDateSelection.apply(this, [props.dateSelection, eventUiBases, component].concat(extraArgs)),
businessHourSegs: this.sliceBusinessHours.apply(this, [props.businessHours, dateProfile, nextDayThreshold, component].concat(extraArgs)),
fgEventSegs: eventSegs.fg,
bgEventSegs: eventSegs.bg,
eventDrag: this.sliceEventDrag.apply(this, [props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)),
eventResize: this.sliceEventResize.apply(this, [props.eventResize, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)),
eventSelection: props.eventSelection
}; // TODO: give interactionSegs?
};
Slicer.prototype.sliceNowDate = function (// does not memoize
date, component) {
var extraArgs = [];
for (var _i = 2; _i < arguments.length; _i++) {
extraArgs[_i - 2] = arguments[_i];
}
return this._sliceDateSpan.apply(this, [{ range: { start: date, end: addMs(date, 1) }, allDay: false },
{},
component].concat(extraArgs));
};
Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 4; _i < arguments.length; _i++) {
extraArgs[_i - 4] = arguments[_i];
}
if (!businessHours) {
return [];
}
return this._sliceEventStore.apply(this, [expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), component.calendar),
{},
dateProfile,
nextDayThreshold,
component].concat(extraArgs)).bg;
};
Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 5; _i < arguments.length; _i++) {
extraArgs[_i - 5] = arguments[_i];
}
if (eventStore) {
var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
bg: this.sliceEventRanges(rangeRes.bg, component, extraArgs),
fg: this.sliceEventRanges(rangeRes.fg, component, extraArgs)
};
}
else {
return { bg: [], fg: [] };
}
};
Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 5; _i < arguments.length; _i++) {
extraArgs[_i - 5] = arguments[_i];
}
if (!interaction) {
return null;
}
var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
segs: this.sliceEventRanges(rangeRes.fg, component, extraArgs),
affectedInstances: interaction.affectedEvents.instances,
isEvent: interaction.isEvent,
sourceSeg: interaction.origSeg
};
};
Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, component) {
var extraArgs = [];
for (var _i = 3; _i < arguments.length; _i++) {
extraArgs[_i - 3] = arguments[_i];
}
if (!dateSpan) {
return [];
}
var eventRange = fabricateEventRange(dateSpan, eventUiBases, component.calendar);
var segs = this.sliceRange.apply(this, [dateSpan.range].concat(extraArgs));
for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) {
var seg = segs_1[_a];
seg.component = component;
seg.eventRange = eventRange;
}
return segs;
};
/*
"complete" seg means it has component and eventRange
*/
Slicer.prototype.sliceEventRanges = function (eventRanges, component, // TODO: kill
extraArgs) {
var segs = [];
for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) {
var eventRange = eventRanges_1[_i];
segs.push.apply(segs, this.sliceEventRange(eventRange, component, extraArgs));
}
return segs;
};
/*
"complete" seg means it has component and eventRange
*/
Slicer.prototype.sliceEventRange = function (eventRange, component, // TODO: kill
extraArgs) {
var segs = this.sliceRange.apply(this, [eventRange.range].concat(extraArgs));
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
seg.component = component;
seg.eventRange = eventRange;
seg.isStart = eventRange.isStart && seg.isStart;
seg.isEnd = eventRange.isEnd && seg.isEnd;
}
return segs;
};
return Slicer;
}());
/*
for incorporating minTime/maxTime if appropriate
TODO: should be part of DateProfile!
TimelineDateProfile already does this btw
*/
function computeActiveRange(dateProfile, isComponentAllDay) {
var range = dateProfile.activeRange;
if (isComponentAllDay) {
return range;
}
return {
start: addMs(range.start, dateProfile.minTime.milliseconds),
end: addMs(range.end, dateProfile.maxTime.milliseconds - 864e5) // 864e5 = ms in a day
};
}
// exports
// --------------------------------------------------------------------------------------------------
var version = '4.3.1';
export { Calendar, Component, DateComponent, DateEnv, DateProfileGenerator, DayHeader, DaySeries, DayTable, ElementDragging, ElementScrollController, EmitterMixin, EventApi, FgEventRenderer, FillRenderer, Interaction, Mixin, NamedTimeZoneImpl, PositionCache, ScrollComponent, ScrollController, Slicer, Splitter, Theme, View, WindowScrollController, addDays, addDurations, addMs, addWeeks, allowContextMenu, allowSelection, appendToElement, applyAll, applyMutationToEventStore, applyStyle, applyStyleProp, asRoughMinutes, asRoughMs, asRoughSeconds, buildGotoAnchorHtml, buildSegCompareObj, capitaliseFirstLetter, combineEventUis, compareByFieldSpec, compareByFieldSpecs, compareNumbers, compensateScroll, computeClippingRect, computeEdges, computeFallbackHeaderFormat, computeHeightAndMargins, computeInnerRect, computeRect, computeVisibleDayRange, config, constrainPoint, createDuration, createElement, createEmptyEventStore, createEventInstance, createFormatter, createPlugin, cssToStr, debounce, diffDates, diffDayAndTime, diffDays, diffPoints, diffWeeks, diffWholeDays, diffWholeWeeks, disableCursor, distributeHeight, elementClosest, elementMatches, enableCursor, eventTupleToStore, filterEventStoreDefs, filterHash, findChildren, findElements, flexibleCompare, forceClassName, formatDate, formatIsoTimeString, formatRange, getAllDayHtml, getClippingParents, getDayClasses, getElSeg, getRectCenter, getRelevantEvents, globalDefaults, greatestDurationDenominator, hasBgRendering, htmlEscape, htmlToElement, insertAfterElement, interactionSettingsStore, interactionSettingsToStore, intersectRanges, intersectRects, isArraysEqual, isDateSpansEqual, isInt, isInteractionValid, isMultiDayRange, isPropsEqual, isPropsValid, isSingleDay, isValidDate, listenBySelector, mapHash, matchCellWidths, memoize, memoizeOutput, memoizeRendering, mergeEventStores, multiplyDuration, padStart, parseBusinessHours, parseDragMeta, parseEventDef, parseFieldSpecs, parse as parseMarker, pointInsideRect, prependToElement, preventContextMenu, preventDefault, preventSelection, processScopedUiProps, rangeContainsMarker, rangeContainsRange, rangesEqual, rangesIntersect, refineProps, removeElement, removeExact, renderDateCell, requestJson, sliceEventStore, startOfDay, subtractInnerElHeight, translateRect, uncompensateScroll, undistributeHeight, unpromisify, version, whenTransitionDone, wholeDivideDurations };
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FullCalendarLocalesAll=t()}(this,function(){"use strict";return[{code:"af",week:{dow:1,doy:4},buttonText:{prev:"Vorige",next:"Volgende",today:"Vandag",year:"Jaar",month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayHtml:"Heeldag",eventLimitText:"Addisionele",noEventsMessage:"Daar is geen gebeurtenisse nie"},{code:"ar-dz",week:{dow:0,doy:4},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar-kw",week:{dow:0,doy:12},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar-ly",week:{dow:6,doy:12},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar-ma",week:{dow:6,doy:12},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar-sa",week:{dow:0,doy:6},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar-tn",week:{dow:1,doy:4},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"ar",week:{dow:6,doy:12},dir:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekLabel:"أسبوع",allDayText:"اليوم كله",eventLimitText:"أخرى",noEventsMessage:"أي أحداث لعرض"},{code:"bg",week:{dow:1,doy:7},buttonText:{prev:"назад",next:"напред",today:"днес",month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",eventLimitText:function(e){return"+още "+e},noEventsMessage:"Няма събития за показване"},{code:"bs",week:{dow:1,doy:7},buttonText:{prev:"Prošli",next:"Sljedeći",today:"Danas",month:"Mjesec",week:"Sedmica",day:"Dan",list:"Raspored"},weekLabel:"Sed",allDayText:"Cijeli dan",eventLimitText:function(e){return"+ još "+e},noEventsMessage:"Nema događaja za prikazivanje"},{code:"ca",week:{dow:1,doy:4},buttonText:{prev:"Anterior",next:"Següent",today:"Avui",month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},weekLabel:"Set",allDayText:"Tot el dia",eventLimitText:"més",noEventsMessage:"No hi ha esdeveniments per mostrar"},{code:"cs",week:{dow:1,doy:4},buttonText:{prev:"Dříve",next:"Později",today:"Nyní",month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},weekLabel:"Týd",allDayText:"Celý den",eventLimitText:function(e){return"+další: "+e},noEventsMessage:"Žádné akce k zobrazení"},{code:"da",week:{dow:1,doy:4},buttonText:{prev:"Forrige",next:"Næste",today:"I dag",month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},weekLabel:"Uge",allDayText:"Hele dagen",eventLimitText:"flere",noEventsMessage:"Ingen arrangementer at vise"},{code:"de",week:{dow:1,doy:4},buttonText:{prev:"Zurück",next:"Vor",today:"Heute",year:"Jahr",month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},weekLabel:"KW",allDayText:"Ganztägig",eventLimitText:function(e){return"+ weitere "+e},noEventsMessage:"Keine Ereignisse anzuzeigen"},{code:"el",week:{dow:1,doy:4},buttonText:{prev:"Προηγούμενος",next:"Επόμενος",today:"Σήμερα",month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},weekLabel:"Εβδ",allDayText:"Ολοήμερο",eventLimitText:"περισσότερα",noEventsMessage:"Δεν υπάρχουν γεγονότα για να εμφανιστεί"},{code:"en-au",week:{dow:1,doy:4}},{code:"en-gb",week:{dow:1,doy:4}},{code:"en-nz",week:{dow:1,doy:4}},{code:"es",week:{dow:0,doy:6},buttonText:{prev:"Ant",next:"Sig",today:"Hoy",month:"Mes",week:"Semana",day:"Día",list:"Agenda"},weekLabel:"Sm",allDayHtml:"Todo
el día",eventLimitText:"más",noEventsMessage:"No hay eventos para mostrar"},{code:"es",week:{dow:1,doy:4},buttonText:{prev:"Ant",next:"Sig",today:"Hoy",month:"Mes",week:"Semana",day:"Día",list:"Agenda"},weekLabel:"Sm",allDayHtml:"Todo
el día",eventLimitText:"más",noEventsMessage:"No hay eventos para mostrar"},{code:"et",week:{dow:1,doy:4},buttonText:{prev:"Eelnev",next:"Järgnev",today:"Täna",month:"Kuu",week:"Nädal",day:"Päev",list:"Päevakord"},weekLabel:"näd",allDayText:"Kogu päev",eventLimitText:function(e){return"+ veel "+e},noEventsMessage:"Kuvamiseks puuduvad sündmused"},{code:"eu",week:{dow:1,doy:7},buttonText:{prev:"Aur",next:"Hur",today:"Gaur",month:"Hilabetea",week:"Astea",day:"Eguna",list:"Agenda"},weekLabel:"As",allDayHtml:"Egun
osoa",eventLimitText:"gehiago",noEventsMessage:"Ez dago ekitaldirik erakusteko"},{code:"fa",week:{dow:6,doy:12},dir:"rtl",buttonText:{prev:"قبلی",next:"بعدی",today:"امروز",month:"ماه",week:"هفته",day:"روز",list:"برنامه"},weekLabel:"هف",allDayText:"تمام روز",eventLimitText:function(e){return"بیش از "+e},noEventsMessage:"هیچ رویدادی به نمایش"},{code:"fi",week:{dow:1,doy:4},buttonText:{prev:"Edellinen",next:"Seuraava",today:"Tänään",month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},weekLabel:"Vk",allDayText:"Koko päivä",eventLimitText:"lisää",noEventsMessage:"Ei näytettäviä tapahtumia"},{code:"fr",buttonText:{prev:"Précédent",next:"Suivant",today:"Aujourd'hui",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},weekLabel:"Sem.",allDayHtml:"Toute la
journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"},{code:"fr-ch",week:{dow:1,doy:4},buttonText:{prev:"Précédent",next:"Suivant",today:"Courant",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},weekLabel:"Sm",allDayHtml:"Toute la
journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"},{code:"fr",week:{dow:1,doy:4},buttonText:{prev:"Précédent",next:"Suivant",today:"Aujourd'hui",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},weekLabel:"Sem.",allDayHtml:"Toute la
journée",eventLimitText:"en plus",noEventsMessage:"Aucun événement à afficher"},{code:"gl",week:{dow:1,doy:4},buttonText:{prev:"Ant",next:"Seg",today:"Hoxe",month:"Mes",week:"Semana",day:"Día",list:"Axenda"},weekLabel:"Sm",allDayHtml:"Todo
o día",eventLimitText:"máis",noEventsMessage:"Non hai eventos para amosar"},{code:"he",dir:"rtl",buttonText:{prev:"הקודם",next:"הבא",today:"היום",month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},allDayText:"כל היום",eventLimitText:"אחר",noEventsMessage:"אין אירועים להצגה",weekLabel:"שבוע"},{code:"hi",week:{dow:0,doy:6},buttonText:{prev:"पिछला",next:"अगला",today:"आज",month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},weekLabel:"हफ्ता",allDayText:"सभी दिन",eventLimitText:function(e){return"+अधिक "+e},noEventsMessage:"कोई घटनाओं को प्रदर्शित करने के लिए"},{code:"hr",week:{dow:1,doy:7},buttonText:{prev:"Prijašnji",next:"Sljedeći",today:"Danas",month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},weekLabel:"Tje",allDayText:"Cijeli dan",eventLimitText:function(e){return"+ još "+e},noEventsMessage:"Nema događaja za prikaz"},{code:"hu",week:{dow:1,doy:4},buttonText:{prev:"vissza",next:"előre",today:"ma",month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},weekLabel:"Hét",allDayText:"Egész nap",eventLimitText:"további",noEventsMessage:"Nincs megjeleníthető esemény"},{code:"id",week:{dow:1,doy:7},buttonText:{prev:"mundur",next:"maju",today:"hari ini",month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},weekLabel:"Mg",allDayHtml:"Sehari
penuh",eventLimitText:"lebih",noEventsMessage:"Tidak ada acara untuk ditampilkan"},{code:"is",week:{dow:1,doy:4},buttonText:{prev:"Fyrri",next:"Næsti",today:"Í dag",month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},weekLabel:"Vika",allDayHtml:"Allan
daginn",eventLimitText:"meira",noEventsMessage:"Engir viðburðir til að sýna"},{code:"it",week:{dow:1,doy:4},buttonText:{prev:"Prec",next:"Succ",today:"Oggi",month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},weekLabel:"Sm",allDayHtml:"Tutto il
giorno",eventLimitText:function(e){return"+altri "+e},noEventsMessage:"Non ci sono eventi da visualizzare"},{code:"ja",buttonText:{prev:"前",next:"次",today:"今日",month:"月",week:"週",day:"日",list:"予定リスト"},weekLabel:"週",allDayText:"終日",eventLimitText:function(e){return"他 "+e+" 件"},noEventsMessage:"表示する予定はありません"},{code:"ka",week:{dow:1,doy:7},buttonText:{prev:"წინა",next:"შემდეგი",today:"დღეს",month:"თვე",week:"კვირა",day:"დღე",list:"დღის წესრიგი"},weekLabel:"კვ",allDayText:"მთელი დღე",eventLimitText:function(e){return"+ კიდევ "+e},noEventsMessage:"ღონისძიებები არ არის"},{code:"kk",week:{dow:1,doy:7},buttonText:{prev:"Алдыңғы",next:"Келесі",today:"Бүгін",month:"Ай",week:"Апта",day:"Күн",list:"Күн тәртібі"},weekLabel:"Не",allDayText:"Күні бойы",eventLimitText:function(e){return"+ тағы "+e},noEventsMessage:"Көрсету үшін оқиғалар жоқ"},{code:"ko",buttonText:{prev:"이전달",next:"다음달",today:"오늘",month:"월",week:"주",day:"일",list:"일정목록"},weekLabel:"주",allDayText:"종일",eventLimitText:"개",noEventsMessage:"일정이 없습니다"},{code:"lb",week:{dow:1,doy:4},buttonText:{prev:"Zréck",next:"Weider",today:"Haut",month:"Mount",week:"Woch",day:"Dag",list:"Terminiwwersiicht"},weekLabel:"W",allDayText:"Ganzen Dag",eventLimitText:"méi",noEventsMessage:"Nee Evenementer ze affichéieren"},{code:"lt",week:{dow:1,doy:4},buttonText:{prev:"Atgal",next:"Pirmyn",today:"Šiandien",month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},weekLabel:"SAV",allDayText:"Visą dieną",eventLimitText:"daugiau",noEventsMessage:"Nėra įvykių rodyti"},{code:"lv",week:{dow:1,doy:4},buttonText:{prev:"Iepr.",next:"Nāk.",today:"Šodien",month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},weekLabel:"Ned.",allDayText:"Visu dienu",eventLimitText:function(e){return"+vēl "+e},noEventsMessage:"Nav notikumu"},{code:"mk",buttonText:{prev:"претходно",next:"следно",today:"Денес",month:"Месец",week:"Недела",day:"Ден",list:"График"},weekLabel:"Сед",allDayText:"Цел ден",eventLimitText:function(e){return"+повеќе "+e},noEventsMessage:"Нема настани за прикажување"},{code:"ms",week:{dow:1,doy:7},buttonText:{prev:"Sebelum",next:"Selepas",today:"hari ini",month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},weekLabel:"Mg",allDayText:"Sepanjang hari",eventLimitText:function(e){return"masih ada "+e+" acara"},noEventsMessage:"Tiada peristiwa untuk dipaparkan"},{code:"nb",week:{dow:1,doy:4},buttonText:{prev:"Forrige",next:"Neste",today:"I dag",month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},weekLabel:"Uke",allDayText:"Hele dagen",eventLimitText:"til",noEventsMessage:"Ingen hendelser å vise"},{code:"nl",week:{dow:1,doy:4},buttonText:{prev:"Voorgaand",next:"Volgende",today:"Vandaag",year:"Jaar",month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra",noEventsMessage:"Geen evenementen om te laten zien"},{code:"nn",week:{dow:1,doy:4},buttonText:{prev:"Førre",next:"Neste",today:"I dag",month:"Månad",week:"Veke",day:"Dag",list:"Agenda"},weekLabel:"Veke",allDayText:"Heile dagen",eventLimitText:"til",noEventsMessage:"Ingen hendelser å vise"},{code:"pl",week:{dow:1,doy:4},buttonText:{prev:"Poprzedni",next:"Następny",today:"Dziś",month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},weekLabel:"Tydz",allDayText:"Cały dzień",eventLimitText:"więcej",noEventsMessage:"Brak wydarzeń do wyświetlenia"},{code:"pt-br",buttonText:{prev:"Anterior",next:"Próximo",today:"Hoje",month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},weekLabel:"Sm",allDayText:"dia inteiro",eventLimitText:function(e){return"mais +"+e},noEventsMessage:"Não há eventos para mostrar"},{code:"pt",week:{dow:1,doy:4},buttonText:{prev:"Anterior",next:"Seguinte",today:"Hoje",month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},weekLabel:"Sem",allDayText:"Todo o dia",eventLimitText:"mais",noEventsMessage:"Não há eventos para mostrar"},{code:"ro",week:{dow:1,doy:7},buttonText:{prev:"precedentă",next:"următoare",today:"Azi",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},weekLabel:"Săpt",allDayText:"Toată ziua",eventLimitText:function(e){return"+alte "+e},noEventsMessage:"Nu există evenimente de afișat"},{code:"ru",week:{dow:1,doy:4},buttonText:{prev:"Пред",next:"След",today:"Сегодня",month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},weekLabel:"Нед",allDayText:"Весь день",eventLimitText:function(e){return"+ ещё "+e},noEventsMessage:"Нет событий для отображения"},{code:"sk",week:{dow:1,doy:4},buttonText:{prev:"Predchádzajúci",next:"Nasledujúci",today:"Dnes",month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},weekLabel:"Ty",allDayText:"Celý deň",eventLimitText:function(e){return"+ďalšie: "+e},noEventsMessage:"Žiadne akcie na zobrazenie"},{code:"sl",week:{dow:1,doy:7},buttonText:{prev:"Prejšnji",next:"Naslednji",today:"Trenutni",month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},weekLabel:"Teden",allDayText:"Ves dan",eventLimitText:"več",noEventsMessage:"Ni dogodkov za prikaz"},{code:"sq",week:{dow:1,doy:4},buttonText:{prev:"mbrapa",next:"Përpara",today:"sot",month:"Muaj",week:"Javë",day:"Ditë",list:"Listë"},weekLabel:"Ja",allDayHtml:"Gjithë
ditën",eventLimitText:function(e){return"+më tepër "+e},noEventsMessage:"Nuk ka evente për të shfaqur"},{code:"sr-cyrl",week:{dow:1,doy:7},buttonText:{prev:"Претходна",next:"следећи",today:"Данас",month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},weekLabel:"Сед",allDayText:"Цео дан",eventLimitText:function(e){return"+ још "+e},noEventsMessage:"Нема догађаја за приказ"},{code:"sr",week:{dow:1,doy:7},buttonText:{prev:"Prethodna",next:"Sledeći",today:"Danas",month:"Mеsеc",week:"Nеdеlja",day:"Dan",list:"Planеr"},weekLabel:"Sed",allDayText:"Cеo dan",eventLimitText:function(e){return"+ još "+e},noEventsMessage:"Nеma događaja za prikaz"},{code:"sv",week:{dow:1,doy:4},buttonText:{prev:"Förra",next:"Nästa",today:"Idag",month:"Månad",week:"Vecka",day:"Dag",list:"Program"},weekLabel:"v.",allDayText:"Heldag",eventLimitText:"till",noEventsMessage:"Inga händelser att visa"},{code:"th",buttonText:{prev:"ย้อน",next:"ถัดไป",today:"วันนี้",month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม",noEventsMessage:"ไม่มีกิจกรรมที่จะแสดง"},{code:"tr",week:{dow:1,doy:7},buttonText:{prev:"geri",next:"ileri",today:"bugün",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},weekLabel:"Hf",allDayText:"Tüm gün",eventLimitText:"daha fazla",noEventsMessage:"Gösterilecek etkinlik yok"},{code:"uk",week:{dow:1,doy:7},buttonText:{prev:"Попередній",next:"далі",today:"Сьогодні",month:"Місяць",week:"Тиждень",day:"День",list:"Порядок денний"},weekLabel:"Тиж",allDayText:"Увесь день",eventLimitText:function(e){return"+ще "+e+"..."},noEventsMessage:"Немає подій для відображення"},{code:"vi",week:{dow:1,doy:4},buttonText:{prev:"Trước",next:"Tiếp",today:"Hôm nay",month:"Tháng",week:"Tuần",day:"Ngày",list:"Lịch biểu"},weekLabel:"Tu",allDayText:"Cả ngày",eventLimitText:function(e){return"+ thêm "+e},noEventsMessage:"Không có sự kiện để hiển thị"},{code:"zh-cn",week:{dow:1,doy:4},buttonText:{prev:"上月",next:"下月",today:"今天",month:"月",week:"周",day:"日",list:"日程"},weekLabel:"周",allDayText:"全天",eventLimitText:function(e){return"另外 "+e+" 个"},noEventsMessage:"没有事件显示"},{code:"zh-tw",buttonText:{prev:"上月",next:"下月",today:"今天",month:"月",week:"週",day:"天",list:"活動列表"},weekLabel:"周",allDayText:"整天",eventLimitText:"顯示更多",noEventsMessage:"没有任何活動"}]});
/*!
FullCalendar Core Package v4.3.1
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.FullCalendar = {}));
}(this, function (exports) { 'use strict';
// Creating
// ----------------------------------------------------------------------------------------------------------------
var elementPropHash = {
className: true,
colSpan: true,
rowSpan: true
};
var containerTagHash = {
'= rect.left &&
point.left < rect.right &&
point.top >= rect.top &&
point.top < rect.bottom;
}
// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
function intersectRects(rect1, rect2) {
var res = {
left: Math.max(rect1.left, rect2.left),
right: Math.min(rect1.right, rect2.right),
top: Math.max(rect1.top, rect2.top),
bottom: Math.min(rect1.bottom, rect2.bottom)
};
if (res.left < res.right && res.top < res.bottom) {
return res;
}
return false;
}
function translateRect(rect, deltaX, deltaY) {
return {
left: rect.left + deltaX,
right: rect.right + deltaX,
top: rect.top + deltaY,
bottom: rect.bottom + deltaY
};
}
// Returns a new point that will have been moved to reside within the given rectangle
function constrainPoint(point, rect) {
return {
left: Math.min(Math.max(point.left, rect.left), rect.right),
top: Math.min(Math.max(point.top, rect.top), rect.bottom)
};
}
// Returns a point that is the center of the given rectangle
function getRectCenter(rect) {
return {
left: (rect.left + rect.right) / 2,
top: (rect.top + rect.bottom) / 2
};
}
// Subtracts point2's coordinates from point1's coordinates, returning a delta
function diffPoints(point1, point2) {
return {
left: point1.left - point2.left,
top: point1.top - point2.top
};
}
// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
var isRtlScrollbarOnLeft = null;
function getIsRtlScrollbarOnLeft() {
if (isRtlScrollbarOnLeft === null) {
isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft();
}
return isRtlScrollbarOnLeft;
}
function computeIsRtlScrollbarOnLeft() {
var outerEl = createElement('div', {
style: {
position: 'absolute',
top: -1000,
left: 0,
border: 0,
padding: 0,
overflow: 'scroll',
direction: 'rtl'
}
}, '
function computeEdges(el, getPadding) {
if (getPadding === void 0) { getPadding = false; }
var computedStyle = window.getComputedStyle(el);
var borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0;
var borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0;
var borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0;
var borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
// must use offset(Width|Height) because compatible with client(Width|Height)
var scrollbarLeftRight = sanitizeScrollbarWidth(el.offsetWidth - el.clientWidth - borderLeft - borderRight);
var scrollbarBottom = sanitizeScrollbarWidth(el.offsetHeight - el.clientHeight - borderTop - borderBottom);
var res = {
borderLeft: borderLeft,
borderRight: borderRight,
borderTop: borderTop,
borderBottom: borderBottom,
scrollbarBottom: scrollbarBottom,
scrollbarLeft: 0,
scrollbarRight: 0
};
if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side?
res.scrollbarLeft = scrollbarLeftRight;
}
else {
res.scrollbarRight = scrollbarLeftRight;
}
if (getPadding) {
res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
}
return res;
}
function computeInnerRect(el, goWithinPadding) {
if (goWithinPadding === void 0) { goWithinPadding = false; }
var outerRect = computeRect(el);
var edges = computeEdges(el, goWithinPadding);
var res = {
left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,
right: outerRect.right - edges.borderRight - edges.scrollbarRight,
top: outerRect.top + edges.borderTop,
bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom
};
if (goWithinPadding) {
res.left += edges.paddingLeft;
res.right -= edges.paddingRight;
res.top += edges.paddingTop;
res.bottom -= edges.paddingBottom;
}
return res;
}
function computeRect(el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + window.pageXOffset,
top: rect.top + window.pageYOffset,
right: rect.right + window.pageXOffset,
bottom: rect.bottom + window.pageYOffset
};
}
function computeViewportRect() {
return {
left: window.pageXOffset,
right: window.pageXOffset + document.documentElement.clientWidth,
top: window.pageYOffset,
bottom: window.pageYOffset + document.documentElement.clientHeight
};
}
function computeHeightAndMargins(el) {
return el.getBoundingClientRect().height + computeVMargins(el);
}
function computeVMargins(el) {
var computed = window.getComputedStyle(el);
return parseInt(computed.marginTop, 10) +
parseInt(computed.marginBottom, 10);
}
// does not return window
function getClippingParents(el) {
var parents = [];
while (el instanceof HTMLElement) { // will stop when gets to document or null
var computedStyle = window.getComputedStyle(el);
if (computedStyle.position === 'fixed') {
break;
}
if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) {
parents.push(el);
}
el = el.parentNode;
}
return parents;
}
function computeClippingRect(el) {
return getClippingParents(el)
.map(function (el) {
return computeInnerRect(el);
})
.concat(computeViewportRect())
.reduce(function (rect0, rect1) {
return intersectRects(rect0, rect1) || rect1; // should always intersect
});
}
// Stops a mouse/touch event from doing it's native browser action
function preventDefault(ev) {
ev.preventDefault();
}
// Event Delegation
// ----------------------------------------------------------------------------------------------------------------
function listenBySelector(container, eventType, selector, handler) {
function realHandler(ev) {
var matchedChild = elementClosest(ev.target, selector);
if (matchedChild) {
handler.call(matchedChild, ev, matchedChild);
}
}
container.addEventListener(eventType, realHandler);
return function () {
container.removeEventListener(eventType, realHandler);
};
}
function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) {
var currentMatchedChild;
return listenBySelector(container, 'mouseover', selector, function (ev, matchedChild) {
if (matchedChild !== currentMatchedChild) {
currentMatchedChild = matchedChild;
onMouseEnter(ev, matchedChild);
var realOnMouseLeave_1 = function (ev) {
currentMatchedChild = null;
onMouseLeave(ev, matchedChild);
matchedChild.removeEventListener('mouseleave', realOnMouseLeave_1);
};
// listen to the next mouseleave, and then unattach
matchedChild.addEventListener('mouseleave', realOnMouseLeave_1);
}
});
}
// Animation
// ----------------------------------------------------------------------------------------------------------------
var transitionEventNames = [
'webkitTransitionEnd',
'otransitionend',
'oTransitionEnd',
'msTransitionEnd',
'transitionend'
];
// triggered only when the next single subsequent transition finishes
function whenTransitionDone(el, callback) {
var realCallback = function (ev) {
callback(ev);
transitionEventNames.forEach(function (eventName) {
el.removeEventListener(eventName, realCallback);
});
};
transitionEventNames.forEach(function (eventName) {
el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes
});
}
var DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
// Adding
function addWeeks(m, n) {
var a = dateToUtcArray(m);
a[2] += n * 7;
return arrayToUtcDate(a);
}
function addDays(m, n) {
var a = dateToUtcArray(m);
a[2] += n;
return arrayToUtcDate(a);
}
function addMs(m, n) {
var a = dateToUtcArray(m);
a[6] += n;
return arrayToUtcDate(a);
}
// Diffing (all return floats)
function diffWeeks(m0, m1) {
return diffDays(m0, m1) / 7;
}
function diffDays(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24);
}
function diffHours(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60);
}
function diffMinutes(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / (1000 * 60);
}
function diffSeconds(m0, m1) {
return (m1.valueOf() - m0.valueOf()) / 1000;
}
function diffDayAndTime(m0, m1) {
var m0day = startOfDay(m0);
var m1day = startOfDay(m1);
return {
years: 0,
months: 0,
days: Math.round(diffDays(m0day, m1day)),
milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf())
};
}
// Diffing Whole Units
function diffWholeWeeks(m0, m1) {
var d = diffWholeDays(m0, m1);
if (d !== null && d % 7 === 0) {
return d / 7;
}
return null;
}
function diffWholeDays(m0, m1) {
if (timeAsMs(m0) === timeAsMs(m1)) {
return Math.round(diffDays(m0, m1));
}
return null;
}
// Start-Of
function startOfDay(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate()
]);
}
function startOfHour(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours()
]);
}
function startOfMinute(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours(),
m.getUTCMinutes()
]);
}
function startOfSecond(m) {
return arrayToUtcDate([
m.getUTCFullYear(),
m.getUTCMonth(),
m.getUTCDate(),
m.getUTCHours(),
m.getUTCMinutes(),
m.getUTCSeconds()
]);
}
// Week Computation
function weekOfYear(marker, dow, doy) {
var y = marker.getUTCFullYear();
var w = weekOfGivenYear(marker, y, dow, doy);
if (w < 1) {
return weekOfGivenYear(marker, y - 1, dow, doy);
}
var nextW = weekOfGivenYear(marker, y + 1, dow, doy);
if (nextW >= 1) {
return Math.min(w, nextW);
}
return w;
}
function weekOfGivenYear(marker, year, dow, doy) {
var firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]);
var dayStart = startOfDay(marker);
var days = Math.round(diffDays(firstWeekStart, dayStart));
return Math.floor(days / 7) + 1; // zero-indexed
}
// start-of-first-week - start-of-year
function firstWeekOffset(year, dow, doy) {
// first-week day -- which january is always in the first week (4 for iso, 1 for other)
var fwd = 7 + dow - doy;
// first-week day local weekday -- which local weekday is fwd
var fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7;
return -fwdlw + fwd - 1;
}
// Array Conversion
function dateToLocalArray(date) {
return [
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
];
}
function arrayToLocalDate(a) {
return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month
a[3] || 0, a[4] || 0, a[5] || 0);
}
function dateToUtcArray(date) {
return [
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
];
}
function arrayToUtcDate(a) {
// according to web standards (and Safari), a month index is required.
// massage if only given a year.
if (a.length === 1) {
a = a.concat([0]);
}
return new Date(Date.UTC.apply(Date, a));
}
// Other Utils
function isValidDate(m) {
return !isNaN(m.valueOf());
}
function timeAsMs(m) {
return m.getUTCHours() * 1000 * 60 * 60 +
m.getUTCMinutes() * 1000 * 60 +
m.getUTCSeconds() * 1000 +
m.getUTCMilliseconds();
}
var INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds'];
var PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/;
// Parsing and Creation
function createDuration(input, unit) {
var _a;
if (typeof input === 'string') {
return parseString(input);
}
else if (typeof input === 'object' && input) { // non-null object
return normalizeObject(input);
}
else if (typeof input === 'number') {
return normalizeObject((_a = {}, _a[unit || 'milliseconds'] = input, _a));
}
else {
return null;
}
}
function parseString(s) {
var m = PARSE_RE.exec(s);
if (m) {
var sign = m[1] ? -1 : 1;
return {
years: 0,
months: 0,
days: sign * (m[2] ? parseInt(m[2], 10) : 0),
milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours
(m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes
(m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds
(m[6] ? parseInt(m[6], 10) : 0) // ms
)
};
}
return null;
}
function normalizeObject(obj) {
return {
years: obj.years || obj.year || 0,
months: obj.months || obj.month || 0,
days: (obj.days || obj.day || 0) +
getWeeksFromInput(obj) * 7,
milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours
(obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes
(obj.seconds || obj.second || 0) * 1000 + // seconds
(obj.milliseconds || obj.millisecond || obj.ms || 0) // ms
};
}
function getWeeksFromInput(obj) {
return obj.weeks || obj.week || 0;
}
// Equality
function durationsEqual(d0, d1) {
return d0.years === d1.years &&
d0.months === d1.months &&
d0.days === d1.days &&
d0.milliseconds === d1.milliseconds;
}
function isSingleDay(dur) {
return dur.years === 0 && dur.months === 0 && dur.days === 1 && dur.milliseconds === 0;
}
// Simple Math
function addDurations(d0, d1) {
return {
years: d0.years + d1.years,
months: d0.months + d1.months,
days: d0.days + d1.days,
milliseconds: d0.milliseconds + d1.milliseconds
};
}
function subtractDurations(d1, d0) {
return {
years: d1.years - d0.years,
months: d1.months - d0.months,
days: d1.days - d0.days,
milliseconds: d1.milliseconds - d0.milliseconds
};
}
function multiplyDuration(d, n) {
return {
years: d.years * n,
months: d.months * n,
days: d.days * n,
milliseconds: d.milliseconds * n
};
}
// Conversions
// "Rough" because they are based on average-case Gregorian months/years
function asRoughYears(dur) {
return asRoughDays(dur) / 365;
}
function asRoughMonths(dur) {
return asRoughDays(dur) / 30;
}
function asRoughDays(dur) {
return asRoughMs(dur) / 864e5;
}
function asRoughMinutes(dur) {
return asRoughMs(dur) / (1000 * 60);
}
function asRoughSeconds(dur) {
return asRoughMs(dur) / 1000;
}
function asRoughMs(dur) {
return dur.years * (365 * 864e5) +
dur.months * (30 * 864e5) +
dur.days * 864e5 +
dur.milliseconds;
}
// Advanced Math
function wholeDivideDurations(numerator, denominator) {
var res = null;
for (var i = 0; i < INTERNAL_UNITS.length; i++) {
var unit = INTERNAL_UNITS[i];
if (denominator[unit]) {
var localRes = numerator[unit] / denominator[unit];
if (!isInt(localRes) || (res !== null && res !== localRes)) {
return null;
}
res = localRes;
}
else if (numerator[unit]) {
// needs to divide by something but can't!
return null;
}
}
return res;
}
function greatestDurationDenominator(dur, dontReturnWeeks) {
var ms = dur.milliseconds;
if (ms) {
if (ms % 1000 !== 0) {
return { unit: 'millisecond', value: ms };
}
if (ms % (1000 * 60) !== 0) {
return { unit: 'second', value: ms / 1000 };
}
if (ms % (1000 * 60 * 60) !== 0) {
return { unit: 'minute', value: ms / (1000 * 60) };
}
if (ms) {
return { unit: 'hour', value: ms / (1000 * 60 * 60) };
}
}
if (dur.days) {
if (!dontReturnWeeks && dur.days % 7 === 0) {
return { unit: 'week', value: dur.days / 7 };
}
return { unit: 'day', value: dur.days };
}
if (dur.months) {
return { unit: 'month', value: dur.months };
}
if (dur.years) {
return { unit: 'year', value: dur.years };
}
return { unit: 'millisecond', value: 0 };
}
/* FullCalendar-specific DOM Utilities
----------------------------------------------------------------------------------------------------------------------*/
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
function compensateScroll(rowEl, scrollbarWidths) {
if (scrollbarWidths.left) {
applyStyle(rowEl, {
borderLeftWidth: 1,
marginLeft: scrollbarWidths.left - 1
});
}
if (scrollbarWidths.right) {
applyStyle(rowEl, {
borderRightWidth: 1,
marginRight: scrollbarWidths.right - 1
});
}
}
// Undoes compensateScroll and restores all borders/margins
function uncompensateScroll(rowEl) {
applyStyle(rowEl, {
marginLeft: '',
marginRight: '',
borderLeftWidth: '',
borderRightWidth: ''
});
}
// Make the mouse cursor express that an event is not allowed in the current area
function disableCursor() {
document.body.classList.add('fc-not-allowed');
}
// Returns the mouse cursor to its original look
function enableCursor() {
document.body.classList.remove('fc-not-allowed');
}
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
// reduces the available height.
function distributeHeight(els, availableHeight, shouldRedistribute) {
// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
var flexEls = []; // elements that are allowed to expand. array of DOM nodes
var flexOffsets = []; // amount of vertical space it takes up
var flexHeights = []; // actual css height
var usedHeight = 0;
undistributeHeight(els); // give all elements their natural height
// find elements that are below the recommended height (expandable).
// important to query for heights in a single first pass (to avoid reflow oscillation).
els.forEach(function (el, i) {
var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
var naturalHeight = el.getBoundingClientRect().height;
var naturalOffset = naturalHeight + computeVMargins(el);
if (naturalOffset < minOffset) {
flexEls.push(el);
flexOffsets.push(naturalOffset);
flexHeights.push(naturalHeight);
}
else {
// this element stretches past recommended height (non-expandable). mark the space as occupied.
usedHeight += naturalOffset;
}
});
// readjust the recommended height to only consider the height available to non-maxed-out rows.
if (shouldRedistribute) {
availableHeight -= usedHeight;
minOffset1 = Math.floor(availableHeight / flexEls.length);
minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
}
// assign heights to all expandable elements
flexEls.forEach(function (el, i) {
var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
var naturalOffset = flexOffsets[i];
var naturalHeight = flexHeights[i];
var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
el.style.height = newHeight + 'px';
}
});
}
// Undoes distrubuteHeight, restoring all els to their natural height
function undistributeHeight(els) {
els.forEach(function (el) {
el.style.height = '';
});
}
// Given `els`, a set of cells, find the cell with the largest natural width and set the widths of all the
// cells to be that width.
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
function matchCellWidths(els) {
var maxInnerWidth = 0;
els.forEach(function (el) {
var innerEl = el.firstChild; // hopefully an element
if (innerEl instanceof HTMLElement) {
var innerWidth_1 = innerEl.getBoundingClientRect().width;
if (innerWidth_1 > maxInnerWidth) {
maxInnerWidth = innerWidth_1;
}
}
});
maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
els.forEach(function (el) {
el.style.width = maxInnerWidth + 'px';
});
return maxInnerWidth;
}
// Given one element that resides inside another,
// Subtracts the height of the inner element from the outer element.
function subtractInnerElHeight(outerEl, innerEl) {
// effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
var reflowStyleProps = {
position: 'relative',
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
};
applyStyle(outerEl, reflowStyleProps);
applyStyle(innerEl, reflowStyleProps);
var diff = // grab the dimensions
outerEl.getBoundingClientRect().height -
innerEl.getBoundingClientRect().height;
// undo hack
var resetStyleProps = { position: '', left: '' };
applyStyle(outerEl, resetStyleProps);
applyStyle(innerEl, resetStyleProps);
return diff;
}
/* Selection
----------------------------------------------------------------------------------------------------------------------*/
function preventSelection(el) {
el.classList.add('fc-unselectable');
el.addEventListener('selectstart', preventDefault);
}
function allowSelection(el) {
el.classList.remove('fc-unselectable');
el.removeEventListener('selectstart', preventDefault);
}
/* Context Menu
----------------------------------------------------------------------------------------------------------------------*/
function preventContextMenu(el) {
el.addEventListener('contextmenu', preventDefault);
}
function allowContextMenu(el) {
el.removeEventListener('contextmenu', preventDefault);
}
/* Object Ordering by Field
----------------------------------------------------------------------------------------------------------------------*/
function parseFieldSpecs(input) {
var specs = [];
var tokens = [];
var i;
var token;
if (typeof input === 'string') {
tokens = input.split(/\s*,\s*/);
}
else if (typeof input === 'function') {
tokens = [input];
}
else if (Array.isArray(input)) {
tokens = input;
}
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
if (typeof token === 'string') {
specs.push(token.charAt(0) === '-' ?
{ field: token.substring(1), order: -1 } :
{ field: token, order: 1 });
}
else if (typeof token === 'function') {
specs.push({ func: token });
}
}
return specs;
}
function compareByFieldSpecs(obj0, obj1, fieldSpecs) {
var i;
var cmp;
for (i = 0; i < fieldSpecs.length; i++) {
cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]);
if (cmp) {
return cmp;
}
}
return 0;
}
function compareByFieldSpec(obj0, obj1, fieldSpec) {
if (fieldSpec.func) {
return fieldSpec.func(obj0, obj1);
}
return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field])
* (fieldSpec.order || 1);
}
function flexibleCompare(a, b) {
if (!a && !b) {
return 0;
}
if (b == null) {
return -1;
}
if (a == null) {
return 1;
}
if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b));
}
return a - b;
}
/* String Utilities
----------------------------------------------------------------------------------------------------------------------*/
function capitaliseFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function padStart(val, len) {
var s = String(val);
return '000'.substr(0, len - s.length) + s;
}
/* Number Utilities
----------------------------------------------------------------------------------------------------------------------*/
function compareNumbers(a, b) {
return a - b;
}
function isInt(n) {
return n % 1 === 0;
}
/* Weird Utilities
----------------------------------------------------------------------------------------------------------------------*/
function applyAll(functions, thisObj, args) {
if (typeof functions === 'function') { // supplied a single function
functions = [functions];
}
if (functions) {
var i = void 0;
var ret = void 0;
for (i = 0; i < functions.length; i++) {
ret = functions[i].apply(thisObj, args) || ret;
}
return ret;
}
}
function firstDefined() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
if (args[i] !== undefined) {
return args[i];
}
}
}
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
function debounce(func, wait) {
var timeout;
var args;
var context;
var timestamp;
var result;
var later = function () {
var last = new Date().valueOf() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
}
else {
timeout = null;
result = func.apply(context, args);
context = args = null;
}
};
return function () {
context = this;
args = arguments;
timestamp = new Date().valueOf();
if (!timeout) {
timeout = setTimeout(later, wait);
}
return result;
};
}
// Number and Boolean are only types that defaults or not computed for
// TODO: write more comments
function refineProps(rawProps, processors, defaults, leftoverProps) {
if (defaults === void 0) { defaults = {}; }
var refined = {};
for (var key in processors) {
var processor = processors[key];
if (rawProps[key] !== undefined) {
// found
if (processor === Function) {
refined[key] = typeof rawProps[key] === 'function' ? rawProps[key] : null;
}
else if (processor) { // a refining function?
refined[key] = processor(rawProps[key]);
}
else {
refined[key] = rawProps[key];
}
}
else if (defaults[key] !== undefined) {
// there's an explicit default
refined[key] = defaults[key];
}
else {
// must compute a default
if (processor === String) {
refined[key] = ''; // empty string is default for String
}
else if (!processor || processor === Number || processor === Boolean || processor === Function) {
refined[key] = null; // assign null for other non-custom processor funcs
}
else {
refined[key] = processor(null); // run the custom processor func
}
}
}
if (leftoverProps) {
for (var key in rawProps) {
if (processors[key] === undefined) {
leftoverProps[key] = rawProps[key];
}
}
}
return refined;
}
/* Date stuff that doesn't belong in datelib core
----------------------------------------------------------------------------------------------------------------------*/
// given a timed range, computes an all-day range that has the same exact duration,
// but whose start time is aligned with the start of the day.
function computeAlignedDayRange(timedRange) {
var dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1;
var start = startOfDay(timedRange.start);
var end = addDays(start, dayCnt);
return { start: start, end: end };
}
// given a timed range, computes an all-day range based on how for the end date bleeds into the next day
// TODO: give nextDayThreshold a default arg
function computeVisibleDayRange(timedRange, nextDayThreshold) {
if (nextDayThreshold === void 0) { nextDayThreshold = createDuration(0); }
var startDay = null;
var endDay = null;
if (timedRange.end) {
endDay = startOfDay(timedRange.end);
var endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay`
// If the end time is actually inclusively part of the next day and is equal to or
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
endDay = addDays(endDay, 1);
}
}
if (timedRange.start) {
startDay = startOfDay(timedRange.start); // the beginning of the day the range starts
// If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
if (endDay && endDay <= startDay) {
endDay = addDays(startDay, 1);
}
}
return { start: startDay, end: endDay };
}
// spans from one day into another?
function isMultiDayRange(range) {
var visibleRange = computeVisibleDayRange(range);
return diffDays(visibleRange.start, visibleRange.end) > 1;
}
function diffDates(date0, date1, dateEnv, largeUnit) {
if (largeUnit === 'year') {
return createDuration(dateEnv.diffWholeYears(date0, date1), 'year');
}
else if (largeUnit === 'month') {
return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month');
}
else {
return diffDayAndTime(date0, date1); // returns a duration
}
}
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function parseRecurring(eventInput, allDayDefault, dateEnv, recurringTypes, leftovers) {
for (var i = 0; i < recurringTypes.length; i++) {
var localLeftovers = {};
var parsed = recurringTypes[i].parse(eventInput, localLeftovers, dateEnv);
if (parsed) {
var allDay = localLeftovers.allDay;
delete localLeftovers.allDay; // remove from leftovers
if (allDay == null) {
allDay = allDayDefault;
if (allDay == null) {
allDay = parsed.allDayGuess;
if (allDay == null) {
allDay = false;
}
}
}
__assign(leftovers, localLeftovers);
return {
allDay: allDay,
duration: parsed.duration,
typeData: parsed.typeData,
typeId: i
};
}
}
return null;
}
/*
Event MUST have a recurringDef
*/
function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) {
var typeDef = recurringTypes[eventDef.recurringDef.typeId];
var markers = typeDef.expand(eventDef.recurringDef.typeData, {
start: dateEnv.subtract(framingRange.start, duration),
end: framingRange.end
}, dateEnv);
// the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to
if (eventDef.allDay) {
markers = markers.map(startOfDay);
}
return markers;
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
// Merges an array of objects into a single object.
// The second argument allows for an array of property names who's object values will be merged together.
function mergeProps(propObjs, complexProps) {
var dest = {};
var i;
var name;
var complexObjs;
var j;
var val;
var props;
if (complexProps) {
for (i = 0; i < complexProps.length; i++) {
name = complexProps[i];
complexObjs = [];
// collect the trailing object values, stopping when a non-object is discovered
for (j = propObjs.length - 1; j >= 0; j--) {
val = propObjs[j][name];
if (typeof val === 'object' && val) { // non-null object
complexObjs.unshift(val);
}
else if (val !== undefined) {
dest[name] = val; // if there were no objects, this value will be used
break;
}
}
// if the trailing values were objects, use the merged value
if (complexObjs.length) {
dest[name] = mergeProps(complexObjs);
}
}
}
// copy values into the destination, going from last to first
for (i = propObjs.length - 1; i >= 0; i--) {
props = propObjs[i];
for (name in props) {
if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
dest[name] = props[name];
}
}
}
return dest;
}
function filterHash(hash, func) {
var filtered = {};
for (var key in hash) {
if (func(hash[key], key)) {
filtered[key] = hash[key];
}
}
return filtered;
}
function mapHash(hash, func) {
var newHash = {};
for (var key in hash) {
newHash[key] = func(hash[key], key);
}
return newHash;
}
function arrayToHash(a) {
var hash = {};
for (var _i = 0, a_1 = a; _i < a_1.length; _i++) {
var item = a_1[_i];
hash[item] = true;
}
return hash;
}
function hashValuesToArray(obj) {
var a = [];
for (var key in obj) {
a.push(obj[key]);
}
return a;
}
function isPropsEqual(obj0, obj1) {
for (var key in obj0) {
if (hasOwnProperty.call(obj0, key)) {
if (!(key in obj1)) {
return false;
}
}
}
for (var key in obj1) {
if (hasOwnProperty.call(obj1, key)) {
if (obj0[key] !== obj1[key]) {
return false;
}
}
}
return true;
}
function parseEvents(rawEvents, sourceId, calendar, allowOpenRange) {
var eventStore = createEmptyEventStore();
for (var _i = 0, rawEvents_1 = rawEvents; _i < rawEvents_1.length; _i++) {
var rawEvent = rawEvents_1[_i];
var tuple = parseEvent(rawEvent, sourceId, calendar, allowOpenRange);
if (tuple) {
eventTupleToStore(tuple, eventStore);
}
}
return eventStore;
}
function eventTupleToStore(tuple, eventStore) {
if (eventStore === void 0) { eventStore = createEmptyEventStore(); }
eventStore.defs[tuple.def.defId] = tuple.def;
if (tuple.instance) {
eventStore.instances[tuple.instance.instanceId] = tuple.instance;
}
return eventStore;
}
function expandRecurring(eventStore, framingRange, calendar) {
var dateEnv = calendar.dateEnv;
var defs = eventStore.defs, instances = eventStore.instances;
// remove existing recurring instances
instances = filterHash(instances, function (instance) {
return !defs[instance.defId].recurringDef;
});
for (var defId in defs) {
var def = defs[defId];
if (def.recurringDef) {
var duration = def.recurringDef.duration;
if (!duration) {
duration = def.allDay ?
calendar.defaultAllDayEventDuration :
calendar.defaultTimedEventDuration;
}
var starts = expandRecurringRanges(def, duration, framingRange, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes);
for (var _i = 0, starts_1 = starts; _i < starts_1.length; _i++) {
var start = starts_1[_i];
var instance = createEventInstance(defId, {
start: start,
end: dateEnv.add(start, duration)
});
instances[instance.instanceId] = instance;
}
}
}
return { defs: defs, instances: instances };
}
// retrieves events that have the same groupId as the instance specified by `instanceId`
// or they are the same as the instance.
// why might instanceId not be in the store? an event from another calendar?
function getRelevantEvents(eventStore, instanceId) {
var instance = eventStore.instances[instanceId];
if (instance) {
var def_1 = eventStore.defs[instance.defId];
// get events/instances with same group
var newStore = filterEventStoreDefs(eventStore, function (lookDef) {
return isEventDefsGrouped(def_1, lookDef);
});
// add the original
// TODO: wish we could use eventTupleToStore or something like it
newStore.defs[def_1.defId] = def_1;
newStore.instances[instance.instanceId] = instance;
return newStore;
}
return createEmptyEventStore();
}
function isEventDefsGrouped(def0, def1) {
return Boolean(def0.groupId && def0.groupId === def1.groupId);
}
function transformRawEvents(rawEvents, eventSource, calendar) {
var calEachTransform = calendar.opt('eventDataTransform');
var sourceEachTransform = eventSource ? eventSource.eventDataTransform : null;
if (sourceEachTransform) {
rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform);
}
if (calEachTransform) {
rawEvents = transformEachRawEvent(rawEvents, calEachTransform);
}
return rawEvents;
}
function transformEachRawEvent(rawEvents, func) {
var refinedEvents;
if (!func) {
refinedEvents = rawEvents;
}
else {
refinedEvents = [];
for (var _i = 0, rawEvents_2 = rawEvents; _i < rawEvents_2.length; _i++) {
var rawEvent = rawEvents_2[_i];
var refinedEvent = func(rawEvent);
if (refinedEvent) {
refinedEvents.push(refinedEvent);
}
else if (refinedEvent == null) {
refinedEvents.push(rawEvent);
} // if a different falsy value, do nothing
}
}
return refinedEvents;
}
function createEmptyEventStore() {
return { defs: {}, instances: {} };
}
function mergeEventStores(store0, store1) {
return {
defs: __assign({}, store0.defs, store1.defs),
instances: __assign({}, store0.instances, store1.instances)
};
}
function filterEventStoreDefs(eventStore, filterFunc) {
var defs = filterHash(eventStore.defs, filterFunc);
var instances = filterHash(eventStore.instances, function (instance) {
return defs[instance.defId]; // still exists?
});
return { defs: defs, instances: instances };
}
function parseRange(input, dateEnv) {
var start = null;
var end = null;
if (input.start) {
start = dateEnv.createMarker(input.start);
}
if (input.end) {
end = dateEnv.createMarker(input.end);
}
if (!start && !end) {
return null;
}
if (start && end && end < start) {
return null;
}
return { start: start, end: end };
}
// SIDE-EFFECT: will mutate ranges.
// Will return a new array result.
function invertRanges(ranges, constraintRange) {
var invertedRanges = [];
var start = constraintRange.start; // the end of the previous range. the start of the new range
var i;
var dateRange;
// ranges need to be in order. required for our date-walking algorithm
ranges.sort(compareRanges);
for (i = 0; i < ranges.length; i++) {
dateRange = ranges[i];
// add the span of time before the event (if there is any)
if (dateRange.start > start) { // compare millisecond time (skip any ambig logic)
invertedRanges.push({ start: start, end: dateRange.start });
}
if (dateRange.end > start) {
start = dateRange.end;
}
}
// add the span of time after the last event (if there is any)
if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic)
invertedRanges.push({ start: start, end: constraintRange.end });
}
return invertedRanges;
}
function compareRanges(range0, range1) {
return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first
}
function intersectRanges(range0, range1) {
var start = range0.start;
var end = range0.end;
var newRange = null;
if (range1.start !== null) {
if (start === null) {
start = range1.start;
}
else {
start = new Date(Math.max(start.valueOf(), range1.start.valueOf()));
}
}
if (range1.end != null) {
if (end === null) {
end = range1.end;
}
else {
end = new Date(Math.min(end.valueOf(), range1.end.valueOf()));
}
}
if (start === null || end === null || start < end) {
newRange = { start: start, end: end };
}
return newRange;
}
function rangesEqual(range0, range1) {
return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) &&
(range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf());
}
function rangesIntersect(range0, range1) {
return (range0.end === null || range1.start === null || range0.end > range1.start) &&
(range0.start === null || range1.end === null || range0.start < range1.end);
}
function rangeContainsRange(outerRange, innerRange) {
return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) &&
(outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end));
}
function rangeContainsMarker(range, date) {
return (range.start === null || date >= range.start) &&
(range.end === null || date < range.end);
}
// If the given date is not within the given range, move it inside.
// (If it's past the end, make it one millisecond before the end).
function constrainMarkerToRange(date, range) {
if (range.start != null && date < range.start) {
return range.start;
}
if (range.end != null && date >= range.end) {
return new Date(range.end.valueOf() - 1);
}
return date;
}
function removeExact(array, exactVal) {
var removeCnt = 0;
var i = 0;
while (i < array.length) {
if (array[i] === exactVal) {
array.splice(i, 1);
removeCnt++;
}
else {
i++;
}
}
return removeCnt;
}
function isArraysEqual(a0, a1) {
var len = a0.length;
var i;
if (len !== a1.length) { // not array? or not same length?
return false;
}
for (i = 0; i < len; i++) {
if (a0[i] !== a1[i]) {
return false;
}
}
return true;
}
function memoize(workerFunc) {
var args;
var res;
return function () {
if (!args || !isArraysEqual(args, arguments)) {
args = arguments;
res = workerFunc.apply(this, arguments);
}
return res;
};
}
/*
always executes the workerFunc, but if the result is equal to the previous result,
return the previous result instead.
*/
function memoizeOutput(workerFunc, equalityFunc) {
var cachedRes = null;
return function () {
var newRes = workerFunc.apply(this, arguments);
if (cachedRes === null || !(cachedRes === newRes || equalityFunc(cachedRes, newRes))) {
cachedRes = newRes;
}
return cachedRes;
};
}
var EXTENDED_SETTINGS_AND_SEVERITIES = {
week: 3,
separator: 0,
omitZeroMinute: 0,
meridiem: 0,
omitCommas: 0
};
var STANDARD_DATE_PROP_SEVERITIES = {
timeZoneName: 7,
era: 6,
year: 5,
month: 4,
day: 2,
weekday: 2,
hour: 1,
minute: 1,
second: 1
};
var MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too
var COMMA_RE = /,/g; // we need re for globalness
var MULTI_SPACE_RE = /\s+/g;
var LTR_RE = /\u200e/g; // control character
var UTC_RE = /UTC|GMT/;
var NativeFormatter = /** @class */ (function () {
function NativeFormatter(formatSettings) {
var standardDateProps = {};
var extendedSettings = {};
var severity = 0;
for (var name_1 in formatSettings) {
if (name_1 in EXTENDED_SETTINGS_AND_SEVERITIES) {
extendedSettings[name_1] = formatSettings[name_1];
severity = Math.max(EXTENDED_SETTINGS_AND_SEVERITIES[name_1], severity);
}
else {
standardDateProps[name_1] = formatSettings[name_1];
if (name_1 in STANDARD_DATE_PROP_SEVERITIES) {
severity = Math.max(STANDARD_DATE_PROP_SEVERITIES[name_1], severity);
}
}
}
this.standardDateProps = standardDateProps;
this.extendedSettings = extendedSettings;
this.severity = severity;
this.buildFormattingFunc = memoize(buildFormattingFunc);
}
NativeFormatter.prototype.format = function (date, context) {
return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date);
};
NativeFormatter.prototype.formatRange = function (start, end, context) {
var _a = this, standardDateProps = _a.standardDateProps, extendedSettings = _a.extendedSettings;
var diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem);
if (!diffSeverity) {
return this.format(start, context);
}
var biggestUnitForPartial = diffSeverity;
if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time
(standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') &&
(standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') &&
(standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) {
biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time
}
var full0 = this.format(start, context);
var full1 = this.format(end, context);
if (full0 === full1) {
return full0;
}
var partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial);
var partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context);
var partial0 = partialFormattingFunc(start);
var partial1 = partialFormattingFunc(end);
var insertion = findCommonInsertion(full0, partial0, full1, partial1);
var separator = extendedSettings.separator || '';
if (insertion) {
return insertion.before + partial0 + separator + partial1 + insertion.after;
}
return full0 + separator + full1;
};
NativeFormatter.prototype.getLargestUnit = function () {
switch (this.severity) {
case 7:
case 6:
case 5:
return 'year';
case 4:
return 'month';
case 3:
return 'week';
default:
return 'day';
}
};
return NativeFormatter;
}());
function buildFormattingFunc(standardDateProps, extendedSettings, context) {
var standardDatePropCnt = Object.keys(standardDateProps).length;
if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') {
return function (date) {
return formatTimeZoneOffset(date.timeZoneOffset);
};
}
if (standardDatePropCnt === 0 && extendedSettings.week) {
return function (date) {
return formatWeekNumber(context.computeWeekNumber(date.marker), context.weekLabel, context.locale, extendedSettings.week);
};
}
return buildNativeFormattingFunc(standardDateProps, extendedSettings, context);
}
function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) {
standardDateProps = __assign({}, standardDateProps); // copy
extendedSettings = __assign({}, extendedSettings); // copy
sanitizeSettings(standardDateProps, extendedSettings);
standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers
var normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps);
var zeroFormat; // needed?
if (extendedSettings.omitZeroMinute) {
var zeroProps = __assign({}, standardDateProps);
delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings
zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps);
}
return function (date) {
var marker = date.marker;
var format;
if (zeroFormat && !marker.getUTCMinutes()) {
format = zeroFormat;
}
else {
format = normalFormat;
}
var s = format.format(marker);
return postProcess(s, date, standardDateProps, extendedSettings, context);
};
}
function sanitizeSettings(standardDateProps, extendedSettings) {
// deal with a browser inconsistency where formatting the timezone
// requires that the hour/minute be present.
if (standardDateProps.timeZoneName) {
if (!standardDateProps.hour) {
standardDateProps.hour = '2-digit';
}
if (!standardDateProps.minute) {
standardDateProps.minute = '2-digit';
}
}
// only support short timezone names
if (standardDateProps.timeZoneName === 'long') {
standardDateProps.timeZoneName = 'short';
}
// if requesting to display seconds, MUST display minutes
if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) {
delete extendedSettings.omitZeroMinute;
}
}
function postProcess(s, date, standardDateProps, extendedSettings, context) {
s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes
if (standardDateProps.timeZoneName === 'short') {
s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ?
'UTC' : // important to normalize for IE, which does "GMT"
formatTimeZoneOffset(date.timeZoneOffset));
}
if (extendedSettings.omitCommas) {
s = s.replace(COMMA_RE, '').trim();
}
if (extendedSettings.omitZeroMinute) {
s = s.replace(':00', ''); // zeroFormat doesn't always achieve this
}
// ^ do anything that might create adjacent spaces before this point,
// because MERIDIEM_RE likes to eat up loading spaces
if (extendedSettings.meridiem === false) {
s = s.replace(MERIDIEM_RE, '').trim();
}
else if (extendedSettings.meridiem === 'narrow') { // a/p
s = s.replace(MERIDIEM_RE, function (m0, m1) {
return m1.toLocaleLowerCase();
});
}
else if (extendedSettings.meridiem === 'short') { // am/pm
s = s.replace(MERIDIEM_RE, function (m0, m1) {
return m1.toLocaleLowerCase() + 'm';
});
}
else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase
s = s.replace(MERIDIEM_RE, function (m0) {
return m0.toLocaleLowerCase();
});
}
s = s.replace(MULTI_SPACE_RE, ' ');
s = s.trim();
return s;
}
function injectTzoStr(s, tzoStr) {
var replaced = false;
s = s.replace(UTC_RE, function () {
replaced = true;
return tzoStr;
});
// IE11 doesn't include UTC/GMT in the original string, so append to end
if (!replaced) {
s += ' ' + tzoStr;
}
return s;
}
function formatWeekNumber(num, weekLabel, locale, display) {
var parts = [];
if (display === 'narrow') {
parts.push(weekLabel);
}
else if (display === 'short') {
parts.push(weekLabel, ' ');
}
// otherwise, considered 'numeric'
parts.push(locale.simpleNumberFormat.format(num));
if (locale.options.isRtl) { // TODO: use control characters instead?
parts.reverse();
}
return parts.join('');
}
// Range Formatting Utils
// 0 = exactly the same
// 1 = different by time
// and bigger
function computeMarkerDiffSeverity(d0, d1, ca) {
if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) {
return 5;
}
if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) {
return 4;
}
if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) {
return 2;
}
if (timeAsMs(d0) !== timeAsMs(d1)) {
return 1;
}
return 0;
}
function computePartialFormattingOptions(options, biggestUnit) {
var partialOptions = {};
for (var name_2 in options) {
if (!(name_2 in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone)
STANDARD_DATE_PROP_SEVERITIES[name_2] <= biggestUnit) {
partialOptions[name_2] = options[name_2];
}
}
return partialOptions;
}
function findCommonInsertion(full0, partial0, full1, partial1) {
var i0 = 0;
while (i0 < full0.length) {
var found0 = full0.indexOf(partial0, i0);
if (found0 === -1) {
break;
}
var before0 = full0.substr(0, found0);
i0 = found0 + partial0.length;
var after0 = full0.substr(i0);
var i1 = 0;
while (i1 < full1.length) {
var found1 = full1.indexOf(partial1, i1);
if (found1 === -1) {
break;
}
var before1 = full1.substr(0, found1);
i1 = found1 + partial1.length;
var after1 = full1.substr(i1);
if (before0 === before1 && after0 === after1) {
return {
before: before0,
after: after0
};
}
}
}
return null;
}
/*
TODO: fix the terminology of "formatter" vs "formatting func"
*/
/*
At the time of instantiation, this object does not know which cmd-formatting system it will use.
It receives this at the time of formatting, as a setting.
*/
var CmdFormatter = /** @class */ (function () {
function CmdFormatter(cmdStr, separator) {
this.cmdStr = cmdStr;
this.separator = separator;
}
CmdFormatter.prototype.format = function (date, context) {
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, this.separator));
};
CmdFormatter.prototype.formatRange = function (start, end, context) {
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, this.separator));
};
return CmdFormatter;
}());
var FuncFormatter = /** @class */ (function () {
function FuncFormatter(func) {
this.func = func;
}
FuncFormatter.prototype.format = function (date, context) {
return this.func(createVerboseFormattingArg(date, null, context));
};
FuncFormatter.prototype.formatRange = function (start, end, context) {
return this.func(createVerboseFormattingArg(start, end, context));
};
return FuncFormatter;
}());
// Formatter Object Creation
function createFormatter(input, defaultSeparator) {
if (typeof input === 'object' && input) { // non-null object
if (typeof defaultSeparator === 'string') {
input = __assign({ separator: defaultSeparator }, input);
}
return new NativeFormatter(input);
}
else if (typeof input === 'string') {
return new CmdFormatter(input, defaultSeparator);
}
else if (typeof input === 'function') {
return new FuncFormatter(input);
}
}
// String Utils
// timeZoneOffset is in minutes
function buildIsoString(marker, timeZoneOffset, stripZeroTime) {
if (stripZeroTime === void 0) { stripZeroTime = false; }
var s = marker.toISOString();
s = s.replace('.000', '');
if (stripZeroTime) {
s = s.replace('T00:00:00Z', '');
}
if (s.length > 10) { // time part wasn't stripped, can add timezone info
if (timeZoneOffset == null) {
s = s.replace('Z', '');
}
else if (timeZoneOffset !== 0) {
s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true));
}
// otherwise, its UTC-0 and we want to keep the Z
}
return s;
}
function formatIsoTimeString(marker) {
return padStart(marker.getUTCHours(), 2) + ':' +
padStart(marker.getUTCMinutes(), 2) + ':' +
padStart(marker.getUTCSeconds(), 2);
}
function formatTimeZoneOffset(minutes, doIso) {
if (doIso === void 0) { doIso = false; }
var sign = minutes < 0 ? '-' : '+';
var abs = Math.abs(minutes);
var hours = Math.floor(abs / 60);
var mins = Math.round(abs % 60);
if (doIso) {
return sign + padStart(hours, 2) + ':' + padStart(mins, 2);
}
else {
return 'GMT' + sign + hours + (mins ? ':' + padStart(mins, 2) : '');
}
}
// Arg Utils
function createVerboseFormattingArg(start, end, context, separator) {
var startInfo = expandZonedMarker(start, context.calendarSystem);
var endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null;
return {
date: startInfo,
start: startInfo,
end: endInfo,
timeZone: context.timeZone,
localeCodes: context.locale.codes,
separator: separator
};
}
function expandZonedMarker(dateInfo, calendarSystem) {
var a = calendarSystem.markerToArray(dateInfo.marker);
return {
marker: dateInfo.marker,
timeZoneOffset: dateInfo.timeZoneOffset,
array: a,
year: a[0],
month: a[1],
day: a[2],
hour: a[3],
minute: a[4],
second: a[5],
millisecond: a[6]
};
}
var EventSourceApi = /** @class */ (function () {
function EventSourceApi(calendar, internalEventSource) {
this.calendar = calendar;
this.internalEventSource = internalEventSource;
}
EventSourceApi.prototype.remove = function () {
this.calendar.dispatch({
type: 'REMOVE_EVENT_SOURCE',
sourceId: this.internalEventSource.sourceId
});
};
EventSourceApi.prototype.refetch = function () {
this.calendar.dispatch({
type: 'FETCH_EVENT_SOURCES',
sourceIds: [this.internalEventSource.sourceId]
});
};
Object.defineProperty(EventSourceApi.prototype, "id", {
get: function () {
return this.internalEventSource.publicId;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventSourceApi.prototype, "url", {
// only relevant to json-feed event sources
get: function () {
return this.internalEventSource.meta.url;
},
enumerable: true,
configurable: true
});
return EventSourceApi;
}());
var EventApi = /** @class */ (function () {
function EventApi(calendar, def, instance) {
this._calendar = calendar;
this._def = def;
this._instance = instance || null;
}
/*
TODO: make event struct more responsible for this
*/
EventApi.prototype.setProp = function (name, val) {
var _a, _b;
if (name in DATE_PROPS) ;
else if (name in NON_DATE_PROPS) {
if (typeof NON_DATE_PROPS[name] === 'function') {
val = NON_DATE_PROPS[name](val);
}
this.mutate({
standardProps: (_a = {}, _a[name] = val, _a)
});
}
else if (name in UNSCOPED_EVENT_UI_PROPS) {
var ui = void 0;
if (typeof UNSCOPED_EVENT_UI_PROPS[name] === 'function') {
val = UNSCOPED_EVENT_UI_PROPS[name](val);
}
if (name === 'color') {
ui = { backgroundColor: val, borderColor: val };
}
else if (name === 'editable') {
ui = { startEditable: val, durationEditable: val };
}
else {
ui = (_b = {}, _b[name] = val, _b);
}
this.mutate({
standardProps: { ui: ui }
});
}
};
EventApi.prototype.setExtendedProp = function (name, val) {
var _a;
this.mutate({
extendedProps: (_a = {}, _a[name] = val, _a)
});
};
EventApi.prototype.setStart = function (startInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var start = dateEnv.createMarker(startInput);
if (start && this._instance) { // TODO: warning if parsed bad
var instanceRange = this._instance.range;
var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!?
if (options.maintainDuration) {
this.mutate({ datesDelta: startDelta });
}
else {
this.mutate({ startDelta: startDelta });
}
}
};
EventApi.prototype.setEnd = function (endInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var end;
if (endInput != null) {
end = dateEnv.createMarker(endInput);
if (!end) {
return; // TODO: warning if parsed bad
}
}
if (this._instance) {
if (end) {
var endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity);
this.mutate({ endDelta: endDelta });
}
else {
this.mutate({ standardProps: { hasEnd: false } });
}
}
};
EventApi.prototype.setDates = function (startInput, endInput, options) {
if (options === void 0) { options = {}; }
var dateEnv = this._calendar.dateEnv;
var standardProps = { allDay: options.allDay };
var start = dateEnv.createMarker(startInput);
var end;
if (!start) {
return; // TODO: warning if parsed bad
}
if (endInput != null) {
end = dateEnv.createMarker(endInput);
if (!end) { // TODO: warning if parsed bad
return;
}
}
if (this._instance) {
var instanceRange = this._instance.range;
// when computing the diff for an event being converted to all-day,
// compute diff off of the all-day values the way event-mutation does.
if (options.allDay === true) {
instanceRange = computeAlignedDayRange(instanceRange);
}
var startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity);
if (end) {
var endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity);
if (durationsEqual(startDelta, endDelta)) {
this.mutate({ datesDelta: startDelta, standardProps: standardProps });
}
else {
this.mutate({ startDelta: startDelta, endDelta: endDelta, standardProps: standardProps });
}
}
else { // means "clear the end"
standardProps.hasEnd = false;
this.mutate({ datesDelta: startDelta, standardProps: standardProps });
}
}
};
EventApi.prototype.moveStart = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ startDelta: delta });
}
};
EventApi.prototype.moveEnd = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ endDelta: delta });
}
};
EventApi.prototype.moveDates = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // TODO: warning if parsed bad
this.mutate({ datesDelta: delta });
}
};
EventApi.prototype.setAllDay = function (allDay, options) {
if (options === void 0) { options = {}; }
var standardProps = { allDay: allDay };
var maintainDuration = options.maintainDuration;
if (maintainDuration == null) {
maintainDuration = this._calendar.opt('allDayMaintainDuration');
}
if (this._def.allDay !== allDay) {
standardProps.hasEnd = maintainDuration;
}
this.mutate({ standardProps: standardProps });
};
EventApi.prototype.formatRange = function (formatInput) {
var dateEnv = this._calendar.dateEnv;
var instance = this._instance;
var formatter = createFormatter(formatInput, this._calendar.opt('defaultRangeSeparator'));
if (this._def.hasEnd) {
return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, {
forcedStartTzo: instance.forcedStartTzo,
forcedEndTzo: instance.forcedEndTzo
});
}
else {
return dateEnv.format(instance.range.start, formatter, {
forcedTzo: instance.forcedStartTzo
});
}
};
EventApi.prototype.mutate = function (mutation) {
var def = this._def;
var instance = this._instance;
if (instance) {
this._calendar.dispatch({
type: 'MUTATE_EVENTS',
instanceId: instance.instanceId,
mutation: mutation,
fromApi: true
});
var eventStore = this._calendar.state.eventStore;
this._def = eventStore.defs[def.defId];
this._instance = eventStore.instances[instance.instanceId];
}
};
EventApi.prototype.remove = function () {
this._calendar.dispatch({
type: 'REMOVE_EVENT_DEF',
defId: this._def.defId
});
};
Object.defineProperty(EventApi.prototype, "source", {
get: function () {
var sourceId = this._def.sourceId;
if (sourceId) {
return new EventSourceApi(this._calendar, this._calendar.state.eventSources[sourceId]);
}
return null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "start", {
get: function () {
return this._instance ?
this._calendar.dateEnv.toDate(this._instance.range.start) :
null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "end", {
get: function () {
return (this._instance && this._def.hasEnd) ?
this._calendar.dateEnv.toDate(this._instance.range.end) :
null;
},
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "id", {
// computable props that all access the def
// TODO: find a TypeScript-compatible way to do this at scale
get: function () { return this._def.publicId; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "groupId", {
get: function () { return this._def.groupId; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "allDay", {
get: function () { return this._def.allDay; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "title", {
get: function () { return this._def.title; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "url", {
get: function () { return this._def.url; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "rendering", {
get: function () { return this._def.rendering; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "startEditable", {
get: function () { return this._def.ui.startEditable; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "durationEditable", {
get: function () { return this._def.ui.durationEditable; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "constraint", {
get: function () { return this._def.ui.constraints[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "overlap", {
get: function () { return this._def.ui.overlap; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "allow", {
get: function () { return this._def.ui.allows[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "backgroundColor", {
get: function () { return this._def.ui.backgroundColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "borderColor", {
get: function () { return this._def.ui.borderColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "textColor", {
get: function () { return this._def.ui.textColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "classNames", {
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
get: function () { return this._def.ui.classNames; },
enumerable: true,
configurable: true
});
Object.defineProperty(EventApi.prototype, "extendedProps", {
get: function () { return this._def.extendedProps; },
enumerable: true,
configurable: true
});
return EventApi;
}());
/*
Specifying nextDayThreshold signals that all-day ranges should be sliced.
*/
function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) {
var inverseBgByGroupId = {};
var inverseBgByDefId = {};
var defByGroupId = {};
var bgRanges = [];
var fgRanges = [];
var eventUis = compileEventUis(eventStore.defs, eventUiBases);
for (var defId in eventStore.defs) {
var def = eventStore.defs[defId];
if (def.rendering === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId] = [];
if (!defByGroupId[def.groupId]) {
defByGroupId[def.groupId] = def;
}
}
else {
inverseBgByDefId[defId] = [];
}
}
}
for (var instanceId in eventStore.instances) {
var instance = eventStore.instances[instanceId];
var def = eventStore.defs[instance.defId];
var ui = eventUis[def.defId];
var origRange = instance.range;
var normalRange = (!def.allDay && nextDayThreshold) ?
computeVisibleDayRange(origRange, nextDayThreshold) :
origRange;
var slicedRange = intersectRanges(normalRange, framingRange);
if (slicedRange) {
if (def.rendering === 'inverse-background') {
if (def.groupId) {
inverseBgByGroupId[def.groupId].push(slicedRange);
}
else {
inverseBgByDefId[instance.defId].push(slicedRange);
}
}
else {
(def.rendering === 'background' ? bgRanges : fgRanges).push({
def: def,
ui: ui,
instance: instance,
range: slicedRange,
isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(),
isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf()
});
}
}
}
for (var groupId in inverseBgByGroupId) { // BY GROUP
var ranges = inverseBgByGroupId[groupId];
var invertedRanges = invertRanges(ranges, framingRange);
for (var _i = 0, invertedRanges_1 = invertedRanges; _i < invertedRanges_1.length; _i++) {
var invertedRange = invertedRanges_1[_i];
var def = defByGroupId[groupId];
var ui = eventUis[def.defId];
bgRanges.push({
def: def,
ui: ui,
instance: null,
range: invertedRange,
isStart: false,
isEnd: false
});
}
}
for (var defId in inverseBgByDefId) {
var ranges = inverseBgByDefId[defId];
var invertedRanges = invertRanges(ranges, framingRange);
for (var _a = 0, invertedRanges_2 = invertedRanges; _a < invertedRanges_2.length; _a++) {
var invertedRange = invertedRanges_2[_a];
bgRanges.push({
def: eventStore.defs[defId],
ui: eventUis[defId],
instance: null,
range: invertedRange,
isStart: false,
isEnd: false
});
}
}
return { bg: bgRanges, fg: fgRanges };
}
function hasBgRendering(def) {
return def.rendering === 'background' || def.rendering === 'inverse-background';
}
function filterSegsViaEls(view, segs, isMirror) {
if (view.hasPublicHandlers('eventRender')) {
segs = segs.filter(function (seg) {
var custom = view.publiclyTrigger('eventRender', [
{
event: new EventApi(view.calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirror,
isStart: seg.isStart,
isEnd: seg.isEnd,
// TODO: include seg.range once all components consistently generate it
el: seg.el,
view: view
}
]);
if (custom === false) { // means don't render at all
return false;
}
else if (custom && custom !== true) {
seg.el = custom;
}
return true;
});
}
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
setElSeg(seg.el, seg);
}
return segs;
}
function setElSeg(el, seg) {
el.fcSeg = seg;
}
function getElSeg(el) {
return el.fcSeg || null;
}
// event ui computation
function compileEventUis(eventDefs, eventUiBases) {
return mapHash(eventDefs, function (eventDef) {
return compileEventUi(eventDef, eventUiBases);
});
}
function compileEventUi(eventDef, eventUiBases) {
var uis = [];
if (eventUiBases['']) {
uis.push(eventUiBases['']);
}
if (eventUiBases[eventDef.defId]) {
uis.push(eventUiBases[eventDef.defId]);
}
uis.push(eventDef.ui);
return combineEventUis(uis);
}
// applies the mutation to ALL defs/instances within the event store
function applyMutationToEventStore(eventStore, eventConfigBase, mutation, calendar) {
var eventConfigs = compileEventUis(eventStore.defs, eventConfigBase);
var dest = createEmptyEventStore();
for (var defId in eventStore.defs) {
var def = eventStore.defs[defId];
dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, calendar.pluginSystem.hooks.eventDefMutationAppliers, calendar);
}
for (var instanceId in eventStore.instances) {
var instance = eventStore.instances[instanceId];
var def = dest.defs[instance.defId]; // important to grab the newly modified def
dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, calendar);
}
return dest;
}
function applyMutationToEventDef(eventDef, eventConfig, mutation, appliers, calendar) {
var standardProps = mutation.standardProps || {};
// if hasEnd has not been specified, guess a good value based on deltas.
// if duration will change, there's no way the default duration will persist,
// and thus, we need to mark the event as having a real end
if (standardProps.hasEnd == null &&
eventConfig.durationEditable &&
(mutation.startDelta || mutation.endDelta)) {
standardProps.hasEnd = true; // TODO: is this mutation okay?
}
var copy = __assign({}, eventDef, standardProps, { ui: __assign({}, eventDef.ui, standardProps.ui) });
if (mutation.extendedProps) {
copy.extendedProps = __assign({}, copy.extendedProps, mutation.extendedProps);
}
for (var _i = 0, appliers_1 = appliers; _i < appliers_1.length; _i++) {
var applier = appliers_1[_i];
applier(copy, mutation, calendar);
}
if (!copy.hasEnd && calendar.opt('forceEventDuration')) {
copy.hasEnd = true;
}
return copy;
}
function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef
eventConfig, mutation, calendar) {
var dateEnv = calendar.dateEnv;
var forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true;
var clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false;
var copy = __assign({}, eventInstance);
if (forceAllDay) {
copy.range = computeAlignedDayRange(copy.range);
}
if (mutation.datesDelta && eventConfig.startEditable) {
copy.range = {
start: dateEnv.add(copy.range.start, mutation.datesDelta),
end: dateEnv.add(copy.range.end, mutation.datesDelta)
};
}
if (mutation.startDelta && eventConfig.durationEditable) {
copy.range = {
start: dateEnv.add(copy.range.start, mutation.startDelta),
end: copy.range.end
};
}
if (mutation.endDelta && eventConfig.durationEditable) {
copy.range = {
start: copy.range.start,
end: dateEnv.add(copy.range.end, mutation.endDelta)
};
}
if (clearEnd) {
copy.range = {
start: copy.range.start,
end: calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start)
};
}
// in case event was all-day but the supplied deltas were not
// better util for this?
if (eventDef.allDay) {
copy.range = {
start: startOfDay(copy.range.start),
end: startOfDay(copy.range.end)
};
}
// handle invalid durations
if (copy.range.end < copy.range.start) {
copy.range.end = calendar.getDefaultEventEnd(eventDef.allDay, copy.range.start);
}
return copy;
}
function reduceEventStore (eventStore, action, eventSources, dateProfile, calendar) {
switch (action.type) {
case 'RECEIVE_EVENTS': // raw
return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, calendar);
case 'ADD_EVENTS': // already parsed, but not expanded
return addEvent(eventStore, action.eventStore, // new ones
dateProfile ? dateProfile.activeRange : null, calendar);
case 'MERGE_EVENTS': // already parsed and expanded
return mergeEventStores(eventStore, action.eventStore);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
if (dateProfile) {
return expandRecurring(eventStore, dateProfile.activeRange, calendar);
}
else {
return eventStore;
}
case 'CHANGE_TIMEZONE':
return rezoneDates(eventStore, action.oldDateEnv, calendar.dateEnv);
case 'MUTATE_EVENTS':
return applyMutationToRelated(eventStore, action.instanceId, action.mutation, action.fromApi, calendar);
case 'REMOVE_EVENT_INSTANCES':
return excludeInstances(eventStore, action.instances);
case 'REMOVE_EVENT_DEF':
return filterEventStoreDefs(eventStore, function (eventDef) {
return eventDef.defId !== action.defId;
});
case 'REMOVE_EVENT_SOURCE':
return excludeEventsBySourceId(eventStore, action.sourceId);
case 'REMOVE_ALL_EVENT_SOURCES':
return filterEventStoreDefs(eventStore, function (eventDef) {
return !eventDef.sourceId; // only keep events with no source id
});
case 'REMOVE_ALL_EVENTS':
return createEmptyEventStore();
case 'RESET_EVENTS':
return {
defs: eventStore.defs,
instances: eventStore.instances
};
default:
return eventStore;
}
}
function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, calendar) {
if (eventSource && // not already removed
fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
) {
var subset = parseEvents(transformRawEvents(rawEvents, eventSource, calendar), eventSource.sourceId, calendar);
if (fetchRange) {
subset = expandRecurring(subset, fetchRange, calendar);
}
return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset);
}
return eventStore;
}
function addEvent(eventStore, subset, expandRange, calendar) {
if (expandRange) {
subset = expandRecurring(subset, expandRange, calendar);
}
return mergeEventStores(eventStore, subset);
}
function rezoneDates(eventStore, oldDateEnv, newDateEnv) {
var defs = eventStore.defs;
var instances = mapHash(eventStore.instances, function (instance) {
var def = defs[instance.defId];
if (def.allDay || def.recurringDef) {
return instance; // isn't dependent on timezone
}
else {
return __assign({}, instance, { range: {
start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)),
end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo))
}, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo });
}
});
return { defs: defs, instances: instances };
}
function applyMutationToRelated(eventStore, instanceId, mutation, fromApi, calendar) {
var relevant = getRelevantEvents(eventStore, instanceId);
var eventConfigBase = fromApi ?
{ '': {
startEditable: true,
durationEditable: true,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: []
} } :
calendar.eventUiBases;
relevant = applyMutationToEventStore(relevant, eventConfigBase, mutation, calendar);
return mergeEventStores(eventStore, relevant);
}
function excludeEventsBySourceId(eventStore, sourceId) {
return filterEventStoreDefs(eventStore, function (eventDef) {
return eventDef.sourceId !== sourceId;
});
}
// QUESTION: why not just return instances? do a general object-property-exclusion util
function excludeInstances(eventStore, removals) {
return {
defs: eventStore.defs,
instances: filterHash(eventStore.instances, function (instance) {
return !removals[instance.instanceId];
})
};
}
// high-level segmenting-aware tester functions
// ------------------------------------------------------------------------------------------------------------------------
function isInteractionValid(interaction, calendar) {
return isNewPropsValid({ eventDrag: interaction }, calendar); // HACK: the eventDrag props is used for ALL interactions
}
function isDateSelectionValid(dateSelection, calendar) {
return isNewPropsValid({ dateSelection: dateSelection }, calendar);
}
function isNewPropsValid(newProps, calendar) {
var view = calendar.view;
var props = __assign({ businessHours: view ? view.props.businessHours : createEmptyEventStore(), dateSelection: '', eventStore: calendar.state.eventStore, eventUiBases: calendar.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps);
return (calendar.pluginSystem.hooks.isPropsValid || isPropsValid)(props, calendar);
}
function isPropsValid(state, calendar, dateSpanMeta, filterConfig) {
if (dateSpanMeta === void 0) { dateSpanMeta = {}; }
if (state.eventDrag && !isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
return false;
}
if (state.dateSelection && !isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
return false;
}
return true;
}
// Moving Event Validation
// ------------------------------------------------------------------------------------------------------------------------
function isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig) {
var interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions
var subjectEventStore = interaction.mutatedEvents;
var subjectDefs = subjectEventStore.defs;
var subjectInstances = subjectEventStore.instances;
var subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ?
state.eventUiBases :
{ '': calendar.selectionConfig } // if not a real event, validate as a selection
);
if (filterConfig) {
subjectConfigs = mapHash(subjectConfigs, filterConfig);
}
var otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances); // exclude the subject events. TODO: exclude defs too?
var otherDefs = otherEventStore.defs;
var otherInstances = otherEventStore.instances;
var otherConfigs = compileEventUis(otherDefs, state.eventUiBases);
for (var subjectInstanceId in subjectInstances) {
var subjectInstance = subjectInstances[subjectInstanceId];
var subjectRange = subjectInstance.range;
var subjectConfig = subjectConfigs[subjectInstance.defId];
var subjectDef = subjectDefs[subjectInstance.defId];
// constraint
if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, calendar)) {
return false;
}
// overlap
var overlapFunc = calendar.opt('eventOverlap');
if (typeof overlapFunc !== 'function') {
overlapFunc = null;
}
for (var otherInstanceId in otherInstances) {
var otherInstance = otherInstances[otherInstanceId];
// intersect! evaluate
if (rangesIntersect(subjectRange, otherInstance.range)) {
var otherOverlap = otherConfigs[otherInstance.defId].overlap;
// consider the other event's overlap. only do this if the subject event is a "real" event
if (otherOverlap === false && interaction.isEvent) {
return false;
}
if (subjectConfig.overlap === false) {
return false;
}
if (overlapFunc && !overlapFunc(new EventApi(calendar, otherDefs[otherInstance.defId], otherInstance), // still event
new EventApi(calendar, subjectDef, subjectInstance) // moving event
)) {
return false;
}
}
}
// allow (a function)
var calendarEventStore = calendar.state.eventStore; // need global-to-calendar, not local to component (splittable)state
for (var _i = 0, _a = subjectConfig.allows; _i < _a.length; _i++) {
var subjectAllow = _a[_i];
var subjectDateSpan = __assign({}, dateSpanMeta, { range: subjectInstance.range, allDay: subjectDef.allDay });
var origDef = calendarEventStore.defs[subjectDef.defId];
var origInstance = calendarEventStore.instances[subjectInstanceId];
var eventApi = void 0;
if (origDef) { // was previously in the calendar
eventApi = new EventApi(calendar, origDef, origInstance);
}
else { // was an external event
eventApi = new EventApi(calendar, subjectDef); // no instance, because had no dates
}
if (!subjectAllow(calendar.buildDateSpanApi(subjectDateSpan), eventApi)) {
return false;
}
}
}
return true;
}
// Date Selection Validation
// ------------------------------------------------------------------------------------------------------------------------
function isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig) {
var relevantEventStore = state.eventStore;
var relevantDefs = relevantEventStore.defs;
var relevantInstances = relevantEventStore.instances;
var selection = state.dateSelection;
var selectionRange = selection.range;
var selectionConfig = calendar.selectionConfig;
if (filterConfig) {
selectionConfig = filterConfig(selectionConfig);
}
// constraint
if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, calendar)) {
return false;
}
// overlap
var overlapFunc = calendar.opt('selectOverlap');
if (typeof overlapFunc !== 'function') {
overlapFunc = null;
}
for (var relevantInstanceId in relevantInstances) {
var relevantInstance = relevantInstances[relevantInstanceId];
// intersect! evaluate
if (rangesIntersect(selectionRange, relevantInstance.range)) {
if (selectionConfig.overlap === false) {
return false;
}
if (overlapFunc && !overlapFunc(new EventApi(calendar, relevantDefs[relevantInstance.defId], relevantInstance))) {
return false;
}
}
}
// allow (a function)
for (var _i = 0, _a = selectionConfig.allows; _i < _a.length; _i++) {
var selectionAllow = _a[_i];
var fullDateSpan = __assign({}, dateSpanMeta, selection);
if (!selectionAllow(calendar.buildDateSpanApi(fullDateSpan), null)) {
return false;
}
}
return true;
}
// Constraint Utils
// ------------------------------------------------------------------------------------------------------------------------
function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, calendar) {
for (var _i = 0, constraints_1 = constraints; _i < constraints_1.length; _i++) {
var constraint = constraints_1[_i];
if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar), subjectRange)) {
return false;
}
}
return true;
}
function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours
otherEventStore, // for if constraint is an even group ID
businessHoursUnexpanded, // for if constraint is 'businessHours'
calendar // for expanding businesshours
) {
if (constraint === 'businessHours') {
return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, calendar));
}
else if (typeof constraint === 'string') { // an group ID
return eventStoreToRanges(filterEventStoreDefs(otherEventStore, function (eventDef) {
return eventDef.groupId === constraint;
}));
}
else if (typeof constraint === 'object' && constraint) { // non-null object
return eventStoreToRanges(expandRecurring(constraint, subjectRange, calendar));
}
return []; // if it's false
}
// TODO: move to event-store file?
function eventStoreToRanges(eventStore) {
var instances = eventStore.instances;
var ranges = [];
for (var instanceId in instances) {
ranges.push(instances[instanceId].range);
}
return ranges;
}
// TODO: move to geom file?
function anyRangesContainRange(outerRanges, innerRange) {
for (var _i = 0, outerRanges_1 = outerRanges; _i < outerRanges_1.length; _i++) {
var outerRange = outerRanges_1[_i];
if (rangeContainsRange(outerRange, innerRange)) {
return true;
}
}
return false;
}
// Parsing
// ------------------------------------------------------------------------------------------------------------------------
function normalizeConstraint(input, calendar) {
if (Array.isArray(input)) {
return parseEvents(input, '', calendar, true); // allowOpenRange=true
}
else if (typeof input === 'object' && input) { // non-null object
return parseEvents([input], '', calendar, true); // allowOpenRange=true
}
else if (input != null) {
return String(input);
}
else {
return null;
}
}
function htmlEscape(s) {
return (s + '').replace(/&/g, '&')
.replace(//g, '>')
.replace(/'/g, ''')
.replace(/"/g, '"')
.replace(/\n/g, '
');
}
// Given a hash of CSS properties, returns a string of CSS.
// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
function cssToStr(cssProps) {
var statements = [];
for (var name_1 in cssProps) {
var val = cssProps[name_1];
if (val != null && val !== '') {
statements.push(name_1 + ':' + val);
}
}
return statements.join(';');
}
// Given an object hash of HTML attribute names to values,
// generates a string that can be injected between < > in HTML
function attrsToStr(attrs) {
var parts = [];
for (var name_2 in attrs) {
var val = attrs[name_2];
if (val != null) {
parts.push(name_2 + '="' + htmlEscape(val) + '"');
}
}
return parts.join(' ');
}
function parseClassName(raw) {
if (Array.isArray(raw)) {
return raw;
}
else if (typeof raw === 'string') {
return raw.split(/\s+/);
}
else {
return [];
}
}
var UNSCOPED_EVENT_UI_PROPS = {
editable: Boolean,
startEditable: Boolean,
durationEditable: Boolean,
constraint: null,
overlap: null,
allow: null,
className: parseClassName,
classNames: parseClassName,
color: String,
backgroundColor: String,
borderColor: String,
textColor: String
};
function processUnscopedUiProps(rawProps, calendar, leftovers) {
var props = refineProps(rawProps, UNSCOPED_EVENT_UI_PROPS, {}, leftovers);
var constraint = normalizeConstraint(props.constraint, calendar);
return {
startEditable: props.startEditable != null ? props.startEditable : props.editable,
durationEditable: props.durationEditable != null ? props.durationEditable : props.editable,
constraints: constraint != null ? [constraint] : [],
overlap: props.overlap,
allows: props.allow != null ? [props.allow] : [],
backgroundColor: props.backgroundColor || props.color,
borderColor: props.borderColor || props.color,
textColor: props.textColor,
classNames: props.classNames.concat(props.className)
};
}
function processScopedUiProps(prefix, rawScoped, calendar, leftovers) {
var rawUnscoped = {};
var wasFound = {};
for (var key in UNSCOPED_EVENT_UI_PROPS) {
var scopedKey = prefix + capitaliseFirstLetter(key);
rawUnscoped[key] = rawScoped[scopedKey];
wasFound[scopedKey] = true;
}
if (prefix === 'event') {
rawUnscoped.editable = rawScoped.editable; // special case. there is no 'eventEditable', just 'editable'
}
if (leftovers) {
for (var key in rawScoped) {
if (!wasFound[key]) {
leftovers[key] = rawScoped[key];
}
}
}
return processUnscopedUiProps(rawUnscoped, calendar);
}
var EMPTY_EVENT_UI = {
startEditable: null,
durationEditable: null,
constraints: [],
overlap: null,
allows: [],
backgroundColor: '',
borderColor: '',
textColor: '',
classNames: []
};
// prevent against problems with <2 args!
function combineEventUis(uis) {
return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI);
}
function combineTwoEventUis(item0, item1) {
return {
startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable,
durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable,
constraints: item0.constraints.concat(item1.constraints),
overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap,
allows: item0.allows.concat(item1.allows),
backgroundColor: item1.backgroundColor || item0.backgroundColor,
borderColor: item1.borderColor || item0.borderColor,
textColor: item1.textColor || item0.textColor,
classNames: item0.classNames.concat(item1.classNames)
};
}
var NON_DATE_PROPS = {
id: String,
groupId: String,
title: String,
url: String,
rendering: String,
extendedProps: null
};
var DATE_PROPS = {
start: null,
date: null,
end: null,
allDay: null
};
var uid = 0;
function parseEvent(raw, sourceId, calendar, allowOpenRange) {
var allDayDefault = computeIsAllDayDefault(sourceId, calendar);
var leftovers0 = {};
var recurringRes = parseRecurring(raw, // raw, but with single-event stuff stripped out
allDayDefault, calendar.dateEnv, calendar.pluginSystem.hooks.recurringTypes, leftovers0 // will populate with non-recurring props
);
if (recurringRes) {
var def = parseEventDef(leftovers0, sourceId, recurringRes.allDay, Boolean(recurringRes.duration), calendar);
def.recurringDef = {
typeId: recurringRes.typeId,
typeData: recurringRes.typeData,
duration: recurringRes.duration
};
return { def: def, instance: null };
}
else {
var leftovers1 = {};
var singleRes = parseSingle(raw, allDayDefault, calendar, leftovers1, allowOpenRange);
if (singleRes) {
var def = parseEventDef(leftovers1, sourceId, singleRes.allDay, singleRes.hasEnd, calendar);
var instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo);
return { def: def, instance: instance };
}
}
return null;
}
/*
Will NOT populate extendedProps with the leftover properties.
Will NOT populate date-related props.
The EventNonDateInput has been normalized (id => publicId, etc).
*/
function parseEventDef(raw, sourceId, allDay, hasEnd, calendar) {
var leftovers = {};
var def = pluckNonDateProps(raw, calendar, leftovers);
def.defId = String(uid++);
def.sourceId = sourceId;
def.allDay = allDay;
def.hasEnd = hasEnd;
for (var _i = 0, _a = calendar.pluginSystem.hooks.eventDefParsers; _i < _a.length; _i++) {
var eventDefParser = _a[_i];
var newLeftovers = {};
eventDefParser(def, leftovers, newLeftovers);
leftovers = newLeftovers;
}
def.extendedProps = __assign(leftovers, def.extendedProps || {});
// help out EventApi from having user modify props
Object.freeze(def.ui.classNames);
Object.freeze(def.extendedProps);
return def;
}
function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) {
return {
instanceId: String(uid++),
defId: defId,
range: range,
forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo,
forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo
};
}
function parseSingle(raw, allDayDefault, calendar, leftovers, allowOpenRange) {
var props = pluckDateProps(raw, leftovers);
var allDay = props.allDay;
var startMeta;
var startMarker = null;
var hasEnd = false;
var endMeta;
var endMarker = null;
startMeta = calendar.dateEnv.createMarkerMeta(props.start);
if (startMeta) {
startMarker = startMeta.marker;
}
else if (!allowOpenRange) {
return null;
}
if (props.end != null) {
endMeta = calendar.dateEnv.createMarkerMeta(props.end);
}
if (allDay == null) {
if (allDayDefault != null) {
allDay = allDayDefault;
}
else {
// fall back to the date props LAST
allDay = (!startMeta || startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
}
if (allDay && startMarker) {
startMarker = startOfDay(startMarker);
}
if (endMeta) {
endMarker = endMeta.marker;
if (allDay) {
endMarker = startOfDay(endMarker);
}
if (startMarker && endMarker <= startMarker) {
endMarker = null;
}
}
if (endMarker) {
hasEnd = true;
}
else if (!allowOpenRange) {
hasEnd = calendar.opt('forceEventDuration') || false;
endMarker = calendar.dateEnv.add(startMarker, allDay ?
calendar.defaultAllDayEventDuration :
calendar.defaultTimedEventDuration);
}
return {
allDay: allDay,
hasEnd: hasEnd,
range: { start: startMarker, end: endMarker },
forcedStartTzo: startMeta ? startMeta.forcedTzo : null,
forcedEndTzo: endMeta ? endMeta.forcedTzo : null
};
}
function pluckDateProps(raw, leftovers) {
var props = refineProps(raw, DATE_PROPS, {}, leftovers);
props.start = (props.start !== null) ? props.start : props.date;
delete props.date;
return props;
}
function pluckNonDateProps(raw, calendar, leftovers) {
var preLeftovers = {};
var props = refineProps(raw, NON_DATE_PROPS, {}, preLeftovers);
var ui = processUnscopedUiProps(preLeftovers, calendar, leftovers);
props.publicId = props.id;
delete props.id;
props.ui = ui;
return props;
}
function computeIsAllDayDefault(sourceId, calendar) {
var res = null;
if (sourceId) {
var source = calendar.state.eventSources[sourceId];
res = source.allDayDefault;
}
if (res == null) {
res = calendar.opt('allDayDefault');
}
return res;
}
var DEF_DEFAULTS = {
startTime: '09:00',
endTime: '17:00',
daysOfWeek: [1, 2, 3, 4, 5],
rendering: 'inverse-background',
classNames: 'fc-nonbusiness',
groupId: '_businessHours' // so multiple defs get grouped
};
/*
TODO: pass around as EventDefHash!!!
*/
function parseBusinessHours(input, calendar) {
return parseEvents(refineInputs(input), '', calendar);
}
function refineInputs(input) {
var rawDefs;
if (input === true) {
rawDefs = [{}]; // will get DEF_DEFAULTS verbatim
}
else if (Array.isArray(input)) {
// if specifying an array, every sub-definition NEEDS a day-of-week
rawDefs = input.filter(function (rawDef) {
return rawDef.daysOfWeek;
});
}
else if (typeof input === 'object' && input) { // non-null object
rawDefs = [input];
}
else { // is probably false
rawDefs = [];
}
rawDefs = rawDefs.map(function (rawDef) {
return __assign({}, DEF_DEFAULTS, rawDef);
});
return rawDefs;
}
function memoizeRendering(renderFunc, unrenderFunc, dependencies) {
if (dependencies === void 0) { dependencies = []; }
var dependents = [];
var thisContext;
var prevArgs;
function unrender() {
if (prevArgs) {
for (var _i = 0, dependents_1 = dependents; _i < dependents_1.length; _i++) {
var dependent = dependents_1[_i];
dependent.unrender();
}
if (unrenderFunc) {
unrenderFunc.apply(thisContext, prevArgs);
}
prevArgs = null;
}
}
function res() {
if (!prevArgs || !isArraysEqual(prevArgs, arguments)) {
unrender();
thisContext = this;
prevArgs = arguments;
renderFunc.apply(this, arguments);
}
}
res.dependents = dependents;
res.unrender = unrender;
for (var _i = 0, dependencies_1 = dependencies; _i < dependencies_1.length; _i++) {
var dependency = dependencies_1[_i];
dependency.dependents.push(res);
}
return res;
}
var EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere
var Splitter = /** @class */ (function () {
function Splitter() {
this.getKeysForEventDefs = memoize(this._getKeysForEventDefs);
this.splitDateSelection = memoize(this._splitDateSpan);
this.splitEventStore = memoize(this._splitEventStore);
this.splitIndividualUi = memoize(this._splitIndividualUi);
this.splitEventDrag = memoize(this._splitInteraction);
this.splitEventResize = memoize(this._splitInteraction);
this.eventUiBuilders = {}; // TODO: typescript protection
}
Splitter.prototype.splitProps = function (props) {
var _this = this;
var keyInfos = this.getKeyInfo(props);
var defKeys = this.getKeysForEventDefs(props.eventStore);
var dateSelections = this.splitDateSelection(props.dateSelection);
var individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases*
var eventStores = this.splitEventStore(props.eventStore, defKeys);
var eventDrags = this.splitEventDrag(props.eventDrag);
var eventResizes = this.splitEventResize(props.eventResize);
var splitProps = {};
this.eventUiBuilders = mapHash(keyInfos, function (info, key) {
return _this.eventUiBuilders[key] || memoize(buildEventUiForKey);
});
for (var key in keyInfos) {
var keyInfo = keyInfos[key];
var eventStore = eventStores[key] || EMPTY_EVENT_STORE;
var buildEventUi = this.eventUiBuilders[key];
splitProps[key] = {
businessHours: keyInfo.businessHours || props.businessHours,
dateSelection: dateSelections[key] || null,
eventStore: eventStore,
eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
eventDrag: eventDrags[key] || null,
eventResize: eventResizes[key] || null
};
}
return splitProps;
};
Splitter.prototype._splitDateSpan = function (dateSpan) {
var dateSpans = {};
if (dateSpan) {
var keys = this.getKeysForDateSpan(dateSpan);
for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
var key = keys_1[_i];
dateSpans[key] = dateSpan;
}
}
return dateSpans;
};
Splitter.prototype._getKeysForEventDefs = function (eventStore) {
var _this = this;
return mapHash(eventStore.defs, function (eventDef) {
return _this.getKeysForEventDef(eventDef);
});
};
Splitter.prototype._splitEventStore = function (eventStore, defKeys) {
var defs = eventStore.defs, instances = eventStore.instances;
var splitStores = {};
for (var defId in defs) {
for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
var key = _a[_i];
if (!splitStores[key]) {
splitStores[key] = createEmptyEventStore();
}
splitStores[key].defs[defId] = defs[defId];
}
}
for (var instanceId in instances) {
var instance = instances[instanceId];
for (var _b = 0, _c = defKeys[instance.defId]; _b < _c.length; _b++) {
var key = _c[_b];
if (splitStores[key]) { // must have already been created
splitStores[key].instances[instanceId] = instance;
}
}
}
return splitStores;
};
Splitter.prototype._splitIndividualUi = function (eventUiBases, defKeys) {
var splitHashes = {};
for (var defId in eventUiBases) {
if (defId) { // not the '' key
for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
var key = _a[_i];
if (!splitHashes[key]) {
splitHashes[key] = {};
}
splitHashes[key][defId] = eventUiBases[defId];
}
}
}
return splitHashes;
};
Splitter.prototype._splitInteraction = function (interaction) {
var splitStates = {};
if (interaction) {
var affectedStores_1 = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents) // can't use cached. might be events from other calendar
);
// can't rely on defKeys because event data is mutated
var mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents);
var mutatedStores_1 = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId);
var populate = function (key) {
if (!splitStates[key]) {
splitStates[key] = {
affectedEvents: affectedStores_1[key] || EMPTY_EVENT_STORE,
mutatedEvents: mutatedStores_1[key] || EMPTY_EVENT_STORE,
isEvent: interaction.isEvent,
origSeg: interaction.origSeg
};
}
};
for (var key in affectedStores_1) {
populate(key);
}
for (var key in mutatedStores_1) {
populate(key);
}
}
return splitStates;
};
return Splitter;
}());
function buildEventUiForKey(allUi, eventUiForKey, individualUi) {
var baseParts = [];
if (allUi) {
baseParts.push(allUi);
}
if (eventUiForKey) {
baseParts.push(eventUiForKey);
}
var stuff = {
'': combineEventUis(baseParts)
};
if (individualUi) {
__assign(stuff, individualUi);
}
return stuff;
}
// Generates HTML for an anchor to another view into the calendar.
// Will either generate an tag or a non-clickable tag, depending on enabled settings.
// `gotoOptions` can either be a DateMarker, or an object with the form:
// { date, type, forceOff }
// `type` is a view-type like "day" or "week". default value is "day".
// `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
function buildGotoAnchorHtml(component, gotoOptions, attrs, innerHtml) {
var dateEnv = component.dateEnv;
var date;
var type;
var forceOff;
var finalOptions;
if (gotoOptions instanceof Date) {
date = gotoOptions; // a single date-like input
}
else {
date = gotoOptions.date;
type = gotoOptions.type;
forceOff = gotoOptions.forceOff;
}
finalOptions = {
date: dateEnv.formatIso(date, { omitTime: true }),
type: type || 'day'
};
if (typeof attrs === 'string') {
innerHtml = attrs;
attrs = null;
}
attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
innerHtml = innerHtml || '';
if (!forceOff && component.opt('navLinks')) {
return '' +
innerHtml +
'';
}
else {
return '' +
innerHtml +
'';
}
}
function getAllDayHtml(component) {
return component.opt('allDayHtml') || htmlEscape(component.opt('allDayText'));
}
// Computes HTML classNames for a single-day element
function getDayClasses(date, dateProfile, context, noThemeHighlight) {
var calendar = context.calendar, view = context.view, theme = context.theme, dateEnv = context.dateEnv;
var classes = [];
var todayStart;
var todayEnd;
if (!rangeContainsMarker(dateProfile.activeRange, date)) {
classes.push('fc-disabled-day');
}
else {
classes.push('fc-' + DAY_IDS[date.getUTCDay()]);
if (view.opt('monthMode') &&
dateEnv.getMonth(date) !== dateEnv.getMonth(dateProfile.currentRange.start)) {
classes.push('fc-other-month');
}
todayStart = startOfDay(calendar.getNow());
todayEnd = addDays(todayStart, 1);
if (date < todayStart) {
classes.push('fc-past');
}
else if (date >= todayEnd) {
classes.push('fc-future');
}
else {
classes.push('fc-today');
if (noThemeHighlight !== true) {
classes.push(theme.getClass('today'));
}
}
}
return classes;
}
// given a function that resolves a result asynchronously.
// the function can either call passed-in success and failure callbacks,
// or it can return a promise.
// if you need to pass additional params to func, bind them first.
function unpromisify(func, success, failure) {
// guard against success/failure callbacks being called more than once
// and guard against a promise AND callback being used together.
var isResolved = false;
var wrappedSuccess = function () {
if (!isResolved) {
isResolved = true;
success.apply(this, arguments);
}
};
var wrappedFailure = function () {
if (!isResolved) {
isResolved = true;
if (failure) {
failure.apply(this, arguments);
}
}
};
var res = func(wrappedSuccess, wrappedFailure);
if (res && typeof res.then === 'function') {
res.then(wrappedSuccess, wrappedFailure);
}
}
var Mixin = /** @class */ (function () {
function Mixin() {
}
// mix into a CLASS
Mixin.mixInto = function (destClass) {
this.mixIntoObj(destClass.prototype);
};
// mix into ANY object
Mixin.mixIntoObj = function (destObj) {
var _this = this;
Object.getOwnPropertyNames(this.prototype).forEach(function (name) {
if (!destObj[name]) { // if destination doesn't already define it
destObj[name] = _this.prototype[name];
}
});
};
/*
will override existing methods
TODO: remove! not used anymore
*/
Mixin.mixOver = function (destClass) {
var _this = this;
Object.getOwnPropertyNames(this.prototype).forEach(function (name) {
destClass.prototype[name] = _this.prototype[name];
});
};
return Mixin;
}());
/*
USAGE:
import { default as EmitterMixin, EmitterInterface } from './EmitterMixin'
in class:
on: EmitterInterface['on']
one: EmitterInterface['one']
off: EmitterInterface['off']
trigger: EmitterInterface['trigger']
triggerWith: EmitterInterface['triggerWith']
hasHandlers: EmitterInterface['hasHandlers']
after class:
EmitterMixin.mixInto(TheClass)
*/
var EmitterMixin = /** @class */ (function (_super) {
__extends(EmitterMixin, _super);
function EmitterMixin() {
return _super !== null && _super.apply(this, arguments) || this;
}
EmitterMixin.prototype.on = function (type, handler) {
addToHash(this._handlers || (this._handlers = {}), type, handler);
return this; // for chaining
};
// todo: add comments
EmitterMixin.prototype.one = function (type, handler) {
addToHash(this._oneHandlers || (this._oneHandlers = {}), type, handler);
return this; // for chaining
};
EmitterMixin.prototype.off = function (type, handler) {
if (this._handlers) {
removeFromHash(this._handlers, type, handler);
}
if (this._oneHandlers) {
removeFromHash(this._oneHandlers, type, handler);
}
return this; // for chaining
};
EmitterMixin.prototype.trigger = function (type) {
var args = [];
for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i];
}
this.triggerWith(type, this, args);
return this; // for chaining
};
EmitterMixin.prototype.triggerWith = function (type, context, args) {
if (this._handlers) {
applyAll(this._handlers[type], context, args);
}
if (this._oneHandlers) {
applyAll(this._oneHandlers[type], context, args);
delete this._oneHandlers[type]; // will never fire again
}
return this; // for chaining
};
EmitterMixin.prototype.hasHandlers = function (type) {
return (this._handlers && this._handlers[type] && this._handlers[type].length) ||
(this._oneHandlers && this._oneHandlers[type] && this._oneHandlers[type].length);
};
return EmitterMixin;
}(Mixin));
function addToHash(hash, type, handler) {
(hash[type] || (hash[type] = []))
.push(handler);
}
function removeFromHash(hash, type, handler) {
if (handler) {
if (hash[type]) {
hash[type] = hash[type].filter(function (func) {
return func !== handler;
});
}
}
else {
delete hash[type]; // remove all handler funcs for this type
}
}
/*
Records offset information for a set of elements, relative to an origin element.
Can record the left/right OR the top/bottom OR both.
Provides methods for querying the cache by position.
*/
var PositionCache = /** @class */ (function () {
function PositionCache(originEl, els, isHorizontal, isVertical) {
this.originEl = originEl;
this.els = els;
this.isHorizontal = isHorizontal;
this.isVertical = isVertical;
}
// Queries the els for coordinates and stores them.
// Call this method before using and of the get* methods below.
PositionCache.prototype.build = function () {
var originEl = this.originEl;
var originClientRect = this.originClientRect =
originEl.getBoundingClientRect(); // relative to viewport top-left
if (this.isHorizontal) {
this.buildElHorizontals(originClientRect.left);
}
if (this.isVertical) {
this.buildElVerticals(originClientRect.top);
}
};
// Populates the left/right internal coordinate arrays
PositionCache.prototype.buildElHorizontals = function (originClientLeft) {
var lefts = [];
var rights = [];
for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
var el = _a[_i];
var rect = el.getBoundingClientRect();
lefts.push(rect.left - originClientLeft);
rights.push(rect.right - originClientLeft);
}
this.lefts = lefts;
this.rights = rights;
};
// Populates the top/bottom internal coordinate arrays
PositionCache.prototype.buildElVerticals = function (originClientTop) {
var tops = [];
var bottoms = [];
for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
var el = _a[_i];
var rect = el.getBoundingClientRect();
tops.push(rect.top - originClientTop);
bottoms.push(rect.bottom - originClientTop);
}
this.tops = tops;
this.bottoms = bottoms;
};
// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
// If no intersection is made, returns undefined.
PositionCache.prototype.leftToIndex = function (leftPosition) {
var lefts = this.lefts;
var rights = this.rights;
var len = lefts.length;
var i;
for (i = 0; i < len; i++) {
if (leftPosition >= lefts[i] && leftPosition < rights[i]) {
return i;
}
}
};
// Given a top offset (from document top), returns the index of the el that it vertically intersects.
// If no intersection is made, returns undefined.
PositionCache.prototype.topToIndex = function (topPosition) {
var tops = this.tops;
var bottoms = this.bottoms;
var len = tops.length;
var i;
for (i = 0; i < len; i++) {
if (topPosition >= tops[i] && topPosition < bottoms[i]) {
return i;
}
}
};
// Gets the width of the element at the given index
PositionCache.prototype.getWidth = function (leftIndex) {
return this.rights[leftIndex] - this.lefts[leftIndex];
};
// Gets the height of the element at the given index
PositionCache.prototype.getHeight = function (topIndex) {
return this.bottoms[topIndex] - this.tops[topIndex];
};
return PositionCache;
}());
/*
An object for getting/setting scroll-related information for an element.
Internally, this is done very differently for window versus DOM element,
so this object serves as a common interface.
*/
var ScrollController = /** @class */ (function () {
function ScrollController() {
}
ScrollController.prototype.getMaxScrollTop = function () {
return this.getScrollHeight() - this.getClientHeight();
};
ScrollController.prototype.getMaxScrollLeft = function () {
return this.getScrollWidth() - this.getClientWidth();
};
ScrollController.prototype.canScrollVertically = function () {
return this.getMaxScrollTop() > 0;
};
ScrollController.prototype.canScrollHorizontally = function () {
return this.getMaxScrollLeft() > 0;
};
ScrollController.prototype.canScrollUp = function () {
return this.getScrollTop() > 0;
};
ScrollController.prototype.canScrollDown = function () {
return this.getScrollTop() < this.getMaxScrollTop();
};
ScrollController.prototype.canScrollLeft = function () {
return this.getScrollLeft() > 0;
};
ScrollController.prototype.canScrollRight = function () {
return this.getScrollLeft() < this.getMaxScrollLeft();
};
return ScrollController;
}());
var ElementScrollController = /** @class */ (function (_super) {
__extends(ElementScrollController, _super);
function ElementScrollController(el) {
var _this = _super.call(this) || this;
_this.el = el;
return _this;
}
ElementScrollController.prototype.getScrollTop = function () {
return this.el.scrollTop;
};
ElementScrollController.prototype.getScrollLeft = function () {
return this.el.scrollLeft;
};
ElementScrollController.prototype.setScrollTop = function (top) {
this.el.scrollTop = top;
};
ElementScrollController.prototype.setScrollLeft = function (left) {
this.el.scrollLeft = left;
};
ElementScrollController.prototype.getScrollWidth = function () {
return this.el.scrollWidth;
};
ElementScrollController.prototype.getScrollHeight = function () {
return this.el.scrollHeight;
};
ElementScrollController.prototype.getClientHeight = function () {
return this.el.clientHeight;
};
ElementScrollController.prototype.getClientWidth = function () {
return this.el.clientWidth;
};
return ElementScrollController;
}(ScrollController));
var WindowScrollController = /** @class */ (function (_super) {
__extends(WindowScrollController, _super);
function WindowScrollController() {
return _super !== null && _super.apply(this, arguments) || this;
}
WindowScrollController.prototype.getScrollTop = function () {
return window.pageYOffset;
};
WindowScrollController.prototype.getScrollLeft = function () {
return window.pageXOffset;
};
WindowScrollController.prototype.setScrollTop = function (n) {
window.scroll(window.pageXOffset, n);
};
WindowScrollController.prototype.setScrollLeft = function (n) {
window.scroll(n, window.pageYOffset);
};
WindowScrollController.prototype.getScrollWidth = function () {
return document.documentElement.scrollWidth;
};
WindowScrollController.prototype.getScrollHeight = function () {
return document.documentElement.scrollHeight;
};
WindowScrollController.prototype.getClientHeight = function () {
return document.documentElement.clientHeight;
};
WindowScrollController.prototype.getClientWidth = function () {
return document.documentElement.clientWidth;
};
return WindowScrollController;
}(ScrollController));
/*
Embodies a div that has potential scrollbars
*/
var ScrollComponent = /** @class */ (function (_super) {
__extends(ScrollComponent, _super);
function ScrollComponent(overflowX, overflowY) {
var _this = _super.call(this, createElement('div', {
className: 'fc-scroller'
})) || this;
_this.overflowX = overflowX;
_this.overflowY = overflowY;
_this.applyOverflow();
return _this;
}
// sets to natural height, unlocks overflow
ScrollComponent.prototype.clear = function () {
this.setHeight('auto');
this.applyOverflow();
};
ScrollComponent.prototype.destroy = function () {
removeElement(this.el);
};
// Overflow
// -----------------------------------------------------------------------------------------------------------------
ScrollComponent.prototype.applyOverflow = function () {
applyStyle(this.el, {
overflowX: this.overflowX,
overflowY: this.overflowY
});
};
// Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'.
// Useful for preserving scrollbar widths regardless of future resizes.
// Can pass in scrollbarWidths for optimization.
ScrollComponent.prototype.lockOverflow = function (scrollbarWidths) {
var overflowX = this.overflowX;
var overflowY = this.overflowY;
scrollbarWidths = scrollbarWidths || this.getScrollbarWidths();
if (overflowX === 'auto') {
overflowX = (scrollbarWidths.bottom || // horizontal scrollbars?
this.canScrollHorizontally() // OR scrolling pane with massless scrollbars?
) ? 'scroll' : 'hidden';
}
if (overflowY === 'auto') {
overflowY = (scrollbarWidths.left || scrollbarWidths.right || // horizontal scrollbars?
this.canScrollVertically() // OR scrolling pane with massless scrollbars?
) ? 'scroll' : 'hidden';
}
applyStyle(this.el, { overflowX: overflowX, overflowY: overflowY });
};
ScrollComponent.prototype.setHeight = function (height) {
applyStyleProp(this.el, 'height', height);
};
ScrollComponent.prototype.getScrollbarWidths = function () {
var edges = computeEdges(this.el);
return {
left: edges.scrollbarLeft,
right: edges.scrollbarRight,
bottom: edges.scrollbarBottom
};
};
return ScrollComponent;
}(ElementScrollController));
var Theme = /** @class */ (function () {
function Theme(calendarOptions) {
this.calendarOptions = calendarOptions;
this.processIconOverride();
}
Theme.prototype.processIconOverride = function () {
if (this.iconOverrideOption) {
this.setIconOverride(this.calendarOptions[this.iconOverrideOption]);
}
};
Theme.prototype.setIconOverride = function (iconOverrideHash) {
var iconClassesCopy;
var buttonName;
if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object
iconClassesCopy = __assign({}, this.iconClasses);
for (buttonName in iconOverrideHash) {
iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
}
this.iconClasses = iconClassesCopy;
}
else if (iconOverrideHash === false) {
this.iconClasses = {};
}
};
Theme.prototype.applyIconOverridePrefix = function (className) {
var prefix = this.iconOverridePrefix;
if (prefix && className.indexOf(prefix) !== 0) { // if not already present
className = prefix + className;
}
return className;
};
Theme.prototype.getClass = function (key) {
return this.classes[key] || '';
};
Theme.prototype.getIconClass = function (buttonName) {
var className = this.iconClasses[buttonName];
if (className) {
return this.baseIconClass + ' ' + className;
}
return '';
};
Theme.prototype.getCustomButtonIconClass = function (customButtonProps) {
var className;
if (this.iconOverrideCustomButtonOption) {
className = customButtonProps[this.iconOverrideCustomButtonOption];
if (className) {
return this.baseIconClass + ' ' + this.applyIconOverridePrefix(className);
}
}
return '';
};
return Theme;
}());
Theme.prototype.classes = {};
Theme.prototype.iconClasses = {};
Theme.prototype.baseIconClass = '';
Theme.prototype.iconOverridePrefix = '';
var guid = 0;
var Component = /** @class */ (function () {
function Component(context, isView) {
// HACK to populate view at top of component instantiation call chain
if (isView) {
context.view = this;
}
this.uid = String(guid++);
this.context = context;
this.dateEnv = context.dateEnv;
this.theme = context.theme;
this.view = context.view;
this.calendar = context.calendar;
this.isRtl = this.opt('dir') === 'rtl';
}
Component.addEqualityFuncs = function (newFuncs) {
this.prototype.equalityFuncs = __assign({}, this.prototype.equalityFuncs, newFuncs);
};
Component.prototype.opt = function (name) {
return this.context.options[name];
};
Component.prototype.receiveProps = function (props) {
var _a = recycleProps(this.props || {}, props, this.equalityFuncs), anyChanges = _a.anyChanges, comboProps = _a.comboProps;
this.props = comboProps;
if (anyChanges) {
this.render(comboProps);
}
};
Component.prototype.render = function (props) {
};
// after destroy is called, this component won't ever be used again
Component.prototype.destroy = function () {
};
return Component;
}());
Component.prototype.equalityFuncs = {};
/*
Reuses old values when equal. If anything is unequal, returns newProps as-is.
Great for PureComponent, but won't be feasible with React, so just eliminate and use React's DOM diffing.
*/
function recycleProps(oldProps, newProps, equalityFuncs) {
var comboProps = {}; // some old, some new
var anyChanges = false;
for (var key in newProps) {
if (key in oldProps && (oldProps[key] === newProps[key] ||
(equalityFuncs[key] && equalityFuncs[key](oldProps[key], newProps[key])))) {
// equal to old? use old prop
comboProps[key] = oldProps[key];
}
else {
comboProps[key] = newProps[key];
anyChanges = true;
}
}
for (var key in oldProps) {
if (!(key in newProps)) {
anyChanges = true;
break;
}
}
return { anyChanges: anyChanges, comboProps: comboProps };
}
/*
PURPOSES:
- hook up to fg, fill, and mirror renderers
- interface for dragging and hits
*/
var DateComponent = /** @class */ (function (_super) {
__extends(DateComponent, _super);
function DateComponent(context, el, isView) {
var _this = _super.call(this, context, isView) || this;
_this.el = el;
return _this;
}
DateComponent.prototype.destroy = function () {
_super.prototype.destroy.call(this);
removeElement(this.el);
};
// TODO: WHAT ABOUT (sourceSeg && sourceSeg.component.doesDragMirror)
//
// Event Drag-n-Drop Rendering (for both events and external elements)
// ---------------------------------------------------------------------------------------------------------------
/*
renderEventDragSegs(state: EventSegUiInteractionState) {
if (state) {
let { isEvent, segs, sourceSeg } = state
if (this.eventRenderer) {
this.eventRenderer.hideByHash(state.affectedInstances)
}
// if the user is dragging something that is considered an event with real event data,
// and this component likes to do drag mirrors OR the component where the seg came from
// likes to do drag mirrors, then render a drag mirror.
if (isEvent && (this.doesDragMirror || sourceSeg && sourceSeg.component.doesDragMirror)) {
if (this.mirrorRenderer) {
this.mirrorRenderer.renderSegs(segs, { isDragging: true, sourceSeg })
}
}
// if it would be impossible to render a drag mirror OR this component likes to render
// highlights, then render a highlight.
if (!isEvent || this.doesDragHighlight) {
if (this.fillRenderer) {
this.fillRenderer.renderSegs('highlight', segs)
}
}
}
}
*/
// Hit System
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.buildPositionCaches = function () {
};
DateComponent.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) {
return null; // this should be abstract
};
// Validation
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.isInteractionValid = function (interaction) {
var calendar = this.calendar;
var dateProfile = this.props.dateProfile; // HACK
var instances = interaction.mutatedEvents.instances;
if (dateProfile) { // HACK for DayTile
for (var instanceId in instances) {
if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
return false;
}
}
}
return isInteractionValid(interaction, calendar);
};
DateComponent.prototype.isDateSelectionValid = function (selection) {
var dateProfile = this.props.dateProfile; // HACK
if (dateProfile && // HACK for DayTile
!rangeContainsRange(dateProfile.validRange, selection.range)) {
return false;
}
return isDateSelectionValid(selection, this.calendar);
};
// Triggering
// -----------------------------------------------------------------------------------------------------------------
// TODO: move to Calendar
DateComponent.prototype.publiclyTrigger = function (name, args) {
var calendar = this.calendar;
return calendar.publiclyTrigger(name, args);
};
DateComponent.prototype.publiclyTriggerAfterSizing = function (name, args) {
var calendar = this.calendar;
return calendar.publiclyTriggerAfterSizing(name, args);
};
DateComponent.prototype.hasPublicHandlers = function (name) {
var calendar = this.calendar;
return calendar.hasPublicHandlers(name);
};
DateComponent.prototype.triggerRenderedSegs = function (segs, isMirrors) {
var calendar = this.calendar;
if (this.hasPublicHandlers('eventPositioned')) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
this.publiclyTriggerAfterSizing('eventPositioned', [
{
event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirrors,
isStart: seg.isStart,
isEnd: seg.isEnd,
el: seg.el,
view: this // safe to cast because this method is only called on context.view
}
]);
}
}
if (!calendar.state.loadingLevel) { // avoid initial empty state while pending
calendar.afterSizingTriggers._eventsPositioned = [null]; // fire once
}
};
DateComponent.prototype.triggerWillRemoveSegs = function (segs, isMirrors) {
var calendar = this.calendar;
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
calendar.trigger('eventElRemove', seg.el);
}
if (this.hasPublicHandlers('eventDestroy')) {
for (var _a = 0, segs_3 = segs; _a < segs_3.length; _a++) {
var seg = segs_3[_a];
this.publiclyTrigger('eventDestroy', [
{
event: new EventApi(calendar, seg.eventRange.def, seg.eventRange.instance),
isMirror: isMirrors,
el: seg.el,
view: this // safe to cast because this method is only called on context.view
}
]);
}
}
};
// Pointer Interaction Utils
// -----------------------------------------------------------------------------------------------------------------
DateComponent.prototype.isValidSegDownEl = function (el) {
return !this.props.eventDrag && // HACK
!this.props.eventResize && // HACK
!elementClosest(el, '.fc-mirror') &&
(this.isPopover() || !this.isInPopover(el));
// ^above line ensures we don't detect a seg interaction within a nested component.
// it's a HACK because it only supports a popover as the nested component.
};
DateComponent.prototype.isValidDateDownEl = function (el) {
var segEl = elementClosest(el, this.fgSegSelector);
return (!segEl || segEl.classList.contains('fc-mirror')) &&
!elementClosest(el, '.fc-more') && // a "more.." link
!elementClosest(el, 'a[data-goto]') && // a clickable nav link
!this.isInPopover(el);
};
DateComponent.prototype.isPopover = function () {
return this.el.classList.contains('fc-popover');
};
DateComponent.prototype.isInPopover = function (el) {
return Boolean(elementClosest(el, '.fc-popover'));
};
return DateComponent;
}(Component));
DateComponent.prototype.fgSegSelector = '.fc-event-container > *';
DateComponent.prototype.bgSegSelector = '.fc-bgevent:not(.fc-nonbusiness)';
var uid$1 = 0;
function createPlugin(input) {
return {
id: String(uid$1++),
deps: input.deps || [],
reducers: input.reducers || [],
eventDefParsers: input.eventDefParsers || [],
isDraggableTransformers: input.isDraggableTransformers || [],
eventDragMutationMassagers: input.eventDragMutationMassagers || [],
eventDefMutationAppliers: input.eventDefMutationAppliers || [],
dateSelectionTransformers: input.dateSelectionTransformers || [],
datePointTransforms: input.datePointTransforms || [],
dateSpanTransforms: input.dateSpanTransforms || [],
views: input.views || {},
viewPropsTransformers: input.viewPropsTransformers || [],
isPropsValid: input.isPropsValid || null,
externalDefTransforms: input.externalDefTransforms || [],
eventResizeJoinTransforms: input.eventResizeJoinTransforms || [],
viewContainerModifiers: input.viewContainerModifiers || [],
eventDropTransformers: input.eventDropTransformers || [],
componentInteractions: input.componentInteractions || [],
calendarInteractions: input.calendarInteractions || [],
themeClasses: input.themeClasses || {},
eventSourceDefs: input.eventSourceDefs || [],
cmdFormatter: input.cmdFormatter,
recurringTypes: input.recurringTypes || [],
namedTimeZonedImpl: input.namedTimeZonedImpl,
defaultView: input.defaultView || '',
elementDraggingImpl: input.elementDraggingImpl,
optionChangeHandlers: input.optionChangeHandlers || {}
};
}
var PluginSystem = /** @class */ (function () {
function PluginSystem() {
this.hooks = {
reducers: [],
eventDefParsers: [],
isDraggableTransformers: [],
eventDragMutationMassagers: [],
eventDefMutationAppliers: [],
dateSelectionTransformers: [],
datePointTransforms: [],
dateSpanTransforms: [],
views: {},
viewPropsTransformers: [],
isPropsValid: null,
externalDefTransforms: [],
eventResizeJoinTransforms: [],
viewContainerModifiers: [],
eventDropTransformers: [],
componentInteractions: [],
calendarInteractions: [],
themeClasses: {},
eventSourceDefs: [],
cmdFormatter: null,
recurringTypes: [],
namedTimeZonedImpl: null,
defaultView: '',
elementDraggingImpl: null,
optionChangeHandlers: {}
};
this.addedHash = {};
}
PluginSystem.prototype.add = function (plugin) {
if (!this.addedHash[plugin.id]) {
this.addedHash[plugin.id] = true;
for (var _i = 0, _a = plugin.deps; _i < _a.length; _i++) {
var dep = _a[_i];
this.add(dep);
}
this.hooks = combineHooks(this.hooks, plugin);
}
};
return PluginSystem;
}());
function combineHooks(hooks0, hooks1) {
return {
reducers: hooks0.reducers.concat(hooks1.reducers),
eventDefParsers: hooks0.eventDefParsers.concat(hooks1.eventDefParsers),
isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
views: __assign({}, hooks0.views, hooks1.views),
viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
eventResizeJoinTransforms: hooks0.eventResizeJoinTransforms.concat(hooks1.eventResizeJoinTransforms),
viewContainerModifiers: hooks0.viewContainerModifiers.concat(hooks1.viewContainerModifiers),
eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
themeClasses: __assign({}, hooks0.themeClasses, hooks1.themeClasses),
eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
defaultView: hooks0.defaultView || hooks1.defaultView,
elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
optionChangeHandlers: __assign({}, hooks0.optionChangeHandlers, hooks1.optionChangeHandlers)
};
}
var eventSourceDef = {
ignoreRange: true,
parseMeta: function (raw) {
if (Array.isArray(raw)) { // short form
return raw;
}
else if (Array.isArray(raw.events)) {
return raw.events;
}
return null;
},
fetch: function (arg, success) {
success({
rawEvents: arg.eventSource.meta
});
}
};
var ArrayEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef]
});
var eventSourceDef$1 = {
parseMeta: function (raw) {
if (typeof raw === 'function') { // short form
return raw;
}
else if (typeof raw.events === 'function') {
return raw.events;
}
return null;
},
fetch: function (arg, success, failure) {
var dateEnv = arg.calendar.dateEnv;
var func = arg.eventSource.meta;
unpromisify(func.bind(null, {
start: dateEnv.toDate(arg.range.start),
end: dateEnv.toDate(arg.range.end),
startStr: dateEnv.formatIso(arg.range.start),
endStr: dateEnv.formatIso(arg.range.end),
timeZone: dateEnv.timeZone
}), function (rawEvents) {
success({ rawEvents: rawEvents }); // needs an object response
}, failure // send errorObj directly to failure callback
);
}
};
var FuncEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef$1]
});
function requestJson(method, url, params, successCallback, failureCallback) {
method = method.toUpperCase();
var body = null;
if (method === 'GET') {
url = injectQueryStringParams(url, params);
}
else {
body = encodeParams(params);
}
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (method !== 'GET') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 400) {
try {
var res = JSON.parse(xhr.responseText);
successCallback(res, xhr);
}
catch (err) {
failureCallback('Failure parsing JSON', xhr);
}
}
else {
failureCallback('Request failed', xhr);
}
};
xhr.onerror = function () {
failureCallback('Request failed', xhr);
};
xhr.send(body);
}
function injectQueryStringParams(url, params) {
return url +
(url.indexOf('?') === -1 ? '?' : '&') +
encodeParams(params);
}
function encodeParams(params) {
var parts = [];
for (var key in params) {
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
}
return parts.join('&');
}
var eventSourceDef$2 = {
parseMeta: function (raw) {
if (typeof raw === 'string') { // short form
raw = { url: raw };
}
else if (!raw || typeof raw !== 'object' || !raw.url) {
return null;
}
return {
url: raw.url,
method: (raw.method || 'GET').toUpperCase(),
extraParams: raw.extraParams,
startParam: raw.startParam,
endParam: raw.endParam,
timeZoneParam: raw.timeZoneParam
};
},
fetch: function (arg, success, failure) {
var meta = arg.eventSource.meta;
var requestParams = buildRequestParams(meta, arg.range, arg.calendar);
requestJson(meta.method, meta.url, requestParams, function (rawEvents, xhr) {
success({ rawEvents: rawEvents, xhr: xhr });
}, function (errorMessage, xhr) {
failure({ message: errorMessage, xhr: xhr });
});
}
};
var JsonFeedEventSourcePlugin = createPlugin({
eventSourceDefs: [eventSourceDef$2]
});
function buildRequestParams(meta, range, calendar) {
var dateEnv = calendar.dateEnv;
var startParam;
var endParam;
var timeZoneParam;
var customRequestParams;
var params = {};
startParam = meta.startParam;
if (startParam == null) {
startParam = calendar.opt('startParam');
}
endParam = meta.endParam;
if (endParam == null) {
endParam = calendar.opt('endParam');
}
timeZoneParam = meta.timeZoneParam;
if (timeZoneParam == null) {
timeZoneParam = calendar.opt('timeZoneParam');
}
// retrieve any outbound GET/POST data from the options
if (typeof meta.extraParams === 'function') {
// supplied as a function that returns a key/value object
customRequestParams = meta.extraParams();
}
else {
// probably supplied as a straight key/value object
customRequestParams = meta.extraParams || {};
}
__assign(params, customRequestParams);
params[startParam] = dateEnv.formatIso(range.start);
params[endParam] = dateEnv.formatIso(range.end);
if (dateEnv.timeZone !== 'local') {
params[timeZoneParam] = dateEnv.timeZone;
}
return params;
}
var recurring = {
parse: function (rawEvent, leftoverProps, dateEnv) {
var createMarker = dateEnv.createMarker.bind(dateEnv);
var processors = {
daysOfWeek: null,
startTime: createDuration,
endTime: createDuration,
startRecur: createMarker,
endRecur: createMarker
};
var props = refineProps(rawEvent, processors, {}, leftoverProps);
var anyValid = false;
for (var propName in props) {
if (props[propName] != null) {
anyValid = true;
break;
}
}
if (anyValid) {
var duration = null;
if ('duration' in leftoverProps) {
duration = createDuration(leftoverProps.duration);
delete leftoverProps.duration;
}
if (!duration && props.startTime && props.endTime) {
duration = subtractDurations(props.endTime, props.startTime);
}
return {
allDayGuess: Boolean(!props.startTime && !props.endTime),
duration: duration,
typeData: props // doesn't need endTime anymore but oh well
};
}
return null;
},
expand: function (typeData, framingRange, dateEnv) {
var clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
if (clippedFramingRange) {
return expandRanges(typeData.daysOfWeek, typeData.startTime, clippedFramingRange, dateEnv);
}
else {
return [];
}
}
};
var SimpleRecurrencePlugin = createPlugin({
recurringTypes: [recurring]
});
function expandRanges(daysOfWeek, startTime, framingRange, dateEnv) {
var dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null;
var dayMarker = startOfDay(framingRange.start);
var endMarker = framingRange.end;
var instanceStarts = [];
while (dayMarker < endMarker) {
var instanceStart
// if everyday, or this particular day-of-week
= void 0;
// if everyday, or this particular day-of-week
if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
if (startTime) {
instanceStart = dateEnv.add(dayMarker, startTime);
}
else {
instanceStart = dayMarker;
}
instanceStarts.push(instanceStart);
}
dayMarker = addDays(dayMarker, 1);
}
return instanceStarts;
}
var DefaultOptionChangeHandlers = createPlugin({
optionChangeHandlers: {
events: function (events, calendar, deepEqual) {
handleEventSources([events], calendar, deepEqual);
},
eventSources: handleEventSources,
plugins: handlePlugins
}
});
function handleEventSources(inputs, calendar, deepEqual) {
var unfoundSources = hashValuesToArray(calendar.state.eventSources);
var newInputs = [];
for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) {
var input = inputs_1[_i];
var inputFound = false;
for (var i = 0; i < unfoundSources.length; i++) {
if (deepEqual(unfoundSources[i]._raw, input)) {
unfoundSources.splice(i, 1); // delete
inputFound = true;
break;
}
}
if (!inputFound) {
newInputs.push(input);
}
}
for (var _a = 0, unfoundSources_1 = unfoundSources; _a < unfoundSources_1.length; _a++) {
var unfoundSource = unfoundSources_1[_a];
calendar.dispatch({
type: 'REMOVE_EVENT_SOURCE',
sourceId: unfoundSource.sourceId
});
}
for (var _b = 0, newInputs_1 = newInputs; _b < newInputs_1.length; _b++) {
var newInput = newInputs_1[_b];
calendar.addEventSource(newInput);
}
}
// shortcoming: won't remove plugins
function handlePlugins(inputs, calendar) {
calendar.addPluginInputs(inputs); // will gracefully handle duplicates
}
var config = {}; // TODO: make these options
var globalDefaults = {
defaultRangeSeparator: ' - ',
titleRangeSeparator: ' \u2013 ',
defaultTimedEventDuration: '01:00:00',
defaultAllDayEventDuration: { day: 1 },
forceEventDuration: false,
nextDayThreshold: '00:00:00',
// display
columnHeader: true,
defaultView: '',
aspectRatio: 1.35,
header: {
left: 'title',
center: '',
right: 'today prev,next'
},
weekends: true,
weekNumbers: false,
weekNumberCalculation: 'local',
editable: false,
// nowIndicator: false,
scrollTime: '06:00:00',
minTime: '00:00:00',
maxTime: '24:00:00',
showNonCurrentDates: true,
// event ajax
lazyFetching: true,
startParam: 'start',
endParam: 'end',
timeZoneParam: 'timeZone',
timeZone: 'local',
// allDayDefault: undefined,
// locale
locales: [],
locale: '',
// dir: will get this from the default locale
// buttonIcons: null,
// allows setting a min-height to the event segment to prevent short events overlapping each other
timeGridEventMinHeight: 0,
themeSystem: 'standard',
// eventResizableFromStart: false,
dragRevertDuration: 500,
dragScroll: true,
allDayMaintainDuration: false,
// selectable: false,
unselectAuto: true,
// selectMinDistance: 0,
dropAccept: '*',
eventOrder: 'start,-duration,allDay,title',
// ^ if start tie, longer events go before shorter. final tie-breaker is title text
// rerenderDelay: null,
eventLimit: false,
eventLimitClick: 'popover',
dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
handleWindowResize: true,
windowResizeDelay: 100,
longPressDelay: 1000,
eventDragMinDistance: 5 // only applies to mouse
};
var rtlDefaults = {
header: {
left: 'next,prev today',
center: '',
right: 'title'
},
buttonIcons: {
// TODO: make RTL support the responibility of the theme
prev: 'fc-icon-chevron-right',
next: 'fc-icon-chevron-left',
prevYear: 'fc-icon-chevrons-right',
nextYear: 'fc-icon-chevrons-left'
}
};
var complexOptions = [
'header',
'footer',
'buttonText',
'buttonIcons'
];
// Merges an array of option objects into a single object
function mergeOptions(optionObjs) {
return mergeProps(optionObjs, complexOptions);
}
// TODO: move this stuff to a "plugin"-related file...
var INTERNAL_PLUGINS = [
ArrayEventSourcePlugin,
FuncEventSourcePlugin,
JsonFeedEventSourcePlugin,
SimpleRecurrencePlugin,
DefaultOptionChangeHandlers
];
function refinePluginDefs(pluginInputs) {
var plugins = [];
for (var _i = 0, pluginInputs_1 = pluginInputs; _i < pluginInputs_1.length; _i++) {
var pluginInput = pluginInputs_1[_i];
if (typeof pluginInput === 'string') {
var globalName = 'FullCalendar' + capitaliseFirstLetter(pluginInput);
if (!window[globalName]) {
console.warn('Plugin file not loaded for ' + pluginInput);
}
else {
plugins.push(window[globalName].default); // is an ES6 module
}
}
else {
plugins.push(pluginInput);
}
}
return INTERNAL_PLUGINS.concat(plugins);
}
var RAW_EN_LOCALE = {
code: 'en',
week: {
dow: 0,
doy: 4 // 4 days need to be within the year to be considered the first week
},
dir: 'ltr',
buttonText: {
prev: 'prev',
next: 'next',
prevYear: 'prev year',
nextYear: 'next year',
year: 'year',
today: 'today',
month: 'month',
week: 'week',
day: 'day',
list: 'list'
},
weekLabel: 'W',
allDayText: 'all-day',
eventLimitText: 'more',
noEventsMessage: 'No events to display'
};
function parseRawLocales(explicitRawLocales) {
var defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en';
var globalArray = window['FullCalendarLocalesAll'] || []; // from locales-all.js
var globalObject = window['FullCalendarLocales'] || {}; // from locales/*.js. keys are meaningless
var allRawLocales = globalArray.concat(// globalArray is low prio
hashValuesToArray(globalObject), // medium prio
explicitRawLocales // highest prio
);
var rawLocaleMap = {
en: RAW_EN_LOCALE // necessary?
};
for (var _i = 0, allRawLocales_1 = allRawLocales; _i < allRawLocales_1.length; _i++) {
var rawLocale = allRawLocales_1[_i];
rawLocaleMap[rawLocale.code] = rawLocale;
}
return {
map: rawLocaleMap,
defaultCode: defaultCode
};
}
function buildLocale(inputSingular, available) {
if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
}
else {
return queryLocale(inputSingular, available);
}
}
function queryLocale(codeArg, available) {
var codes = [].concat(codeArg || []); // will convert to array
var raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
return parseLocale(codeArg, codes, raw);
}
function queryRawLocale(codes, available) {
for (var i = 0; i < codes.length; i++) {
var parts = codes[i].toLocaleLowerCase().split('-');
for (var j = parts.length; j > 0; j--) {
var simpleId = parts.slice(0, j).join('-');
if (available[simpleId]) {
return available[simpleId];
}
}
}
return null;
}
function parseLocale(codeArg, codes, raw) {
var merged = mergeProps([RAW_EN_LOCALE, raw], ['buttonText']);
delete merged.code; // don't want this part of the options
var week = merged.week;
delete merged.week;
return {
codeArg: codeArg,
codes: codes,
week: week,
simpleNumberFormat: new Intl.NumberFormat(codeArg),
options: merged
};
}
var OptionsManager = /** @class */ (function () {
function OptionsManager(overrides) {
this.overrides = __assign({}, overrides); // make a copy
this.dynamicOverrides = {};
this.compute();
}
OptionsManager.prototype.mutate = function (updates, removals, isDynamic) {
var overrideHash = isDynamic ? this.dynamicOverrides : this.overrides;
__assign(overrideHash, updates);
for (var _i = 0, removals_1 = removals; _i < removals_1.length; _i++) {
var propName = removals_1[_i];
delete overrideHash[propName];
}
this.compute();
};
// Computes the flattened options hash for the calendar and assigns to `this.options`.
// Assumes this.overrides and this.dynamicOverrides have already been initialized.
OptionsManager.prototype.compute = function () {
// TODO: not a very efficient system
var locales = firstDefined(// explicit locale option given?
this.dynamicOverrides.locales, this.overrides.locales, globalDefaults.locales);
var locale = firstDefined(// explicit locales option given?
this.dynamicOverrides.locale, this.overrides.locale, globalDefaults.locale);
var available = parseRawLocales(locales);
var localeDefaults = buildLocale(locale || available.defaultCode, available.map).options;
var dir = firstDefined(// based on options computed so far, is direction RTL?
this.dynamicOverrides.dir, this.overrides.dir, localeDefaults.dir);
var dirDefaults = dir === 'rtl' ? rtlDefaults : {};
this.dirDefaults = dirDefaults;
this.localeDefaults = localeDefaults;
this.computed = mergeOptions([
globalDefaults,
dirDefaults,
localeDefaults,
this.overrides,
this.dynamicOverrides
]);
};
return OptionsManager;
}());
var calendarSystemClassMap = {};
function registerCalendarSystem(name, theClass) {
calendarSystemClassMap[name] = theClass;
}
function createCalendarSystem(name) {
return new calendarSystemClassMap[name]();
}
var GregorianCalendarSystem = /** @class */ (function () {
function GregorianCalendarSystem() {
}
GregorianCalendarSystem.prototype.getMarkerYear = function (d) {
return d.getUTCFullYear();
};
GregorianCalendarSystem.prototype.getMarkerMonth = function (d) {
return d.getUTCMonth();
};
GregorianCalendarSystem.prototype.getMarkerDay = function (d) {
return d.getUTCDate();
};
GregorianCalendarSystem.prototype.arrayToMarker = function (arr) {
return arrayToUtcDate(arr);
};
GregorianCalendarSystem.prototype.markerToArray = function (marker) {
return dateToUtcArray(marker);
};
return GregorianCalendarSystem;
}());
registerCalendarSystem('gregory', GregorianCalendarSystem);
var ISO_RE = /^\s*(\d{4})(-(\d{2})(-(\d{2})([T ](\d{2}):(\d{2})(:(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/;
function parse(str) {
var m = ISO_RE.exec(str);
if (m) {
var marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number('0.' + m[12]) * 1000 : 0));
if (isValidDate(marker)) {
var timeZoneOffset = null;
if (m[13]) {
timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 +
Number(m[18] || 0));
}
return {
marker: marker,
isTimeUnspecified: !m[6],
timeZoneOffset: timeZoneOffset
};
}
}
return null;
}
var DateEnv = /** @class */ (function () {
function DateEnv(settings) {
var timeZone = this.timeZone = settings.timeZone;
var isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC';
if (settings.namedTimeZoneImpl && isNamedTimeZone) {
this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone);
}
this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl);
this.calendarSystem = createCalendarSystem(settings.calendarSystem);
this.locale = settings.locale;
this.weekDow = settings.locale.week.dow;
this.weekDoy = settings.locale.week.doy;
if (settings.weekNumberCalculation === 'ISO') {
this.weekDow = 1;
this.weekDoy = 4;
}
if (typeof settings.firstDay === 'number') {
this.weekDow = settings.firstDay;
}
if (typeof settings.weekNumberCalculation === 'function') {
this.weekNumberFunc = settings.weekNumberCalculation;
}
this.weekLabel = settings.weekLabel != null ? settings.weekLabel : settings.locale.options.weekLabel;
this.cmdFormatter = settings.cmdFormatter;
}
// Creating / Parsing
DateEnv.prototype.createMarker = function (input) {
var meta = this.createMarkerMeta(input);
if (meta === null) {
return null;
}
return meta.marker;
};
DateEnv.prototype.createNowMarker = function () {
if (this.canComputeOffset) {
return this.timestampToMarker(new Date().valueOf());
}
else {
// if we can't compute the current date val for a timezone,
// better to give the current local date vals than UTC
return arrayToUtcDate(dateToLocalArray(new Date()));
}
};
DateEnv.prototype.createMarkerMeta = function (input) {
if (typeof input === 'string') {
return this.parse(input);
}
var marker = null;
if (typeof input === 'number') {
marker = this.timestampToMarker(input);
}
else if (input instanceof Date) {
input = input.valueOf();
if (!isNaN(input)) {
marker = this.timestampToMarker(input);
}
}
else if (Array.isArray(input)) {
marker = arrayToUtcDate(input);
}
if (marker === null || !isValidDate(marker)) {
return null;
}
return { marker: marker, isTimeUnspecified: false, forcedTzo: null };
};
DateEnv.prototype.parse = function (s) {
var parts = parse(s);
if (parts === null) {
return null;
}
var marker = parts.marker;
var forcedTzo = null;
if (parts.timeZoneOffset !== null) {
if (this.canComputeOffset) {
marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000);
}
else {
forcedTzo = parts.timeZoneOffset;
}
}
return { marker: marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo: forcedTzo };
};
// Accessors
DateEnv.prototype.getYear = function (marker) {
return this.calendarSystem.getMarkerYear(marker);
};
DateEnv.prototype.getMonth = function (marker) {
return this.calendarSystem.getMarkerMonth(marker);
};
// Adding / Subtracting
DateEnv.prototype.add = function (marker, dur) {
var a = this.calendarSystem.markerToArray(marker);
a[0] += dur.years;
a[1] += dur.months;
a[2] += dur.days;
a[6] += dur.milliseconds;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.subtract = function (marker, dur) {
var a = this.calendarSystem.markerToArray(marker);
a[0] -= dur.years;
a[1] -= dur.months;
a[2] -= dur.days;
a[6] -= dur.milliseconds;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.addYears = function (marker, n) {
var a = this.calendarSystem.markerToArray(marker);
a[0] += n;
return this.calendarSystem.arrayToMarker(a);
};
DateEnv.prototype.addMonths = function (marker, n) {
var a = this.calendarSystem.markerToArray(marker);
a[1] += n;
return this.calendarSystem.arrayToMarker(a);
};
// Diffing Whole Units
DateEnv.prototype.diffWholeYears = function (m0, m1) {
var calendarSystem = this.calendarSystem;
if (timeAsMs(m0) === timeAsMs(m1) &&
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) &&
calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) {
return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0);
}
return null;
};
DateEnv.prototype.diffWholeMonths = function (m0, m1) {
var calendarSystem = this.calendarSystem;
if (timeAsMs(m0) === timeAsMs(m1) &&
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) {
return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
(calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12;
}
return null;
};
// Range / Duration
DateEnv.prototype.greatestWholeUnit = function (m0, m1) {
var n = this.diffWholeYears(m0, m1);
if (n !== null) {
return { unit: 'year', value: n };
}
n = this.diffWholeMonths(m0, m1);
if (n !== null) {
return { unit: 'month', value: n };
}
n = diffWholeWeeks(m0, m1);
if (n !== null) {
return { unit: 'week', value: n };
}
n = diffWholeDays(m0, m1);
if (n !== null) {
return { unit: 'day', value: n };
}
n = diffHours(m0, m1);
if (isInt(n)) {
return { unit: 'hour', value: n };
}
n = diffMinutes(m0, m1);
if (isInt(n)) {
return { unit: 'minute', value: n };
}
n = diffSeconds(m0, m1);
if (isInt(n)) {
return { unit: 'second', value: n };
}
return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() };
};
DateEnv.prototype.countDurationsBetween = function (m0, m1, d) {
// TODO: can use greatestWholeUnit
var diff;
if (d.years) {
diff = this.diffWholeYears(m0, m1);
if (diff !== null) {
return diff / asRoughYears(d);
}
}
if (d.months) {
diff = this.diffWholeMonths(m0, m1);
if (diff !== null) {
return diff / asRoughMonths(d);
}
}
if (d.days) {
diff = diffWholeDays(m0, m1);
if (diff !== null) {
return diff / asRoughDays(d);
}
}
return (m1.valueOf() - m0.valueOf()) / asRoughMs(d);
};
// Start-Of
DateEnv.prototype.startOf = function (m, unit) {
if (unit === 'year') {
return this.startOfYear(m);
}
else if (unit === 'month') {
return this.startOfMonth(m);
}
else if (unit === 'week') {
return this.startOfWeek(m);
}
else if (unit === 'day') {
return startOfDay(m);
}
else if (unit === 'hour') {
return startOfHour(m);
}
else if (unit === 'minute') {
return startOfMinute(m);
}
else if (unit === 'second') {
return startOfSecond(m);
}
};
DateEnv.prototype.startOfYear = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m)
]);
};
DateEnv.prototype.startOfMonth = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m),
this.calendarSystem.getMarkerMonth(m)
]);
};
DateEnv.prototype.startOfWeek = function (m) {
return this.calendarSystem.arrayToMarker([
this.calendarSystem.getMarkerYear(m),
this.calendarSystem.getMarkerMonth(m),
m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7)
]);
};
// Week Number
DateEnv.prototype.computeWeekNumber = function (marker) {
if (this.weekNumberFunc) {
return this.weekNumberFunc(this.toDate(marker));
}
else {
return weekOfYear(marker, this.weekDow, this.weekDoy);
}
};
// TODO: choke on timeZoneName: long
DateEnv.prototype.format = function (marker, formatter, dateOptions) {
if (dateOptions === void 0) { dateOptions = {}; }
return formatter.format({
marker: marker,
timeZoneOffset: dateOptions.forcedTzo != null ?
dateOptions.forcedTzo :
this.offsetForMarker(marker)
}, this);
};
DateEnv.prototype.formatRange = function (start, end, formatter, dateOptions) {
if (dateOptions === void 0) { dateOptions = {}; }
if (dateOptions.isEndExclusive) {
end = addMs(end, -1);
}
return formatter.formatRange({
marker: start,
timeZoneOffset: dateOptions.forcedStartTzo != null ?
dateOptions.forcedStartTzo :
this.offsetForMarker(start)
}, {
marker: end,
timeZoneOffset: dateOptions.forcedEndTzo != null ?
dateOptions.forcedEndTzo :
this.offsetForMarker(end)
}, this);
};
DateEnv.prototype.formatIso = function (marker, extraOptions) {
if (extraOptions === void 0) { extraOptions = {}; }
var timeZoneOffset = null;
if (!extraOptions.omitTimeZoneOffset) {
if (extraOptions.forcedTzo != null) {
timeZoneOffset = extraOptions.forcedTzo;
}
else {
timeZoneOffset = this.offsetForMarker(marker);
}
}
return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime);
};
// TimeZone
DateEnv.prototype.timestampToMarker = function (ms) {
if (this.timeZone === 'local') {
return arrayToUtcDate(dateToLocalArray(new Date(ms)));
}
else if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
return new Date(ms);
}
else {
return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms));
}
};
DateEnv.prototype.offsetForMarker = function (m) {
if (this.timeZone === 'local') {
return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset
}
else if (this.timeZone === 'UTC') {
return 0;
}
else if (this.namedTimeZoneImpl) {
return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m));
}
return null;
};
// Conversion
DateEnv.prototype.toDate = function (m, forcedTzo) {
if (this.timeZone === 'local') {
return arrayToLocalDate(dateToUtcArray(m));
}
else if (this.timeZone === 'UTC') {
return new Date(m.valueOf()); // make sure it's a copy
}
else if (!this.namedTimeZoneImpl) {
return new Date(m.valueOf() - (forcedTzo || 0));
}
else {
return new Date(m.valueOf() -
this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60 // convert minutes -> ms
);
}
};
return DateEnv;
}());
var SIMPLE_SOURCE_PROPS = {
id: String,
allDayDefault: Boolean,
eventDataTransform: Function,
success: Function,
failure: Function
};
var uid$2 = 0;
function doesSourceNeedRange(eventSource, calendar) {
var defs = calendar.pluginSystem.hooks.eventSourceDefs;
return !defs[eventSource.sourceDefId].ignoreRange;
}
function parseEventSource(raw, calendar) {
var defs = calendar.pluginSystem.hooks.eventSourceDefs;
for (var i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
var def = defs[i];
var meta = def.parseMeta(raw);
if (meta) {
var res = parseEventSourceProps(typeof raw === 'object' ? raw : {}, meta, i, calendar);
res._raw = raw;
return res;
}
}
return null;
}
function parseEventSourceProps(raw, meta, sourceDefId, calendar) {
var leftovers0 = {};
var props = refineProps(raw, SIMPLE_SOURCE_PROPS, {}, leftovers0);
var leftovers1 = {};
var ui = processUnscopedUiProps(leftovers0, calendar, leftovers1);
props.isFetching = false;
props.latestFetchId = '';
props.fetchRange = null;
props.publicId = String(raw.id || '');
props.sourceId = String(uid$2++);
props.sourceDefId = sourceDefId;
props.meta = meta;
props.ui = ui;
props.extendedProps = leftovers1;
return props;
}
function reduceEventSources (eventSources, action, dateProfile, calendar) {
switch (action.type) {
case 'ADD_EVENT_SOURCES': // already parsed
return addSources(eventSources, action.sources, dateProfile ? dateProfile.activeRange : null, calendar);
case 'REMOVE_EVENT_SOURCE':
return removeSource(eventSources, action.sourceId);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
if (dateProfile) {
return fetchDirtySources(eventSources, dateProfile.activeRange, calendar);
}
else {
return eventSources;
}
case 'FETCH_EVENT_SOURCES':
case 'CHANGE_TIMEZONE':
return fetchSourcesByIds(eventSources, action.sourceIds ?
arrayToHash(action.sourceIds) :
excludeStaticSources(eventSources, calendar), dateProfile ? dateProfile.activeRange : null, calendar);
case 'RECEIVE_EVENTS':
case 'RECEIVE_EVENT_ERROR':
return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
case 'REMOVE_ALL_EVENT_SOURCES':
return {};
default:
return eventSources;
}
}
var uid$3 = 0;
function addSources(eventSourceHash, sources, fetchRange, calendar) {
var hash = {};
for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) {
var source = sources_1[_i];
hash[source.sourceId] = source;
}
if (fetchRange) {
hash = fetchDirtySources(hash, fetchRange, calendar);
}
return __assign({}, eventSourceHash, hash);
}
function removeSource(eventSourceHash, sourceId) {
return filterHash(eventSourceHash, function (eventSource) {
return eventSource.sourceId !== sourceId;
});
}
function fetchDirtySources(sourceHash, fetchRange, calendar) {
return fetchSourcesByIds(sourceHash, filterHash(sourceHash, function (eventSource) {
return isSourceDirty(eventSource, fetchRange, calendar);
}), fetchRange, calendar);
}
function isSourceDirty(eventSource, fetchRange, calendar) {
if (!doesSourceNeedRange(eventSource, calendar)) {
return !eventSource.latestFetchId;
}
else {
return !calendar.opt('lazyFetching') ||
!eventSource.fetchRange ||
fetchRange.start < eventSource.fetchRange.start ||
fetchRange.end > eventSource.fetchRange.end;
}
}
function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, calendar) {
var nextSources = {};
for (var sourceId in prevSources) {
var source = prevSources[sourceId];
if (sourceIdHash[sourceId]) {
nextSources[sourceId] = fetchSource(source, fetchRange, calendar);
}
else {
nextSources[sourceId] = source;
}
}
return nextSources;
}
function fetchSource(eventSource, fetchRange, calendar) {
var sourceDef = calendar.pluginSystem.hooks.eventSourceDefs[eventSource.sourceDefId];
var fetchId = String(uid$3++);
sourceDef.fetch({
eventSource: eventSource,
calendar: calendar,
range: fetchRange
}, function (res) {
var rawEvents = res.rawEvents;
var calSuccess = calendar.opt('eventSourceSuccess');
var calSuccessRes;
var sourceSuccessRes;
if (eventSource.success) {
sourceSuccessRes = eventSource.success(rawEvents, res.xhr);
}
if (calSuccess) {
calSuccessRes = calSuccess(rawEvents, res.xhr);
}
rawEvents = sourceSuccessRes || calSuccessRes || rawEvents;
calendar.dispatch({
type: 'RECEIVE_EVENTS',
sourceId: eventSource.sourceId,
fetchId: fetchId,
fetchRange: fetchRange,
rawEvents: rawEvents
});
}, function (error) {
var callFailure = calendar.opt('eventSourceFailure');
console.warn(error.message, error);
if (eventSource.failure) {
eventSource.failure(error);
}
if (callFailure) {
callFailure(error);
}
calendar.dispatch({
type: 'RECEIVE_EVENT_ERROR',
sourceId: eventSource.sourceId,
fetchId: fetchId,
fetchRange: fetchRange,
error: error
});
});
return __assign({}, eventSource, { isFetching: true, latestFetchId: fetchId });
}
function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
var _a;
var eventSource = sourceHash[sourceId];
if (eventSource && // not already removed
fetchId === eventSource.latestFetchId) {
return __assign({}, sourceHash, (_a = {}, _a[sourceId] = __assign({}, eventSource, { isFetching: false, fetchRange: fetchRange }), _a));
}
return sourceHash;
}
function excludeStaticSources(eventSources, calendar) {
return filterHash(eventSources, function (eventSource) {
return doesSourceNeedRange(eventSource, calendar);
});
}
var DateProfileGenerator = /** @class */ (function () {
function DateProfileGenerator(viewSpec, calendar) {
this.viewSpec = viewSpec;
this.options = viewSpec.options;
this.dateEnv = calendar.dateEnv;
this.calendar = calendar;
this.initHiddenDays();
}
/* Date Range Computation
------------------------------------------------------------------------------------------------------------------*/
// Builds a structure with info about what the dates/ranges will be for the "prev" view.
DateProfileGenerator.prototype.buildPrev = function (currentDateProfile, currentDate) {
var dateEnv = this.dateEnv;
var prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(prevDate, -1);
};
// Builds a structure with info about what the dates/ranges will be for the "next" view.
DateProfileGenerator.prototype.buildNext = function (currentDateProfile, currentDate) {
var dateEnv = this.dateEnv;
var nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
currentDateProfile.dateIncrement);
return this.build(nextDate, 1);
};
// Builds a structure holding dates/ranges for rendering around the given date.
// Optional direction param indicates whether the date is being incremented/decremented
// from its previous value. decremented = -1, incremented = 1 (default).
DateProfileGenerator.prototype.build = function (currentDate, direction, forceToValid) {
if (forceToValid === void 0) { forceToValid = false; }
var validRange;
var minTime = null;
var maxTime = null;
var currentInfo;
var isRangeAllDay;
var renderRange;
var activeRange;
var isValid;
validRange = this.buildValidRange();
validRange = this.trimHiddenDays(validRange);
if (forceToValid) {
currentDate = constrainMarkerToRange(currentDate, validRange);
}
currentInfo = this.buildCurrentRangeInfo(currentDate, direction);
isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay);
renderRange = this.trimHiddenDays(renderRange);
activeRange = renderRange;
if (!this.options.showNonCurrentDates) {
activeRange = intersectRanges(activeRange, currentInfo.range);
}
minTime = createDuration(this.options.minTime);
maxTime = createDuration(this.options.maxTime);
activeRange = this.adjustActiveRange(activeRange, minTime, maxTime);
activeRange = intersectRanges(activeRange, validRange); // might return null
// it's invalid if the originally requested date is not contained,
// or if the range is completely outside of the valid range.
isValid = rangesIntersect(currentInfo.range, validRange);
return {
// constraint for where prev/next operations can go and where events can be dragged/resized to.
// an object with optional start and end properties.
validRange: validRange,
// range the view is formally responsible for.
// for example, a month view might have 1st-31st, excluding padded dates
currentRange: currentInfo.range,
// name of largest unit being displayed, like "month" or "week"
currentRangeUnit: currentInfo.unit,
isRangeAllDay: isRangeAllDay,
// dates that display events and accept drag-n-drop
// will be `null` if no dates accept events
activeRange: activeRange,
// date range with a rendered skeleton
// includes not-active days that need some sort of DOM
renderRange: renderRange,
// Duration object that denotes the first visible time of any given day
minTime: minTime,
// Duration object that denotes the exclusive visible end time of any given day
maxTime: maxTime,
isValid: isValid,
// how far the current date will move for a prev/next operation
dateIncrement: this.buildDateIncrement(currentInfo.duration)
// pass a fallback (might be null) ^
};
};
// Builds an object with optional start/end properties.
// Indicates the minimum/maximum dates to display.
// not responsible for trimming hidden days.
DateProfileGenerator.prototype.buildValidRange = function () {
return this.getRangeOption('validRange', this.calendar.getNow()) ||
{ start: null, end: null }; // completely open-ended
};
// Builds a structure with info about the "current" range, the range that is
// highlighted as being the current month for example.
// See build() for a description of `direction`.
// Guaranteed to have `range` and `unit` properties. `duration` is optional.
DateProfileGenerator.prototype.buildCurrentRangeInfo = function (date, direction) {
var _a = this, viewSpec = _a.viewSpec, dateEnv = _a.dateEnv;
var duration = null;
var unit = null;
var range = null;
var dayCount;
if (viewSpec.duration) {
duration = viewSpec.duration;
unit = viewSpec.durationUnit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
else if ((dayCount = this.options.dayCount)) {
unit = 'day';
range = this.buildRangeFromDayCount(date, direction, dayCount);
}
else if ((range = this.buildCustomVisibleRange(date))) {
unit = dateEnv.greatestWholeUnit(range.start, range.end).unit;
}
else {
duration = this.getFallbackDuration();
unit = greatestDurationDenominator(duration).unit;
range = this.buildRangeFromDuration(date, direction, duration, unit);
}
return { duration: duration, unit: unit, range: range };
};
DateProfileGenerator.prototype.getFallbackDuration = function () {
return createDuration({ day: 1 });
};
// Returns a new activeRange to have time values (un-ambiguate)
// minTime or maxTime causes the range to expand.
DateProfileGenerator.prototype.adjustActiveRange = function (range, minTime, maxTime) {
var dateEnv = this.dateEnv;
var start = range.start;
var end = range.end;
if (this.viewSpec.class.prototype.usesMinMaxTime) {
// expand active range if minTime is negative (why not when positive?)
if (asRoughDays(minTime) < 0) {
start = startOfDay(start); // necessary?
start = dateEnv.add(start, minTime);
}
// expand active range if maxTime is beyond one day (why not when positive?)
if (asRoughDays(maxTime) > 1) {
end = startOfDay(end); // necessary?
end = addDays(end, -1);
end = dateEnv.add(end, maxTime);
}
}
return { start: start, end: end };
};
// Builds the "current" range when it is specified as an explicit duration.
// `unit` is the already-computed greatestDurationDenominator unit of duration.
DateProfileGenerator.prototype.buildRangeFromDuration = function (date, direction, duration, unit) {
var dateEnv = this.dateEnv;
var alignment = this.options.dateAlignment;
var dateIncrementInput;
var dateIncrementDuration;
var start;
var end;
var res;
// compute what the alignment should be
if (!alignment) {
dateIncrementInput = this.options.dateIncrement;
if (dateIncrementInput) {
dateIncrementDuration = createDuration(dateIncrementInput);
// use the smaller of the two units
if (asRoughMs(dateIncrementDuration) < asRoughMs(duration)) {
alignment = greatestDurationDenominator(dateIncrementDuration, !getWeeksFromInput(dateIncrementInput)).unit;
}
else {
alignment = unit;
}
}
else {
alignment = unit;
}
}
// if the view displays a single day or smaller
if (asRoughDays(duration) <= 1) {
if (this.isHiddenDay(start)) {
start = this.skipHiddenDays(start, direction);
start = startOfDay(start);
}
}
function computeRes() {
start = dateEnv.startOf(date, alignment);
end = dateEnv.add(start, duration);
res = { start: start, end: end };
}
computeRes();
// if range is completely enveloped by hidden days, go past the hidden days
if (!this.trimHiddenDays(res)) {
date = this.skipHiddenDays(date, direction);
computeRes();
}
return res;
};
// Builds the "current" range when a dayCount is specified.
DateProfileGenerator.prototype.buildRangeFromDayCount = function (date, direction, dayCount) {
var dateEnv = this.dateEnv;
var customAlignment = this.options.dateAlignment;
var runningCount = 0;
var start = date;
var end;
if (customAlignment) {
start = dateEnv.startOf(start, customAlignment);
}
start = startOfDay(start);
start = this.skipHiddenDays(start, direction);
end = start;
do {
end = addDays(end, 1);
if (!this.isHiddenDay(end)) {
runningCount++;
}
} while (runningCount < dayCount);
return { start: start, end: end };
};
// Builds a normalized range object for the "visible" range,
// which is a way to define the currentRange and activeRange at the same time.
DateProfileGenerator.prototype.buildCustomVisibleRange = function (date) {
var dateEnv = this.dateEnv;
var visibleRange = this.getRangeOption('visibleRange', dateEnv.toDate(date));
if (visibleRange && (visibleRange.start == null || visibleRange.end == null)) {
return null;
}
return visibleRange;
};
// Computes the range that will represent the element/cells for *rendering*,
// but which may have voided days/times.
// not responsible for trimming hidden days.
DateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
return currentRange;
};
// Compute the duration value that should be added/substracted to the current date
// when a prev/next operation happens.
DateProfileGenerator.prototype.buildDateIncrement = function (fallback) {
var dateIncrementInput = this.options.dateIncrement;
var customAlignment;
if (dateIncrementInput) {
return createDuration(dateIncrementInput);
}
else if ((customAlignment = this.options.dateAlignment)) {
return createDuration(1, customAlignment);
}
else if (fallback) {
return fallback;
}
else {
return createDuration({ days: 1 });
}
};
// Arguments after name will be forwarded to a hypothetical function value
// WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
// Always clone your objects if you fear mutation.
DateProfileGenerator.prototype.getRangeOption = function (name) {
var otherArgs = [];
for (var _i = 1; _i < arguments.length; _i++) {
otherArgs[_i - 1] = arguments[_i];
}
var val = this.options[name];
if (typeof val === 'function') {
val = val.apply(null, otherArgs);
}
if (val) {
val = parseRange(val, this.dateEnv);
}
if (val) {
val = computeVisibleDayRange(val);
}
return val;
};
/* Hidden Days
------------------------------------------------------------------------------------------------------------------*/
// Initializes internal variables related to calculating hidden days-of-week
DateProfileGenerator.prototype.initHiddenDays = function () {
var hiddenDays = this.options.hiddenDays || []; // array of day-of-week indices that are hidden
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
var dayCnt = 0;
var i;
if (this.options.weekends === false) {
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
}
for (i = 0; i < 7; i++) {
if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) {
dayCnt++;
}
}
if (!dayCnt) {
throw new Error('invalid hiddenDays'); // all days were hidden? bad.
}
this.isHiddenDayHash = isHiddenDayHash;
};
// Remove days from the beginning and end of the range that are computed as hidden.
// If the whole range is trimmed off, returns null
DateProfileGenerator.prototype.trimHiddenDays = function (range) {
var start = range.start;
var end = range.end;
if (start) {
start = this.skipHiddenDays(start);
}
if (end) {
end = this.skipHiddenDays(end, -1, true);
}
if (start == null || end == null || start < end) {
return { start: start, end: end };
}
return null;
};
// Is the current day hidden?
// `day` is a day-of-week index (0-6), or a Date (used for UTC)
DateProfileGenerator.prototype.isHiddenDay = function (day) {
if (day instanceof Date) {
day = day.getUTCDay();
}
return this.isHiddenDayHash[day];
};
// Incrementing the current day until it is no longer a hidden day, returning a copy.
// DOES NOT CONSIDER validRange!
// If the initial value of `date` is not a hidden day, don't do anything.
// Pass `isExclusive` as `true` if you are dealing with an end date.
// `inc` defaults to `1` (increment one day forward each time)
DateProfileGenerator.prototype.skipHiddenDays = function (date, inc, isExclusive) {
if (inc === void 0) { inc = 1; }
if (isExclusive === void 0) { isExclusive = false; }
while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) {
date = addDays(date, inc);
}
return date;
};
return DateProfileGenerator;
}());
// TODO: find a way to avoid comparing DateProfiles. it's tedious
function isDateProfilesEqual(p0, p1) {
return rangesEqual(p0.validRange, p1.validRange) &&
rangesEqual(p0.activeRange, p1.activeRange) &&
rangesEqual(p0.renderRange, p1.renderRange) &&
durationsEqual(p0.minTime, p1.minTime) &&
durationsEqual(p0.maxTime, p1.maxTime);
/*
TODO: compare more?
currentRange: DateRange
currentRangeUnit: string
isRangeAllDay: boolean
isValid: boolean
dateIncrement: Duration
*/
}
function reduce (state, action, calendar) {
var viewType = reduceViewType(state.viewType, action);
var dateProfile = reduceDateProfile(state.dateProfile, action, state.currentDate, viewType, calendar);
var eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendar);
var nextState = __assign({}, state, { viewType: viewType,
dateProfile: dateProfile, currentDate: reduceCurrentDate(state.currentDate, action, dateProfile), eventSources: eventSources, eventStore: reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendar), dateSelection: reduceDateSelection(state.dateSelection, action, calendar), eventSelection: reduceSelectedEvent(state.eventSelection, action), eventDrag: reduceEventDrag(state.eventDrag, action, eventSources, calendar), eventResize: reduceEventResize(state.eventResize, action, eventSources, calendar), eventSourceLoadingLevel: computeLoadingLevel(eventSources), loadingLevel: computeLoadingLevel(eventSources) });
for (var _i = 0, _a = calendar.pluginSystem.hooks.reducers; _i < _a.length; _i++) {
var reducerFunc = _a[_i];
nextState = reducerFunc(nextState, action, calendar);
}
// console.log(action.type, nextState)
return nextState;
}
function reduceViewType(currentViewType, action) {
switch (action.type) {
case 'SET_VIEW_TYPE':
return action.viewType;
default:
return currentViewType;
}
}
function reduceDateProfile(currentDateProfile, action, currentDate, viewType, calendar) {
var newDateProfile;
switch (action.type) {
case 'PREV':
newDateProfile = calendar.dateProfileGenerators[viewType].buildPrev(currentDateProfile, currentDate);
break;
case 'NEXT':
newDateProfile = calendar.dateProfileGenerators[viewType].buildNext(currentDateProfile, currentDate);
break;
case 'SET_DATE':
if (!currentDateProfile.activeRange ||
!rangeContainsMarker(currentDateProfile.currentRange, action.dateMarker)) {
newDateProfile = calendar.dateProfileGenerators[viewType].build(action.dateMarker, undefined, true // forceToValid
);
}
break;
case 'SET_VIEW_TYPE':
var generator = calendar.dateProfileGenerators[viewType];
if (!generator) {
throw new Error(viewType ?
'The FullCalendar view "' + viewType + '" does not exist. Make sure your plugins are loaded correctly.' :
'No available FullCalendar view plugins.');
}
newDateProfile = generator.build(action.dateMarker || currentDate, undefined, true // forceToValid
);
break;
}
if (newDateProfile &&
newDateProfile.isValid &&
!(currentDateProfile && isDateProfilesEqual(currentDateProfile, newDateProfile))) {
return newDateProfile;
}
else {
return currentDateProfile;
}
}
function reduceCurrentDate(currentDate, action, dateProfile) {
switch (action.type) {
case 'PREV':
case 'NEXT':
if (!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
return dateProfile.currentRange.start;
}
else {
return currentDate;
}
case 'SET_DATE':
case 'SET_VIEW_TYPE':
var newDate = action.dateMarker || currentDate;
if (dateProfile.activeRange && !rangeContainsMarker(dateProfile.activeRange, newDate)) {
return dateProfile.currentRange.start;
}
else {
return newDate;
}
default:
return currentDate;
}
}
function reduceDateSelection(currentSelection, action, calendar) {
switch (action.type) {
case 'SELECT_DATES':
return action.selection;
case 'UNSELECT_DATES':
return null;
default:
return currentSelection;
}
}
function reduceSelectedEvent(currentInstanceId, action) {
switch (action.type) {
case 'SELECT_EVENT':
return action.eventInstanceId;
case 'UNSELECT_EVENT':
return '';
default:
return currentInstanceId;
}
}
function reduceEventDrag(currentDrag, action, sources, calendar) {
switch (action.type) {
case 'SET_EVENT_DRAG':
var newDrag = action.state;
return {
affectedEvents: newDrag.affectedEvents,
mutatedEvents: newDrag.mutatedEvents,
isEvent: newDrag.isEvent,
origSeg: newDrag.origSeg
};
case 'UNSET_EVENT_DRAG':
return null;
default:
return currentDrag;
}
}
function reduceEventResize(currentResize, action, sources, calendar) {
switch (action.type) {
case 'SET_EVENT_RESIZE':
var newResize = action.state;
return {
affectedEvents: newResize.affectedEvents,
mutatedEvents: newResize.mutatedEvents,
isEvent: newResize.isEvent,
origSeg: newResize.origSeg
};
case 'UNSET_EVENT_RESIZE':
return null;
default:
return currentResize;
}
}
function computeLoadingLevel(eventSources) {
var cnt = 0;
for (var sourceId in eventSources) {
if (eventSources[sourceId].isFetching) {
cnt++;
}
}
return cnt;
}
var STANDARD_PROPS = {
start: null,
end: null,
allDay: Boolean
};
function parseDateSpan(raw, dateEnv, defaultDuration) {
var span = parseOpenDateSpan(raw, dateEnv);
var range = span.range;
if (!range.start) {
return null;
}
if (!range.end) {
if (defaultDuration == null) {
return null;
}
else {
range.end = dateEnv.add(range.start, defaultDuration);
}
}
return span;
}
/*
TODO: somehow combine with parseRange?
Will return null if the start/end props were present but parsed invalidly.
*/
function parseOpenDateSpan(raw, dateEnv) {
var leftovers = {};
var standardProps = refineProps(raw, STANDARD_PROPS, {}, leftovers);
var startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null;
var endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null;
var allDay = standardProps.allDay;
if (allDay == null) {
allDay = (startMeta && startMeta.isTimeUnspecified) &&
(!endMeta || endMeta.isTimeUnspecified);
}
// use this leftover object as the selection object
leftovers.range = {
start: startMeta ? startMeta.marker : null,
end: endMeta ? endMeta.marker : null
};
leftovers.allDay = allDay;
return leftovers;
}
function isDateSpansEqual(span0, span1) {
return rangesEqual(span0.range, span1.range) &&
span0.allDay === span1.allDay &&
isSpanPropsEqual(span0, span1);
}
// the NON-DATE-RELATED props
function isSpanPropsEqual(span0, span1) {
for (var propName in span1) {
if (propName !== 'range' && propName !== 'allDay') {
if (span0[propName] !== span1[propName]) {
return false;
}
}
}
// are there any props that span0 has that span1 DOESN'T have?
// both have range/allDay, so no need to special-case.
for (var propName in span0) {
if (!(propName in span1)) {
return false;
}
}
return true;
}
function buildDateSpanApi(span, dateEnv) {
return {
start: dateEnv.toDate(span.range.start),
end: dateEnv.toDate(span.range.end),
startStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
endStr: dateEnv.formatIso(span.range.end, { omitTime: span.allDay }),
allDay: span.allDay
};
}
function buildDatePointApi(span, dateEnv) {
return {
date: dateEnv.toDate(span.range.start),
dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
allDay: span.allDay
};
}
function fabricateEventRange(dateSpan, eventUiBases, calendar) {
var def = parseEventDef({ editable: false }, '', // sourceId
dateSpan.allDay, true, // hasEnd
calendar);
return {
def: def,
ui: compileEventUi(def, eventUiBases),
instance: createEventInstance(def.defId, dateSpan.range),
range: dateSpan.range,
isStart: true,
isEnd: true
};
}
function compileViewDefs(defaultConfigs, overrideConfigs) {
var hash = {};
var viewType;
for (viewType in defaultConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
for (viewType in overrideConfigs) {
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
}
return hash;
}
function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
if (hash[viewType]) {
return hash[viewType];
}
var viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
if (viewDef) {
hash[viewType] = viewDef;
}
return viewDef;
}
function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
var defaultConfig = defaultConfigs[viewType];
var overrideConfig = overrideConfigs[viewType];
var queryProp = function (name) {
return (defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null);
};
var theClass = queryProp('class');
var superType = queryProp('superType');
if (!superType && theClass) {
superType =
findViewNameBySubclass(theClass, overrideConfigs) ||
findViewNameBySubclass(theClass, defaultConfigs);
}
var superDef = null;
if (superType) {
if (superType === viewType) {
throw new Error('Can\'t have a custom view type that references itself');
}
superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
}
if (!theClass && superDef) {
theClass = superDef.class;
}
if (!theClass) {
return null; // don't throw a warning, might be settings for a single-unit view
}
return {
type: viewType,
class: theClass,
defaults: __assign({}, (superDef ? superDef.defaults : {}), (defaultConfig ? defaultConfig.options : {})),
overrides: __assign({}, (superDef ? superDef.overrides : {}), (overrideConfig ? overrideConfig.options : {}))
};
}
function findViewNameBySubclass(viewSubclass, configs) {
var superProto = Object.getPrototypeOf(viewSubclass.prototype);
for (var viewType in configs) {
var parsed = configs[viewType];
// need DIRECT subclass, so instanceof won't do it
if (parsed.class && parsed.class.prototype === superProto) {
return viewType;
}
}
return '';
}
function parseViewConfigs(inputs) {
return mapHash(inputs, parseViewConfig);
}
var VIEW_DEF_PROPS = {
type: String,
class: null
};
function parseViewConfig(input) {
if (typeof input === 'function') {
input = { class: input };
}
var options = {};
var props = refineProps(input, VIEW_DEF_PROPS, {}, options);
return {
superType: props.type,
class: props.class,
options: options
};
}
function buildViewSpecs(defaultInputs, optionsManager) {
var defaultConfigs = parseViewConfigs(defaultInputs);
var overrideConfigs = parseViewConfigs(optionsManager.overrides.views);
var viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
return mapHash(viewDefs, function (viewDef) {
return buildViewSpec(viewDef, overrideConfigs, optionsManager);
});
}
function buildViewSpec(viewDef, overrideConfigs, optionsManager) {
var durationInput = viewDef.overrides.duration ||
viewDef.defaults.duration ||
optionsManager.dynamicOverrides.duration ||
optionsManager.overrides.duration;
var duration = null;
var durationUnit = '';
var singleUnit = '';
var singleUnitOverrides = {};
if (durationInput) {
duration = createDuration(durationInput);
if (duration) { // valid?
var denom = greatestDurationDenominator(duration, !getWeeksFromInput(durationInput));
durationUnit = denom.unit;
if (denom.value === 1) {
singleUnit = durationUnit;
singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].options : {};
}
}
}
var queryButtonText = function (options) {
var buttonTextMap = options.buttonText || {};
var buttonTextKey = viewDef.defaults.buttonTextKey;
if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
return buttonTextMap[buttonTextKey];
}
if (buttonTextMap[viewDef.type] != null) {
return buttonTextMap[viewDef.type];
}
if (buttonTextMap[singleUnit] != null) {
return buttonTextMap[singleUnit];
}
};
return {
type: viewDef.type,
class: viewDef.class,
duration: duration,
durationUnit: durationUnit,
singleUnit: singleUnit,
options: __assign({}, globalDefaults, viewDef.defaults, optionsManager.dirDefaults, optionsManager.localeDefaults, optionsManager.overrides, singleUnitOverrides, viewDef.overrides, optionsManager.dynamicOverrides),
buttonTextOverride: queryButtonText(optionsManager.dynamicOverrides) ||
queryButtonText(optionsManager.overrides) || // constructor-specified buttonText lookup hash takes precedence
viewDef.overrides.buttonText,
buttonTextDefault: queryButtonText(optionsManager.localeDefaults) ||
queryButtonText(optionsManager.dirDefaults) ||
viewDef.defaults.buttonText ||
queryButtonText(globalDefaults) ||
viewDef.type // fall back to given view name
};
}
var Toolbar = /** @class */ (function (_super) {
__extends(Toolbar, _super);
function Toolbar(context, extraClassName) {
var _this = _super.call(this, context) || this;
_this._renderLayout = memoizeRendering(_this.renderLayout, _this.unrenderLayout);
_this._updateTitle = memoizeRendering(_this.updateTitle, null, [_this._renderLayout]);
_this._updateActiveButton = memoizeRendering(_this.updateActiveButton, null, [_this._renderLayout]);
_this._updateToday = memoizeRendering(_this.updateToday, null, [_this._renderLayout]);
_this._updatePrev = memoizeRendering(_this.updatePrev, null, [_this._renderLayout]);
_this._updateNext = memoizeRendering(_this.updateNext, null, [_this._renderLayout]);
_this.el = createElement('div', { className: 'fc-toolbar ' + extraClassName });
return _this;
}
Toolbar.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderLayout.unrender(); // should unrender everything else
removeElement(this.el);
};
Toolbar.prototype.render = function (props) {
this._renderLayout(props.layout);
this._updateTitle(props.title);
this._updateActiveButton(props.activeButton);
this._updateToday(props.isTodayEnabled);
this._updatePrev(props.isPrevEnabled);
this._updateNext(props.isNextEnabled);
};
Toolbar.prototype.renderLayout = function (layout) {
var el = this.el;
this.viewsWithButtons = [];
appendToElement(el, this.renderSection('left', layout.left));
appendToElement(el, this.renderSection('center', layout.center));
appendToElement(el, this.renderSection('right', layout.right));
};
Toolbar.prototype.unrenderLayout = function () {
this.el.innerHTML = '';
};
Toolbar.prototype.renderSection = function (position, buttonStr) {
var _this = this;
var _a = this, theme = _a.theme, calendar = _a.calendar;
var optionsManager = calendar.optionsManager;
var viewSpecs = calendar.viewSpecs;
var sectionEl = createElement('div', { className: 'fc-' + position });
var calendarCustomButtons = optionsManager.computed.customButtons || {};
var calendarButtonTextOverrides = optionsManager.overrides.buttonText || {};
var calendarButtonText = optionsManager.computed.buttonText || {};
if (buttonStr) {
buttonStr.split(' ').forEach(function (buttonGroupStr, i) {
var groupChildren = [];
var isOnlyButtons = true;
var groupEl;
buttonGroupStr.split(',').forEach(function (buttonName, j) {
var customButtonProps;
var viewSpec;
var buttonClick;
var buttonIcon; // only one of these will be set
var buttonText; // "
var buttonInnerHtml;
var buttonClasses;
var buttonEl;
var buttonAriaAttr;
if (buttonName === 'title') {
groupChildren.push(htmlToElement('
var CalendarComponent = /** @class */ (function (_super) {
__extends(CalendarComponent, _super);
function CalendarComponent(context, el) {
var _this = _super.call(this, context) || this;
_this._renderToolbars = memoizeRendering(_this.renderToolbars);
_this.buildViewPropTransformers = memoize(buildViewPropTransformers);
_this.el = el;
prependToElement(el, _this.contentEl = createElement('div', { className: 'fc-view-container' }));
var calendar = _this.calendar;
for (var _i = 0, _a = calendar.pluginSystem.hooks.viewContainerModifiers; _i < _a.length; _i++) {
var modifyViewContainer = _a[_i];
modifyViewContainer(_this.contentEl, calendar);
}
_this.toggleElClassNames(true);
_this.computeTitle = memoize(computeTitle);
_this.parseBusinessHours = memoize(function (input) {
return parseBusinessHours(input, _this.calendar);
});
return _this;
}
CalendarComponent.prototype.destroy = function () {
if (this.header) {
this.header.destroy();
}
if (this.footer) {
this.footer.destroy();
}
if (this.view) {
this.view.destroy();
}
removeElement(this.contentEl);
this.toggleElClassNames(false);
_super.prototype.destroy.call(this);
};
CalendarComponent.prototype.toggleElClassNames = function (bool) {
var classList = this.el.classList;
var dirClassName = 'fc-' + this.opt('dir');
var themeClassName = this.theme.getClass('widget');
if (bool) {
classList.add('fc');
classList.add(dirClassName);
classList.add(themeClassName);
}
else {
classList.remove('fc');
classList.remove(dirClassName);
classList.remove(themeClassName);
}
};
CalendarComponent.prototype.render = function (props) {
this.freezeHeight();
var title = this.computeTitle(props.dateProfile, props.viewSpec.options);
this._renderToolbars(props.viewSpec, props.dateProfile, props.currentDate, props.dateProfileGenerator, title);
this.renderView(props, title);
this.updateSize();
this.thawHeight();
};
CalendarComponent.prototype.renderToolbars = function (viewSpec, dateProfile, currentDate, dateProfileGenerator, title) {
var headerLayout = this.opt('header');
var footerLayout = this.opt('footer');
var now = this.calendar.getNow();
var todayInfo = dateProfileGenerator.build(now);
var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate);
var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate);
var toolbarProps = {
title: title,
activeButton: viewSpec.type,
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
isPrevEnabled: prevInfo.isValid,
isNextEnabled: nextInfo.isValid
};
if (headerLayout) {
if (!this.header) {
this.header = new Toolbar(this.context, 'fc-header-toolbar');
prependToElement(this.el, this.header.el);
}
this.header.receiveProps(__assign({ layout: headerLayout }, toolbarProps));
}
else if (this.header) {
this.header.destroy();
this.header = null;
}
if (footerLayout) {
if (!this.footer) {
this.footer = new Toolbar(this.context, 'fc-footer-toolbar');
appendToElement(this.el, this.footer.el);
}
this.footer.receiveProps(__assign({ layout: footerLayout }, toolbarProps));
}
else if (this.footer) {
this.footer.destroy();
this.footer = null;
}
};
CalendarComponent.prototype.renderView = function (props, title) {
var view = this.view;
var viewSpec = props.viewSpec, dateProfileGenerator = props.dateProfileGenerator;
if (!view || view.viewSpec !== viewSpec) {
if (view) {
view.destroy();
}
view = this.view = new viewSpec['class']({
calendar: this.calendar,
view: null,
dateEnv: this.dateEnv,
theme: this.theme,
options: viewSpec.options
}, viewSpec, dateProfileGenerator, this.contentEl);
}
else {
view.addScroll(view.queryScroll());
}
view.title = title; // for the API
var viewProps = {
dateProfile: props.dateProfile,
businessHours: this.parseBusinessHours(viewSpec.options.businessHours),
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize
};
var transformers = this.buildViewPropTransformers(this.calendar.pluginSystem.hooks.viewPropsTransformers);
for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) {
var transformer = transformers_1[_i];
__assign(viewProps, transformer.transform(viewProps, viewSpec, props, view));
}
view.receiveProps(viewProps);
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
CalendarComponent.prototype.updateSize = function (isResize) {
if (isResize === void 0) { isResize = false; }
var view = this.view;
if (isResize) {
view.addScroll(view.queryScroll());
}
if (isResize || this.isHeightAuto == null) {
this.computeHeightVars();
}
view.updateSize(isResize, this.viewHeight, this.isHeightAuto);
view.updateNowIndicator(); // we need to guarantee this will run after updateSize
view.popScroll(isResize);
};
CalendarComponent.prototype.computeHeightVars = function () {
var calendar = this.calendar; // yuck. need to handle dynamic options
var heightInput = calendar.opt('height');
var contentHeightInput = calendar.opt('contentHeight');
this.isHeightAuto = heightInput === 'auto' || contentHeightInput === 'auto';
if (typeof contentHeightInput === 'number') { // exists and not 'auto'
this.viewHeight = contentHeightInput;
}
else if (typeof contentHeightInput === 'function') { // exists and is a function
this.viewHeight = contentHeightInput();
}
else if (typeof heightInput === 'number') { // exists and not 'auto'
this.viewHeight = heightInput - this.queryToolbarsHeight();
}
else if (typeof heightInput === 'function') { // exists and is a function
this.viewHeight = heightInput() - this.queryToolbarsHeight();
}
else if (heightInput === 'parent') { // set to height of parent element
var parentEl = this.el.parentNode;
this.viewHeight = parentEl.getBoundingClientRect().height - this.queryToolbarsHeight();
}
else {
this.viewHeight = Math.round(this.contentEl.getBoundingClientRect().width /
Math.max(calendar.opt('aspectRatio'), .5));
}
};
CalendarComponent.prototype.queryToolbarsHeight = function () {
var height = 0;
if (this.header) {
height += computeHeightAndMargins(this.header.el);
}
if (this.footer) {
height += computeHeightAndMargins(this.footer.el);
}
return height;
};
// Height "Freezing"
// -----------------------------------------------------------------------------------------------------------------
CalendarComponent.prototype.freezeHeight = function () {
applyStyle(this.el, {
height: this.el.getBoundingClientRect().height,
overflow: 'hidden'
});
};
CalendarComponent.prototype.thawHeight = function () {
applyStyle(this.el, {
height: '',
overflow: ''
});
};
return CalendarComponent;
}(Component));
// Title and Date Formatting
// -----------------------------------------------------------------------------------------------------------------
// Computes what the title at the top of the calendar should be for this view
function computeTitle(dateProfile, viewOptions) {
var range;
// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
range = dateProfile.currentRange;
}
else { // for day units or smaller, use the actual day range
range = dateProfile.activeRange;
}
return this.dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || computeTitleFormat(dateProfile), viewOptions.titleRangeSeparator), { isEndExclusive: dateProfile.isRangeAllDay });
}
// Generates the format string that should be used to generate the title for the current date range.
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
function computeTitleFormat(dateProfile) {
var currentRangeUnit = dateProfile.currentRangeUnit;
if (currentRangeUnit === 'year') {
return { year: 'numeric' };
}
else if (currentRangeUnit === 'month') {
return { year: 'numeric', month: 'long' }; // like "September 2014"
}
else {
var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
if (days !== null && days > 1) {
// multi-day range. shorter, like "Sep 9 - 10 2014"
return { year: 'numeric', month: 'short', day: 'numeric' };
}
else {
// one day. longer, like "September 9 2014"
return { year: 'numeric', month: 'long', day: 'numeric' };
}
}
}
// Plugin
// -----------------------------------------------------------------------------------------------------------------
function buildViewPropTransformers(theClasses) {
return theClasses.map(function (theClass) {
return new theClass();
});
}
var Interaction = /** @class */ (function () {
function Interaction(settings) {
this.component = settings.component;
}
Interaction.prototype.destroy = function () {
};
return Interaction;
}());
function parseInteractionSettings(component, input) {
return {
component: component,
el: input.el,
useEventCenter: input.useEventCenter != null ? input.useEventCenter : true
};
}
function interactionSettingsToStore(settings) {
var _a;
return _a = {},
_a[settings.component.uid] = settings,
_a;
}
// global state
var interactionSettingsStore = {};
/*
Detects when the user clicks on an event within a DateComponent
*/
var EventClicking = /** @class */ (function (_super) {
__extends(EventClicking, _super);
function EventClicking(settings) {
var _this = _super.call(this, settings) || this;
_this.handleSegClick = function (ev, segEl) {
var component = _this.component;
var seg = getElSeg(segEl);
if (seg && // might be the
/*
Triggers events and adds/removes core classNames when the user's pointer
enters/leaves event-elements of a component.
*/
var EventHovering = /** @class */ (function (_super) {
__extends(EventHovering, _super);
function EventHovering(settings) {
var _this = _super.call(this, settings) || this;
// for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
_this.handleEventElRemove = function (el) {
if (el === _this.currentSegEl) {
_this.handleSegLeave(null, _this.currentSegEl);
}
};
_this.handleSegEnter = function (ev, segEl) {
if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
segEl.classList.add('fc-allow-mouse-resize');
_this.currentSegEl = segEl;
_this.triggerEvent('eventMouseEnter', ev, segEl);
}
};
_this.handleSegLeave = function (ev, segEl) {
if (_this.currentSegEl) {
segEl.classList.remove('fc-allow-mouse-resize');
_this.currentSegEl = null;
_this.triggerEvent('eventMouseLeave', ev, segEl);
}
};
var component = settings.component;
_this.removeHoverListeners = listenToHoverBySelector(component.el, component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegEnter, _this.handleSegLeave);
component.calendar.on('eventElRemove', _this.handleEventElRemove);
return _this;
}
EventHovering.prototype.destroy = function () {
this.removeHoverListeners();
this.component.calendar.off('eventElRemove', this.handleEventElRemove);
};
EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) {
var component = this.component;
var seg = getElSeg(segEl);
if (!ev || component.isValidSegDownEl(ev.target)) {
component.publiclyTrigger(publicEvName, [
{
el: segEl,
event: new EventApi(this.component.calendar, seg.eventRange.def, seg.eventRange.instance),
jsEvent: ev,
view: component.view
}
]);
}
};
return EventHovering;
}(Interaction));
var StandardTheme = /** @class */ (function (_super) {
__extends(StandardTheme, _super);
function StandardTheme() {
return _super !== null && _super.apply(this, arguments) || this;
}
return StandardTheme;
}(Theme));
StandardTheme.prototype.classes = {
widget: 'fc-unthemed',
widgetHeader: 'fc-widget-header',
widgetContent: 'fc-widget-content',
buttonGroup: 'fc-button-group',
button: 'fc-button fc-button-primary',
buttonActive: 'fc-button-active',
popoverHeader: 'fc-widget-header',
popoverContent: 'fc-widget-content',
// day grid
headerRow: 'fc-widget-header',
dayRow: 'fc-widget-content',
// list view
listView: 'fc-widget-content'
};
StandardTheme.prototype.baseIconClass = 'fc-icon';
StandardTheme.prototype.iconClasses = {
close: 'fc-icon-x',
prev: 'fc-icon-chevron-left',
next: 'fc-icon-chevron-right',
prevYear: 'fc-icon-chevrons-left',
nextYear: 'fc-icon-chevrons-right'
};
StandardTheme.prototype.iconOverrideOption = 'buttonIcons';
StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
var Calendar = /** @class */ (function () {
function Calendar(el, overrides) {
var _this = this;
this.parseRawLocales = memoize(parseRawLocales);
this.buildLocale = memoize(buildLocale);
this.buildDateEnv = memoize(buildDateEnv);
this.buildTheme = memoize(buildTheme);
this.buildEventUiSingleBase = memoize(this._buildEventUiSingleBase);
this.buildSelectionConfig = memoize(this._buildSelectionConfig);
this.buildEventUiBySource = memoizeOutput(buildEventUiBySource, isPropsEqual);
this.buildEventUiBases = memoize(buildEventUiBases);
this.interactionsStore = {};
this.actionQueue = [];
this.isReducing = false;
// isDisplaying: boolean = false // installed in DOM? accepting renders?
this.needsRerender = false; // needs a render?
this.needsFullRerender = false;
this.isRendering = false; // currently in the executeRender function?
this.renderingPauseDepth = 0;
this.buildDelayedRerender = memoize(buildDelayedRerender);
this.afterSizingTriggers = {};
this.isViewUpdated = false;
this.isDatesUpdated = false;
this.isEventsUpdated = false;
this.el = el;
this.optionsManager = new OptionsManager(overrides || {});
this.pluginSystem = new PluginSystem();
// only do once. don't do in handleOptions. because can't remove plugins
this.addPluginInputs(this.optionsManager.computed.plugins || []);
this.handleOptions(this.optionsManager.computed);
this.publiclyTrigger('_init'); // for tests
this.hydrate();
this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions
.map(function (calendarInteractionClass) {
return new calendarInteractionClass(_this);
});
}
Calendar.prototype.addPluginInputs = function (pluginInputs) {
var pluginDefs = refinePluginDefs(pluginInputs);
for (var _i = 0, pluginDefs_1 = pluginDefs; _i < pluginDefs_1.length; _i++) {
var pluginDef = pluginDefs_1[_i];
this.pluginSystem.add(pluginDef);
}
};
Object.defineProperty(Calendar.prototype, "view", {
// public API
get: function () {
return this.component ? this.component.view : null;
},
enumerable: true,
configurable: true
});
// Public API for rendering
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.render = function () {
if (!this.component) {
this.renderableEventStore = createEmptyEventStore();
this.bindHandlers();
this.executeRender();
}
else {
this.requestRerender(true);
}
};
Calendar.prototype.destroy = function () {
if (this.component) {
this.unbindHandlers();
this.component.destroy(); // don't null-out. in case API needs access
this.component = null; // umm ???
for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) {
var interaction = _a[_i];
interaction.destroy();
}
this.publiclyTrigger('_destroyed');
}
};
// Handlers
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.bindHandlers = function () {
var _this = this;
// event delegation for nav links
this.removeNavLinkListener = listenBySelector(this.el, 'click', 'a[data-goto]', function (ev, anchorEl) {
var gotoOptions = anchorEl.getAttribute('data-goto');
gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {};
var dateEnv = _this.dateEnv;
var dateMarker = dateEnv.createMarker(gotoOptions.date);
var viewType = gotoOptions.type;
// property like "navLinkDayClick". might be a string or a function
var customAction = _this.viewOpt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
if (typeof customAction === 'function') {
customAction(dateEnv.toDate(dateMarker), ev);
}
else {
if (typeof customAction === 'string') {
viewType = customAction;
}
_this.zoomTo(dateMarker, viewType);
}
});
if (this.opt('handleWindowResize')) {
window.addEventListener('resize', this.windowResizeProxy = debounce(// prevents rapid calls
this.windowResize.bind(this), this.opt('windowResizeDelay')));
}
};
Calendar.prototype.unbindHandlers = function () {
this.removeNavLinkListener();
if (this.windowResizeProxy) {
window.removeEventListener('resize', this.windowResizeProxy);
this.windowResizeProxy = null;
}
};
// Dispatcher
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.hydrate = function () {
var _this = this;
this.state = this.buildInitialState();
var rawSources = this.opt('eventSources') || [];
var singleRawSource = this.opt('events');
var sources = []; // parsed
if (singleRawSource) {
rawSources.unshift(singleRawSource);
}
for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) {
var rawSource = rawSources_1[_i];
var source = parseEventSource(rawSource, this);
if (source) {
sources.push(source);
}
}
this.batchRendering(function () {
_this.dispatch({ type: 'INIT' }); // pass in sources here?
_this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: sources });
_this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: _this.opt('defaultView') || _this.pluginSystem.hooks.defaultView
});
});
};
Calendar.prototype.buildInitialState = function () {
return {
viewType: null,
loadingLevel: 0,
eventSourceLoadingLevel: 0,
currentDate: this.getInitialDate(),
dateProfile: null,
eventSources: {},
eventStore: createEmptyEventStore(),
dateSelection: null,
eventSelection: '',
eventDrag: null,
eventResize: null
};
};
Calendar.prototype.dispatch = function (action) {
this.actionQueue.push(action);
if (!this.isReducing) {
this.isReducing = true;
var oldState = this.state;
while (this.actionQueue.length) {
this.state = this.reduce(this.state, this.actionQueue.shift(), this);
}
var newState = this.state;
this.isReducing = false;
if (!oldState.loadingLevel && newState.loadingLevel) {
this.publiclyTrigger('loading', [true]);
}
else if (oldState.loadingLevel && !newState.loadingLevel) {
this.publiclyTrigger('loading', [false]);
}
var view = this.component && this.component.view;
if (oldState.eventStore !== newState.eventStore || this.needsFullRerender) {
if (oldState.eventStore) {
this.isEventsUpdated = true;
}
}
if (oldState.dateProfile !== newState.dateProfile || this.needsFullRerender) {
if (oldState.dateProfile && view) { // why would view be null!?
this.publiclyTrigger('datesDestroy', [
{
view: view,
el: view.el
}
]);
}
this.isDatesUpdated = true;
}
if (oldState.viewType !== newState.viewType || this.needsFullRerender) {
if (oldState.viewType && view) { // why would view be null!?
this.publiclyTrigger('viewSkeletonDestroy', [
{
view: view,
el: view.el
}
]);
}
this.isViewUpdated = true;
}
this.requestRerender();
}
};
Calendar.prototype.reduce = function (state, action, calendar) {
return reduce(state, action, calendar);
};
// Render Queue
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.requestRerender = function (needsFull) {
if (needsFull === void 0) { needsFull = false; }
this.needsRerender = true;
this.needsFullRerender = this.needsFullRerender || needsFull;
this.delayedRerender(); // will call a debounced-version of tryRerender
};
Calendar.prototype.tryRerender = function () {
if (this.component && // must be accepting renders
this.needsRerender && // indicates that a rerender was requested
!this.renderingPauseDepth && // not paused
!this.isRendering // not currently in the render loop
) {
this.executeRender();
}
};
Calendar.prototype.batchRendering = function (func) {
this.renderingPauseDepth++;
func();
this.renderingPauseDepth--;
if (this.needsRerender) {
this.requestRerender();
}
};
// Rendering
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.executeRender = function () {
var needsFullRerender = this.needsFullRerender; // save before clearing
// clear these BEFORE the render so that new values will accumulate during render
this.needsRerender = false;
this.needsFullRerender = false;
this.isRendering = true;
this.renderComponent(needsFullRerender);
this.isRendering = false;
// received a rerender request while rendering
if (this.needsRerender) {
this.delayedRerender();
}
};
/*
don't call this directly. use executeRender instead
*/
Calendar.prototype.renderComponent = function (needsFull) {
var _a = this, state = _a.state, component = _a.component;
var viewType = state.viewType;
var viewSpec = this.viewSpecs[viewType];
var savedScroll = (needsFull && component) ? component.view.queryScroll() : null;
if (!viewSpec) {
throw new Error("View type \"" + viewType + "\" is not valid");
}
// if event sources are still loading and progressive rendering hasn't been enabled,
// keep rendering the last fully loaded set of events
var renderableEventStore = this.renderableEventStore =
(state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ?
this.renderableEventStore :
state.eventStore;
var eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options);
var eventUiBySource = this.buildEventUiBySource(state.eventSources);
var eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
if (needsFull || !component) {
if (component) {
component.freezeHeight(); // next component will unfreeze it
component.destroy();
}
component = this.component = new CalendarComponent({
calendar: this,
view: null,
dateEnv: this.dateEnv,
theme: this.theme,
options: this.optionsManager.computed
}, this.el);
this.isViewUpdated = true;
this.isDatesUpdated = true;
this.isEventsUpdated = true;
}
component.receiveProps(__assign({}, state, { viewSpec: viewSpec, dateProfile: state.dateProfile, dateProfileGenerator: this.dateProfileGenerators[viewType], eventStore: renderableEventStore, eventUiBases: eventUiBases, dateSelection: state.dateSelection, eventSelection: state.eventSelection, eventDrag: state.eventDrag, eventResize: state.eventResize }));
if (savedScroll) {
component.view.applyScroll(savedScroll, false);
}
if (this.isViewUpdated) {
this.isViewUpdated = false;
this.publiclyTrigger('viewSkeletonRender', [
{
view: component.view,
el: component.view.el
}
]);
}
if (this.isDatesUpdated) {
this.isDatesUpdated = false;
this.publiclyTrigger('datesRender', [
{
view: component.view,
el: component.view.el
}
]);
}
if (this.isEventsUpdated) {
this.isEventsUpdated = false;
}
this.releaseAfterSizingTriggers();
};
// Options
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.setOption = function (name, val) {
var _a;
this.mutateOptions((_a = {}, _a[name] = val, _a), [], true);
};
Calendar.prototype.getOption = function (name) {
return this.optionsManager.computed[name];
};
Calendar.prototype.opt = function (name) {
return this.optionsManager.computed[name];
};
Calendar.prototype.viewOpt = function (name) {
return this.viewOpts()[name];
};
Calendar.prototype.viewOpts = function () {
return this.viewSpecs[this.state.viewType].options;
};
/*
handles option changes (like a diff)
*/
Calendar.prototype.mutateOptions = function (updates, removals, isDynamic, deepEqual) {
var _this = this;
var changeHandlers = this.pluginSystem.hooks.optionChangeHandlers;
var normalUpdates = {};
var specialUpdates = {};
var oldDateEnv = this.dateEnv; // do this before handleOptions
var isTimeZoneDirty = false;
var isSizeDirty = false;
var anyDifficultOptions = Boolean(removals.length);
for (var name_1 in updates) {
if (changeHandlers[name_1]) {
specialUpdates[name_1] = updates[name_1];
}
else {
normalUpdates[name_1] = updates[name_1];
}
}
for (var name_2 in normalUpdates) {
if (/^(height|contentHeight|aspectRatio)$/.test(name_2)) {
isSizeDirty = true;
}
else if (/^(defaultDate|defaultView)$/.test(name_2)) ;
else {
anyDifficultOptions = true;
if (name_2 === 'timeZone') {
isTimeZoneDirty = true;
}
}
}
this.optionsManager.mutate(normalUpdates, removals, isDynamic);
if (anyDifficultOptions) {
this.handleOptions(this.optionsManager.computed);
this.needsFullRerender = true;
}
this.batchRendering(function () {
if (anyDifficultOptions) {
if (isTimeZoneDirty) {
_this.dispatch({
type: 'CHANGE_TIMEZONE',
oldDateEnv: oldDateEnv
});
}
/* HACK
has the same effect as calling this.requestRerender(true)
but recomputes the state's dateProfile
*/
_this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: _this.state.viewType
});
}
else if (isSizeDirty) {
_this.updateSize();
}
// special updates
if (deepEqual) {
for (var name_3 in specialUpdates) {
changeHandlers[name_3](specialUpdates[name_3], _this, deepEqual);
}
}
});
};
/*
rebuilds things based off of a complete set of refined options
*/
Calendar.prototype.handleOptions = function (options) {
var _this = this;
var pluginHooks = this.pluginSystem.hooks;
this.defaultAllDayEventDuration = createDuration(options.defaultAllDayEventDuration);
this.defaultTimedEventDuration = createDuration(options.defaultTimedEventDuration);
this.delayedRerender = this.buildDelayedRerender(options.rerenderDelay);
this.theme = this.buildTheme(options);
var available = this.parseRawLocales(options.locales);
this.availableRawLocales = available.map;
var locale = this.buildLocale(options.locale || available.defaultCode, available.map);
this.dateEnv = this.buildDateEnv(locale, options.timeZone, pluginHooks.namedTimeZonedImpl, options.firstDay, options.weekNumberCalculation, options.weekLabel, pluginHooks.cmdFormatter);
this.selectionConfig = this.buildSelectionConfig(options); // needs dateEnv. do after :(
// ineffecient to do every time?
this.viewSpecs = buildViewSpecs(pluginHooks.views, this.optionsManager);
// ineffecient to do every time?
this.dateProfileGenerators = mapHash(this.viewSpecs, function (viewSpec) {
return new viewSpec.class.prototype.dateProfileGeneratorClass(viewSpec, _this);
});
};
Calendar.prototype.getAvailableLocaleCodes = function () {
return Object.keys(this.availableRawLocales);
};
Calendar.prototype._buildSelectionConfig = function (rawOpts) {
return processScopedUiProps('select', rawOpts, this);
};
Calendar.prototype._buildEventUiSingleBase = function (rawOpts) {
if (rawOpts.editable) { // so 'editable' affected events
rawOpts = __assign({}, rawOpts, { eventEditable: true });
}
return processScopedUiProps('event', rawOpts, this);
};
// Trigger
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.hasPublicHandlers = function (name) {
return this.hasHandlers(name) ||
this.opt(name); // handler specified in options
};
Calendar.prototype.publiclyTrigger = function (name, args) {
var optHandler = this.opt(name);
this.triggerWith(name, this, args);
if (optHandler) {
return optHandler.apply(this, args);
}
};
Calendar.prototype.publiclyTriggerAfterSizing = function (name, args) {
var afterSizingTriggers = this.afterSizingTriggers;
(afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args);
};
Calendar.prototype.releaseAfterSizingTriggers = function () {
var afterSizingTriggers = this.afterSizingTriggers;
for (var name_4 in afterSizingTriggers) {
for (var _i = 0, _a = afterSizingTriggers[name_4]; _i < _a.length; _i++) {
var args = _a[_i];
this.publiclyTrigger(name_4, args);
}
}
this.afterSizingTriggers = {};
};
// View
// -----------------------------------------------------------------------------------------------------------------
// Returns a boolean about whether the view is okay to instantiate at some point
Calendar.prototype.isValidViewType = function (viewType) {
return Boolean(this.viewSpecs[viewType]);
};
Calendar.prototype.changeView = function (viewType, dateOrRange) {
var dateMarker = null;
if (dateOrRange) {
if (dateOrRange.start && dateOrRange.end) { // a range
this.optionsManager.mutate({ visibleRange: dateOrRange }, []); // will not rerender
this.handleOptions(this.optionsManager.computed); // ...but yuck
}
else { // a date
dateMarker = this.dateEnv.createMarker(dateOrRange); // just like gotoDate
}
}
this.unselect();
this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: viewType,
dateMarker: dateMarker
});
};
// Forces navigation to a view for the given date.
// `viewType` can be a specific view name or a generic one like "week" or "day".
// needs to change
Calendar.prototype.zoomTo = function (dateMarker, viewType) {
var spec;
viewType = viewType || 'day'; // day is default zoom
spec = this.viewSpecs[viewType] ||
this.getUnitViewSpec(viewType);
this.unselect();
if (spec) {
this.dispatch({
type: 'SET_VIEW_TYPE',
viewType: spec.type,
dateMarker: dateMarker
});
}
else {
this.dispatch({
type: 'SET_DATE',
dateMarker: dateMarker
});
}
};
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
// Preference is given to views that have corresponding buttons.
Calendar.prototype.getUnitViewSpec = function (unit) {
var component = this.component;
var viewTypes = [];
var i;
var spec;
// put views that have buttons first. there will be duplicates, but oh
if (component.header) {
viewTypes.push.apply(viewTypes, component.header.viewsWithButtons);
}
if (component.footer) {
viewTypes.push.apply(viewTypes, component.footer.viewsWithButtons);
}
for (var viewType in this.viewSpecs) {
viewTypes.push(viewType);
}
for (i = 0; i < viewTypes.length; i++) {
spec = this.viewSpecs[viewTypes[i]];
if (spec) {
if (spec.singleUnit === unit) {
return spec;
}
}
}
};
// Current Date
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.getInitialDate = function () {
var defaultDateInput = this.opt('defaultDate');
// compute the initial ambig-timezone date
if (defaultDateInput != null) {
return this.dateEnv.createMarker(defaultDateInput);
}
else {
return this.getNow(); // getNow already returns unzoned
}
};
Calendar.prototype.prev = function () {
this.unselect();
this.dispatch({ type: 'PREV' });
};
Calendar.prototype.next = function () {
this.unselect();
this.dispatch({ type: 'NEXT' });
};
Calendar.prototype.prevYear = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.addYears(this.state.currentDate, -1)
});
};
Calendar.prototype.nextYear = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.addYears(this.state.currentDate, 1)
});
};
Calendar.prototype.today = function () {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.getNow()
});
};
Calendar.prototype.gotoDate = function (zonedDateInput) {
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.createMarker(zonedDateInput)
});
};
Calendar.prototype.incrementDate = function (deltaInput) {
var delta = createDuration(deltaInput);
if (delta) { // else, warn about invalid input?
this.unselect();
this.dispatch({
type: 'SET_DATE',
dateMarker: this.dateEnv.add(this.state.currentDate, delta)
});
}
};
// for external API
Calendar.prototype.getDate = function () {
return this.dateEnv.toDate(this.state.currentDate);
};
// Date Formatting Utils
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.formatDate = function (d, formatter) {
var dateEnv = this.dateEnv;
return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter));
};
// `settings` is for formatter AND isEndExclusive
Calendar.prototype.formatRange = function (d0, d1, settings) {
var dateEnv = this.dateEnv;
return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings, this.opt('defaultRangeSeparator')), settings);
};
Calendar.prototype.formatIso = function (d, omitTime) {
var dateEnv = this.dateEnv;
return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime });
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.windowResize = function (ev) {
if (!this.isHandlingWindowResize &&
this.component && // why?
ev.target === window // not a jqui resize event
) {
this.isHandlingWindowResize = true;
this.updateSize();
this.publiclyTrigger('windowResize', [this.view]);
this.isHandlingWindowResize = false;
}
};
Calendar.prototype.updateSize = function () {
if (this.component) { // when?
this.component.updateSize(true);
}
};
// Component Registration
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.registerInteractiveComponent = function (component, settingsInput) {
var settings = parseInteractionSettings(component, settingsInput);
var DEFAULT_INTERACTIONS = [
EventClicking,
EventHovering
];
var interactionClasses = DEFAULT_INTERACTIONS.concat(this.pluginSystem.hooks.componentInteractions);
var interactions = interactionClasses.map(function (interactionClass) {
return new interactionClass(settings);
});
this.interactionsStore[component.uid] = interactions;
interactionSettingsStore[component.uid] = settings;
};
Calendar.prototype.unregisterInteractiveComponent = function (component) {
for (var _i = 0, _a = this.interactionsStore[component.uid]; _i < _a.length; _i++) {
var listener = _a[_i];
listener.destroy();
}
delete this.interactionsStore[component.uid];
delete interactionSettingsStore[component.uid];
};
// Date Selection / Event Selection / DayClick
// -----------------------------------------------------------------------------------------------------------------
// this public method receives start/end dates in any format, with any timezone
// NOTE: args were changed from v3
Calendar.prototype.select = function (dateOrObj, endDate) {
var selectionInput;
if (endDate == null) {
if (dateOrObj.start != null) {
selectionInput = dateOrObj;
}
else {
selectionInput = {
start: dateOrObj,
end: null
};
}
}
else {
selectionInput = {
start: dateOrObj,
end: endDate
};
}
var selection = parseDateSpan(selectionInput, this.dateEnv, createDuration({ days: 1 }) // TODO: cache this?
);
if (selection) { // throw parse error otherwise?
this.dispatch({ type: 'SELECT_DATES', selection: selection });
this.triggerDateSelect(selection);
}
};
// public method
Calendar.prototype.unselect = function (pev) {
if (this.state.dateSelection) {
this.dispatch({ type: 'UNSELECT_DATES' });
this.triggerDateUnselect(pev);
}
};
Calendar.prototype.triggerDateSelect = function (selection, pev) {
var arg = __assign({}, this.buildDateSpanApi(selection), { jsEvent: pev ? pev.origEvent : null, view: this.view });
this.publiclyTrigger('select', [arg]);
};
Calendar.prototype.triggerDateUnselect = function (pev) {
this.publiclyTrigger('unselect', [
{
jsEvent: pev ? pev.origEvent : null,
view: this.view
}
]);
};
// TODO: receive pev?
Calendar.prototype.triggerDateClick = function (dateSpan, dayEl, view, ev) {
var arg = __assign({}, this.buildDatePointApi(dateSpan), { dayEl: dayEl, jsEvent: ev, // Is this always a mouse event? See #4655
view: view });
this.publiclyTrigger('dateClick', [arg]);
};
Calendar.prototype.buildDatePointApi = function (dateSpan) {
var props = {};
for (var _i = 0, _a = this.pluginSystem.hooks.datePointTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(props, transform(dateSpan, this));
}
__assign(props, buildDatePointApi(dateSpan, this.dateEnv));
return props;
};
Calendar.prototype.buildDateSpanApi = function (dateSpan) {
var props = {};
for (var _i = 0, _a = this.pluginSystem.hooks.dateSpanTransforms; _i < _a.length; _i++) {
var transform = _a[_i];
__assign(props, transform(dateSpan, this));
}
__assign(props, buildDateSpanApi(dateSpan, this.dateEnv));
return props;
};
// Date Utils
// -----------------------------------------------------------------------------------------------------------------
// Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option
Calendar.prototype.getNow = function () {
var now = this.opt('now');
if (typeof now === 'function') {
now = now();
}
if (now == null) {
return this.dateEnv.createNowMarker();
}
return this.dateEnv.createMarker(now);
};
// Event-Date Utilities
// -----------------------------------------------------------------------------------------------------------------
// Given an event's allDay status and start date, return what its fallback end date should be.
// TODO: rename to computeDefaultEventEnd
Calendar.prototype.getDefaultEventEnd = function (allDay, marker) {
var end = marker;
if (allDay) {
end = startOfDay(end);
end = this.dateEnv.add(end, this.defaultAllDayEventDuration);
}
else {
end = this.dateEnv.add(end, this.defaultTimedEventDuration);
}
return end;
};
// Public Events API
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.addEvent = function (eventInput, sourceInput) {
if (eventInput instanceof EventApi) {
var def = eventInput._def;
var instance = eventInput._instance;
// not already present? don't want to add an old snapshot
if (!this.state.eventStore.defs[def.defId]) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore({ def: def, instance: instance }) // TODO: better util for two args?
});
}
return eventInput;
}
var sourceId;
if (sourceInput instanceof EventSourceApi) {
sourceId = sourceInput.internalEventSource.sourceId;
}
else if (sourceInput != null) {
var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function
if (!sourceApi) {
console.warn('Could not find an event source with ID "' + sourceInput + '"'); // TODO: test
return null;
}
else {
sourceId = sourceApi.internalEventSource.sourceId;
}
}
var tuple = parseEvent(eventInput, sourceId, this);
if (tuple) {
this.dispatch({
type: 'ADD_EVENTS',
eventStore: eventTupleToStore(tuple)
});
return new EventApi(this, tuple.def, tuple.def.recurringDef ? null : tuple.instance);
}
return null;
};
// TODO: optimize
Calendar.prototype.getEventById = function (id) {
var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances;
id = String(id);
for (var defId in defs) {
var def = defs[defId];
if (def.publicId === id) {
if (def.recurringDef) {
return new EventApi(this, def, null);
}
else {
for (var instanceId in instances) {
var instance = instances[instanceId];
if (instance.defId === def.defId) {
return new EventApi(this, def, instance);
}
}
}
}
}
return null;
};
Calendar.prototype.getEvents = function () {
var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances;
var eventApis = [];
for (var id in instances) {
var instance = instances[id];
var def = defs[instance.defId];
eventApis.push(new EventApi(this, def, instance));
}
return eventApis;
};
Calendar.prototype.removeAllEvents = function () {
this.dispatch({ type: 'REMOVE_ALL_EVENTS' });
};
Calendar.prototype.rerenderEvents = function () {
this.dispatch({ type: 'RESET_EVENTS' });
};
// Public Event Sources API
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.getEventSources = function () {
var sourceHash = this.state.eventSources;
var sourceApis = [];
for (var internalId in sourceHash) {
sourceApis.push(new EventSourceApi(this, sourceHash[internalId]));
}
return sourceApis;
};
Calendar.prototype.getEventSourceById = function (id) {
var sourceHash = this.state.eventSources;
id = String(id);
for (var sourceId in sourceHash) {
if (sourceHash[sourceId].publicId === id) {
return new EventSourceApi(this, sourceHash[sourceId]);
}
}
return null;
};
Calendar.prototype.addEventSource = function (sourceInput) {
if (sourceInput instanceof EventSourceApi) {
// not already present? don't want to add an old snapshot
if (!this.state.eventSources[sourceInput.internalEventSource.sourceId]) {
this.dispatch({
type: 'ADD_EVENT_SOURCES',
sources: [sourceInput.internalEventSource]
});
}
return sourceInput;
}
var eventSource = parseEventSource(sourceInput, this);
if (eventSource) { // TODO: error otherwise?
this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] });
return new EventSourceApi(this, eventSource);
}
return null;
};
Calendar.prototype.removeAllEventSources = function () {
this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' });
};
Calendar.prototype.refetchEvents = function () {
this.dispatch({ type: 'FETCH_EVENT_SOURCES' });
};
// Scroll
// -----------------------------------------------------------------------------------------------------------------
Calendar.prototype.scrollToTime = function (timeInput) {
var duration = createDuration(timeInput);
if (duration) {
this.component.view.scrollToDuration(duration);
}
};
return Calendar;
}());
EmitterMixin.mixInto(Calendar);
// for memoizers
// -----------------------------------------------------------------------------------------------------------------
function buildDateEnv(locale, timeZone, namedTimeZoneImpl, firstDay, weekNumberCalculation, weekLabel, cmdFormatter) {
return new DateEnv({
calendarSystem: 'gregory',
timeZone: timeZone,
namedTimeZoneImpl: namedTimeZoneImpl,
locale: locale,
weekNumberCalculation: weekNumberCalculation,
firstDay: firstDay,
weekLabel: weekLabel,
cmdFormatter: cmdFormatter
});
}
function buildTheme(calendarOptions) {
var themeClass = this.pluginSystem.hooks.themeClasses[calendarOptions.themeSystem] || StandardTheme;
return new themeClass(calendarOptions);
}
function buildDelayedRerender(wait) {
var func = this.tryRerender.bind(this);
if (wait != null) {
func = debounce(func, wait);
}
return func;
}
function buildEventUiBySource(eventSources) {
return mapHash(eventSources, function (eventSource) {
return eventSource.ui;
});
}
function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
var eventUiBases = { '': eventUiSingleBase };
for (var defId in eventDefs) {
var def = eventDefs[defId];
if (def.sourceId && eventUiBySource[def.sourceId]) {
eventUiBases[defId] = eventUiBySource[def.sourceId];
}
}
return eventUiBases;
}
var View = /** @class */ (function (_super) {
__extends(View, _super);
function View(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' }), true // isView (HACK)
) || this;
_this.renderDatesMem = memoizeRendering(_this.renderDatesWrap, _this.unrenderDatesWrap);
_this.renderBusinessHoursMem = memoizeRendering(_this.renderBusinessHours, _this.unrenderBusinessHours, [_this.renderDatesMem]);
_this.renderDateSelectionMem = memoizeRendering(_this.renderDateSelectionWrap, _this.unrenderDateSelectionWrap, [_this.renderDatesMem]);
_this.renderEventsMem = memoizeRendering(_this.renderEvents, _this.unrenderEvents, [_this.renderDatesMem]);
_this.renderEventSelectionMem = memoizeRendering(_this.renderEventSelectionWrap, _this.unrenderEventSelectionWrap, [_this.renderEventsMem]);
_this.renderEventDragMem = memoizeRendering(_this.renderEventDragWrap, _this.unrenderEventDragWrap, [_this.renderDatesMem]);
_this.renderEventResizeMem = memoizeRendering(_this.renderEventResizeWrap, _this.unrenderEventResizeWrap, [_this.renderDatesMem]);
_this.viewSpec = viewSpec;
_this.dateProfileGenerator = dateProfileGenerator;
_this.type = viewSpec.type;
_this.eventOrderSpecs = parseFieldSpecs(_this.opt('eventOrder'));
_this.nextDayThreshold = createDuration(_this.opt('nextDayThreshold'));
parentEl.appendChild(_this.el);
_this.initialize();
return _this;
}
View.prototype.initialize = function () {
};
Object.defineProperty(View.prototype, "activeStart", {
// Date Setting/Unsetting
// -----------------------------------------------------------------------------------------------------------------
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.activeRange.start);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "activeEnd", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.activeRange.end);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "currentStart", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.currentRange.start);
},
enumerable: true,
configurable: true
});
Object.defineProperty(View.prototype, "currentEnd", {
get: function () {
return this.dateEnv.toDate(this.props.dateProfile.currentRange.end);
},
enumerable: true,
configurable: true
});
// General Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.render = function (props) {
this.renderDatesMem(props.dateProfile);
this.renderBusinessHoursMem(props.businessHours);
this.renderDateSelectionMem(props.dateSelection);
this.renderEventsMem(props.eventStore);
this.renderEventSelectionMem(props.eventSelection);
this.renderEventDragMem(props.eventDrag);
this.renderEventResizeMem(props.eventResize);
};
View.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderDatesMem.unrender(); // should unrender everything else
};
// Sizing
// -----------------------------------------------------------------------------------------------------------------
View.prototype.updateSize = function (isResize, viewHeight, isAuto) {
var calendar = this.calendar;
if (isResize || // HACKS...
calendar.isViewUpdated ||
calendar.isDatesUpdated ||
calendar.isEventsUpdated) {
// sort of the catch-all sizing
// anything that might cause dimension changes
this.updateBaseSize(isResize, viewHeight, isAuto);
}
};
View.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) {
};
// Date Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderDatesWrap = function (dateProfile) {
this.renderDates(dateProfile);
this.addScroll({
duration: createDuration(this.opt('scrollTime'))
});
this.startNowIndicator(dateProfile); // shouldn't render yet because updateSize will be called soon
};
View.prototype.unrenderDatesWrap = function () {
this.stopNowIndicator();
this.unrenderDates();
};
View.prototype.renderDates = function (dateProfile) { };
View.prototype.unrenderDates = function () { };
// Business Hours
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderBusinessHours = function (businessHours) { };
View.prototype.unrenderBusinessHours = function () { };
// Date Selection
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderDateSelectionWrap = function (selection) {
if (selection) {
this.renderDateSelection(selection);
}
};
View.prototype.unrenderDateSelectionWrap = function (selection) {
if (selection) {
this.unrenderDateSelection(selection);
}
};
View.prototype.renderDateSelection = function (selection) { };
View.prototype.unrenderDateSelection = function (selection) { };
// Event Rendering
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEvents = function (eventStore) { };
View.prototype.unrenderEvents = function () { };
// util for subclasses
View.prototype.sliceEvents = function (eventStore, allDay) {
var props = this.props;
return sliceEventStore(eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? this.nextDayThreshold : null).fg;
};
View.prototype.computeEventDraggable = function (eventDef, eventUi) {
var transformers = this.calendar.pluginSystem.hooks.isDraggableTransformers;
var val = eventUi.startEditable;
for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) {
var transformer = transformers_1[_i];
val = transformer(val, eventDef, eventUi, this);
}
return val;
};
View.prototype.computeEventStartResizable = function (eventDef, eventUi) {
return eventUi.durationEditable && this.opt('eventResizableFromStart');
};
View.prototype.computeEventEndResizable = function (eventDef, eventUi) {
return eventUi.durationEditable;
};
// Event Selection
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventSelectionWrap = function (instanceId) {
if (instanceId) {
this.renderEventSelection(instanceId);
}
};
View.prototype.unrenderEventSelectionWrap = function (instanceId) {
if (instanceId) {
this.unrenderEventSelection(instanceId);
}
};
View.prototype.renderEventSelection = function (instanceId) { };
View.prototype.unrenderEventSelection = function (instanceId) { };
// Event Drag
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventDragWrap = function (state) {
if (state) {
this.renderEventDrag(state);
}
};
View.prototype.unrenderEventDragWrap = function (state) {
if (state) {
this.unrenderEventDrag(state);
}
};
View.prototype.renderEventDrag = function (state) { };
View.prototype.unrenderEventDrag = function (state) { };
// Event Resize
// -----------------------------------------------------------------------------------------------------------------
View.prototype.renderEventResizeWrap = function (state) {
if (state) {
this.renderEventResize(state);
}
};
View.prototype.unrenderEventResizeWrap = function (state) {
if (state) {
this.unrenderEventResize(state);
}
};
View.prototype.renderEventResize = function (state) { };
View.prototype.unrenderEventResize = function (state) { };
/* Now Indicator
------------------------------------------------------------------------------------------------------------------*/
// Immediately render the current time indicator and begins re-rendering it at an interval,
// which is defined by this.getNowIndicatorUnit().
// TODO: somehow do this for the current whole day's background too
View.prototype.startNowIndicator = function (dateProfile) {
var _this = this;
var dateEnv = this.dateEnv;
var unit;
var update;
var delay; // ms wait value
if (this.opt('nowIndicator')) {
unit = this.getNowIndicatorUnit(dateProfile);
if (unit) {
update = this.updateNowIndicator.bind(this);
this.initialNowDate = this.calendar.getNow();
this.initialNowQueriedMs = new Date().valueOf();
// wait until the beginning of the next interval
delay = dateEnv.add(dateEnv.startOf(this.initialNowDate, unit), createDuration(1, unit)).valueOf() - this.initialNowDate.valueOf();
// TODO: maybe always use setTimeout, waiting until start of next unit
this.nowIndicatorTimeoutID = setTimeout(function () {
_this.nowIndicatorTimeoutID = null;
update();
if (unit === 'second') {
delay = 1000; // every second
}
else {
delay = 1000 * 60; // otherwise, every minute
}
_this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
}, delay);
}
// rendering will be initiated in updateSize
}
};
// rerenders the now indicator, computing the new current time from the amount of time that has passed
// since the initial getNow call.
View.prototype.updateNowIndicator = function () {
if (this.props.dateProfile && // a way to determine if dates were rendered yet
this.initialNowDate // activated before?
) {
this.unrenderNowIndicator(); // won't unrender if unnecessary
this.renderNowIndicator(addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs));
this.isNowIndicatorRendered = true;
}
};
// Immediately unrenders the view's current time indicator and stops any re-rendering timers.
// Won't cause side effects if indicator isn't rendered.
View.prototype.stopNowIndicator = function () {
if (this.isNowIndicatorRendered) {
if (this.nowIndicatorTimeoutID) {
clearTimeout(this.nowIndicatorTimeoutID);
this.nowIndicatorTimeoutID = null;
}
if (this.nowIndicatorIntervalID) {
clearInterval(this.nowIndicatorIntervalID);
this.nowIndicatorIntervalID = null;
}
this.unrenderNowIndicator();
this.isNowIndicatorRendered = false;
}
};
View.prototype.getNowIndicatorUnit = function (dateProfile) {
// subclasses should implement
};
// Renders a current time indicator at the given datetime
View.prototype.renderNowIndicator = function (date) {
// SUBCLASSES MUST PASS TO CHILDREN!
};
// Undoes the rendering actions from renderNowIndicator
View.prototype.unrenderNowIndicator = function () {
// SUBCLASSES MUST PASS TO CHILDREN!
};
/* Scroller
------------------------------------------------------------------------------------------------------------------*/
View.prototype.addScroll = function (scroll) {
var queuedScroll = this.queuedScroll || (this.queuedScroll = {});
__assign(queuedScroll, scroll);
};
View.prototype.popScroll = function (isResize) {
this.applyQueuedScroll(isResize);
this.queuedScroll = null;
};
View.prototype.applyQueuedScroll = function (isResize) {
this.applyScroll(this.queuedScroll || {}, isResize);
};
View.prototype.queryScroll = function () {
var scroll = {};
if (this.props.dateProfile) { // dates rendered yet?
__assign(scroll, this.queryDateScroll());
}
return scroll;
};
View.prototype.applyScroll = function (scroll, isResize) {
var duration = scroll.duration;
if (duration != null) {
delete scroll.duration;
if (this.props.dateProfile) { // dates rendered yet?
__assign(scroll, this.computeDateScroll(duration));
}
}
if (this.props.dateProfile) { // dates rendered yet?
this.applyDateScroll(scroll);
}
};
View.prototype.computeDateScroll = function (duration) {
return {}; // subclasses must implement
};
View.prototype.queryDateScroll = function () {
return {}; // subclasses must implement
};
View.prototype.applyDateScroll = function (scroll) {
// subclasses must implement
};
// for API
View.prototype.scrollToDuration = function (duration) {
this.applyScroll({ duration: duration }, false);
};
return View;
}(DateComponent));
EmitterMixin.mixInto(View);
View.prototype.usesMinMaxTime = false;
View.prototype.dateProfileGeneratorClass = DateProfileGenerator;
var FgEventRenderer = /** @class */ (function () {
function FgEventRenderer(context) {
this.segs = [];
this.isSizeDirty = false;
this.context = context;
}
FgEventRenderer.prototype.renderSegs = function (segs, mirrorInfo) {
this.rangeUpdated(); // called too frequently :(
// render an `.el` on each seg
// returns a subset of the segs. segs that were actually rendered
segs = this.renderSegEls(segs, mirrorInfo);
this.segs = segs;
this.attachSegs(segs, mirrorInfo);
this.isSizeDirty = true;
this.context.view.triggerRenderedSegs(this.segs, Boolean(mirrorInfo));
};
FgEventRenderer.prototype.unrender = function (_segs, mirrorInfo) {
this.context.view.triggerWillRemoveSegs(this.segs, Boolean(mirrorInfo));
this.detachSegs(this.segs);
this.segs = [];
};
// Updates values that rely on options and also relate to range
FgEventRenderer.prototype.rangeUpdated = function () {
var options = this.context.options;
var displayEventTime;
var displayEventEnd;
this.eventTimeFormat = createFormatter(options.eventTimeFormat || this.computeEventTimeFormat(), options.defaultRangeSeparator);
displayEventTime = options.displayEventTime;
if (displayEventTime == null) {
displayEventTime = this.computeDisplayEventTime(); // might be based off of range
}
displayEventEnd = options.displayEventEnd;
if (displayEventEnd == null) {
displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
}
this.displayEventTime = displayEventTime;
this.displayEventEnd = displayEventEnd;
};
// Renders and assigns an `el` property for each foreground event segment.
// Only returns segments that successfully rendered.
FgEventRenderer.prototype.renderSegEls = function (segs, mirrorInfo) {
var html = '';
var i;
if (segs.length) { // don't build an empty html string
// build a large concatenation of event segment HTML
for (i = 0; i < segs.length; i++) {
html += this.renderSegHtml(segs[i], mirrorInfo);
}
// Grab individual elements from the combined HTML string. Use each as the default rendering.
// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
htmlToElements(html).forEach(function (el, i) {
var seg = segs[i];
if (el) {
seg.el = el;
}
});
segs = filterSegsViaEls(this.context.view, segs, Boolean(mirrorInfo));
}
return segs;
};
// Generic utility for generating the HTML classNames for an event segment's element
FgEventRenderer.prototype.getSegClasses = function (seg, isDraggable, isResizable, mirrorInfo) {
var classes = [
'fc-event',
seg.isStart ? 'fc-start' : 'fc-not-start',
seg.isEnd ? 'fc-end' : 'fc-not-end'
].concat(seg.eventRange.ui.classNames);
if (isDraggable) {
classes.push('fc-draggable');
}
if (isResizable) {
classes.push('fc-resizable');
}
if (mirrorInfo) {
classes.push('fc-mirror');
if (mirrorInfo.isDragging) {
classes.push('fc-dragging');
}
if (mirrorInfo.isResizing) {
classes.push('fc-resizing');
}
}
return classes;
};
// Compute the text that should be displayed on an event's element.
// `range` can be the Event object itself, or something range-like, with at least a `start`.
// If event times are disabled, or the event has no time, will return a blank string.
// If not specified, formatter will default to the eventTimeFormat setting,
// and displayEnd will default to the displayEventEnd setting.
FgEventRenderer.prototype.getTimeText = function (eventRange, formatter, displayEnd) {
var def = eventRange.def, instance = eventRange.instance;
return this._getTimeText(instance.range.start, def.hasEnd ? instance.range.end : null, def.allDay, formatter, displayEnd, instance.forcedStartTzo, instance.forcedEndTzo);
};
FgEventRenderer.prototype._getTimeText = function (start, end, allDay, formatter, displayEnd, forcedStartTzo, forcedEndTzo) {
var dateEnv = this.context.dateEnv;
if (formatter == null) {
formatter = this.eventTimeFormat;
}
if (displayEnd == null) {
displayEnd = this.displayEventEnd;
}
if (this.displayEventTime && !allDay) {
if (displayEnd && end) {
return dateEnv.formatRange(start, end, formatter, {
forcedStartTzo: forcedStartTzo,
forcedEndTzo: forcedEndTzo
});
}
else {
return dateEnv.format(start, formatter, {
forcedTzo: forcedStartTzo
});
}
}
return '';
};
FgEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true
};
};
FgEventRenderer.prototype.computeDisplayEventTime = function () {
return true;
};
FgEventRenderer.prototype.computeDisplayEventEnd = function () {
return true;
};
// Utility for generating event skin-related CSS properties
FgEventRenderer.prototype.getSkinCss = function (ui) {
return {
'background-color': ui.backgroundColor,
'border-color': ui.borderColor,
color: ui.textColor
};
};
FgEventRenderer.prototype.sortEventSegs = function (segs) {
var specs = this.context.view.eventOrderSpecs;
var objs = segs.map(buildSegCompareObj);
objs.sort(function (obj0, obj1) {
return compareByFieldSpecs(obj0, obj1, specs);
});
return objs.map(function (c) {
return c._seg;
});
};
FgEventRenderer.prototype.computeSizes = function (force) {
if (force || this.isSizeDirty) {
this.computeSegSizes(this.segs);
}
};
FgEventRenderer.prototype.assignSizes = function (force) {
if (force || this.isSizeDirty) {
this.assignSegSizes(this.segs);
this.isSizeDirty = false;
}
};
FgEventRenderer.prototype.computeSegSizes = function (segs) {
};
FgEventRenderer.prototype.assignSegSizes = function (segs) {
};
// Manipulation on rendered segs
FgEventRenderer.prototype.hideByHash = function (hash) {
if (hash) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (hash[seg.eventRange.instance.instanceId]) {
seg.el.style.visibility = 'hidden';
}
}
}
};
FgEventRenderer.prototype.showByHash = function (hash) {
if (hash) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (hash[seg.eventRange.instance.instanceId]) {
seg.el.style.visibility = '';
}
}
}
};
FgEventRenderer.prototype.selectByInstanceId = function (instanceId) {
if (instanceId) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
var eventInstance = seg.eventRange.instance;
if (eventInstance && eventInstance.instanceId === instanceId &&
seg.el // necessary?
) {
seg.el.classList.add('fc-selected');
}
}
}
};
FgEventRenderer.prototype.unselectByInstanceId = function (instanceId) {
if (instanceId) {
for (var _i = 0, _a = this.segs; _i < _a.length; _i++) {
var seg = _a[_i];
if (seg.el) { // necessary?
seg.el.classList.remove('fc-selected');
}
}
}
};
return FgEventRenderer;
}());
// returns a object with all primitive props that can be compared
function buildSegCompareObj(seg) {
var eventDef = seg.eventRange.def;
var range = seg.eventRange.instance.range;
var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events
var end = range.end ? range.end.valueOf() : 0; // "
return __assign({}, eventDef.extendedProps, eventDef, { id: eventDef.publicId, start: start,
end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg // for later retrieval
});
}
var FillRenderer = /** @class */ (function () {
function FillRenderer(context) {
this.fillSegTag = 'div';
this.dirtySizeFlags = {};
this.context = context;
this.containerElsByType = {};
this.segsByType = {};
}
FillRenderer.prototype.getSegsByType = function (type) {
return this.segsByType[type] || [];
};
FillRenderer.prototype.renderSegs = function (type, segs) {
var _a;
var renderedSegs = this.renderSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
var containerEls = this.attachSegs(type, renderedSegs);
if (containerEls) {
(_a = (this.containerElsByType[type] || (this.containerElsByType[type] = []))).push.apply(_a, containerEls);
}
this.segsByType[type] = renderedSegs;
if (type === 'bgEvent') {
this.context.view.triggerRenderedSegs(renderedSegs, false); // isMirror=false
}
this.dirtySizeFlags[type] = true;
};
// Unrenders a specific type of fill that is currently rendered on the grid
FillRenderer.prototype.unrender = function (type) {
var segs = this.segsByType[type];
if (segs) {
if (type === 'bgEvent') {
this.context.view.triggerWillRemoveSegs(segs, false); // isMirror=false
}
this.detachSegs(type, segs);
}
};
// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
// Only returns segments that successfully rendered.
FillRenderer.prototype.renderSegEls = function (type, segs) {
var _this = this;
var html = '';
var i;
if (segs.length) {
// build a large concatenation of segment HTML
for (i = 0; i < segs.length; i++) {
html += this.renderSegHtml(type, segs[i]);
}
// Grab individual elements from the combined HTML string. Use each as the default rendering.
// Then, compute the 'el' for each segment.
htmlToElements(html).forEach(function (el, i) {
var seg = segs[i];
if (el) {
seg.el = el;
}
});
if (type === 'bgEvent') {
segs = filterSegsViaEls(this.context.view, segs, false // isMirror. background events can never be mirror elements
);
}
// correct element type? (would be bad if a non-TD were inserted into a table for example)
segs = segs.filter(function (seg) {
return elementMatches(seg.el, _this.fillSegTag);
});
}
return segs;
};
// Builds the HTML needed for one fill segment. Generic enough to work with different types.
FillRenderer.prototype.renderSegHtml = function (type, seg) {
var css = null;
var classNames = [];
if (type !== 'highlight' && type !== 'businessHours') {
css = {
'background-color': seg.eventRange.ui.backgroundColor
};
}
if (type !== 'highlight') {
classNames = classNames.concat(seg.eventRange.ui.classNames);
}
if (type === 'businessHours') {
classNames.push('fc-bgevent');
}
else {
classNames.push('fc-' + type.toLowerCase());
}
return '<' + this.fillSegTag +
(classNames.length ? ' class="' + classNames.join(' ') + '"' : '') +
(css ? ' style="' + cssToStr(css) + '"' : '') +
'>' + this.fillSegTag + '>';
};
FillRenderer.prototype.detachSegs = function (type, segs) {
var containerEls = this.containerElsByType[type];
if (containerEls) {
containerEls.forEach(removeElement);
delete this.containerElsByType[type];
}
};
FillRenderer.prototype.computeSizes = function (force) {
for (var type in this.segsByType) {
if (force || this.dirtySizeFlags[type]) {
this.computeSegSizes(this.segsByType[type]);
}
}
};
FillRenderer.prototype.assignSizes = function (force) {
for (var type in this.segsByType) {
if (force || this.dirtySizeFlags[type]) {
this.assignSegSizes(this.segsByType[type]);
}
}
this.dirtySizeFlags = {};
};
FillRenderer.prototype.computeSegSizes = function (segs) {
};
FillRenderer.prototype.assignSegSizes = function (segs) {
};
return FillRenderer;
}());
var NamedTimeZoneImpl = /** @class */ (function () {
function NamedTimeZoneImpl(timeZoneName) {
this.timeZoneName = timeZoneName;
}
return NamedTimeZoneImpl;
}());
/*
An abstraction for a dragging interaction originating on an event.
Does higher-level things than PointerDragger, such as possibly:
- a "mirror" that moves with the pointer
- a minimum number of pixels or other criteria for a true drag to begin
subclasses must emit:
- pointerdown
- dragstart
- dragmove
- pointerup
- dragend
*/
var ElementDragging = /** @class */ (function () {
function ElementDragging(el) {
this.emitter = new EmitterMixin();
}
ElementDragging.prototype.destroy = function () {
};
ElementDragging.prototype.setMirrorIsVisible = function (bool) {
// optional if subclass doesn't want to support a mirror
};
ElementDragging.prototype.setMirrorNeedsRevert = function (bool) {
// optional if subclass doesn't want to support a mirror
};
ElementDragging.prototype.setAutoScrollEnabled = function (bool) {
// optional
};
return ElementDragging;
}());
function formatDate(dateInput, settings) {
if (settings === void 0) { settings = {}; }
var dateEnv = buildDateEnv$1(settings);
var formatter = createFormatter(settings);
var dateMeta = dateEnv.createMarkerMeta(dateInput);
if (!dateMeta) { // TODO: warning?
return '';
}
return dateEnv.format(dateMeta.marker, formatter, {
forcedTzo: dateMeta.forcedTzo
});
}
function formatRange(startInput, endInput, settings // mixture of env and formatter settings
) {
var dateEnv = buildDateEnv$1(typeof settings === 'object' && settings ? settings : {}); // pass in if non-null object
var formatter = createFormatter(settings, globalDefaults.defaultRangeSeparator);
var startMeta = dateEnv.createMarkerMeta(startInput);
var endMeta = dateEnv.createMarkerMeta(endInput);
if (!startMeta || !endMeta) { // TODO: warning?
return '';
}
return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
forcedStartTzo: startMeta.forcedTzo,
forcedEndTzo: endMeta.forcedTzo,
isEndExclusive: settings.isEndExclusive
});
}
// TODO: more DRY and optimized
function buildDateEnv$1(settings) {
var locale = buildLocale(settings.locale || 'en', parseRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
// ensure required settings
settings = __assign({ timeZone: globalDefaults.timeZone, calendarSystem: 'gregory' }, settings, { locale: locale });
return new DateEnv(settings);
}
var DRAG_META_PROPS = {
startTime: createDuration,
duration: createDuration,
create: Boolean,
sourceId: String
};
var DRAG_META_DEFAULTS = {
create: true
};
function parseDragMeta(raw) {
var leftoverProps = {};
var refined = refineProps(raw, DRAG_META_PROPS, DRAG_META_DEFAULTS, leftoverProps);
refined.leftoverProps = leftoverProps;
return refined;
}
// Computes a default column header formatting string if `colFormat` is not explicitly defined
function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) {
// if more than one week row, or if there are a lot of columns with not much space,
// put just the day numbers will be in each cell
if (!datesRepDistinctDays || dayCnt > 10) {
return { weekday: 'short' }; // "Sat"
}
else if (dayCnt > 1) {
return { weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }; // "Sat 11/12"
}
else {
return { weekday: 'long' }; // "Saturday"
}
}
function renderDateCell(dateMarker, dateProfile, datesRepDistinctDays, colCnt, colHeadFormat, context, colspan, otherAttrs) {
var view = context.view, dateEnv = context.dateEnv, theme = context.theme, options = context.options;
var isDateValid = rangeContainsMarker(dateProfile.activeRange, dateMarker); // TODO: called too frequently. cache somehow.
var classNames = [
'fc-day-header',
theme.getClass('widgetHeader')
];
var innerHtml;
if (typeof options.columnHeaderHtml === 'function') {
innerHtml = options.columnHeaderHtml(dateEnv.toDate(dateMarker));
}
else if (typeof options.columnHeaderText === 'function') {
innerHtml = htmlEscape(options.columnHeaderText(dateEnv.toDate(dateMarker)));
}
else {
innerHtml = htmlEscape(dateEnv.format(dateMarker, colHeadFormat));
}
// if only one row of days, the classNames on the header can represent the specific days beneath
if (datesRepDistinctDays) {
classNames = classNames.concat(
// includes the day-of-week class
// noThemeHighlight=true (don't highlight the header)
getDayClasses(dateMarker, dateProfile, context, true));
}
else {
classNames.push('fc-' + DAY_IDS[dateMarker.getUTCDay()]); // only add the day-of-week class
}
return '' +
'
var DayHeader = /** @class */ (function (_super) {
__extends(DayHeader, _super);
function DayHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.innerHTML = ''; // because might be nbsp
parentEl.appendChild(_this.el = htmlToElement('
var DaySeries = /** @class */ (function () {
function DaySeries(range, dateProfileGenerator) {
var date = range.start;
var end = range.end;
var indices = [];
var dates = [];
var dayIndex = -1;
while (date < end) { // loop each day from start to end
if (dateProfileGenerator.isHiddenDay(date)) {
indices.push(dayIndex + 0.5); // mark that it's between indices
}
else {
dayIndex++;
indices.push(dayIndex);
dates.push(date);
}
date = addDays(date, 1);
}
this.dates = dates;
this.indices = indices;
this.cnt = dates.length;
}
DaySeries.prototype.sliceRange = function (range) {
var firstIndex = this.getDateDayIndex(range.start); // inclusive first index
var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index
var clippedFirstIndex = Math.max(0, firstIndex);
var clippedLastIndex = Math.min(this.cnt - 1, lastIndex);
// deal with in-between indices
clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell
clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell
if (clippedFirstIndex <= clippedLastIndex) {
return {
firstIndex: clippedFirstIndex,
lastIndex: clippedLastIndex,
isStart: firstIndex === clippedFirstIndex,
isEnd: lastIndex === clippedLastIndex
};
}
else {
return null;
}
};
// Given a date, returns its chronolocial cell-index from the first cell of the grid.
// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
// If before the first offset, returns a negative number.
// If after the last offset, returns an offset past the last cell offset.
// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
DaySeries.prototype.getDateDayIndex = function (date) {
var indices = this.indices;
var dayOffset = Math.floor(diffDays(this.dates[0], date));
if (dayOffset < 0) {
return indices[0] - 1;
}
else if (dayOffset >= indices.length) {
return indices[indices.length - 1] + 1;
}
else {
return indices[dayOffset];
}
};
return DaySeries;
}());
var DayTable = /** @class */ (function () {
function DayTable(daySeries, breakOnWeeks) {
var dates = daySeries.dates;
var daysPerRow;
var firstDay;
var rowCnt;
if (breakOnWeeks) {
// count columns until the day-of-week repeats
firstDay = dates[0].getUTCDay();
for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow++) {
if (dates[daysPerRow].getUTCDay() === firstDay) {
break;
}
}
rowCnt = Math.ceil(dates.length / daysPerRow);
}
else {
rowCnt = 1;
daysPerRow = dates.length;
}
this.rowCnt = rowCnt;
this.colCnt = daysPerRow;
this.daySeries = daySeries;
this.cells = this.buildCells();
this.headerDates = this.buildHeaderDates();
}
DayTable.prototype.buildCells = function () {
var rows = [];
for (var row = 0; row < this.rowCnt; row++) {
var cells = [];
for (var col = 0; col < this.colCnt; col++) {
cells.push(this.buildCell(row, col));
}
rows.push(cells);
}
return rows;
};
DayTable.prototype.buildCell = function (row, col) {
return {
date: this.daySeries.dates[row * this.colCnt + col]
};
};
DayTable.prototype.buildHeaderDates = function () {
var dates = [];
for (var col = 0; col < this.colCnt; col++) {
dates.push(this.cells[0][col].date);
}
return dates;
};
DayTable.prototype.sliceRange = function (range) {
var colCnt = this.colCnt;
var seriesSeg = this.daySeries.sliceRange(range);
var segs = [];
if (seriesSeg) {
var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex;
var index = firstIndex;
while (index <= lastIndex) {
var row = Math.floor(index / colCnt);
var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1);
segs.push({
row: row,
firstCol: index % colCnt,
lastCol: (nextIndex - 1) % colCnt,
isStart: seriesSeg.isStart && index === firstIndex,
isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex
});
index = nextIndex;
}
}
return segs;
};
return DayTable;
}());
var Slicer = /** @class */ (function () {
function Slicer() {
this.sliceBusinessHours = memoize(this._sliceBusinessHours);
this.sliceDateSelection = memoize(this._sliceDateSpan);
this.sliceEventStore = memoize(this._sliceEventStore);
this.sliceEventDrag = memoize(this._sliceInteraction);
this.sliceEventResize = memoize(this._sliceInteraction);
}
Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 4; _i < arguments.length; _i++) {
extraArgs[_i - 4] = arguments[_i];
}
var eventUiBases = props.eventUiBases;
var eventSegs = this.sliceEventStore.apply(this, [props.eventStore, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs));
return {
dateSelectionSegs: this.sliceDateSelection.apply(this, [props.dateSelection, eventUiBases, component].concat(extraArgs)),
businessHourSegs: this.sliceBusinessHours.apply(this, [props.businessHours, dateProfile, nextDayThreshold, component].concat(extraArgs)),
fgEventSegs: eventSegs.fg,
bgEventSegs: eventSegs.bg,
eventDrag: this.sliceEventDrag.apply(this, [props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)),
eventResize: this.sliceEventResize.apply(this, [props.eventResize, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)),
eventSelection: props.eventSelection
}; // TODO: give interactionSegs?
};
Slicer.prototype.sliceNowDate = function (// does not memoize
date, component) {
var extraArgs = [];
for (var _i = 2; _i < arguments.length; _i++) {
extraArgs[_i - 2] = arguments[_i];
}
return this._sliceDateSpan.apply(this, [{ range: { start: date, end: addMs(date, 1) }, allDay: false },
{},
component].concat(extraArgs));
};
Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 4; _i < arguments.length; _i++) {
extraArgs[_i - 4] = arguments[_i];
}
if (!businessHours) {
return [];
}
return this._sliceEventStore.apply(this, [expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), component.calendar),
{},
dateProfile,
nextDayThreshold,
component].concat(extraArgs)).bg;
};
Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 5; _i < arguments.length; _i++) {
extraArgs[_i - 5] = arguments[_i];
}
if (eventStore) {
var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
bg: this.sliceEventRanges(rangeRes.bg, component, extraArgs),
fg: this.sliceEventRanges(rangeRes.fg, component, extraArgs)
};
}
else {
return { bg: [], fg: [] };
}
};
Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold, component) {
var extraArgs = [];
for (var _i = 5; _i < arguments.length; _i++) {
extraArgs[_i - 5] = arguments[_i];
}
if (!interaction) {
return null;
}
var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
return {
segs: this.sliceEventRanges(rangeRes.fg, component, extraArgs),
affectedInstances: interaction.affectedEvents.instances,
isEvent: interaction.isEvent,
sourceSeg: interaction.origSeg
};
};
Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, component) {
var extraArgs = [];
for (var _i = 3; _i < arguments.length; _i++) {
extraArgs[_i - 3] = arguments[_i];
}
if (!dateSpan) {
return [];
}
var eventRange = fabricateEventRange(dateSpan, eventUiBases, component.calendar);
var segs = this.sliceRange.apply(this, [dateSpan.range].concat(extraArgs));
for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) {
var seg = segs_1[_a];
seg.component = component;
seg.eventRange = eventRange;
}
return segs;
};
/*
"complete" seg means it has component and eventRange
*/
Slicer.prototype.sliceEventRanges = function (eventRanges, component, // TODO: kill
extraArgs) {
var segs = [];
for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) {
var eventRange = eventRanges_1[_i];
segs.push.apply(segs, this.sliceEventRange(eventRange, component, extraArgs));
}
return segs;
};
/*
"complete" seg means it has component and eventRange
*/
Slicer.prototype.sliceEventRange = function (eventRange, component, // TODO: kill
extraArgs) {
var segs = this.sliceRange.apply(this, [eventRange.range].concat(extraArgs));
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
seg.component = component;
seg.eventRange = eventRange;
seg.isStart = eventRange.isStart && seg.isStart;
seg.isEnd = eventRange.isEnd && seg.isEnd;
}
return segs;
};
return Slicer;
}());
/*
for incorporating minTime/maxTime if appropriate
TODO: should be part of DateProfile!
TimelineDateProfile already does this btw
*/
function computeActiveRange(dateProfile, isComponentAllDay) {
var range = dateProfile.activeRange;
if (isComponentAllDay) {
return range;
}
return {
start: addMs(range.start, dateProfile.minTime.milliseconds),
end: addMs(range.end, dateProfile.maxTime.milliseconds - 864e5) // 864e5 = ms in a day
};
}
// exports
// --------------------------------------------------------------------------------------------------
var version = '4.3.1';
exports.Calendar = Calendar;
exports.Component = Component;
exports.DateComponent = DateComponent;
exports.DateEnv = DateEnv;
exports.DateProfileGenerator = DateProfileGenerator;
exports.DayHeader = DayHeader;
exports.DaySeries = DaySeries;
exports.DayTable = DayTable;
exports.ElementDragging = ElementDragging;
exports.ElementScrollController = ElementScrollController;
exports.EmitterMixin = EmitterMixin;
exports.EventApi = EventApi;
exports.FgEventRenderer = FgEventRenderer;
exports.FillRenderer = FillRenderer;
exports.Interaction = Interaction;
exports.Mixin = Mixin;
exports.NamedTimeZoneImpl = NamedTimeZoneImpl;
exports.PositionCache = PositionCache;
exports.ScrollComponent = ScrollComponent;
exports.ScrollController = ScrollController;
exports.Slicer = Slicer;
exports.Splitter = Splitter;
exports.Theme = Theme;
exports.View = View;
exports.WindowScrollController = WindowScrollController;
exports.addDays = addDays;
exports.addDurations = addDurations;
exports.addMs = addMs;
exports.addWeeks = addWeeks;
exports.allowContextMenu = allowContextMenu;
exports.allowSelection = allowSelection;
exports.appendToElement = appendToElement;
exports.applyAll = applyAll;
exports.applyMutationToEventStore = applyMutationToEventStore;
exports.applyStyle = applyStyle;
exports.applyStyleProp = applyStyleProp;
exports.asRoughMinutes = asRoughMinutes;
exports.asRoughMs = asRoughMs;
exports.asRoughSeconds = asRoughSeconds;
exports.buildGotoAnchorHtml = buildGotoAnchorHtml;
exports.buildSegCompareObj = buildSegCompareObj;
exports.capitaliseFirstLetter = capitaliseFirstLetter;
exports.combineEventUis = combineEventUis;
exports.compareByFieldSpec = compareByFieldSpec;
exports.compareByFieldSpecs = compareByFieldSpecs;
exports.compareNumbers = compareNumbers;
exports.compensateScroll = compensateScroll;
exports.computeClippingRect = computeClippingRect;
exports.computeEdges = computeEdges;
exports.computeFallbackHeaderFormat = computeFallbackHeaderFormat;
exports.computeHeightAndMargins = computeHeightAndMargins;
exports.computeInnerRect = computeInnerRect;
exports.computeRect = computeRect;
exports.computeVisibleDayRange = computeVisibleDayRange;
exports.config = config;
exports.constrainPoint = constrainPoint;
exports.createDuration = createDuration;
exports.createElement = createElement;
exports.createEmptyEventStore = createEmptyEventStore;
exports.createEventInstance = createEventInstance;
exports.createFormatter = createFormatter;
exports.createPlugin = createPlugin;
exports.cssToStr = cssToStr;
exports.debounce = debounce;
exports.diffDates = diffDates;
exports.diffDayAndTime = diffDayAndTime;
exports.diffDays = diffDays;
exports.diffPoints = diffPoints;
exports.diffWeeks = diffWeeks;
exports.diffWholeDays = diffWholeDays;
exports.diffWholeWeeks = diffWholeWeeks;
exports.disableCursor = disableCursor;
exports.distributeHeight = distributeHeight;
exports.elementClosest = elementClosest;
exports.elementMatches = elementMatches;
exports.enableCursor = enableCursor;
exports.eventTupleToStore = eventTupleToStore;
exports.filterEventStoreDefs = filterEventStoreDefs;
exports.filterHash = filterHash;
exports.findChildren = findChildren;
exports.findElements = findElements;
exports.flexibleCompare = flexibleCompare;
exports.forceClassName = forceClassName;
exports.formatDate = formatDate;
exports.formatIsoTimeString = formatIsoTimeString;
exports.formatRange = formatRange;
exports.getAllDayHtml = getAllDayHtml;
exports.getClippingParents = getClippingParents;
exports.getDayClasses = getDayClasses;
exports.getElSeg = getElSeg;
exports.getRectCenter = getRectCenter;
exports.getRelevantEvents = getRelevantEvents;
exports.globalDefaults = globalDefaults;
exports.greatestDurationDenominator = greatestDurationDenominator;
exports.hasBgRendering = hasBgRendering;
exports.htmlEscape = htmlEscape;
exports.htmlToElement = htmlToElement;
exports.insertAfterElement = insertAfterElement;
exports.interactionSettingsStore = interactionSettingsStore;
exports.interactionSettingsToStore = interactionSettingsToStore;
exports.intersectRanges = intersectRanges;
exports.intersectRects = intersectRects;
exports.isArraysEqual = isArraysEqual;
exports.isDateSpansEqual = isDateSpansEqual;
exports.isInt = isInt;
exports.isInteractionValid = isInteractionValid;
exports.isMultiDayRange = isMultiDayRange;
exports.isPropsEqual = isPropsEqual;
exports.isPropsValid = isPropsValid;
exports.isSingleDay = isSingleDay;
exports.isValidDate = isValidDate;
exports.listenBySelector = listenBySelector;
exports.mapHash = mapHash;
exports.matchCellWidths = matchCellWidths;
exports.memoize = memoize;
exports.memoizeOutput = memoizeOutput;
exports.memoizeRendering = memoizeRendering;
exports.mergeEventStores = mergeEventStores;
exports.multiplyDuration = multiplyDuration;
exports.padStart = padStart;
exports.parseBusinessHours = parseBusinessHours;
exports.parseDragMeta = parseDragMeta;
exports.parseEventDef = parseEventDef;
exports.parseFieldSpecs = parseFieldSpecs;
exports.parseMarker = parse;
exports.pointInsideRect = pointInsideRect;
exports.prependToElement = prependToElement;
exports.preventContextMenu = preventContextMenu;
exports.preventDefault = preventDefault;
exports.preventSelection = preventSelection;
exports.processScopedUiProps = processScopedUiProps;
exports.rangeContainsMarker = rangeContainsMarker;
exports.rangeContainsRange = rangeContainsRange;
exports.rangesEqual = rangesEqual;
exports.rangesIntersect = rangesIntersect;
exports.refineProps = refineProps;
exports.removeElement = removeElement;
exports.removeExact = removeExact;
exports.renderDateCell = renderDateCell;
exports.requestJson = requestJson;
exports.sliceEventStore = sliceEventStore;
exports.startOfDay = startOfDay;
exports.subtractInnerElHeight = subtractInnerElHeight;
exports.translateRect = translateRect;
exports.uncompensateScroll = uncompensateScroll;
exports.undistributeHeight = undistributeHeight;
exports.unpromisify = unpromisify;
exports.version = version;
exports.whenTransitionDone = whenTransitionDone;
exports.wholeDivideDurations = wholeDivideDurations;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/core' {
export const version = "<%= version %>";
export { OptionsInput } from '@fullcalendar/core/types/input-types';
export { EventInput, EventDef, EventDefHash, EventInstance, EventInstanceHash, parseEventDef, createEventInstance, EventTuple } from '@fullcalendar/core/structs/event';
export { BusinessHoursInput, parseBusinessHours } from '@fullcalendar/core/structs/business-hours';
export { applyAll, debounce, padStart, isInt, capitaliseFirstLetter, parseFieldSpecs, compareByFieldSpecs, compareByFieldSpec, flexibleCompare, computeVisibleDayRange, refineProps, matchCellWidths, uncompensateScroll, compensateScroll, subtractInnerElHeight, isMultiDayRange, distributeHeight, undistributeHeight, preventSelection, allowSelection, preventContextMenu, allowContextMenu, compareNumbers, enableCursor, disableCursor, diffDates } from '@fullcalendar/core/util/misc';
export { htmlEscape, cssToStr } from '@fullcalendar/core/util/html';
export { removeExact, isArraysEqual } from '@fullcalendar/core/util/array';
export { memoize, memoizeOutput } from '@fullcalendar/core/util/memoize';
export { memoizeRendering, MemoizedRendering } from '@fullcalendar/core/component/memoized-rendering';
export { intersectRects, Rect, pointInsideRect, constrainPoint, getRectCenter, diffPoints, Point, translateRect } from '@fullcalendar/core/util/geom';
export { mapHash, filterHash, isPropsEqual } from '@fullcalendar/core/util/object';
export { findElements, findChildren, htmlToElement, createElement, insertAfterElement, prependToElement, removeElement, appendToElement, applyStyle, applyStyleProp, elementMatches, elementClosest, forceClassName } from '@fullcalendar/core/util/dom-manip';
export { EventStore, filterEventStoreDefs, createEmptyEventStore, mergeEventStores, getRelevantEvents, eventTupleToStore } from '@fullcalendar/core/structs/event-store';
export { EventUiHash, EventUi, processScopedUiProps, combineEventUis } from '@fullcalendar/core/component/event-ui';
export { default as Splitter, SplittableProps } from '@fullcalendar/core/component/event-splitting';
export { buildGotoAnchorHtml, getAllDayHtml, getDayClasses } from '@fullcalendar/core/component/date-rendering';
export { preventDefault, listenBySelector, whenTransitionDone } from '@fullcalendar/core/util/dom-event';
export { computeInnerRect, computeEdges, computeHeightAndMargins, getClippingParents, computeClippingRect, computeRect } from '@fullcalendar/core/util/dom-geom';
export { unpromisify } from '@fullcalendar/core/util/promise';
export { default as EmitterMixin, EmitterInterface } from '@fullcalendar/core/common/EmitterMixin';
export { DateRange, rangeContainsMarker, intersectRanges, rangesEqual, rangesIntersect, rangeContainsRange } from '@fullcalendar/core/datelib/date-range';
export { default as Mixin } from '@fullcalendar/core/common/Mixin';
export { default as PositionCache } from '@fullcalendar/core/common/PositionCache';
export { default as ScrollComponent, ScrollbarWidths } from '@fullcalendar/core/common/ScrollComponent';
export { ScrollController, ElementScrollController, WindowScrollController } from '@fullcalendar/core/common/scroll-controller';
export { default as Theme } from '@fullcalendar/core/theme/Theme';
export { default as Component, ComponentContext } from '@fullcalendar/core/component/Component';
export { default as DateComponent, Seg, EventSegUiInteractionState } from '@fullcalendar/core/component/DateComponent';
export { default as Calendar, DatePointTransform, DateSpanTransform, DateSelectionApi } from '@fullcalendar/core/Calendar';
export { default as View, ViewProps } from '@fullcalendar/core/View';
export { default as FgEventRenderer, buildSegCompareObj } from '@fullcalendar/core/component/renderers/FgEventRenderer';
export { default as FillRenderer } from '@fullcalendar/core/component/renderers/FillRenderer';
export { default as DateProfileGenerator, DateProfile } from '@fullcalendar/core/DateProfileGenerator';
export { ViewDef } from '@fullcalendar/core/structs/view-def';
export { ViewSpec } from '@fullcalendar/core/structs/view-spec';
export { DateSpan, DateSpanApi, DatePointApi, isDateSpansEqual } from '@fullcalendar/core/structs/date-span';
export { DateMarker, addDays, startOfDay, addMs, addWeeks, diffWeeks, diffWholeWeeks, diffWholeDays, diffDayAndTime, diffDays, isValidDate } from '@fullcalendar/core/datelib/marker';
export { Duration, createDuration, isSingleDay, multiplyDuration, addDurations, asRoughMinutes, asRoughSeconds, asRoughMs, wholeDivideDurations, greatestDurationDenominator } from '@fullcalendar/core/datelib/duration';
export { DateEnv, DateMarkerMeta } from '@fullcalendar/core/datelib/env';
export { DateFormatter, createFormatter, VerboseFormattingArg, formatIsoTimeString } from '@fullcalendar/core/datelib/formatting';
export { NamedTimeZoneImpl } from '@fullcalendar/core/datelib/timezone';
export { parse as parseMarker } from '@fullcalendar/core/datelib/parsing';
export { EventSourceDef, EventSource, EventSourceHash } from '@fullcalendar/core/structs/event-source';
export { Interaction, InteractionSettings, interactionSettingsToStore, interactionSettingsStore, InteractionSettingsStore } from '@fullcalendar/core/interactions/interaction';
export { PointerDragEvent } from '@fullcalendar/core/interactions/pointer';
export { Hit } from '@fullcalendar/core/interactions/hit';
export { dateSelectionJoinTransformer } from '@fullcalendar/core/interactions/date-selecting';
export { eventDragMutationMassager, EventDropTransformers } from '@fullcalendar/core/interactions/event-dragging';
export { EventResizeJoinTransforms } from '@fullcalendar/core/interactions/event-resizing';
export { default as ElementDragging } from '@fullcalendar/core/interactions/ElementDragging';
export { formatDate, formatRange } from '@fullcalendar/core/formatting-api';
export { globalDefaults, config } from '@fullcalendar/core/options';
export { RecurringType, ParsedRecurring } from '@fullcalendar/core/structs/recurring-event';
export { DragMetaInput, DragMeta, parseDragMeta } from '@fullcalendar/core/structs/drag-meta';
export { createPlugin, PluginDef, PluginDefInput, ViewPropsTransformer, ViewContainerModifier } from '@fullcalendar/core/plugin-system';
export { reducerFunc, Action, CalendarState } from '@fullcalendar/core/reducers/types';
export { CalendarComponentProps } from '@fullcalendar/core/CalendarComponent';
export { default as DayHeader } from '@fullcalendar/core/common/DayHeader';
export { computeFallbackHeaderFormat, renderDateCell } from '@fullcalendar/core/common/table-utils';
export { default as DaySeries } from '@fullcalendar/core/common/DaySeries';
export { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
export { EventRenderRange, sliceEventStore, hasBgRendering, getElSeg } from '@fullcalendar/core/component/event-rendering';
export { default as DayTable, DayTableSeg, DayTableCell } from '@fullcalendar/core/common/DayTable';
export { default as Slicer, SlicedProps } from '@fullcalendar/core/common/slicing-utils';
export { EventMutation, applyMutationToEventStore } from '@fullcalendar/core/structs/event-mutation';
export { Constraint, ConstraintInput, AllowFunc, isPropsValid, isInteractionValid } from '@fullcalendar/core/validation';
export { default as EventApi } from '@fullcalendar/core/api/EventApi';
export { default as requestJson } from '@fullcalendar/core/util/requestJson';
}
declare module '@fullcalendar/core/types/input-types' {
import View from '@fullcalendar/core/View';
import { EventSourceInput, EventInputTransformer } from '@fullcalendar/core/structs/event-source';
import { Duration, DurationInput } from '@fullcalendar/core/datelib/duration';
import { DateInput } from '@fullcalendar/core/datelib/env';
import { FormatterInput } from '@fullcalendar/core/datelib/formatting';
import { DateRangeInput } from '@fullcalendar/core/datelib/date-range';
import { BusinessHoursInput } from '@fullcalendar/core/structs/business-hours';
import EventApi from '@fullcalendar/core/api/EventApi';
import { AllowFunc, ConstraintInput, OverlapFunc } from '@fullcalendar/core/validation';
import { PluginDef } from '@fullcalendar/core/plugin-system';
import { LocaleSingularArg, RawLocale } from '@fullcalendar/core/datelib/locale';
export interface ToolbarInput {
left?: string;
center?: string;
right?: string;
}
export interface CustomButtonInput {
text: string;
icon?: string;
themeIcon?: string;
bootstrapFontAwesome?: string;
click(element: HTMLElement): void;
}
export interface ButtonIconsInput {
prev?: string;
next?: string;
prevYear?: string;
nextYear?: string;
}
export interface ButtonTextCompoundInput {
prev?: string;
next?: string;
prevYear?: string;
nextYear?: string;
today?: string;
month?: string;
week?: string;
day?: string;
[viewId: string]: string | undefined;
}
export interface EventSegment {
event: EventApi;
start: Date;
end: Date;
isStart: boolean;
isEnd: boolean;
}
export interface CellInfo {
date: Date;
dayEl: HTMLElement;
moreEl: HTMLElement;
segs: EventSegment[];
hiddenSegs: EventSegment[];
}
export interface DropInfo {
start: Date;
end: Date;
}
export type EventHandlerName = '_init' | 'selectAllow' | 'eventAllow' | 'eventDataTransform' | 'datesRender' | 'datesDestroy' | 'dayRender' | 'windowResize' | 'dateClick' | 'eventClick' | 'eventMouseEnter' | 'eventMouseLeave' | 'select' | 'unselect' | 'loading' | 'eventRender' | 'eventPositioned' | '_eventsPositioned' | 'eventDestroy' | 'eventDragStart' | 'eventDragStop' | 'eventDrop' | '_destroyed' | 'drop' | 'eventResizeStart' | 'eventResizeStop' | 'eventResize' | 'eventReceive' | 'eventLeave' | 'viewSkeletonRender' | 'viewSkeletonDestroy' | '_noEventDrop' | '_noEventResize' | 'eventLimitClick' | 'resourceRender';
export type EventHandlerArgs = Parameters any>>;
export type EventHandlerArg = EventHandlerArgs[0];
export interface OptionsInputBase {
header?: boolean | ToolbarInput;
footer?: boolean | ToolbarInput;
customButtons?: {
[name: string]: CustomButtonInput;
};
buttonIcons?: boolean | ButtonIconsInput;
themeSystem?: 'standard' | string;
bootstrapFontAwesome?: boolean | ButtonIconsInput;
firstDay?: number;
dir?: 'ltr' | 'rtl' | 'auto';
weekends?: boolean;
hiddenDays?: number[];
fixedWeekCount?: boolean;
weekNumbers?: boolean;
weekNumbersWithinDays?: boolean;
weekNumberCalculation?: 'local' | 'ISO' | ((m: Date) => number);
businessHours?: BusinessHoursInput;
showNonCurrentDates?: boolean;
height?: number | 'auto' | 'parent' | (() => number);
contentHeight?: number | 'auto' | (() => number);
aspectRatio?: number;
handleWindowResize?: boolean;
windowResizeDelay?: number;
eventLimit?: boolean | number;
eventLimitClick?: 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string | ((arg: {
date: Date;
allDay: boolean;
dayEl: HTMLElement;
moreEl: HTMLElement;
segs: any[];
hiddenSegs: any[];
jsEvent: MouseEvent;
view: View;
}) => void);
timeZone?: string | boolean;
now?: DateInput | (() => DateInput);
defaultView?: string;
allDaySlot?: boolean;
allDayText?: string;
slotDuration?: DurationInput;
slotLabelFormat?: FormatterInput;
slotLabelInterval?: DurationInput;
snapDuration?: DurationInput;
scrollTime?: DurationInput;
minTime?: DurationInput;
maxTime?: DurationInput;
slotEventOverlap?: boolean;
listDayFormat?: FormatterInput | boolean;
listDayAltFormat?: FormatterInput | boolean;
noEventsMessage?: string;
defaultDate?: DateInput;
nowIndicator?: boolean;
visibleRange?: ((currentDate: Date) => DateRangeInput) | DateRangeInput;
validRange?: DateRangeInput;
dateIncrement?: DurationInput;
dateAlignment?: string;
duration?: DurationInput;
dayCount?: number;
locales?: RawLocale[];
locale?: LocaleSingularArg;
eventTimeFormat?: FormatterInput;
columnHeader?: boolean;
columnHeaderFormat?: FormatterInput;
columnHeaderText?: string | ((date: DateInput) => string);
columnHeaderHtml?: string | ((date: DateInput) => string);
titleFormat?: FormatterInput;
weekLabel?: string;
displayEventTime?: boolean;
displayEventEnd?: boolean;
eventLimitText?: string | ((eventCnt: number) => string);
dayPopoverFormat?: FormatterInput;
navLinks?: boolean;
navLinkDayClick?: string | ((date: Date, jsEvent: Event) => void);
navLinkWeekClick?: string | ((weekStart: any, jsEvent: Event) => void);
selectable?: boolean;
selectMirror?: boolean;
unselectAuto?: boolean;
unselectCancel?: string;
defaultAllDayEventDuration?: DurationInput;
defaultTimedEventDuration?: DurationInput;
cmdFormatter?: string;
defaultRangeSeparator?: string;
selectConstraint?: ConstraintInput;
selectOverlap?: boolean | OverlapFunc;
selectAllow?: AllowFunc;
editable?: boolean;
eventStartEditable?: boolean;
eventDurationEditable?: boolean;
eventConstraint?: ConstraintInput;
eventOverlap?: boolean | OverlapFunc;
eventAllow?: AllowFunc;
eventClassName?: string[] | string;
eventClassNames?: string[] | string;
eventBackgroundColor?: string;
eventBorderColor?: string;
eventTextColor?: string;
eventColor?: string;
events?: EventSourceInput;
eventSources?: EventSourceInput[];
allDayDefault?: boolean;
startParam?: string;
endParam?: string;
lazyFetching?: boolean;
nextDayThreshold?: DurationInput;
eventOrder?: string | Array<((a: EventApi, b: EventApi) => number) | (string | ((a: EventApi, b: EventApi) => number))>;
rerenderDelay?: number | null;
dragRevertDuration?: number;
dragScroll?: boolean;
longPressDelay?: number;
eventLongPressDelay?: number;
droppable?: boolean;
dropAccept?: string | ((draggable: any) => boolean);
eventDataTransform?: EventInputTransformer;
allDayMaintainDuration?: boolean;
eventResizableFromStart?: boolean;
timeGridEventMinHeight?: number;
allDayHtml?: string;
eventDragMinDistance?: number;
eventSourceFailure?: any;
eventSourceSuccess?: any;
forceEventDuration?: boolean;
progressiveEventRendering?: boolean;
selectLongPressDelay?: number;
selectMinDistance?: number;
timeZoneParam?: string;
titleRangeSeparator?: string;
datesRender?(arg: {
view: View;
el: HTMLElement;
}): void;
datesDestroy?(arg: {
view: View;
el: HTMLElement;
}): void;
dayRender?(arg: {
view: View;
date: Date;
allDay?: boolean;
el: HTMLElement;
}): void;
windowResize?(view: View): void;
dateClick?(arg: {
date: Date;
dateStr: string;
allDay: boolean;
resource?: any;
dayEl: HTMLElement;
jsEvent: MouseEvent;
view: View;
}): void;
eventClick?(arg: {
el: HTMLElement;
event: EventApi;
jsEvent: MouseEvent;
view: View;
}): boolean | void;
eventMouseEnter?(arg: {
el: HTMLElement;
event: EventApi;
jsEvent: MouseEvent;
view: View;
}): void;
eventMouseLeave?(arg: {
el: HTMLElement;
event: EventApi;
jsEvent: MouseEvent;
view: View;
}): void;
select?(arg: {
start: Date;
end: Date;
startStr: string;
endStr: string;
allDay: boolean;
resource?: any;
jsEvent: MouseEvent;
view: View;
}): void;
unselect?(arg: {
view: View;
jsEvent: Event;
}): void;
loading?(isLoading: boolean): void;
eventRender?(arg: {
isMirror: boolean;
isStart: boolean;
isEnd: boolean;
event: EventApi;
el: HTMLElement;
view: View;
}): void;
eventPositioned?(arg: {
isMirror: boolean;
isStart: boolean;
isEnd: boolean;
event: EventApi;
el: HTMLElement;
view: View;
}): void;
_eventsPositioned?(arg: {
view: View;
}): void;
eventDestroy?(arg: {
isMirror: boolean;
event: EventApi;
el: HTMLElement;
view: View;
}): void;
eventDragStart?(arg: {
event: EventApi;
el: HTMLElement;
jsEvent: MouseEvent;
view: View;
}): void;
eventDragStop?(arg: {
event: EventApi;
el: HTMLElement;
jsEvent: MouseEvent;
view: View;
}): void;
eventDrop?(arg: {
el: HTMLElement;
event: EventApi;
oldEvent: EventApi;
delta: Duration;
revert: () => void;
jsEvent: Event;
view: View;
}): void;
eventResizeStart?(arg: {
el: HTMLElement;
event: EventApi;
jsEvent: MouseEvent;
view: View;
}): void;
eventResizeStop?(arg: {
el: HTMLElement;
event: EventApi;
jsEvent: MouseEvent;
view: View;
}): void;
eventResize?(arg: {
el: HTMLElement;
startDelta: Duration;
endDelta: Duration;
prevEvent: EventApi;
event: EventApi;
revert: () => void;
jsEvent: Event;
view: View;
}): void;
drop?(arg: {
date: Date;
dateStr: string;
allDay: boolean;
draggedEl: HTMLElement;
jsEvent: MouseEvent;
view: View;
}): void;
eventReceive?(arg: {
event: EventApi;
draggedEl: HTMLElement;
view: View;
}): void;
eventLeave?(arg: {
draggedEl: HTMLElement;
event: EventApi;
view: View;
}): void;
viewSkeletonRender?(arg: {
el: HTMLElement;
view: View;
}): void;
viewSkeletonDestroy?(arg: {
el: HTMLElement;
view: View;
}): void;
_destroyed?(): void;
_init?(): void;
_noEventDrop?(): void;
_noEventResize?(): void;
resourceRender?(arg: {
resource: any;
el: HTMLElement;
view: View;
}): void;
}
export interface ViewOptionsInput extends OptionsInputBase {
type?: string;
buttonText?: string;
}
export interface OptionsInput extends OptionsInputBase {
buttonText?: ButtonTextCompoundInput;
views?: {
[viewId: string]: ViewOptionsInput;
};
plugins?: (PluginDef | string)[];
}
}
declare module '@fullcalendar/core/structs/event' {
import { DateInput } from '@fullcalendar/core/datelib/env';
import Calendar from '@fullcalendar/core/Calendar';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { UnscopedEventUiInput, EventUi } from '@fullcalendar/core/component/event-ui';
export type EventRenderingChoice = '' | 'background' | 'inverse-background' | 'none';
export interface EventNonDateInput extends UnscopedEventUiInput {
id?: string | number;
groupId?: string | number;
title?: string;
url?: string;
rendering?: EventRenderingChoice;
extendedProps?: object;
[extendedProp: string]: any;
}
export interface EventDateInput {
start?: DateInput;
end?: DateInput;
date?: DateInput;
allDay?: boolean;
}
export type EventInput = EventNonDateInput & EventDateInput;
export interface EventDef {
defId: string;
sourceId: string;
publicId: string;
groupId: string;
allDay: boolean;
hasEnd: boolean;
recurringDef: {
typeId: number;
typeData: any;
duration: Duration | null;
} | null;
title: string;
url: string;
rendering: EventRenderingChoice;
ui: EventUi;
extendedProps: any;
}
export interface EventInstance {
instanceId: string;
defId: string;
range: DateRange;
forcedStartTzo: number | null;
forcedEndTzo: number | null;
}
export interface EventTuple {
def: EventDef;
instance: EventInstance | null;
}
export type EventInstanceHash = {
[instanceId: string]: EventInstance;
};
export type EventDefHash = {
[defId: string]: EventDef;
};
export const NON_DATE_PROPS: {
id: StringConstructor;
groupId: StringConstructor;
title: StringConstructor;
url: StringConstructor;
rendering: StringConstructor;
extendedProps: any;
};
export const DATE_PROPS: {
start: any;
date: any;
end: any;
allDay: any;
};
export function parseEvent(raw: EventInput, sourceId: string, calendar: Calendar, allowOpenRange?: boolean): EventTuple | null;
export function parseEventDef(raw: EventNonDateInput, sourceId: string, allDay: boolean, hasEnd: boolean, calendar: Calendar): EventDef;
export type eventDefParserFunc = (def: EventDef, props: any, leftovers: any) => void;
export function createEventInstance(defId: string, range: DateRange, forcedStartTzo?: number, forcedEndTzo?: number): EventInstance;
}
declare module '@fullcalendar/core/structs/business-hours' {
import Calendar from '@fullcalendar/core/Calendar';
import { EventInput } from '@fullcalendar/core/structs/event';
import { EventStore } from '@fullcalendar/core/structs/event-store';
export type BusinessHoursInput = boolean | EventInput | EventInput[];
export function parseBusinessHours(input: BusinessHoursInput, calendar: Calendar): EventStore;
}
declare module '@fullcalendar/core/util/misc' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { DateEnv } from '@fullcalendar/core/datelib/env';
import { DateRange, OpenDateRange } from '@fullcalendar/core/datelib/date-range';
export function compensateScroll(rowEl: HTMLElement, scrollbarWidths: any): void;
export function uncompensateScroll(rowEl: HTMLElement): void;
export function disableCursor(): void;
export function enableCursor(): void;
export function distributeHeight(els: HTMLElement[], availableHeight: any, shouldRedistribute: any): void;
export function undistributeHeight(els: HTMLElement[]): void;
export function matchCellWidths(els: HTMLElement[]): number;
export function subtractInnerElHeight(outerEl: HTMLElement, innerEl: HTMLElement): number;
export function preventSelection(el: HTMLElement): void;
export function allowSelection(el: HTMLElement): void;
export function preventContextMenu(el: HTMLElement): void;
export function allowContextMenu(el: HTMLElement): void;
export function parseFieldSpecs(input: any): any[];
export function compareByFieldSpecs(obj0: any, obj1: any, fieldSpecs: any): any;
export function compareByFieldSpec(obj0: any, obj1: any, fieldSpec: any): any;
export function flexibleCompare(a: any, b: any): number;
export function capitaliseFirstLetter(str: any): any;
export function padStart(val: any, len: any): string;
export function compareNumbers(a: any, b: any): number;
export function isInt(n: any): boolean;
export function applyAll(functions: any, thisObj: any, args: any): any;
export function firstDefined(...args: any[]): any;
export function debounce(func: any, wait: any): () => any;
export type GenericHash = {
[key: string]: any;
};
export function refineProps(rawProps: GenericHash, processors: GenericHash, defaults?: GenericHash, leftoverProps?: GenericHash): GenericHash;
export function computeAlignedDayRange(timedRange: DateRange): DateRange;
export function computeVisibleDayRange(timedRange: OpenDateRange, nextDayThreshold?: Duration): OpenDateRange;
export function isMultiDayRange(range: DateRange): boolean;
export function diffDates(date0: DateMarker, date1: DateMarker, dateEnv: DateEnv, largeUnit?: string): Duration;
}
declare module '@fullcalendar/core/util/html' {
export function htmlEscape(s: any): string;
export function cssToStr(cssProps: any): string;
export function attrsToStr(attrs: any): string;
export type ClassNameInput = string | string[];
export function parseClassName(raw: ClassNameInput): string[];
}
declare module '@fullcalendar/core/util/array' {
export function removeMatching(array: any, testFunc: any): number;
export function removeExact(array: any, exactVal: any): number;
export function isArraysEqual(a0: any, a1: any): boolean;
}
declare module '@fullcalendar/core/util/memoize' {
export function memoize(workerFunc: T): T;
export function memoizeOutput(workerFunc: T, equalityFunc: (output0: any, output1: any) => boolean): T;
}
declare module '@fullcalendar/core/component/memoized-rendering' {
export interface MemoizedRendering {
(...args: ArgsType): void;
unrender: () => void;
dependents: MemoizedRendering[];
}
export function memoizeRendering(renderFunc: (...args: ArgsType) => void, unrenderFunc?: (...args: ArgsType) => void, dependencies?: MemoizedRendering[]): MemoizedRendering;
}
declare module '@fullcalendar/core/util/geom' {
export interface Point {
left: number;
top: number;
}
export interface Rect {
left: number;
right: number;
top: number;
bottom: number;
}
export function pointInsideRect(point: Point, rect: Rect): boolean;
export function intersectRects(rect1: Rect, rect2: Rect): Rect | false;
export function translateRect(rect: Rect, deltaX: number, deltaY: number): Rect;
export function constrainPoint(point: Point, rect: Rect): Point;
export function getRectCenter(rect: Rect): Point;
export function diffPoints(point1: Point, point2: Point): Point;
}
declare module '@fullcalendar/core/util/object' {
export function mergeProps(propObjs: any, complexProps?: any): any;
export function filterHash(hash: any, func: any): {};
export function mapHash(hash: {
[key: string]: InputItem;
}, func: (input: InputItem, key: string) => OutputItem): {
[key: string]: OutputItem;
};
export function arrayToHash(a: any): {
[key: string]: true;
};
export function hashValuesToArray(obj: any): any[];
export function isPropsEqual(obj0: any, obj1: any): boolean;
}
declare module '@fullcalendar/core/util/dom-manip' {
export function createElement(tagName: string, attrs: object | null, content?: ElementContent): HTMLElement;
export function htmlToElement(html: string): HTMLElement;
export function htmlToElements(html: string): HTMLElement[];
export type ElementContent = string | Node | Node[] | NodeList;
export function appendToElement(el: HTMLElement, content: ElementContent): void;
export function prependToElement(parent: HTMLElement, content: ElementContent): void;
export function insertAfterElement(refEl: HTMLElement, content: ElementContent): void;
export function removeElement(el: HTMLElement): void;
export function elementClosest(el: HTMLElement, selector: string): HTMLElement;
export function elementMatches(el: HTMLElement, selector: string): HTMLElement;
export function findElements(container: HTMLElement[] | HTMLElement | NodeListOf, selector: string): HTMLElement[];
export function findChildren(parent: HTMLElement[] | HTMLElement, selector?: string): HTMLElement[];
export function forceClassName(el: HTMLElement, className: string, bool: any): void;
export function applyStyle(el: HTMLElement, props: object): void;
export function applyStyleProp(el: HTMLElement, name: string, val: any): void;
}
declare module '@fullcalendar/core/structs/event-store' {
import { EventInput, EventDef, EventDefHash, EventInstanceHash, EventTuple } from '@fullcalendar/core/structs/event';
import { EventSource } from '@fullcalendar/core/structs/event-source';
import Calendar from '@fullcalendar/core/Calendar';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
export interface EventStore {
defs: EventDefHash;
instances: EventInstanceHash;
}
export function parseEvents(rawEvents: EventInput[], sourceId: string, calendar: Calendar, allowOpenRange?: boolean): EventStore;
export function eventTupleToStore(tuple: EventTuple, eventStore?: EventStore): EventStore;
export function expandRecurring(eventStore: EventStore, framingRange: DateRange, calendar: Calendar): EventStore;
export function getRelevantEvents(eventStore: EventStore, instanceId: string): EventStore;
export function transformRawEvents(rawEvents: any, eventSource: EventSource, calendar: Calendar): any;
export function createEmptyEventStore(): EventStore;
export function mergeEventStores(store0: EventStore, store1: EventStore): EventStore;
export function filterEventStoreDefs(eventStore: EventStore, filterFunc: (eventDef: EventDef) => boolean): EventStore;
}
declare module '@fullcalendar/core/component/event-ui' {
import { Constraint, AllowFunc, ConstraintInput } from '@fullcalendar/core/validation';
import { parseClassName } from '@fullcalendar/core/util/html';
import Calendar from '@fullcalendar/core/Calendar';
export interface UnscopedEventUiInput {
editable?: boolean;
startEditable?: boolean;
durationEditable?: boolean;
constraint?: ConstraintInput;
overlap?: boolean;
allow?: AllowFunc;
className?: string[] | string;
classNames?: string[] | string;
backgroundColor?: string;
borderColor?: string;
textColor?: string;
color?: string;
}
export interface EventUi {
startEditable: boolean | null;
durationEditable: boolean | null;
constraints: Constraint[];
overlap: boolean | null;
allows: AllowFunc[];
backgroundColor: string;
borderColor: string;
textColor: string;
classNames: string[];
}
export type EventUiHash = {
[defId: string]: EventUi;
};
export const UNSCOPED_EVENT_UI_PROPS: {
editable: BooleanConstructor;
startEditable: BooleanConstructor;
durationEditable: BooleanConstructor;
constraint: any;
overlap: any;
allow: any;
className: typeof parseClassName;
classNames: typeof parseClassName;
color: StringConstructor;
backgroundColor: StringConstructor;
borderColor: StringConstructor;
textColor: StringConstructor;
};
export function processUnscopedUiProps(rawProps: UnscopedEventUiInput, calendar: Calendar, leftovers?: any): EventUi;
export function processScopedUiProps(prefix: string, rawScoped: any, calendar: Calendar, leftovers?: any): EventUi;
export function combineEventUis(uis: EventUi[]): EventUi;
}
declare module '@fullcalendar/core/component/event-splitting' {
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventDef } from '@fullcalendar/core/structs/event';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { EventUiHash, EventUi } from '@fullcalendar/core/component/event-ui';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
export interface SplittableProps {
businessHours: EventStore | null;
dateSelection: DateSpan | null;
eventStore: EventStore;
eventUiBases: EventUiHash;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export { Splitter as default, Splitter };
abstract class Splitter {
abstract getKeyInfo(props: PropsType): {
[key: string]: {
ui?: EventUi;
businessHours?: EventStore;
};
};
abstract getKeysForDateSpan(dateSpan: DateSpan): string[];
abstract getKeysForEventDef(eventDef: EventDef): string[];
splitProps(props: PropsType): {
[key: string]: SplittableProps;
};
}
}
declare module '@fullcalendar/core/component/date-rendering' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import Component, { ComponentContext } from '@fullcalendar/core/component/Component';
import { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
export function buildGotoAnchorHtml(component: Component, gotoOptions: any, attrs: any, innerHtml?: any): string;
export function getAllDayHtml(component: Component): any;
export function getDayClasses(date: DateMarker, dateProfile: DateProfile, context: ComponentContext, noThemeHighlight?: any): any[];
}
declare module '@fullcalendar/core/util/dom-event' {
export function preventDefault(ev: any): void;
export function listenBySelector(container: HTMLElement, eventType: string, selector: string, handler: (ev: Event, matchedTarget: HTMLElement) => void): () => void;
export function listenToHoverBySelector(container: HTMLElement, selector: string, onMouseEnter: (ev: Event, matchedTarget: HTMLElement) => void, onMouseLeave: (ev: Event, matchedTarget: HTMLElement) => void): () => void;
export function whenTransitionDone(el: HTMLElement, callback: (ev: Event) => void): void;
}
declare module '@fullcalendar/core/util/dom-geom' {
import { Rect } from '@fullcalendar/core/util/geom';
export interface EdgeInfo {
borderLeft: number;
borderRight: number;
borderTop: number;
borderBottom: number;
scrollbarLeft: number;
scrollbarRight: number;
scrollbarBottom: number;
paddingLeft?: number;
paddingRight?: number;
paddingTop?: number;
paddingBottom?: number;
}
export function computeEdges(el: any, getPadding?: boolean): EdgeInfo;
export function computeInnerRect(el: any, goWithinPadding?: boolean): {
left: number;
right: number;
top: number;
bottom: number;
};
export function computeRect(el: any): Rect;
export function computeHeightAndMargins(el: HTMLElement): number;
export function computeVMargins(el: HTMLElement): number;
export function getClippingParents(el: HTMLElement): HTMLElement[];
export function computeClippingRect(el: HTMLElement): Rect;
}
declare module '@fullcalendar/core/util/promise' {
export function unpromisify(func: any, success: any, failure?: any): void;
}
declare module '@fullcalendar/core/common/EmitterMixin' {
import Mixin from '@fullcalendar/core/common/Mixin';
export interface EmitterInterface {
on(types: any, handler: any): any;
one(types: any, handler: any): any;
off(types: any, handler: any): any;
trigger(type: any, ...args: any[]): any;
triggerWith(type: any, context: any, args: any): any;
hasHandlers(type: any): any;
}
export { EmitterMixin as default, EmitterMixin };
class EmitterMixin extends Mixin implements EmitterInterface {
_handlers: any;
_oneHandlers: any;
on(type: any, handler: any): this;
one(type: any, handler: any): this;
off(type: any, handler?: any): this;
trigger(type: any, ...args: any[]): this;
triggerWith(type: any, context: any, args: any): this;
hasHandlers(type: any): any;
}
}
declare module '@fullcalendar/core/datelib/date-range' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { DateEnv, DateInput } from '@fullcalendar/core/datelib/env';
export interface DateRangeInput {
start?: DateInput;
end?: DateInput;
}
export interface OpenDateRange {
start: DateMarker | null;
end: DateMarker | null;
}
export interface DateRange {
start: DateMarker;
end: DateMarker;
}
export function parseRange(input: DateRangeInput, dateEnv: DateEnv): OpenDateRange;
export function invertRanges(ranges: DateRange[], constraintRange: DateRange): DateRange[];
export function intersectRanges(range0: OpenDateRange, range1: OpenDateRange): OpenDateRange;
export function rangesEqual(range0: OpenDateRange, range1: OpenDateRange): boolean;
export function rangesIntersect(range0: OpenDateRange, range1: OpenDateRange): boolean;
export function rangeContainsRange(outerRange: OpenDateRange, innerRange: OpenDateRange): boolean;
export function rangeContainsMarker(range: OpenDateRange, date: DateMarker | number): boolean;
export function constrainMarkerToRange(date: DateMarker, range: DateRange): DateMarker;
}
declare module '@fullcalendar/core/common/Mixin' {
export { Mixin as default, Mixin };
class Mixin {
static mixInto(destClass: any): void;
static mixIntoObj(destObj: any): void;
static mixOver(destClass: any): void;
}
}
declare module '@fullcalendar/core/common/PositionCache' {
export { PositionCache as default, PositionCache };
class PositionCache {
originClientRect: ClientRect;
els: HTMLElement[];
originEl: HTMLElement;
isHorizontal: boolean;
isVertical: boolean;
lefts: any;
rights: any;
tops: any;
bottoms: any;
constructor(originEl: HTMLElement, els: HTMLElement[], isHorizontal: boolean, isVertical: boolean);
build(): void;
buildElHorizontals(originClientLeft: number): void;
buildElVerticals(originClientTop: number): void;
leftToIndex(leftPosition: number): any;
topToIndex(topPosition: number): any;
getWidth(leftIndex: number): number;
getHeight(topIndex: number): number;
}
}
declare module '@fullcalendar/core/common/ScrollComponent' {
import { ElementScrollController } from '@fullcalendar/core/common/scroll-controller';
export interface ScrollbarWidths {
left: number;
right: number;
bottom: number;
}
export { ScrollComponent as default, ScrollComponent };
class ScrollComponent extends ElementScrollController {
overflowX: string;
overflowY: string;
constructor(overflowX: string, overflowY: string);
clear(): void;
destroy(): void;
applyOverflow(): void;
lockOverflow(scrollbarWidths: ScrollbarWidths): void;
setHeight(height: number | string): void;
getScrollbarWidths(): ScrollbarWidths;
}
}
declare module '@fullcalendar/core/common/scroll-controller' {
export abstract class ScrollController {
abstract getScrollTop(): number;
abstract getScrollLeft(): number;
abstract setScrollTop(top: number): void;
abstract setScrollLeft(left: number): void;
abstract getClientWidth(): number;
abstract getClientHeight(): number;
abstract getScrollWidth(): number;
abstract getScrollHeight(): number;
getMaxScrollTop(): number;
getMaxScrollLeft(): number;
canScrollVertically(): boolean;
canScrollHorizontally(): boolean;
canScrollUp(): boolean;
canScrollDown(): boolean;
canScrollLeft(): boolean;
canScrollRight(): boolean;
}
export class ElementScrollController extends ScrollController {
el: HTMLElement;
constructor(el: HTMLElement);
getScrollTop(): number;
getScrollLeft(): number;
setScrollTop(top: number): void;
setScrollLeft(left: number): void;
getScrollWidth(): number;
getScrollHeight(): number;
getClientHeight(): number;
getClientWidth(): number;
}
export class WindowScrollController extends ScrollController {
getScrollTop(): number;
getScrollLeft(): number;
setScrollTop(n: number): void;
setScrollLeft(n: number): void;
getScrollWidth(): number;
getScrollHeight(): number;
getClientHeight(): number;
getClientWidth(): number;
}
}
declare module '@fullcalendar/core/theme/Theme' {
export { Theme as default, Theme };
class Theme {
calendarOptions: any;
classes: any;
iconClasses: any;
baseIconClass: string;
iconOverrideOption: any;
iconOverrideCustomButtonOption: any;
iconOverridePrefix: string;
constructor(calendarOptions: any);
processIconOverride(): void;
setIconOverride(iconOverrideHash: any): void;
applyIconOverridePrefix(className: any): any;
getClass(key: any): any;
getIconClass(buttonName: any): string;
getCustomButtonIconClass(customButtonProps: any): string;
}
export type ThemeClass = {
new (calendarOptions: any): Theme;
};
}
declare module '@fullcalendar/core/component/Component' {
import Calendar from '@fullcalendar/core/Calendar';
import View from '@fullcalendar/core/View';
import Theme from '@fullcalendar/core/theme/Theme';
import { DateEnv } from '@fullcalendar/core/datelib/env';
export interface ComponentContext {
options: any;
dateEnv: DateEnv;
theme: Theme;
calendar: Calendar;
view: View;
}
export type EqualityFuncHash = {
[propName: string]: (obj0: any, obj1: any) => boolean;
};
export { Component as default, Component };
class Component {
equalityFuncs: EqualityFuncHash;
uid: string;
props: PropsType | null;
context: ComponentContext;
dateEnv: DateEnv;
theme: Theme;
view: View;
calendar: Calendar;
isRtl: boolean;
constructor(context: ComponentContext, isView?: boolean);
static addEqualityFuncs(newFuncs: EqualityFuncHash): void;
opt(name: any): any;
receiveProps(props: PropsType): void;
protected render(props: PropsType): void;
destroy(): void;
}
}
declare module '@fullcalendar/core/component/DateComponent' {
import Component, { ComponentContext } from '@fullcalendar/core/component/Component';
import { EventRenderRange } from '@fullcalendar/core/component/event-rendering';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { EventInstanceHash } from '@fullcalendar/core/structs/event';
import { Hit } from '@fullcalendar/core/interactions/hit';
import FgEventRenderer from '@fullcalendar/core/component/renderers/FgEventRenderer';
import FillRenderer from '@fullcalendar/core/component/renderers/FillRenderer';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { EventHandlerName, EventHandlerArgs } from '@fullcalendar/core/types/input-types';
export type DateComponentHash = {
[uid: string]: DateComponent;
};
export interface Seg {
component?: DateComponent;
isStart: boolean;
isEnd: boolean;
eventRange?: EventRenderRange;
el?: HTMLElement;
[otherProp: string]: any;
}
export interface EventSegUiInteractionState {
affectedInstances: EventInstanceHash;
segs: Seg[];
isEvent: boolean;
sourceSeg: any;
}
export { DateComponent as default, DateComponent };
class DateComponent extends Component {
fgSegSelector: string;
bgSegSelector: string;
largeUnit: any;
eventRenderer: FgEventRenderer;
mirrorRenderer: FgEventRenderer;
fillRenderer: FillRenderer;
el: HTMLElement;
constructor(context: ComponentContext, el: HTMLElement, isView?: boolean);
destroy(): void;
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit | null;
isInteractionValid(interaction: EventInteractionState): boolean;
isDateSelectionValid(selection: DateSpan): boolean;
publiclyTrigger(name: T, args?: EventHandlerArgs): any;
publiclyTriggerAfterSizing(name: T, args: EventHandlerArgs): void;
hasPublicHandlers(name: T): boolean;
triggerRenderedSegs(segs: Seg[], isMirrors: boolean): void;
triggerWillRemoveSegs(segs: Seg[], isMirrors: boolean): void;
isValidSegDownEl(el: HTMLElement): boolean;
isValidDateDownEl(el: HTMLElement): boolean;
isPopover(): boolean;
isInPopover(el: HTMLElement): boolean;
}
}
declare module '@fullcalendar/core/Calendar' {
import { EmitterInterface } from '@fullcalendar/core/common/EmitterMixin';
import OptionsManager from '@fullcalendar/core/OptionsManager';
import View from '@fullcalendar/core/View';
import Theme from '@fullcalendar/core/theme/Theme';
import { OptionsInput, EventHandlerName, EventHandlerArgs } from '@fullcalendar/core/types/input-types';
import { RawLocaleMap } from '@fullcalendar/core/datelib/locale';
import { DateEnv, DateInput } from '@fullcalendar/core/datelib/env';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { Duration, DurationInput } from '@fullcalendar/core/datelib/duration';
import { DateSpan, DateSpanApi, DatePointApi } from '@fullcalendar/core/structs/date-span';
import { DateRangeInput } from '@fullcalendar/core/datelib/date-range';
import DateProfileGenerator from '@fullcalendar/core/DateProfileGenerator';
import { EventSourceInput } from '@fullcalendar/core/structs/event-source';
import { EventInput } from '@fullcalendar/core/structs/event';
import { CalendarState, Action } from '@fullcalendar/core/reducers/types';
import EventSourceApi from '@fullcalendar/core/api/EventSourceApi';
import EventApi from '@fullcalendar/core/api/EventApi';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventUiHash, EventUi } from '@fullcalendar/core/component/event-ui';
import { ViewSpecHash, ViewSpec } from '@fullcalendar/core/structs/view-spec';
import { PluginSystem } from '@fullcalendar/core/plugin-system';
import CalendarComponent from '@fullcalendar/core/CalendarComponent';
import DateComponent from '@fullcalendar/core/component/DateComponent';
import { PointerDragEvent } from '@fullcalendar/core/interactions/pointer';
import { InteractionSettingsInput, Interaction } from '@fullcalendar/core/interactions/interaction';
export interface DateClickApi extends DatePointApi {
dayEl: HTMLElement;
jsEvent: UIEvent;
view: View;
}
export interface DateSelectionApi extends DateSpanApi {
jsEvent: UIEvent;
view: View;
}
export type DatePointTransform = (dateSpan: DateSpan, calendar: Calendar) => any;
export type DateSpanTransform = (dateSpan: DateSpan, calendar: Calendar) => any;
export type CalendarInteraction = {
destroy(): any;
};
export type CalendarInteractionClass = {
new (calendar: Calendar): CalendarInteraction;
};
export type OptionChangeHandler = (propValue: any, calendar: Calendar, deepEqual: any) => void;
export type OptionChangeHandlerMap = {
[propName: string]: OptionChangeHandler;
};
export { Calendar as default, Calendar };
class Calendar {
static on: EmitterInterface['on'];
static off: EmitterInterface['off'];
static trigger: EmitterInterface['trigger'];
on: EmitterInterface['on'];
one: EmitterInterface['one'];
off: EmitterInterface['off'];
trigger: EmitterInterface['trigger'];
triggerWith: EmitterInterface['triggerWith'];
hasHandlers: EmitterInterface['hasHandlers'];
eventUiBases: EventUiHash;
selectionConfig: EventUi;
optionsManager: OptionsManager;
viewSpecs: ViewSpecHash;
dateProfileGenerators: {
[viewName: string]: DateProfileGenerator;
};
theme: Theme;
dateEnv: DateEnv;
availableRawLocales: RawLocaleMap;
pluginSystem: PluginSystem;
defaultAllDayEventDuration: Duration;
defaultTimedEventDuration: Duration;
calendarInteractions: CalendarInteraction[];
interactionsStore: {
[componentUid: string]: Interaction[];
};
removeNavLinkListener: any;
windowResizeProxy: any;
isHandlingWindowResize: boolean;
state: CalendarState;
actionQueue: any[];
isReducing: boolean;
needsRerender: boolean;
needsFullRerender: boolean;
isRendering: boolean;
renderingPauseDepth: number;
renderableEventStore: EventStore;
buildDelayedRerender: typeof buildDelayedRerender;
delayedRerender: any;
afterSizingTriggers: any;
isViewUpdated: boolean;
isDatesUpdated: boolean;
isEventsUpdated: boolean;
el: HTMLElement;
component: CalendarComponent;
constructor(el: HTMLElement, overrides?: OptionsInput);
addPluginInputs(pluginInputs: any): void;
readonly view: View;
render(): void;
destroy(): void;
bindHandlers(): void;
unbindHandlers(): void;
hydrate(): void;
buildInitialState(): CalendarState;
reduce(state: CalendarState, action: Action, calendar: Calendar): CalendarState;
requestRerender(needsFull?: boolean): void;
tryRerender(): void;
batchRendering(func: any): void;
executeRender(): void;
renderComponent(needsFull: any): void;
setOption(name: string, val: any): void;
getOption(name: string): any;
opt(name: string): any;
viewOpt(name: string): any;
viewOpts(): any;
mutateOptions(updates: any, removals: string[], isDynamic?: boolean, deepEqual?: any): void;
handleOptions(options: any): void;
getAvailableLocaleCodes(): string[];
_buildSelectionConfig(rawOpts: any): EventUi;
_buildEventUiSingleBase(rawOpts: any): EventUi;
hasPublicHandlers(name: T): boolean;
publiclyTrigger(name: T, args?: EventHandlerArgs): any;
publiclyTriggerAfterSizing(name: T, args: EventHandlerArgs): void;
releaseAfterSizingTriggers(): void;
isValidViewType(viewType: string): boolean;
changeView(viewType: string, dateOrRange?: DateRangeInput | DateInput): void;
zoomTo(dateMarker: DateMarker, viewType?: string): void;
getUnitViewSpec(unit: string): ViewSpec | null;
getInitialDate(): Date;
prev(): void;
next(): void;
prevYear(): void;
nextYear(): void;
today(): void;
gotoDate(zonedDateInput: any): void;
incrementDate(deltaInput: any): void;
getDate(): Date;
formatDate(d: DateInput, formatter: any): string;
formatRange(d0: DateInput, d1: DateInput, settings: any): any;
formatIso(d: DateInput, omitTime?: boolean): string;
windowResize(ev: Event): void;
updateSize(): void;
registerInteractiveComponent(component: DateComponent, settingsInput: InteractionSettingsInput): void;
unregisterInteractiveComponent(component: DateComponent): void;
select(dateOrObj: DateInput | any, endDate?: DateInput): void;
unselect(pev?: PointerDragEvent): void;
triggerDateSelect(selection: DateSpan, pev?: PointerDragEvent): void;
triggerDateUnselect(pev?: PointerDragEvent): void;
triggerDateClick(dateSpan: DateSpan, dayEl: HTMLElement, view: View, ev: UIEvent): void;
buildDatePointApi(dateSpan: DateSpan): import("@fullcalendar/core/structs/date-span").DatePointApi;
buildDateSpanApi(dateSpan: DateSpan): import("@fullcalendar/core/structs/date-span").DateSpanApi;
getNow(): DateMarker;
getDefaultEventEnd(allDay: boolean, marker: DateMarker): DateMarker;
addEvent(eventInput: EventInput, sourceInput?: EventSourceApi | string | number): EventApi | null;
getEventById(id: string): EventApi | null;
getEvents(): EventApi[];
removeAllEvents(): void;
rerenderEvents(): void;
getEventSources(): EventSourceApi[];
getEventSourceById(id: string | number): EventSourceApi | null;
addEventSource(sourceInput: EventSourceInput): EventSourceApi;
removeAllEventSources(): void;
refetchEvents(): void;
scrollToTime(timeInput: DurationInput): void;
}
function buildDelayedRerender(this: Calendar, wait: any): any;
export {};
}
declare module '@fullcalendar/core/View' {
import DateProfileGenerator, { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { EmitterInterface } from '@fullcalendar/core/common/EmitterMixin';
import { ViewSpec } from '@fullcalendar/core/structs/view-spec';
import { ComponentContext } from '@fullcalendar/core/component/Component';
import DateComponent from '@fullcalendar/core/component/DateComponent';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventUiHash, EventUi } from '@fullcalendar/core/component/event-ui';
import { EventRenderRange } from '@fullcalendar/core/component/event-rendering';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { EventDef } from '@fullcalendar/core/structs/event';
export interface ViewProps {
dateProfile: DateProfile;
businessHours: EventStore;
eventStore: EventStore;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export { View as default, View };
abstract class View extends DateComponent {
usesMinMaxTime: boolean;
dateProfileGeneratorClass: any;
on: EmitterInterface['on'];
one: EmitterInterface['one'];
off: EmitterInterface['off'];
trigger: EmitterInterface['trigger'];
triggerWith: EmitterInterface['triggerWith'];
hasHandlers: EmitterInterface['hasHandlers'];
viewSpec: ViewSpec;
dateProfileGenerator: DateProfileGenerator;
type: string;
title: string;
queuedScroll: any;
eventOrderSpecs: any;
nextDayThreshold: Duration;
isNowIndicatorRendered: boolean;
initialNowDate: DateMarker;
initialNowQueriedMs: number;
nowIndicatorTimeoutID: any;
nowIndicatorIntervalID: any;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
initialize(): void;
readonly activeStart: Date;
readonly activeEnd: Date;
readonly currentStart: Date;
readonly currentEnd: Date;
render(props: ViewProps): void;
destroy(): void;
updateSize(isResize: boolean, viewHeight: number, isAuto: boolean): void;
updateBaseSize(isResize: boolean, viewHeight: number, isAuto: boolean): void;
renderDatesWrap(dateProfile: DateProfile): void;
unrenderDatesWrap(): void;
renderDates(dateProfile: DateProfile): void;
unrenderDates(): void;
renderBusinessHours(businessHours: EventStore): void;
unrenderBusinessHours(): void;
renderDateSelectionWrap(selection: DateSpan): void;
unrenderDateSelectionWrap(selection: DateSpan): void;
renderDateSelection(selection: DateSpan): void;
unrenderDateSelection(selection: DateSpan): void;
renderEvents(eventStore: EventStore): void;
unrenderEvents(): void;
sliceEvents(eventStore: EventStore, allDay: boolean): EventRenderRange[];
computeEventDraggable(eventDef: EventDef, eventUi: EventUi): boolean;
computeEventStartResizable(eventDef: EventDef, eventUi: EventUi): any;
computeEventEndResizable(eventDef: EventDef, eventUi: EventUi): boolean;
renderEventSelectionWrap(instanceId: string): void;
unrenderEventSelectionWrap(instanceId: string): void;
renderEventSelection(instanceId: string): void;
unrenderEventSelection(instanceId: string): void;
renderEventDragWrap(state: EventInteractionState): void;
unrenderEventDragWrap(state: EventInteractionState): void;
renderEventDrag(state: EventInteractionState): void;
unrenderEventDrag(state: EventInteractionState): void;
renderEventResizeWrap(state: EventInteractionState): void;
unrenderEventResizeWrap(state: EventInteractionState): void;
renderEventResize(state: EventInteractionState): void;
unrenderEventResize(state: EventInteractionState): void;
startNowIndicator(dateProfile: DateProfile): void;
updateNowIndicator(): void;
stopNowIndicator(): void;
getNowIndicatorUnit(dateProfile: DateProfile): void;
renderNowIndicator(date: any): void;
unrenderNowIndicator(): void;
addScroll(scroll: any): void;
popScroll(isResize: boolean): void;
applyQueuedScroll(isResize: boolean): void;
queryScroll(): any;
applyScroll(scroll: any, isResize: boolean): void;
computeDateScroll(duration: Duration): {};
queryDateScroll(): {};
applyDateScroll(scroll: any): void;
scrollToDuration(duration: Duration): void;
}
}
declare module '@fullcalendar/core/component/renderers/FgEventRenderer' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { DateFormatter } from '@fullcalendar/core/datelib/formatting';
import { EventUi } from '@fullcalendar/core/component/event-ui';
import { EventRenderRange } from '@fullcalendar/core/component/event-rendering';
import { Seg } from '@fullcalendar/core/component/DateComponent';
import { ComponentContext } from '@fullcalendar/core/component/Component';
export { FgEventRenderer as default, FgEventRenderer };
abstract class FgEventRenderer {
context: ComponentContext;
eventTimeFormat: DateFormatter;
displayEventTime: boolean;
displayEventEnd: boolean;
segs: Seg[];
isSizeDirty: boolean;
constructor(context: ComponentContext);
renderSegs(segs: Seg[], mirrorInfo?: any): void;
unrender(_segs: Seg[], mirrorInfo?: any): void;
abstract renderSegHtml(seg: Seg, mirrorInfo: any): string;
abstract attachSegs(segs: Seg[], mirrorInfo: any): any;
abstract detachSegs(segs: Seg[]): any;
rangeUpdated(): void;
renderSegEls(segs: Seg[], mirrorInfo: any): Seg[];
getSegClasses(seg: Seg, isDraggable: any, isResizable: any, mirrorInfo: any): string[];
getTimeText(eventRange: EventRenderRange, formatter?: any, displayEnd?: any): any;
_getTimeText(start: DateMarker, end: DateMarker, allDay: any, formatter?: any, displayEnd?: any, forcedStartTzo?: number, forcedEndTzo?: number): any;
computeEventTimeFormat(): any;
computeDisplayEventTime(): boolean;
computeDisplayEventEnd(): boolean;
getSkinCss(ui: EventUi): {
'background-color': string;
'border-color': string;
color: string;
};
sortEventSegs(segs: any): Seg[];
computeSizes(force: boolean): void;
assignSizes(force: boolean): void;
computeSegSizes(segs: Seg[]): void;
assignSegSizes(segs: Seg[]): void;
hideByHash(hash: any): void;
showByHash(hash: any): void;
selectByInstanceId(instanceId: string): void;
unselectByInstanceId(instanceId: string): void;
}
export function buildSegCompareObj(seg: Seg): any;
}
declare module '@fullcalendar/core/component/renderers/FillRenderer' {
import { Seg } from '@fullcalendar/core/component/DateComponent';
import { ComponentContext } from '@fullcalendar/core/component/Component';
export { FillRenderer as default, FillRenderer };
abstract class FillRenderer {
context: ComponentContext;
fillSegTag: string;
containerElsByType: any;
segsByType: any;
dirtySizeFlags: any;
constructor(context: ComponentContext);
getSegsByType(type: string): any;
renderSegs(type: any, segs: Seg[]): void;
unrender(type: any): void;
renderSegEls(type: any, segs: Seg[]): Seg[];
renderSegHtml(type: any, seg: Seg): string;
abstract attachSegs(type: any, segs: Seg[]): HTMLElement[] | void;
detachSegs(type: any, segs: Seg[]): void;
computeSizes(force: boolean): void;
assignSizes(force: boolean): void;
computeSegSizes(segs: Seg[]): void;
assignSegSizes(segs: Seg[]): void;
}
}
declare module '@fullcalendar/core/DateProfileGenerator' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { DateRange, OpenDateRange } from '@fullcalendar/core/datelib/date-range';
import { ViewSpec } from '@fullcalendar/core/structs/view-spec';
import { DateEnv } from '@fullcalendar/core/datelib/env';
import Calendar from '@fullcalendar/core/Calendar';
export interface DateProfile {
currentRange: DateRange;
currentRangeUnit: string;
isRangeAllDay: boolean;
validRange: OpenDateRange;
activeRange: DateRange;
renderRange: DateRange;
minTime: Duration;
maxTime: Duration;
isValid: boolean;
dateIncrement: Duration;
}
export { DateProfileGenerator as default, DateProfileGenerator };
class DateProfileGenerator {
viewSpec: ViewSpec;
options: any;
dateEnv: DateEnv;
calendar: Calendar;
isHiddenDayHash: boolean[];
constructor(viewSpec: ViewSpec, calendar: Calendar);
buildPrev(currentDateProfile: DateProfile, currentDate: DateMarker): DateProfile;
buildNext(currentDateProfile: DateProfile, currentDate: DateMarker): DateProfile;
build(currentDate: DateMarker, direction?: any, forceToValid?: boolean): DateProfile;
buildValidRange(): OpenDateRange;
buildCurrentRangeInfo(date: DateMarker, direction: any): {
duration: any;
unit: any;
range: any;
};
getFallbackDuration(): Duration;
adjustActiveRange(range: DateRange, minTime: Duration, maxTime: Duration): {
start: Date;
end: Date;
};
buildRangeFromDuration(date: DateMarker, direction: any, duration: Duration, unit: any): any;
buildRangeFromDayCount(date: DateMarker, direction: any, dayCount: any): {
start: Date;
end: Date;
};
buildCustomVisibleRange(date: DateMarker): OpenDateRange;
buildRenderRange(currentRange: DateRange, currentRangeUnit: any, isRangeAllDay: any): DateRange;
buildDateIncrement(fallback: any): Duration;
getRangeOption(name: any, ...otherArgs: any[]): OpenDateRange;
initHiddenDays(): void;
trimHiddenDays(range: DateRange): DateRange | null;
isHiddenDay(day: any): boolean;
skipHiddenDays(date: DateMarker, inc?: number, isExclusive?: boolean): Date;
}
export function isDateProfilesEqual(p0: DateProfile, p1: DateProfile): boolean;
}
declare module '@fullcalendar/core/structs/view-def' {
import { ViewClass, ViewConfigHash } from '@fullcalendar/core/structs/view-config';
export interface ViewDef {
type: string;
class: ViewClass;
overrides: any;
defaults: any;
}
export type ViewDefHash = {
[viewType: string]: ViewDef;
};
export function compileViewDefs(defaultConfigs: ViewConfigHash, overrideConfigs: ViewConfigHash): ViewDefHash;
}
declare module '@fullcalendar/core/structs/view-spec' {
import { Duration } from '@fullcalendar/core/datelib/duration';
import OptionsManager from '@fullcalendar/core/OptionsManager';
import { ViewConfigInputHash, ViewClass } from '@fullcalendar/core/structs/view-config';
export interface ViewSpec {
type: string;
class: ViewClass;
duration: Duration;
durationUnit: string;
singleUnit: string;
options: any;
buttonTextOverride: string;
buttonTextDefault: string;
}
export type ViewSpecHash = {
[viewType: string]: ViewSpec;
};
export function buildViewSpecs(defaultInputs: ViewConfigInputHash, optionsManager: OptionsManager): ViewSpecHash;
}
declare module '@fullcalendar/core/structs/date-span' {
import { DateRange, OpenDateRange } from '@fullcalendar/core/datelib/date-range';
import { DateInput, DateEnv } from '@fullcalendar/core/datelib/env';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { EventRenderRange } from '@fullcalendar/core/component/event-rendering';
import { EventUiHash } from '@fullcalendar/core/component/event-ui';
import Calendar from '@fullcalendar/core/Calendar';
export interface OpenDateSpanInput {
start?: DateInput;
end?: DateInput;
allDay?: boolean;
[otherProp: string]: any;
}
export interface DateSpanInput extends OpenDateSpanInput {
start: DateInput;
end: DateInput;
}
export interface OpenDateSpan {
range: OpenDateRange;
allDay: boolean;
[otherProp: string]: any;
}
export interface DateSpan extends OpenDateSpan {
range: DateRange;
}
export interface DateSpanApi {
start: Date;
end: Date;
startStr: string;
endStr: string;
allDay: boolean;
}
export interface DatePointApi {
date: Date;
dateStr: string;
allDay: boolean;
}
export function parseDateSpan(raw: DateSpanInput, dateEnv: DateEnv, defaultDuration?: Duration): DateSpan | null;
export function parseOpenDateSpan(raw: OpenDateSpanInput, dateEnv: DateEnv): OpenDateSpan | null;
export function isDateSpansEqual(span0: DateSpan, span1: DateSpan): boolean;
export function buildDateSpanApi(span: DateSpan, dateEnv: DateEnv): DateSpanApi;
export function buildDatePointApi(span: DateSpan, dateEnv: DateEnv): DatePointApi;
export function fabricateEventRange(dateSpan: DateSpan, eventUiBases: EventUiHash, calendar: Calendar): EventRenderRange;
}
declare module '@fullcalendar/core/datelib/marker' {
import { Duration } from '@fullcalendar/core/datelib/duration';
export type DateMarker = Date;
export const DAY_IDS: string[];
export function addWeeks(m: DateMarker, n: number): Date;
export function addDays(m: DateMarker, n: number): Date;
export function addMs(m: DateMarker, n: number): Date;
export function diffWeeks(m0: any, m1: any): number;
export function diffDays(m0: any, m1: any): number;
export function diffHours(m0: any, m1: any): number;
export function diffMinutes(m0: any, m1: any): number;
export function diffSeconds(m0: any, m1: any): number;
export function diffDayAndTime(m0: DateMarker, m1: DateMarker): Duration;
export function diffWholeWeeks(m0: DateMarker, m1: DateMarker): number;
export function diffWholeDays(m0: DateMarker, m1: DateMarker): number;
export function startOfDay(m: DateMarker): DateMarker;
export function startOfHour(m: DateMarker): Date;
export function startOfMinute(m: DateMarker): Date;
export function startOfSecond(m: DateMarker): Date;
export function weekOfYear(marker: any, dow: any, doy: any): number;
export function dateToLocalArray(date: any): any[];
export function arrayToLocalDate(a: any): Date;
export function dateToUtcArray(date: any): any[];
export function arrayToUtcDate(a: any): Date;
export function isValidDate(m: DateMarker): boolean;
export function timeAsMs(m: DateMarker): number;
}
declare module '@fullcalendar/core/datelib/duration' {
export type DurationInput = DurationObjectInput | string | number;
export interface DurationObjectInput {
years?: number;
year?: number;
months?: number;
month?: number;
weeks?: number;
week?: number;
days?: number;
day?: number;
hours?: number;
hour?: number;
minutes?: number;
minute?: number;
seconds?: number;
second?: number;
milliseconds?: number;
millisecond?: number;
ms?: number;
}
export interface Duration {
years: number;
months: number;
days: number;
milliseconds: number;
}
export function createDuration(input: DurationInput, unit?: string): Duration | null;
export function getWeeksFromInput(obj: DurationObjectInput): number;
export function durationsEqual(d0: Duration, d1: Duration): boolean;
export function isSingleDay(dur: Duration): boolean;
export function addDurations(d0: Duration, d1: Duration): {
years: number;
months: number;
days: number;
milliseconds: number;
};
export function subtractDurations(d1: Duration, d0: Duration): Duration;
export function multiplyDuration(d: Duration, n: number): {
years: number;
months: number;
days: number;
milliseconds: number;
};
export function asRoughYears(dur: Duration): number;
export function asRoughMonths(dur: Duration): number;
export function asRoughDays(dur: Duration): number;
export function asRoughHours(dur: Duration): number;
export function asRoughMinutes(dur: Duration): number;
export function asRoughSeconds(dur: Duration): number;
export function asRoughMs(dur: Duration): number;
export function wholeDivideDurations(numerator: Duration, denominator: Duration): number;
export function greatestDurationDenominator(dur: Duration, dontReturnWeeks?: boolean): {
unit: string;
value: number;
};
}
declare module '@fullcalendar/core/datelib/env' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { CalendarSystem } from '@fullcalendar/core/datelib/calendar-system';
import { Locale } from '@fullcalendar/core/datelib/locale';
import { NamedTimeZoneImpl, NamedTimeZoneImplClass } from '@fullcalendar/core/datelib/timezone';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { DateFormatter } from '@fullcalendar/core/datelib/formatting';
import { CmdFormatterFunc } from '@fullcalendar/core/datelib/formatting-cmd';
export interface DateEnvSettings {
timeZone: string;
namedTimeZoneImpl?: NamedTimeZoneImplClass;
calendarSystem: string;
locale: Locale;
weekNumberCalculation?: any;
firstDay?: any;
weekLabel?: string;
cmdFormatter?: CmdFormatterFunc;
}
export type DateInput = Date | string | number | number[];
export interface DateMarkerMeta {
marker: DateMarker;
isTimeUnspecified: boolean;
forcedTzo: number | null;
}
export class DateEnv {
timeZone: string;
namedTimeZoneImpl: NamedTimeZoneImpl;
canComputeOffset: boolean;
calendarSystem: CalendarSystem;
locale: Locale;
weekDow: number;
weekDoy: number;
weekNumberFunc: any;
weekLabel: string;
cmdFormatter?: CmdFormatterFunc;
constructor(settings: DateEnvSettings);
createMarker(input: DateInput): DateMarker;
createNowMarker(): DateMarker;
createMarkerMeta(input: DateInput): DateMarkerMeta;
parse(s: string): {
marker: Date;
isTimeUnspecified: boolean;
forcedTzo: any;
};
getYear(marker: DateMarker): number;
getMonth(marker: DateMarker): number;
add(marker: DateMarker, dur: Duration): DateMarker;
subtract(marker: DateMarker, dur: Duration): DateMarker;
addYears(marker: DateMarker, n: number): Date;
addMonths(marker: DateMarker, n: number): Date;
diffWholeYears(m0: DateMarker, m1: DateMarker): number;
diffWholeMonths(m0: DateMarker, m1: DateMarker): number;
greatestWholeUnit(m0: DateMarker, m1: DateMarker): {
unit: string;
value: number;
};
countDurationsBetween(m0: DateMarker, m1: DateMarker, d: Duration): number;
startOf(m: DateMarker, unit: string): Date;
startOfYear(m: DateMarker): DateMarker;
startOfMonth(m: DateMarker): DateMarker;
startOfWeek(m: DateMarker): DateMarker;
computeWeekNumber(marker: DateMarker): number;
format(marker: DateMarker, formatter: DateFormatter, dateOptions?: {
forcedTzo?: number;
}): any;
formatRange(start: DateMarker, end: DateMarker, formatter: DateFormatter, dateOptions?: {
forcedStartTzo?: number;
forcedEndTzo?: number;
isEndExclusive?: boolean;
}): any;
formatIso(marker: DateMarker, extraOptions?: any): string;
timestampToMarker(ms: number): Date;
offsetForMarker(m: DateMarker): number;
toDate(m: DateMarker, forcedTzo?: number): Date;
}
}
declare module '@fullcalendar/core/datelib/formatting' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { CalendarSystem } from '@fullcalendar/core/datelib/calendar-system';
import { Locale } from '@fullcalendar/core/datelib/locale';
import { CmdFormatterFunc } from '@fullcalendar/core/datelib/formatting-cmd';
import { FuncFormatterFunc } from '@fullcalendar/core/datelib/formatting-func';
export interface ZonedMarker {
marker: DateMarker;
timeZoneOffset: number;
}
export interface ExpandedZonedMarker extends ZonedMarker {
array: number[];
year: number;
month: number;
day: number;
hour: number;
minute: number;
second: number;
millisecond: number;
}
export interface VerboseFormattingArg {
date: ExpandedZonedMarker;
start: ExpandedZonedMarker;
end?: ExpandedZonedMarker;
timeZone: string;
localeCodes: string[];
separator: string;
}
export interface DateFormattingContext {
timeZone: string;
locale: Locale;
calendarSystem: CalendarSystem;
computeWeekNumber: (d: DateMarker) => number;
weekLabel: string;
cmdFormatter?: CmdFormatterFunc;
}
export interface DateFormatter {
format(date: ZonedMarker, context: DateFormattingContext): any;
formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext): any;
}
export type FormatterInput = object | string | FuncFormatterFunc;
export function createFormatter(input: FormatterInput, defaultSeparator?: string): DateFormatter;
export function buildIsoString(marker: DateMarker, timeZoneOffset?: number, stripZeroTime?: boolean): string;
export function formatIsoTimeString(marker: DateMarker): string;
export function formatTimeZoneOffset(minutes: number, doIso?: boolean): string;
export function createVerboseFormattingArg(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext, separator?: string): VerboseFormattingArg;
}
declare module '@fullcalendar/core/datelib/timezone' {
export abstract class NamedTimeZoneImpl {
timeZoneName: string;
constructor(timeZoneName: string);
abstract offsetForArray(a: number[]): number;
abstract timestampToArray(ms: number): number[];
}
export type NamedTimeZoneImplClass = {
new (timeZoneName: string): NamedTimeZoneImpl;
};
}
declare module '@fullcalendar/core/datelib/parsing' {
export function parse(str: any): {
marker: Date;
isTimeUnspecified: boolean;
timeZoneOffset: any;
};
}
declare module '@fullcalendar/core/structs/event-source' {
import { EventInput } from '@fullcalendar/core/structs/event';
import Calendar from '@fullcalendar/core/Calendar';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { EventSourceFunc } from '@fullcalendar/core/event-sources/func-event-source';
import { EventUi } from '@fullcalendar/core/component/event-ui';
import { ConstraintInput, AllowFunc } from '@fullcalendar/core/validation';
export type EventSourceError = {
message: string;
response?: any;
[otherProp: string]: any;
};
export type EventInputTransformer = (eventInput: EventInput) => EventInput | null;
export type EventSourceSuccessResponseHandler = (rawData: any, response: any) => EventInput[] | void;
export type EventSourceErrorResponseHandler = (error: EventSourceError) => void;
export interface ExtendedEventSourceInput {
id?: string | number;
allDayDefault?: boolean;
eventDataTransform?: EventInputTransformer;
events?: EventInput[] | EventSourceFunc;
url?: string;
method?: string;
extraParams?: object | (() => object);
startParam?: string;
endParam?: string;
timeZoneParam?: string;
success?: EventSourceSuccessResponseHandler;
failure?: EventSourceErrorResponseHandler;
editable?: boolean;
startEditable?: boolean;
durationEditable?: boolean;
constraint?: ConstraintInput;
overlap?: boolean;
allow?: AllowFunc;
className?: string[] | string;
classNames?: string[] | string;
backgroundColor?: string;
borderColor?: string;
textColor?: string;
color?: string;
[otherProp: string]: any;
}
export type EventSourceInput = ExtendedEventSourceInput | // object in extended form
EventSourceFunc | // just a function
string;
export interface EventSource {
_raw: any;
sourceId: string;
sourceDefId: number;
meta: any;
publicId: string;
isFetching: boolean;
latestFetchId: string;
fetchRange: DateRange | null;
allDayDefault: boolean | null;
eventDataTransform: EventInputTransformer;
ui: EventUi;
success: EventSourceSuccessResponseHandler | null;
failure: EventSourceErrorResponseHandler | null;
extendedProps: any;
}
export type EventSourceHash = {
[sourceId: string]: EventSource;
};
export type EventSourceFetcher = (arg: {
eventSource: EventSource;
calendar: Calendar;
range: DateRange;
}, success: (res: {
rawEvents: EventInput[];
xhr?: XMLHttpRequest;
}) => void, failure: (error: EventSourceError) => void) => (void | PromiseLike);
export interface EventSourceDef {
ignoreRange?: boolean;
parseMeta: (raw: EventSourceInput) => object | null;
fetch: EventSourceFetcher;
}
export function doesSourceNeedRange(eventSource: EventSource, calendar: Calendar): boolean;
export function parseEventSource(raw: EventSourceInput, calendar: Calendar): EventSource | null;
}
declare module '@fullcalendar/core/interactions/interaction' {
import DateComponent from '@fullcalendar/core/component/DateComponent';
export abstract class Interaction {
component: DateComponent;
constructor(settings: InteractionSettings);
destroy(): void;
}
export type InteractionClass = {
new (settings: InteractionSettings): Interaction;
};
export interface InteractionSettingsInput {
el: HTMLElement;
useEventCenter?: boolean;
}
export interface InteractionSettings {
component: DateComponent;
el: HTMLElement;
useEventCenter: boolean;
}
export type InteractionSettingsStore = {
[componenUid: string]: InteractionSettings;
};
export function parseInteractionSettings(component: DateComponent, input: InteractionSettingsInput): InteractionSettings;
export function interactionSettingsToStore(settings: InteractionSettings): {
[x: string]: InteractionSettings;
};
export let interactionSettingsStore: InteractionSettingsStore;
}
declare module '@fullcalendar/core/interactions/pointer' {
export interface PointerDragEvent {
origEvent: UIEvent;
isTouch: boolean;
subjectEl: EventTarget;
pageX: number;
pageY: number;
deltaX: number;
deltaY: number;
}
}
declare module '@fullcalendar/core/interactions/hit' {
import DateComponent from '@fullcalendar/core/component/DateComponent';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { Rect } from '@fullcalendar/core/util/geom';
export interface Hit {
component: DateComponent;
dateSpan: DateSpan;
dayEl: HTMLElement;
rect: Rect;
layer: number;
}
}
declare module '@fullcalendar/core/interactions/date-selecting' {
import { Hit } from '@fullcalendar/core/interactions/hit';
export type dateSelectionJoinTransformer = (hit0: Hit, hit1: Hit) => any;
}
declare module '@fullcalendar/core/interactions/event-dragging' {
import Calendar from '@fullcalendar/core/Calendar';
import { EventMutation } from '@fullcalendar/core/structs/event-mutation';
import { Hit } from '@fullcalendar/core/interactions/hit';
import { EventDef } from '@fullcalendar/core/structs/event';
import { EventUi } from '@fullcalendar/core/component/event-ui';
import { View } from '@fullcalendar/core';
export type eventDragMutationMassager = (mutation: EventMutation, hit0: Hit, hit1: Hit) => void;
export type EventDropTransformers = (mutation: EventMutation, calendar: Calendar) => any;
export type eventIsDraggableTransformer = (val: boolean, eventDef: EventDef, eventUi: EventUi, view: View) => boolean;
}
declare module '@fullcalendar/core/interactions/event-resizing' {
import { Hit } from '@fullcalendar/core/interactions/hit';
export type EventResizeJoinTransforms = (hit0: Hit, hit1: Hit) => false | object;
}
declare module '@fullcalendar/core/interactions/ElementDragging' {
import EmitterMixin from '@fullcalendar/core/common/EmitterMixin';
export { ElementDragging as default, ElementDragging };
abstract class ElementDragging {
emitter: EmitterMixin;
constructor(el: HTMLElement);
destroy(): void;
abstract setIgnoreMove(bool: boolean): void;
setMirrorIsVisible(bool: boolean): void;
setMirrorNeedsRevert(bool: boolean): void;
setAutoScrollEnabled(bool: boolean): void;
}
export type ElementDraggingClass = {
new (el: HTMLElement): ElementDragging;
};
}
declare module '@fullcalendar/core/formatting-api' {
import { DateInput } from '@fullcalendar/core/datelib/env';
export function formatDate(dateInput: DateInput, settings?: {}): any;
export function formatRange(startInput: DateInput, endInput: DateInput, settings: any): any;
}
declare module '@fullcalendar/core/options' {
import { PluginDef } from '@fullcalendar/core/plugin-system';
export const config: any;
export const globalDefaults: {
defaultRangeSeparator: string;
titleRangeSeparator: string;
defaultTimedEventDuration: string;
defaultAllDayEventDuration: {
day: number;
};
forceEventDuration: boolean;
nextDayThreshold: string;
columnHeader: boolean;
defaultView: string;
aspectRatio: number;
header: {
left: string;
center: string;
right: string;
};
weekends: boolean;
weekNumbers: boolean;
weekNumberCalculation: string;
editable: boolean;
scrollTime: string;
minTime: string;
maxTime: string;
showNonCurrentDates: boolean;
lazyFetching: boolean;
startParam: string;
endParam: string;
timeZoneParam: string;
timeZone: string;
locales: any[];
locale: string;
timeGridEventMinHeight: number;
themeSystem: string;
dragRevertDuration: number;
dragScroll: boolean;
allDayMaintainDuration: boolean;
unselectAuto: boolean;
dropAccept: string;
eventOrder: string;
eventLimit: boolean;
eventLimitClick: string;
dayPopoverFormat: {
month: string;
day: string;
year: string;
};
handleWindowResize: boolean;
windowResizeDelay: number;
longPressDelay: number;
eventDragMinDistance: number;
};
export const rtlDefaults: {
header: {
left: string;
center: string;
right: string;
};
buttonIcons: {
prev: string;
next: string;
prevYear: string;
nextYear: string;
};
};
export function mergeOptions(optionObjs: any): any;
export function refinePluginDefs(pluginInputs: any[]): PluginDef[];
}
declare module '@fullcalendar/core/structs/recurring-event' {
import { EventInput, EventDef } from '@fullcalendar/core/structs/event';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { DateEnv } from '@fullcalendar/core/datelib/env';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
export interface ParsedRecurring {
typeData: any;
allDayGuess: boolean | null;
duration: Duration | null;
}
export interface RecurringType {
parse: (rawEvent: EventInput, leftoverProps: any, dateEnv: DateEnv) => ParsedRecurring | null;
expand: (typeData: any, framingRange: DateRange, dateEnv: DateEnv) => DateMarker[];
}
export function parseRecurring(eventInput: EventInput, allDayDefault: boolean | null, dateEnv: DateEnv, recurringTypes: RecurringType[], leftovers: any): {
allDay: any;
duration: Duration;
typeData: any;
typeId: number;
};
export function expandRecurringRanges(eventDef: EventDef, duration: Duration, framingRange: DateRange, dateEnv: DateEnv, recurringTypes: RecurringType[]): DateMarker[];
}
declare module '@fullcalendar/core/structs/drag-meta' {
import { Duration, DurationInput } from '@fullcalendar/core/datelib/duration';
import { EventNonDateInput } from '@fullcalendar/core/structs/event';
export interface DragMetaInput extends EventNonDateInput {
startTime?: DurationInput;
duration?: DurationInput;
create?: boolean;
sourceId?: string;
}
export interface DragMeta {
startTime: Duration | null;
duration: Duration | null;
create: boolean;
sourceId: string;
leftoverProps: object;
}
export function parseDragMeta(raw: DragMetaInput): DragMeta;
}
declare module '@fullcalendar/core/plugin-system' {
import { reducerFunc } from '@fullcalendar/core/reducers/types';
import { eventDefParserFunc } from '@fullcalendar/core/structs/event';
import { eventDefMutationApplier } from '@fullcalendar/core/structs/event-mutation';
import Calendar, { DatePointTransform, DateSpanTransform, CalendarInteractionClass, OptionChangeHandlerMap } from '@fullcalendar/core/Calendar';
import { ViewConfigInputHash } from '@fullcalendar/core/structs/view-config';
import { ViewSpec } from '@fullcalendar/core/structs/view-spec';
import View, { ViewProps } from '@fullcalendar/core/View';
import { CalendarComponentProps } from '@fullcalendar/core/CalendarComponent';
import { isPropsValidTester } from '@fullcalendar/core/validation';
import { eventDragMutationMassager, eventIsDraggableTransformer, EventDropTransformers } from '@fullcalendar/core/interactions/event-dragging';
import { dateSelectionJoinTransformer } from '@fullcalendar/core/interactions/date-selecting';
import { EventResizeJoinTransforms } from '@fullcalendar/core/interactions/event-resizing';
import { ExternalDefTransform } from '@fullcalendar/core/interactions/external-element-dragging';
import { InteractionClass } from '@fullcalendar/core/interactions/interaction';
import { ThemeClass } from '@fullcalendar/core/theme/Theme';
import { EventSourceDef } from '@fullcalendar/core/structs/event-source';
import { CmdFormatterFunc } from '@fullcalendar/core/datelib/formatting-cmd';
import { RecurringType } from '@fullcalendar/core/structs/recurring-event';
import { NamedTimeZoneImplClass } from '@fullcalendar/core/datelib/timezone';
import { ElementDraggingClass } from '@fullcalendar/core/interactions/ElementDragging';
export interface PluginDefInput {
deps?: PluginDef[];
reducers?: reducerFunc[];
eventDefParsers?: eventDefParserFunc[];
isDraggableTransformers?: eventIsDraggableTransformer[];
eventDragMutationMassagers?: eventDragMutationMassager[];
eventDefMutationAppliers?: eventDefMutationApplier[];
dateSelectionTransformers?: dateSelectionJoinTransformer[];
datePointTransforms?: DatePointTransform[];
dateSpanTransforms?: DateSpanTransform[];
views?: ViewConfigInputHash;
viewPropsTransformers?: ViewPropsTransformerClass[];
isPropsValid?: isPropsValidTester;
externalDefTransforms?: ExternalDefTransform[];
eventResizeJoinTransforms?: EventResizeJoinTransforms[];
viewContainerModifiers?: ViewContainerModifier[];
eventDropTransformers?: EventDropTransformers[];
componentInteractions?: InteractionClass[];
calendarInteractions?: CalendarInteractionClass[];
themeClasses?: {
[themeSystemName: string]: ThemeClass;
};
eventSourceDefs?: EventSourceDef[];
cmdFormatter?: CmdFormatterFunc;
recurringTypes?: RecurringType[];
namedTimeZonedImpl?: NamedTimeZoneImplClass;
defaultView?: string;
elementDraggingImpl?: ElementDraggingClass;
optionChangeHandlers?: OptionChangeHandlerMap;
}
export interface PluginHooks {
reducers: reducerFunc[];
eventDefParsers: eventDefParserFunc[];
isDraggableTransformers: eventIsDraggableTransformer[];
eventDragMutationMassagers: eventDragMutationMassager[];
eventDefMutationAppliers: eventDefMutationApplier[];
dateSelectionTransformers: dateSelectionJoinTransformer[];
datePointTransforms: DatePointTransform[];
dateSpanTransforms: DateSpanTransform[];
views: ViewConfigInputHash;
viewPropsTransformers: ViewPropsTransformerClass[];
isPropsValid: isPropsValidTester | null;
externalDefTransforms: ExternalDefTransform[];
eventResizeJoinTransforms: EventResizeJoinTransforms[];
viewContainerModifiers: ViewContainerModifier[];
eventDropTransformers: EventDropTransformers[];
componentInteractions: InteractionClass[];
calendarInteractions: CalendarInteractionClass[];
themeClasses: {
[themeSystemName: string]: ThemeClass;
};
eventSourceDefs: EventSourceDef[];
cmdFormatter?: CmdFormatterFunc;
recurringTypes: RecurringType[];
namedTimeZonedImpl?: NamedTimeZoneImplClass;
defaultView: string;
elementDraggingImpl?: ElementDraggingClass;
optionChangeHandlers: OptionChangeHandlerMap;
}
export interface PluginDef extends PluginHooks {
id: string;
deps: PluginDef[];
}
export type ViewPropsTransformerClass = new () => ViewPropsTransformer;
export interface ViewPropsTransformer {
transform(viewProps: ViewProps, viewSpec: ViewSpec, calendarProps: CalendarComponentProps, view: View): any;
}
export type ViewContainerModifier = (contentEl: HTMLElement, calendar: Calendar) => void;
export function createPlugin(input: PluginDefInput): PluginDef;
export class PluginSystem {
hooks: PluginHooks;
addedHash: {
[pluginId: string]: true;
};
constructor();
add(plugin: PluginDef): void;
}
}
declare module '@fullcalendar/core/reducers/types' {
import { EventInput, EventInstanceHash } from '@fullcalendar/core/structs/event';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventMutation } from '@fullcalendar/core/structs/event-mutation';
import { EventSource, EventSourceHash, EventSourceError } from '@fullcalendar/core/structs/event-source';
import { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { DateEnv } from '@fullcalendar/core/datelib/env';
import Calendar from '@fullcalendar/core/Calendar';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
export interface CalendarState {
eventSources: EventSourceHash;
eventSourceLoadingLevel: number;
loadingLevel: number;
viewType: string;
currentDate: DateMarker;
dateProfile: DateProfile | null;
eventStore: EventStore;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export type reducerFunc = (state: CalendarState, action: Action, calendar: Calendar) => CalendarState;
export type Action = {
type: 'INIT';
} | // wont it create another rerender?
{
type: 'PREV';
} | {
type: 'NEXT';
} | {
type: 'SET_DATE';
dateMarker: DateMarker;
} | {
type: 'SET_VIEW_TYPE';
viewType: string;
dateMarker?: DateMarker;
} | {
type: 'SELECT_DATES';
selection: DateSpan;
} | {
type: 'UNSELECT_DATES';
} | {
type: 'SELECT_EVENT';
eventInstanceId: string;
} | {
type: 'UNSELECT_EVENT';
} | {
type: 'SET_EVENT_DRAG';
state: EventInteractionState;
} | {
type: 'UNSET_EVENT_DRAG';
} | {
type: 'SET_EVENT_RESIZE';
state: EventInteractionState;
} | {
type: 'UNSET_EVENT_RESIZE';
} | {
type: 'ADD_EVENT_SOURCES';
sources: EventSource[];
} | {
type: 'REMOVE_EVENT_SOURCE';
sourceId: string;
} | {
type: 'REMOVE_ALL_EVENT_SOURCES';
} | {
type: 'FETCH_EVENT_SOURCES';
sourceIds?: string[];
} | // if no sourceIds, fetch all
{
type: 'CHANGE_TIMEZONE';
oldDateEnv: DateEnv;
} | {
type: 'RECEIVE_EVENTS';
sourceId: string;
fetchId: string;
fetchRange: DateRange | null;
rawEvents: EventInput[];
} | {
type: 'RECEIVE_EVENT_ERROR';
sourceId: string;
fetchId: string;
fetchRange: DateRange | null;
error: EventSourceError;
} | // need all these?
{
type: 'ADD_EVENTS';
eventStore: EventStore;
} | {
type: 'MERGE_EVENTS';
eventStore: EventStore;
} | {
type: 'MUTATE_EVENTS';
instanceId: string;
mutation: EventMutation;
fromApi?: boolean;
} | {
type: 'REMOVE_EVENT_DEF';
defId: string;
} | {
type: 'REMOVE_EVENT_INSTANCES';
instances: EventInstanceHash;
} | {
type: 'REMOVE_ALL_EVENTS';
} | {
type: 'RESET_EVENTS';
};
}
declare module '@fullcalendar/core/CalendarComponent' {
import Component, { ComponentContext } from '@fullcalendar/core/component/Component';
import { ViewSpec } from '@fullcalendar/core/structs/view-spec';
import View from '@fullcalendar/core/View';
import Toolbar from '@fullcalendar/core/Toolbar';
import DateProfileGenerator, { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventUiHash } from '@fullcalendar/core/component/event-ui';
import { BusinessHoursInput } from '@fullcalendar/core/structs/business-hours';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { CalendarState } from '@fullcalendar/core/reducers/types';
export interface CalendarComponentProps extends CalendarState {
viewSpec: ViewSpec;
dateProfileGenerator: DateProfileGenerator;
eventUiBases: EventUiHash;
}
export { CalendarComponent as default, CalendarComponent };
class CalendarComponent extends Component {
view: View;
header: Toolbar;
footer: Toolbar;
computeTitle: (dateProfile: any, viewOptions: any) => string;
parseBusinessHours: (input: BusinessHoursInput) => EventStore;
el: HTMLElement;
contentEl: HTMLElement;
isHeightAuto: boolean;
viewHeight: number;
constructor(context: ComponentContext, el: HTMLElement);
destroy(): void;
toggleElClassNames(bool: boolean): void;
render(props: CalendarComponentProps): void;
renderToolbars(viewSpec: ViewSpec, dateProfile: DateProfile, currentDate: DateMarker, dateProfileGenerator: DateProfileGenerator, title: string): void;
renderView(props: CalendarComponentProps, title: string): void;
updateSize(isResize?: boolean): void;
computeHeightVars(): void;
queryToolbarsHeight(): number;
freezeHeight(): void;
thawHeight(): void;
}
}
declare module '@fullcalendar/core/common/DayHeader' {
import Component, { ComponentContext } from '@fullcalendar/core/component/Component';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
export interface DayTableHeaderProps {
dates: DateMarker[];
dateProfile: DateProfile;
datesRepDistinctDays: boolean;
renderIntroHtml?: () => string;
}
export { DayHeader as default, DayHeader };
class DayHeader extends Component {
el: HTMLElement;
thead: HTMLElement;
constructor(context: ComponentContext, parentEl: HTMLElement);
destroy(): void;
render(props: DayTableHeaderProps): void;
}
}
declare module '@fullcalendar/core/common/table-utils' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
import { ComponentContext } from '@fullcalendar/core/component/Component';
export function computeFallbackHeaderFormat(datesRepDistinctDays: boolean, dayCnt: number): {
weekday: string;
month?: undefined;
day?: undefined;
omitCommas?: undefined;
} | {
weekday: string;
month: string;
day: string;
omitCommas: boolean;
};
export function renderDateCell(dateMarker: DateMarker, dateProfile: DateProfile, datesRepDistinctDays: any, colCnt: any, colHeadFormat: any, context: ComponentContext, colspan?: any, otherAttrs?: any): string;
}
declare module '@fullcalendar/core/common/DaySeries' {
import DateProfileGenerator from '@fullcalendar/core/DateProfileGenerator';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
export interface DaySeriesSeg {
firstIndex: number;
lastIndex: number;
isStart: boolean;
isEnd: boolean;
}
export { DaySeries as default, DaySeries };
class DaySeries {
cnt: number;
dates: DateMarker[];
indices: number[];
constructor(range: DateRange, dateProfileGenerator: DateProfileGenerator);
sliceRange(range: DateRange): DaySeriesSeg | null;
}
}
declare module '@fullcalendar/core/interactions/event-interaction-state' {
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { Seg } from '@fullcalendar/core/component/DateComponent';
export interface EventInteractionState {
affectedEvents: EventStore;
mutatedEvents: EventStore;
isEvent: boolean;
origSeg: Seg | null;
}
}
declare module '@fullcalendar/core/component/event-rendering' {
import { EventDef, EventTuple, EventDefHash } from '@fullcalendar/core/structs/event';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { Seg } from '@fullcalendar/core/component/DateComponent';
import View from '@fullcalendar/core/View';
import { EventUi, EventUiHash } from '@fullcalendar/core/component/event-ui';
export interface EventRenderRange extends EventTuple {
ui: EventUi;
range: DateRange;
isStart: boolean;
isEnd: boolean;
}
export function sliceEventStore(eventStore: EventStore, eventUiBases: EventUiHash, framingRange: DateRange, nextDayThreshold?: Duration): {
bg: EventRenderRange[];
fg: EventRenderRange[];
};
export function hasBgRendering(def: EventDef): boolean;
export function filterSegsViaEls(view: View, segs: Seg[], isMirror: boolean): Seg[];
export function getElSeg(el: HTMLElement): Seg | null;
export function compileEventUis(eventDefs: EventDefHash, eventUiBases: EventUiHash): {
[key: string]: EventUi;
};
export function compileEventUi(eventDef: EventDef, eventUiBases: EventUiHash): EventUi;
}
declare module '@fullcalendar/core/common/DayTable' {
import DaySeries from '@fullcalendar/core/common/DaySeries';
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
import { Seg } from '@fullcalendar/core/component/DateComponent';
export interface DayTableSeg extends Seg {
row: number;
firstCol: number;
lastCol: number;
}
export interface DayTableCell {
date: DateMarker;
htmlAttrs?: string;
}
export { DayTable as default, DayTable };
class DayTable {
rowCnt: number;
colCnt: number;
cells: DayTableCell[][];
headerDates: DateMarker[];
constructor(daySeries: DaySeries, breakOnWeeks: boolean);
sliceRange(range: DateRange): DayTableSeg[];
}
}
declare module '@fullcalendar/core/common/slicing-utils' {
import { DateRange } from '@fullcalendar/core/datelib/date-range';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventUiHash } from '@fullcalendar/core/component/event-ui';
import { DateProfile } from '@fullcalendar/core/DateProfileGenerator';
import DateComponent, { Seg, EventSegUiInteractionState } from '@fullcalendar/core/component/DateComponent';
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { Duration } from '@fullcalendar/core/datelib/duration';
import { DateMarker } from '@fullcalendar/core/datelib/marker';
export interface SliceableProps {
dateSelection: DateSpan;
businessHours: EventStore;
eventStore: EventStore;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
eventSelection: string;
eventUiBases: EventUiHash;
}
export interface SlicedProps {
dateSelectionSegs: SegType[];
businessHourSegs: SegType[];
fgEventSegs: SegType[];
bgEventSegs: SegType[];
eventDrag: EventSegUiInteractionState | null;
eventResize: EventSegUiInteractionState | null;
eventSelection: string;
}
export { Slicer as default, Slicer };
abstract class Slicer {
abstract sliceRange(dateRange: DateRange, ...extraArgs: ExtraArgs): SegType[];
sliceProps(props: SliceableProps, dateProfile: DateProfile, nextDayThreshold: Duration | null, component: DateComponent, // TODO: kill
...extraArgs: ExtraArgs): SlicedProps;
sliceNowDate(// does not memoize
date: DateMarker, component: DateComponent, // TODO: kill
...extraArgs: ExtraArgs): SegType[];
}
}
declare module '@fullcalendar/core/structs/event-mutation' {
import { Duration } from '@fullcalendar/core/datelib/duration';
import { EventStore } from '@fullcalendar/core/structs/event-store';
import { EventDef } from '@fullcalendar/core/structs/event';
import Calendar from '@fullcalendar/core/Calendar';
import { EventUiHash } from '@fullcalendar/core/component/event-ui';
export interface EventMutation {
datesDelta?: Duration;
startDelta?: Duration;
endDelta?: Duration;
standardProps?: any;
extendedProps?: any;
}
export function applyMutationToEventStore(eventStore: EventStore, eventConfigBase: EventUiHash, mutation: EventMutation, calendar: Calendar): EventStore;
export type eventDefMutationApplier = (eventDef: EventDef, mutation: EventMutation, calendar: Calendar) => void;
}
declare module '@fullcalendar/core/validation' {
import { EventStore } from '@fullcalendar/core/structs/event-store';
import Calendar from '@fullcalendar/core/Calendar';
import { DateSpan, DateSpanApi } from '@fullcalendar/core/structs/date-span';
import EventApi from '@fullcalendar/core/api/EventApi';
import { EventInput } from '@fullcalendar/core/structs/event';
import { EventInteractionState } from '@fullcalendar/core/interactions/event-interaction-state';
import { SplittableProps } from '@fullcalendar/core/component/event-splitting';
export type ConstraintInput = 'businessHours' | string | EventInput | EventInput[];
export type Constraint = 'businessHours' | string | EventStore | false;
export type OverlapFunc = ((stillEvent: EventApi, movingEvent: EventApi | null) => boolean);
export type AllowFunc = (span: DateSpanApi, movingEvent: EventApi | null) => boolean;
export type isPropsValidTester = (props: SplittableProps, calendar: Calendar) => boolean;
export function isInteractionValid(interaction: EventInteractionState, calendar: Calendar): boolean;
export function isDateSelectionValid(dateSelection: DateSpan, calendar: Calendar): boolean;
export function isPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta?: {}, filterConfig?: any): boolean;
export function normalizeConstraint(input: ConstraintInput, calendar: Calendar): Constraint | null;
}
declare module '@fullcalendar/core/api/EventApi' {
import Calendar from '@fullcalendar/core/Calendar';
import { EventDef, EventInstance } from '@fullcalendar/core/structs/event';
import { EventMutation } from '@fullcalendar/core/structs/event-mutation';
import { DateInput } from '@fullcalendar/core/datelib/env';
import { DurationInput } from '@fullcalendar/core/datelib/duration';
import { FormatterInput } from '@fullcalendar/core/datelib/formatting';
import EventSourceApi from '@fullcalendar/core/api/EventSourceApi';
export { EventApi as default, EventApi };
class EventApi {
_calendar: Calendar;
_def: EventDef;
_instance: EventInstance | null;
constructor(calendar: Calendar, def: EventDef, instance?: EventInstance);
setProp(name: string, val: string): void;
setExtendedProp(name: string, val: any): void;
setStart(startInput: DateInput, options?: {
granularity?: string;
maintainDuration?: boolean;
}): void;
setEnd(endInput: DateInput | null, options?: {
granularity?: string;
}): void;
setDates(startInput: DateInput, endInput: DateInput | null, options?: {
allDay?: boolean;
granularity?: string;
}): void;
moveStart(deltaInput: DurationInput): void;
moveEnd(deltaInput: DurationInput): void;
moveDates(deltaInput: DurationInput): void;
setAllDay(allDay: boolean, options?: {
maintainDuration?: boolean;
}): void;
formatRange(formatInput: FormatterInput): any;
mutate(mutation: EventMutation): void;
remove(): void;
readonly source: EventSourceApi | null;
readonly start: Date | null;
readonly end: Date | null;
readonly id: string;
readonly groupId: string;
readonly allDay: boolean;
readonly title: string;
readonly url: string;
readonly rendering: string;
readonly startEditable: boolean;
readonly durationEditable: boolean;
readonly constraint: any;
readonly overlap: any;
readonly allow: any;
readonly backgroundColor: string;
readonly borderColor: string;
readonly textColor: string;
readonly classNames: string[];
readonly extendedProps: any;
}
}
declare module '@fullcalendar/core/util/requestJson' {
export default function requestJson(method: string, url: string, params: object, successCallback: any, failureCallback: any): void;
}
declare module '@fullcalendar/core/datelib/locale' {
export type LocaleCodeArg = string | string[];
export type LocaleSingularArg = LocaleCodeArg | RawLocale;
export interface Locale {
codeArg: LocaleCodeArg;
codes: string[];
week: {
dow: number;
doy: number;
};
simpleNumberFormat: Intl.NumberFormat;
options: any;
}
export interface RawLocale {
code: string;
[otherProp: string]: any;
}
export type RawLocaleMap = {
[code: string]: RawLocale;
};
export interface RawLocaleInfo {
map: RawLocaleMap;
defaultCode: string;
}
export function parseRawLocales(explicitRawLocales: RawLocale[]): RawLocaleInfo;
export function buildLocale(inputSingular: LocaleSingularArg, available: RawLocaleMap): Locale;
}
declare module '@fullcalendar/core/OptionsManager' {
export { OptionsManager as default, OptionsManager };
class OptionsManager {
dirDefaults: any;
localeDefaults: any;
overrides: any;
dynamicOverrides: any;
computed: any;
constructor(overrides: any);
mutate(updates: any, removals: string[], isDynamic?: boolean): void;
compute(): void;
}
}
declare module '@fullcalendar/core/api/EventSourceApi' {
import Calendar from '@fullcalendar/core/Calendar';
import { EventSource } from '@fullcalendar/core/structs/event-source';
export { EventSourceApi as default, EventSourceApi };
class EventSourceApi {
calendar: Calendar;
internalEventSource: EventSource;
constructor(calendar: Calendar, internalEventSource: EventSource);
remove(): void;
refetch(): void;
readonly id: string;
readonly url: string;
}
}
declare module '@fullcalendar/core/structs/view-config' {
import View from '@fullcalendar/core/View';
import { ViewSpec } from '@fullcalendar/core/structs/view-spec';
import { ComponentContext } from '@fullcalendar/core/component/Component';
import DateProfileGenerator from '@fullcalendar/core/DateProfileGenerator';
export type ViewClass = new (context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement) => View;
export interface ViewConfigObjectInput {
type?: string;
class?: ViewClass;
[optionName: string]: any;
}
export type ViewConfigInput = ViewClass | ViewConfigObjectInput;
export type ViewConfigInputHash = {
[viewType: string]: ViewConfigInput;
};
export interface ViewConfig {
superType: string;
class: ViewClass | null;
options: any;
}
export type ViewConfigHash = {
[viewType: string]: ViewConfig;
};
export function parseViewConfigs(inputs: ViewConfigInputHash): ViewConfigHash;
}
declare module '@fullcalendar/core/datelib/calendar-system' {
import { DateMarker } from '@fullcalendar/core/datelib/marker';
export interface CalendarSystem {
getMarkerYear(d: DateMarker): number;
getMarkerMonth(d: DateMarker): number;
getMarkerDay(d: DateMarker): number;
arrayToMarker(arr: number[]): DateMarker;
markerToArray(d: DateMarker): number[];
}
export function registerCalendarSystem(name: any, theClass: any): void;
export function createCalendarSystem(name: any): any;
}
declare module '@fullcalendar/core/datelib/formatting-cmd' {
import { DateFormatter, DateFormattingContext, ZonedMarker, VerboseFormattingArg } from '@fullcalendar/core/datelib/formatting';
export type CmdFormatterFunc = (cmd: string, arg: VerboseFormattingArg) => string;
export class CmdFormatter implements DateFormatter {
cmdStr: string;
separator: string;
constructor(cmdStr: string, separator?: string);
format(date: ZonedMarker, context: DateFormattingContext): string;
formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext): string;
}
}
declare module '@fullcalendar/core/datelib/formatting-func' {
import { DateFormatter, DateFormattingContext, ZonedMarker, VerboseFormattingArg } from '@fullcalendar/core/datelib/formatting';
export type FuncFormatterFunc = (arg: VerboseFormattingArg) => string;
export class FuncFormatter implements DateFormatter {
func: FuncFormatterFunc;
constructor(func: FuncFormatterFunc);
format(date: ZonedMarker, context: DateFormattingContext): string;
formatRange(start: ZonedMarker, end: ZonedMarker, context: DateFormattingContext): string;
}
}
declare module '@fullcalendar/core/event-sources/func-event-source' {
import { EventSourceError } from '@fullcalendar/core/structs/event-source';
import { EventInput } from '@fullcalendar/core/structs/event';
export type EventSourceFunc = (arg: {
start: Date;
end: Date;
timeZone: string;
}, successCallback: (events: EventInput[]) => void, failureCallback: (error: EventSourceError) => void) => (void | PromiseLike);
const _default: import("@fullcalendar/core/plugin-system").PluginDef;
export default _default;
}
declare module '@fullcalendar/core/interactions/external-element-dragging' {
import { DateSpan } from '@fullcalendar/core/structs/date-span';
import { DragMeta } from '@fullcalendar/core/structs/drag-meta';
export type ExternalDefTransform = (dateSpan: DateSpan, dragMeta: DragMeta) => any;
}
declare module '@fullcalendar/core/Toolbar' {
import Component, { ComponentContext } from '@fullcalendar/core/component/Component';
export interface ToolbarRenderProps {
layout: any;
title: string;
activeButton: string;
isTodayEnabled: boolean;
isPrevEnabled: boolean;
isNextEnabled: boolean;
}
export { Toolbar as default, Toolbar };
class Toolbar extends Component {
el: HTMLElement;
viewsWithButtons: any;
constructor(context: ComponentContext, extraClassName: any);
destroy(): void;
render(props: ToolbarRenderProps): void;
renderLayout(layout: any): void;
unrenderLayout(): void;
renderSection(position: any, buttonStr: any): HTMLElement;
updateToday(isTodayEnabled: any): void;
updatePrev(isPrevEnabled: any): void;
updateNext(isNextEnabled: any): void;
updateTitle(text: any): void;
updateActiveButton(buttonName?: any): void;
toggleButtonEnabled(buttonName: any, bool: any): void;
}
}
/*!
FullCalendar Core Package v4.3.1
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).FullCalendar={})}(this,function(e){"use strict";var t={className:!0,colSpan:!0,rowSpan:!0},n={"");document.body.appendChild(e);var t=e.firstChild.getBoundingClientRect().left>e.getBoundingClientRect().left;return c(e),t}()),S}function D(e){return e=Math.max(0,e),e=Math.round(e)}function T(e,t){void 0===t&&(t=!1);var n=window.getComputedStyle(e),r=parseInt(n.borderLeftWidth,10)||0,i=parseInt(n.borderRightWidth,10)||0,o=parseInt(n.borderTopWidth,10)||0,a=parseInt(n.borderBottomWidth,10)||0,s=D(e.offsetWidth-e.clientWidth-r-i),u={borderLeft:r,borderRight:i,borderTop:o,borderBottom:a,scrollbarBottom:D(e.offsetHeight-e.clientHeight-o-a),scrollbarLeft:0,scrollbarRight:0};return b()&&"rtl"===n.direction?u.scrollbarLeft=s:u.scrollbarRight=s,t&&(u.paddingLeft=parseInt(n.paddingLeft,10)||0,u.paddingRight=parseInt(n.paddingRight,10)||0,u.paddingTop=parseInt(n.paddingTop,10)||0,u.paddingBottom=parseInt(n.paddingBottom,10)||0),u}function w(e,t){void 0===t&&(t=!1);var n=R(e),r=T(e,t),i={left:n.left+r.borderLeft+r.scrollbarLeft,right:n.right-r.borderRight-r.scrollbarRight,top:n.top+r.borderTop,bottom:n.bottom-r.borderBottom-r.scrollbarBottom};return t&&(i.left+=r.paddingLeft,i.right-=r.paddingRight,i.top+=r.paddingTop,i.bottom-=r.paddingBottom),i}function R(e){var t=e.getBoundingClientRect();return{left:t.left+window.pageXOffset,top:t.top+window.pageYOffset,right:t.right+window.pageXOffset,bottom:t.bottom+window.pageYOffset}}function I(e){return e.getBoundingClientRect().height+C(e)}function C(e){var t=window.getComputedStyle(e);return parseInt(t.marginTop,10)+parseInt(t.marginBottom,10)}function M(e){for(var t=[];e instanceof HTMLElement;){var n=window.getComputedStyle(e);if("fixed"===n.position)break;/(auto|scroll)/.test(n.overflow+n.overflowY+n.overflowX)&&t.push(e),e=e.parentNode}return t}function k(e){e.preventDefault()}function O(e,t,n,r){function i(e){var t=p(e.target,n);t&&r.call(t,e,t)}return e.addEventListener(t,i),function(){e.removeEventListener(t,i)}}var _=["webkitTransitionEnd","otransitionend","oTransitionEnd","msTransitionEnd","transitionend"];var P=["sun","mon","tue","wed","thu","fri","sat"];function x(e,t){var n=Z(e);return n[2]+=t,j(n)}function H(e,t){var n=Z(e);return n[6]+=t,j(n)}function N(e,t){return(t.valueOf()-e.valueOf())/864e5}function z(e,t){var n=B(e),r=B(t);return{years:0,months:0,days:Math.round(N(n,r)),milliseconds:t.valueOf()-r.valueOf()-(e.valueOf()-n.valueOf())}}function U(e,t){var n=L(e,t);return null!==n&&n%7==0?n/7:null}function L(e,t){return q(e)===q(t)?Math.round(N(e,t)):null}function B(e){return j([e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()])}function V(e,t,n,r){var i=j([t,0,1+A(t,n,r)]),o=B(e),a=Math.round(N(i,o));return Math.floor(a/7)+1}function A(e,t,n){var r=7+t-n;return-((7+j([e,0,r]).getUTCDay()-t)%7)+r-1}function F(e){return[e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()]}function W(e){return new Date(e[0],e[1]||0,null==e[2]?1:e[2],e[3]||0,e[4]||0,e[5]||0)}function Z(e){return[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds()]}function j(e){return 1===e.length&&(e=e.concat([0])),new Date(Date.UTC.apply(Date,e))}function Y(e){return!isNaN(e.valueOf())}function q(e){return 1e3*e.getUTCHours()*60*60+1e3*e.getUTCMinutes()*60+1e3*e.getUTCSeconds()+e.getUTCMilliseconds()}var G=["years","months","days","milliseconds"],X=/^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/;function J(e,t){var n;return"string"==typeof e?function(e){var t=X.exec(e);if(t){var n=t[1]?-1:1;return{years:0,months:0,days:n*(t[2]?parseInt(t[2],10):0),milliseconds:n*(60*(t[3]?parseInt(t[3],10):0)*60*1e3+60*(t[4]?parseInt(t[4],10):0)*1e3+1e3*(t[5]?parseInt(t[5],10):0)+(t[6]?parseInt(t[6],10):0))}}return null}(e):"object"==typeof e&&e?K(e):"number"==typeof e?K(((n={})[t||"milliseconds"]=e,n)):null}function K(e){return{years:e.years||e.year||0,months:e.months||e.month||0,days:(e.days||e.day||0)+7*Q(e),milliseconds:60*(e.hours||e.hour||0)*60*1e3+60*(e.minutes||e.minute||0)*1e3+1e3*(e.seconds||e.second||0)+(e.milliseconds||e.millisecond||e.ms||0)}}function Q(e){return e.weeks||e.week||0}function $(e,t){return e.years===t.years&&e.months===t.months&&e.days===t.days&&e.milliseconds===t.milliseconds}function ee(e){return te(e)/864e5}function te(e){return 31536e6*e.years+2592e6*e.months+864e5*e.days+e.milliseconds}function ne(e,t){var n=e.milliseconds;if(n){if(n%1e3!=0)return{unit:"millisecond",value:n};if(n%6e4!=0)return{unit:"second",value:n/1e3};if(n%36e5!=0)return{unit:"minute",value:n/6e4};if(n)return{unit:"hour",value:n/36e5}}return e.days?t||e.days%7!=0?{unit:"day",value:e.days}:{unit:"week",value:e.days/7}:e.months?{unit:"month",value:e.months}:e.years?{unit:"year",value:e.years}:{unit:"millisecond",value:0}}function re(e){e.forEach(function(e){e.style.height=""})}function ie(e){var t,n,r=[],i=[];for("string"==typeof e?i=e.split(/\s*,\s*/):"function"==typeof e?i=[e]:Array.isArray(e)&&(i=e),t=0;t=te(t)&&(r=x(r,1))}return e.start&&(n=B(e.start),r&&r<=n&&(r=x(n,1))),{start:n,end:r}}function ye(e,t,n,r){return"year"===r?J(n.diffWholeYears(e,t),"year"):"month"===r?J(n.diffWholeMonths(e,t),"month"):z(e,t)}var me=function(e,t){return(me=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)};function Ee(e,t){function n(){this.constructor=e}me(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var Se=function(){return(Se=Object.assign||function(e){for(var t,n=1,r=arguments.length;n=0;o--)if("object"==typeof(a=e[o][r])&&a)i.unshift(a);else if(void 0!==a){u[r]=a;break}i.length&&(u[r]=Te(i))}for(n=e.length-1;n>=0;n--)for(r in s=e[n])r in u||(u[r]=s[r]);return u}function we(e,t){var n={};for(var r in e)t(e[r],r)&&(n[r]=e[r]);return n}function Re(e,t){var n={};for(var r in e)n[r]=t(e[r],r);return n}function Ie(e){for(var t={},n=0,r=e;no&&i.push({start:o,end:r.start}),r.end>o&&(o=r.end);return ot.start)&&(null===e.start||null===t.end||e.start=e.start)&&(null===e.end||null!==t.end&&t.end<=e.end)}function Ze(e,t){return(null===e.start||t>=e.start)&&(null===e.end||t1)||"numeric"!==r.year&&"2-digit"!==r.year||"numeric"!==r.month&&"2-digit"!==r.month||"numeric"!==r.day&&"2-digit"!==r.day||(a=1);var s=this.format(e,n),u=this.format(t,n);if(s===u)return s;var l=nt(function(e,t){var n={};for(var r in e)r in Xe&&!(Xe[r]<=t)||(n[r]=e[r]);return n}(r,a),i,n),c=l(e),d=l(t),f=function(e,t,n,r){var i=0;for(;i/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function _t(e){var t=[];for(var n in e){var r=e[n];null!=r&&""!==r&&t.push(n+":"+r)}return t.join(";")}function Pt(e){return Array.isArray(e)?e:"string"==typeof e?e.split(/\s+/):[]}var xt={editable:Boolean,startEditable:Boolean,durationEditable:Boolean,constraint:null,overlap:null,allow:null,className:Pt,classNames:Pt,color:String,backgroundColor:String,borderColor:String,textColor:String};function Ht(e,t,n){var r=he(e,xt,{},n),i=function(e,t){return Array.isArray(e)?ke(e,"",t,!0):"object"==typeof e&&e?ke([e],"",t,!0):null!=e?String(e):null}(r.constraint,t);return{startEditable:null!=r.startEditable?r.startEditable:r.editable,durationEditable:null!=r.durationEditable?r.durationEditable:r.editable,constraints:null!=i?[i]:[],overlap:r.overlap,allows:null!=r.allow?[r.allow]:[],backgroundColor:r.backgroundColor||r.color,borderColor:r.borderColor||r.color,textColor:r.textColor,classNames:r.classNames.concat(r.className)}}function Nt(e,t,n,r){var i={},o={};for(var a in xt){var s=e+ue(a);i[a]=t[s],o[s]=!0}if("event"===e&&(i.editable=t.editable),r)for(var a in t)o[a]||(r[a]=t[a]);return Ht(i,n)}var zt={startEditable:null,durationEditable:null,constraints:[],overlap:null,allows:[],backgroundColor:"",borderColor:"",textColor:"",classNames:[]};function Ut(e){return e.reduce(Lt,zt)}function Lt(e,t){return{startEditable:null!=t.startEditable?t.startEditable:e.startEditable,durationEditable:null!=t.durationEditable?t.durationEditable:e.durationEditable,constraints:e.constraints.concat(t.constraints),overlap:"boolean"==typeof t.overlap?t.overlap:e.overlap,allows:e.allows.concat(t.allows),backgroundColor:t.backgroundColor||e.backgroundColor,borderColor:t.borderColor||e.borderColor,textColor:t.textColor||e.textColor,classNames:e.classNames.concat(t.classNames)}}var Bt={id:String,groupId:String,title:String,url:String,rendering:String,extendedProps:null},Vt={start:null,date:null,end:null,allDay:null},At=0;function Ft(e,t,n,r){var i=function(e,t){var n=null;if(e){var r=t.state.eventSources[e];n=r.allDayDefault}null==n&&(n=t.opt("allDayDefault"));return n}(t,n),o={},a=function(e,t,n,r,i){for(var o=0;o'+r+"":""+r+""}function Qt(e,t,n,r){var i,o,a=n.calendar,s=n.view,u=n.theme,l=n.dateEnv,c=[];return Ze(t.activeRange,e)?(c.push("fc-"+P[e.getUTCDay()]),s.opt("monthMode")&&l.getMonth(e)!==l.getMonth(t.currentRange.start)&&c.push("fc-other-month"),o=x(i=B(a.getNow()),1),e=o?c.push("fc-future"):(c.push("fc-today"),!0!==r&&c.push(u.getClass("today")))):c.push("fc-disabled-day"),c}function $t(e,t,n){var r=!1,i=function(){r||(r=!0,t.apply(this,arguments))},o=function(){r||(r=!0,n&&n.apply(this,arguments))},a=e(i,o);a&&"function"==typeof a.then&&a.then(i,o)}var en=function(){function e(){}return e.mixInto=function(e){this.mixIntoObj(e.prototype)},e.mixIntoObj=function(e){var t=this;Object.getOwnPropertyNames(this.prototype).forEach(function(n){e[n]||(e[n]=t.prototype[n])})},e.mixOver=function(e){var t=this;Object.getOwnPropertyNames(this.prototype).forEach(function(n){e.prototype[n]=t.prototype[n]})},e}(),tn=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return Ee(t,e),t.prototype.on=function(e,t){return nn(this._handlers||(this._handlers={}),e,t),this},t.prototype.one=function(e,t){return nn(this._oneHandlers||(this._oneHandlers={}),e,t),this},t.prototype.off=function(e,t){return this._handlers&&rn(this._handlers,e,t),this._oneHandlers&&rn(this._oneHandlers,e,t),this},t.prototype.trigger=function(e){for(var t=[],n=1;n=n[t]&&e=n[t]&&e0},e.prototype.canScrollHorizontally=function(){return this.getMaxScrollLeft()>0},e.prototype.canScrollUp=function(){return this.getScrollTop()>0},e.prototype.canScrollDown=function(){return this.getScrollTop()0},e.prototype.canScrollRight=function(){return this.getScrollLeft() *",pn.prototype.bgSegSelector=".fc-bgevent:not(.fc-nonbusiness)";var hn=0;function vn(e){return{id:String(hn++),deps:e.deps||[],reducers:e.reducers||[],eventDefParsers:e.eventDefParsers||[],isDraggableTransformers:e.isDraggableTransformers||[],eventDragMutationMassagers:e.eventDragMutationMassagers||[],eventDefMutationAppliers:e.eventDefMutationAppliers||[],dateSelectionTransformers:e.dateSelectionTransformers||[],datePointTransforms:e.datePointTransforms||[],dateSpanTransforms:e.dateSpanTransforms||[],views:e.views||{},viewPropsTransformers:e.viewPropsTransformers||[],isPropsValid:e.isPropsValid||null,externalDefTransforms:e.externalDefTransforms||[],eventResizeJoinTransforms:e.eventResizeJoinTransforms||[],viewContainerModifiers:e.viewContainerModifiers||[],eventDropTransformers:e.eventDropTransformers||[],componentInteractions:e.componentInteractions||[],calendarInteractions:e.calendarInteractions||[],themeClasses:e.themeClasses||{},eventSourceDefs:e.eventSourceDefs||[],cmdFormatter:e.cmdFormatter,recurringTypes:e.recurringTypes||[],namedTimeZonedImpl:e.namedTimeZonedImpl,defaultView:e.defaultView||"",elementDraggingImpl:e.elementDraggingImpl,optionChangeHandlers:e.optionChangeHandlers||{}}}var gn=function(){function e(){this.hooks={reducers:[],eventDefParsers:[],isDraggableTransformers:[],eventDragMutationMassagers:[],eventDefMutationAppliers:[],dateSelectionTransformers:[],datePointTransforms:[],dateSpanTransforms:[],views:{},viewPropsTransformers:[],isPropsValid:null,externalDefTransforms:[],eventResizeJoinTransforms:[],viewContainerModifiers:[],eventDropTransformers:[],componentInteractions:[],calendarInteractions:[],themeClasses:{},eventSourceDefs:[],cmdFormatter:null,recurringTypes:[],namedTimeZonedImpl:null,defaultView:"",elementDraggingImpl:null,optionChangeHandlers:{}},this.addedHash={}}return e.prototype.add=function(e){if(!this.addedHash[e.id]){this.addedHash[e.id]=!0;for(var t=0,n=e.deps;t=200&&a.status<400)try{var e=JSON.parse(a.responseText);r(e,a)}catch(e){i("Failure parsing JSON",a)}else i("Request failed",a)},a.onerror=function(){i("Request failed",a)},a.send(o)}function Sn(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t.join("&")}var bn=vn({eventSourceDefs:[{parseMeta:function(e){if("string"==typeof e)e={url:e};else if(!e||"object"!=typeof e||!e.url)return null;return{url:e.url,method:(e.method||"GET").toUpperCase(),extraParams:e.extraParams,startParam:e.startParam,endParam:e.endParam,timeZoneParam:e.timeZoneParam}},fetch:function(e,t,n){var r=e.eventSource.meta,i=function(e,t,n){var r,i,o,a,s=n.dateEnv,u={};null==(r=e.startParam)&&(r=n.opt("startParam"));null==(i=e.endParam)&&(i=n.opt("endParam"));null==(o=e.timeZoneParam)&&(o=n.opt("timeZoneParam"));a="function"==typeof e.extraParams?e.extraParams():e.extraParams||{};Se(u,a),u[r]=s.formatIso(t.start),u[i]=s.formatIso(t.end),"local"!==s.timeZone&&(u[o]=s.timeZone);return u}(r,e.range,e.calendar);En(r.method,r.url,i,function(e,n){t({rawEvents:e,xhr:n})},function(e,t){n({message:e,xhr:t})})}}]});var Dn=vn({recurringTypes:[{parse:function(e,t,n){var r,i,o=n.createMarker.bind(n),a=he(e,{daysOfWeek:null,startTime:J,endTime:J,startRecur:o,endRecur:o},{},t),s=!1;for(var u in a)if(null!=a[u]){s=!0;break}if(s){var l=null;return"duration"in t&&(l=J(t.duration),delete t.duration),!l&&a.startTime&&a.endTime&&(r=a.endTime,i=a.startTime,l={years:r.years-i.years,months:r.months-i.months,days:r.days-i.days,milliseconds:r.milliseconds-i.milliseconds}),{allDayGuess:Boolean(!a.startTime&&!a.endTime),duration:l,typeData:a}}return null},expand:function(e,t,n){var r=Ve(t,{start:e.startRecur,end:e.endRecur});return r?function(e,t,n,r){var i=e?Ie(e):null,o=B(n.start),a=n.end,s=[];for(;o0?e[0].code:"en",n=window.FullCalendarLocalesAll||[],r=window.FullCalendarLocales||{},i=n.concat(Ce(r),e),o={en:kn},a=0,s=i;a0;i--){var o=r.slice(0,i).join("-");if(t[o])return t[o]}return null}(n,t)||kn;return Pn(e,n,r)}(e,t):Pn(e.code,[e.code],e)}function Pn(e,t,n){var r=Te([kn,n],["buttonText"]);delete r.code;var i=r.week;return delete r.week,{codeArg:e,codes:t,week:i,simpleNumberFormat:new Intl.NumberFormat(e),options:r}}var xn=function(){function e(e){this.overrides=Se({},e),this.dynamicOverrides={},this.compute()}return e.prototype.mutate=function(e,t,n){var r=n?this.dynamicOverrides:this.overrides;Se(r,e);for(var i=0,o=t;i=1?Math.min(i,o):i}(e,this.weekDow,this.weekDoy)},e.prototype.format=function(e,t,n){return void 0===n&&(n={}),t.format({marker:e,timeZoneOffset:null!=n.forcedTzo?n.forcedTzo:this.offsetForMarker(e)},this)},e.prototype.formatRange=function(e,t,n,r){return void 0===r&&(r={}),r.isEndExclusive&&(t=H(t,-1)),n.formatRange({marker:e,timeZoneOffset:null!=r.forcedStartTzo?r.forcedStartTzo:this.offsetForMarker(e)},{marker:t,timeZoneOffset:null!=r.forcedEndTzo?r.forcedEndTzo:this.offsetForMarker(t)},this)},e.prototype.formatIso=function(e,t){void 0===t&&(t={});var n=null;return t.omitTimeZoneOffset||(n=null!=t.forcedTzo?t.forcedTzo:this.offsetForMarker(e)),function(e,t,n){void 0===n&&(n=!1);var r=e.toISOString();return r=r.replace(".000",""),n&&(r=r.replace("T00:00:00Z","")),r.length>10&&(null==t?r=r.replace("Z",""):0!==t&&(r=r.replace("Z",at(t,!0)))),r}(e,n,t.omitTime)},e.prototype.timestampToMarker=function(e){return"local"===this.timeZone?j(F(new Date(e))):"UTC"!==this.timeZone&&this.namedTimeZoneImpl?j(this.namedTimeZoneImpl.timestampToArray(e)):new Date(e)},e.prototype.offsetForMarker=function(e){return"local"===this.timeZone?-W(Z(e)).getTimezoneOffset():"UTC"===this.timeZone?0:this.namedTimeZoneImpl?this.namedTimeZoneImpl.offsetForArray(Z(e)):null},e.prototype.toDate=function(e,t){return"local"===this.timeZone?W(Z(e)):"UTC"===this.timeZone?new Date(e.valueOf()):this.namedTimeZoneImpl?new Date(e.valueOf()-1e3*this.namedTimeZoneImpl.offsetForArray(Z(e))*60):new Date(e.valueOf()-(t||0))},e}(),Vn={id:String,allDayDefault:Boolean,eventDataTransform:Function,success:Function,failure:Function},An=0;function Fn(e,t){return!t.pluginSystem.hooks.eventSourceDefs[e.sourceDefId].ignoreRange}function Wn(e,t){for(var n=t.pluginSystem.hooks.eventSourceDefs,r=n.length-1;r>=0;r--){var i=n[r].parseMeta(e);if(i){var o=Zn("object"==typeof e?e:{},i,r,t);return o._raw=e,o}}return null}function Zn(e,t,n,r){var i={},o=he(e,Vn,{},i),a={},s=Ht(i,r,a);return o.isFetching=!1,o.latestFetchId="",o.fetchRange=null,o.publicId=String(e.id||""),o.sourceId=String(An++),o.sourceDefId=n,o.meta=t,o.ui=s,o.extendedProps=a,o}function jn(e,t,n,r){switch(t.type){case"ADD_EVENT_SOURCES":return function(e,t,n,r){for(var i={},o=0,a=t;oe.fetchRange.end:!e.latestFetchId}(e,t,n)}),t,n)}function Gn(e,t,n,r){var i={};for(var o in e){var a=e[o];t[o]?i[o]=Xn(a,n,r):i[o]=a}return i}function Xn(e,t,n){var r=n.pluginSystem.hooks.eventSourceDefs[e.sourceDefId],i=String(Yn++);return r.fetch({eventSource:e,calendar:n,range:t},function(r){var o,a,s=r.rawEvents,u=n.opt("eventSourceSuccess");e.success&&(a=e.success(s,r.xhr)),u&&(o=u(s,r.xhr)),s=a||o||s,n.dispatch({type:"RECEIVE_EVENTS",sourceId:e.sourceId,fetchId:i,fetchRange:t,rawEvents:s})},function(r){var o=n.opt("eventSourceFailure");console.warn(r.message,r),e.failure&&e.failure(r),o&&o(r),n.dispatch({type:"RECEIVE_EVENT_ERROR",sourceId:e.sourceId,fetchId:i,fetchRange:t,error:r})}),Se({},e,{isFetching:!0,latestFetchId:i})}var Jn=function(){function e(e,t){this.viewSpec=e,this.options=e.options,this.dateEnv=t.dateEnv,this.calendar=t,this.initHiddenDays()}return e.prototype.buildPrev=function(e,t){var n=this.dateEnv,r=n.subtract(n.startOf(t,e.currentRangeUnit),e.dateIncrement);return this.build(r,-1)},e.prototype.buildNext=function(e,t){var n=this.dateEnv,r=n.add(n.startOf(t,e.currentRangeUnit),e.dateIncrement);return this.build(r,1)},e.prototype.build=function(e,t,n){var r;void 0===n&&(n=!1);var i,o,a,s,u,l,c,d,f;return r=this.buildValidRange(),r=this.trimHiddenDays(r),n&&(d=e,e=null!=(f=r).start&&d=f.end?new Date(f.end.valueOf()-1):d),a=this.buildCurrentRangeInfo(e,t),s=/^(year|month|week|day)$/.test(a.unit),u=this.buildRenderRange(this.trimHiddenDays(a.range),a.unit,s),l=u=this.trimHiddenDays(u),this.options.showNonCurrentDates||(l=Ve(l,a.range)),i=J(this.options.minTime),o=J(this.options.maxTime),l=Ve(l=this.adjustActiveRange(l,i,o),r),c=Fe(a.range,r),{validRange:r,currentRange:a.range,currentRangeUnit:a.unit,isRangeAllDay:s,activeRange:l,renderRange:u,minTime:i,maxTime:o,isValid:c,dateIncrement:this.buildDateIncrement(a.duration)}},e.prototype.buildValidRange=function(){return this.getRangeOption("validRange",this.calendar.getNow())||{start:null,end:null}},e.prototype.buildCurrentRangeInfo=function(e,t){var n,r=this.viewSpec,i=this.dateEnv,o=null,a=null,s=null;return r.duration?(o=r.duration,a=r.durationUnit,s=this.buildRangeFromDuration(e,t,o,a)):(n=this.options.dayCount)?(a="day",s=this.buildRangeFromDayCount(e,t,n)):(s=this.buildCustomVisibleRange(e))?a=i.greatestWholeUnit(s.start,s.end).unit:(a=ne(o=this.getFallbackDuration()).unit,s=this.buildRangeFromDuration(e,t,o,a)),{duration:o,unit:a,range:s}},e.prototype.getFallbackDuration=function(){return J({day:1})},e.prototype.adjustActiveRange=function(e,t,n){var r=this.dateEnv,i=e.start,o=e.end;return this.viewSpec.class.prototype.usesMinMaxTime&&(ee(t)<0&&(i=B(i),i=r.add(i,t)),ee(n)>1&&(o=x(o=B(o),-1),o=r.add(o,n))),{start:i,end:o}},e.prototype.buildRangeFromDuration=function(e,t,n,r){var i,o,a,s,u,l=this.dateEnv,c=this.options.dateAlignment;function d(){a=l.startOf(e,c),s=l.add(a,n),u={start:a,end:s}}return c||((i=this.options.dateIncrement)?(o=J(i),c=te(o) ")),h=!1):((r=d[e])?(c=function(e){r.click&&r.click.call(E,e)},(v=o.getCustomButtonIconClass(r))||(v=o.getIconClass(e))||(g=r.text)):(s=l[e])?(n.viewsWithButtons.push(e),c=function(){a.changeView(e)},(g=s.buttonTextOverride)||(v=o.getIconClass(e))||(g=s.buttonTextDefault)):a[e]&&(c=function(){a[e]()},(g=f[e])||(v=o.getIconClass(e))||(g=p[e])),c&&(m=["fc-"+e+"-button",o.getClass("button")],g?(y=Ot(g),S=""):v&&(y="",S=' aria-label="'+e+'"'),(E=i('"+y+"")).addEventListener("click",c),u.push(E)))}),u.length>1){r=document.createElement("div");var v=o.getClass("buttonGroup");h&&v&&r.classList.add(v),s(r,u),c.appendChild(r)}else s(c,u)}),c},t.prototype.updateToday=function(e){this.toggleButtonEnabled("today",e)},t.prototype.updatePrev=function(e){this.toggleButtonEnabled("prev",e)},t.prototype.updateNext=function(e){this.toggleButtonEnabled("next",e)},t.prototype.updateTitle=function(e){v(this.el,"h2").forEach(function(t){t.innerText=e})},t.prototype.updateActiveButton=function(e){var t=this.theme.getClass("buttonActive");v(this.el,"button").forEach(function(n){e&&n.classList.contains("fc-"+e+"-button")?n.classList.add(t):n.classList.remove(t)})},t.prototype.toggleButtonEnabled=function(e,t){v(this.el,".fc-"+e+"-button").forEach(function(e){e.disabled=!t})},t}(fn),hr=function(e){function t(t,n){var i=e.call(this,t)||this;i._renderToolbars=qt(i.renderToolbars),i.buildViewPropTransformers=Ye(gr),i.el=n,u(n,i.contentEl=r("div",{className:"fc-view-container"}));for(var o=i.calendar,a=0,s=o.pluginSystem.hooks.viewContainerModifiers;a1?{year:"numeric",month:"short",day:"numeric"}:{year:"numeric",month:"long",day:"numeric"}}(e),t.titleRangeSeparator),{isEndExclusive:e.isRangeAllDay})}function gr(e){return e.map(function(e){return new e})}var yr=function(){function e(e){this.component=e.component}return e.prototype.destroy=function(){},e}();var mr={},Er=function(e){function t(t){var n=e.call(this,t)||this;n.handleSegClick=function(e,t){var r=n.component,i=ht(t);if(i&&r.isValidSegDownEl(e.target)){var o=p(e.target,".fc-has-url"),a=o?o.querySelector("a[href]").href:"";r.publiclyTrigger("eventClick",[{el:t,event:new ct(r.calendar,i.eventRange.def,i.eventRange.instance),jsEvent:e,view:r.view}]),a&&!e.defaultPrevented&&(window.location.href=a)}};var r=t.component;return n.destroy=O(r.el,"click",r.fgSegSelector+","+r.bgSegSelector,n.handleSegClick),n}return Ee(t,e),t}(yr),Sr=function(e){function t(t){var n=e.call(this,t)||this;n.handleEventElRemove=function(e){e===n.currentSegEl&&n.handleSegLeave(null,n.currentSegEl)},n.handleSegEnter=function(e,t){ht(t)&&(t.classList.add("fc-allow-mouse-resize"),n.currentSegEl=t,n.triggerEvent("eventMouseEnter",e,t))},n.handleSegLeave=function(e,t){n.currentSegEl&&(t.classList.remove("fc-allow-mouse-resize"),n.currentSegEl=null,n.triggerEvent("eventMouseLeave",e,t))};var r,i,o,a,s,u=t.component;return n.removeHoverListeners=(r=u.el,i=u.fgSegSelector+","+u.bgSegSelector,o=n.handleSegEnter,a=n.handleSegLeave,O(r,"mouseover",i,function(e,t){if(t!==s){s=t,o(e,t);var n=function(e){s=null,a(e,t),t.removeEventListener("mouseleave",n)};t.addEventListener("mouseleave",n)}})),u.calendar.on("eventElRemove",n.handleEventElRemove),n}return Ee(t,e),t.prototype.destroy=function(){this.removeHoverListeners(),this.component.calendar.off("eventElRemove",this.handleEventElRemove)},t.prototype.triggerEvent=function(e,t,n){var r=this.component,i=ht(n);t&&!r.isValidSegDownEl(t.target)||r.publiclyTrigger(e,[{el:n,event:new ct(this.component.calendar,i.eventRange.def,i.eventRange.instance),jsEvent:t,view:r.view}])},t}(yr),br=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return Ee(t,e),t}(cn);br.prototype.classes={widget:"fc-unthemed",widgetHeader:"fc-widget-header",widgetContent:"fc-widget-content",buttonGroup:"fc-button-group",button:"fc-button fc-button-primary",buttonActive:"fc-button-active",popoverHeader:"fc-widget-header",popoverContent:"fc-widget-content",headerRow:"fc-widget-header",dayRow:"fc-widget-content",listView:"fc-widget-content"},br.prototype.baseIconClass="fc-icon",br.prototype.iconClasses={close:"fc-icon-x",prev:"fc-icon-chevron-left",next:"fc-icon-chevron-right",prevYear:"fc-icon-chevrons-left",nextYear:"fc-icon-chevrons-right"},br.prototype.iconOverrideOption="buttonIcons",br.prototype.iconOverrideCustomButtonOption="icon",br.prototype.iconOverridePrefix="fc-icon-";var Dr=function(){function e(e,t){var n=this;this.parseRawLocales=Ye(On),this.buildLocale=Ye(_n),this.buildDateEnv=Ye(Tr),this.buildTheme=Ye(wr),this.buildEventUiSingleBase=Ye(this._buildEventUiSingleBase),this.buildSelectionConfig=Ye(this._buildSelectionConfig),this.buildEventUiBySource=qe(Ir,Me),this.buildEventUiBases=Ye(Cr),this.interactionsStore={},this.actionQueue=[],this.isReducing=!1,this.needsRerender=!1,this.needsFullRerender=!1,this.isRendering=!1,this.renderingPauseDepth=0,this.buildDelayedRerender=Ye(Rr),this.afterSizingTriggers={},this.isViewUpdated=!1,this.isDatesUpdated=!1,this.isEventsUpdated=!1,this.el=e,this.optionsManager=new xn(t||{}),this.pluginSystem=new gn,this.addPluginInputs(this.optionsManager.computed.plugins||[]),this.handleOptions(this.optionsManager.computed),this.publiclyTrigger("_init"),this.hydrate(),this.calendarInteractions=this.pluginSystem.hooks.calendarInteractions.map(function(e){return new e(n)})}return e.prototype.addPluginInputs=function(e){for(var t=function(e){for(var t=[],n=0,r=e;n"+this.fillSegTag+">"},e.prototype.detachSegs=function(e,t){var n=this.containerElsByType[e];n&&(n.forEach(c),delete this.containerElsByType[e])},e.prototype.computeSizes=function(e){for(var t in this.segsByType)(e||this.dirtySizeFlags[t])&&this.computeSegSizes(this.segsByType[t])},e.prototype.assignSizes=function(e){for(var t in this.segsByType)(e||this.dirtySizeFlags[t])&&this.assignSegSizes(this.segsByType[t]);this.dirtySizeFlags={}},e.prototype.computeSegSizes=function(e){},e.prototype.assignSegSizes=function(e){},e}(),Pr=function(e){this.timeZoneName=e},xr=function(){function e(e){this.emitter=new tn}return e.prototype.destroy=function(){},e.prototype.setMirrorIsVisible=function(e){},e.prototype.setMirrorNeedsRevert=function(e){},e.prototype.setAutoScrollEnabled=function(e){},e}();function Hr(e){var t=_n(e.locale||"en",On([]).map);return e=Se({timeZone:Rn.timeZone,calendarSystem:"gregory"},e,{locale:t}),new Bn(e)}var Nr={startTime:J,duration:J,create:Boolean,sourceId:String},zr={create:!0};function Ur(e,t){return!e||t>10?{weekday:"short"}:t>1?{weekday:"short",month:"numeric",day:"numeric",omitCommas:!0}:{weekday:"long"}}function Lr(e,t,n,r,i,o,a,s){var u,l=o.view,c=o.dateEnv,d=o.theme,f=o.options,p=Ze(t.activeRange,e),h=["fc-day-header",d.getClass("widgetHeader")];return u="function"==typeof f.columnHeaderHtml?f.columnHeaderHtml(c.toDate(e)):"function"==typeof f.columnHeaderText?Ot(f.columnHeaderText(c.toDate(e))):Ot(c.format(e,i)),n?h=h.concat(Qt(e,t,o,!0)):h.push("fc-"+P[e.getUTCDay()]),'1?' colspan="'+a+'"':"")+(s?" "+s:"")+">"+(p?Kt(l,{date:e,forceOff:!n||1===r},u):u)+""}var Br=function(e){function t(t,n){var r=e.call(this,t)||this;return n.innerHTML="",n.appendChild(r.el=i('')),r.thead=r.el.querySelector("thead"),r}return Ee(t,e),t.prototype.destroy=function(){c(this.el)},t.prototype.render=function(e){var t=e.dates,n=e.datesRepDistinctDays,r=[];e.renderIntroHtml&&r.push(e.renderIntroHtml());for(var i=ot(this.opt("columnHeaderFormat")||Ur(n,t.length)),o=0,a=t;o"+r.join("")+""},t}(fn),Vr=function(){function e(e,t){for(var n=e.start,r=e.end,i=[],o=[],a=-1;n=t.length?t[t.length-1]+1:t[n]},e}(),Ar=function(){function e(e,t){var n,r,i,o=e.dates;if(t){for(r=o[0].getUTCDay(),n=1;n1},e.isPropsEqual=Me,e.isPropsValid=Rt,e.isSingleDay=function(e){return 0===e.years&&0===e.months&&1===e.days&&0===e.milliseconds},e.isValidDate=Y,e.listenBySelector=O,e.mapHash=Re,e.matchCellWidths=function(e){var t=0;return e.forEach(function(e){var n=e.firstChild;if(n instanceof HTMLElement){var r=n.getBoundingClientRect().width;r>t&&(t=r)}}),t++,e.forEach(function(e){e.style.width=t+"px"}),t},e.memoize=Ye,e.memoizeOutput=qe,e.memoizeRendering=qt,e.mergeEventStores=Ne,e.multiplyDuration=function(e,t){return{years:e.years*t,months:e.months*t,days:e.days*t,milliseconds:e.milliseconds*t}},e.padStart=le,e.parseBusinessHours=Yt,e.parseDragMeta=function(e){var t={},n=he(e,Nr,zr,t);return n.leftoverProps=t,n},e.parseEventDef=Wt,e.parseFieldSpecs=ie,e.parseMarker=Ln,e.pointInsideRect=function(e,t){return e.left>=t.left&&e.left=t.top&&e.top
/*!
FullCalendar Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
import { htmlToElement, forceClassName, applyStyle, debounce, preventDefault, ScrollComponent, EmitterMixin, removeElement, createElement, computeEdges, asRoughMs, isSingleDay, getDayClasses, findElements, Component, PositionCache, findChildren, isInt, multiplyDuration, config, createFormatter, greatestDurationDenominator, createDuration, wholeDivideDurations, addDays, startOfDay, computeVisibleDayRange, asRoughMinutes, padStart, asRoughSeconds, diffWholeDays, buildGotoAnchorHtml, htmlEscape, translateRect, rangeContainsMarker, cssToStr, computeHeightAndMargins, applyStyleProp, FgEventRenderer, FillRenderer, memoizeRendering, DateComponent, intersectRanges, addMs, Slicer, View, createPlugin } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/*
A rectangular area of content that lives within a Scroller.
Can have "gutters", areas of dead spacing around the perimeter.
Also very useful for forcing a width, which a Scroller cannot do alone.
Has a content area that lives above a background area.
*/
var ScrollerCanvas = /** @class */ (function () {
function ScrollerCanvas() {
this.gutters = {};
this.el = htmlToElement("
var EnhancedScroller = /** @class */ (function (_super) {
__extends(EnhancedScroller, _super);
function EnhancedScroller(overflowX, overflowY) {
var _this = _super.call(this, overflowX, overflowY) || this;
// Scroll Events
// ----------------------------------------------------------------------------------------------
_this.reportScroll = function () {
if (!_this.isScrolling) {
_this.reportScrollStart();
}
_this.trigger('scroll');
_this.isMoving = true;
_this.requestMovingEnd();
};
_this.reportScrollStart = function () {
if (!_this.isScrolling) {
_this.isScrolling = true;
_this.trigger('scrollStart', _this.isTouching); // created in constructor
}
};
// Touch Events
// ----------------------------------------------------------------------------------------------
// will fire *before* the scroll event is fired
_this.reportTouchStart = function () {
_this.isTouching = true;
};
_this.reportTouchEnd = function () {
if (_this.isTouching) {
_this.isTouching = false;
// if touch scrolling was re-enabled during a recent touch scroll
// then unbind the handlers that are preventing it from happening.
if (_this.isTouchScrollEnabled) {
_this.unbindPreventTouchScroll(); // won't do anything if not bound
}
// if the user ended their touch, and the scroll area wasn't moving,
// we consider this to be the end of the scroll.
if (!_this.isMoving) {
_this.reportScrollEnd(); // won't fire if already ended
}
}
};
_this.isScrolling = false;
_this.isTouching = false;
_this.isMoving = false;
_this.isTouchScrollEnabled = true;
_this.requestMovingEnd = debounce(_this.reportMovingEnd, 500);
_this.canvas = new ScrollerCanvas();
_this.el.appendChild(_this.canvas.el);
_this.applyOverflow();
_this.bindHandlers();
return _this;
}
EnhancedScroller.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.unbindHandlers();
};
// Touch scroll prevention
// ----------------------------------------------------------------------------------------------
EnhancedScroller.prototype.disableTouchScroll = function () {
this.isTouchScrollEnabled = false;
this.bindPreventTouchScroll(); // will be unbound in enableTouchScroll or reportTouchEnd
};
EnhancedScroller.prototype.enableTouchScroll = function () {
this.isTouchScrollEnabled = true;
// only immediately unbind if a touch event is NOT in progress.
// otherwise, it will be handled by reportTouchEnd.
if (!this.isTouching) {
this.unbindPreventTouchScroll();
}
};
EnhancedScroller.prototype.bindPreventTouchScroll = function () {
if (!this.preventTouchScrollHandler) {
this.el.addEventListener('touchmove', (this.preventTouchScrollHandler = preventDefault));
}
};
EnhancedScroller.prototype.unbindPreventTouchScroll = function () {
if (this.preventTouchScrollHandler) {
this.el.removeEventListener('touchmove', this.preventTouchScrollHandler);
this.preventTouchScrollHandler = null;
}
};
// Handlers
// ----------------------------------------------------------------------------------------------
EnhancedScroller.prototype.bindHandlers = function () {
this.el.addEventListener('scroll', this.reportScroll);
this.el.addEventListener('touchstart', this.reportTouchStart, { passive: true });
this.el.addEventListener('touchend', this.reportTouchEnd);
};
EnhancedScroller.prototype.unbindHandlers = function () {
this.el.removeEventListener('scroll', this.reportScroll);
this.el.removeEventListener('touchstart', this.reportTouchStart, { passive: true });
this.el.removeEventListener('touchend', this.reportTouchEnd);
};
EnhancedScroller.prototype.reportMovingEnd = function () {
this.isMoving = false;
// only end the scroll if not currently touching.
// if touching, the scrolling will end later, on touchend.
if (!this.isTouching) {
this.reportScrollEnd();
}
};
EnhancedScroller.prototype.reportScrollEnd = function () {
if (this.isScrolling) {
this.trigger('scrollEnd');
this.isScrolling = false;
}
};
// Horizontal Scroll Normalization
// ----------------------------------------------------------------------------------------------
// http://stackoverflow.com/questions/24276619/better-way-to-get-the-viewport-of-a-scrollable-div-in-rtl-mode/24394376#24394376
// TODO: move all this to util functions
/*
If RTL, and scrolled to the left, returns NEGATIVE value (like Firefox)
*/
EnhancedScroller.prototype.getScrollLeft = function () {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
var val = el.scrollLeft;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'positive':
val = (val + el.clientWidth) - el.scrollWidth;
break;
case 'reverse':
val = -val;
break;
}
}
return val;
};
/*
Accepts a NEGATIVE value for when scrolled in RTL
*/
EnhancedScroller.prototype.setScrollLeft = function (val) {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'positive':
val = (val - el.clientWidth) + el.scrollWidth;
break;
case 'reverse':
val = -val;
break;
}
}
el.scrollLeft = val;
};
/*
Always returns the number of pixels scrolled from the leftmost position (even if RTL).
Always positive.
*/
EnhancedScroller.prototype.getScrollFromLeft = function () {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
var val = el.scrollLeft;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'negative':
val = (val - el.clientWidth) + el.scrollWidth;
break;
case 'reverse':
val = (-val - el.clientWidth) + el.scrollWidth;
break;
}
}
return val;
};
return EnhancedScroller;
}(ScrollComponent));
EmitterMixin.mixInto(EnhancedScroller);
// Horizontal Scroll System Detection
// ----------------------------------------------------------------------------------------------
var _rtlScrollSystem;
function getRtlScrollSystem() {
return _rtlScrollSystem || (_rtlScrollSystem = detectRtlScrollSystem());
}
function detectRtlScrollSystem() {
var el = htmlToElement("
/*
A Scroller, but with a wrapping div that allows "clipping" away of native scrollbars,
giving the appearance that there are no scrollbars.
*/
var ClippedScroller = /** @class */ (function () {
/*
Received overflows can be set to 'clipped', meaning scrollbars shouldn't be visible
to the user, but the area should still scroll.
*/
function ClippedScroller(overflowX, overflowY, parentEl) {
this.isHScrollbarsClipped = false;
this.isVScrollbarsClipped = false;
if (overflowX === 'clipped-scroll') {
overflowX = 'scroll';
this.isHScrollbarsClipped = true;
}
if (overflowY === 'clipped-scroll') {
overflowY = 'scroll';
this.isVScrollbarsClipped = true;
}
this.enhancedScroll = new EnhancedScroller(overflowX, overflowY);
parentEl.appendChild(this.el = createElement('div', {
className: 'fc-scroller-clip'
}));
this.el.appendChild(this.enhancedScroll.el);
}
ClippedScroller.prototype.destroy = function () {
removeElement(this.el);
};
ClippedScroller.prototype.updateSize = function () {
var enhancedScroll = this.enhancedScroll;
var scrollEl = enhancedScroll.el;
var edges = computeEdges(scrollEl);
var cssProps = { marginLeft: 0, marginRight: 0, marginTop: 0, marginBottom: 0 };
// give the inner scrolling div negative margins so that its scrollbars
// are nudged outside of the bounding box of the wrapper, which is overflow:hidden
if (this.isVScrollbarsClipped) {
cssProps.marginLeft = -edges.scrollbarLeft;
cssProps.marginRight = -edges.scrollbarRight;
}
if (this.isHScrollbarsClipped) {
cssProps.marginBottom = -edges.scrollbarBottom;
}
applyStyle(scrollEl, cssProps);
// if we are attempting to hide the scrollbars offscreen, OSX/iOS will still
// display the floating scrollbars. attach a className to force-hide them.
if ((this.isHScrollbarsClipped || (enhancedScroll.overflowX === 'hidden')) && // should never show?
(this.isVScrollbarsClipped || (enhancedScroll.overflowY === 'hidden')) && // should never show?
!( // doesn't have any scrollbar mass
edges.scrollbarLeft ||
edges.scrollbarRight ||
edges.scrollbarBottom)) {
scrollEl.classList.add('fc-no-scrollbars');
}
else {
scrollEl.classList.remove('fc-no-scrollbars');
}
};
ClippedScroller.prototype.setHeight = function (height) {
this.enhancedScroll.setHeight(height);
};
/*
Accounts for 'clipped' scrollbars
*/
ClippedScroller.prototype.getScrollbarWidths = function () {
var widths = this.enhancedScroll.getScrollbarWidths();
if (this.isVScrollbarsClipped) {
widths.left = 0;
widths.right = 0;
}
if (this.isHScrollbarsClipped) {
widths.bottom = 0;
}
return widths;
};
return ClippedScroller;
}());
var ScrollJoiner = /** @class */ (function () {
function ScrollJoiner(axis, scrollers) {
this.axis = axis;
this.scrollers = scrollers;
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var scroller = _a[_i];
this.initScroller(scroller);
}
}
ScrollJoiner.prototype.initScroller = function (scroller) {
var _this = this;
var enhancedScroll = scroller.enhancedScroll;
// when the user scrolls via mousewheel, we know for sure the target
// scroller should be the master. capture the various x-browser events that fire.
var onScroll = function () {
_this.assignMasterScroller(scroller);
};
'wheel mousewheel DomMouseScroll MozMousePixelScroll'.split(' ').forEach(function (evName) {
enhancedScroll.el.addEventListener(evName, onScroll);
});
enhancedScroll
.on('scrollStart', function () {
if (!_this.masterScroller) {
_this.assignMasterScroller(scroller);
}
})
.on('scroll', function () {
if (scroller === _this.masterScroller) {
for (var _i = 0, _a = _this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
if (otherScroller !== scroller) {
switch (_this.axis) {
case 'horizontal':
otherScroller.enhancedScroll.el.scrollLeft = enhancedScroll.el.scrollLeft;
break;
case 'vertical':
otherScroller.enhancedScroll.setScrollTop(enhancedScroll.getScrollTop());
break;
}
}
}
}
})
.on('scrollEnd', function () {
if (scroller === _this.masterScroller) {
_this.unassignMasterScroller();
}
});
};
ScrollJoiner.prototype.assignMasterScroller = function (scroller) {
this.unassignMasterScroller();
this.masterScroller = scroller;
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
if (otherScroller !== scroller) {
otherScroller.enhancedScroll.disableTouchScroll();
}
}
};
ScrollJoiner.prototype.unassignMasterScroller = function () {
if (this.masterScroller) {
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
otherScroller.enhancedScroll.enableTouchScroll();
}
this.masterScroller = null;
}
};
ScrollJoiner.prototype.update = function () {
var allWidths = this.scrollers.map(function (scroller) { return scroller.getScrollbarWidths(); });
var maxLeft = 0;
var maxRight = 0;
var maxTop = 0;
var maxBottom = 0;
var widths;
var i;
for (var _i = 0, allWidths_1 = allWidths; _i < allWidths_1.length; _i++) {
widths = allWidths_1[_i];
maxLeft = Math.max(maxLeft, widths.left);
maxRight = Math.max(maxRight, widths.right);
maxTop = Math.max(maxTop, widths.top);
maxBottom = Math.max(maxBottom, widths.bottom);
}
for (i = 0; i < this.scrollers.length; i++) {
var scroller = this.scrollers[i];
widths = allWidths[i];
scroller.enhancedScroll.canvas.setGutters(this.axis === 'horizontal' ?
{
left: maxLeft - widths.left,
right: maxRight - widths.right
} :
{
top: maxTop - widths.top,
bottom: maxBottom - widths.bottom
});
}
};
return ScrollJoiner;
}());
var HeaderBodyLayout = /** @class */ (function () {
/*
verticalScroll = 'auto' | 'clipped-scroll'
*/
function HeaderBodyLayout(headerContainerEl, bodyContainerEl, verticalScroll) {
this.headerScroller = new ClippedScroller('clipped-scroll', 'hidden', headerContainerEl);
this.bodyScroller = new ClippedScroller('auto', verticalScroll, bodyContainerEl);
this.scrollJoiner = new ScrollJoiner('horizontal', [
this.headerScroller,
this.bodyScroller
]);
}
HeaderBodyLayout.prototype.destroy = function () {
this.headerScroller.destroy();
this.bodyScroller.destroy();
};
HeaderBodyLayout.prototype.setHeight = function (totalHeight, isAuto) {
var bodyHeight;
if (isAuto) {
bodyHeight = 'auto';
}
else {
bodyHeight = totalHeight - this.queryHeadHeight();
}
this.bodyScroller.setHeight(bodyHeight);
this.headerScroller.updateSize(); // adjusts gutters and classNames
this.bodyScroller.updateSize(); // adjusts gutters and classNames
this.scrollJoiner.update();
};
HeaderBodyLayout.prototype.queryHeadHeight = function () {
return this.headerScroller.enhancedScroll.canvas.contentEl.getBoundingClientRect().height;
};
return HeaderBodyLayout;
}());
var TimelineHeader = /** @class */ (function (_super) {
__extends(TimelineHeader, _super);
function TimelineHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.appendChild(_this.tableEl = createElement('table', {
className: _this.theme.getClass('tableGrid')
}));
return _this;
}
TimelineHeader.prototype.destroy = function () {
removeElement(this.tableEl);
_super.prototype.destroy.call(this);
};
TimelineHeader.prototype.render = function (props) {
this.renderDates(props.tDateProfile);
};
TimelineHeader.prototype.renderDates = function (tDateProfile) {
var _a = this, dateEnv = _a.dateEnv, theme = _a.theme;
var cellRows = tDateProfile.cellRows;
var lastRow = cellRows[cellRows.length - 1];
var isChrono = asRoughMs(tDateProfile.labelInterval) > asRoughMs(tDateProfile.slotDuration);
var oneDay = isSingleDay(tDateProfile.slotDuration);
var html = '';
// needs to be a col for each body slat. header cells will have colspans
for (var i = tDateProfile.slotCnt - 1; i >= 0; i--) {
html += '';
}
html += '';
html += '';
for (var _i = 0, cellRows_1 = cellRows; _i < cellRows_1.length; _i++) {
var rowCells = cellRows_1[_i];
var isLast = rowCells === lastRow;
html += '';
for (var _b = 0, rowCells_1 = rowCells; _b < rowCells_1.length; _b++) {
var cell = rowCells_1[_b];
var headerCellClassNames = [theme.getClass('widgetHeader')];
if (cell.isWeekStart) {
headerCellClassNames.push('fc-em-cell');
}
if (oneDay) {
headerCellClassNames = headerCellClassNames.concat(getDayClasses(cell.date, this.props.dateProfile, this.context, true) // adds "today" class and other day-based classes
);
}
html +=
' 1 ? ' colspan="' + cell.colspan + '"' : '') +
'>' +
'
var TimelineSlats = /** @class */ (function (_super) {
__extends(TimelineSlats, _super);
function TimelineSlats(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.appendChild(_this.el = createElement('div', { className: 'fc-slats' }));
return _this;
}
TimelineSlats.prototype.destroy = function () {
removeElement(this.el);
_super.prototype.destroy.call(this);
};
TimelineSlats.prototype.render = function (props) {
this.renderDates(props.tDateProfile);
};
TimelineSlats.prototype.renderDates = function (tDateProfile) {
var _a = this, theme = _a.theme, view = _a.view, dateEnv = _a.dateEnv;
var slotDates = tDateProfile.slotDates, isWeekStarts = tDateProfile.isWeekStarts;
var html = '' +
'';
for (var i = 0; i < slotDates.length; i++) {
html += '';
}
html += '';
html += '';
for (var i = 0; i < slotDates.length; i++) {
html += this.slatCellHtml(slotDates[i], isWeekStarts[i], tDateProfile);
}
html += '';
this.el.innerHTML = html;
this.slatColEls = findElements(this.el, 'col');
this.slatEls = findElements(this.el, 'td');
for (var i = 0; i < slotDates.length; i++) {
view.publiclyTrigger('dayRender', [
{
date: dateEnv.toDate(slotDates[i]),
el: this.slatEls[i],
view: view
}
]);
}
this.outerCoordCache = new PositionCache(this.el, this.slatEls, true, // isHorizontal
false // isVertical
);
// for the inner divs within the slats
// used for event rendering and scrollTime, to disregard slat border
this.innerCoordCache = new PositionCache(this.el, findChildren(this.slatEls, 'div'), true, // isHorizontal
false // isVertical
);
};
TimelineSlats.prototype.slatCellHtml = function (date, isEm, tDateProfile) {
var _a = this, theme = _a.theme, dateEnv = _a.dateEnv;
var classes;
if (tDateProfile.isTimeScale) {
classes = [];
classes.push(isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval)) ?
'fc-major' :
'fc-minor');
}
else {
classes = getDayClasses(date, this.props.dateProfile, this.context);
classes.push('fc-day');
}
classes.unshift(theme.getClass('widgetContent'));
if (isEm) {
classes.push('fc-em-cell');
}
return '
var MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours
var MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour
var MAX_AUTO_CELLS = 200; // allows 4-days to have a :30 slot duration
config.MAX_TIMELINE_SLOTS = 1000;
// potential nice values for slot-duration and interval-duration
var STOCK_SUB_DURATIONS = [
{ years: 1 },
{ months: 1 },
{ days: 1 },
{ hours: 1 },
{ minutes: 30 },
{ minutes: 15 },
{ minutes: 10 },
{ minutes: 5 },
{ minutes: 1 },
{ seconds: 30 },
{ seconds: 15 },
{ seconds: 10 },
{ seconds: 5 },
{ seconds: 1 },
{ milliseconds: 500 },
{ milliseconds: 100 },
{ milliseconds: 10 },
{ milliseconds: 1 }
];
function buildTimelineDateProfile(dateProfile, view) {
var dateEnv = view.dateEnv;
var tDateProfile = {
labelInterval: queryDurationOption(view, 'slotLabelInterval'),
slotDuration: queryDurationOption(view, 'slotDuration')
};
validateLabelAndSlot(tDateProfile, dateProfile, dateEnv); // validate after computed grid duration
ensureLabelInterval(tDateProfile, dateProfile, dateEnv);
ensureSlotDuration(tDateProfile, dateProfile, dateEnv);
var input = view.opt('slotLabelFormat');
var rawFormats = Array.isArray(input) ?
input
: (input != null) ?
[input]
:
computeHeaderFormats(tDateProfile, dateProfile, dateEnv, view);
tDateProfile.headerFormats = rawFormats.map(function (rawFormat) {
return createFormatter(rawFormat);
});
tDateProfile.isTimeScale = Boolean(tDateProfile.slotDuration.milliseconds);
var largeUnit = null;
if (!tDateProfile.isTimeScale) {
var slotUnit = greatestDurationDenominator(tDateProfile.slotDuration).unit;
if (/year|month|week/.test(slotUnit)) {
largeUnit = slotUnit;
}
}
tDateProfile.largeUnit = largeUnit;
tDateProfile.emphasizeWeeks =
isSingleDay(tDateProfile.slotDuration) &&
currentRangeAs('weeks', dateProfile, dateEnv) >= 2 &&
!view.opt('businessHours');
/*
console.log('label interval =', timelineView.labelInterval.humanize())
console.log('slot duration =', timelineView.slotDuration.humanize())
console.log('header formats =', timelineView.headerFormats)
console.log('isTimeScale', timelineView.isTimeScale)
console.log('largeUnit', timelineView.largeUnit)
*/
var rawSnapDuration = view.opt('snapDuration');
var snapDuration;
var snapsPerSlot;
if (rawSnapDuration) {
snapDuration = createDuration(rawSnapDuration);
snapsPerSlot = wholeDivideDurations(tDateProfile.slotDuration, snapDuration);
// ^ TODO: warning if not whole?
}
if (snapsPerSlot == null) {
snapDuration = tDateProfile.slotDuration;
snapsPerSlot = 1;
}
tDateProfile.snapDuration = snapDuration;
tDateProfile.snapsPerSlot = snapsPerSlot;
// more...
var timeWindowMs = asRoughMs(dateProfile.maxTime) - asRoughMs(dateProfile.minTime);
// TODO: why not use normalizeRange!?
var normalizedStart = normalizeDate(dateProfile.renderRange.start, tDateProfile, dateEnv);
var normalizedEnd = normalizeDate(dateProfile.renderRange.end, tDateProfile, dateEnv);
// apply minTime/maxTime
// TODO: View should be responsible.
if (tDateProfile.isTimeScale) {
normalizedStart = dateEnv.add(normalizedStart, dateProfile.minTime);
normalizedEnd = dateEnv.add(addDays(normalizedEnd, -1), dateProfile.maxTime);
}
tDateProfile.timeWindowMs = timeWindowMs;
tDateProfile.normalizedRange = { start: normalizedStart, end: normalizedEnd };
var slotDates = [];
var date = normalizedStart;
while (date < normalizedEnd) {
if (isValidDate(date, tDateProfile, dateProfile, view)) {
slotDates.push(date);
}
date = dateEnv.add(date, tDateProfile.slotDuration);
}
tDateProfile.slotDates = slotDates;
// more...
var snapIndex = -1;
var snapDiff = 0; // index of the diff :(
var snapDiffToIndex = [];
var snapIndexToDiff = [];
date = normalizedStart;
while (date < normalizedEnd) {
if (isValidDate(date, tDateProfile, dateProfile, view)) {
snapIndex++;
snapDiffToIndex.push(snapIndex);
snapIndexToDiff.push(snapDiff);
}
else {
snapDiffToIndex.push(snapIndex + 0.5);
}
date = dateEnv.add(date, tDateProfile.snapDuration);
snapDiff++;
}
tDateProfile.snapDiffToIndex = snapDiffToIndex;
tDateProfile.snapIndexToDiff = snapIndexToDiff;
tDateProfile.snapCnt = snapIndex + 1; // is always one behind
tDateProfile.slotCnt = tDateProfile.snapCnt / tDateProfile.snapsPerSlot;
// more...
tDateProfile.isWeekStarts = buildIsWeekStarts(tDateProfile, dateEnv);
tDateProfile.cellRows = buildCellRows(tDateProfile, dateEnv, view);
return tDateProfile;
}
/*
snaps to appropriate unit
*/
function normalizeDate(date, tDateProfile, dateEnv) {
var normalDate = date;
if (!tDateProfile.isTimeScale) {
normalDate = startOfDay(normalDate);
if (tDateProfile.largeUnit) {
normalDate = dateEnv.startOf(normalDate, tDateProfile.largeUnit);
}
}
return normalDate;
}
/*
snaps to appropriate unit
*/
function normalizeRange(range, tDateProfile, dateEnv) {
if (!tDateProfile.isTimeScale) {
range = computeVisibleDayRange(range);
if (tDateProfile.largeUnit) {
var dayRange = range; // preserve original result
range = {
start: dateEnv.startOf(range.start, tDateProfile.largeUnit),
end: dateEnv.startOf(range.end, tDateProfile.largeUnit)
};
// if date is partially through the interval, or is in the same interval as the start,
// make the exclusive end be the *next* interval
if (range.end.valueOf() !== dayRange.end.valueOf() || range.end <= range.start) {
range = {
start: range.start,
end: dateEnv.add(range.end, tDateProfile.slotDuration)
};
}
}
}
return range;
}
function isValidDate(date, tDateProfile, dateProfile, view) {
if (view.dateProfileGenerator.isHiddenDay(date)) {
return false;
}
else if (tDateProfile.isTimeScale) {
// determine if the time is within minTime/maxTime, which may have wacky values
var day = startOfDay(date);
var timeMs = date.valueOf() - day.valueOf();
var ms = timeMs - asRoughMs(dateProfile.minTime); // milliseconds since minTime
ms = ((ms % 86400000) + 86400000) % 86400000; // make negative values wrap to 24hr clock
return ms < tDateProfile.timeWindowMs; // before the maxTime?
}
else {
return true;
}
}
function queryDurationOption(view, name) {
var input = view.opt(name);
if (input != null) {
return createDuration(input);
}
}
function validateLabelAndSlot(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
// make sure labelInterval doesn't exceed the max number of cells
if (tDateProfile.labelInterval) {
var labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.labelInterval);
if (labelCnt > config.MAX_TIMELINE_SLOTS) {
console.warn('slotLabelInterval results in too many cells');
tDateProfile.labelInterval = null;
}
}
// make sure slotDuration doesn't exceed the maximum number of cells
if (tDateProfile.slotDuration) {
var slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.slotDuration);
if (slotCnt > config.MAX_TIMELINE_SLOTS) {
console.warn('slotDuration results in too many cells');
tDateProfile.slotDuration = null;
}
}
// make sure labelInterval is a multiple of slotDuration
if (tDateProfile.labelInterval && tDateProfile.slotDuration) {
var slotsPerLabel = wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
if (slotsPerLabel === null || slotsPerLabel < 1) {
console.warn('slotLabelInterval must be a multiple of slotDuration');
tDateProfile.slotDuration = null;
}
}
}
function ensureLabelInterval(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
var labelInterval = tDateProfile.labelInterval;
if (!labelInterval) {
// compute based off the slot duration
// find the largest label interval with an acceptable slots-per-label
var input = void 0;
if (tDateProfile.slotDuration) {
for (var _i = 0, STOCK_SUB_DURATIONS_1 = STOCK_SUB_DURATIONS; _i < STOCK_SUB_DURATIONS_1.length; _i++) {
input = STOCK_SUB_DURATIONS_1[_i];
var tryLabelInterval = createDuration(input);
var slotsPerLabel = wholeDivideDurations(tryLabelInterval, tDateProfile.slotDuration);
if (slotsPerLabel !== null && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
labelInterval = tryLabelInterval;
break;
}
}
// use the slot duration as a last resort
if (!labelInterval) {
labelInterval = tDateProfile.slotDuration;
}
// compute based off the view's duration
// find the largest label interval that yields the minimum number of labels
}
else {
for (var _a = 0, STOCK_SUB_DURATIONS_2 = STOCK_SUB_DURATIONS; _a < STOCK_SUB_DURATIONS_2.length; _a++) {
input = STOCK_SUB_DURATIONS_2[_a];
labelInterval = createDuration(input);
var labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, labelInterval);
if (labelCnt >= MIN_AUTO_LABELS) {
break;
}
}
}
tDateProfile.labelInterval = labelInterval;
}
return labelInterval;
}
function ensureSlotDuration(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
var slotDuration = tDateProfile.slotDuration;
if (!slotDuration) {
var labelInterval = ensureLabelInterval(tDateProfile, dateProfile, dateEnv); // will compute if necessary
// compute based off the label interval
// find the largest slot duration that is different from labelInterval, but still acceptable
for (var _i = 0, STOCK_SUB_DURATIONS_3 = STOCK_SUB_DURATIONS; _i < STOCK_SUB_DURATIONS_3.length; _i++) {
var input = STOCK_SUB_DURATIONS_3[_i];
var trySlotDuration = createDuration(input);
var slotsPerLabel = wholeDivideDurations(labelInterval, trySlotDuration);
if (slotsPerLabel !== null && slotsPerLabel > 1 && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
slotDuration = trySlotDuration;
break;
}
}
// only allow the value if it won't exceed the view's # of slots limit
if (slotDuration) {
var slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, slotDuration);
if (slotCnt > MAX_AUTO_CELLS) {
slotDuration = null;
}
}
// use the label interval as a last resort
if (!slotDuration) {
slotDuration = labelInterval;
}
tDateProfile.slotDuration = slotDuration;
}
return slotDuration;
}
function computeHeaderFormats(tDateProfile, dateProfile, dateEnv, view) {
var format1;
var format2;
var labelInterval = tDateProfile.labelInterval;
var unit = greatestDurationDenominator(labelInterval).unit;
var weekNumbersVisible = view.opt('weekNumbers');
var format0 = (format1 = (format2 = null));
// NOTE: weekNumber computation function wont work
if ((unit === 'week') && !weekNumbersVisible) {
unit = 'day';
}
switch (unit) {
case 'year':
format0 = { year: 'numeric' }; // '2015'
break;
case 'month':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric' }; // '2015'
}
format1 = { month: 'short' }; // 'Jan'
break;
case 'week':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric' }; // '2015'
}
format1 = { week: 'narrow' }; // 'Wk4'
break;
case 'day':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric', month: 'long' }; // 'January 2014'
}
else if (currentRangeAs('months', dateProfile, dateEnv) > 1) {
format0 = { month: 'long' }; // 'January'
}
if (weekNumbersVisible) {
format1 = { week: 'short' }; // 'Wk 4'
}
format2 = { weekday: 'narrow', day: 'numeric' }; // 'Su 9'
break;
case 'hour':
if (weekNumbersVisible) {
format0 = { week: 'short' }; // 'Wk 4'
}
if (currentRangeAs('days', dateProfile, dateEnv) > 1) {
format1 = { weekday: 'short', day: 'numeric', month: 'numeric', omitCommas: true }; // Sat 4/7
}
format2 = {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'short'
};
break;
case 'minute':
// sufficiently large number of different minute cells?
if ((asRoughMinutes(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
format0 = {
hour: 'numeric',
meridiem: 'short'
};
format1 = function (params) {
return ':' + padStart(params.date.minute, 2); // ':30'
};
}
else {
format0 = {
hour: 'numeric',
minute: 'numeric',
meridiem: 'short'
};
}
break;
case 'second':
// sufficiently large number of different second cells?
if ((asRoughSeconds(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
format0 = { hour: 'numeric', minute: '2-digit', meridiem: 'lowercase' }; // '8:30 PM'
format1 = function (params) {
return ':' + padStart(params.date.second, 2); // ':30'
};
}
else {
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
}
break;
case 'millisecond':
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
format1 = function (params) {
return '.' + padStart(params.millisecond, 3);
};
break;
}
return [].concat(format0 || [], format1 || [], format2 || []);
}
// Compute the number of the give units in the "current" range.
// Won't go more precise than days.
// Will return `0` if there's not a clean whole interval.
function currentRangeAs(unit, dateProfile, dateEnv) {
var range = dateProfile.currentRange;
var res = null;
if (unit === 'years') {
res = dateEnv.diffWholeYears(range.start, range.end);
}
else if (unit === 'months') {
res = dateEnv.diffWholeMonths(range.start, range.end);
}
else if (unit === 'weeks') {
res = dateEnv.diffWholeMonths(range.start, range.end);
}
else if (unit === 'days') {
res = diffWholeDays(range.start, range.end);
}
return res || 0;
}
function buildIsWeekStarts(tDateProfile, dateEnv) {
var slotDates = tDateProfile.slotDates, emphasizeWeeks = tDateProfile.emphasizeWeeks;
var prevWeekNumber = null;
var isWeekStarts = [];
for (var _i = 0, slotDates_1 = slotDates; _i < slotDates_1.length; _i++) {
var slotDate = slotDates_1[_i];
var weekNumber = dateEnv.computeWeekNumber(slotDate);
var isWeekStart = emphasizeWeeks && (prevWeekNumber !== null) && (prevWeekNumber !== weekNumber);
prevWeekNumber = weekNumber;
isWeekStarts.push(isWeekStart);
}
return isWeekStarts;
}
function buildCellRows(tDateProfile, dateEnv, view) {
var slotDates = tDateProfile.slotDates;
var formats = tDateProfile.headerFormats;
var cellRows = formats.map(function (format) { return []; }); // indexed by row,col
// specifically for navclicks
var rowUnits = formats.map(function (format) {
return format.getLargestUnit ? format.getLargestUnit() : null;
});
// builds cellRows and slotCells
for (var i = 0; i < slotDates.length; i++) {
var date = slotDates[i];
var isWeekStart = tDateProfile.isWeekStarts[i];
for (var row = 0; row < formats.length; row++) {
var format = formats[row];
var rowCells = cellRows[row];
var leadingCell = rowCells[rowCells.length - 1];
var isSuperRow = (formats.length > 1) && (row < (formats.length - 1)); // more than one row and not the last
var newCell = null;
if (isSuperRow) {
var text = dateEnv.format(date, format);
if (!leadingCell || (leadingCell.text !== text)) {
newCell = buildCellObject(date, text, rowUnits[row], view);
}
else {
leadingCell.colspan += 1;
}
}
else {
if (!leadingCell ||
isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval))) {
var text = dateEnv.format(date, format);
newCell = buildCellObject(date, text, rowUnits[row], view);
}
else {
leadingCell.colspan += 1;
}
}
if (newCell) {
newCell.weekStart = isWeekStart;
rowCells.push(newCell);
}
}
}
return cellRows;
}
function buildCellObject(date, text, rowUnit, view) {
var spanHtml = buildGotoAnchorHtml(view, {
date: date,
type: rowUnit,
forceOff: !rowUnit
}, {
'class': 'fc-cell-text'
}, htmlEscape(text));
return { text: text, spanHtml: spanHtml, date: date, colspan: 1, isWeekStart: false };
}
var TimelineNowIndicator = /** @class */ (function () {
function TimelineNowIndicator(headParent, bodyParent) {
this.headParent = headParent;
this.bodyParent = bodyParent;
}
TimelineNowIndicator.prototype.render = function (coord, isRtl) {
var styleProps = isRtl ? { right: -coord } : { left: coord };
this.headParent.appendChild(this.arrowEl = createElement('div', {
className: 'fc-now-indicator fc-now-indicator-arrow',
style: styleProps
}));
this.bodyParent.appendChild(this.lineEl = createElement('div', {
className: 'fc-now-indicator fc-now-indicator-line',
style: styleProps
}));
};
TimelineNowIndicator.prototype.unrender = function () {
if (this.arrowEl) {
removeElement(this.arrowEl);
}
if (this.lineEl) {
removeElement(this.lineEl);
}
};
return TimelineNowIndicator;
}());
var STICKY_PROP_VAL = computeStickyPropVal(); // if null, means not supported at all
var IS_MS_EDGE = /Edge/.test(navigator.userAgent);
var IS_SAFARI = STICKY_PROP_VAL === '-webkit-sticky'; // good b/c doesn't confuse chrome
var STICKY_CLASSNAME = 'fc-sticky';
/*
useful beyond the native position:sticky for these reasons:
- support in IE11
- nice centering support
*/
var StickyScroller = /** @class */ (function () {
function StickyScroller(scroller, isRtl, isVertical) {
var _this = this;
this.usingRelative = null;
/*
known bug: called twice on init. problem when mixing with ScrollJoiner
*/
this.updateSize = function () {
var els = Array.prototype.slice.call(_this.scroller.canvas.el.querySelectorAll('.' + STICKY_CLASSNAME));
var elGeoms = _this.queryElGeoms(els);
var viewportWidth = _this.scroller.el.clientWidth;
if (_this.usingRelative) {
var elDestinations = _this.computeElDestinations(elGeoms, viewportWidth); // read before prepPositioning
assignRelativePositions(els, elGeoms, elDestinations);
}
else {
assignStickyPositions(els, elGeoms, viewportWidth);
}
};
this.scroller = scroller;
this.usingRelative =
!STICKY_PROP_VAL || // IE11
(IS_MS_EDGE && isRtl) || // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/18883305/
((IS_MS_EDGE || IS_SAFARI) && isVertical); // because doesn't work with rowspan in tables, our only vertial use
if (this.usingRelative) {
scroller.on('scrollEnd', this.updateSize);
}
}
StickyScroller.prototype.destroy = function () {
this.scroller.off('scrollEnd', this.updateSize);
};
StickyScroller.prototype.queryElGeoms = function (els) {
var canvasOrigin = this.scroller.canvas.el.getBoundingClientRect();
var elGeoms = [];
for (var _i = 0, els_1 = els; _i < els_1.length; _i++) {
var el = els_1[_i];
var parentBound = translateRect(el.parentNode.getBoundingClientRect(), -canvasOrigin.left, -canvasOrigin.top);
var elRect = el.getBoundingClientRect();
var computedStyles = window.getComputedStyle(el);
var computedTextAlign = window.getComputedStyle(el.parentNode).textAlign; // ask the parent
var intendedTextAlign = computedTextAlign;
var naturalBound = null;
if (computedStyles.position !== 'sticky') {
naturalBound = translateRect(elRect, -canvasOrigin.left - (parseFloat(computedStyles.left) || 0), // could be 'auto'
-canvasOrigin.top - (parseFloat(computedStyles.top) || 0));
}
if (el.hasAttribute('data-sticky-center')) {
intendedTextAlign = 'center';
}
elGeoms.push({
parentBound: parentBound,
naturalBound: naturalBound,
elWidth: elRect.width,
elHeight: elRect.height,
computedTextAlign: computedTextAlign,
intendedTextAlign: intendedTextAlign
});
}
return elGeoms;
};
StickyScroller.prototype.computeElDestinations = function (elGeoms, viewportWidth) {
var viewportLeft = this.scroller.getScrollFromLeft();
var viewportTop = this.scroller.getScrollTop();
var viewportRight = viewportLeft + viewportWidth;
return elGeoms.map(function (elGeom) {
var elWidth = elGeom.elWidth, elHeight = elGeom.elHeight, parentBound = elGeom.parentBound, naturalBound = elGeom.naturalBound;
var destLeft; // relative to canvas topleft
var destTop; // "
switch (elGeom.intendedTextAlign) {
case 'left':
destLeft = viewportLeft;
break;
case 'right':
destLeft = viewportRight - elWidth;
break;
case 'center':
destLeft = (viewportLeft + viewportRight) / 2 - elWidth / 2;
break;
}
destLeft = Math.min(destLeft, parentBound.right - elWidth);
destLeft = Math.max(destLeft, parentBound.left);
destTop = viewportTop;
destTop = Math.min(destTop, parentBound.bottom - elHeight);
destTop = Math.max(destTop, naturalBound.top); // better to use natural top for upper bound
return { left: destLeft, top: destTop };
});
};
return StickyScroller;
}());
function assignRelativePositions(els, elGeoms, elDestinations) {
els.forEach(function (el, i) {
var naturalBound = elGeoms[i].naturalBound;
applyStyle(el, {
position: 'relative',
left: elDestinations[i].left - naturalBound.left,
top: elDestinations[i].top - naturalBound.top
});
});
}
function assignStickyPositions(els, elGeoms, viewportWidth) {
els.forEach(function (el, i) {
var stickyLeft = 0;
if (elGeoms[i].intendedTextAlign === 'center') {
stickyLeft = (viewportWidth - elGeoms[i].elWidth) / 2;
// needs to be forced to left?
if (elGeoms[i].computedTextAlign === 'center') {
el.setAttribute('data-sticky-center', '') // remember for next queryElGeoms
;
el.parentNode.style.textAlign = 'left';
}
}
applyStyle(el, {
position: STICKY_PROP_VAL,
left: stickyLeft,
right: 0,
top: 0
});
});
}
function computeStickyPropVal() {
var el = htmlToElement('
var TimeAxis = /** @class */ (function (_super) {
__extends(TimeAxis, _super);
function TimeAxis(context, headerContainerEl, bodyContainerEl) {
var _this = _super.call(this, context) || this;
var layout = _this.layout = new HeaderBodyLayout(headerContainerEl, bodyContainerEl, 'auto');
var headerEnhancedScroller = layout.headerScroller.enhancedScroll;
var bodyEnhancedScroller = layout.bodyScroller.enhancedScroll;
// needs to go after layout, which has ScrollJoiner
_this.headStickyScroller = new StickyScroller(headerEnhancedScroller, _this.isRtl, false); // isVertical=false
_this.bodyStickyScroller = new StickyScroller(bodyEnhancedScroller, _this.isRtl, false); // isVertical=false
_this.header = new TimelineHeader(context, headerEnhancedScroller.canvas.contentEl);
_this.slats = new TimelineSlats(context, bodyEnhancedScroller.canvas.bgEl);
_this.nowIndicator = new TimelineNowIndicator(headerEnhancedScroller.canvas.el, bodyEnhancedScroller.canvas.el);
return _this;
}
TimeAxis.prototype.destroy = function () {
this.layout.destroy();
this.header.destroy();
this.slats.destroy();
this.nowIndicator.unrender();
this.headStickyScroller.destroy();
this.bodyStickyScroller.destroy();
_super.prototype.destroy.call(this);
};
TimeAxis.prototype.render = function (props) {
var tDateProfile = this.tDateProfile =
buildTimelineDateProfile(props.dateProfile, this.view); // TODO: cache
this.header.receiveProps({
dateProfile: props.dateProfile,
tDateProfile: tDateProfile
});
this.slats.receiveProps({
dateProfile: props.dateProfile,
tDateProfile: tDateProfile
});
};
// Now Indicator
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.getNowIndicatorUnit = function (dateProfile) {
// yuck
var tDateProfile = this.tDateProfile =
buildTimelineDateProfile(dateProfile, this.view); // TODO: cache
if (tDateProfile.isTimeScale) {
return greatestDurationDenominator(tDateProfile.slotDuration).unit;
}
};
// will only execute if isTimeScale
TimeAxis.prototype.renderNowIndicator = function (date) {
if (rangeContainsMarker(this.tDateProfile.normalizedRange, date)) {
this.nowIndicator.render(this.dateToCoord(date), this.isRtl);
}
};
// will only execute if isTimeScale
TimeAxis.prototype.unrenderNowIndicator = function () {
this.nowIndicator.unrender();
};
// Sizing
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.applySlotWidth(this.computeSlotWidth());
// adjusts gutters. do after slot widths set
this.layout.setHeight(totalHeight, isAuto);
// pretty much just queries coords. do last
this.slats.updateSize();
};
TimeAxis.prototype.updateStickyScrollers = function () {
this.headStickyScroller.updateSize();
this.bodyStickyScroller.updateSize();
};
TimeAxis.prototype.computeSlotWidth = function () {
var slotWidth = this.opt('slotWidth') || '';
if (slotWidth === '') {
slotWidth = this.computeDefaultSlotWidth(this.tDateProfile);
}
return slotWidth;
};
TimeAxis.prototype.computeDefaultSlotWidth = function (tDateProfile) {
var maxInnerWidth = 0; // TODO: harness core's `matchCellWidths` for this
this.header.innerEls.forEach(function (innerEl, i) {
maxInnerWidth = Math.max(maxInnerWidth, innerEl.getBoundingClientRect().width);
});
var headingCellWidth = Math.ceil(maxInnerWidth) + 1; // assume no padding, and one pixel border
// in TimelineView.defaults we ensured that labelInterval is an interval of slotDuration
// TODO: rename labelDuration?
var slotsPerLabel = wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
var slotWidth = Math.ceil(headingCellWidth / slotsPerLabel);
var minWidth = window.getComputedStyle(this.header.slatColEls[0]).minWidth;
if (minWidth) {
minWidth = parseInt(minWidth, 10);
if (minWidth) {
slotWidth = Math.max(slotWidth, minWidth);
}
}
return slotWidth;
};
TimeAxis.prototype.applySlotWidth = function (slotWidth) {
var _a = this, layout = _a.layout, tDateProfile = _a.tDateProfile;
var containerWidth = '';
var containerMinWidth = '';
var nonLastSlotWidth = '';
if (slotWidth !== '') {
slotWidth = Math.round(slotWidth);
containerWidth = slotWidth * tDateProfile.slotDates.length;
containerMinWidth = '';
nonLastSlotWidth = slotWidth;
var availableWidth = layout.bodyScroller.enhancedScroll.getClientWidth();
if (availableWidth > containerWidth) {
containerMinWidth = availableWidth;
containerWidth = '';
nonLastSlotWidth = Math.floor(availableWidth / tDateProfile.slotDates.length);
}
}
layout.headerScroller.enhancedScroll.canvas.setWidth(containerWidth);
layout.headerScroller.enhancedScroll.canvas.setMinWidth(containerMinWidth);
layout.bodyScroller.enhancedScroll.canvas.setWidth(containerWidth);
layout.bodyScroller.enhancedScroll.canvas.setMinWidth(containerMinWidth);
if (nonLastSlotWidth !== '') {
this.header.slatColEls.slice(0, -1).concat(this.slats.slatColEls.slice(0, -1)).forEach(function (el) {
el.style.width = nonLastSlotWidth + 'px';
});
}
};
// returned value is between 0 and the number of snaps
TimeAxis.prototype.computeDateSnapCoverage = function (date) {
var _a = this, dateEnv = _a.dateEnv, tDateProfile = _a.tDateProfile;
var snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
if (snapDiff < 0) {
return 0;
}
else if (snapDiff >= tDateProfile.snapDiffToIndex.length) {
return tDateProfile.snapCnt;
}
else {
var snapDiffInt = Math.floor(snapDiff);
var snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt];
if (isInt(snapCoverage)) { // not an in-between value
snapCoverage += snapDiff - snapDiffInt; // add the remainder
}
else {
// a fractional value, meaning the date is not visible
// always round up in this case. works for start AND end dates in a range.
snapCoverage = Math.ceil(snapCoverage);
}
return snapCoverage;
}
};
// for LTR, results range from 0 to width of area
// for RTL, results range from negative width of area to 0
TimeAxis.prototype.dateToCoord = function (date) {
var tDateProfile = this.tDateProfile;
var snapCoverage = this.computeDateSnapCoverage(date);
var slotCoverage = snapCoverage / tDateProfile.snapsPerSlot;
var slotIndex = Math.floor(slotCoverage);
slotIndex = Math.min(slotIndex, tDateProfile.slotCnt - 1);
var partial = slotCoverage - slotIndex;
var _a = this.slats, innerCoordCache = _a.innerCoordCache, outerCoordCache = _a.outerCoordCache;
if (this.isRtl) {
return (outerCoordCache.rights[slotIndex] -
(innerCoordCache.getWidth(slotIndex) * partial)) - outerCoordCache.originClientRect.width;
}
else {
return (outerCoordCache.lefts[slotIndex] +
(innerCoordCache.getWidth(slotIndex) * partial));
}
};
TimeAxis.prototype.rangeToCoords = function (range) {
if (this.isRtl) {
return { right: this.dateToCoord(range.start), left: this.dateToCoord(range.end) };
}
else {
return { left: this.dateToCoord(range.start), right: this.dateToCoord(range.end) };
}
};
// Scrolling
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.computeDateScroll = function (duration) {
var dateEnv = this.dateEnv;
var dateProfile = this.props.dateProfile;
var left = 0;
if (dateProfile) {
left = this.dateToCoord(dateEnv.add(startOfDay(dateProfile.activeRange.start), // startOfDay needed?
duration));
// hack to overcome the left borders of non-first slat
if (!this.isRtl && left) {
left += 1;
}
}
return { left: left };
};
TimeAxis.prototype.queryDateScroll = function () {
var enhancedScroll = this.layout.bodyScroller.enhancedScroll;
return {
left: enhancedScroll.getScrollLeft()
};
};
TimeAxis.prototype.applyDateScroll = function (scroll) {
// TODO: lame we have to update both. use the scrolljoiner instead maybe
this.layout.bodyScroller.enhancedScroll.setScrollLeft(scroll.left || 0);
this.layout.headerScroller.enhancedScroll.setScrollLeft(scroll.left || 0);
};
return TimeAxis;
}(Component));
var TimelineLaneEventRenderer = /** @class */ (function (_super) {
__extends(TimelineLaneEventRenderer, _super);
function TimelineLaneEventRenderer(context, masterContainerEl, timeAxis) {
var _this = _super.call(this, context) || this;
_this.masterContainerEl = masterContainerEl;
_this.timeAxis = timeAxis;
return _this;
}
TimelineLaneEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var view = this.context.view;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
classes.unshift('fc-timeline-event', 'fc-h-event');
var timeText = this.getTimeText(eventRange);
return '' +
(timeText ?
'' +
'' +
htmlEscape(timeText) +
'' +
''
:
'') +
'' +
'' +
(eventDef.title ? htmlEscape(eventDef.title) : ' ') +
'' +
'' +
(isResizableFromStart ?
'' :
'') +
(isResizableFromEnd ?
'' :
'') +
'';
};
TimelineLaneEventRenderer.prototype.computeDisplayEventTime = function () {
return !this.timeAxis.tDateProfile.isTimeScale; // because times should be obvious via axis
};
TimelineLaneEventRenderer.prototype.computeDisplayEventEnd = function () {
return false;
};
// Computes a default event time formatting string if `timeFormat` is not explicitly defined
TimelineLaneEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'narrow'
};
};
TimelineLaneEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
if (!this.el && this.masterContainerEl) {
this.el = createElement('div', { className: 'fc-event-container' });
if (mirrorInfo) {
this.el.classList.add('fc-mirror-container');
}
this.masterContainerEl.appendChild(this.el);
}
if (this.el) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
this.el.appendChild(seg.el);
}
}
};
TimelineLaneEventRenderer.prototype.detachSegs = function (segs) {
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
removeElement(seg.el);
}
};
// computes AND assigns (assigns the left/right at least). bad
TimelineLaneEventRenderer.prototype.computeSegSizes = function (segs) {
var timeAxis = this.timeAxis;
for (var _i = 0, segs_3 = segs; _i < segs_3.length; _i++) {
var seg = segs_3[_i];
var coords = timeAxis.rangeToCoords(seg); // works because Seg has start/end
applyStyle(seg.el, {
left: (seg.left = coords.left),
right: -(seg.right = coords.right)
});
}
};
TimelineLaneEventRenderer.prototype.assignSegSizes = function (segs) {
if (!this.el) {
return;
}
// compute seg verticals
for (var _i = 0, segs_4 = segs; _i < segs_4.length; _i++) {
var seg = segs_4[_i];
seg.height = computeHeightAndMargins(seg.el);
}
this.buildSegLevels(segs); // populates above/below props for computeOffsetForSegs
var totalHeight = computeOffsetForSegs(segs); // also assigns seg.top
applyStyleProp(this.el, 'height', totalHeight);
// assign seg verticals
for (var _a = 0, segs_5 = segs; _a < segs_5.length; _a++) {
var seg = segs_5[_a];
applyStyleProp(seg.el, 'top', seg.top);
}
};
TimelineLaneEventRenderer.prototype.buildSegLevels = function (segs) {
var segLevels = [];
segs = this.sortEventSegs(segs);
for (var _i = 0, segs_6 = segs; _i < segs_6.length; _i++) {
var unplacedSeg = segs_6[_i];
unplacedSeg.above = [];
// determine the first level with no collisions
var level = 0; // level index
while (level < segLevels.length) {
var isLevelCollision = false;
// determine collisions
for (var _a = 0, _b = segLevels[level]; _a < _b.length; _a++) {
var placedSeg = _b[_a];
if (timeRowSegsCollide(unplacedSeg, placedSeg)) {
unplacedSeg.above.push(placedSeg);
isLevelCollision = true;
}
}
if (isLevelCollision) {
level += 1;
}
else {
break;
}
}
// insert into the first non-colliding level. create if necessary
(segLevels[level] || (segLevels[level] = []))
.push(unplacedSeg);
// record possible colliding segments below (TODO: automated test for this)
level += 1;
while (level < segLevels.length) {
for (var _c = 0, _d = segLevels[level]; _c < _d.length; _c++) {
var belowSeg = _d[_c];
if (timeRowSegsCollide(unplacedSeg, belowSeg)) {
belowSeg.above.push(unplacedSeg);
}
}
level += 1;
}
}
return segLevels;
};
return TimelineLaneEventRenderer;
}(FgEventRenderer));
function computeOffsetForSegs(segs) {
var max = 0;
for (var _i = 0, segs_7 = segs; _i < segs_7.length; _i++) {
var seg = segs_7[_i];
max = Math.max(max, computeOffsetForSeg(seg));
}
return max;
}
function computeOffsetForSeg(seg) {
if ((seg.top == null)) {
seg.top = computeOffsetForSegs(seg.above);
}
return seg.top + seg.height;
}
function timeRowSegsCollide(seg0, seg1) {
return (seg0.left < seg1.right) && (seg0.right > seg1.left);
}
var TimelineLaneFillRenderer = /** @class */ (function (_super) {
__extends(TimelineLaneFillRenderer, _super);
function TimelineLaneFillRenderer(context, masterContainerEl, timeAxis) {
var _this = _super.call(this, context) || this;
_this.masterContainerEl = masterContainerEl;
_this.timeAxis = timeAxis;
return _this;
}
TimelineLaneFillRenderer.prototype.attachSegs = function (type, segs) {
if (segs.length) {
var className = void 0;
if (type === 'businessHours') {
className = 'bgevent';
}
else {
className = type.toLowerCase();
}
// making a new container each time is OKAY
// all types of segs (background or business hours or whatever) are rendered in one pass
var containerEl = createElement('div', { className: 'fc-' + className + '-container' });
this.masterContainerEl.appendChild(containerEl);
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
containerEl.appendChild(seg.el);
}
return [containerEl]; // return value
}
};
TimelineLaneFillRenderer.prototype.computeSegSizes = function (segs) {
var timeAxis = this.timeAxis;
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
var coords = timeAxis.rangeToCoords(seg);
seg.left = coords.left;
seg.right = coords.right;
}
};
TimelineLaneFillRenderer.prototype.assignSegSizes = function (segs) {
for (var _i = 0, segs_3 = segs; _i < segs_3.length; _i++) {
var seg = segs_3[_i];
applyStyle(seg.el, {
left: seg.left,
right: -seg.right
});
}
};
return TimelineLaneFillRenderer;
}(FillRenderer));
var TimelineLane = /** @class */ (function (_super) {
__extends(TimelineLane, _super);
function TimelineLane(context, fgContainerEl, bgContainerEl, timeAxis) {
var _this = _super.call(this, context, bgContainerEl) // should el be bgContainerEl???
|| this;
_this.slicer = new TimelineLaneSlicer();
_this.renderEventDrag = memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag);
_this.renderEventResize = memoizeRendering(_this._renderEventResize, _this._unrenderEventResize);
var fillRenderer = _this.fillRenderer = new TimelineLaneFillRenderer(context, bgContainerEl, timeAxis);
var eventRenderer = _this.eventRenderer = new TimelineLaneEventRenderer(context, fgContainerEl, timeAxis);
_this.mirrorRenderer = new TimelineLaneEventRenderer(context, fgContainerEl, timeAxis);
_this.renderBusinessHours = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'));
_this.renderDateSelection = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'));
_this.renderBgEvents = memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'));
_this.renderFgEvents = memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer));
_this.renderEventSelection = memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.timeAxis = timeAxis;
return _this;
}
TimelineLane.prototype.render = function (props) {
var slicedProps = this.slicer.sliceProps(props, props.dateProfile, this.timeAxis.tDateProfile.isTimeScale ? null : props.nextDayThreshold, this, this.timeAxis);
this.renderBusinessHours(slicedProps.businessHourSegs);
this.renderDateSelection(slicedProps.dateSelectionSegs);
this.renderBgEvents(slicedProps.bgEventSegs);
this.renderFgEvents(slicedProps.fgEventSegs);
this.renderEventSelection(slicedProps.eventSelection);
this.renderEventDrag(slicedProps.eventDrag);
this.renderEventResize(slicedProps.eventResize);
};
TimelineLane.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderBusinessHours.unrender();
this.renderDateSelection.unrender();
this.renderBgEvents.unrender();
this.renderFgEvents.unrender();
this.renderEventSelection.unrender();
this.renderEventDrag.unrender();
this.renderEventResize.unrender();
};
TimelineLane.prototype._renderEventDrag = function (state) {
if (state) {
this.eventRenderer.hideByHash(state.affectedInstances);
this.mirrorRenderer.renderSegs(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._unrenderEventDrag = function (state) {
if (state) {
this.eventRenderer.showByHash(state.affectedInstances);
this.mirrorRenderer.unrender(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._renderEventResize = function (state) {
if (state) {
// HACK. eventRenderer and fillRenderer both use these segs. would compete over seg.el
var segsForHighlight = state.segs.map(function (seg) {
return __assign({}, seg);
});
this.eventRenderer.hideByHash(state.affectedInstances);
this.fillRenderer.renderSegs('highlight', segsForHighlight);
this.mirrorRenderer.renderSegs(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._unrenderEventResize = function (state) {
if (state) {
this.eventRenderer.showByHash(state.affectedInstances);
this.fillRenderer.unrender('highlight');
this.mirrorRenderer.unrender(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype.updateSize = function (isResize) {
var _a = this, fillRenderer = _a.fillRenderer, eventRenderer = _a.eventRenderer, mirrorRenderer = _a.mirrorRenderer;
fillRenderer.computeSizes(isResize);
eventRenderer.computeSizes(isResize);
mirrorRenderer.computeSizes(isResize);
fillRenderer.assignSizes(isResize);
eventRenderer.assignSizes(isResize);
mirrorRenderer.assignSizes(isResize);
};
return TimelineLane;
}(DateComponent));
var TimelineLaneSlicer = /** @class */ (function (_super) {
__extends(TimelineLaneSlicer, _super);
function TimelineLaneSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimelineLaneSlicer.prototype.sliceRange = function (origRange, timeAxis) {
var tDateProfile = timeAxis.tDateProfile;
var dateProfile = timeAxis.props.dateProfile;
var normalRange = normalizeRange(origRange, tDateProfile, timeAxis.dateEnv);
var segs = [];
// protect against when the span is entirely in an invalid date region
if (timeAxis.computeDateSnapCoverage(normalRange.start) < timeAxis.computeDateSnapCoverage(normalRange.end)) {
// intersect the footprint's range with the grid's range
var slicedRange = intersectRanges(normalRange, tDateProfile.normalizedRange);
if (slicedRange) {
segs.push({
start: slicedRange.start,
end: slicedRange.end,
isStart: slicedRange.start.valueOf() === normalRange.start.valueOf() && isValidDate(slicedRange.start, tDateProfile, dateProfile, timeAxis.view),
isEnd: slicedRange.end.valueOf() === normalRange.end.valueOf() && isValidDate(addMs(slicedRange.end, -1), tDateProfile, dateProfile, timeAxis.view)
});
}
}
return segs;
};
return TimelineLaneSlicer;
}(Slicer));
var TimelineView = /** @class */ (function (_super) {
__extends(TimelineView, _super);
function TimelineView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.el.classList.add('fc-timeline');
if (_this.opt('eventOverlap') === false) {
_this.el.classList.add('fc-no-overlap');
}
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.timeAxis = new TimeAxis(_this.context, _this.el.querySelector('thead .fc-time-area'), _this.el.querySelector('tbody .fc-time-area'));
_this.lane = new TimelineLane(_this.context, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.contentEl, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.bgEl, _this.timeAxis);
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeAxis.slats.el
});
return _this;
}
TimelineView.prototype.destroy = function () {
this.timeAxis.destroy();
this.lane.destroy();
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
TimelineView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return " ";
};
TimelineView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // flags for updateSize, addScroll
this.timeAxis.receiveProps({
dateProfile: props.dateProfile
});
this.lane.receiveProps(__assign({}, props, { nextDayThreshold: this.nextDayThreshold }));
};
TimelineView.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.timeAxis.updateSize(isResize, totalHeight, isAuto);
this.lane.updateSize(isResize);
};
// Now Indicator
// ------------------------------------------------------------------------------------------
TimelineView.prototype.getNowIndicatorUnit = function (dateProfile) {
return this.timeAxis.getNowIndicatorUnit(dateProfile);
};
TimelineView.prototype.renderNowIndicator = function (date) {
this.timeAxis.renderNowIndicator(date);
};
TimelineView.prototype.unrenderNowIndicator = function () {
this.timeAxis.unrenderNowIndicator();
};
// Scroll System
// ------------------------------------------------------------------------------------------
TimelineView.prototype.computeDateScroll = function (duration) {
return this.timeAxis.computeDateScroll(duration);
};
TimelineView.prototype.applyScroll = function (scroll, isResize) {
_super.prototype.applyScroll.call(this, scroll, isResize); // will call applyDateScroll
// avoid updating stickyscroll too often
// TODO: repeat code as ResourceTimelineView::updateSize
var calendar = this.calendar;
if (isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated) {
this.timeAxis.updateStickyScrollers();
}
};
TimelineView.prototype.applyDateScroll = function (scroll) {
this.timeAxis.applyDateScroll(scroll);
};
TimelineView.prototype.queryScroll = function () {
var enhancedScroll = this.timeAxis.layout.bodyScroller.enhancedScroll;
return {
top: enhancedScroll.getScrollTop(),
left: enhancedScroll.getScrollLeft()
};
};
// Hit System
// ------------------------------------------------------------------------------------------
TimelineView.prototype.buildPositionCaches = function () {
this.timeAxis.slats.updateSize();
};
TimelineView.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) {
var slatHit = this.timeAxis.slats.positionToHit(positionLeft);
if (slatHit) {
return {
component: this,
dateSpan: slatHit.dateSpan,
rect: {
left: slatHit.left,
right: slatHit.right,
top: 0,
bottom: elHeight
},
dayEl: slatHit.dayEl,
layer: 0
};
}
};
return TimelineView;
}(View));
var main = createPlugin({
defaultView: 'timelineDay',
views: {
timeline: {
class: TimelineView,
eventResizableFromStart: true // how is this consumed for TimelineView tho?
},
timelineDay: {
type: 'timeline',
duration: { days: 1 }
},
timelineWeek: {
type: 'timeline',
duration: { weeks: 1 }
},
timelineMonth: {
type: 'timeline',
duration: { months: 1 }
},
timelineYear: {
type: 'timeline',
duration: { years: 1 }
}
}
});
export default main;
export { HeaderBodyLayout, ScrollJoiner, StickyScroller, TimeAxis, TimelineLane, TimelineView };
/*!
FullCalendar Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarTimeline = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
/*
A rectangular area of content that lives within a Scroller.
Can have "gutters", areas of dead spacing around the perimeter.
Also very useful for forcing a width, which a Scroller cannot do alone.
Has a content area that lives above a background area.
*/
var ScrollerCanvas = /** @class */ (function () {
function ScrollerCanvas() {
this.gutters = {};
this.el = core.htmlToElement("
var EnhancedScroller = /** @class */ (function (_super) {
__extends(EnhancedScroller, _super);
function EnhancedScroller(overflowX, overflowY) {
var _this = _super.call(this, overflowX, overflowY) || this;
// Scroll Events
// ----------------------------------------------------------------------------------------------
_this.reportScroll = function () {
if (!_this.isScrolling) {
_this.reportScrollStart();
}
_this.trigger('scroll');
_this.isMoving = true;
_this.requestMovingEnd();
};
_this.reportScrollStart = function () {
if (!_this.isScrolling) {
_this.isScrolling = true;
_this.trigger('scrollStart', _this.isTouching); // created in constructor
}
};
// Touch Events
// ----------------------------------------------------------------------------------------------
// will fire *before* the scroll event is fired
_this.reportTouchStart = function () {
_this.isTouching = true;
};
_this.reportTouchEnd = function () {
if (_this.isTouching) {
_this.isTouching = false;
// if touch scrolling was re-enabled during a recent touch scroll
// then unbind the handlers that are preventing it from happening.
if (_this.isTouchScrollEnabled) {
_this.unbindPreventTouchScroll(); // won't do anything if not bound
}
// if the user ended their touch, and the scroll area wasn't moving,
// we consider this to be the end of the scroll.
if (!_this.isMoving) {
_this.reportScrollEnd(); // won't fire if already ended
}
}
};
_this.isScrolling = false;
_this.isTouching = false;
_this.isMoving = false;
_this.isTouchScrollEnabled = true;
_this.requestMovingEnd = core.debounce(_this.reportMovingEnd, 500);
_this.canvas = new ScrollerCanvas();
_this.el.appendChild(_this.canvas.el);
_this.applyOverflow();
_this.bindHandlers();
return _this;
}
EnhancedScroller.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.unbindHandlers();
};
// Touch scroll prevention
// ----------------------------------------------------------------------------------------------
EnhancedScroller.prototype.disableTouchScroll = function () {
this.isTouchScrollEnabled = false;
this.bindPreventTouchScroll(); // will be unbound in enableTouchScroll or reportTouchEnd
};
EnhancedScroller.prototype.enableTouchScroll = function () {
this.isTouchScrollEnabled = true;
// only immediately unbind if a touch event is NOT in progress.
// otherwise, it will be handled by reportTouchEnd.
if (!this.isTouching) {
this.unbindPreventTouchScroll();
}
};
EnhancedScroller.prototype.bindPreventTouchScroll = function () {
if (!this.preventTouchScrollHandler) {
this.el.addEventListener('touchmove', (this.preventTouchScrollHandler = core.preventDefault));
}
};
EnhancedScroller.prototype.unbindPreventTouchScroll = function () {
if (this.preventTouchScrollHandler) {
this.el.removeEventListener('touchmove', this.preventTouchScrollHandler);
this.preventTouchScrollHandler = null;
}
};
// Handlers
// ----------------------------------------------------------------------------------------------
EnhancedScroller.prototype.bindHandlers = function () {
this.el.addEventListener('scroll', this.reportScroll);
this.el.addEventListener('touchstart', this.reportTouchStart, { passive: true });
this.el.addEventListener('touchend', this.reportTouchEnd);
};
EnhancedScroller.prototype.unbindHandlers = function () {
this.el.removeEventListener('scroll', this.reportScroll);
this.el.removeEventListener('touchstart', this.reportTouchStart, { passive: true });
this.el.removeEventListener('touchend', this.reportTouchEnd);
};
EnhancedScroller.prototype.reportMovingEnd = function () {
this.isMoving = false;
// only end the scroll if not currently touching.
// if touching, the scrolling will end later, on touchend.
if (!this.isTouching) {
this.reportScrollEnd();
}
};
EnhancedScroller.prototype.reportScrollEnd = function () {
if (this.isScrolling) {
this.trigger('scrollEnd');
this.isScrolling = false;
}
};
// Horizontal Scroll Normalization
// ----------------------------------------------------------------------------------------------
// http://stackoverflow.com/questions/24276619/better-way-to-get-the-viewport-of-a-scrollable-div-in-rtl-mode/24394376#24394376
// TODO: move all this to util functions
/*
If RTL, and scrolled to the left, returns NEGATIVE value (like Firefox)
*/
EnhancedScroller.prototype.getScrollLeft = function () {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
var val = el.scrollLeft;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'positive':
val = (val + el.clientWidth) - el.scrollWidth;
break;
case 'reverse':
val = -val;
break;
}
}
return val;
};
/*
Accepts a NEGATIVE value for when scrolled in RTL
*/
EnhancedScroller.prototype.setScrollLeft = function (val) {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'positive':
val = (val - el.clientWidth) + el.scrollWidth;
break;
case 'reverse':
val = -val;
break;
}
}
el.scrollLeft = val;
};
/*
Always returns the number of pixels scrolled from the leftmost position (even if RTL).
Always positive.
*/
EnhancedScroller.prototype.getScrollFromLeft = function () {
var el = this.el;
var direction = window.getComputedStyle(el).direction;
var val = el.scrollLeft;
if (direction === 'rtl') {
switch (getRtlScrollSystem()) {
case 'negative':
val = (val - el.clientWidth) + el.scrollWidth;
break;
case 'reverse':
val = (-val - el.clientWidth) + el.scrollWidth;
break;
}
}
return val;
};
return EnhancedScroller;
}(core.ScrollComponent));
core.EmitterMixin.mixInto(EnhancedScroller);
// Horizontal Scroll System Detection
// ----------------------------------------------------------------------------------------------
var _rtlScrollSystem;
function getRtlScrollSystem() {
return _rtlScrollSystem || (_rtlScrollSystem = detectRtlScrollSystem());
}
function detectRtlScrollSystem() {
var el = core.htmlToElement("
/*
A Scroller, but with a wrapping div that allows "clipping" away of native scrollbars,
giving the appearance that there are no scrollbars.
*/
var ClippedScroller = /** @class */ (function () {
/*
Received overflows can be set to 'clipped', meaning scrollbars shouldn't be visible
to the user, but the area should still scroll.
*/
function ClippedScroller(overflowX, overflowY, parentEl) {
this.isHScrollbarsClipped = false;
this.isVScrollbarsClipped = false;
if (overflowX === 'clipped-scroll') {
overflowX = 'scroll';
this.isHScrollbarsClipped = true;
}
if (overflowY === 'clipped-scroll') {
overflowY = 'scroll';
this.isVScrollbarsClipped = true;
}
this.enhancedScroll = new EnhancedScroller(overflowX, overflowY);
parentEl.appendChild(this.el = core.createElement('div', {
className: 'fc-scroller-clip'
}));
this.el.appendChild(this.enhancedScroll.el);
}
ClippedScroller.prototype.destroy = function () {
core.removeElement(this.el);
};
ClippedScroller.prototype.updateSize = function () {
var enhancedScroll = this.enhancedScroll;
var scrollEl = enhancedScroll.el;
var edges = core.computeEdges(scrollEl);
var cssProps = { marginLeft: 0, marginRight: 0, marginTop: 0, marginBottom: 0 };
// give the inner scrolling div negative margins so that its scrollbars
// are nudged outside of the bounding box of the wrapper, which is overflow:hidden
if (this.isVScrollbarsClipped) {
cssProps.marginLeft = -edges.scrollbarLeft;
cssProps.marginRight = -edges.scrollbarRight;
}
if (this.isHScrollbarsClipped) {
cssProps.marginBottom = -edges.scrollbarBottom;
}
core.applyStyle(scrollEl, cssProps);
// if we are attempting to hide the scrollbars offscreen, OSX/iOS will still
// display the floating scrollbars. attach a className to force-hide them.
if ((this.isHScrollbarsClipped || (enhancedScroll.overflowX === 'hidden')) && // should never show?
(this.isVScrollbarsClipped || (enhancedScroll.overflowY === 'hidden')) && // should never show?
!( // doesn't have any scrollbar mass
edges.scrollbarLeft ||
edges.scrollbarRight ||
edges.scrollbarBottom)) {
scrollEl.classList.add('fc-no-scrollbars');
}
else {
scrollEl.classList.remove('fc-no-scrollbars');
}
};
ClippedScroller.prototype.setHeight = function (height) {
this.enhancedScroll.setHeight(height);
};
/*
Accounts for 'clipped' scrollbars
*/
ClippedScroller.prototype.getScrollbarWidths = function () {
var widths = this.enhancedScroll.getScrollbarWidths();
if (this.isVScrollbarsClipped) {
widths.left = 0;
widths.right = 0;
}
if (this.isHScrollbarsClipped) {
widths.bottom = 0;
}
return widths;
};
return ClippedScroller;
}());
var ScrollJoiner = /** @class */ (function () {
function ScrollJoiner(axis, scrollers) {
this.axis = axis;
this.scrollers = scrollers;
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var scroller = _a[_i];
this.initScroller(scroller);
}
}
ScrollJoiner.prototype.initScroller = function (scroller) {
var _this = this;
var enhancedScroll = scroller.enhancedScroll;
// when the user scrolls via mousewheel, we know for sure the target
// scroller should be the master. capture the various x-browser events that fire.
var onScroll = function () {
_this.assignMasterScroller(scroller);
};
'wheel mousewheel DomMouseScroll MozMousePixelScroll'.split(' ').forEach(function (evName) {
enhancedScroll.el.addEventListener(evName, onScroll);
});
enhancedScroll
.on('scrollStart', function () {
if (!_this.masterScroller) {
_this.assignMasterScroller(scroller);
}
})
.on('scroll', function () {
if (scroller === _this.masterScroller) {
for (var _i = 0, _a = _this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
if (otherScroller !== scroller) {
switch (_this.axis) {
case 'horizontal':
otherScroller.enhancedScroll.el.scrollLeft = enhancedScroll.el.scrollLeft;
break;
case 'vertical':
otherScroller.enhancedScroll.setScrollTop(enhancedScroll.getScrollTop());
break;
}
}
}
}
})
.on('scrollEnd', function () {
if (scroller === _this.masterScroller) {
_this.unassignMasterScroller();
}
});
};
ScrollJoiner.prototype.assignMasterScroller = function (scroller) {
this.unassignMasterScroller();
this.masterScroller = scroller;
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
if (otherScroller !== scroller) {
otherScroller.enhancedScroll.disableTouchScroll();
}
}
};
ScrollJoiner.prototype.unassignMasterScroller = function () {
if (this.masterScroller) {
for (var _i = 0, _a = this.scrollers; _i < _a.length; _i++) {
var otherScroller = _a[_i];
otherScroller.enhancedScroll.enableTouchScroll();
}
this.masterScroller = null;
}
};
ScrollJoiner.prototype.update = function () {
var allWidths = this.scrollers.map(function (scroller) { return scroller.getScrollbarWidths(); });
var maxLeft = 0;
var maxRight = 0;
var maxTop = 0;
var maxBottom = 0;
var widths;
var i;
for (var _i = 0, allWidths_1 = allWidths; _i < allWidths_1.length; _i++) {
widths = allWidths_1[_i];
maxLeft = Math.max(maxLeft, widths.left);
maxRight = Math.max(maxRight, widths.right);
maxTop = Math.max(maxTop, widths.top);
maxBottom = Math.max(maxBottom, widths.bottom);
}
for (i = 0; i < this.scrollers.length; i++) {
var scroller = this.scrollers[i];
widths = allWidths[i];
scroller.enhancedScroll.canvas.setGutters(this.axis === 'horizontal' ?
{
left: maxLeft - widths.left,
right: maxRight - widths.right
} :
{
top: maxTop - widths.top,
bottom: maxBottom - widths.bottom
});
}
};
return ScrollJoiner;
}());
var HeaderBodyLayout = /** @class */ (function () {
/*
verticalScroll = 'auto' | 'clipped-scroll'
*/
function HeaderBodyLayout(headerContainerEl, bodyContainerEl, verticalScroll) {
this.headerScroller = new ClippedScroller('clipped-scroll', 'hidden', headerContainerEl);
this.bodyScroller = new ClippedScroller('auto', verticalScroll, bodyContainerEl);
this.scrollJoiner = new ScrollJoiner('horizontal', [
this.headerScroller,
this.bodyScroller
]);
}
HeaderBodyLayout.prototype.destroy = function () {
this.headerScroller.destroy();
this.bodyScroller.destroy();
};
HeaderBodyLayout.prototype.setHeight = function (totalHeight, isAuto) {
var bodyHeight;
if (isAuto) {
bodyHeight = 'auto';
}
else {
bodyHeight = totalHeight - this.queryHeadHeight();
}
this.bodyScroller.setHeight(bodyHeight);
this.headerScroller.updateSize(); // adjusts gutters and classNames
this.bodyScroller.updateSize(); // adjusts gutters and classNames
this.scrollJoiner.update();
};
HeaderBodyLayout.prototype.queryHeadHeight = function () {
return this.headerScroller.enhancedScroll.canvas.contentEl.getBoundingClientRect().height;
};
return HeaderBodyLayout;
}());
var TimelineHeader = /** @class */ (function (_super) {
__extends(TimelineHeader, _super);
function TimelineHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.appendChild(_this.tableEl = core.createElement('table', {
className: _this.theme.getClass('tableGrid')
}));
return _this;
}
TimelineHeader.prototype.destroy = function () {
core.removeElement(this.tableEl);
_super.prototype.destroy.call(this);
};
TimelineHeader.prototype.render = function (props) {
this.renderDates(props.tDateProfile);
};
TimelineHeader.prototype.renderDates = function (tDateProfile) {
var _a = this, dateEnv = _a.dateEnv, theme = _a.theme;
var cellRows = tDateProfile.cellRows;
var lastRow = cellRows[cellRows.length - 1];
var isChrono = core.asRoughMs(tDateProfile.labelInterval) > core.asRoughMs(tDateProfile.slotDuration);
var oneDay = core.isSingleDay(tDateProfile.slotDuration);
var html = '';
// needs to be a col for each body slat. header cells will have colspans
for (var i = tDateProfile.slotCnt - 1; i >= 0; i--) {
html += '';
}
html += '';
html += '';
for (var _i = 0, cellRows_1 = cellRows; _i < cellRows_1.length; _i++) {
var rowCells = cellRows_1[_i];
var isLast = rowCells === lastRow;
html += '';
for (var _b = 0, rowCells_1 = rowCells; _b < rowCells_1.length; _b++) {
var cell = rowCells_1[_b];
var headerCellClassNames = [theme.getClass('widgetHeader')];
if (cell.isWeekStart) {
headerCellClassNames.push('fc-em-cell');
}
if (oneDay) {
headerCellClassNames = headerCellClassNames.concat(core.getDayClasses(cell.date, this.props.dateProfile, this.context, true) // adds "today" class and other day-based classes
);
}
html +=
' 1 ? ' colspan="' + cell.colspan + '"' : '') +
'>' +
'
var TimelineSlats = /** @class */ (function (_super) {
__extends(TimelineSlats, _super);
function TimelineSlats(context, parentEl) {
var _this = _super.call(this, context) || this;
parentEl.appendChild(_this.el = core.createElement('div', { className: 'fc-slats' }));
return _this;
}
TimelineSlats.prototype.destroy = function () {
core.removeElement(this.el);
_super.prototype.destroy.call(this);
};
TimelineSlats.prototype.render = function (props) {
this.renderDates(props.tDateProfile);
};
TimelineSlats.prototype.renderDates = function (tDateProfile) {
var _a = this, theme = _a.theme, view = _a.view, dateEnv = _a.dateEnv;
var slotDates = tDateProfile.slotDates, isWeekStarts = tDateProfile.isWeekStarts;
var html = '' +
'';
for (var i = 0; i < slotDates.length; i++) {
html += '';
}
html += '';
html += '';
for (var i = 0; i < slotDates.length; i++) {
html += this.slatCellHtml(slotDates[i], isWeekStarts[i], tDateProfile);
}
html += '';
this.el.innerHTML = html;
this.slatColEls = core.findElements(this.el, 'col');
this.slatEls = core.findElements(this.el, 'td');
for (var i = 0; i < slotDates.length; i++) {
view.publiclyTrigger('dayRender', [
{
date: dateEnv.toDate(slotDates[i]),
el: this.slatEls[i],
view: view
}
]);
}
this.outerCoordCache = new core.PositionCache(this.el, this.slatEls, true, // isHorizontal
false // isVertical
);
// for the inner divs within the slats
// used for event rendering and scrollTime, to disregard slat border
this.innerCoordCache = new core.PositionCache(this.el, core.findChildren(this.slatEls, 'div'), true, // isHorizontal
false // isVertical
);
};
TimelineSlats.prototype.slatCellHtml = function (date, isEm, tDateProfile) {
var _a = this, theme = _a.theme, dateEnv = _a.dateEnv;
var classes;
if (tDateProfile.isTimeScale) {
classes = [];
classes.push(core.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval)) ?
'fc-major' :
'fc-minor');
}
else {
classes = core.getDayClasses(date, this.props.dateProfile, this.context);
classes.push('fc-day');
}
classes.unshift(theme.getClass('widgetContent'));
if (isEm) {
classes.push('fc-em-cell');
}
return '
var MIN_AUTO_LABELS = 18; // more than `12` months but less that `24` hours
var MAX_AUTO_SLOTS_PER_LABEL = 6; // allows 6 10-min slots in an hour
var MAX_AUTO_CELLS = 200; // allows 4-days to have a :30 slot duration
core.config.MAX_TIMELINE_SLOTS = 1000;
// potential nice values for slot-duration and interval-duration
var STOCK_SUB_DURATIONS = [
{ years: 1 },
{ months: 1 },
{ days: 1 },
{ hours: 1 },
{ minutes: 30 },
{ minutes: 15 },
{ minutes: 10 },
{ minutes: 5 },
{ minutes: 1 },
{ seconds: 30 },
{ seconds: 15 },
{ seconds: 10 },
{ seconds: 5 },
{ seconds: 1 },
{ milliseconds: 500 },
{ milliseconds: 100 },
{ milliseconds: 10 },
{ milliseconds: 1 }
];
function buildTimelineDateProfile(dateProfile, view) {
var dateEnv = view.dateEnv;
var tDateProfile = {
labelInterval: queryDurationOption(view, 'slotLabelInterval'),
slotDuration: queryDurationOption(view, 'slotDuration')
};
validateLabelAndSlot(tDateProfile, dateProfile, dateEnv); // validate after computed grid duration
ensureLabelInterval(tDateProfile, dateProfile, dateEnv);
ensureSlotDuration(tDateProfile, dateProfile, dateEnv);
var input = view.opt('slotLabelFormat');
var rawFormats = Array.isArray(input) ?
input
: (input != null) ?
[input]
:
computeHeaderFormats(tDateProfile, dateProfile, dateEnv, view);
tDateProfile.headerFormats = rawFormats.map(function (rawFormat) {
return core.createFormatter(rawFormat);
});
tDateProfile.isTimeScale = Boolean(tDateProfile.slotDuration.milliseconds);
var largeUnit = null;
if (!tDateProfile.isTimeScale) {
var slotUnit = core.greatestDurationDenominator(tDateProfile.slotDuration).unit;
if (/year|month|week/.test(slotUnit)) {
largeUnit = slotUnit;
}
}
tDateProfile.largeUnit = largeUnit;
tDateProfile.emphasizeWeeks =
core.isSingleDay(tDateProfile.slotDuration) &&
currentRangeAs('weeks', dateProfile, dateEnv) >= 2 &&
!view.opt('businessHours');
/*
console.log('label interval =', timelineView.labelInterval.humanize())
console.log('slot duration =', timelineView.slotDuration.humanize())
console.log('header formats =', timelineView.headerFormats)
console.log('isTimeScale', timelineView.isTimeScale)
console.log('largeUnit', timelineView.largeUnit)
*/
var rawSnapDuration = view.opt('snapDuration');
var snapDuration;
var snapsPerSlot;
if (rawSnapDuration) {
snapDuration = core.createDuration(rawSnapDuration);
snapsPerSlot = core.wholeDivideDurations(tDateProfile.slotDuration, snapDuration);
// ^ TODO: warning if not whole?
}
if (snapsPerSlot == null) {
snapDuration = tDateProfile.slotDuration;
snapsPerSlot = 1;
}
tDateProfile.snapDuration = snapDuration;
tDateProfile.snapsPerSlot = snapsPerSlot;
// more...
var timeWindowMs = core.asRoughMs(dateProfile.maxTime) - core.asRoughMs(dateProfile.minTime);
// TODO: why not use normalizeRange!?
var normalizedStart = normalizeDate(dateProfile.renderRange.start, tDateProfile, dateEnv);
var normalizedEnd = normalizeDate(dateProfile.renderRange.end, tDateProfile, dateEnv);
// apply minTime/maxTime
// TODO: View should be responsible.
if (tDateProfile.isTimeScale) {
normalizedStart = dateEnv.add(normalizedStart, dateProfile.minTime);
normalizedEnd = dateEnv.add(core.addDays(normalizedEnd, -1), dateProfile.maxTime);
}
tDateProfile.timeWindowMs = timeWindowMs;
tDateProfile.normalizedRange = { start: normalizedStart, end: normalizedEnd };
var slotDates = [];
var date = normalizedStart;
while (date < normalizedEnd) {
if (isValidDate(date, tDateProfile, dateProfile, view)) {
slotDates.push(date);
}
date = dateEnv.add(date, tDateProfile.slotDuration);
}
tDateProfile.slotDates = slotDates;
// more...
var snapIndex = -1;
var snapDiff = 0; // index of the diff :(
var snapDiffToIndex = [];
var snapIndexToDiff = [];
date = normalizedStart;
while (date < normalizedEnd) {
if (isValidDate(date, tDateProfile, dateProfile, view)) {
snapIndex++;
snapDiffToIndex.push(snapIndex);
snapIndexToDiff.push(snapDiff);
}
else {
snapDiffToIndex.push(snapIndex + 0.5);
}
date = dateEnv.add(date, tDateProfile.snapDuration);
snapDiff++;
}
tDateProfile.snapDiffToIndex = snapDiffToIndex;
tDateProfile.snapIndexToDiff = snapIndexToDiff;
tDateProfile.snapCnt = snapIndex + 1; // is always one behind
tDateProfile.slotCnt = tDateProfile.snapCnt / tDateProfile.snapsPerSlot;
// more...
tDateProfile.isWeekStarts = buildIsWeekStarts(tDateProfile, dateEnv);
tDateProfile.cellRows = buildCellRows(tDateProfile, dateEnv, view);
return tDateProfile;
}
/*
snaps to appropriate unit
*/
function normalizeDate(date, tDateProfile, dateEnv) {
var normalDate = date;
if (!tDateProfile.isTimeScale) {
normalDate = core.startOfDay(normalDate);
if (tDateProfile.largeUnit) {
normalDate = dateEnv.startOf(normalDate, tDateProfile.largeUnit);
}
}
return normalDate;
}
/*
snaps to appropriate unit
*/
function normalizeRange(range, tDateProfile, dateEnv) {
if (!tDateProfile.isTimeScale) {
range = core.computeVisibleDayRange(range);
if (tDateProfile.largeUnit) {
var dayRange = range; // preserve original result
range = {
start: dateEnv.startOf(range.start, tDateProfile.largeUnit),
end: dateEnv.startOf(range.end, tDateProfile.largeUnit)
};
// if date is partially through the interval, or is in the same interval as the start,
// make the exclusive end be the *next* interval
if (range.end.valueOf() !== dayRange.end.valueOf() || range.end <= range.start) {
range = {
start: range.start,
end: dateEnv.add(range.end, tDateProfile.slotDuration)
};
}
}
}
return range;
}
function isValidDate(date, tDateProfile, dateProfile, view) {
if (view.dateProfileGenerator.isHiddenDay(date)) {
return false;
}
else if (tDateProfile.isTimeScale) {
// determine if the time is within minTime/maxTime, which may have wacky values
var day = core.startOfDay(date);
var timeMs = date.valueOf() - day.valueOf();
var ms = timeMs - core.asRoughMs(dateProfile.minTime); // milliseconds since minTime
ms = ((ms % 86400000) + 86400000) % 86400000; // make negative values wrap to 24hr clock
return ms < tDateProfile.timeWindowMs; // before the maxTime?
}
else {
return true;
}
}
function queryDurationOption(view, name) {
var input = view.opt(name);
if (input != null) {
return core.createDuration(input);
}
}
function validateLabelAndSlot(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
// make sure labelInterval doesn't exceed the max number of cells
if (tDateProfile.labelInterval) {
var labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.labelInterval);
if (labelCnt > core.config.MAX_TIMELINE_SLOTS) {
console.warn('slotLabelInterval results in too many cells');
tDateProfile.labelInterval = null;
}
}
// make sure slotDuration doesn't exceed the maximum number of cells
if (tDateProfile.slotDuration) {
var slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, tDateProfile.slotDuration);
if (slotCnt > core.config.MAX_TIMELINE_SLOTS) {
console.warn('slotDuration results in too many cells');
tDateProfile.slotDuration = null;
}
}
// make sure labelInterval is a multiple of slotDuration
if (tDateProfile.labelInterval && tDateProfile.slotDuration) {
var slotsPerLabel = core.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
if (slotsPerLabel === null || slotsPerLabel < 1) {
console.warn('slotLabelInterval must be a multiple of slotDuration');
tDateProfile.slotDuration = null;
}
}
}
function ensureLabelInterval(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
var labelInterval = tDateProfile.labelInterval;
if (!labelInterval) {
// compute based off the slot duration
// find the largest label interval with an acceptable slots-per-label
var input = void 0;
if (tDateProfile.slotDuration) {
for (var _i = 0, STOCK_SUB_DURATIONS_1 = STOCK_SUB_DURATIONS; _i < STOCK_SUB_DURATIONS_1.length; _i++) {
input = STOCK_SUB_DURATIONS_1[_i];
var tryLabelInterval = core.createDuration(input);
var slotsPerLabel = core.wholeDivideDurations(tryLabelInterval, tDateProfile.slotDuration);
if (slotsPerLabel !== null && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
labelInterval = tryLabelInterval;
break;
}
}
// use the slot duration as a last resort
if (!labelInterval) {
labelInterval = tDateProfile.slotDuration;
}
// compute based off the view's duration
// find the largest label interval that yields the minimum number of labels
}
else {
for (var _a = 0, STOCK_SUB_DURATIONS_2 = STOCK_SUB_DURATIONS; _a < STOCK_SUB_DURATIONS_2.length; _a++) {
input = STOCK_SUB_DURATIONS_2[_a];
labelInterval = core.createDuration(input);
var labelCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, labelInterval);
if (labelCnt >= MIN_AUTO_LABELS) {
break;
}
}
}
tDateProfile.labelInterval = labelInterval;
}
return labelInterval;
}
function ensureSlotDuration(tDateProfile, dateProfile, dateEnv) {
var currentRange = dateProfile.currentRange;
var slotDuration = tDateProfile.slotDuration;
if (!slotDuration) {
var labelInterval = ensureLabelInterval(tDateProfile, dateProfile, dateEnv); // will compute if necessary
// compute based off the label interval
// find the largest slot duration that is different from labelInterval, but still acceptable
for (var _i = 0, STOCK_SUB_DURATIONS_3 = STOCK_SUB_DURATIONS; _i < STOCK_SUB_DURATIONS_3.length; _i++) {
var input = STOCK_SUB_DURATIONS_3[_i];
var trySlotDuration = core.createDuration(input);
var slotsPerLabel = core.wholeDivideDurations(labelInterval, trySlotDuration);
if (slotsPerLabel !== null && slotsPerLabel > 1 && slotsPerLabel <= MAX_AUTO_SLOTS_PER_LABEL) {
slotDuration = trySlotDuration;
break;
}
}
// only allow the value if it won't exceed the view's # of slots limit
if (slotDuration) {
var slotCnt = dateEnv.countDurationsBetween(currentRange.start, currentRange.end, slotDuration);
if (slotCnt > MAX_AUTO_CELLS) {
slotDuration = null;
}
}
// use the label interval as a last resort
if (!slotDuration) {
slotDuration = labelInterval;
}
tDateProfile.slotDuration = slotDuration;
}
return slotDuration;
}
function computeHeaderFormats(tDateProfile, dateProfile, dateEnv, view) {
var format1;
var format2;
var labelInterval = tDateProfile.labelInterval;
var unit = core.greatestDurationDenominator(labelInterval).unit;
var weekNumbersVisible = view.opt('weekNumbers');
var format0 = (format1 = (format2 = null));
// NOTE: weekNumber computation function wont work
if ((unit === 'week') && !weekNumbersVisible) {
unit = 'day';
}
switch (unit) {
case 'year':
format0 = { year: 'numeric' }; // '2015'
break;
case 'month':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric' }; // '2015'
}
format1 = { month: 'short' }; // 'Jan'
break;
case 'week':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric' }; // '2015'
}
format1 = { week: 'narrow' }; // 'Wk4'
break;
case 'day':
if (currentRangeAs('years', dateProfile, dateEnv) > 1) {
format0 = { year: 'numeric', month: 'long' }; // 'January 2014'
}
else if (currentRangeAs('months', dateProfile, dateEnv) > 1) {
format0 = { month: 'long' }; // 'January'
}
if (weekNumbersVisible) {
format1 = { week: 'short' }; // 'Wk 4'
}
format2 = { weekday: 'narrow', day: 'numeric' }; // 'Su 9'
break;
case 'hour':
if (weekNumbersVisible) {
format0 = { week: 'short' }; // 'Wk 4'
}
if (currentRangeAs('days', dateProfile, dateEnv) > 1) {
format1 = { weekday: 'short', day: 'numeric', month: 'numeric', omitCommas: true }; // Sat 4/7
}
format2 = {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'short'
};
break;
case 'minute':
// sufficiently large number of different minute cells?
if ((core.asRoughMinutes(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
format0 = {
hour: 'numeric',
meridiem: 'short'
};
format1 = function (params) {
return ':' + core.padStart(params.date.minute, 2); // ':30'
};
}
else {
format0 = {
hour: 'numeric',
minute: 'numeric',
meridiem: 'short'
};
}
break;
case 'second':
// sufficiently large number of different second cells?
if ((core.asRoughSeconds(labelInterval) / 60) >= MAX_AUTO_SLOTS_PER_LABEL) {
format0 = { hour: 'numeric', minute: '2-digit', meridiem: 'lowercase' }; // '8:30 PM'
format1 = function (params) {
return ':' + core.padStart(params.date.second, 2); // ':30'
};
}
else {
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
}
break;
case 'millisecond':
format0 = { hour: 'numeric', minute: '2-digit', second: '2-digit', meridiem: 'lowercase' }; // '8:30:45 PM'
format1 = function (params) {
return '.' + core.padStart(params.millisecond, 3);
};
break;
}
return [].concat(format0 || [], format1 || [], format2 || []);
}
// Compute the number of the give units in the "current" range.
// Won't go more precise than days.
// Will return `0` if there's not a clean whole interval.
function currentRangeAs(unit, dateProfile, dateEnv) {
var range = dateProfile.currentRange;
var res = null;
if (unit === 'years') {
res = dateEnv.diffWholeYears(range.start, range.end);
}
else if (unit === 'months') {
res = dateEnv.diffWholeMonths(range.start, range.end);
}
else if (unit === 'weeks') {
res = dateEnv.diffWholeMonths(range.start, range.end);
}
else if (unit === 'days') {
res = core.diffWholeDays(range.start, range.end);
}
return res || 0;
}
function buildIsWeekStarts(tDateProfile, dateEnv) {
var slotDates = tDateProfile.slotDates, emphasizeWeeks = tDateProfile.emphasizeWeeks;
var prevWeekNumber = null;
var isWeekStarts = [];
for (var _i = 0, slotDates_1 = slotDates; _i < slotDates_1.length; _i++) {
var slotDate = slotDates_1[_i];
var weekNumber = dateEnv.computeWeekNumber(slotDate);
var isWeekStart = emphasizeWeeks && (prevWeekNumber !== null) && (prevWeekNumber !== weekNumber);
prevWeekNumber = weekNumber;
isWeekStarts.push(isWeekStart);
}
return isWeekStarts;
}
function buildCellRows(tDateProfile, dateEnv, view) {
var slotDates = tDateProfile.slotDates;
var formats = tDateProfile.headerFormats;
var cellRows = formats.map(function (format) { return []; }); // indexed by row,col
// specifically for navclicks
var rowUnits = formats.map(function (format) {
return format.getLargestUnit ? format.getLargestUnit() : null;
});
// builds cellRows and slotCells
for (var i = 0; i < slotDates.length; i++) {
var date = slotDates[i];
var isWeekStart = tDateProfile.isWeekStarts[i];
for (var row = 0; row < formats.length; row++) {
var format = formats[row];
var rowCells = cellRows[row];
var leadingCell = rowCells[rowCells.length - 1];
var isSuperRow = (formats.length > 1) && (row < (formats.length - 1)); // more than one row and not the last
var newCell = null;
if (isSuperRow) {
var text = dateEnv.format(date, format);
if (!leadingCell || (leadingCell.text !== text)) {
newCell = buildCellObject(date, text, rowUnits[row], view);
}
else {
leadingCell.colspan += 1;
}
}
else {
if (!leadingCell ||
core.isInt(dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.labelInterval))) {
var text = dateEnv.format(date, format);
newCell = buildCellObject(date, text, rowUnits[row], view);
}
else {
leadingCell.colspan += 1;
}
}
if (newCell) {
newCell.weekStart = isWeekStart;
rowCells.push(newCell);
}
}
}
return cellRows;
}
function buildCellObject(date, text, rowUnit, view) {
var spanHtml = core.buildGotoAnchorHtml(view, {
date: date,
type: rowUnit,
forceOff: !rowUnit
}, {
'class': 'fc-cell-text'
}, core.htmlEscape(text));
return { text: text, spanHtml: spanHtml, date: date, colspan: 1, isWeekStart: false };
}
var TimelineNowIndicator = /** @class */ (function () {
function TimelineNowIndicator(headParent, bodyParent) {
this.headParent = headParent;
this.bodyParent = bodyParent;
}
TimelineNowIndicator.prototype.render = function (coord, isRtl) {
var styleProps = isRtl ? { right: -coord } : { left: coord };
this.headParent.appendChild(this.arrowEl = core.createElement('div', {
className: 'fc-now-indicator fc-now-indicator-arrow',
style: styleProps
}));
this.bodyParent.appendChild(this.lineEl = core.createElement('div', {
className: 'fc-now-indicator fc-now-indicator-line',
style: styleProps
}));
};
TimelineNowIndicator.prototype.unrender = function () {
if (this.arrowEl) {
core.removeElement(this.arrowEl);
}
if (this.lineEl) {
core.removeElement(this.lineEl);
}
};
return TimelineNowIndicator;
}());
var STICKY_PROP_VAL = computeStickyPropVal(); // if null, means not supported at all
var IS_MS_EDGE = /Edge/.test(navigator.userAgent);
var IS_SAFARI = STICKY_PROP_VAL === '-webkit-sticky'; // good b/c doesn't confuse chrome
var STICKY_CLASSNAME = 'fc-sticky';
/*
useful beyond the native position:sticky for these reasons:
- support in IE11
- nice centering support
*/
var StickyScroller = /** @class */ (function () {
function StickyScroller(scroller, isRtl, isVertical) {
var _this = this;
this.usingRelative = null;
/*
known bug: called twice on init. problem when mixing with ScrollJoiner
*/
this.updateSize = function () {
var els = Array.prototype.slice.call(_this.scroller.canvas.el.querySelectorAll('.' + STICKY_CLASSNAME));
var elGeoms = _this.queryElGeoms(els);
var viewportWidth = _this.scroller.el.clientWidth;
if (_this.usingRelative) {
var elDestinations = _this.computeElDestinations(elGeoms, viewportWidth); // read before prepPositioning
assignRelativePositions(els, elGeoms, elDestinations);
}
else {
assignStickyPositions(els, elGeoms, viewportWidth);
}
};
this.scroller = scroller;
this.usingRelative =
!STICKY_PROP_VAL || // IE11
(IS_MS_EDGE && isRtl) || // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/18883305/
((IS_MS_EDGE || IS_SAFARI) && isVertical); // because doesn't work with rowspan in tables, our only vertial use
if (this.usingRelative) {
scroller.on('scrollEnd', this.updateSize);
}
}
StickyScroller.prototype.destroy = function () {
this.scroller.off('scrollEnd', this.updateSize);
};
StickyScroller.prototype.queryElGeoms = function (els) {
var canvasOrigin = this.scroller.canvas.el.getBoundingClientRect();
var elGeoms = [];
for (var _i = 0, els_1 = els; _i < els_1.length; _i++) {
var el = els_1[_i];
var parentBound = core.translateRect(el.parentNode.getBoundingClientRect(), -canvasOrigin.left, -canvasOrigin.top);
var elRect = el.getBoundingClientRect();
var computedStyles = window.getComputedStyle(el);
var computedTextAlign = window.getComputedStyle(el.parentNode).textAlign; // ask the parent
var intendedTextAlign = computedTextAlign;
var naturalBound = null;
if (computedStyles.position !== 'sticky') {
naturalBound = core.translateRect(elRect, -canvasOrigin.left - (parseFloat(computedStyles.left) || 0), // could be 'auto'
-canvasOrigin.top - (parseFloat(computedStyles.top) || 0));
}
if (el.hasAttribute('data-sticky-center')) {
intendedTextAlign = 'center';
}
elGeoms.push({
parentBound: parentBound,
naturalBound: naturalBound,
elWidth: elRect.width,
elHeight: elRect.height,
computedTextAlign: computedTextAlign,
intendedTextAlign: intendedTextAlign
});
}
return elGeoms;
};
StickyScroller.prototype.computeElDestinations = function (elGeoms, viewportWidth) {
var viewportLeft = this.scroller.getScrollFromLeft();
var viewportTop = this.scroller.getScrollTop();
var viewportRight = viewportLeft + viewportWidth;
return elGeoms.map(function (elGeom) {
var elWidth = elGeom.elWidth, elHeight = elGeom.elHeight, parentBound = elGeom.parentBound, naturalBound = elGeom.naturalBound;
var destLeft; // relative to canvas topleft
var destTop; // "
switch (elGeom.intendedTextAlign) {
case 'left':
destLeft = viewportLeft;
break;
case 'right':
destLeft = viewportRight - elWidth;
break;
case 'center':
destLeft = (viewportLeft + viewportRight) / 2 - elWidth / 2;
break;
}
destLeft = Math.min(destLeft, parentBound.right - elWidth);
destLeft = Math.max(destLeft, parentBound.left);
destTop = viewportTop;
destTop = Math.min(destTop, parentBound.bottom - elHeight);
destTop = Math.max(destTop, naturalBound.top); // better to use natural top for upper bound
return { left: destLeft, top: destTop };
});
};
return StickyScroller;
}());
function assignRelativePositions(els, elGeoms, elDestinations) {
els.forEach(function (el, i) {
var naturalBound = elGeoms[i].naturalBound;
core.applyStyle(el, {
position: 'relative',
left: elDestinations[i].left - naturalBound.left,
top: elDestinations[i].top - naturalBound.top
});
});
}
function assignStickyPositions(els, elGeoms, viewportWidth) {
els.forEach(function (el, i) {
var stickyLeft = 0;
if (elGeoms[i].intendedTextAlign === 'center') {
stickyLeft = (viewportWidth - elGeoms[i].elWidth) / 2;
// needs to be forced to left?
if (elGeoms[i].computedTextAlign === 'center') {
el.setAttribute('data-sticky-center', '') // remember for next queryElGeoms
;
el.parentNode.style.textAlign = 'left';
}
}
core.applyStyle(el, {
position: STICKY_PROP_VAL,
left: stickyLeft,
right: 0,
top: 0
});
});
}
function computeStickyPropVal() {
var el = core.htmlToElement('
var TimeAxis = /** @class */ (function (_super) {
__extends(TimeAxis, _super);
function TimeAxis(context, headerContainerEl, bodyContainerEl) {
var _this = _super.call(this, context) || this;
var layout = _this.layout = new HeaderBodyLayout(headerContainerEl, bodyContainerEl, 'auto');
var headerEnhancedScroller = layout.headerScroller.enhancedScroll;
var bodyEnhancedScroller = layout.bodyScroller.enhancedScroll;
// needs to go after layout, which has ScrollJoiner
_this.headStickyScroller = new StickyScroller(headerEnhancedScroller, _this.isRtl, false); // isVertical=false
_this.bodyStickyScroller = new StickyScroller(bodyEnhancedScroller, _this.isRtl, false); // isVertical=false
_this.header = new TimelineHeader(context, headerEnhancedScroller.canvas.contentEl);
_this.slats = new TimelineSlats(context, bodyEnhancedScroller.canvas.bgEl);
_this.nowIndicator = new TimelineNowIndicator(headerEnhancedScroller.canvas.el, bodyEnhancedScroller.canvas.el);
return _this;
}
TimeAxis.prototype.destroy = function () {
this.layout.destroy();
this.header.destroy();
this.slats.destroy();
this.nowIndicator.unrender();
this.headStickyScroller.destroy();
this.bodyStickyScroller.destroy();
_super.prototype.destroy.call(this);
};
TimeAxis.prototype.render = function (props) {
var tDateProfile = this.tDateProfile =
buildTimelineDateProfile(props.dateProfile, this.view); // TODO: cache
this.header.receiveProps({
dateProfile: props.dateProfile,
tDateProfile: tDateProfile
});
this.slats.receiveProps({
dateProfile: props.dateProfile,
tDateProfile: tDateProfile
});
};
// Now Indicator
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.getNowIndicatorUnit = function (dateProfile) {
// yuck
var tDateProfile = this.tDateProfile =
buildTimelineDateProfile(dateProfile, this.view); // TODO: cache
if (tDateProfile.isTimeScale) {
return core.greatestDurationDenominator(tDateProfile.slotDuration).unit;
}
};
// will only execute if isTimeScale
TimeAxis.prototype.renderNowIndicator = function (date) {
if (core.rangeContainsMarker(this.tDateProfile.normalizedRange, date)) {
this.nowIndicator.render(this.dateToCoord(date), this.isRtl);
}
};
// will only execute if isTimeScale
TimeAxis.prototype.unrenderNowIndicator = function () {
this.nowIndicator.unrender();
};
// Sizing
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.applySlotWidth(this.computeSlotWidth());
// adjusts gutters. do after slot widths set
this.layout.setHeight(totalHeight, isAuto);
// pretty much just queries coords. do last
this.slats.updateSize();
};
TimeAxis.prototype.updateStickyScrollers = function () {
this.headStickyScroller.updateSize();
this.bodyStickyScroller.updateSize();
};
TimeAxis.prototype.computeSlotWidth = function () {
var slotWidth = this.opt('slotWidth') || '';
if (slotWidth === '') {
slotWidth = this.computeDefaultSlotWidth(this.tDateProfile);
}
return slotWidth;
};
TimeAxis.prototype.computeDefaultSlotWidth = function (tDateProfile) {
var maxInnerWidth = 0; // TODO: harness core's `matchCellWidths` for this
this.header.innerEls.forEach(function (innerEl, i) {
maxInnerWidth = Math.max(maxInnerWidth, innerEl.getBoundingClientRect().width);
});
var headingCellWidth = Math.ceil(maxInnerWidth) + 1; // assume no padding, and one pixel border
// in TimelineView.defaults we ensured that labelInterval is an interval of slotDuration
// TODO: rename labelDuration?
var slotsPerLabel = core.wholeDivideDurations(tDateProfile.labelInterval, tDateProfile.slotDuration);
var slotWidth = Math.ceil(headingCellWidth / slotsPerLabel);
var minWidth = window.getComputedStyle(this.header.slatColEls[0]).minWidth;
if (minWidth) {
minWidth = parseInt(minWidth, 10);
if (minWidth) {
slotWidth = Math.max(slotWidth, minWidth);
}
}
return slotWidth;
};
TimeAxis.prototype.applySlotWidth = function (slotWidth) {
var _a = this, layout = _a.layout, tDateProfile = _a.tDateProfile;
var containerWidth = '';
var containerMinWidth = '';
var nonLastSlotWidth = '';
if (slotWidth !== '') {
slotWidth = Math.round(slotWidth);
containerWidth = slotWidth * tDateProfile.slotDates.length;
containerMinWidth = '';
nonLastSlotWidth = slotWidth;
var availableWidth = layout.bodyScroller.enhancedScroll.getClientWidth();
if (availableWidth > containerWidth) {
containerMinWidth = availableWidth;
containerWidth = '';
nonLastSlotWidth = Math.floor(availableWidth / tDateProfile.slotDates.length);
}
}
layout.headerScroller.enhancedScroll.canvas.setWidth(containerWidth);
layout.headerScroller.enhancedScroll.canvas.setMinWidth(containerMinWidth);
layout.bodyScroller.enhancedScroll.canvas.setWidth(containerWidth);
layout.bodyScroller.enhancedScroll.canvas.setMinWidth(containerMinWidth);
if (nonLastSlotWidth !== '') {
this.header.slatColEls.slice(0, -1).concat(this.slats.slatColEls.slice(0, -1)).forEach(function (el) {
el.style.width = nonLastSlotWidth + 'px';
});
}
};
// returned value is between 0 and the number of snaps
TimeAxis.prototype.computeDateSnapCoverage = function (date) {
var _a = this, dateEnv = _a.dateEnv, tDateProfile = _a.tDateProfile;
var snapDiff = dateEnv.countDurationsBetween(tDateProfile.normalizedRange.start, date, tDateProfile.snapDuration);
if (snapDiff < 0) {
return 0;
}
else if (snapDiff >= tDateProfile.snapDiffToIndex.length) {
return tDateProfile.snapCnt;
}
else {
var snapDiffInt = Math.floor(snapDiff);
var snapCoverage = tDateProfile.snapDiffToIndex[snapDiffInt];
if (core.isInt(snapCoverage)) { // not an in-between value
snapCoverage += snapDiff - snapDiffInt; // add the remainder
}
else {
// a fractional value, meaning the date is not visible
// always round up in this case. works for start AND end dates in a range.
snapCoverage = Math.ceil(snapCoverage);
}
return snapCoverage;
}
};
// for LTR, results range from 0 to width of area
// for RTL, results range from negative width of area to 0
TimeAxis.prototype.dateToCoord = function (date) {
var tDateProfile = this.tDateProfile;
var snapCoverage = this.computeDateSnapCoverage(date);
var slotCoverage = snapCoverage / tDateProfile.snapsPerSlot;
var slotIndex = Math.floor(slotCoverage);
slotIndex = Math.min(slotIndex, tDateProfile.slotCnt - 1);
var partial = slotCoverage - slotIndex;
var _a = this.slats, innerCoordCache = _a.innerCoordCache, outerCoordCache = _a.outerCoordCache;
if (this.isRtl) {
return (outerCoordCache.rights[slotIndex] -
(innerCoordCache.getWidth(slotIndex) * partial)) - outerCoordCache.originClientRect.width;
}
else {
return (outerCoordCache.lefts[slotIndex] +
(innerCoordCache.getWidth(slotIndex) * partial));
}
};
TimeAxis.prototype.rangeToCoords = function (range) {
if (this.isRtl) {
return { right: this.dateToCoord(range.start), left: this.dateToCoord(range.end) };
}
else {
return { left: this.dateToCoord(range.start), right: this.dateToCoord(range.end) };
}
};
// Scrolling
// ------------------------------------------------------------------------------------------
TimeAxis.prototype.computeDateScroll = function (duration) {
var dateEnv = this.dateEnv;
var dateProfile = this.props.dateProfile;
var left = 0;
if (dateProfile) {
left = this.dateToCoord(dateEnv.add(core.startOfDay(dateProfile.activeRange.start), // startOfDay needed?
duration));
// hack to overcome the left borders of non-first slat
if (!this.isRtl && left) {
left += 1;
}
}
return { left: left };
};
TimeAxis.prototype.queryDateScroll = function () {
var enhancedScroll = this.layout.bodyScroller.enhancedScroll;
return {
left: enhancedScroll.getScrollLeft()
};
};
TimeAxis.prototype.applyDateScroll = function (scroll) {
// TODO: lame we have to update both. use the scrolljoiner instead maybe
this.layout.bodyScroller.enhancedScroll.setScrollLeft(scroll.left || 0);
this.layout.headerScroller.enhancedScroll.setScrollLeft(scroll.left || 0);
};
return TimeAxis;
}(core.Component));
var TimelineLaneEventRenderer = /** @class */ (function (_super) {
__extends(TimelineLaneEventRenderer, _super);
function TimelineLaneEventRenderer(context, masterContainerEl, timeAxis) {
var _this = _super.call(this, context) || this;
_this.masterContainerEl = masterContainerEl;
_this.timeAxis = timeAxis;
return _this;
}
TimelineLaneEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) {
var view = this.context.view;
var eventRange = seg.eventRange;
var eventDef = eventRange.def;
var eventUi = eventRange.ui;
var isDraggable = view.computeEventDraggable(eventDef, eventUi);
var isResizableFromStart = seg.isStart && view.computeEventStartResizable(eventDef, eventUi);
var isResizableFromEnd = seg.isEnd && view.computeEventEndResizable(eventDef, eventUi);
var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo);
classes.unshift('fc-timeline-event', 'fc-h-event');
var timeText = this.getTimeText(eventRange);
return '' +
(timeText ?
'' +
'' +
core.htmlEscape(timeText) +
'' +
''
:
'') +
'' +
'' +
(eventDef.title ? core.htmlEscape(eventDef.title) : ' ') +
'' +
'' +
(isResizableFromStart ?
'' :
'') +
(isResizableFromEnd ?
'' :
'') +
'';
};
TimelineLaneEventRenderer.prototype.computeDisplayEventTime = function () {
return !this.timeAxis.tDateProfile.isTimeScale; // because times should be obvious via axis
};
TimelineLaneEventRenderer.prototype.computeDisplayEventEnd = function () {
return false;
};
// Computes a default event time formatting string if `timeFormat` is not explicitly defined
TimelineLaneEventRenderer.prototype.computeEventTimeFormat = function () {
return {
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: true,
meridiem: 'narrow'
};
};
TimelineLaneEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) {
if (!this.el && this.masterContainerEl) {
this.el = core.createElement('div', { className: 'fc-event-container' });
if (mirrorInfo) {
this.el.classList.add('fc-mirror-container');
}
this.masterContainerEl.appendChild(this.el);
}
if (this.el) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
this.el.appendChild(seg.el);
}
}
};
TimelineLaneEventRenderer.prototype.detachSegs = function (segs) {
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
core.removeElement(seg.el);
}
};
// computes AND assigns (assigns the left/right at least). bad
TimelineLaneEventRenderer.prototype.computeSegSizes = function (segs) {
var timeAxis = this.timeAxis;
for (var _i = 0, segs_3 = segs; _i < segs_3.length; _i++) {
var seg = segs_3[_i];
var coords = timeAxis.rangeToCoords(seg); // works because Seg has start/end
core.applyStyle(seg.el, {
left: (seg.left = coords.left),
right: -(seg.right = coords.right)
});
}
};
TimelineLaneEventRenderer.prototype.assignSegSizes = function (segs) {
if (!this.el) {
return;
}
// compute seg verticals
for (var _i = 0, segs_4 = segs; _i < segs_4.length; _i++) {
var seg = segs_4[_i];
seg.height = core.computeHeightAndMargins(seg.el);
}
this.buildSegLevels(segs); // populates above/below props for computeOffsetForSegs
var totalHeight = computeOffsetForSegs(segs); // also assigns seg.top
core.applyStyleProp(this.el, 'height', totalHeight);
// assign seg verticals
for (var _a = 0, segs_5 = segs; _a < segs_5.length; _a++) {
var seg = segs_5[_a];
core.applyStyleProp(seg.el, 'top', seg.top);
}
};
TimelineLaneEventRenderer.prototype.buildSegLevels = function (segs) {
var segLevels = [];
segs = this.sortEventSegs(segs);
for (var _i = 0, segs_6 = segs; _i < segs_6.length; _i++) {
var unplacedSeg = segs_6[_i];
unplacedSeg.above = [];
// determine the first level with no collisions
var level = 0; // level index
while (level < segLevels.length) {
var isLevelCollision = false;
// determine collisions
for (var _a = 0, _b = segLevels[level]; _a < _b.length; _a++) {
var placedSeg = _b[_a];
if (timeRowSegsCollide(unplacedSeg, placedSeg)) {
unplacedSeg.above.push(placedSeg);
isLevelCollision = true;
}
}
if (isLevelCollision) {
level += 1;
}
else {
break;
}
}
// insert into the first non-colliding level. create if necessary
(segLevels[level] || (segLevels[level] = []))
.push(unplacedSeg);
// record possible colliding segments below (TODO: automated test for this)
level += 1;
while (level < segLevels.length) {
for (var _c = 0, _d = segLevels[level]; _c < _d.length; _c++) {
var belowSeg = _d[_c];
if (timeRowSegsCollide(unplacedSeg, belowSeg)) {
belowSeg.above.push(unplacedSeg);
}
}
level += 1;
}
}
return segLevels;
};
return TimelineLaneEventRenderer;
}(core.FgEventRenderer));
function computeOffsetForSegs(segs) {
var max = 0;
for (var _i = 0, segs_7 = segs; _i < segs_7.length; _i++) {
var seg = segs_7[_i];
max = Math.max(max, computeOffsetForSeg(seg));
}
return max;
}
function computeOffsetForSeg(seg) {
if ((seg.top == null)) {
seg.top = computeOffsetForSegs(seg.above);
}
return seg.top + seg.height;
}
function timeRowSegsCollide(seg0, seg1) {
return (seg0.left < seg1.right) && (seg0.right > seg1.left);
}
var TimelineLaneFillRenderer = /** @class */ (function (_super) {
__extends(TimelineLaneFillRenderer, _super);
function TimelineLaneFillRenderer(context, masterContainerEl, timeAxis) {
var _this = _super.call(this, context) || this;
_this.masterContainerEl = masterContainerEl;
_this.timeAxis = timeAxis;
return _this;
}
TimelineLaneFillRenderer.prototype.attachSegs = function (type, segs) {
if (segs.length) {
var className = void 0;
if (type === 'businessHours') {
className = 'bgevent';
}
else {
className = type.toLowerCase();
}
// making a new container each time is OKAY
// all types of segs (background or business hours or whatever) are rendered in one pass
var containerEl = core.createElement('div', { className: 'fc-' + className + '-container' });
this.masterContainerEl.appendChild(containerEl);
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
containerEl.appendChild(seg.el);
}
return [containerEl]; // return value
}
};
TimelineLaneFillRenderer.prototype.computeSegSizes = function (segs) {
var timeAxis = this.timeAxis;
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var seg = segs_2[_i];
var coords = timeAxis.rangeToCoords(seg);
seg.left = coords.left;
seg.right = coords.right;
}
};
TimelineLaneFillRenderer.prototype.assignSegSizes = function (segs) {
for (var _i = 0, segs_3 = segs; _i < segs_3.length; _i++) {
var seg = segs_3[_i];
core.applyStyle(seg.el, {
left: seg.left,
right: -seg.right
});
}
};
return TimelineLaneFillRenderer;
}(core.FillRenderer));
var TimelineLane = /** @class */ (function (_super) {
__extends(TimelineLane, _super);
function TimelineLane(context, fgContainerEl, bgContainerEl, timeAxis) {
var _this = _super.call(this, context, bgContainerEl) // should el be bgContainerEl???
|| this;
_this.slicer = new TimelineLaneSlicer();
_this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag);
_this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize);
var fillRenderer = _this.fillRenderer = new TimelineLaneFillRenderer(context, bgContainerEl, timeAxis);
var eventRenderer = _this.eventRenderer = new TimelineLaneEventRenderer(context, fgContainerEl, timeAxis);
_this.mirrorRenderer = new TimelineLaneEventRenderer(context, fgContainerEl, timeAxis);
_this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'));
_this.renderDateSelection = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'));
_this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'));
_this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer));
_this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]);
_this.timeAxis = timeAxis;
return _this;
}
TimelineLane.prototype.render = function (props) {
var slicedProps = this.slicer.sliceProps(props, props.dateProfile, this.timeAxis.tDateProfile.isTimeScale ? null : props.nextDayThreshold, this, this.timeAxis);
this.renderBusinessHours(slicedProps.businessHourSegs);
this.renderDateSelection(slicedProps.dateSelectionSegs);
this.renderBgEvents(slicedProps.bgEventSegs);
this.renderFgEvents(slicedProps.fgEventSegs);
this.renderEventSelection(slicedProps.eventSelection);
this.renderEventDrag(slicedProps.eventDrag);
this.renderEventResize(slicedProps.eventResize);
};
TimelineLane.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.renderBusinessHours.unrender();
this.renderDateSelection.unrender();
this.renderBgEvents.unrender();
this.renderFgEvents.unrender();
this.renderEventSelection.unrender();
this.renderEventDrag.unrender();
this.renderEventResize.unrender();
};
TimelineLane.prototype._renderEventDrag = function (state) {
if (state) {
this.eventRenderer.hideByHash(state.affectedInstances);
this.mirrorRenderer.renderSegs(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._unrenderEventDrag = function (state) {
if (state) {
this.eventRenderer.showByHash(state.affectedInstances);
this.mirrorRenderer.unrender(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._renderEventResize = function (state) {
if (state) {
// HACK. eventRenderer and fillRenderer both use these segs. would compete over seg.el
var segsForHighlight = state.segs.map(function (seg) {
return __assign({}, seg);
});
this.eventRenderer.hideByHash(state.affectedInstances);
this.fillRenderer.renderSegs('highlight', segsForHighlight);
this.mirrorRenderer.renderSegs(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype._unrenderEventResize = function (state) {
if (state) {
this.eventRenderer.showByHash(state.affectedInstances);
this.fillRenderer.unrender('highlight');
this.mirrorRenderer.unrender(state.segs, { isDragging: true, sourceSeg: state.sourceSeg });
}
};
TimelineLane.prototype.updateSize = function (isResize) {
var _a = this, fillRenderer = _a.fillRenderer, eventRenderer = _a.eventRenderer, mirrorRenderer = _a.mirrorRenderer;
fillRenderer.computeSizes(isResize);
eventRenderer.computeSizes(isResize);
mirrorRenderer.computeSizes(isResize);
fillRenderer.assignSizes(isResize);
eventRenderer.assignSizes(isResize);
mirrorRenderer.assignSizes(isResize);
};
return TimelineLane;
}(core.DateComponent));
var TimelineLaneSlicer = /** @class */ (function (_super) {
__extends(TimelineLaneSlicer, _super);
function TimelineLaneSlicer() {
return _super !== null && _super.apply(this, arguments) || this;
}
TimelineLaneSlicer.prototype.sliceRange = function (origRange, timeAxis) {
var tDateProfile = timeAxis.tDateProfile;
var dateProfile = timeAxis.props.dateProfile;
var normalRange = normalizeRange(origRange, tDateProfile, timeAxis.dateEnv);
var segs = [];
// protect against when the span is entirely in an invalid date region
if (timeAxis.computeDateSnapCoverage(normalRange.start) < timeAxis.computeDateSnapCoverage(normalRange.end)) {
// intersect the footprint's range with the grid's range
var slicedRange = core.intersectRanges(normalRange, tDateProfile.normalizedRange);
if (slicedRange) {
segs.push({
start: slicedRange.start,
end: slicedRange.end,
isStart: slicedRange.start.valueOf() === normalRange.start.valueOf() && isValidDate(slicedRange.start, tDateProfile, dateProfile, timeAxis.view),
isEnd: slicedRange.end.valueOf() === normalRange.end.valueOf() && isValidDate(core.addMs(slicedRange.end, -1), tDateProfile, dateProfile, timeAxis.view)
});
}
}
return segs;
};
return TimelineLaneSlicer;
}(core.Slicer));
var TimelineView = /** @class */ (function (_super) {
__extends(TimelineView, _super);
function TimelineView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.el.classList.add('fc-timeline');
if (_this.opt('eventOverlap') === false) {
_this.el.classList.add('fc-no-overlap');
}
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.timeAxis = new TimeAxis(_this.context, _this.el.querySelector('thead .fc-time-area'), _this.el.querySelector('tbody .fc-time-area'));
_this.lane = new TimelineLane(_this.context, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.contentEl, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.bgEl, _this.timeAxis);
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeAxis.slats.el
});
return _this;
}
TimelineView.prototype.destroy = function () {
this.timeAxis.destroy();
this.lane.destroy();
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
TimelineView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return " ";
};
TimelineView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // flags for updateSize, addScroll
this.timeAxis.receiveProps({
dateProfile: props.dateProfile
});
this.lane.receiveProps(__assign({}, props, { nextDayThreshold: this.nextDayThreshold }));
};
TimelineView.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.timeAxis.updateSize(isResize, totalHeight, isAuto);
this.lane.updateSize(isResize);
};
// Now Indicator
// ------------------------------------------------------------------------------------------
TimelineView.prototype.getNowIndicatorUnit = function (dateProfile) {
return this.timeAxis.getNowIndicatorUnit(dateProfile);
};
TimelineView.prototype.renderNowIndicator = function (date) {
this.timeAxis.renderNowIndicator(date);
};
TimelineView.prototype.unrenderNowIndicator = function () {
this.timeAxis.unrenderNowIndicator();
};
// Scroll System
// ------------------------------------------------------------------------------------------
TimelineView.prototype.computeDateScroll = function (duration) {
return this.timeAxis.computeDateScroll(duration);
};
TimelineView.prototype.applyScroll = function (scroll, isResize) {
_super.prototype.applyScroll.call(this, scroll, isResize); // will call applyDateScroll
// avoid updating stickyscroll too often
// TODO: repeat code as ResourceTimelineView::updateSize
var calendar = this.calendar;
if (isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated) {
this.timeAxis.updateStickyScrollers();
}
};
TimelineView.prototype.applyDateScroll = function (scroll) {
this.timeAxis.applyDateScroll(scroll);
};
TimelineView.prototype.queryScroll = function () {
var enhancedScroll = this.timeAxis.layout.bodyScroller.enhancedScroll;
return {
top: enhancedScroll.getScrollTop(),
left: enhancedScroll.getScrollLeft()
};
};
// Hit System
// ------------------------------------------------------------------------------------------
TimelineView.prototype.buildPositionCaches = function () {
this.timeAxis.slats.updateSize();
};
TimelineView.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) {
var slatHit = this.timeAxis.slats.positionToHit(positionLeft);
if (slatHit) {
return {
component: this,
dateSpan: slatHit.dateSpan,
rect: {
left: slatHit.left,
right: slatHit.right,
top: 0,
bottom: elHeight
},
dayEl: slatHit.dayEl,
layer: 0
};
}
};
return TimelineView;
}(core.View));
var main = core.createPlugin({
defaultView: 'timelineDay',
views: {
timeline: {
class: TimelineView,
eventResizableFromStart: true // how is this consumed for TimelineView tho?
},
timelineDay: {
type: 'timeline',
duration: { days: 1 }
},
timelineWeek: {
type: 'timeline',
duration: { weeks: 1 }
},
timelineMonth: {
type: 'timeline',
duration: { months: 1 }
},
timelineYear: {
type: 'timeline',
duration: { years: 1 }
}
}
});
exports.HeaderBodyLayout = HeaderBodyLayout;
exports.ScrollJoiner = ScrollJoiner;
exports.StickyScroller = StickyScroller;
exports.TimeAxis = TimeAxis;
exports.TimelineLane = TimelineLane;
exports.TimelineView = TimelineView;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/timeline' {
import TimelineView from '@fullcalendar/timeline/TimelineView';
export { TimelineView };
export { default as TimelineLane } from '@fullcalendar/timeline/TimelineLane';
export { default as ScrollJoiner } from '@fullcalendar/timeline/util/ScrollJoiner';
export { default as StickyScroller } from '@fullcalendar/timeline/util/StickyScroller';
export { default as TimeAxis } from '@fullcalendar/timeline/TimeAxis';
export { default as HeaderBodyLayout } from '@fullcalendar/timeline/HeaderBodyLayout';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/timeline/TimelineView' {
import { Hit, View, ViewProps, ComponentContext, ViewSpec, DateProfileGenerator, DateProfile, Duration } from '@fullcalendar/core';
import TimeAxis from '@fullcalendar/timeline/TimeAxis';
import TimelineLane from '@fullcalendar/timeline/TimelineLane';
export { TimelineView as default, TimelineView };
class TimelineView extends View {
timeAxis: TimeAxis;
lane: TimelineLane;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
destroy(): void;
renderSkeletonHtml(): string;
render(props: ViewProps): void;
updateSize(isResize: any, totalHeight: any, isAuto: any): void;
getNowIndicatorUnit(dateProfile: DateProfile): string;
renderNowIndicator(date: any): void;
unrenderNowIndicator(): void;
computeDateScroll(duration: Duration): {
left: number;
};
applyScroll(scroll: any, isResize: any): void;
applyDateScroll(scroll: any): void;
queryScroll(): {
top: number;
left: number;
};
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number, elWidth: number, elHeight: number): Hit;
}
}
declare module '@fullcalendar/timeline/TimelineLane' {
import { Duration, EventStore, EventUiHash, DateMarker, DateSpan, EventInteractionState, EventSegUiInteractionState, DateComponent, ComponentContext, Seg, DateProfile } from '@fullcalendar/core';
import TimeAxis from '@fullcalendar/timeline/TimeAxis';
export interface TimelineLaneSeg extends Seg {
start: DateMarker;
end: DateMarker;
}
export interface TimelineLaneProps {
dateProfile: DateProfile;
nextDayThreshold: Duration;
businessHours: EventStore | null;
eventStore: EventStore | null;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export { TimelineLane as default, TimelineLane };
class TimelineLane extends DateComponent {
timeAxis: TimeAxis;
constructor(context: ComponentContext, fgContainerEl: HTMLElement, bgContainerEl: HTMLElement, timeAxis: TimeAxis);
render(props: TimelineLaneProps): void;
destroy(): void;
_renderEventDrag(state: EventSegUiInteractionState): void;
_unrenderEventDrag(state: EventSegUiInteractionState): void;
_renderEventResize(state: EventSegUiInteractionState): void;
_unrenderEventResize(state: EventSegUiInteractionState): void;
updateSize(isResize: boolean): void;
}
}
declare module '@fullcalendar/timeline/util/ScrollJoiner' {
import ClippedScroller from '@fullcalendar/timeline/util/ClippedScroller';
export { ScrollJoiner as default, ScrollJoiner };
class ScrollJoiner {
axis: any;
scrollers: ClippedScroller[];
masterScroller: ClippedScroller;
constructor(axis: any, scrollers: ClippedScroller[]);
initScroller(scroller: ClippedScroller): void;
assignMasterScroller(scroller: any): void;
unassignMasterScroller(): void;
update(): void;
}
}
declare module '@fullcalendar/timeline/util/StickyScroller' {
import { Rect, Point } from '@fullcalendar/core';
import EnhancedScroller from '@fullcalendar/timeline/util/EnhancedScroller';
interface ElementGeom {
parentBound: Rect;
naturalBound: Rect | null;
elWidth: number;
elHeight: number;
computedTextAlign: string;
intendedTextAlign: string;
}
export { StickyScroller as default, StickyScroller };
class StickyScroller {
scroller: EnhancedScroller;
usingRelative: boolean | null;
constructor(scroller: EnhancedScroller, isRtl: boolean, isVertical: boolean);
destroy(): void;
updateSize: () => void;
queryElGeoms(els: HTMLElement[]): ElementGeom[];
computeElDestinations(elGeoms: ElementGeom[], viewportWidth: number): Point[];
}
export {};
}
declare module '@fullcalendar/timeline/TimeAxis' {
import { DateProfile, DateMarker, Component, ComponentContext, Duration } from '@fullcalendar/core';
import HeaderBodyLayout from '@fullcalendar/timeline/HeaderBodyLayout';
import TimelineHeader from '@fullcalendar/timeline/TimelineHeader';
import TimelineSlats from '@fullcalendar/timeline/TimelineSlats';
import { TimelineDateProfile } from '@fullcalendar/timeline/timeline-date-profile';
import TimelineNowIndicator from '@fullcalendar/timeline/TimelineNowIndicator';
import StickyScroller from '@fullcalendar/timeline/util/StickyScroller';
export interface TimeAxisProps {
dateProfile: DateProfile;
}
export { TimeAxis as default, TimeAxis };
class TimeAxis extends Component {
layout: HeaderBodyLayout;
header: TimelineHeader;
slats: TimelineSlats;
nowIndicator: TimelineNowIndicator;
headStickyScroller: StickyScroller;
bodyStickyScroller: StickyScroller;
tDateProfile: TimelineDateProfile;
constructor(context: ComponentContext, headerContainerEl: any, bodyContainerEl: any);
destroy(): void;
render(props: TimeAxisProps): void;
getNowIndicatorUnit(dateProfile: DateProfile): string;
renderNowIndicator(date: any): void;
unrenderNowIndicator(): void;
updateSize(isResize: any, totalHeight: any, isAuto: any): void;
updateStickyScrollers(): void;
computeSlotWidth(): any;
computeDefaultSlotWidth(tDateProfile: any): number;
applySlotWidth(slotWidth: number | string): void;
computeDateSnapCoverage(date: DateMarker): number;
dateToCoord(date: any): any;
rangeToCoords(range: any): {
right: any;
left: any;
};
computeDateScroll(duration: Duration): {
left: number;
};
queryDateScroll(): {
left: number;
};
applyDateScroll(scroll: any): void;
}
}
declare module '@fullcalendar/timeline/HeaderBodyLayout' {
import ClippedScroller from '@fullcalendar/timeline/util/ClippedScroller';
import ScrollJoiner from '@fullcalendar/timeline/util/ScrollJoiner';
export { HeaderBodyLayout as default, HeaderBodyLayout };
class HeaderBodyLayout {
headerScroller: ClippedScroller;
bodyScroller: ClippedScroller;
scrollJoiner: ScrollJoiner;
constructor(headerContainerEl: any, bodyContainerEl: any, verticalScroll: any);
destroy(): void;
setHeight(totalHeight: any, isAuto: any): void;
queryHeadHeight(): number;
}
}
declare module '@fullcalendar/timeline/util/ClippedScroller' {
import { ScrollbarWidths } from '@fullcalendar/core';
import EnhancedScroller from '@fullcalendar/timeline/util/EnhancedScroller';
export { ClippedScroller as default, ClippedScroller };
class ClippedScroller {
el: HTMLElement;
enhancedScroll: EnhancedScroller;
isHScrollbarsClipped: boolean;
isVScrollbarsClipped: boolean;
constructor(overflowX: string, overflowY: string, parentEl: HTMLElement);
destroy(): void;
updateSize(): void;
setHeight(height: number | string): void;
getScrollbarWidths(): ScrollbarWidths;
}
}
declare module '@fullcalendar/timeline/util/EnhancedScroller' {
import { ScrollComponent, EmitterInterface } from '@fullcalendar/core';
import ScrollerCanvas from '@fullcalendar/timeline/util/ScrollerCanvas';
export { EnhancedScroller as default, EnhancedScroller };
class EnhancedScroller extends ScrollComponent {
on: EmitterInterface['on'];
one: EmitterInterface['one'];
off: EmitterInterface['off'];
trigger: EmitterInterface['trigger'];
triggerWith: EmitterInterface['triggerWith'];
hasHandlers: EmitterInterface['hasHandlers'];
canvas: ScrollerCanvas;
isScrolling: boolean;
isTouching: boolean;
isMoving: boolean;
isTouchScrollEnabled: boolean;
preventTouchScrollHandler: any;
requestMovingEnd: any;
constructor(overflowX: string, overflowY: string);
destroy(): void;
disableTouchScroll(): void;
enableTouchScroll(): void;
bindPreventTouchScroll(): void;
unbindPreventTouchScroll(): void;
bindHandlers(): void;
unbindHandlers(): void;
reportScroll: () => void;
reportScrollStart: () => void;
reportMovingEnd(): void;
reportScrollEnd(): void;
reportTouchStart: () => void;
reportTouchEnd: () => void;
getScrollLeft(): number;
setScrollLeft(val: any): void;
getScrollFromLeft(): number;
}
}
declare module '@fullcalendar/timeline/TimelineHeader' {
import { Component, ComponentContext, DateProfile } from '@fullcalendar/core';
import { TimelineDateProfile } from '@fullcalendar/timeline/timeline-date-profile';
export interface TimelineHeaderProps {
dateProfile: DateProfile;
tDateProfile: TimelineDateProfile;
}
export { TimelineHeader as default, TimelineHeader };
class TimelineHeader extends Component {
tableEl: HTMLElement;
slatColEls: HTMLElement[];
innerEls: HTMLElement[];
constructor(context: ComponentContext, parentEl: HTMLElement);
destroy(): void;
render(props: TimelineHeaderProps): void;
renderDates(tDateProfile: TimelineDateProfile): void;
}
}
declare module '@fullcalendar/timeline/TimelineSlats' {
import { PositionCache, Component, ComponentContext, DateProfile } from '@fullcalendar/core';
import { TimelineDateProfile } from '@fullcalendar/timeline/timeline-date-profile';
export interface TimelineSlatsProps {
dateProfile: DateProfile;
tDateProfile: TimelineDateProfile;
}
export { TimelineSlats as default, TimelineSlats };
class TimelineSlats extends Component {
el: HTMLElement;
slatColEls: HTMLElement[];
slatEls: HTMLElement[];
outerCoordCache: PositionCache;
innerCoordCache: PositionCache;
constructor(context: ComponentContext, parentEl: HTMLElement);
destroy(): void;
render(props: TimelineSlatsProps): void;
renderDates(tDateProfile: TimelineDateProfile): void;
slatCellHtml(date: any, isEm: any, tDateProfile: TimelineDateProfile): string;
updateSize(): void;
positionToHit(leftPosition: any): {
dateSpan: {
range: {
start: Date;
end: Date;
};
allDay: boolean;
};
dayEl: HTMLElement;
left: any;
right: any;
};
}
}
declare module '@fullcalendar/timeline/timeline-date-profile' {
import { Duration, View, DateProfile, DateMarker, DateEnv, DateRange } from '@fullcalendar/core';
export interface TimelineDateProfile {
labelInterval: Duration;
slotDuration: Duration;
headerFormats: any;
isTimeScale: boolean;
largeUnit: string;
emphasizeWeeks: boolean;
snapDuration: Duration;
snapsPerSlot: number;
normalizedRange: DateRange;
timeWindowMs: number;
slotDates: DateMarker[];
isWeekStarts: boolean[];
snapDiffToIndex: number[];
snapIndexToDiff: number[];
snapCnt: number;
slotCnt: number;
cellRows: TimelineHeaderCell[][];
}
export interface TimelineHeaderCell {
text: string;
spanHtml: string;
date: DateMarker;
colspan: number;
isWeekStart: boolean;
}
export function buildTimelineDateProfile(dateProfile: DateProfile, view: View): TimelineDateProfile;
export function normalizeDate(date: DateMarker, tDateProfile: TimelineDateProfile, dateEnv: DateEnv): DateMarker;
export function normalizeRange(range: DateRange, tDateProfile: TimelineDateProfile, dateEnv: DateEnv): DateRange;
export function isValidDate(date: DateMarker, tDateProfile: TimelineDateProfile, dateProfile: DateProfile, view: View): boolean;
}
declare module '@fullcalendar/timeline/TimelineNowIndicator' {
export { TimelineNowIndicator as default, TimelineNowIndicator };
class TimelineNowIndicator {
headParent: HTMLElement;
bodyParent: HTMLElement;
arrowEl: HTMLElement;
lineEl: HTMLElement;
constructor(headParent: HTMLElement, bodyParent: HTMLElement);
render(coord: number, isRtl: boolean): void;
unrender(): void;
}
}
declare module '@fullcalendar/timeline/util/ScrollerCanvas' {
export { ScrollerCanvas as default, ScrollerCanvas };
class ScrollerCanvas {
el: HTMLElement;
contentEl: HTMLElement;
bgEl: HTMLElement;
gutters: any;
width: any;
minWidth: any;
constructor();
setGutters(gutters: any): void;
setWidth(width: any): void;
setMinWidth(minWidth: any): void;
clearWidth(): void;
updateSize(): void;
}
}
/*!
FullCalendar Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):t((e=e||self).FullCalendarTimeline={},e.FullCalendar)}(this,function(e,t){"use strict";var r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)};function n(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var i,o=function(){return(o=Object.assign||function(e){for(var t,r=1,n=arguments.length;r
/*!
FullCalendar Resources Common Plugin v4.3.1
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
import { memoize, filterHash, rangesIntersect, memoizeOutput, isPropsEqual, mapHash, combineEventUis, refineProps, rangesEqual, processScopedUiProps, parseBusinessHours, EventApi, Calendar, Splitter, mergeEventStores, isPropsValid, appendToElement, htmlEscape, cssToStr, config, isValidDate, addDays, unpromisify, requestJson, htmlToElement, removeElement, createFormatter, computeFallbackHeaderFormat, renderDateCell, findElements, Component, flexibleCompare, compareByFieldSpecs, createPlugin } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function massageEventDragMutation(eventMutation, hit0, hit1) {
var resource0 = hit0.dateSpan.resourceId;
var resource1 = hit1.dateSpan.resourceId;
if (resource0 && resource1 &&
resource0 !== resource1) {
eventMutation.resourceMutation = {
matchResourceId: resource0,
setResourceId: resource1
};
}
}
/*
TODO: all this would be much easier if we were using a hash!
*/
function applyEventDefMutation(eventDef, mutation, calendar) {
var resourceMutation = mutation.resourceMutation;
if (resourceMutation && computeResourceEditable(eventDef, calendar)) {
var index = eventDef.resourceIds.indexOf(resourceMutation.matchResourceId);
if (index !== -1) {
var resourceIds = eventDef.resourceIds.slice(); // copy
resourceIds.splice(index, 1); // remove
if (resourceIds.indexOf(resourceMutation.setResourceId) === -1) { // not already in there
resourceIds.push(resourceMutation.setResourceId); // add
}
eventDef.resourceIds = resourceIds;
}
}
}
/*
HACK
TODO: use EventUi system instead of this
*/
function computeResourceEditable(eventDef, calendar) {
var resourceEditable = eventDef.resourceEditable;
if (resourceEditable == null) {
var source = eventDef.sourceId && calendar.state.eventSources[eventDef.sourceId];
if (source) {
resourceEditable = source.extendedProps.resourceEditable; // used the Source::extendedProps hack
}
if (resourceEditable == null) {
resourceEditable = calendar.opt('eventResourceEditable');
if (resourceEditable == null) {
resourceEditable = calendar.opt('editable'); // TODO: use defaults system instead
}
}
}
return resourceEditable;
}
function transformEventDrop(mutation, calendar) {
var resourceMutation = mutation.resourceMutation;
if (resourceMutation) {
return {
oldResource: calendar.getResourceById(resourceMutation.matchResourceId),
newResource: calendar.getResourceById(resourceMutation.setResourceId)
};
}
else {
return {
oldResource: null,
newResource: null
};
}
}
var ResourceDataAdder = /** @class */ (function () {
function ResourceDataAdder() {
this.filterResources = memoize(filterResources);
}
ResourceDataAdder.prototype.transform = function (viewProps, viewSpec, calendarProps, view) {
if (viewSpec.class.needsResourceData) {
return {
resourceStore: this.filterResources(calendarProps.resourceStore, view.opt('filterResourcesWithEvents'), calendarProps.eventStore, calendarProps.dateProfile.activeRange),
resourceEntityExpansions: calendarProps.resourceEntityExpansions
};
}
};
return ResourceDataAdder;
}());
function filterResources(resourceStore, doFilterResourcesWithEvents, eventStore, activeRange) {
if (doFilterResourcesWithEvents) {
var instancesInRange = filterEventInstancesInRange(eventStore.instances, activeRange);
var hasEvents_1 = computeHasEvents(instancesInRange, eventStore.defs);
__assign(hasEvents_1, computeAncestorHasEvents(hasEvents_1, resourceStore));
return filterHash(resourceStore, function (resource, resourceId) {
return hasEvents_1[resourceId];
});
}
else {
return resourceStore;
}
}
function filterEventInstancesInRange(eventInstances, activeRange) {
return filterHash(eventInstances, function (eventInstance) {
return rangesIntersect(eventInstance.range, activeRange);
});
}
function computeHasEvents(eventInstances, eventDefs) {
var hasEvents = {};
for (var instanceId in eventInstances) {
var instance = eventInstances[instanceId];
for (var _i = 0, _a = eventDefs[instance.defId].resourceIds; _i < _a.length; _i++) {
var resourceId = _a[_i];
hasEvents[resourceId] = true;
}
}
return hasEvents;
}
/*
mark resources as having events if any of their ancestors have them
NOTE: resourceStore might not have all the resources that hasEvents{} has keyed
*/
function computeAncestorHasEvents(hasEvents, resourceStore) {
var res = {};
for (var resourceId in hasEvents) {
var resource = void 0;
while ((resource = resourceStore[resourceId])) {
resourceId = resource.parentId; // now functioning as the parentId
if (resourceId) {
res[resourceId] = true;
}
else {
break;
}
}
}
return res;
}
// for when non-resource view should be given EventUi info (for event coloring/constraints based off of resource data)
var ResourceEventConfigAdder = /** @class */ (function () {
function ResourceEventConfigAdder() {
this.buildResourceEventUis = memoizeOutput(buildResourceEventUis, isPropsEqual);
this.injectResourceEventUis = memoize(injectResourceEventUis);
}
ResourceEventConfigAdder.prototype.transform = function (viewProps, viewSpec, calendarProps) {
if (!viewSpec.class.needsResourceData) { // is a non-resource view?
return {
eventUiBases: this.injectResourceEventUis(viewProps.eventUiBases, viewProps.eventStore.defs, this.buildResourceEventUis(calendarProps.resourceStore))
};
}
};
return ResourceEventConfigAdder;
}());
function buildResourceEventUis(resourceStore) {
return mapHash(resourceStore, function (resource) {
return resource.ui;
});
}
function injectResourceEventUis(eventUiBases, eventDefs, resourceEventUis) {
return mapHash(eventUiBases, function (eventUi, defId) {
if (defId) { // not the '' key
return injectResourceEventUi(eventUi, eventDefs[defId], resourceEventUis);
}
else {
return eventUi;
}
});
}
function injectResourceEventUi(origEventUi, eventDef, resourceEventUis) {
var parts = [];
// first resource takes precedence, which fights with the ordering of combineEventUis, thus the unshifts
for (var _i = 0, _a = eventDef.resourceIds; _i < _a.length; _i++) {
var resourceId = _a[_i];
if (resourceEventUis[resourceId]) {
parts.unshift(resourceEventUis[resourceId]);
}
}
parts.unshift(origEventUi);
return combineEventUis(parts);
}
// for making sure events that have editable resources are always draggable in resource views
function transformIsDraggable(val, eventDef, eventUi, view) {
if (!val) {
if (view.viewSpec.class.needsResourceData) {
if (computeResourceEditable(eventDef, view.calendar)) {
return true;
}
}
}
return val;
}
var RESOURCE_SOURCE_PROPS = {
id: String
};
var defs = [];
var uid = 0;
function registerResourceSourceDef(def) {
defs.push(def);
}
function getResourceSourceDef(id) {
return defs[id];
}
function doesSourceIgnoreRange(source) {
return Boolean(defs[source.sourceDefId].ignoreRange);
}
function parseResourceSource(input) {
for (var i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
var def = defs[i];
var meta = def.parseMeta(input);
if (meta) {
var res = parseResourceSourceProps((typeof input === 'object' && input) ? input : {}, meta, i);
res._raw = input;
return res;
}
}
return null;
}
function parseResourceSourceProps(input, meta, sourceDefId) {
var props = refineProps(input, RESOURCE_SOURCE_PROPS);
props.sourceId = String(uid++);
props.sourceDefId = sourceDefId;
props.meta = meta;
props.publicId = props.id;
props.isFetching = false;
props.latestFetchId = '';
props.fetchRange = null;
delete props.id;
return props;
}
function reduceResourceSource (source, action, dateProfile, calendar) {
switch (action.type) {
case 'INIT':
return createSource(calendar.opt('resources'), calendar);
case 'RESET_RESOURCE_SOURCE':
return createSource(action.resourceSourceInput, calendar, true);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
return handleRange(source, dateProfile.activeRange, calendar);
case 'RECEIVE_RESOURCES':
case 'RECEIVE_RESOURCE_ERROR':
return receiveResponse(source, action.fetchId, action.fetchRange);
case 'REFETCH_RESOURCES':
return fetchSource(source, dateProfile.activeRange, calendar);
default:
return source;
}
}
var uid$1 = 0;
function createSource(input, calendar, forceFetch) {
if (input) {
var source = parseResourceSource(input);
if (forceFetch || !calendar.opt('refetchResourcesOnNavigate')) { // because assumes handleRange will do it later
source = fetchSource(source, null, calendar);
}
return source;
}
return null;
}
function handleRange(source, activeRange, calendar) {
if (calendar.opt('refetchResourcesOnNavigate') &&
!doesSourceIgnoreRange(source) &&
(!source.fetchRange || !rangesEqual(source.fetchRange, activeRange))) {
return fetchSource(source, activeRange, calendar);
}
else {
return source;
}
}
function fetchSource(source, fetchRange, calendar) {
var sourceDef = getResourceSourceDef(source.sourceDefId);
var fetchId = String(uid$1++);
sourceDef.fetch({
resourceSource: source,
calendar: calendar,
range: fetchRange
}, function (res) {
// HACK
// do before calling dispatch in case dispatch renders synchronously
calendar.afterSizingTriggers._resourcesRendered = [null]; // fire once
calendar.dispatch({
type: 'RECEIVE_RESOURCES',
fetchId: fetchId,
fetchRange: fetchRange,
rawResources: res.rawResources
});
}, function (error) {
calendar.dispatch({
type: 'RECEIVE_RESOURCE_ERROR',
fetchId: fetchId,
fetchRange: fetchRange,
error: error
});
});
return __assign({}, source, { isFetching: true, latestFetchId: fetchId });
}
function receiveResponse(source, fetchId, fetchRange) {
if (fetchId === source.latestFetchId) {
return __assign({}, source, { isFetching: false, fetchRange: fetchRange });
}
return source;
}
var RESOURCE_PROPS = {
id: String,
title: String,
parentId: String,
businessHours: null,
children: null,
extendedProps: null
};
var PRIVATE_ID_PREFIX = '_fc:';
var uid$2 = 0;
/*
needs a full store so that it can populate children too
*/
function parseResource(input, parentId, store, calendar) {
if (parentId === void 0) { parentId = ''; }
var leftovers0 = {};
var props = refineProps(input, RESOURCE_PROPS, {}, leftovers0);
var leftovers1 = {};
var ui = processScopedUiProps('event', leftovers0, calendar, leftovers1);
if (!props.id) {
props.id = PRIVATE_ID_PREFIX + (uid$2++);
}
if (!props.parentId) { // give precedence to the parentId property
props.parentId = parentId;
}
props.businessHours = props.businessHours ? parseBusinessHours(props.businessHours, calendar) : null;
props.ui = ui;
props.extendedProps = __assign({}, leftovers1, props.extendedProps);
// help out ResourceApi from having user modify props
Object.freeze(ui.classNames);
Object.freeze(props.extendedProps);
if (store[props.id]) ;
else {
store[props.id] = props;
if (props.children) {
for (var _i = 0, _a = props.children; _i < _a.length; _i++) {
var childInput = _a[_i];
parseResource(childInput, props.id, store, calendar);
}
delete props.children;
}
}
return props;
}
/*
TODO: use this in more places
*/
function getPublicId(id) {
if (id.indexOf(PRIVATE_ID_PREFIX) === 0) {
return '';
}
return id;
}
function reduceResourceStore (store, action, source, calendar) {
switch (action.type) {
case 'INIT':
return {};
case 'RECEIVE_RESOURCES':
return receiveRawResources(store, action.rawResources, action.fetchId, source, calendar);
case 'ADD_RESOURCE':
return addResource(store, action.resourceHash);
case 'REMOVE_RESOURCE':
return removeResource(store, action.resourceId);
case 'SET_RESOURCE_PROP':
return setResourceProp(store, action.resourceId, action.propName, action.propValue);
case 'RESET_RESOURCES':
// must make the calendar think each resource is a new object :/
return mapHash(store, function (resource) {
return __assign({}, resource);
});
default:
return store;
}
}
function receiveRawResources(existingStore, inputs, fetchId, source, calendar) {
if (source.latestFetchId === fetchId) {
var nextStore = {};
for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) {
var input = inputs_1[_i];
parseResource(input, '', nextStore, calendar);
}
return nextStore;
}
else {
return existingStore;
}
}
function addResource(existingStore, additions) {
// TODO: warn about duplicate IDs
return __assign({}, existingStore, additions);
}
function removeResource(existingStore, resourceId) {
var newStore = __assign({}, existingStore);
delete newStore[resourceId];
// promote children
for (var childResourceId in newStore) { // a child, *maybe* but probably not
if (newStore[childResourceId].parentId === resourceId) {
newStore[childResourceId] = __assign({}, newStore[childResourceId], { parentId: '' });
}
}
return newStore;
}
function setResourceProp(existingStore, resourceId, name, value) {
var _a, _b;
var existingResource = existingStore[resourceId];
// TODO: sanitization
if (existingResource) {
return __assign({}, existingStore, (_a = {}, _a[resourceId] = __assign({}, existingResource, (_b = {}, _b[name] = value, _b)), _a));
}
else {
return existingStore;
}
}
function reduceResourceEntityExpansions(expansions, action) {
var _a;
switch (action.type) {
case 'INIT':
return {};
case 'SET_RESOURCE_ENTITY_EXPANDED':
return __assign({}, expansions, (_a = {}, _a[action.id] = action.isExpanded, _a));
default:
return expansions;
}
}
function resourcesReducers (state, action, calendar) {
var resourceSource = reduceResourceSource(state.resourceSource, action, state.dateProfile, calendar);
var resourceStore = reduceResourceStore(state.resourceStore, action, resourceSource, calendar);
var resourceEntityExpansions = reduceResourceEntityExpansions(state.resourceEntityExpansions, action);
return __assign({}, state, { resourceSource: resourceSource,
resourceStore: resourceStore,
resourceEntityExpansions: resourceEntityExpansions });
}
var RESOURCE_RELATED_PROPS = {
resourceId: String,
resourceIds: function (items) {
return (items || []).map(function (item) {
return String(item);
});
},
resourceEditable: Boolean
};
function parseEventDef(def, props, leftovers) {
var resourceRelatedProps = refineProps(props, RESOURCE_RELATED_PROPS, {}, leftovers);
var resourceIds = resourceRelatedProps.resourceIds;
if (resourceRelatedProps.resourceId) {
resourceIds.push(resourceRelatedProps.resourceId);
}
def.resourceIds = resourceIds;
def.resourceEditable = resourceRelatedProps.resourceEditable;
}
function transformDateSelectionJoin(hit0, hit1) {
var resourceId0 = hit0.dateSpan.resourceId;
var resourceId1 = hit1.dateSpan.resourceId;
if (resourceId0 && resourceId1) {
if (hit0.component.allowAcrossResources === false &&
resourceId0 !== resourceId1) {
return false;
}
else {
return { resourceId: resourceId0 };
}
}
}
var ResourceApi = /** @class */ (function () {
function ResourceApi(calendar, rawResource) {
this._calendar = calendar;
this._resource = rawResource;
}
ResourceApi.prototype.setProp = function (name, value) {
this._calendar.dispatch({
type: 'SET_RESOURCE_PROP',
resourceId: this._resource.id,
propName: name,
propValue: value
});
};
ResourceApi.prototype.remove = function () {
this._calendar.dispatch({
type: 'REMOVE_RESOURCE',
resourceId: this._resource.id
});
};
ResourceApi.prototype.getParent = function () {
var calendar = this._calendar;
var parentId = this._resource.parentId;
if (parentId) {
return new ResourceApi(calendar, calendar.state.resourceSource[parentId]);
}
else {
return null;
}
};
ResourceApi.prototype.getChildren = function () {
var thisResourceId = this._resource.id;
var calendar = this._calendar;
var resourceStore = calendar.state.resourceStore;
var childApis = [];
for (var resourceId in resourceStore) {
if (resourceStore[resourceId].parentId === thisResourceId) {
childApis.push(new ResourceApi(calendar, resourceStore[resourceId]));
}
}
return childApis;
};
/*
this is really inefficient!
TODO: make EventApi::resourceIds a hash or keep an index in the Calendar's state
*/
ResourceApi.prototype.getEvents = function () {
var thisResourceId = this._resource.id;
var calendar = this._calendar;
var _a = calendar.state.eventStore, defs = _a.defs, instances = _a.instances;
var eventApis = [];
for (var instanceId in instances) {
var instance = instances[instanceId];
var def = defs[instance.defId];
if (def.resourceIds.indexOf(thisResourceId) !== -1) { // inefficient!!!
eventApis.push(new EventApi(calendar, def, instance));
}
}
return eventApis;
};
Object.defineProperty(ResourceApi.prototype, "id", {
get: function () { return this._resource.id; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "title", {
get: function () { return this._resource.title; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventConstraint", {
get: function () { return this._resource.ui.constraints[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventOverlap", {
get: function () { return this._resource.ui.overlap; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventAllow", {
get: function () { return this._resource.ui.allows[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventBackgroundColor", {
get: function () { return this._resource.ui.backgroundColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventBorderColor", {
get: function () { return this._resource.ui.borderColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventTextColor", {
get: function () { return this._resource.ui.textColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventClassNames", {
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
get: function () { return this._resource.ui.classNames; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "extendedProps", {
get: function () { return this._resource.extendedProps; },
enumerable: true,
configurable: true
});
return ResourceApi;
}());
Calendar.prototype.addResource = function (input, scrollTo) {
var _a;
if (scrollTo === void 0) { scrollTo = true; }
var resourceHash;
var resource;
if (input instanceof ResourceApi) {
resource = input._resource;
resourceHash = (_a = {}, _a[resource.id] = resource, _a);
}
else {
resourceHash = {};
resource = parseResource(input, '', resourceHash, this);
}
// HACK
if (scrollTo) {
this.component.view.addScroll({ forcedRowId: resource.id });
}
this.dispatch({
type: 'ADD_RESOURCE',
resourceHash: resourceHash
});
return new ResourceApi(this, resource);
};
Calendar.prototype.getResourceById = function (id) {
id = String(id);
if (this.state.resourceStore) { // guard against calendar with no resource functionality
var rawResource = this.state.resourceStore[id];
if (rawResource) {
return new ResourceApi(this, rawResource);
}
}
return null;
};
Calendar.prototype.getResources = function () {
var resourceStore = this.state.resourceStore;
var resourceApis = [];
if (resourceStore) { // guard against calendar with no resource functionality
for (var resourceId in resourceStore) {
resourceApis.push(new ResourceApi(this, resourceStore[resourceId]));
}
}
return resourceApis;
};
Calendar.prototype.getTopLevelResources = function () {
var resourceStore = this.state.resourceStore;
var resourceApis = [];
if (resourceStore) { // guard against calendar with no resource functionality
for (var resourceId in resourceStore) {
if (!resourceStore[resourceId].parentId) {
resourceApis.push(new ResourceApi(this, resourceStore[resourceId]));
}
}
}
return resourceApis;
};
Calendar.prototype.rerenderResources = function () {
this.dispatch({
type: 'RESET_RESOURCES'
});
};
Calendar.prototype.refetchResources = function () {
this.dispatch({
type: 'REFETCH_RESOURCES'
});
};
function transformDatePoint(dateSpan, calendar) {
return dateSpan.resourceId ?
{ resource: calendar.getResourceById(dateSpan.resourceId) } :
{};
}
function transformDateSpan(dateSpan, calendar) {
return dateSpan.resourceId ?
{ resource: calendar.getResourceById(dateSpan.resourceId) } :
{};
}
/*
splits things BASED OFF OF which resources they are associated with.
creates a '' entry which is when something has NO resource.
*/
var ResourceSplitter = /** @class */ (function (_super) {
__extends(ResourceSplitter, _super);
function ResourceSplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceSplitter.prototype.getKeyInfo = function (props) {
return __assign({ '': {} }, props.resourceStore // already has `ui` and `businessHours` keys!
);
};
ResourceSplitter.prototype.getKeysForDateSpan = function (dateSpan) {
return [dateSpan.resourceId || ''];
};
ResourceSplitter.prototype.getKeysForEventDef = function (eventDef) {
var resourceIds = eventDef.resourceIds;
if (!resourceIds.length) {
return [''];
}
return resourceIds;
};
return ResourceSplitter;
}(Splitter));
function isPropsValidWithResources(props, calendar) {
var splitter = new ResourceSplitter();
var sets = splitter.splitProps(__assign({}, props, { resourceStore: calendar.state.resourceStore }));
for (var resourceId in sets) {
var props_1 = sets[resourceId];
// merge in event data from the non-resource segment
if (resourceId && sets['']) { // current segment is not the non-resource one, and there IS a non-resource one
props_1 = __assign({}, props_1, { eventStore: mergeEventStores(sets[''].eventStore, props_1.eventStore), eventUiBases: __assign({}, sets[''].eventUiBases, props_1.eventUiBases) });
}
if (!isPropsValid(props_1, calendar, { resourceId: resourceId }, filterConfig.bind(null, resourceId))) {
return false;
}
}
return true;
}
function filterConfig(resourceId, config) {
return __assign({}, config, { constraints: filterConstraints(resourceId, config.constraints) });
}
function filterConstraints(resourceId, constraints) {
return constraints.map(function (constraint) {
var defs = constraint.defs;
if (defs) { // we are dealing with an EventStore
// if any of the events define constraints to resources that are NOT this resource,
// then this resource is unconditionally prohibited, which is what a `false` value does.
for (var defId in defs) {
var resourceIds = defs[defId].resourceIds;
if (resourceIds.length && resourceIds.indexOf(resourceId) === -1) { // TODO: use a hash?!!! (for other reasons too)
return false;
}
}
}
return constraint;
});
}
function transformExternalDef(dateSpan) {
return dateSpan.resourceId ?
{ resourceId: dateSpan.resourceId } :
{};
}
function transformEventResizeJoin(hit0, hit1) {
var component = hit0.component;
if (component.allowAcrossResources === false &&
hit0.dateSpan.resourceId !== hit1.dateSpan.resourceId) {
return false;
}
}
EventApi.prototype.getResources = function () {
var calendar = this._calendar;
return this._def.resourceIds.map(function (resourceId) {
return calendar.getResourceById(resourceId);
});
};
EventApi.prototype.setResources = function (resources) {
var resourceIds = [];
// massage resources -> resourceIds
for (var _i = 0, resources_1 = resources; _i < resources_1.length; _i++) {
var resource = resources_1[_i];
var resourceId = null;
if (typeof resource === 'string') {
resourceId = resource;
}
else if (typeof resource === 'number') {
resourceId = String(resource);
}
else if (resource instanceof ResourceApi) {
resourceId = resource.id; // guaranteed to always have an ID. hmmm
}
else {
console.warn('unknown resource type: ' + resource);
}
if (resourceId) {
resourceIds.push(resourceId);
}
}
this.mutate({
standardProps: {
resourceIds: resourceIds
}
});
};
var RELEASE_DATE = '2019-08-10'; // for Scheduler
var UPGRADE_WINDOW = 365 + 7; // days. 1 week leeway, for tz shift reasons too
var LICENSE_INFO_URL = 'http://fullcalendar.io/scheduler/license/';
var PRESET_LICENSE_KEYS = [
'GPL-My-Project-Is-Open-Source',
'CC-Attribution-NonCommercial-NoDerivatives'
];
var CSS = {
position: 'absolute',
'z-index': 99999,
bottom: '1px',
left: '1px',
background: '#eee',
'border-color': '#ddd',
'border-style': 'solid',
'border-width': '1px 1px 0 0',
padding: '2px 4px',
'font-size': '12px',
'border-top-right-radius': '3px'
};
function injectLicenseWarning(containerEl, calendar) {
var key = calendar.opt('schedulerLicenseKey');
if (!isImmuneUrl(window.location.href) && !isValidKey(key)) {
appendToElement(containerEl, '
var optionChangeHandlers = {
resources: handleResources
};
function handleResources(newSourceInput, calendar, deepEqual) {
var oldSourceInput = calendar.state.resourceSource._raw;
if (!deepEqual(oldSourceInput, newSourceInput)) {
calendar.dispatch({
type: 'RESET_RESOURCE_SOURCE',
resourceSourceInput: newSourceInput
});
}
}
registerResourceSourceDef({
ignoreRange: true,
parseMeta: function (raw) {
if (Array.isArray(raw)) {
return raw;
}
else if (Array.isArray(raw.resources)) {
return raw.resources;
}
return null;
},
fetch: function (arg, successCallback) {
successCallback({
rawResources: arg.resourceSource.meta
});
}
});
registerResourceSourceDef({
parseMeta: function (raw) {
if (typeof raw === 'function') {
return raw;
}
else if (typeof raw.resources === 'function') {
return raw.resources;
}
return null;
},
fetch: function (arg, success, failure) {
var dateEnv = arg.calendar.dateEnv;
var func = arg.resourceSource.meta;
var publicArg = {};
if (arg.range) {
publicArg = {
start: dateEnv.toDate(arg.range.start),
end: dateEnv.toDate(arg.range.end),
startStr: dateEnv.formatIso(arg.range.start),
endStr: dateEnv.formatIso(arg.range.end),
timeZone: dateEnv.timeZone
};
}
// TODO: make more dry with EventSourceFunc
// TODO: accept a response?
unpromisify(func.bind(null, publicArg), function (rawResources) {
success({ rawResources: rawResources }); // needs an object response
}, failure // send errorObj directly to failure callback
);
}
});
registerResourceSourceDef({
parseMeta: function (raw) {
if (typeof raw === 'string') {
raw = { url: raw };
}
else if (!raw || typeof raw !== 'object' || !raw.url) {
return null;
}
return {
url: raw.url,
method: (raw.method || 'GET').toUpperCase(),
extraParams: raw.extraParams
};
},
fetch: function (arg, successCallback, failureCallback) {
var meta = arg.resourceSource.meta;
var requestParams = buildRequestParams(meta, arg.range, arg.calendar);
requestJson(meta.method, meta.url, requestParams, function (rawResources, xhr) {
successCallback({ rawResources: rawResources, xhr: xhr });
}, function (message, xhr) {
failureCallback({ message: message, xhr: xhr });
});
}
});
// TODO: somehow consolidate with event json feed
function buildRequestParams(meta, range, calendar) {
var dateEnv = calendar.dateEnv;
var startParam;
var endParam;
var timeZoneParam;
var customRequestParams;
var params = {};
if (range) {
// startParam = meta.startParam
// if (startParam == null) {
startParam = calendar.opt('startParam');
// }
// endParam = meta.endParam
// if (endParam == null) {
endParam = calendar.opt('endParam');
// }
// timeZoneParam = meta.timeZoneParam
// if (timeZoneParam == null) {
timeZoneParam = calendar.opt('timeZoneParam');
// }
params[startParam] = dateEnv.formatIso(range.start);
params[endParam] = dateEnv.formatIso(range.end);
if (dateEnv.timeZone !== 'local') {
params[timeZoneParam] = dateEnv.timeZone;
}
}
// retrieve any outbound GET/POST data from the options
if (typeof meta.extraParams === 'function') {
// supplied as a function that returns a key/value object
customRequestParams = meta.extraParams();
}
else {
// probably supplied as a straight key/value object
customRequestParams = meta.extraParams || {};
}
__assign(params, customRequestParams);
return params;
}
function buildResourceTextFunc(resourceTextSetting, calendar) {
if (typeof resourceTextSetting === 'function') {
return function (resource) {
return resourceTextSetting(new ResourceApi(calendar, resource));
};
}
else {
return function (resource) {
return resource.title || getPublicId(resource.id);
};
}
}
var ResourceDayHeader = /** @class */ (function (_super) {
__extends(ResourceDayHeader, _super);
function ResourceDayHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
_this.datesAboveResources = _this.opt('datesAboveResources');
_this.resourceTextFunc = buildResourceTextFunc(_this.opt('resourceText'), _this.calendar);
parentEl.innerHTML = ''; // because might be nbsp
parentEl.appendChild(_this.el = htmlToElement('
var AbstractResourceDayTable = /** @class */ (function () {
function AbstractResourceDayTable(dayTable, resources) {
this.dayTable = dayTable;
this.resources = resources;
this.resourceIndex = new ResourceIndex(resources);
this.rowCnt = dayTable.rowCnt;
this.colCnt = dayTable.colCnt * resources.length;
this.cells = this.buildCells();
}
AbstractResourceDayTable.prototype.buildCells = function () {
var _a = this, rowCnt = _a.rowCnt, dayTable = _a.dayTable, resources = _a.resources;
var rows = [];
for (var row = 0; row < rowCnt; row++) {
var rowCells = [];
for (var dateCol = 0; dateCol < dayTable.colCnt; dateCol++) {
for (var resourceCol = 0; resourceCol < resources.length; resourceCol++) {
var resource = resources[resourceCol];
var htmlAttrs = 'data-resource-id="' + resource.id + '"';
rowCells[this.computeCol(dateCol, resourceCol)] = {
date: dayTable.cells[row][dateCol].date,
resource: resource,
htmlAttrs: htmlAttrs
};
}
}
rows.push(rowCells);
}
return rows;
};
return AbstractResourceDayTable;
}());
/*
resources over dates
*/
var ResourceDayTable = /** @class */ (function (_super) {
__extends(ResourceDayTable, _super);
function ResourceDayTable() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceDayTable.prototype.computeCol = function (dateI, resourceI) {
return resourceI * this.dayTable.colCnt + dateI;
};
/*
all date ranges are intact
*/
ResourceDayTable.prototype.computeColRanges = function (dateStartI, dateEndI, resourceI) {
return [
{
firstCol: this.computeCol(dateStartI, resourceI),
lastCol: this.computeCol(dateEndI, resourceI),
isStart: true,
isEnd: true
}
];
};
return ResourceDayTable;
}(AbstractResourceDayTable));
/*
dates over resources
*/
var DayResourceTable = /** @class */ (function (_super) {
__extends(DayResourceTable, _super);
function DayResourceTable() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayResourceTable.prototype.computeCol = function (dateI, resourceI) {
return dateI * this.resources.length + resourceI;
};
/*
every single day is broken up
*/
DayResourceTable.prototype.computeColRanges = function (dateStartI, dateEndI, resourceI) {
var segs = [];
for (var i = dateStartI; i <= dateEndI; i++) {
var col = this.computeCol(i, resourceI);
segs.push({
firstCol: col,
lastCol: col,
isStart: i === dateStartI,
isEnd: i === dateEndI
});
}
return segs;
};
return DayResourceTable;
}(AbstractResourceDayTable));
var ResourceIndex = /** @class */ (function () {
function ResourceIndex(resources) {
var indicesById = {};
var ids = [];
for (var i = 0; i < resources.length; i++) {
var id = resources[i].id;
ids.push(id);
indicesById[id] = i;
}
this.ids = ids;
this.indicesById = indicesById;
this.length = resources.length;
}
return ResourceIndex;
}());
/*
TODO: just use ResourceHash somehow? could then use the generic ResourceSplitter
*/
var VResourceSplitter = /** @class */ (function (_super) {
__extends(VResourceSplitter, _super);
function VResourceSplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
VResourceSplitter.prototype.getKeyInfo = function (props) {
var resourceDayTable = props.resourceDayTable;
var hash = mapHash(resourceDayTable.resourceIndex.indicesById, function (i) {
return resourceDayTable.resources[i]; // has `ui` AND `businessHours` keys!
}); // :(
hash[''] = {};
return hash;
};
VResourceSplitter.prototype.getKeysForDateSpan = function (dateSpan) {
return [dateSpan.resourceId || ''];
};
VResourceSplitter.prototype.getKeysForEventDef = function (eventDef) {
var resourceIds = eventDef.resourceIds;
if (!resourceIds.length) {
return [''];
}
return resourceIds;
};
return VResourceSplitter;
}(Splitter));
// joiner
var NO_SEGS = []; // for memoizing
var VResourceJoiner = /** @class */ (function () {
function VResourceJoiner() {
this.joinDateSelection = memoize(this.joinSegs);
this.joinBusinessHours = memoize(this.joinSegs);
this.joinFgEvents = memoize(this.joinSegs);
this.joinBgEvents = memoize(this.joinSegs);
this.joinEventDrags = memoize(this.joinInteractions);
this.joinEventResizes = memoize(this.joinInteractions);
}
/*
propSets also has a '' key for things with no resource
*/
VResourceJoiner.prototype.joinProps = function (propSets, resourceDayTable) {
var dateSelectionSets = [];
var businessHoursSets = [];
var fgEventSets = [];
var bgEventSets = [];
var eventDrags = [];
var eventResizes = [];
var eventSelection = '';
var keys = resourceDayTable.resourceIndex.ids.concat(['']); // add in the all-resource key
for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
var key = keys_1[_i];
var props = propSets[key];
dateSelectionSets.push(props.dateSelectionSegs);
businessHoursSets.push(key ? props.businessHourSegs : NO_SEGS); // don't include redundant all-resource businesshours
fgEventSets.push(key ? props.fgEventSegs : NO_SEGS); // don't include fg all-resource segs
bgEventSets.push(props.bgEventSegs);
eventDrags.push(props.eventDrag);
eventResizes.push(props.eventResize);
eventSelection = eventSelection || props.eventSelection;
}
return {
dateSelectionSegs: this.joinDateSelection.apply(this, [resourceDayTable].concat(dateSelectionSets)),
businessHourSegs: this.joinBusinessHours.apply(this, [resourceDayTable].concat(businessHoursSets)),
fgEventSegs: this.joinFgEvents.apply(this, [resourceDayTable].concat(fgEventSets)),
bgEventSegs: this.joinBgEvents.apply(this, [resourceDayTable].concat(bgEventSets)),
eventDrag: this.joinEventDrags.apply(this, [resourceDayTable].concat(eventDrags)),
eventResize: this.joinEventResizes.apply(this, [resourceDayTable].concat(eventResizes)),
eventSelection: eventSelection
};
};
VResourceJoiner.prototype.joinSegs = function (resourceDayTable) {
var segGroups = [];
for (var _i = 1; _i < arguments.length; _i++) {
segGroups[_i - 1] = arguments[_i];
}
var resourceCnt = resourceDayTable.resources.length;
var transformedSegs = [];
for (var i = 0; i < resourceCnt; i++) {
for (var _a = 0, _b = segGroups[i]; _a < _b.length; _a++) {
var seg = _b[_a];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
for (var _c = 0, _d = segGroups[resourceCnt]; _c < _d.length; _c++) { // one beyond. the all-resource
var seg = _d[_c];
transformedSegs.push.apply(// one beyond. the all-resource
transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
}
return transformedSegs;
};
/*
for expanding non-resource segs to all resources.
only for public use.
no memoizing.
*/
VResourceJoiner.prototype.expandSegs = function (resourceDayTable, segs) {
var resourceCnt = resourceDayTable.resources.length;
var transformedSegs = [];
for (var i = 0; i < resourceCnt; i++) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
}
return transformedSegs;
};
VResourceJoiner.prototype.joinInteractions = function (resourceDayTable) {
var interactions = [];
for (var _i = 1; _i < arguments.length; _i++) {
interactions[_i - 1] = arguments[_i];
}
var resourceCnt = resourceDayTable.resources.length;
var affectedInstances = {};
var transformedSegs = [];
var isEvent = false;
var sourceSeg = null;
for (var i = 0; i < resourceCnt; i++) {
var interaction = interactions[i];
if (interaction) {
for (var _a = 0, _b = interaction.segs; _a < _b.length; _a++) {
var seg = _b[_a];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i) // TODO: templateify Interaction::segs
);
}
__assign(affectedInstances, interaction.affectedInstances);
isEvent = isEvent || interaction.isEvent;
sourceSeg = sourceSeg || interaction.sourceSeg;
}
if (interactions[resourceCnt]) { // one beyond. the all-resource
for (var _c = 0, _d = interactions[resourceCnt].segs; _c < _d.length; _c++) {
var seg = _d[_c];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i) // TODO: templateify Interaction::segs
);
}
}
}
return {
affectedInstances: affectedInstances,
segs: transformedSegs,
isEvent: isEvent,
sourceSeg: sourceSeg
};
};
return VResourceJoiner;
}());
/*
doesn't accept grouping
*/
function flattenResources(resourceStore, orderSpecs) {
return buildRowNodes(resourceStore, [], orderSpecs, false, {}, true)
.map(function (node) {
return node.resource;
});
}
function buildRowNodes(resourceStore, groupSpecs, orderSpecs, isVGrouping, expansions, expansionDefault) {
var complexNodes = buildHierarchy(resourceStore, isVGrouping ? -1 : 1, groupSpecs, orderSpecs);
var flatNodes = [];
flattenNodes(complexNodes, flatNodes, isVGrouping, [], 0, expansions, expansionDefault);
return flatNodes;
}
function flattenNodes(complexNodes, res, isVGrouping, rowSpans, depth, expansions, expansionDefault) {
for (var i = 0; i < complexNodes.length; i++) {
var complexNode = complexNodes[i];
var group = complexNode.group;
if (group) {
if (isVGrouping) {
var firstRowIndex = res.length;
var rowSpanIndex = rowSpans.length;
flattenNodes(complexNode.children, res, isVGrouping, rowSpans.concat(0), depth, expansions, expansionDefault);
if (firstRowIndex < res.length) {
var firstRow = res[firstRowIndex];
var firstRowSpans = firstRow.rowSpans = firstRow.rowSpans.slice();
firstRowSpans[rowSpanIndex] = res.length - firstRowIndex;
}
}
else {
var id = group.spec.field + ':' + group.value;
var isExpanded = expansions[id] != null ? expansions[id] : expansionDefault;
res.push({ id: id, group: group, isExpanded: isExpanded });
if (isExpanded) {
flattenNodes(complexNode.children, res, isVGrouping, rowSpans, depth + 1, expansions, expansionDefault);
}
}
}
else if (complexNode.resource) {
var id = complexNode.resource.id;
var isExpanded = expansions[id] != null ? expansions[id] : expansionDefault;
res.push({
id: id,
rowSpans: rowSpans,
depth: depth,
isExpanded: isExpanded,
hasChildren: Boolean(complexNode.children.length),
resource: complexNode.resource,
resourceFields: complexNode.resourceFields
});
if (isExpanded) {
flattenNodes(complexNode.children, res, isVGrouping, rowSpans, depth + 1, expansions, expansionDefault);
}
}
}
}
function buildHierarchy(resourceStore, maxDepth, groupSpecs, orderSpecs) {
var resourceNodes = buildResourceNodes(resourceStore, orderSpecs);
var builtNodes = [];
for (var resourceId in resourceNodes) {
var resourceNode = resourceNodes[resourceId];
if (!resourceNode.resource.parentId) {
insertResourceNode(resourceNode, builtNodes, groupSpecs, 0, maxDepth, orderSpecs);
}
}
return builtNodes;
}
function buildResourceNodes(resourceStore, orderSpecs) {
var nodeHash = {};
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
nodeHash[resourceId] = {
resource: resource,
resourceFields: buildResourceFields(resource),
children: []
};
}
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
if (resource.parentId) {
var parentNode = nodeHash[resource.parentId];
if (parentNode) {
insertResourceNodeInSiblings(nodeHash[resourceId], parentNode.children, orderSpecs);
}
}
}
return nodeHash;
}
function insertResourceNode(resourceNode, nodes, groupSpecs, depth, maxDepth, orderSpecs) {
if (groupSpecs.length && (maxDepth === -1 || depth <= maxDepth)) {
var groupNode = ensureGroupNodes(resourceNode, nodes, groupSpecs[0]);
insertResourceNode(resourceNode, groupNode.children, groupSpecs.slice(1), depth + 1, maxDepth, orderSpecs);
}
else {
insertResourceNodeInSiblings(resourceNode, nodes, orderSpecs);
}
}
function ensureGroupNodes(resourceNode, nodes, groupSpec) {
var groupValue = resourceNode.resourceFields[groupSpec.field];
var groupNode;
var newGroupIndex;
// find an existing group that matches, or determine the position for a new group
if (groupSpec.order) {
for (newGroupIndex = 0; newGroupIndex < nodes.length; newGroupIndex++) {
var node = nodes[newGroupIndex];
if (node.group) {
var cmp = flexibleCompare(groupValue, node.group.value) * groupSpec.order;
if (cmp === 0) {
groupNode = node;
break;
}
else if (cmp < 0) {
break;
}
}
}
}
else { // the groups are unordered
for (newGroupIndex = 0; newGroupIndex < nodes.length; newGroupIndex++) {
var node = nodes[newGroupIndex];
if (node.group && groupValue === node.group.value) {
groupNode = node;
break;
}
}
}
if (!groupNode) {
groupNode = {
group: {
value: groupValue,
spec: groupSpec
},
children: []
};
nodes.splice(newGroupIndex, 0, groupNode);
}
return groupNode;
}
function insertResourceNodeInSiblings(resourceNode, siblings, orderSpecs) {
var i;
for (i = 0; i < siblings.length; i++) {
var cmp = compareByFieldSpecs(siblings[i].resourceFields, resourceNode.resourceFields, orderSpecs);
if (cmp > 0) { // went 1 past. insert at i
break;
}
}
siblings.splice(i, 0, resourceNode);
}
function buildResourceFields(resource) {
var obj = __assign({}, resource.extendedProps, resource.ui, resource);
delete obj.ui;
delete obj.extendedProps;
return obj;
}
function isGroupsEqual(group0, group1) {
return group0.spec === group1.spec && group0.value === group1.value;
}
var main = createPlugin({
reducers: [resourcesReducers],
eventDefParsers: [parseEventDef],
isDraggableTransformers: [transformIsDraggable],
eventDragMutationMassagers: [massageEventDragMutation],
eventDefMutationAppliers: [applyEventDefMutation],
dateSelectionTransformers: [transformDateSelectionJoin],
datePointTransforms: [transformDatePoint],
dateSpanTransforms: [transformDateSpan],
viewPropsTransformers: [ResourceDataAdder, ResourceEventConfigAdder],
isPropsValid: isPropsValidWithResources,
externalDefTransforms: [transformExternalDef],
eventResizeJoinTransforms: [transformEventResizeJoin],
viewContainerModifiers: [injectLicenseWarning],
eventDropTransformers: [transformEventDrop],
optionChangeHandlers: optionChangeHandlers
});
export default main;
export { AbstractResourceDayTable, DayResourceTable, ResourceApi, ResourceDayHeader, ResourceDayTable, ResourceSplitter, VResourceJoiner, VResourceSplitter, buildResourceFields, buildResourceTextFunc, buildRowNodes, flattenResources, isGroupsEqual };
/*!
FullCalendar Resources Common Plugin v4.3.1
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarResourceCommon = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function massageEventDragMutation(eventMutation, hit0, hit1) {
var resource0 = hit0.dateSpan.resourceId;
var resource1 = hit1.dateSpan.resourceId;
if (resource0 && resource1 &&
resource0 !== resource1) {
eventMutation.resourceMutation = {
matchResourceId: resource0,
setResourceId: resource1
};
}
}
/*
TODO: all this would be much easier if we were using a hash!
*/
function applyEventDefMutation(eventDef, mutation, calendar) {
var resourceMutation = mutation.resourceMutation;
if (resourceMutation && computeResourceEditable(eventDef, calendar)) {
var index = eventDef.resourceIds.indexOf(resourceMutation.matchResourceId);
if (index !== -1) {
var resourceIds = eventDef.resourceIds.slice(); // copy
resourceIds.splice(index, 1); // remove
if (resourceIds.indexOf(resourceMutation.setResourceId) === -1) { // not already in there
resourceIds.push(resourceMutation.setResourceId); // add
}
eventDef.resourceIds = resourceIds;
}
}
}
/*
HACK
TODO: use EventUi system instead of this
*/
function computeResourceEditable(eventDef, calendar) {
var resourceEditable = eventDef.resourceEditable;
if (resourceEditable == null) {
var source = eventDef.sourceId && calendar.state.eventSources[eventDef.sourceId];
if (source) {
resourceEditable = source.extendedProps.resourceEditable; // used the Source::extendedProps hack
}
if (resourceEditable == null) {
resourceEditable = calendar.opt('eventResourceEditable');
if (resourceEditable == null) {
resourceEditable = calendar.opt('editable'); // TODO: use defaults system instead
}
}
}
return resourceEditable;
}
function transformEventDrop(mutation, calendar) {
var resourceMutation = mutation.resourceMutation;
if (resourceMutation) {
return {
oldResource: calendar.getResourceById(resourceMutation.matchResourceId),
newResource: calendar.getResourceById(resourceMutation.setResourceId)
};
}
else {
return {
oldResource: null,
newResource: null
};
}
}
var ResourceDataAdder = /** @class */ (function () {
function ResourceDataAdder() {
this.filterResources = core.memoize(filterResources);
}
ResourceDataAdder.prototype.transform = function (viewProps, viewSpec, calendarProps, view) {
if (viewSpec.class.needsResourceData) {
return {
resourceStore: this.filterResources(calendarProps.resourceStore, view.opt('filterResourcesWithEvents'), calendarProps.eventStore, calendarProps.dateProfile.activeRange),
resourceEntityExpansions: calendarProps.resourceEntityExpansions
};
}
};
return ResourceDataAdder;
}());
function filterResources(resourceStore, doFilterResourcesWithEvents, eventStore, activeRange) {
if (doFilterResourcesWithEvents) {
var instancesInRange = filterEventInstancesInRange(eventStore.instances, activeRange);
var hasEvents_1 = computeHasEvents(instancesInRange, eventStore.defs);
__assign(hasEvents_1, computeAncestorHasEvents(hasEvents_1, resourceStore));
return core.filterHash(resourceStore, function (resource, resourceId) {
return hasEvents_1[resourceId];
});
}
else {
return resourceStore;
}
}
function filterEventInstancesInRange(eventInstances, activeRange) {
return core.filterHash(eventInstances, function (eventInstance) {
return core.rangesIntersect(eventInstance.range, activeRange);
});
}
function computeHasEvents(eventInstances, eventDefs) {
var hasEvents = {};
for (var instanceId in eventInstances) {
var instance = eventInstances[instanceId];
for (var _i = 0, _a = eventDefs[instance.defId].resourceIds; _i < _a.length; _i++) {
var resourceId = _a[_i];
hasEvents[resourceId] = true;
}
}
return hasEvents;
}
/*
mark resources as having events if any of their ancestors have them
NOTE: resourceStore might not have all the resources that hasEvents{} has keyed
*/
function computeAncestorHasEvents(hasEvents, resourceStore) {
var res = {};
for (var resourceId in hasEvents) {
var resource = void 0;
while ((resource = resourceStore[resourceId])) {
resourceId = resource.parentId; // now functioning as the parentId
if (resourceId) {
res[resourceId] = true;
}
else {
break;
}
}
}
return res;
}
// for when non-resource view should be given EventUi info (for event coloring/constraints based off of resource data)
var ResourceEventConfigAdder = /** @class */ (function () {
function ResourceEventConfigAdder() {
this.buildResourceEventUis = core.memoizeOutput(buildResourceEventUis, core.isPropsEqual);
this.injectResourceEventUis = core.memoize(injectResourceEventUis);
}
ResourceEventConfigAdder.prototype.transform = function (viewProps, viewSpec, calendarProps) {
if (!viewSpec.class.needsResourceData) { // is a non-resource view?
return {
eventUiBases: this.injectResourceEventUis(viewProps.eventUiBases, viewProps.eventStore.defs, this.buildResourceEventUis(calendarProps.resourceStore))
};
}
};
return ResourceEventConfigAdder;
}());
function buildResourceEventUis(resourceStore) {
return core.mapHash(resourceStore, function (resource) {
return resource.ui;
});
}
function injectResourceEventUis(eventUiBases, eventDefs, resourceEventUis) {
return core.mapHash(eventUiBases, function (eventUi, defId) {
if (defId) { // not the '' key
return injectResourceEventUi(eventUi, eventDefs[defId], resourceEventUis);
}
else {
return eventUi;
}
});
}
function injectResourceEventUi(origEventUi, eventDef, resourceEventUis) {
var parts = [];
// first resource takes precedence, which fights with the ordering of combineEventUis, thus the unshifts
for (var _i = 0, _a = eventDef.resourceIds; _i < _a.length; _i++) {
var resourceId = _a[_i];
if (resourceEventUis[resourceId]) {
parts.unshift(resourceEventUis[resourceId]);
}
}
parts.unshift(origEventUi);
return core.combineEventUis(parts);
}
// for making sure events that have editable resources are always draggable in resource views
function transformIsDraggable(val, eventDef, eventUi, view) {
if (!val) {
if (view.viewSpec.class.needsResourceData) {
if (computeResourceEditable(eventDef, view.calendar)) {
return true;
}
}
}
return val;
}
var RESOURCE_SOURCE_PROPS = {
id: String
};
var defs = [];
var uid = 0;
function registerResourceSourceDef(def) {
defs.push(def);
}
function getResourceSourceDef(id) {
return defs[id];
}
function doesSourceIgnoreRange(source) {
return Boolean(defs[source.sourceDefId].ignoreRange);
}
function parseResourceSource(input) {
for (var i = defs.length - 1; i >= 0; i--) { // later-added plugins take precedence
var def = defs[i];
var meta = def.parseMeta(input);
if (meta) {
var res = parseResourceSourceProps((typeof input === 'object' && input) ? input : {}, meta, i);
res._raw = input;
return res;
}
}
return null;
}
function parseResourceSourceProps(input, meta, sourceDefId) {
var props = core.refineProps(input, RESOURCE_SOURCE_PROPS);
props.sourceId = String(uid++);
props.sourceDefId = sourceDefId;
props.meta = meta;
props.publicId = props.id;
props.isFetching = false;
props.latestFetchId = '';
props.fetchRange = null;
delete props.id;
return props;
}
function reduceResourceSource (source, action, dateProfile, calendar) {
switch (action.type) {
case 'INIT':
return createSource(calendar.opt('resources'), calendar);
case 'RESET_RESOURCE_SOURCE':
return createSource(action.resourceSourceInput, calendar, true);
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
case 'NEXT':
case 'SET_DATE':
case 'SET_VIEW_TYPE':
return handleRange(source, dateProfile.activeRange, calendar);
case 'RECEIVE_RESOURCES':
case 'RECEIVE_RESOURCE_ERROR':
return receiveResponse(source, action.fetchId, action.fetchRange);
case 'REFETCH_RESOURCES':
return fetchSource(source, dateProfile.activeRange, calendar);
default:
return source;
}
}
var uid$1 = 0;
function createSource(input, calendar, forceFetch) {
if (input) {
var source = parseResourceSource(input);
if (forceFetch || !calendar.opt('refetchResourcesOnNavigate')) { // because assumes handleRange will do it later
source = fetchSource(source, null, calendar);
}
return source;
}
return null;
}
function handleRange(source, activeRange, calendar) {
if (calendar.opt('refetchResourcesOnNavigate') &&
!doesSourceIgnoreRange(source) &&
(!source.fetchRange || !core.rangesEqual(source.fetchRange, activeRange))) {
return fetchSource(source, activeRange, calendar);
}
else {
return source;
}
}
function fetchSource(source, fetchRange, calendar) {
var sourceDef = getResourceSourceDef(source.sourceDefId);
var fetchId = String(uid$1++);
sourceDef.fetch({
resourceSource: source,
calendar: calendar,
range: fetchRange
}, function (res) {
// HACK
// do before calling dispatch in case dispatch renders synchronously
calendar.afterSizingTriggers._resourcesRendered = [null]; // fire once
calendar.dispatch({
type: 'RECEIVE_RESOURCES',
fetchId: fetchId,
fetchRange: fetchRange,
rawResources: res.rawResources
});
}, function (error) {
calendar.dispatch({
type: 'RECEIVE_RESOURCE_ERROR',
fetchId: fetchId,
fetchRange: fetchRange,
error: error
});
});
return __assign({}, source, { isFetching: true, latestFetchId: fetchId });
}
function receiveResponse(source, fetchId, fetchRange) {
if (fetchId === source.latestFetchId) {
return __assign({}, source, { isFetching: false, fetchRange: fetchRange });
}
return source;
}
var RESOURCE_PROPS = {
id: String,
title: String,
parentId: String,
businessHours: null,
children: null,
extendedProps: null
};
var PRIVATE_ID_PREFIX = '_fc:';
var uid$2 = 0;
/*
needs a full store so that it can populate children too
*/
function parseResource(input, parentId, store, calendar) {
if (parentId === void 0) { parentId = ''; }
var leftovers0 = {};
var props = core.refineProps(input, RESOURCE_PROPS, {}, leftovers0);
var leftovers1 = {};
var ui = core.processScopedUiProps('event', leftovers0, calendar, leftovers1);
if (!props.id) {
props.id = PRIVATE_ID_PREFIX + (uid$2++);
}
if (!props.parentId) { // give precedence to the parentId property
props.parentId = parentId;
}
props.businessHours = props.businessHours ? core.parseBusinessHours(props.businessHours, calendar) : null;
props.ui = ui;
props.extendedProps = __assign({}, leftovers1, props.extendedProps);
// help out ResourceApi from having user modify props
Object.freeze(ui.classNames);
Object.freeze(props.extendedProps);
if (store[props.id]) ;
else {
store[props.id] = props;
if (props.children) {
for (var _i = 0, _a = props.children; _i < _a.length; _i++) {
var childInput = _a[_i];
parseResource(childInput, props.id, store, calendar);
}
delete props.children;
}
}
return props;
}
/*
TODO: use this in more places
*/
function getPublicId(id) {
if (id.indexOf(PRIVATE_ID_PREFIX) === 0) {
return '';
}
return id;
}
function reduceResourceStore (store, action, source, calendar) {
switch (action.type) {
case 'INIT':
return {};
case 'RECEIVE_RESOURCES':
return receiveRawResources(store, action.rawResources, action.fetchId, source, calendar);
case 'ADD_RESOURCE':
return addResource(store, action.resourceHash);
case 'REMOVE_RESOURCE':
return removeResource(store, action.resourceId);
case 'SET_RESOURCE_PROP':
return setResourceProp(store, action.resourceId, action.propName, action.propValue);
case 'RESET_RESOURCES':
// must make the calendar think each resource is a new object :/
return core.mapHash(store, function (resource) {
return __assign({}, resource);
});
default:
return store;
}
}
function receiveRawResources(existingStore, inputs, fetchId, source, calendar) {
if (source.latestFetchId === fetchId) {
var nextStore = {};
for (var _i = 0, inputs_1 = inputs; _i < inputs_1.length; _i++) {
var input = inputs_1[_i];
parseResource(input, '', nextStore, calendar);
}
return nextStore;
}
else {
return existingStore;
}
}
function addResource(existingStore, additions) {
// TODO: warn about duplicate IDs
return __assign({}, existingStore, additions);
}
function removeResource(existingStore, resourceId) {
var newStore = __assign({}, existingStore);
delete newStore[resourceId];
// promote children
for (var childResourceId in newStore) { // a child, *maybe* but probably not
if (newStore[childResourceId].parentId === resourceId) {
newStore[childResourceId] = __assign({}, newStore[childResourceId], { parentId: '' });
}
}
return newStore;
}
function setResourceProp(existingStore, resourceId, name, value) {
var _a, _b;
var existingResource = existingStore[resourceId];
// TODO: sanitization
if (existingResource) {
return __assign({}, existingStore, (_a = {}, _a[resourceId] = __assign({}, existingResource, (_b = {}, _b[name] = value, _b)), _a));
}
else {
return existingStore;
}
}
function reduceResourceEntityExpansions(expansions, action) {
var _a;
switch (action.type) {
case 'INIT':
return {};
case 'SET_RESOURCE_ENTITY_EXPANDED':
return __assign({}, expansions, (_a = {}, _a[action.id] = action.isExpanded, _a));
default:
return expansions;
}
}
function resourcesReducers (state, action, calendar) {
var resourceSource = reduceResourceSource(state.resourceSource, action, state.dateProfile, calendar);
var resourceStore = reduceResourceStore(state.resourceStore, action, resourceSource, calendar);
var resourceEntityExpansions = reduceResourceEntityExpansions(state.resourceEntityExpansions, action);
return __assign({}, state, { resourceSource: resourceSource,
resourceStore: resourceStore,
resourceEntityExpansions: resourceEntityExpansions });
}
var RESOURCE_RELATED_PROPS = {
resourceId: String,
resourceIds: function (items) {
return (items || []).map(function (item) {
return String(item);
});
},
resourceEditable: Boolean
};
function parseEventDef(def, props, leftovers) {
var resourceRelatedProps = core.refineProps(props, RESOURCE_RELATED_PROPS, {}, leftovers);
var resourceIds = resourceRelatedProps.resourceIds;
if (resourceRelatedProps.resourceId) {
resourceIds.push(resourceRelatedProps.resourceId);
}
def.resourceIds = resourceIds;
def.resourceEditable = resourceRelatedProps.resourceEditable;
}
function transformDateSelectionJoin(hit0, hit1) {
var resourceId0 = hit0.dateSpan.resourceId;
var resourceId1 = hit1.dateSpan.resourceId;
if (resourceId0 && resourceId1) {
if (hit0.component.allowAcrossResources === false &&
resourceId0 !== resourceId1) {
return false;
}
else {
return { resourceId: resourceId0 };
}
}
}
var ResourceApi = /** @class */ (function () {
function ResourceApi(calendar, rawResource) {
this._calendar = calendar;
this._resource = rawResource;
}
ResourceApi.prototype.setProp = function (name, value) {
this._calendar.dispatch({
type: 'SET_RESOURCE_PROP',
resourceId: this._resource.id,
propName: name,
propValue: value
});
};
ResourceApi.prototype.remove = function () {
this._calendar.dispatch({
type: 'REMOVE_RESOURCE',
resourceId: this._resource.id
});
};
ResourceApi.prototype.getParent = function () {
var calendar = this._calendar;
var parentId = this._resource.parentId;
if (parentId) {
return new ResourceApi(calendar, calendar.state.resourceSource[parentId]);
}
else {
return null;
}
};
ResourceApi.prototype.getChildren = function () {
var thisResourceId = this._resource.id;
var calendar = this._calendar;
var resourceStore = calendar.state.resourceStore;
var childApis = [];
for (var resourceId in resourceStore) {
if (resourceStore[resourceId].parentId === thisResourceId) {
childApis.push(new ResourceApi(calendar, resourceStore[resourceId]));
}
}
return childApis;
};
/*
this is really inefficient!
TODO: make EventApi::resourceIds a hash or keep an index in the Calendar's state
*/
ResourceApi.prototype.getEvents = function () {
var thisResourceId = this._resource.id;
var calendar = this._calendar;
var _a = calendar.state.eventStore, defs = _a.defs, instances = _a.instances;
var eventApis = [];
for (var instanceId in instances) {
var instance = instances[instanceId];
var def = defs[instance.defId];
if (def.resourceIds.indexOf(thisResourceId) !== -1) { // inefficient!!!
eventApis.push(new core.EventApi(calendar, def, instance));
}
}
return eventApis;
};
Object.defineProperty(ResourceApi.prototype, "id", {
get: function () { return this._resource.id; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "title", {
get: function () { return this._resource.title; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventConstraint", {
get: function () { return this._resource.ui.constraints[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventOverlap", {
get: function () { return this._resource.ui.overlap; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventAllow", {
get: function () { return this._resource.ui.allows[0] || null; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventBackgroundColor", {
get: function () { return this._resource.ui.backgroundColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventBorderColor", {
get: function () { return this._resource.ui.borderColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventTextColor", {
get: function () { return this._resource.ui.textColor; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "eventClassNames", {
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
get: function () { return this._resource.ui.classNames; },
enumerable: true,
configurable: true
});
Object.defineProperty(ResourceApi.prototype, "extendedProps", {
get: function () { return this._resource.extendedProps; },
enumerable: true,
configurable: true
});
return ResourceApi;
}());
core.Calendar.prototype.addResource = function (input, scrollTo) {
var _a;
if (scrollTo === void 0) { scrollTo = true; }
var resourceHash;
var resource;
if (input instanceof ResourceApi) {
resource = input._resource;
resourceHash = (_a = {}, _a[resource.id] = resource, _a);
}
else {
resourceHash = {};
resource = parseResource(input, '', resourceHash, this);
}
// HACK
if (scrollTo) {
this.component.view.addScroll({ forcedRowId: resource.id });
}
this.dispatch({
type: 'ADD_RESOURCE',
resourceHash: resourceHash
});
return new ResourceApi(this, resource);
};
core.Calendar.prototype.getResourceById = function (id) {
id = String(id);
if (this.state.resourceStore) { // guard against calendar with no resource functionality
var rawResource = this.state.resourceStore[id];
if (rawResource) {
return new ResourceApi(this, rawResource);
}
}
return null;
};
core.Calendar.prototype.getResources = function () {
var resourceStore = this.state.resourceStore;
var resourceApis = [];
if (resourceStore) { // guard against calendar with no resource functionality
for (var resourceId in resourceStore) {
resourceApis.push(new ResourceApi(this, resourceStore[resourceId]));
}
}
return resourceApis;
};
core.Calendar.prototype.getTopLevelResources = function () {
var resourceStore = this.state.resourceStore;
var resourceApis = [];
if (resourceStore) { // guard against calendar with no resource functionality
for (var resourceId in resourceStore) {
if (!resourceStore[resourceId].parentId) {
resourceApis.push(new ResourceApi(this, resourceStore[resourceId]));
}
}
}
return resourceApis;
};
core.Calendar.prototype.rerenderResources = function () {
this.dispatch({
type: 'RESET_RESOURCES'
});
};
core.Calendar.prototype.refetchResources = function () {
this.dispatch({
type: 'REFETCH_RESOURCES'
});
};
function transformDatePoint(dateSpan, calendar) {
return dateSpan.resourceId ?
{ resource: calendar.getResourceById(dateSpan.resourceId) } :
{};
}
function transformDateSpan(dateSpan, calendar) {
return dateSpan.resourceId ?
{ resource: calendar.getResourceById(dateSpan.resourceId) } :
{};
}
/*
splits things BASED OFF OF which resources they are associated with.
creates a '' entry which is when something has NO resource.
*/
var ResourceSplitter = /** @class */ (function (_super) {
__extends(ResourceSplitter, _super);
function ResourceSplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceSplitter.prototype.getKeyInfo = function (props) {
return __assign({ '': {} }, props.resourceStore // already has `ui` and `businessHours` keys!
);
};
ResourceSplitter.prototype.getKeysForDateSpan = function (dateSpan) {
return [dateSpan.resourceId || ''];
};
ResourceSplitter.prototype.getKeysForEventDef = function (eventDef) {
var resourceIds = eventDef.resourceIds;
if (!resourceIds.length) {
return [''];
}
return resourceIds;
};
return ResourceSplitter;
}(core.Splitter));
function isPropsValidWithResources(props, calendar) {
var splitter = new ResourceSplitter();
var sets = splitter.splitProps(__assign({}, props, { resourceStore: calendar.state.resourceStore }));
for (var resourceId in sets) {
var props_1 = sets[resourceId];
// merge in event data from the non-resource segment
if (resourceId && sets['']) { // current segment is not the non-resource one, and there IS a non-resource one
props_1 = __assign({}, props_1, { eventStore: core.mergeEventStores(sets[''].eventStore, props_1.eventStore), eventUiBases: __assign({}, sets[''].eventUiBases, props_1.eventUiBases) });
}
if (!core.isPropsValid(props_1, calendar, { resourceId: resourceId }, filterConfig.bind(null, resourceId))) {
return false;
}
}
return true;
}
function filterConfig(resourceId, config) {
return __assign({}, config, { constraints: filterConstraints(resourceId, config.constraints) });
}
function filterConstraints(resourceId, constraints) {
return constraints.map(function (constraint) {
var defs = constraint.defs;
if (defs) { // we are dealing with an EventStore
// if any of the events define constraints to resources that are NOT this resource,
// then this resource is unconditionally prohibited, which is what a `false` value does.
for (var defId in defs) {
var resourceIds = defs[defId].resourceIds;
if (resourceIds.length && resourceIds.indexOf(resourceId) === -1) { // TODO: use a hash?!!! (for other reasons too)
return false;
}
}
}
return constraint;
});
}
function transformExternalDef(dateSpan) {
return dateSpan.resourceId ?
{ resourceId: dateSpan.resourceId } :
{};
}
function transformEventResizeJoin(hit0, hit1) {
var component = hit0.component;
if (component.allowAcrossResources === false &&
hit0.dateSpan.resourceId !== hit1.dateSpan.resourceId) {
return false;
}
}
core.EventApi.prototype.getResources = function () {
var calendar = this._calendar;
return this._def.resourceIds.map(function (resourceId) {
return calendar.getResourceById(resourceId);
});
};
core.EventApi.prototype.setResources = function (resources) {
var resourceIds = [];
// massage resources -> resourceIds
for (var _i = 0, resources_1 = resources; _i < resources_1.length; _i++) {
var resource = resources_1[_i];
var resourceId = null;
if (typeof resource === 'string') {
resourceId = resource;
}
else if (typeof resource === 'number') {
resourceId = String(resource);
}
else if (resource instanceof ResourceApi) {
resourceId = resource.id; // guaranteed to always have an ID. hmmm
}
else {
console.warn('unknown resource type: ' + resource);
}
if (resourceId) {
resourceIds.push(resourceId);
}
}
this.mutate({
standardProps: {
resourceIds: resourceIds
}
});
};
var RELEASE_DATE = '2019-08-10'; // for Scheduler
var UPGRADE_WINDOW = 365 + 7; // days. 1 week leeway, for tz shift reasons too
var LICENSE_INFO_URL = 'http://fullcalendar.io/scheduler/license/';
var PRESET_LICENSE_KEYS = [
'GPL-My-Project-Is-Open-Source',
'CC-Attribution-NonCommercial-NoDerivatives'
];
var CSS = {
position: 'absolute',
'z-index': 99999,
bottom: '1px',
left: '1px',
background: '#eee',
'border-color': '#ddd',
'border-style': 'solid',
'border-width': '1px 1px 0 0',
padding: '2px 4px',
'font-size': '12px',
'border-top-right-radius': '3px'
};
function injectLicenseWarning(containerEl, calendar) {
var key = calendar.opt('schedulerLicenseKey');
if (!isImmuneUrl(window.location.href) && !isValidKey(key)) {
core.appendToElement(containerEl, '
var optionChangeHandlers = {
resources: handleResources
};
function handleResources(newSourceInput, calendar, deepEqual) {
var oldSourceInput = calendar.state.resourceSource._raw;
if (!deepEqual(oldSourceInput, newSourceInput)) {
calendar.dispatch({
type: 'RESET_RESOURCE_SOURCE',
resourceSourceInput: newSourceInput
});
}
}
registerResourceSourceDef({
ignoreRange: true,
parseMeta: function (raw) {
if (Array.isArray(raw)) {
return raw;
}
else if (Array.isArray(raw.resources)) {
return raw.resources;
}
return null;
},
fetch: function (arg, successCallback) {
successCallback({
rawResources: arg.resourceSource.meta
});
}
});
registerResourceSourceDef({
parseMeta: function (raw) {
if (typeof raw === 'function') {
return raw;
}
else if (typeof raw.resources === 'function') {
return raw.resources;
}
return null;
},
fetch: function (arg, success, failure) {
var dateEnv = arg.calendar.dateEnv;
var func = arg.resourceSource.meta;
var publicArg = {};
if (arg.range) {
publicArg = {
start: dateEnv.toDate(arg.range.start),
end: dateEnv.toDate(arg.range.end),
startStr: dateEnv.formatIso(arg.range.start),
endStr: dateEnv.formatIso(arg.range.end),
timeZone: dateEnv.timeZone
};
}
// TODO: make more dry with EventSourceFunc
// TODO: accept a response?
core.unpromisify(func.bind(null, publicArg), function (rawResources) {
success({ rawResources: rawResources }); // needs an object response
}, failure // send errorObj directly to failure callback
);
}
});
registerResourceSourceDef({
parseMeta: function (raw) {
if (typeof raw === 'string') {
raw = { url: raw };
}
else if (!raw || typeof raw !== 'object' || !raw.url) {
return null;
}
return {
url: raw.url,
method: (raw.method || 'GET').toUpperCase(),
extraParams: raw.extraParams
};
},
fetch: function (arg, successCallback, failureCallback) {
var meta = arg.resourceSource.meta;
var requestParams = buildRequestParams(meta, arg.range, arg.calendar);
core.requestJson(meta.method, meta.url, requestParams, function (rawResources, xhr) {
successCallback({ rawResources: rawResources, xhr: xhr });
}, function (message, xhr) {
failureCallback({ message: message, xhr: xhr });
});
}
});
// TODO: somehow consolidate with event json feed
function buildRequestParams(meta, range, calendar) {
var dateEnv = calendar.dateEnv;
var startParam;
var endParam;
var timeZoneParam;
var customRequestParams;
var params = {};
if (range) {
// startParam = meta.startParam
// if (startParam == null) {
startParam = calendar.opt('startParam');
// }
// endParam = meta.endParam
// if (endParam == null) {
endParam = calendar.opt('endParam');
// }
// timeZoneParam = meta.timeZoneParam
// if (timeZoneParam == null) {
timeZoneParam = calendar.opt('timeZoneParam');
// }
params[startParam] = dateEnv.formatIso(range.start);
params[endParam] = dateEnv.formatIso(range.end);
if (dateEnv.timeZone !== 'local') {
params[timeZoneParam] = dateEnv.timeZone;
}
}
// retrieve any outbound GET/POST data from the options
if (typeof meta.extraParams === 'function') {
// supplied as a function that returns a key/value object
customRequestParams = meta.extraParams();
}
else {
// probably supplied as a straight key/value object
customRequestParams = meta.extraParams || {};
}
__assign(params, customRequestParams);
return params;
}
function buildResourceTextFunc(resourceTextSetting, calendar) {
if (typeof resourceTextSetting === 'function') {
return function (resource) {
return resourceTextSetting(new ResourceApi(calendar, resource));
};
}
else {
return function (resource) {
return resource.title || getPublicId(resource.id);
};
}
}
var ResourceDayHeader = /** @class */ (function (_super) {
__extends(ResourceDayHeader, _super);
function ResourceDayHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
_this.datesAboveResources = _this.opt('datesAboveResources');
_this.resourceTextFunc = buildResourceTextFunc(_this.opt('resourceText'), _this.calendar);
parentEl.innerHTML = ''; // because might be nbsp
parentEl.appendChild(_this.el = core.htmlToElement('
var AbstractResourceDayTable = /** @class */ (function () {
function AbstractResourceDayTable(dayTable, resources) {
this.dayTable = dayTable;
this.resources = resources;
this.resourceIndex = new ResourceIndex(resources);
this.rowCnt = dayTable.rowCnt;
this.colCnt = dayTable.colCnt * resources.length;
this.cells = this.buildCells();
}
AbstractResourceDayTable.prototype.buildCells = function () {
var _a = this, rowCnt = _a.rowCnt, dayTable = _a.dayTable, resources = _a.resources;
var rows = [];
for (var row = 0; row < rowCnt; row++) {
var rowCells = [];
for (var dateCol = 0; dateCol < dayTable.colCnt; dateCol++) {
for (var resourceCol = 0; resourceCol < resources.length; resourceCol++) {
var resource = resources[resourceCol];
var htmlAttrs = 'data-resource-id="' + resource.id + '"';
rowCells[this.computeCol(dateCol, resourceCol)] = {
date: dayTable.cells[row][dateCol].date,
resource: resource,
htmlAttrs: htmlAttrs
};
}
}
rows.push(rowCells);
}
return rows;
};
return AbstractResourceDayTable;
}());
/*
resources over dates
*/
var ResourceDayTable = /** @class */ (function (_super) {
__extends(ResourceDayTable, _super);
function ResourceDayTable() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceDayTable.prototype.computeCol = function (dateI, resourceI) {
return resourceI * this.dayTable.colCnt + dateI;
};
/*
all date ranges are intact
*/
ResourceDayTable.prototype.computeColRanges = function (dateStartI, dateEndI, resourceI) {
return [
{
firstCol: this.computeCol(dateStartI, resourceI),
lastCol: this.computeCol(dateEndI, resourceI),
isStart: true,
isEnd: true
}
];
};
return ResourceDayTable;
}(AbstractResourceDayTable));
/*
dates over resources
*/
var DayResourceTable = /** @class */ (function (_super) {
__extends(DayResourceTable, _super);
function DayResourceTable() {
return _super !== null && _super.apply(this, arguments) || this;
}
DayResourceTable.prototype.computeCol = function (dateI, resourceI) {
return dateI * this.resources.length + resourceI;
};
/*
every single day is broken up
*/
DayResourceTable.prototype.computeColRanges = function (dateStartI, dateEndI, resourceI) {
var segs = [];
for (var i = dateStartI; i <= dateEndI; i++) {
var col = this.computeCol(i, resourceI);
segs.push({
firstCol: col,
lastCol: col,
isStart: i === dateStartI,
isEnd: i === dateEndI
});
}
return segs;
};
return DayResourceTable;
}(AbstractResourceDayTable));
var ResourceIndex = /** @class */ (function () {
function ResourceIndex(resources) {
var indicesById = {};
var ids = [];
for (var i = 0; i < resources.length; i++) {
var id = resources[i].id;
ids.push(id);
indicesById[id] = i;
}
this.ids = ids;
this.indicesById = indicesById;
this.length = resources.length;
}
return ResourceIndex;
}());
/*
TODO: just use ResourceHash somehow? could then use the generic ResourceSplitter
*/
var VResourceSplitter = /** @class */ (function (_super) {
__extends(VResourceSplitter, _super);
function VResourceSplitter() {
return _super !== null && _super.apply(this, arguments) || this;
}
VResourceSplitter.prototype.getKeyInfo = function (props) {
var resourceDayTable = props.resourceDayTable;
var hash = core.mapHash(resourceDayTable.resourceIndex.indicesById, function (i) {
return resourceDayTable.resources[i]; // has `ui` AND `businessHours` keys!
}); // :(
hash[''] = {};
return hash;
};
VResourceSplitter.prototype.getKeysForDateSpan = function (dateSpan) {
return [dateSpan.resourceId || ''];
};
VResourceSplitter.prototype.getKeysForEventDef = function (eventDef) {
var resourceIds = eventDef.resourceIds;
if (!resourceIds.length) {
return [''];
}
return resourceIds;
};
return VResourceSplitter;
}(core.Splitter));
// joiner
var NO_SEGS = []; // for memoizing
var VResourceJoiner = /** @class */ (function () {
function VResourceJoiner() {
this.joinDateSelection = core.memoize(this.joinSegs);
this.joinBusinessHours = core.memoize(this.joinSegs);
this.joinFgEvents = core.memoize(this.joinSegs);
this.joinBgEvents = core.memoize(this.joinSegs);
this.joinEventDrags = core.memoize(this.joinInteractions);
this.joinEventResizes = core.memoize(this.joinInteractions);
}
/*
propSets also has a '' key for things with no resource
*/
VResourceJoiner.prototype.joinProps = function (propSets, resourceDayTable) {
var dateSelectionSets = [];
var businessHoursSets = [];
var fgEventSets = [];
var bgEventSets = [];
var eventDrags = [];
var eventResizes = [];
var eventSelection = '';
var keys = resourceDayTable.resourceIndex.ids.concat(['']); // add in the all-resource key
for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
var key = keys_1[_i];
var props = propSets[key];
dateSelectionSets.push(props.dateSelectionSegs);
businessHoursSets.push(key ? props.businessHourSegs : NO_SEGS); // don't include redundant all-resource businesshours
fgEventSets.push(key ? props.fgEventSegs : NO_SEGS); // don't include fg all-resource segs
bgEventSets.push(props.bgEventSegs);
eventDrags.push(props.eventDrag);
eventResizes.push(props.eventResize);
eventSelection = eventSelection || props.eventSelection;
}
return {
dateSelectionSegs: this.joinDateSelection.apply(this, [resourceDayTable].concat(dateSelectionSets)),
businessHourSegs: this.joinBusinessHours.apply(this, [resourceDayTable].concat(businessHoursSets)),
fgEventSegs: this.joinFgEvents.apply(this, [resourceDayTable].concat(fgEventSets)),
bgEventSegs: this.joinBgEvents.apply(this, [resourceDayTable].concat(bgEventSets)),
eventDrag: this.joinEventDrags.apply(this, [resourceDayTable].concat(eventDrags)),
eventResize: this.joinEventResizes.apply(this, [resourceDayTable].concat(eventResizes)),
eventSelection: eventSelection
};
};
VResourceJoiner.prototype.joinSegs = function (resourceDayTable) {
var segGroups = [];
for (var _i = 1; _i < arguments.length; _i++) {
segGroups[_i - 1] = arguments[_i];
}
var resourceCnt = resourceDayTable.resources.length;
var transformedSegs = [];
for (var i = 0; i < resourceCnt; i++) {
for (var _a = 0, _b = segGroups[i]; _a < _b.length; _a++) {
var seg = _b[_a];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
for (var _c = 0, _d = segGroups[resourceCnt]; _c < _d.length; _c++) { // one beyond. the all-resource
var seg = _d[_c];
transformedSegs.push.apply(// one beyond. the all-resource
transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
}
return transformedSegs;
};
/*
for expanding non-resource segs to all resources.
only for public use.
no memoizing.
*/
VResourceJoiner.prototype.expandSegs = function (resourceDayTable, segs) {
var resourceCnt = resourceDayTable.resources.length;
var transformedSegs = [];
for (var i = 0; i < resourceCnt; i++) {
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i));
}
}
return transformedSegs;
};
VResourceJoiner.prototype.joinInteractions = function (resourceDayTable) {
var interactions = [];
for (var _i = 1; _i < arguments.length; _i++) {
interactions[_i - 1] = arguments[_i];
}
var resourceCnt = resourceDayTable.resources.length;
var affectedInstances = {};
var transformedSegs = [];
var isEvent = false;
var sourceSeg = null;
for (var i = 0; i < resourceCnt; i++) {
var interaction = interactions[i];
if (interaction) {
for (var _a = 0, _b = interaction.segs; _a < _b.length; _a++) {
var seg = _b[_a];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i) // TODO: templateify Interaction::segs
);
}
__assign(affectedInstances, interaction.affectedInstances);
isEvent = isEvent || interaction.isEvent;
sourceSeg = sourceSeg || interaction.sourceSeg;
}
if (interactions[resourceCnt]) { // one beyond. the all-resource
for (var _c = 0, _d = interactions[resourceCnt].segs; _c < _d.length; _c++) {
var seg = _d[_c];
transformedSegs.push.apply(transformedSegs, this.transformSeg(seg, resourceDayTable, i) // TODO: templateify Interaction::segs
);
}
}
}
return {
affectedInstances: affectedInstances,
segs: transformedSegs,
isEvent: isEvent,
sourceSeg: sourceSeg
};
};
return VResourceJoiner;
}());
/*
doesn't accept grouping
*/
function flattenResources(resourceStore, orderSpecs) {
return buildRowNodes(resourceStore, [], orderSpecs, false, {}, true)
.map(function (node) {
return node.resource;
});
}
function buildRowNodes(resourceStore, groupSpecs, orderSpecs, isVGrouping, expansions, expansionDefault) {
var complexNodes = buildHierarchy(resourceStore, isVGrouping ? -1 : 1, groupSpecs, orderSpecs);
var flatNodes = [];
flattenNodes(complexNodes, flatNodes, isVGrouping, [], 0, expansions, expansionDefault);
return flatNodes;
}
function flattenNodes(complexNodes, res, isVGrouping, rowSpans, depth, expansions, expansionDefault) {
for (var i = 0; i < complexNodes.length; i++) {
var complexNode = complexNodes[i];
var group = complexNode.group;
if (group) {
if (isVGrouping) {
var firstRowIndex = res.length;
var rowSpanIndex = rowSpans.length;
flattenNodes(complexNode.children, res, isVGrouping, rowSpans.concat(0), depth, expansions, expansionDefault);
if (firstRowIndex < res.length) {
var firstRow = res[firstRowIndex];
var firstRowSpans = firstRow.rowSpans = firstRow.rowSpans.slice();
firstRowSpans[rowSpanIndex] = res.length - firstRowIndex;
}
}
else {
var id = group.spec.field + ':' + group.value;
var isExpanded = expansions[id] != null ? expansions[id] : expansionDefault;
res.push({ id: id, group: group, isExpanded: isExpanded });
if (isExpanded) {
flattenNodes(complexNode.children, res, isVGrouping, rowSpans, depth + 1, expansions, expansionDefault);
}
}
}
else if (complexNode.resource) {
var id = complexNode.resource.id;
var isExpanded = expansions[id] != null ? expansions[id] : expansionDefault;
res.push({
id: id,
rowSpans: rowSpans,
depth: depth,
isExpanded: isExpanded,
hasChildren: Boolean(complexNode.children.length),
resource: complexNode.resource,
resourceFields: complexNode.resourceFields
});
if (isExpanded) {
flattenNodes(complexNode.children, res, isVGrouping, rowSpans, depth + 1, expansions, expansionDefault);
}
}
}
}
function buildHierarchy(resourceStore, maxDepth, groupSpecs, orderSpecs) {
var resourceNodes = buildResourceNodes(resourceStore, orderSpecs);
var builtNodes = [];
for (var resourceId in resourceNodes) {
var resourceNode = resourceNodes[resourceId];
if (!resourceNode.resource.parentId) {
insertResourceNode(resourceNode, builtNodes, groupSpecs, 0, maxDepth, orderSpecs);
}
}
return builtNodes;
}
function buildResourceNodes(resourceStore, orderSpecs) {
var nodeHash = {};
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
nodeHash[resourceId] = {
resource: resource,
resourceFields: buildResourceFields(resource),
children: []
};
}
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
if (resource.parentId) {
var parentNode = nodeHash[resource.parentId];
if (parentNode) {
insertResourceNodeInSiblings(nodeHash[resourceId], parentNode.children, orderSpecs);
}
}
}
return nodeHash;
}
function insertResourceNode(resourceNode, nodes, groupSpecs, depth, maxDepth, orderSpecs) {
if (groupSpecs.length && (maxDepth === -1 || depth <= maxDepth)) {
var groupNode = ensureGroupNodes(resourceNode, nodes, groupSpecs[0]);
insertResourceNode(resourceNode, groupNode.children, groupSpecs.slice(1), depth + 1, maxDepth, orderSpecs);
}
else {
insertResourceNodeInSiblings(resourceNode, nodes, orderSpecs);
}
}
function ensureGroupNodes(resourceNode, nodes, groupSpec) {
var groupValue = resourceNode.resourceFields[groupSpec.field];
var groupNode;
var newGroupIndex;
// find an existing group that matches, or determine the position for a new group
if (groupSpec.order) {
for (newGroupIndex = 0; newGroupIndex < nodes.length; newGroupIndex++) {
var node = nodes[newGroupIndex];
if (node.group) {
var cmp = core.flexibleCompare(groupValue, node.group.value) * groupSpec.order;
if (cmp === 0) {
groupNode = node;
break;
}
else if (cmp < 0) {
break;
}
}
}
}
else { // the groups are unordered
for (newGroupIndex = 0; newGroupIndex < nodes.length; newGroupIndex++) {
var node = nodes[newGroupIndex];
if (node.group && groupValue === node.group.value) {
groupNode = node;
break;
}
}
}
if (!groupNode) {
groupNode = {
group: {
value: groupValue,
spec: groupSpec
},
children: []
};
nodes.splice(newGroupIndex, 0, groupNode);
}
return groupNode;
}
function insertResourceNodeInSiblings(resourceNode, siblings, orderSpecs) {
var i;
for (i = 0; i < siblings.length; i++) {
var cmp = core.compareByFieldSpecs(siblings[i].resourceFields, resourceNode.resourceFields, orderSpecs);
if (cmp > 0) { // went 1 past. insert at i
break;
}
}
siblings.splice(i, 0, resourceNode);
}
function buildResourceFields(resource) {
var obj = __assign({}, resource.extendedProps, resource.ui, resource);
delete obj.ui;
delete obj.extendedProps;
return obj;
}
function isGroupsEqual(group0, group1) {
return group0.spec === group1.spec && group0.value === group1.value;
}
var main = core.createPlugin({
reducers: [resourcesReducers],
eventDefParsers: [parseEventDef],
isDraggableTransformers: [transformIsDraggable],
eventDragMutationMassagers: [massageEventDragMutation],
eventDefMutationAppliers: [applyEventDefMutation],
dateSelectionTransformers: [transformDateSelectionJoin],
datePointTransforms: [transformDatePoint],
dateSpanTransforms: [transformDateSpan],
viewPropsTransformers: [ResourceDataAdder, ResourceEventConfigAdder],
isPropsValid: isPropsValidWithResources,
externalDefTransforms: [transformExternalDef],
eventResizeJoinTransforms: [transformEventResizeJoin],
viewContainerModifiers: [injectLicenseWarning],
eventDropTransformers: [transformEventDrop],
optionChangeHandlers: optionChangeHandlers
});
exports.AbstractResourceDayTable = AbstractResourceDayTable;
exports.DayResourceTable = DayResourceTable;
exports.ResourceApi = ResourceApi;
exports.ResourceDayHeader = ResourceDayHeader;
exports.ResourceDayTable = ResourceDayTable;
exports.ResourceSplitter = ResourceSplitter;
exports.VResourceJoiner = VResourceJoiner;
exports.VResourceSplitter = VResourceSplitter;
exports.buildResourceFields = buildResourceFields;
exports.buildResourceTextFunc = buildResourceTextFunc;
exports.buildRowNodes = buildRowNodes;
exports.default = main;
exports.flattenResources = flattenResources;
exports.isGroupsEqual = isGroupsEqual;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ambient.d.ts
declare module '@fullcalendar/resource-common' {
import '@fullcalendar/resource-common/ambient';
import '@fullcalendar/resource-common/api/EventApi';
import '@fullcalendar/resource-common/resource-sources/resource-array';
import '@fullcalendar/resource-common/resource-sources/resource-func';
import '@fullcalendar/resource-common/resource-sources/resource-json-feed';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
export { default as ResourceDayHeader } from '@fullcalendar/resource-common/common/ResourceDayHeader';
export { VResourceJoiner, AbstractResourceDayTable, ResourceDayTable, DayResourceTable, VResourceSplitter } from '@fullcalendar/resource-common/common/resource-day-table';
export { Resource, ResourceHash } from '@fullcalendar/resource-common/structs/resource';
export { ResourceViewProps } from '@fullcalendar/resource-common/View';
export { flattenResources, Group, isGroupsEqual, GroupNode, ResourceNode, buildRowNodes, buildResourceFields } from '@fullcalendar/resource-common/common/resource-hierarchy';
export { buildResourceTextFunc } from '@fullcalendar/resource-common/common/resource-rendering';
export { default as ResourceApi } from '@fullcalendar/resource-common/api/ResourceApi';
export { default as ResourceSplitter } from '@fullcalendar/resource-common/common/ResourceSplitter';
}
declare module '@fullcalendar/resource-common/ambient' {
import ResourceApi from '@fullcalendar/resource-common/api/ResourceApi';
import { ResourceSourceInput, ResourceSource } from '@fullcalendar/resource-common/structs/resource-source';
import { View } from '@fullcalendar/core';
import { ResourceHash } from '@fullcalendar/resource-common/structs/resource';
import { ResourceEntityExpansions } from '@fullcalendar/resource-common/reducers/resourceEntityExpansions';
import { ResourceAction } from '@fullcalendar/resource-common/reducers/resources';
module '@fullcalendar/core' {
interface Calendar {
addResource(input: ResourceSourceInput): ResourceApi;
getResourceById(id: string): ResourceApi | null;
getResources(): ResourceApi[];
getTopLevelResources(): ResourceApi[];
rerenderResources(): void;
refetchResources(): void;
}
interface Calendar {
dispatch(action: ResourceAction): any;
}
interface CalendarState {
resourceSource?: ResourceSource | null;
resourceStore?: ResourceHash;
resourceEntityExpansions?: ResourceEntityExpansions;
}
interface OptionsInput {
schedulerLicenseKey?: string;
resources?: ResourceSourceInput;
resourceLabelText?: string;
resourceOrder?: any;
filterResourcesWithEvents?: any;
resourceText?: any;
resourceGroupField?: any;
resourceGroupText?: any;
resourceAreaWidth?: any;
resourceColumns?: any;
resourcesInitiallyExpanded?: any;
slotWidth?: any;
datesAboveResources?: any;
eventResourceEditable?: boolean;
refetchResourcesOnNavigate?: boolean;
resourceRender?(arg: {
resource: ResourceApi;
el: HTMLElement;
view: View;
}): void;
}
interface DatePointApi {
resource?: ResourceApi;
}
interface DateSpanApi {
resource?: ResourceApi;
}
interface EventMutation {
resourceMutation?: {
matchResourceId: string;
setResourceId: string;
};
}
interface EventApi {
getResources: () => ResourceApi[];
setResources: (resources: (string | ResourceApi)[]) => void;
}
interface EventDef {
resourceIds: string[];
resourceEditable: boolean;
}
}
}
declare module '@fullcalendar/resource-common/api/EventApi' {
import '@fullcalendar/resource-common/ambient';
}
declare module '@fullcalendar/resource-common/resource-sources/resource-array' {
export {};
}
declare module '@fullcalendar/resource-common/resource-sources/resource-func' {
import { ResourceSourceError } from '@fullcalendar/resource-common/structs/resource-source';
import { ResourceInput } from '@fullcalendar/resource-common/structs/resource';
export type ResourceFunc = (arg: {
start: Date;
end: Date;
timeZone: string;
}, successCallback: (events: ResourceInput[]) => void, failureCallback: (errorObj: ResourceSourceError) => void) => any;
}
declare module '@fullcalendar/resource-common/resource-sources/resource-json-feed' {
export {};
}
declare module '@fullcalendar/resource-common/common/ResourceDayHeader' {
import { Component, ComponentContext, DateMarker, DateProfile, DateFormatter } from '@fullcalendar/core';
import { Resource } from '@fullcalendar/resource-common/structs/resource';
export interface ResourceDayHeaderProps {
dates: DateMarker[];
dateProfile: DateProfile;
datesRepDistinctDays: boolean;
resources: Resource[];
renderIntroHtml?: () => string;
}
export { ResourceDayHeader as default, ResourceDayHeader };
class ResourceDayHeader extends Component {
datesAboveResources: boolean;
resourceTextFunc: (resource: Resource) => string;
dateFormat: DateFormatter;
el: HTMLElement;
thead: HTMLElement;
constructor(context: ComponentContext, parentEl: HTMLElement);
destroy(): void;
render(props: ResourceDayHeaderProps): void;
renderResourceRow(resources: Resource[]): string;
renderDayAndResourceRows(dates: DateMarker[], resources: Resource[]): string;
renderResourceAndDayRows(resources: Resource[], dates: DateMarker[]): string;
renderResourceCell(resource: Resource, colspan: number, date?: DateMarker): string;
renderDateCell(date: DateMarker, colspan: number, resource?: Resource): string;
buildTr(cellHtmls: string[]): string;
processResourceEls(resources: Resource[]): void;
}
}
declare module '@fullcalendar/resource-common/common/resource-day-table' {
import { SlicedProps, EventDef, Splitter, DayTable, DayTableCell, SplittableProps, DateSpan, Seg, EventSegUiInteractionState } from '@fullcalendar/core';
import { Resource } from '@fullcalendar/resource-common/structs/resource';
export interface ResourceDayTableCell extends DayTableCell {
resource: Resource;
}
export abstract class AbstractResourceDayTable {
cells: ResourceDayTableCell[][];
rowCnt: number;
colCnt: number;
dayTable: DayTable;
resources: Resource[];
resourceIndex: ResourceIndex;
constructor(dayTable: DayTable, resources: Resource[]);
abstract computeCol(dateI: any, resourceI: any): number;
abstract computeColRanges(dateStartI: any, dateEndI: any, resourceI: any): {
firstCol: number;
lastCol: number;
isStart: boolean;
isEnd: boolean;
}[];
buildCells(): ResourceDayTableCell[][];
}
export class ResourceDayTable extends AbstractResourceDayTable {
computeCol(dateI: any, resourceI: any): any;
computeColRanges(dateStartI: any, dateEndI: any, resourceI: any): {
firstCol: any;
lastCol: any;
isStart: boolean;
isEnd: boolean;
}[];
}
export class DayResourceTable extends AbstractResourceDayTable {
computeCol(dateI: any, resourceI: any): any;
computeColRanges(dateStartI: any, dateEndI: any, resourceI: any): any[];
}
export class ResourceIndex {
indicesById: {
[resourceId: string]: number;
};
ids: string[];
length: number;
constructor(resources: Resource[]);
}
export interface VResourceProps extends SplittableProps {
resourceDayTable: AbstractResourceDayTable;
}
export class VResourceSplitter extends Splitter {
getKeyInfo(props: VResourceProps): any;
getKeysForDateSpan(dateSpan: DateSpan): string[];
getKeysForEventDef(eventDef: EventDef): string[];
}
export abstract class VResourceJoiner {
joinProps(propSets: {
[resourceId: string]: SlicedProps;
}, resourceDayTable: AbstractResourceDayTable): SlicedProps;
joinSegs(resourceDayTable: AbstractResourceDayTable, ...segGroups: SegType[][]): SegType[];
expandSegs(resourceDayTable: AbstractResourceDayTable, segs: SegType[]): any[];
joinInteractions(resourceDayTable: AbstractResourceDayTable, ...interactions: EventSegUiInteractionState[]): EventSegUiInteractionState;
abstract transformSeg(seg: SegType, resourceDayTable: AbstractResourceDayTable, resourceI: number): SegType[];
}
}
declare module '@fullcalendar/resource-common/structs/resource' {
import { ConstraintInput, AllowFunc, EventStore, Calendar, EventUi, BusinessHoursInput } from '@fullcalendar/core';
export interface ResourceInput {
id?: string;
parentId?: string;
children?: ResourceInput[];
title?: string;
businessHours?: BusinessHoursInput;
eventEditable?: boolean;
eventStartEditable?: boolean;
eventDurationEditable?: boolean;
eventConstraint?: ConstraintInput;
eventOverlap?: boolean;
eventAllow?: AllowFunc;
eventClassName?: string[] | string;
eventClassNames?: string[] | string;
eventBackgroundColor?: string;
eventBorderColor?: string;
eventTextColor?: string;
eventColor?: string;
extendedProps?: {
[extendedProp: string]: any;
};
[otherProp: string]: any;
}
export interface Resource {
id: string;
parentId: string;
title: string;
businessHours: EventStore | null;
ui: EventUi;
extendedProps: {
[extendedProp: string]: any;
};
}
export type ResourceHash = {
[resourceId: string]: Resource;
};
export function parseResource(input: ResourceInput, parentId: string, store: ResourceHash, calendar: Calendar): Resource;
export function getPublicId(id: string): string;
}
declare module '@fullcalendar/resource-common/View' {
import { View, ViewProps, ViewSpec, ViewPropsTransformer, CalendarComponentProps, EventUi, EventUiHash, EventDefHash, EventDef, EventStore, DateRange } from '@fullcalendar/core';
import { ResourceHash } from '@fullcalendar/resource-common/structs/resource';
import { ResourceEntityExpansions } from '@fullcalendar/resource-common/reducers/resourceEntityExpansions';
export interface ResourceViewProps extends ViewProps {
resourceStore: ResourceHash;
resourceEntityExpansions: ResourceEntityExpansions;
}
export class ResourceDataAdder implements ViewPropsTransformer {
filterResources: typeof filterResources;
transform(viewProps: ViewProps, viewSpec: ViewSpec, calendarProps: CalendarComponentProps, view: View): {
resourceStore: ResourceHash;
resourceEntityExpansions: ResourceEntityExpansions;
};
}
function filterResources(resourceStore: ResourceHash, doFilterResourcesWithEvents: boolean, eventStore: EventStore, activeRange: DateRange): ResourceHash;
export class ResourceEventConfigAdder implements ViewPropsTransformer {
buildResourceEventUis: typeof buildResourceEventUis;
injectResourceEventUis: typeof injectResourceEventUis;
transform(viewProps: ViewProps, viewSpec: ViewSpec, calendarProps: CalendarComponentProps): {
eventUiBases: {
[key: string]: EventUi;
};
};
}
function buildResourceEventUis(resourceStore: ResourceHash): {
[key: string]: EventUi;
};
function injectResourceEventUis(eventUiBases: EventUiHash, eventDefs: EventDefHash, resourceEventUis: EventUiHash): {
[key: string]: EventUi;
};
export function transformIsDraggable(val: boolean, eventDef: EventDef, eventUi: EventUi, view: View): boolean;
export {};
}
declare module '@fullcalendar/resource-common/common/resource-hierarchy' {
import { ResourceHash, Resource } from '@fullcalendar/resource-common/structs/resource';
import { ResourceEntityExpansions } from '@fullcalendar/resource-common/reducers/resourceEntityExpansions';
export interface Group {
value: any;
spec: any;
}
export interface GroupNode {
id: string;
isExpanded: boolean;
group: Group;
}
export interface ResourceNode {
id: string;
rowSpans: number[];
depth: number;
isExpanded: boolean;
hasChildren: boolean;
resource: Resource;
resourceFields: any;
}
export function flattenResources(resourceStore: ResourceHash, orderSpecs: any): Resource[];
export function buildRowNodes(resourceStore: ResourceHash, groupSpecs: any, orderSpecs: any, isVGrouping: boolean, expansions: ResourceEntityExpansions, expansionDefault: boolean): (GroupNode | ResourceNode)[];
export function buildResourceFields(resource: Resource): {
id: string;
parentId: string;
title: string;
businessHours: import("@fullcalendar/core").EventStore;
ui: import("@fullcalendar/core").EventUi;
extendedProps: {
[extendedProp: string]: any;
};
startEditable: boolean;
durationEditable: boolean;
constraints: import("@fullcalendar/core").Constraint[];
overlap: boolean;
allows: import("@fullcalendar/core").AllowFunc[];
backgroundColor: string;
borderColor: string;
textColor: string;
classNames: string[];
};
export function isGroupsEqual(group0: Group, group1: Group): boolean;
}
declare module '@fullcalendar/resource-common/common/resource-rendering' {
import { Resource } from '@fullcalendar/resource-common/structs/resource';
export function buildResourceTextFunc(resourceTextSetting: any, calendar: any): (resource: Resource) => any;
}
declare module '@fullcalendar/resource-common/api/ResourceApi' {
import { Calendar, EventApi } from '@fullcalendar/core';
import { Resource } from '@fullcalendar/resource-common/structs/resource';
export { ResourceApi as default, ResourceApi };
class ResourceApi {
_calendar: Calendar;
_resource: Resource;
constructor(calendar: Calendar, rawResource: Resource);
setProp(name: string, value: any): void;
remove(): void;
getParent(): ResourceApi | null;
getChildren(): ResourceApi[];
getEvents(): EventApi[];
readonly id: string;
readonly title: string;
readonly eventConstraint: any;
readonly eventOverlap: any;
readonly eventAllow: any;
readonly eventBackgroundColor: string;
readonly eventBorderColor: string;
readonly eventTextColor: string;
readonly eventClassNames: string[];
readonly extendedProps: any;
}
}
declare module '@fullcalendar/resource-common/common/ResourceSplitter' {
import { Splitter, SplittableProps, DateSpan, EventDef } from '@fullcalendar/core';
import { ResourceHash } from '@fullcalendar/resource-common/structs/resource';
export interface SplittableResourceProps extends SplittableProps {
resourceStore: ResourceHash;
}
export { ResourceSplitter as default, ResourceSplitter };
class ResourceSplitter extends Splitter {
getKeyInfo(props: SplittableResourceProps): {
'': {};
};
getKeysForDateSpan(dateSpan: DateSpan): string[];
getKeysForEventDef(eventDef: EventDef): string[];
}
}
declare module '@fullcalendar/resource-common/structs/resource-source' {
import { DateRange, Calendar } from '@fullcalendar/core';
import { ResourceInput } from '@fullcalendar/resource-common/structs/resource';
import { ResourceFunc } from '@fullcalendar/resource-common/resource-sources/resource-func';
export type ResourceSourceError = {
message: string;
xhr?: XMLHttpRequest;
[otherProp: string]: any;
};
export type ResourceFetcher = (arg: {
resourceSource: ResourceSource;
calendar: Calendar;
range: DateRange | null;
}, success: (res: {
rawResources: ResourceInput[];
xhr?: XMLHttpRequest;
}) => void, failure: (error: ResourceSourceError) => void) => void;
export interface ExtendedResourceSourceInput {
id?: string;
resources?: ResourceInput[];
url?: string;
method?: string;
extraParams?: object | (() => object);
}
export type ResourceSourceInput = ResourceInput[] | ExtendedResourceSourceInput | ResourceFunc | string;
export interface ResourceSource {
_raw: any;
sourceId: string;
sourceDefId: number;
meta: any;
publicId: string;
isFetching: boolean;
latestFetchId: string;
fetchRange: DateRange | null;
}
export interface ResourceSourceDef {
ignoreRange?: boolean;
parseMeta: (raw: ResourceSourceInput) => object | null;
fetch: ResourceFetcher;
}
export function registerResourceSourceDef(def: ResourceSourceDef): void;
export function getResourceSourceDef(id: number): ResourceSourceDef;
export function doesSourceIgnoreRange(source: ResourceSource): boolean;
export function parseResourceSource(input: ResourceSourceInput): ResourceSource;
}
declare module '@fullcalendar/resource-common/reducers/resourceEntityExpansions' {
import { ResourceAction } from '@fullcalendar/resource-common/reducers/resources';
export type ResourceEntityExpansions = {
[id: string]: boolean;
};
export function reduceResourceEntityExpansions(expansions: ResourceEntityExpansions, action: ResourceAction): ResourceEntityExpansions;
}
declare module '@fullcalendar/resource-common/reducers/resources' {
import { Calendar, CalendarState, Action, DateRange } from '@fullcalendar/core';
import { ResourceSourceError } from '@fullcalendar/resource-common/structs/resource-source';
import { ResourceHash, ResourceInput } from '@fullcalendar/resource-common/structs/resource';
export type ResourceAction = Action | {
type: 'FETCH_RESOURCE';
} | {
type: 'RECEIVE_RESOURCES';
rawResources: ResourceInput[];
fetchId: string;
fetchRange: DateRange | null;
} | {
type: 'RECEIVE_RESOURCE_ERROR';
error: ResourceSourceError;
fetchId: string;
fetchRange: DateRange | null;
} | {
type: 'ADD_RESOURCE';
resourceHash: ResourceHash;
} | // use a hash because needs to accept children
{
type: 'REMOVE_RESOURCE';
resourceId: string;
} | {
type: 'SET_RESOURCE_PROP';
resourceId: string;
propName: string;
propValue: any;
} | {
type: 'SET_RESOURCE_ENTITY_EXPANDED';
id: string;
isExpanded: boolean;
} | {
type: 'RESET_RESOURCES';
} | {
type: 'RESET_RESOURCE_SOURCE';
resourceSourceInput: any;
} | {
type: 'REFETCH_RESOURCES';
};
export default function (state: CalendarState, action: ResourceAction, calendar: Calendar): {
resourceSource: import("@fullcalendar/resource-common/structs/resource-source").ResourceSource;
resourceStore: ResourceHash;
resourceEntityExpansions: import("@fullcalendar/resource-common/reducers/resourceEntityExpansions").ResourceEntityExpansions;
eventSources: import("@fullcalendar/core").EventSourceHash;
eventSourceLoadingLevel: number;
loadingLevel: number;
viewType: string;
currentDate: Date;
dateProfile: import("@fullcalendar/core").DateProfile;
eventStore: import("@fullcalendar/core").EventStore;
dateSelection: import("@fullcalendar/core").DateSpan;
eventSelection: string;
eventDrag: import("@fullcalendar/core").EventInteractionState;
eventResize: import("@fullcalendar/core").EventInteractionState;
};
}
/*!
FullCalendar Resources Common Plugin v4.3.1
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],r):r((e=e||self).FullCalendarResourceCommon={},e.FullCalendar)}(this,function(e,r){"use strict";var t=function(e,r){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])})(e,r)};function n(e,r){function n(){this.constructor=e}t(e,r),e.prototype=null===r?Object.create(r):(n.prototype=r.prototype,new n)}var o=function(){return(o=Object.assign||function(e){for(var r,t=1,n=arguments.length;t=0;r--){var t=f[r].parseMeta(e);if(t){var n=v("object"==typeof e&&e?e:{},t,r);return n._raw=e,n}}return null}(e);return!t&&r.opt("refetchResourcesOnNavigate")||(n=E(n,null,r)),n}return null}function E(e,r,t){var n,s=(n=e.sourceDefId,f[n]),u=String(R++);return s.fetch({resourceSource:e,calendar:t,range:r},function(e){t.afterSizingTriggers._resourcesRendered=[null],t.dispatch({type:"RECEIVE_RESOURCES",fetchId:u,fetchRange:r,rawResources:e.rawResources})},function(e){t.dispatch({type:"RECEIVE_RESOURCE_ERROR",fetchId:u,fetchRange:r,error:e})}),o({},e,{isFetching:!0,latestFetchId:u})}var m={id:String,title:String,parentId:String,businessHours:null,children:null,extendedProps:null},S="_fc:",I=0;function b(e,t,n,s){void 0===t&&(t="");var u={},i=r.refineProps(e,m,{},u),a={},c=r.processScopedUiProps("event",u,s,a);if(i.id||(i.id=S+I++),i.parentId||(i.parentId=t),i.businessHours=i.businessHours?r.parseBusinessHours(i.businessHours,s):null,i.ui=c,i.extendedProps=o({},a,i.extendedProps),Object.freeze(c.classNames),Object.freeze(i.extendedProps),n[i.id]);else if(n[i.id]=i,i.children){for(var l=0,d=i.children;l')),o.thead=o.el.querySelector("thead"),o}return n(t,e),t.prototype.destroy=function(){r.removeElement(this.el)},t.prototype.render=function(e){var t;this.dateFormat=r.createFormatter(this.opt("columnHeaderFormat")||r.computeFallbackHeaderFormat(e.datesRepDistinctDays,e.dates.length)),t=1===e.dates.length?this.renderResourceRow(e.resources):this.datesAboveResources?this.renderDayAndResourceRows(e.dates,e.resources):this.renderResourceAndDayRows(e.resources,e.dates),this.thead.innerHTML=t,this.processResourceEls(e.resources)},t.prototype.renderResourceRow=function(e){var r=this,t=e.map(function(e){return r.renderResourceCell(e,1)});return this.buildTr(t)},t.prototype.renderDayAndResourceRows=function(e,r){for(var t=[],n=[],o=0,s=e;o1?' colspan="'+t+'"':"")+">"+r.htmlEscape(this.resourceTextFunc(e))+""},t.prototype.renderDateCell=function(e,t,n){var o=this.props;return r.renderDateCell(e,o.dateProfile,o.datesRepDistinctDays,o.dates.length*o.resources.length,this.dateFormat,this.context,t,n?'data-resource-id="'+n.id+'"':"")},t.prototype.buildTr=function(e){return e.length||(e=[" "]),this.props.renderIntroHtml&&(e=[this.props.renderIntroHtml()].concat(e)),this.isRtl&&e.reverse(),""+e.join("")+""},t.prototype.processResourceEls=function(e){var t=this,n=this.view;r.findElements(this.thead,".fc-resource-cell").forEach(function(r,o){o%=e.length,t.isRtl&&(o=e.length-1-o);var s=e[o];n.publiclyTrigger("resourceRender",[{resource:new w(t.calendar,s),el:r,view:n}])})},t}(r.Component),z=function(){function e(e,r){this.dayTable=e,this.resources=r,this.resourceIndex=new V(r),this.rowCnt=e.rowCnt,this.colCnt=e.colCnt*r.length,this.cells=this.buildCells()}return e.prototype.buildCells=function(){for(var e=this.rowCnt,r=this.dayTable,t=this.resources,n=[],o=0;o0)break}t.splice(o,0,e)}function J(e){var r=o({},e.extendedProps,e.ui,e);return delete r.ui,delete r.extendedProps,r}var W=r.createPlugin({reducers:[function(e,r,t){var n=g(e.resourceSource,r,e.dateProfile,t),s=C(e.resourceStore,r,n,t),u=function(e,r){var t;switch(r.type){case"INIT":return{};case"SET_RESOURCE_ENTITY_EXPANDED":return o({},e,((t={})[r.id]=r.isExpanded,t));default:return e}}(e.resourceEntityExpansions,r);return o({},e,{resourceSource:n,resourceStore:s,resourceEntityExpansions:u})}],eventDefParsers:[function(e,t,n){var o=r.refineProps(t,_,{},n),s=o.resourceIds;o.resourceId&&s.push(o.resourceId),e.resourceIds=s,e.resourceEditable=o.resourceEditable}],isDraggableTransformers:[function(e,r,t,n){return!(e||!n.viewSpec.class.needsResourceData||!s(r,n.calendar))||e}],eventDragMutationMassagers:[function(e,r,t){var n=r.dateSpan.resourceId,o=t.dateSpan.resourceId;n&&o&&n!==o&&(e.resourceMutation={matchResourceId:n,setResourceId:o})}],eventDefMutationAppliers:[function(e,r,t){var n=r.resourceMutation;if(n&&s(e,t)){var o=e.resourceIds.indexOf(n.matchResourceId);if(-1!==o){var u=e.resourceIds.slice();u.splice(o,1),-1===u.indexOf(n.setResourceId)&&u.push(n.setResourceId),e.resourceIds=u}}}],dateSelectionTransformers:[function(e,r){var t=e.dateSpan.resourceId,n=r.dateSpan.resourceId;if(t&&n)return(!1!==e.component.allowAcrossResources||t===n)&&{resourceId:t}}],datePointTransforms:[function(e,r){return e.resourceId?{resource:r.getResourceById(e.resourceId)}:{}}],dateSpanTransforms:[function(e,r){return e.resourceId?{resource:r.getResourceById(e.resourceId)}:{}}],viewPropsTransformers:[u,a],isPropsValid:function(e,t){var n=(new P).splitProps(o({},e,{resourceStore:t.state.resourceStore}));for(var s in n){var u=n[s];if(s&&n[""]&&(u=o({},u,{eventStore:r.mergeEventStores(n[""].eventStore,u.eventStore),eventUiBases:o({},n[""].eventUiBases,u.eventUiBases)})),!r.isPropsValid(u,t,{resourceId:s},O.bind(null,s)))return!1}return!0},externalDefTransforms:[function(e){return e.resourceId?{resourceId:e.resourceId}:{}}],eventResizeJoinTransforms:[function(e,r){if(!1===e.component.allowAcrossResources&&e.dateSpan.resourceId!==r.dateSpan.resourceId)return!1}],viewContainerModifiers:[function(e,t){var n,o=t.opt("schedulerLicenseKey");n=window.location.href,/\w+\:\/\/fullcalendar\.io\/|\/examples\/[\w-]+\.html$/.test(n)||function(e){if(-1!==F.indexOf(e))return!0;var t=(e||"").match(/^(\d+)\-fcs\-(\d+)$/);if(t&&10===t[1].length){var n=new Date(1e3*parseInt(t[2],10)),o=new Date(r.config.mockSchedulerReleaseDate||T);if(r.isValidDate(o)){var s=r.addDays(o,-D);if(sPlease use a valid license key. More Info')}],eventDropTransformers:[function(e,r){var t=e.resourceMutation;return t?{oldResource:r.getResourceById(t.matchResourceId),newResource:r.getResourceById(t.setResourceId)}:{oldResource:null,newResource:null}}],optionChangeHandlers:A});e.AbstractResourceDayTable=z,e.DayResourceTable=N,e.ResourceApi=w,e.ResourceDayHeader=B,e.ResourceDayTable=M,e.ResourceSplitter=P,e.VResourceJoiner=q,e.VResourceSplitter=k,e.buildResourceFields=J,e.buildResourceTextFunc=H,e.buildRowNodes=L,e.default=W,e.flattenResources=function(e,r){return L(e,[],r,!1,{},!0).map(function(e){return e.resource})},e.isGroupsEqual=function(e,r){return e.spec===r.spec&&e.value===r.value},Object.defineProperty(e,"__esModule",{value:!0})});
/*!
FullCalendar Resource Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
import { memoize, mapHash, DateComponent, parseFieldSpecs, createPlugin } from '@fullcalendar/core';
import ResourceCommonPlugin, { VResourceSplitter, VResourceJoiner, flattenResources, ResourceDayHeader, DayResourceTable, ResourceDayTable } from '@fullcalendar/resource-common';
import TimeGridPlugin, { buildDayRanges, TimeGridSlicer, AbstractTimeGridView, buildDayTable } from '@fullcalendar/timegrid';
import { ResourceDayGrid } from '@fullcalendar/resource-daygrid';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ResourceTimeGrid = /** @class */ (function (_super) {
__extends(ResourceTimeGrid, _super);
function ResourceTimeGrid(context, timeGrid) {
var _this = _super.call(this, context, timeGrid.el) || this;
_this.buildDayRanges = memoize(buildDayRanges);
_this.splitter = new VResourceSplitter();
_this.slicers = {};
_this.joiner = new ResourceTimeGridJoiner();
_this.timeGrid = timeGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeGrid.el
});
return _this;
}
ResourceTimeGrid.prototype.destroy = function () {
this.calendar.unregisterInteractiveComponent(this);
};
ResourceTimeGrid.prototype.render = function (props) {
var _this = this;
var timeGrid = this.timeGrid;
var dateProfile = props.dateProfile, resourceDayTable = props.resourceDayTable;
var dayRanges = this.dayRanges = this.buildDayRanges(resourceDayTable.dayTable, dateProfile, this.dateEnv);
var splitProps = this.splitter.splitProps(props);
this.slicers = mapHash(splitProps, function (split, resourceId) {
return _this.slicers[resourceId] || new TimeGridSlicer();
});
var slicedProps = mapHash(this.slicers, function (slicer, resourceId) {
return slicer.sliceProps(splitProps[resourceId], dateProfile, null, timeGrid, dayRanges);
});
timeGrid.allowAcrossResources = dayRanges.length === 1;
timeGrid.receiveProps(__assign({}, this.joiner.joinProps(slicedProps, resourceDayTable), { dateProfile: dateProfile, cells: resourceDayTable.cells[0] }));
};
ResourceTimeGrid.prototype.renderNowIndicator = function (date) {
var timeGrid = this.timeGrid;
var resourceDayTable = this.props.resourceDayTable;
var nonResourceSegs = this.slicers[''].sliceNowDate(date, timeGrid, this.dayRanges);
var segs = this.joiner.expandSegs(resourceDayTable, nonResourceSegs);
timeGrid.renderNowIndicator(segs, date);
};
ResourceTimeGrid.prototype.buildPositionCaches = function () {
this.timeGrid.buildPositionCaches();
};
ResourceTimeGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.timeGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.timeGrid,
dateSpan: {
range: rawHit.dateSpan.range,
allDay: rawHit.dateSpan.allDay,
resourceId: this.props.resourceDayTable.cells[0][rawHit.col].resource.id
},
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return ResourceTimeGrid;
}(DateComponent));
var ResourceTimeGridJoiner = /** @class */ (function (_super) {
__extends(ResourceTimeGridJoiner, _super);
function ResourceTimeGridJoiner() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceTimeGridJoiner.prototype.transformSeg = function (seg, resourceDayTable, resourceI) {
return [
__assign({}, seg, { col: resourceDayTable.computeCol(seg.col, resourceI) })
];
};
return ResourceTimeGridJoiner;
}(VResourceJoiner));
var ResourceTimeGridView = /** @class */ (function (_super) {
__extends(ResourceTimeGridView, _super);
function ResourceTimeGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.flattenResources = memoize(flattenResources);
_this.buildResourceDayTable = memoize(buildResourceDayTable);
_this.resourceOrderSpecs = parseFieldSpecs(_this.opt('resourceOrder'));
if (_this.opt('columnHeader')) {
_this.header = new ResourceDayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.resourceTimeGrid = new ResourceTimeGrid(context, _this.timeGrid);
if (_this.dayGrid) {
_this.resourceDayGrid = new ResourceDayGrid(context, _this.dayGrid);
}
return _this;
}
ResourceTimeGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.resourceTimeGrid.destroy();
if (this.resourceDayGrid) {
this.resourceDayGrid.destroy();
}
};
ResourceTimeGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var splitProps = this.splitter.splitProps(props);
var resources = this.flattenResources(props.resourceStore, this.resourceOrderSpecs);
var resourceDayTable = this.buildResourceDayTable(this.props.dateProfile, this.dateProfileGenerator, resources, this.opt('datesAboveResources'));
if (this.header) {
this.header.receiveProps({
resources: resources,
dates: resourceDayTable.dayTable.headerDates,
dateProfile: props.dateProfile,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.resourceTimeGrid.receiveProps(__assign({}, splitProps['timed'], { dateProfile: props.dateProfile, resourceDayTable: resourceDayTable }));
if (this.resourceDayGrid) {
this.resourceDayGrid.receiveProps(__assign({}, splitProps['allDay'], { dateProfile: props.dateProfile, resourceDayTable: resourceDayTable, isRigid: false, nextDayThreshold: this.nextDayThreshold }));
}
};
ResourceTimeGridView.prototype.renderNowIndicator = function (date) {
this.resourceTimeGrid.renderNowIndicator(date);
};
ResourceTimeGridView.needsResourceData = true; // for ResourceViewProps
return ResourceTimeGridView;
}(AbstractTimeGridView));
function buildResourceDayTable(dateProfile, dateProfileGenerator, resources, datesAboveResources) {
var dayTable = buildDayTable(dateProfile, dateProfileGenerator);
return datesAboveResources ?
new DayResourceTable(dayTable, resources) :
new ResourceDayTable(dayTable, resources);
}
var main = createPlugin({
deps: [ResourceCommonPlugin, TimeGridPlugin],
defaultView: 'resourceTimeGridDay',
views: {
resourceTimeGrid: {
class: ResourceTimeGridView,
// TODO: wish we didn't have to C&P from timeGrid's file
allDaySlot: true,
slotDuration: '00:30:00',
slotEventOverlap: true // a bad name. confused with overlap/constraint system
},
resourceTimeGridDay: {
type: 'resourceTimeGrid',
duration: { days: 1 }
},
resourceTimeGridWeek: {
type: 'resourceTimeGrid',
duration: { weeks: 1 }
}
}
});
export default main;
export { ResourceTimeGrid, ResourceTimeGridView };
/*!
FullCalendar Resource Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core'), require('@fullcalendar/resource-common'), require('@fullcalendar/timegrid'), require('@fullcalendar/resource-daygrid')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core', '@fullcalendar/resource-common', '@fullcalendar/timegrid', '@fullcalendar/resource-daygrid'], factory) :
(global = global || self, factory(global.FullCalendarResourceTimeGrid = {}, global.FullCalendar, global.FullCalendarResourceCommon, global.FullCalendarTimeGrid, global.FullCalendarResourceDayGrid));
}(this, function (exports, core, ResourceCommonPlugin, TimeGridPlugin, resourceDaygrid) { 'use strict';
var ResourceCommonPlugin__default = 'default' in ResourceCommonPlugin ? ResourceCommonPlugin['default'] : ResourceCommonPlugin;
var TimeGridPlugin__default = 'default' in TimeGridPlugin ? TimeGridPlugin['default'] : TimeGridPlugin;
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ResourceTimeGrid = /** @class */ (function (_super) {
__extends(ResourceTimeGrid, _super);
function ResourceTimeGrid(context, timeGrid) {
var _this = _super.call(this, context, timeGrid.el) || this;
_this.buildDayRanges = core.memoize(TimeGridPlugin.buildDayRanges);
_this.splitter = new ResourceCommonPlugin.VResourceSplitter();
_this.slicers = {};
_this.joiner = new ResourceTimeGridJoiner();
_this.timeGrid = timeGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeGrid.el
});
return _this;
}
ResourceTimeGrid.prototype.destroy = function () {
this.calendar.unregisterInteractiveComponent(this);
};
ResourceTimeGrid.prototype.render = function (props) {
var _this = this;
var timeGrid = this.timeGrid;
var dateProfile = props.dateProfile, resourceDayTable = props.resourceDayTable;
var dayRanges = this.dayRanges = this.buildDayRanges(resourceDayTable.dayTable, dateProfile, this.dateEnv);
var splitProps = this.splitter.splitProps(props);
this.slicers = core.mapHash(splitProps, function (split, resourceId) {
return _this.slicers[resourceId] || new TimeGridPlugin.TimeGridSlicer();
});
var slicedProps = core.mapHash(this.slicers, function (slicer, resourceId) {
return slicer.sliceProps(splitProps[resourceId], dateProfile, null, timeGrid, dayRanges);
});
timeGrid.allowAcrossResources = dayRanges.length === 1;
timeGrid.receiveProps(__assign({}, this.joiner.joinProps(slicedProps, resourceDayTable), { dateProfile: dateProfile, cells: resourceDayTable.cells[0] }));
};
ResourceTimeGrid.prototype.renderNowIndicator = function (date) {
var timeGrid = this.timeGrid;
var resourceDayTable = this.props.resourceDayTable;
var nonResourceSegs = this.slicers[''].sliceNowDate(date, timeGrid, this.dayRanges);
var segs = this.joiner.expandSegs(resourceDayTable, nonResourceSegs);
timeGrid.renderNowIndicator(segs, date);
};
ResourceTimeGrid.prototype.buildPositionCaches = function () {
this.timeGrid.buildPositionCaches();
};
ResourceTimeGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.timeGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.timeGrid,
dateSpan: {
range: rawHit.dateSpan.range,
allDay: rawHit.dateSpan.allDay,
resourceId: this.props.resourceDayTable.cells[0][rawHit.col].resource.id
},
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return ResourceTimeGrid;
}(core.DateComponent));
var ResourceTimeGridJoiner = /** @class */ (function (_super) {
__extends(ResourceTimeGridJoiner, _super);
function ResourceTimeGridJoiner() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceTimeGridJoiner.prototype.transformSeg = function (seg, resourceDayTable, resourceI) {
return [
__assign({}, seg, { col: resourceDayTable.computeCol(seg.col, resourceI) })
];
};
return ResourceTimeGridJoiner;
}(ResourceCommonPlugin.VResourceJoiner));
var ResourceTimeGridView = /** @class */ (function (_super) {
__extends(ResourceTimeGridView, _super);
function ResourceTimeGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.flattenResources = core.memoize(ResourceCommonPlugin.flattenResources);
_this.buildResourceDayTable = core.memoize(buildResourceDayTable);
_this.resourceOrderSpecs = core.parseFieldSpecs(_this.opt('resourceOrder'));
if (_this.opt('columnHeader')) {
_this.header = new ResourceCommonPlugin.ResourceDayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.resourceTimeGrid = new ResourceTimeGrid(context, _this.timeGrid);
if (_this.dayGrid) {
_this.resourceDayGrid = new resourceDaygrid.ResourceDayGrid(context, _this.dayGrid);
}
return _this;
}
ResourceTimeGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.resourceTimeGrid.destroy();
if (this.resourceDayGrid) {
this.resourceDayGrid.destroy();
}
};
ResourceTimeGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var splitProps = this.splitter.splitProps(props);
var resources = this.flattenResources(props.resourceStore, this.resourceOrderSpecs);
var resourceDayTable = this.buildResourceDayTable(this.props.dateProfile, this.dateProfileGenerator, resources, this.opt('datesAboveResources'));
if (this.header) {
this.header.receiveProps({
resources: resources,
dates: resourceDayTable.dayTable.headerDates,
dateProfile: props.dateProfile,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.resourceTimeGrid.receiveProps(__assign({}, splitProps['timed'], { dateProfile: props.dateProfile, resourceDayTable: resourceDayTable }));
if (this.resourceDayGrid) {
this.resourceDayGrid.receiveProps(__assign({}, splitProps['allDay'], { dateProfile: props.dateProfile, resourceDayTable: resourceDayTable, isRigid: false, nextDayThreshold: this.nextDayThreshold }));
}
};
ResourceTimeGridView.prototype.renderNowIndicator = function (date) {
this.resourceTimeGrid.renderNowIndicator(date);
};
ResourceTimeGridView.needsResourceData = true; // for ResourceViewProps
return ResourceTimeGridView;
}(TimeGridPlugin.AbstractTimeGridView));
function buildResourceDayTable(dateProfile, dateProfileGenerator, resources, datesAboveResources) {
var dayTable = TimeGridPlugin.buildDayTable(dateProfile, dateProfileGenerator);
return datesAboveResources ?
new ResourceCommonPlugin.DayResourceTable(dayTable, resources) :
new ResourceCommonPlugin.ResourceDayTable(dayTable, resources);
}
var main = core.createPlugin({
deps: [ResourceCommonPlugin__default, TimeGridPlugin__default],
defaultView: 'resourceTimeGridDay',
views: {
resourceTimeGrid: {
class: ResourceTimeGridView,
// TODO: wish we didn't have to C&P from timeGrid's file
allDaySlot: true,
slotDuration: '00:30:00',
slotEventOverlap: true // a bad name. confused with overlap/constraint system
},
resourceTimeGridDay: {
type: 'resourceTimeGrid',
duration: { days: 1 }
},
resourceTimeGridWeek: {
type: 'resourceTimeGrid',
duration: { weeks: 1 }
}
}
});
exports.ResourceTimeGrid = ResourceTimeGrid;
exports.ResourceTimeGridView = ResourceTimeGridView;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
// ../../../../../@fullcalendar/timegrid
// ../../../../../@fullcalendar/resource-common
// ../../../../../@fullcalendar/resource-daygrid
declare module '@fullcalendar/resource-timegrid' {
import ResourceTimeGridView from '@fullcalendar/resource-timegrid/ResourceTimeGridView';
export { ResourceTimeGridView };
export { default as ResourceTimeGrid } from '@fullcalendar/resource-timegrid/ResourceTimeGrid';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/resource-timegrid/ResourceTimeGridView' {
import { ComponentContext, ViewSpec, DateProfileGenerator } from '@fullcalendar/core';
import { AbstractTimeGridView } from '@fullcalendar/timegrid';
import { ResourceDayHeader, ResourceViewProps } from '@fullcalendar/resource-common';
import { ResourceDayGrid } from '@fullcalendar/resource-daygrid';
import ResourceTimeGrid from '@fullcalendar/resource-timegrid/ResourceTimeGrid';
export { ResourceTimeGridView as default, ResourceTimeGridView };
class ResourceTimeGridView extends AbstractTimeGridView {
static needsResourceData: boolean;
props: ResourceViewProps;
header: ResourceDayHeader;
resourceTimeGrid: ResourceTimeGrid;
resourceDayGrid: ResourceDayGrid;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
destroy(): void;
render(props: ResourceViewProps): void;
renderNowIndicator(date: any): void;
}
}
declare module '@fullcalendar/resource-timegrid/ResourceTimeGrid' {
import { DateSpan, DateComponent, DateProfile, EventStore, EventUiHash, EventInteractionState, ComponentContext, DateMarker, Hit } from '@fullcalendar/core';
import { TimeGrid } from '@fullcalendar/timegrid';
import { AbstractResourceDayTable } from '@fullcalendar/resource-common';
export interface ResourceTimeGridProps {
dateProfile: DateProfile | null;
resourceDayTable: AbstractResourceDayTable;
businessHours: EventStore;
eventStore: EventStore;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
}
export { ResourceTimeGrid as default, ResourceTimeGrid };
class ResourceTimeGrid extends DateComponent {
timeGrid: TimeGrid;
constructor(context: ComponentContext, timeGrid: TimeGrid);
destroy(): void;
render(props: ResourceTimeGridProps): void;
renderNowIndicator(date: DateMarker): void;
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number): Hit;
}
}
/*!
FullCalendar Resource Time Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@fullcalendar/core"),require("@fullcalendar/resource-common"),require("@fullcalendar/timegrid"),require("@fullcalendar/resource-daygrid")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core","@fullcalendar/resource-common","@fullcalendar/timegrid","@fullcalendar/resource-daygrid"],r):r((e=e||self).FullCalendarResourceTimeGrid={},e.FullCalendar,e.FullCalendarResourceCommon,e.FullCalendarTimeGrid,e.FullCalendarResourceDayGrid)}(this,function(e,r,t,i,o){"use strict";var s="default"in t?t.default:t,a="default"in i?i.default:i,n=function(e,r){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])})(e,r)};function l(e,r){function t(){this.constructor=e}n(e,r),e.prototype=null===r?Object.create(r):(t.prototype=r.prototype,new t)}var c=function(){return(c=Object.assign||function(e){for(var r,t=1,i=arguments.length;t
/*!
FullCalendar Resource Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
import { removeElement, Component, memoizeRendering, createElement, htmlToElement, htmlEscape, isArraysEqual, EmitterMixin, memoize, parseFieldSpecs, PositionCache, applyStyleProp, View, createPlugin } from '@fullcalendar/core';
import TimelinePlugin, { TimelineLane, HeaderBodyLayout, TimeAxis, ScrollJoiner, StickyScroller } from '@fullcalendar/timeline';
import ResourceCommonPlugin, { isGroupsEqual, buildResourceFields, buildResourceTextFunc, ResourceApi, ResourceSplitter, buildRowNodes } from '@fullcalendar/resource-common';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var Row = /** @class */ (function (_super) {
__extends(Row, _super);
function Row(context, spreadsheetParent, spreadsheetNextSibling, timeAxisParent, timeAxisNextSibling) {
var _this = _super.call(this, context) || this;
_this.isSizeDirty = false;
spreadsheetParent.insertBefore(_this.spreadsheetTr = document.createElement('tr'), spreadsheetNextSibling);
timeAxisParent.insertBefore(_this.timeAxisTr = document.createElement('tr'), timeAxisNextSibling);
return _this;
}
Row.prototype.destroy = function () {
removeElement(this.spreadsheetTr);
removeElement(this.timeAxisTr);
_super.prototype.destroy.call(this);
};
Row.prototype.updateSize = function (isResize) {
this.isSizeDirty = false;
};
return Row;
}(Component));
function updateExpanderIcon(el, isExpanded) {
var classList = el.classList;
if (isExpanded) {
classList.remove('fc-icon-plus-square');
classList.add('fc-icon-minus-square');
}
else {
classList.remove('fc-icon-minus-square');
classList.add('fc-icon-plus-square');
}
}
function clearExpanderIcon(el) {
var classList = el.classList;
classList.remove('fc-icon-minus-square');
classList.remove('fc-icon-plus-square');
}
function updateTrResourceId(tr, resourceId) {
tr.setAttribute('data-resource-id', resourceId);
}
var GroupRow = /** @class */ (function (_super) {
__extends(GroupRow, _super);
function GroupRow() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this._renderCells = memoizeRendering(_this.renderCells, _this.unrenderCells);
_this._updateExpanderIcon = memoizeRendering(_this.updateExpanderIcon, null, [_this._renderCells]);
_this.onExpanderClick = function (ev) {
var props = _this.props;
_this.calendar.dispatch({
type: 'SET_RESOURCE_ENTITY_EXPANDED',
id: props.id,
isExpanded: !props.isExpanded
});
};
return _this;
}
GroupRow.prototype.render = function (props) {
this._renderCells(props.group, props.spreadsheetColCnt);
this._updateExpanderIcon(props.isExpanded);
this.isSizeDirty = true;
};
GroupRow.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderCells.unrender(); // should unrender everything else
};
GroupRow.prototype.renderCells = function (group, spreadsheetColCnt) {
var spreadsheetContentEl = this.renderSpreadsheetContent(group);
this.spreadsheetTr.appendChild(createElement('td', {
className: 'fc-divider',
colSpan: spreadsheetColCnt // span across all columns
}, this.spreadsheetHeightEl = createElement('div', null, spreadsheetContentEl)) // needed by setTrInnerHeight
);
this.expanderIconEl = spreadsheetContentEl.querySelector('.fc-icon');
this.expanderIconEl.parentElement.addEventListener('click', this.onExpanderClick);
// insert a single cell, with a single empty
var SpreadsheetRow = /** @class */ (function (_super) {
__extends(SpreadsheetRow, _super);
function SpreadsheetRow(context, tr) {
var _this = _super.call(this, context) || this;
_this._renderRow = memoizeRendering(_this.renderRow, _this.unrenderRow);
_this._updateTrResourceId = memoizeRendering(updateTrResourceId, null, [_this._renderRow]);
_this._updateExpanderIcon = memoizeRendering(_this.updateExpanderIcon, null, [_this._renderRow]);
_this.onExpanderClick = function (ev) {
var props = _this.props;
_this.calendar.dispatch({
type: 'SET_RESOURCE_ENTITY_EXPANDED',
id: props.id,
isExpanded: !props.isExpanded
});
};
_this.tr = tr;
return _this;
}
SpreadsheetRow.prototype.render = function (props) {
this._renderRow(props.resource, props.rowSpans, props.depth, props.colSpecs);
this._updateTrResourceId(this.tr, props.resource.id); // TODO: only use public ID?
this._updateExpanderIcon(props.hasChildren, props.isExpanded);
};
SpreadsheetRow.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderRow.unrender(); // should unrender everything else
};
SpreadsheetRow.prototype.renderRow = function (resource, rowSpans, depth, colSpecs) {
var _a = this, tr = _a.tr, theme = _a.theme, calendar = _a.calendar, view = _a.view;
var resourceFields = buildResourceFields(resource); // slightly inefficient. already done up the call stack
var mainTd;
for (var i = 0; i < colSpecs.length; i++) {
var colSpec = colSpecs[i];
var rowSpan = rowSpans[i];
if (rowSpan === 0) { // not responsible for group-based rows. VRowGroup is
continue;
}
else if (rowSpan == null) {
rowSpan = 1;
}
var text = void 0;
if (colSpec.field) {
text = resourceFields[colSpec.field];
}
else {
text = buildResourceTextFunc(colSpec.text, calendar)(resource);
}
var contentEl = htmlToElement('
var ResourceRow = /** @class */ (function (_super) {
__extends(ResourceRow, _super);
function ResourceRow(context, a, b, c, d, timeAxis) {
var _this = _super.call(this, context, a, b, c, d) || this;
_this._updateTrResourceId = memoizeRendering(updateTrResourceId);
_this.spreadsheetRow = new SpreadsheetRow(context, _this.spreadsheetTr);
_this.timeAxisTr.appendChild(createElement('td', { className: _this.theme.getClass('widgetContent') }, _this.innerContainerEl = document.createElement('div')));
_this.lane = new TimelineLane(context, _this.innerContainerEl, _this.innerContainerEl, timeAxis);
return _this;
}
ResourceRow.prototype.destroy = function () {
this.spreadsheetRow.destroy();
this.lane.destroy();
_super.prototype.destroy.call(this);
};
ResourceRow.prototype.render = function (props) {
// spreadsheetRow handles calling updateTrResourceId for spreadsheetTr
this.spreadsheetRow.receiveProps({
colSpecs: props.colSpecs,
id: props.id,
rowSpans: props.rowSpans,
depth: props.depth,
isExpanded: props.isExpanded,
hasChildren: props.hasChildren,
resource: props.resource
});
this._updateTrResourceId(this.timeAxisTr, props.resource.id);
this.lane.receiveProps({
dateProfile: props.dateProfile,
nextDayThreshold: props.nextDayThreshold,
businessHours: props.businessHours,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize
});
this.isSizeDirty = true;
};
ResourceRow.prototype.updateSize = function (isResize) {
_super.prototype.updateSize.call(this, isResize);
this.lane.updateSize(isResize);
};
ResourceRow.prototype.getHeightEls = function () {
return [this.spreadsheetRow.heightEl, this.innerContainerEl];
};
return ResourceRow;
}(Row));
ResourceRow.addEqualityFuncs({
rowSpans: isArraysEqual // HACK for isSizeDirty, ResourceTimelineView::renderRows
});
var COL_MIN_WIDTH = 30;
var SpreadsheetHeader = /** @class */ (function (_super) {
__extends(SpreadsheetHeader, _super);
function SpreadsheetHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
_this.resizables = [];
_this.colWidths = [];
_this.emitter = new EmitterMixin();
parentEl.appendChild(_this.tableEl = createElement('table', {
className: _this.theme.getClass('tableGrid')
}));
return _this;
}
SpreadsheetHeader.prototype.destroy = function () {
for (var _i = 0, _a = this.resizables; _i < _a.length; _i++) {
var resizable = _a[_i];
resizable.destroy();
}
removeElement(this.tableEl);
_super.prototype.destroy.call(this);
};
SpreadsheetHeader.prototype.render = function (props) {
var theme = this.theme;
var colSpecs = props.colSpecs;
var html = '' + props.colTags + '' +
'';
if (props.superHeaderText) {
html +=
'' +
'' +
'
var Spreadsheet = /** @class */ (function (_super) {
__extends(Spreadsheet, _super);
function Spreadsheet(context, headParentEl, bodyParentEl) {
var _this = _super.call(this, context) || this;
_this._renderCells = memoizeRendering(_this.renderCells, _this.unrenderCells);
_this.layout = new HeaderBodyLayout(headParentEl, bodyParentEl, 'clipped-scroll');
var headerEnhancedScroller = _this.layout.headerScroller.enhancedScroll;
var bodyEnhancedScroller = _this.layout.bodyScroller.enhancedScroll;
_this.header = new SpreadsheetHeader(context, headerEnhancedScroller.canvas.contentEl);
_this.header.emitter.on('colwidthchange', function (colWidths) {
_this.applyColWidths(colWidths);
});
bodyEnhancedScroller.canvas.contentEl
.appendChild(_this.bodyContainerEl = createElement('div', { className: 'fc-rows' }, '' +
'' +
'' +
''));
_this.bodyColGroup = _this.bodyContainerEl.querySelector('colgroup');
_this.bodyTbody = _this.bodyContainerEl.querySelector('tbody');
return _this;
}
Spreadsheet.prototype.destroy = function () {
this.header.destroy();
this.layout.destroy();
this._renderCells.unrender();
_super.prototype.destroy.call(this);
};
Spreadsheet.prototype.render = function (props) {
this._renderCells(props.superHeaderText, props.colSpecs);
};
Spreadsheet.prototype.renderCells = function (superHeaderText, colSpecs) {
var colTags = this.renderColTags(colSpecs);
this.header.receiveProps({
superHeaderText: superHeaderText,
colSpecs: colSpecs,
colTags: colTags
});
this.bodyColGroup.innerHTML = colTags;
this.bodyColEls = Array.prototype.slice.call(this.bodyColGroup.querySelectorAll('col'));
this.applyColWidths(colSpecs.map(function (colSpec) { return colSpec.width; }));
};
Spreadsheet.prototype.unrenderCells = function () {
this.bodyColGroup.innerHTML = '';
};
Spreadsheet.prototype.renderColTags = function (colSpecs) {
var html = '';
for (var _i = 0, colSpecs_1 = colSpecs; _i < colSpecs_1.length; _i++) {
var o = colSpecs_1[_i];
if (o.isMain) {
html += '';
}
else {
html += '';
}
}
return html;
};
Spreadsheet.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.layout.setHeight(totalHeight, isAuto);
};
Spreadsheet.prototype.applyColWidths = function (colWidths) {
var _this = this;
colWidths.forEach(function (colWidth, colIndex) {
var headEl = _this.header.colEls[colIndex]; // bad to access child
var bodyEl = _this.bodyColEls[colIndex];
var styleVal;
if (typeof colWidth === 'number') {
styleVal = colWidth + 'px';
}
else if (typeof colWidth == null) {
styleVal = '';
}
headEl.style.width = bodyEl.style.width = styleVal;
});
};
return Spreadsheet;
}(Component));
var MIN_RESOURCE_AREA_WIDTH = 30; // definitely bigger than scrollbars
var ResourceTimelineView = /** @class */ (function (_super) {
__extends(ResourceTimelineView, _super);
function ResourceTimelineView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.isStickyScrollDirty = false;
_this.rowNodes = [];
_this.rowComponents = [];
_this.rowComponentsById = {};
_this.resourceAreaWidthDraggings = [];
_this.splitter = new ResourceSplitter(); // doesn't let it do businessHours tho
_this.hasResourceBusinessHours = memoize(hasResourceBusinessHours);
_this.buildRowNodes = memoize(buildRowNodes);
_this.hasNesting = memoize(hasNesting);
_this._updateHasNesting = memoizeRendering(_this.updateHasNesting);
var allColSpecs = _this.opt('resourceColumns') || [];
var labelText = _this.opt('resourceLabelText'); // TODO: view.override
var defaultLabelText = 'Resources'; // TODO: view.defaults
var superHeaderText = null;
if (!allColSpecs.length) {
allColSpecs.push({
labelText: labelText || defaultLabelText,
text: buildResourceTextFunc(_this.opt('resourceText'), _this.calendar)
});
}
else {
superHeaderText = labelText;
}
var plainColSpecs = [];
var groupColSpecs = [];
var groupSpecs = [];
var isVGrouping = false;
var isHGrouping = false;
for (var _i = 0, allColSpecs_1 = allColSpecs; _i < allColSpecs_1.length; _i++) {
var colSpec = allColSpecs_1[_i];
if (colSpec.group) {
groupColSpecs.push(colSpec);
}
else {
plainColSpecs.push(colSpec);
}
}
plainColSpecs[0].isMain = true;
if (groupColSpecs.length) {
groupSpecs = groupColSpecs;
isVGrouping = true;
}
else {
var hGroupField = _this.opt('resourceGroupField');
if (hGroupField) {
isHGrouping = true;
groupSpecs.push({
field: hGroupField,
text: _this.opt('resourceGroupText'),
render: _this.opt('resourceGroupRender')
});
}
}
var allOrderSpecs = parseFieldSpecs(_this.opt('resourceOrder'));
var plainOrderSpecs = [];
for (var _a = 0, allOrderSpecs_1 = allOrderSpecs; _a < allOrderSpecs_1.length; _a++) {
var orderSpec = allOrderSpecs_1[_a];
var isGroup = false;
for (var _b = 0, groupSpecs_1 = groupSpecs; _b < groupSpecs_1.length; _b++) {
var groupSpec = groupSpecs_1[_b];
if (groupSpec.field === orderSpec.field) {
groupSpec.order = orderSpec.order; // -1, 0, 1
isGroup = true;
break;
}
}
if (!isGroup) {
plainOrderSpecs.push(orderSpec);
}
}
_this.superHeaderText = superHeaderText;
_this.isVGrouping = isVGrouping;
_this.isHGrouping = isHGrouping;
_this.groupSpecs = groupSpecs;
_this.colSpecs = groupColSpecs.concat(plainColSpecs);
_this.orderSpecs = plainOrderSpecs;
// START RENDERING...
_this.el.classList.add('fc-timeline');
if (_this.opt('eventOverlap') === false) {
_this.el.classList.add('fc-no-overlap');
}
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.resourceAreaHeadEl = _this.el.querySelector('thead .fc-resource-area');
_this.setResourceAreaWidth(_this.opt('resourceAreaWidth'));
_this.initResourceAreaWidthDragging();
_this.miscHeight = _this.el.getBoundingClientRect().height;
_this.spreadsheet = new Spreadsheet(_this.context, _this.resourceAreaHeadEl, _this.el.querySelector('tbody .fc-resource-area'));
_this.timeAxis = new TimeAxis(_this.context, _this.el.querySelector('thead .fc-time-area'), _this.el.querySelector('tbody .fc-time-area'));
var timeAxisRowContainer = createElement('div', { className: 'fc-rows' }, '');
_this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.contentEl.appendChild(timeAxisRowContainer);
_this.timeAxisTbody = timeAxisRowContainer.querySelector('tbody');
_this.lane = new TimelineLane(_this.context, null, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.bgEl, _this.timeAxis);
_this.bodyScrollJoiner = new ScrollJoiner('vertical', [
_this.spreadsheet.layout.bodyScroller,
_this.timeAxis.layout.bodyScroller
]);
// after scrolljoiner
_this.spreadsheetBodyStickyScroller = new StickyScroller(_this.spreadsheet.layout.bodyScroller.enhancedScroll, _this.isRtl, true // isVertical
);
_this.spreadsheet.receiveProps({
superHeaderText: _this.superHeaderText,
colSpecs: _this.colSpecs
});
// Component...
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeAxis.slats.el
});
return _this;
}
ResourceTimelineView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return " ";
};
ResourceTimelineView.prototype.render = function (props) {
_super.prototype.render.call(this, props);
var splitProps = this.splitter.splitProps(props);
var hasResourceBusinessHours = this.hasResourceBusinessHours(props.resourceStore);
this.timeAxis.receiveProps({
dateProfile: props.dateProfile
});
// for all-resource bg events / selections / business-hours
this.lane.receiveProps(__assign({}, splitProps[''], { dateProfile: props.dateProfile, nextDayThreshold: this.nextDayThreshold, businessHours: hasResourceBusinessHours ? null : props.businessHours }));
var newRowNodes = this.buildRowNodes(props.resourceStore, this.groupSpecs, this.orderSpecs, this.isVGrouping, props.resourceEntityExpansions, this.opt('resourcesInitiallyExpanded'));
this._updateHasNesting(this.hasNesting(newRowNodes));
this.diffRows(newRowNodes);
this.renderRows(props.dateProfile, hasResourceBusinessHours ? props.businessHours : null, // CONFUSING, comment
splitProps);
};
ResourceTimelineView.prototype.updateHasNesting = function (isNesting) {
var classList = this.el.classList;
if (isNesting) {
classList.remove('fc-flat');
}
else {
classList.add('fc-flat');
}
};
ResourceTimelineView.prototype.diffRows = function (newNodes) {
var oldNodes = this.rowNodes;
var oldLen = oldNodes.length;
var oldIndexHash = {}; // id -> index
var oldI = 0;
var newI = 0;
for (oldI = 0; oldI < oldLen; oldI++) {
oldIndexHash[oldNodes[oldI].id] = oldI;
}
// iterate new nodes
for (oldI = 0, newI = 0; newI < newNodes.length; newI++) {
var newNode = newNodes[newI];
var oldIFound = oldIndexHash[newNode.id];
if (oldIFound != null && oldIFound >= oldI) {
this.removeRows(newI, oldIFound - oldI, oldNodes); // won't do anything if same index
oldI = oldIFound + 1;
}
else {
this.addRow(newI, newNode);
}
}
// old rows that weren't found need to be removed
this.removeRows(newI, oldLen - oldI, oldNodes); // won't do anything if same index
this.rowNodes = newNodes;
};
/*
rowComponents is the in-progress result
*/
ResourceTimelineView.prototype.addRow = function (index, rowNode) {
var _a = this, rowComponents = _a.rowComponents, rowComponentsById = _a.rowComponentsById;
var nextComponent = rowComponents[index];
var newComponent = this.buildChildComponent(rowNode, this.spreadsheet.bodyTbody, nextComponent ? nextComponent.spreadsheetTr : null, this.timeAxisTbody, nextComponent ? nextComponent.timeAxisTr : null);
rowComponents.splice(index, 0, newComponent);
rowComponentsById[rowNode.id] = newComponent;
};
ResourceTimelineView.prototype.removeRows = function (startIndex, len, oldRowNodes) {
if (len) {
var _a = this, rowComponents = _a.rowComponents, rowComponentsById = _a.rowComponentsById;
for (var i = 0; i < len; i++) {
var rowComponent = rowComponents[startIndex + i];
rowComponent.destroy();
delete rowComponentsById[oldRowNodes[i].id];
}
rowComponents.splice(startIndex, len);
}
};
ResourceTimelineView.prototype.buildChildComponent = function (node, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext) {
if (node.group) {
return new GroupRow(this.context, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext);
}
else if (node.resource) {
return new ResourceRow(this.context, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext, this.timeAxis);
}
};
ResourceTimelineView.prototype.renderRows = function (dateProfile, fallbackBusinessHours, splitProps) {
var _a = this, rowNodes = _a.rowNodes, rowComponents = _a.rowComponents;
for (var i = 0; i < rowNodes.length; i++) {
var rowNode = rowNodes[i];
var rowComponent = rowComponents[i];
if (rowNode.group) {
rowComponent.receiveProps({
spreadsheetColCnt: this.colSpecs.length,
id: rowNode.id,
isExpanded: rowNode.isExpanded,
group: rowNode.group
});
}
else {
var resource = rowNode.resource;
rowComponent.receiveProps(__assign({}, splitProps[resource.id], { dateProfile: dateProfile, nextDayThreshold: this.nextDayThreshold, businessHours: resource.businessHours || fallbackBusinessHours, colSpecs: this.colSpecs, id: rowNode.id, rowSpans: rowNode.rowSpans, depth: rowNode.depth, isExpanded: rowNode.isExpanded, hasChildren: rowNode.hasChildren, resource: rowNode.resource }));
}
}
};
ResourceTimelineView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
// FYI: this ordering is really important
var calendar = this.calendar;
var isBaseSizing = isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated;
if (isBaseSizing) {
this.syncHeadHeights();
this.timeAxis.updateSize(isResize, viewHeight - this.miscHeight, isAuto);
this.spreadsheet.updateSize(isResize, viewHeight - this.miscHeight, isAuto);
}
var rowSizingCnt = this.updateRowSizes(isResize);
this.lane.updateSize(isResize); // is efficient. uses flags
if (isBaseSizing || rowSizingCnt) {
this.bodyScrollJoiner.update();
this.timeAxis.layout.scrollJoiner.update(); // hack
this.rowPositions = new PositionCache(this.timeAxis.slats.el, this.rowComponents.map(function (rowComponent) {
return rowComponent.timeAxisTr;
}), false, // isHorizontal
true // isVertical
);
this.rowPositions.build();
this.isStickyScrollDirty = true;
}
};
ResourceTimelineView.prototype.syncHeadHeights = function () {
var spreadsheetHeadEl = this.spreadsheet.header.tableEl;
var timeAxisHeadEl = this.timeAxis.header.tableEl;
spreadsheetHeadEl.style.height = '';
timeAxisHeadEl.style.height = '';
var max = Math.max(spreadsheetHeadEl.getBoundingClientRect().height, timeAxisHeadEl.getBoundingClientRect().height);
spreadsheetHeadEl.style.height =
timeAxisHeadEl.style.height = max + 'px';
};
ResourceTimelineView.prototype.updateRowSizes = function (isResize) {
var dirtyRowComponents = this.rowComponents;
if (!isResize) {
dirtyRowComponents = dirtyRowComponents.filter(function (rowComponent) {
return rowComponent.isSizeDirty;
});
}
var elArrays = dirtyRowComponents.map(function (rowComponent) {
return rowComponent.getHeightEls();
});
// reset to natural heights
for (var _i = 0, elArrays_1 = elArrays; _i < elArrays_1.length; _i++) {
var elArray = elArrays_1[_i];
for (var _a = 0, elArray_1 = elArray; _a < elArray_1.length; _a++) {
var el = elArray_1[_a];
el.style.height = '';
}
}
// let rows update their contents' heights
for (var _b = 0, dirtyRowComponents_1 = dirtyRowComponents; _b < dirtyRowComponents_1.length; _b++) {
var rowComponent = dirtyRowComponents_1[_b];
rowComponent.updateSize(isResize); // will reset isSizeDirty
}
var maxHeights = elArrays.map(function (elArray) {
var maxHeight = null;
for (var _i = 0, elArray_2 = elArray; _i < elArray_2.length; _i++) {
var el = elArray_2[_i];
var height = el.getBoundingClientRect().height;
if (maxHeight === null || height > maxHeight) {
maxHeight = height;
}
}
return maxHeight;
});
for (var i = 0; i < elArrays.length; i++) {
for (var _c = 0, _d = elArrays[i]; _c < _d.length; _c++) {
var el = _d[_c];
el.style.height = maxHeights[i] + 'px';
}
}
return dirtyRowComponents.length;
};
ResourceTimelineView.prototype.destroy = function () {
for (var _i = 0, _a = this.rowComponents; _i < _a.length; _i++) {
var rowComponent = _a[_i];
rowComponent.destroy();
}
this.rowNodes = [];
this.rowComponents = [];
this.spreadsheet.destroy();
this.timeAxis.destroy();
for (var _b = 0, _c = this.resourceAreaWidthDraggings; _b < _c.length; _b++) {
var resourceAreaWidthDragging = _c[_b];
resourceAreaWidthDragging.destroy();
}
this.spreadsheetBodyStickyScroller.destroy();
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
// Now Indicator
// ------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.getNowIndicatorUnit = function (dateProfile) {
return this.timeAxis.getNowIndicatorUnit(dateProfile);
};
ResourceTimelineView.prototype.renderNowIndicator = function (date) {
this.timeAxis.renderNowIndicator(date);
};
ResourceTimelineView.prototype.unrenderNowIndicator = function () {
this.timeAxis.unrenderNowIndicator();
};
// Scrolling
// ------------------------------------------------------------------------------------------------------------------
// this is useful for scrolling prev/next dates while resource is scrolled down
ResourceTimelineView.prototype.queryScroll = function () {
var scroll = _super.prototype.queryScroll.call(this);
if (this.props.resourceStore) {
__assign(scroll, this.queryResourceScroll());
}
return scroll;
};
ResourceTimelineView.prototype.applyScroll = function (scroll, isResize) {
_super.prototype.applyScroll.call(this, scroll, isResize);
if (this.props.resourceStore) {
this.applyResourceScroll(scroll);
}
// avoid updating stickyscroll too often
if (isResize || this.isStickyScrollDirty) {
this.isStickyScrollDirty = false;
this.spreadsheetBodyStickyScroller.updateSize();
this.timeAxis.updateStickyScrollers();
}
};
ResourceTimelineView.prototype.computeDateScroll = function (duration) {
return this.timeAxis.computeDateScroll(duration);
};
ResourceTimelineView.prototype.queryDateScroll = function () {
return this.timeAxis.queryDateScroll();
};
ResourceTimelineView.prototype.applyDateScroll = function (scroll) {
this.timeAxis.applyDateScroll(scroll);
};
ResourceTimelineView.prototype.queryResourceScroll = function () {
var _a = this, rowComponents = _a.rowComponents, rowNodes = _a.rowNodes;
var scroll = {};
var scrollerTop = this.timeAxis.layout.bodyScroller.el.getBoundingClientRect().top; // fixed position
for (var i = 0; i < rowComponents.length; i++) {
var rowComponent = rowComponents[i];
var rowNode = rowNodes[i];
var el = rowComponent.timeAxisTr;
var elBottom = el.getBoundingClientRect().bottom; // fixed position
if (elBottom > scrollerTop) {
scroll.rowId = rowNode.id;
scroll.bottom = elBottom - scrollerTop;
break;
}
}
// TODO: what about left scroll state for spreadsheet area?
return scroll;
};
ResourceTimelineView.prototype.applyResourceScroll = function (scroll) {
var rowId = scroll.forcedRowId || scroll.rowId;
if (rowId) {
var rowComponent = this.rowComponentsById[rowId];
if (rowComponent) {
var el = rowComponent.timeAxisTr;
if (el) {
var innerTop = this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.el.getBoundingClientRect().top;
var rowRect = el.getBoundingClientRect();
var scrollTop = (scroll.forcedRowId ?
rowRect.top : // just use top edge
rowRect.bottom - scroll.bottom) - // pixels from bottom edge
innerTop;
this.timeAxis.layout.bodyScroller.enhancedScroll.setScrollTop(scrollTop);
this.spreadsheet.layout.bodyScroller.enhancedScroll.setScrollTop(scrollTop);
}
}
}
};
// TODO: scrollToResource
// Hit System
// ------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.buildPositionCaches = function () {
this.timeAxis.slats.updateSize();
this.rowPositions.build();
};
ResourceTimelineView.prototype.queryHit = function (positionLeft, positionTop) {
var rowPositions = this.rowPositions;
var slats = this.timeAxis.slats;
var rowIndex = rowPositions.topToIndex(positionTop);
if (rowIndex != null) {
var resource = this.rowNodes[rowIndex].resource;
if (resource) { // not a group
var slatHit = slats.positionToHit(positionLeft);
if (slatHit) {
return {
component: this,
dateSpan: {
range: slatHit.dateSpan.range,
allDay: slatHit.dateSpan.allDay,
resourceId: resource.id
},
rect: {
left: slatHit.left,
right: slatHit.right,
top: rowPositions.tops[rowIndex],
bottom: rowPositions.bottoms[rowIndex]
},
dayEl: slatHit.dayEl,
layer: 0
};
}
}
}
};
// Resource Area
// ------------------------------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.setResourceAreaWidth = function (widthVal) {
this.resourceAreaWidth = widthVal;
applyStyleProp(this.resourceAreaHeadEl, 'width', widthVal || '');
};
ResourceTimelineView.prototype.initResourceAreaWidthDragging = function () {
var _this = this;
var resourceAreaDividerEls = Array.prototype.slice.call(this.el.querySelectorAll('.fc-col-resizer'));
var ElementDraggingImpl = this.calendar.pluginSystem.hooks.elementDraggingImpl;
if (ElementDraggingImpl) {
this.resourceAreaWidthDraggings = resourceAreaDividerEls.map(function (el) {
var dragging = new ElementDraggingImpl(el);
var dragStartWidth;
var viewWidth;
dragging.emitter.on('dragstart', function () {
dragStartWidth = _this.resourceAreaWidth;
if (typeof dragStartWidth !== 'number') {
dragStartWidth = _this.resourceAreaHeadEl.getBoundingClientRect().width;
}
viewWidth = _this.el.getBoundingClientRect().width;
});
dragging.emitter.on('dragmove', function (pev) {
var newWidth = dragStartWidth + pev.deltaX * (_this.isRtl ? -1 : 1);
newWidth = Math.max(newWidth, MIN_RESOURCE_AREA_WIDTH);
newWidth = Math.min(newWidth, viewWidth - MIN_RESOURCE_AREA_WIDTH);
_this.setResourceAreaWidth(newWidth);
});
dragging.setAutoScrollEnabled(false); // because gets weird with auto-scrolling time area
return dragging;
});
}
};
ResourceTimelineView.needsResourceData = true; // for ResourceViewProps
return ResourceTimelineView;
}(View));
function hasResourceBusinessHours(resourceStore) {
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
if (resource.businessHours) {
return true;
}
}
return false;
}
function hasNesting(nodes) {
for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) {
var node = nodes_1[_i];
if (node.group) {
return true;
}
else if (node.resource) {
if (node.hasChildren) {
return true;
}
}
}
return false;
}
var main = createPlugin({
deps: [ResourceCommonPlugin, TimelinePlugin],
defaultView: 'resourceTimelineDay',
views: {
resourceTimeline: {
class: ResourceTimelineView,
resourceAreaWidth: '30%',
resourcesInitiallyExpanded: true,
eventResizableFromStart: true // TODO: not DRY with this same setting in the main timeline config
},
resourceTimelineDay: {
type: 'resourceTimeline',
duration: { days: 1 }
},
resourceTimelineWeek: {
type: 'resourceTimeline',
duration: { weeks: 1 }
},
resourceTimelineMonth: {
type: 'resourceTimeline',
duration: { months: 1 }
},
resourceTimelineYear: {
type: 'resourceTimeline',
duration: { years: 1 }
}
}
});
export default main;
export { ResourceTimelineView };
/*!
FullCalendar Resource Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core'), require('@fullcalendar/timeline'), require('@fullcalendar/resource-common')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core', '@fullcalendar/timeline', '@fullcalendar/resource-common'], factory) :
(global = global || self, factory(global.FullCalendarResourceTimeline = {}, global.FullCalendar, global.FullCalendarTimeline, global.FullCalendarResourceCommon));
}(this, function (exports, core, TimelinePlugin, ResourceCommonPlugin) { 'use strict';
var TimelinePlugin__default = 'default' in TimelinePlugin ? TimelinePlugin['default'] : TimelinePlugin;
var ResourceCommonPlugin__default = 'default' in ResourceCommonPlugin ? ResourceCommonPlugin['default'] : ResourceCommonPlugin;
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var Row = /** @class */ (function (_super) {
__extends(Row, _super);
function Row(context, spreadsheetParent, spreadsheetNextSibling, timeAxisParent, timeAxisNextSibling) {
var _this = _super.call(this, context) || this;
_this.isSizeDirty = false;
spreadsheetParent.insertBefore(_this.spreadsheetTr = document.createElement('tr'), spreadsheetNextSibling);
timeAxisParent.insertBefore(_this.timeAxisTr = document.createElement('tr'), timeAxisNextSibling);
return _this;
}
Row.prototype.destroy = function () {
core.removeElement(this.spreadsheetTr);
core.removeElement(this.timeAxisTr);
_super.prototype.destroy.call(this);
};
Row.prototype.updateSize = function (isResize) {
this.isSizeDirty = false;
};
return Row;
}(core.Component));
function updateExpanderIcon(el, isExpanded) {
var classList = el.classList;
if (isExpanded) {
classList.remove('fc-icon-plus-square');
classList.add('fc-icon-minus-square');
}
else {
classList.remove('fc-icon-minus-square');
classList.add('fc-icon-plus-square');
}
}
function clearExpanderIcon(el) {
var classList = el.classList;
classList.remove('fc-icon-minus-square');
classList.remove('fc-icon-plus-square');
}
function updateTrResourceId(tr, resourceId) {
tr.setAttribute('data-resource-id', resourceId);
}
var GroupRow = /** @class */ (function (_super) {
__extends(GroupRow, _super);
function GroupRow() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this._renderCells = core.memoizeRendering(_this.renderCells, _this.unrenderCells);
_this._updateExpanderIcon = core.memoizeRendering(_this.updateExpanderIcon, null, [_this._renderCells]);
_this.onExpanderClick = function (ev) {
var props = _this.props;
_this.calendar.dispatch({
type: 'SET_RESOURCE_ENTITY_EXPANDED',
id: props.id,
isExpanded: !props.isExpanded
});
};
return _this;
}
GroupRow.prototype.render = function (props) {
this._renderCells(props.group, props.spreadsheetColCnt);
this._updateExpanderIcon(props.isExpanded);
this.isSizeDirty = true;
};
GroupRow.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderCells.unrender(); // should unrender everything else
};
GroupRow.prototype.renderCells = function (group, spreadsheetColCnt) {
var spreadsheetContentEl = this.renderSpreadsheetContent(group);
this.spreadsheetTr.appendChild(core.createElement('td', {
className: 'fc-divider',
colSpan: spreadsheetColCnt // span across all columns
}, this.spreadsheetHeightEl = core.createElement('div', null, spreadsheetContentEl)) // needed by setTrInnerHeight
);
this.expanderIconEl = spreadsheetContentEl.querySelector('.fc-icon');
this.expanderIconEl.parentElement.addEventListener('click', this.onExpanderClick);
// insert a single cell, with a single empty
var SpreadsheetRow = /** @class */ (function (_super) {
__extends(SpreadsheetRow, _super);
function SpreadsheetRow(context, tr) {
var _this = _super.call(this, context) || this;
_this._renderRow = core.memoizeRendering(_this.renderRow, _this.unrenderRow);
_this._updateTrResourceId = core.memoizeRendering(updateTrResourceId, null, [_this._renderRow]);
_this._updateExpanderIcon = core.memoizeRendering(_this.updateExpanderIcon, null, [_this._renderRow]);
_this.onExpanderClick = function (ev) {
var props = _this.props;
_this.calendar.dispatch({
type: 'SET_RESOURCE_ENTITY_EXPANDED',
id: props.id,
isExpanded: !props.isExpanded
});
};
_this.tr = tr;
return _this;
}
SpreadsheetRow.prototype.render = function (props) {
this._renderRow(props.resource, props.rowSpans, props.depth, props.colSpecs);
this._updateTrResourceId(this.tr, props.resource.id); // TODO: only use public ID?
this._updateExpanderIcon(props.hasChildren, props.isExpanded);
};
SpreadsheetRow.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this._renderRow.unrender(); // should unrender everything else
};
SpreadsheetRow.prototype.renderRow = function (resource, rowSpans, depth, colSpecs) {
var _a = this, tr = _a.tr, theme = _a.theme, calendar = _a.calendar, view = _a.view;
var resourceFields = ResourceCommonPlugin.buildResourceFields(resource); // slightly inefficient. already done up the call stack
var mainTd;
for (var i = 0; i < colSpecs.length; i++) {
var colSpec = colSpecs[i];
var rowSpan = rowSpans[i];
if (rowSpan === 0) { // not responsible for group-based rows. VRowGroup is
continue;
}
else if (rowSpan == null) {
rowSpan = 1;
}
var text = void 0;
if (colSpec.field) {
text = resourceFields[colSpec.field];
}
else {
text = ResourceCommonPlugin.buildResourceTextFunc(colSpec.text, calendar)(resource);
}
var contentEl = core.htmlToElement('
var ResourceRow = /** @class */ (function (_super) {
__extends(ResourceRow, _super);
function ResourceRow(context, a, b, c, d, timeAxis) {
var _this = _super.call(this, context, a, b, c, d) || this;
_this._updateTrResourceId = core.memoizeRendering(updateTrResourceId);
_this.spreadsheetRow = new SpreadsheetRow(context, _this.spreadsheetTr);
_this.timeAxisTr.appendChild(core.createElement('td', { className: _this.theme.getClass('widgetContent') }, _this.innerContainerEl = document.createElement('div')));
_this.lane = new TimelinePlugin.TimelineLane(context, _this.innerContainerEl, _this.innerContainerEl, timeAxis);
return _this;
}
ResourceRow.prototype.destroy = function () {
this.spreadsheetRow.destroy();
this.lane.destroy();
_super.prototype.destroy.call(this);
};
ResourceRow.prototype.render = function (props) {
// spreadsheetRow handles calling updateTrResourceId for spreadsheetTr
this.spreadsheetRow.receiveProps({
colSpecs: props.colSpecs,
id: props.id,
rowSpans: props.rowSpans,
depth: props.depth,
isExpanded: props.isExpanded,
hasChildren: props.hasChildren,
resource: props.resource
});
this._updateTrResourceId(this.timeAxisTr, props.resource.id);
this.lane.receiveProps({
dateProfile: props.dateProfile,
nextDayThreshold: props.nextDayThreshold,
businessHours: props.businessHours,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize
});
this.isSizeDirty = true;
};
ResourceRow.prototype.updateSize = function (isResize) {
_super.prototype.updateSize.call(this, isResize);
this.lane.updateSize(isResize);
};
ResourceRow.prototype.getHeightEls = function () {
return [this.spreadsheetRow.heightEl, this.innerContainerEl];
};
return ResourceRow;
}(Row));
ResourceRow.addEqualityFuncs({
rowSpans: core.isArraysEqual // HACK for isSizeDirty, ResourceTimelineView::renderRows
});
var COL_MIN_WIDTH = 30;
var SpreadsheetHeader = /** @class */ (function (_super) {
__extends(SpreadsheetHeader, _super);
function SpreadsheetHeader(context, parentEl) {
var _this = _super.call(this, context) || this;
_this.resizables = [];
_this.colWidths = [];
_this.emitter = new core.EmitterMixin();
parentEl.appendChild(_this.tableEl = core.createElement('table', {
className: _this.theme.getClass('tableGrid')
}));
return _this;
}
SpreadsheetHeader.prototype.destroy = function () {
for (var _i = 0, _a = this.resizables; _i < _a.length; _i++) {
var resizable = _a[_i];
resizable.destroy();
}
core.removeElement(this.tableEl);
_super.prototype.destroy.call(this);
};
SpreadsheetHeader.prototype.render = function (props) {
var theme = this.theme;
var colSpecs = props.colSpecs;
var html = '' + props.colTags + '' +
'';
if (props.superHeaderText) {
html +=
'' +
'' +
'
var Spreadsheet = /** @class */ (function (_super) {
__extends(Spreadsheet, _super);
function Spreadsheet(context, headParentEl, bodyParentEl) {
var _this = _super.call(this, context) || this;
_this._renderCells = core.memoizeRendering(_this.renderCells, _this.unrenderCells);
_this.layout = new TimelinePlugin.HeaderBodyLayout(headParentEl, bodyParentEl, 'clipped-scroll');
var headerEnhancedScroller = _this.layout.headerScroller.enhancedScroll;
var bodyEnhancedScroller = _this.layout.bodyScroller.enhancedScroll;
_this.header = new SpreadsheetHeader(context, headerEnhancedScroller.canvas.contentEl);
_this.header.emitter.on('colwidthchange', function (colWidths) {
_this.applyColWidths(colWidths);
});
bodyEnhancedScroller.canvas.contentEl
.appendChild(_this.bodyContainerEl = core.createElement('div', { className: 'fc-rows' }, '' +
'' +
'' +
''));
_this.bodyColGroup = _this.bodyContainerEl.querySelector('colgroup');
_this.bodyTbody = _this.bodyContainerEl.querySelector('tbody');
return _this;
}
Spreadsheet.prototype.destroy = function () {
this.header.destroy();
this.layout.destroy();
this._renderCells.unrender();
_super.prototype.destroy.call(this);
};
Spreadsheet.prototype.render = function (props) {
this._renderCells(props.superHeaderText, props.colSpecs);
};
Spreadsheet.prototype.renderCells = function (superHeaderText, colSpecs) {
var colTags = this.renderColTags(colSpecs);
this.header.receiveProps({
superHeaderText: superHeaderText,
colSpecs: colSpecs,
colTags: colTags
});
this.bodyColGroup.innerHTML = colTags;
this.bodyColEls = Array.prototype.slice.call(this.bodyColGroup.querySelectorAll('col'));
this.applyColWidths(colSpecs.map(function (colSpec) { return colSpec.width; }));
};
Spreadsheet.prototype.unrenderCells = function () {
this.bodyColGroup.innerHTML = '';
};
Spreadsheet.prototype.renderColTags = function (colSpecs) {
var html = '';
for (var _i = 0, colSpecs_1 = colSpecs; _i < colSpecs_1.length; _i++) {
var o = colSpecs_1[_i];
if (o.isMain) {
html += '';
}
else {
html += '';
}
}
return html;
};
Spreadsheet.prototype.updateSize = function (isResize, totalHeight, isAuto) {
this.layout.setHeight(totalHeight, isAuto);
};
Spreadsheet.prototype.applyColWidths = function (colWidths) {
var _this = this;
colWidths.forEach(function (colWidth, colIndex) {
var headEl = _this.header.colEls[colIndex]; // bad to access child
var bodyEl = _this.bodyColEls[colIndex];
var styleVal;
if (typeof colWidth === 'number') {
styleVal = colWidth + 'px';
}
else if (typeof colWidth == null) {
styleVal = '';
}
headEl.style.width = bodyEl.style.width = styleVal;
});
};
return Spreadsheet;
}(core.Component));
var MIN_RESOURCE_AREA_WIDTH = 30; // definitely bigger than scrollbars
var ResourceTimelineView = /** @class */ (function (_super) {
__extends(ResourceTimelineView, _super);
function ResourceTimelineView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.isStickyScrollDirty = false;
_this.rowNodes = [];
_this.rowComponents = [];
_this.rowComponentsById = {};
_this.resourceAreaWidthDraggings = [];
_this.splitter = new ResourceCommonPlugin.ResourceSplitter(); // doesn't let it do businessHours tho
_this.hasResourceBusinessHours = core.memoize(hasResourceBusinessHours);
_this.buildRowNodes = core.memoize(ResourceCommonPlugin.buildRowNodes);
_this.hasNesting = core.memoize(hasNesting);
_this._updateHasNesting = core.memoizeRendering(_this.updateHasNesting);
var allColSpecs = _this.opt('resourceColumns') || [];
var labelText = _this.opt('resourceLabelText'); // TODO: view.override
var defaultLabelText = 'Resources'; // TODO: view.defaults
var superHeaderText = null;
if (!allColSpecs.length) {
allColSpecs.push({
labelText: labelText || defaultLabelText,
text: ResourceCommonPlugin.buildResourceTextFunc(_this.opt('resourceText'), _this.calendar)
});
}
else {
superHeaderText = labelText;
}
var plainColSpecs = [];
var groupColSpecs = [];
var groupSpecs = [];
var isVGrouping = false;
var isHGrouping = false;
for (var _i = 0, allColSpecs_1 = allColSpecs; _i < allColSpecs_1.length; _i++) {
var colSpec = allColSpecs_1[_i];
if (colSpec.group) {
groupColSpecs.push(colSpec);
}
else {
plainColSpecs.push(colSpec);
}
}
plainColSpecs[0].isMain = true;
if (groupColSpecs.length) {
groupSpecs = groupColSpecs;
isVGrouping = true;
}
else {
var hGroupField = _this.opt('resourceGroupField');
if (hGroupField) {
isHGrouping = true;
groupSpecs.push({
field: hGroupField,
text: _this.opt('resourceGroupText'),
render: _this.opt('resourceGroupRender')
});
}
}
var allOrderSpecs = core.parseFieldSpecs(_this.opt('resourceOrder'));
var plainOrderSpecs = [];
for (var _a = 0, allOrderSpecs_1 = allOrderSpecs; _a < allOrderSpecs_1.length; _a++) {
var orderSpec = allOrderSpecs_1[_a];
var isGroup = false;
for (var _b = 0, groupSpecs_1 = groupSpecs; _b < groupSpecs_1.length; _b++) {
var groupSpec = groupSpecs_1[_b];
if (groupSpec.field === orderSpec.field) {
groupSpec.order = orderSpec.order; // -1, 0, 1
isGroup = true;
break;
}
}
if (!isGroup) {
plainOrderSpecs.push(orderSpec);
}
}
_this.superHeaderText = superHeaderText;
_this.isVGrouping = isVGrouping;
_this.isHGrouping = isHGrouping;
_this.groupSpecs = groupSpecs;
_this.colSpecs = groupColSpecs.concat(plainColSpecs);
_this.orderSpecs = plainOrderSpecs;
// START RENDERING...
_this.el.classList.add('fc-timeline');
if (_this.opt('eventOverlap') === false) {
_this.el.classList.add('fc-no-overlap');
}
_this.el.innerHTML = _this.renderSkeletonHtml();
_this.resourceAreaHeadEl = _this.el.querySelector('thead .fc-resource-area');
_this.setResourceAreaWidth(_this.opt('resourceAreaWidth'));
_this.initResourceAreaWidthDragging();
_this.miscHeight = _this.el.getBoundingClientRect().height;
_this.spreadsheet = new Spreadsheet(_this.context, _this.resourceAreaHeadEl, _this.el.querySelector('tbody .fc-resource-area'));
_this.timeAxis = new TimelinePlugin.TimeAxis(_this.context, _this.el.querySelector('thead .fc-time-area'), _this.el.querySelector('tbody .fc-time-area'));
var timeAxisRowContainer = core.createElement('div', { className: 'fc-rows' }, '');
_this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.contentEl.appendChild(timeAxisRowContainer);
_this.timeAxisTbody = timeAxisRowContainer.querySelector('tbody');
_this.lane = new TimelinePlugin.TimelineLane(_this.context, null, _this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.bgEl, _this.timeAxis);
_this.bodyScrollJoiner = new TimelinePlugin.ScrollJoiner('vertical', [
_this.spreadsheet.layout.bodyScroller,
_this.timeAxis.layout.bodyScroller
]);
// after scrolljoiner
_this.spreadsheetBodyStickyScroller = new TimelinePlugin.StickyScroller(_this.spreadsheet.layout.bodyScroller.enhancedScroll, _this.isRtl, true // isVertical
);
_this.spreadsheet.receiveProps({
superHeaderText: _this.superHeaderText,
colSpecs: _this.colSpecs
});
// Component...
context.calendar.registerInteractiveComponent(_this, {
el: _this.timeAxis.slats.el
});
return _this;
}
ResourceTimelineView.prototype.renderSkeletonHtml = function () {
var theme = this.theme;
return " ";
};
ResourceTimelineView.prototype.render = function (props) {
_super.prototype.render.call(this, props);
var splitProps = this.splitter.splitProps(props);
var hasResourceBusinessHours = this.hasResourceBusinessHours(props.resourceStore);
this.timeAxis.receiveProps({
dateProfile: props.dateProfile
});
// for all-resource bg events / selections / business-hours
this.lane.receiveProps(__assign({}, splitProps[''], { dateProfile: props.dateProfile, nextDayThreshold: this.nextDayThreshold, businessHours: hasResourceBusinessHours ? null : props.businessHours }));
var newRowNodes = this.buildRowNodes(props.resourceStore, this.groupSpecs, this.orderSpecs, this.isVGrouping, props.resourceEntityExpansions, this.opt('resourcesInitiallyExpanded'));
this._updateHasNesting(this.hasNesting(newRowNodes));
this.diffRows(newRowNodes);
this.renderRows(props.dateProfile, hasResourceBusinessHours ? props.businessHours : null, // CONFUSING, comment
splitProps);
};
ResourceTimelineView.prototype.updateHasNesting = function (isNesting) {
var classList = this.el.classList;
if (isNesting) {
classList.remove('fc-flat');
}
else {
classList.add('fc-flat');
}
};
ResourceTimelineView.prototype.diffRows = function (newNodes) {
var oldNodes = this.rowNodes;
var oldLen = oldNodes.length;
var oldIndexHash = {}; // id -> index
var oldI = 0;
var newI = 0;
for (oldI = 0; oldI < oldLen; oldI++) {
oldIndexHash[oldNodes[oldI].id] = oldI;
}
// iterate new nodes
for (oldI = 0, newI = 0; newI < newNodes.length; newI++) {
var newNode = newNodes[newI];
var oldIFound = oldIndexHash[newNode.id];
if (oldIFound != null && oldIFound >= oldI) {
this.removeRows(newI, oldIFound - oldI, oldNodes); // won't do anything if same index
oldI = oldIFound + 1;
}
else {
this.addRow(newI, newNode);
}
}
// old rows that weren't found need to be removed
this.removeRows(newI, oldLen - oldI, oldNodes); // won't do anything if same index
this.rowNodes = newNodes;
};
/*
rowComponents is the in-progress result
*/
ResourceTimelineView.prototype.addRow = function (index, rowNode) {
var _a = this, rowComponents = _a.rowComponents, rowComponentsById = _a.rowComponentsById;
var nextComponent = rowComponents[index];
var newComponent = this.buildChildComponent(rowNode, this.spreadsheet.bodyTbody, nextComponent ? nextComponent.spreadsheetTr : null, this.timeAxisTbody, nextComponent ? nextComponent.timeAxisTr : null);
rowComponents.splice(index, 0, newComponent);
rowComponentsById[rowNode.id] = newComponent;
};
ResourceTimelineView.prototype.removeRows = function (startIndex, len, oldRowNodes) {
if (len) {
var _a = this, rowComponents = _a.rowComponents, rowComponentsById = _a.rowComponentsById;
for (var i = 0; i < len; i++) {
var rowComponent = rowComponents[startIndex + i];
rowComponent.destroy();
delete rowComponentsById[oldRowNodes[i].id];
}
rowComponents.splice(startIndex, len);
}
};
ResourceTimelineView.prototype.buildChildComponent = function (node, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext) {
if (node.group) {
return new GroupRow(this.context, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext);
}
else if (node.resource) {
return new ResourceRow(this.context, spreadsheetTbody, spreadsheetNext, timeAxisTbody, timeAxisNext, this.timeAxis);
}
};
ResourceTimelineView.prototype.renderRows = function (dateProfile, fallbackBusinessHours, splitProps) {
var _a = this, rowNodes = _a.rowNodes, rowComponents = _a.rowComponents;
for (var i = 0; i < rowNodes.length; i++) {
var rowNode = rowNodes[i];
var rowComponent = rowComponents[i];
if (rowNode.group) {
rowComponent.receiveProps({
spreadsheetColCnt: this.colSpecs.length,
id: rowNode.id,
isExpanded: rowNode.isExpanded,
group: rowNode.group
});
}
else {
var resource = rowNode.resource;
rowComponent.receiveProps(__assign({}, splitProps[resource.id], { dateProfile: dateProfile, nextDayThreshold: this.nextDayThreshold, businessHours: resource.businessHours || fallbackBusinessHours, colSpecs: this.colSpecs, id: rowNode.id, rowSpans: rowNode.rowSpans, depth: rowNode.depth, isExpanded: rowNode.isExpanded, hasChildren: rowNode.hasChildren, resource: rowNode.resource }));
}
}
};
ResourceTimelineView.prototype.updateSize = function (isResize, viewHeight, isAuto) {
// FYI: this ordering is really important
var calendar = this.calendar;
var isBaseSizing = isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated;
if (isBaseSizing) {
this.syncHeadHeights();
this.timeAxis.updateSize(isResize, viewHeight - this.miscHeight, isAuto);
this.spreadsheet.updateSize(isResize, viewHeight - this.miscHeight, isAuto);
}
var rowSizingCnt = this.updateRowSizes(isResize);
this.lane.updateSize(isResize); // is efficient. uses flags
if (isBaseSizing || rowSizingCnt) {
this.bodyScrollJoiner.update();
this.timeAxis.layout.scrollJoiner.update(); // hack
this.rowPositions = new core.PositionCache(this.timeAxis.slats.el, this.rowComponents.map(function (rowComponent) {
return rowComponent.timeAxisTr;
}), false, // isHorizontal
true // isVertical
);
this.rowPositions.build();
this.isStickyScrollDirty = true;
}
};
ResourceTimelineView.prototype.syncHeadHeights = function () {
var spreadsheetHeadEl = this.spreadsheet.header.tableEl;
var timeAxisHeadEl = this.timeAxis.header.tableEl;
spreadsheetHeadEl.style.height = '';
timeAxisHeadEl.style.height = '';
var max = Math.max(spreadsheetHeadEl.getBoundingClientRect().height, timeAxisHeadEl.getBoundingClientRect().height);
spreadsheetHeadEl.style.height =
timeAxisHeadEl.style.height = max + 'px';
};
ResourceTimelineView.prototype.updateRowSizes = function (isResize) {
var dirtyRowComponents = this.rowComponents;
if (!isResize) {
dirtyRowComponents = dirtyRowComponents.filter(function (rowComponent) {
return rowComponent.isSizeDirty;
});
}
var elArrays = dirtyRowComponents.map(function (rowComponent) {
return rowComponent.getHeightEls();
});
// reset to natural heights
for (var _i = 0, elArrays_1 = elArrays; _i < elArrays_1.length; _i++) {
var elArray = elArrays_1[_i];
for (var _a = 0, elArray_1 = elArray; _a < elArray_1.length; _a++) {
var el = elArray_1[_a];
el.style.height = '';
}
}
// let rows update their contents' heights
for (var _b = 0, dirtyRowComponents_1 = dirtyRowComponents; _b < dirtyRowComponents_1.length; _b++) {
var rowComponent = dirtyRowComponents_1[_b];
rowComponent.updateSize(isResize); // will reset isSizeDirty
}
var maxHeights = elArrays.map(function (elArray) {
var maxHeight = null;
for (var _i = 0, elArray_2 = elArray; _i < elArray_2.length; _i++) {
var el = elArray_2[_i];
var height = el.getBoundingClientRect().height;
if (maxHeight === null || height > maxHeight) {
maxHeight = height;
}
}
return maxHeight;
});
for (var i = 0; i < elArrays.length; i++) {
for (var _c = 0, _d = elArrays[i]; _c < _d.length; _c++) {
var el = _d[_c];
el.style.height = maxHeights[i] + 'px';
}
}
return dirtyRowComponents.length;
};
ResourceTimelineView.prototype.destroy = function () {
for (var _i = 0, _a = this.rowComponents; _i < _a.length; _i++) {
var rowComponent = _a[_i];
rowComponent.destroy();
}
this.rowNodes = [];
this.rowComponents = [];
this.spreadsheet.destroy();
this.timeAxis.destroy();
for (var _b = 0, _c = this.resourceAreaWidthDraggings; _b < _c.length; _b++) {
var resourceAreaWidthDragging = _c[_b];
resourceAreaWidthDragging.destroy();
}
this.spreadsheetBodyStickyScroller.destroy();
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
// Now Indicator
// ------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.getNowIndicatorUnit = function (dateProfile) {
return this.timeAxis.getNowIndicatorUnit(dateProfile);
};
ResourceTimelineView.prototype.renderNowIndicator = function (date) {
this.timeAxis.renderNowIndicator(date);
};
ResourceTimelineView.prototype.unrenderNowIndicator = function () {
this.timeAxis.unrenderNowIndicator();
};
// Scrolling
// ------------------------------------------------------------------------------------------------------------------
// this is useful for scrolling prev/next dates while resource is scrolled down
ResourceTimelineView.prototype.queryScroll = function () {
var scroll = _super.prototype.queryScroll.call(this);
if (this.props.resourceStore) {
__assign(scroll, this.queryResourceScroll());
}
return scroll;
};
ResourceTimelineView.prototype.applyScroll = function (scroll, isResize) {
_super.prototype.applyScroll.call(this, scroll, isResize);
if (this.props.resourceStore) {
this.applyResourceScroll(scroll);
}
// avoid updating stickyscroll too often
if (isResize || this.isStickyScrollDirty) {
this.isStickyScrollDirty = false;
this.spreadsheetBodyStickyScroller.updateSize();
this.timeAxis.updateStickyScrollers();
}
};
ResourceTimelineView.prototype.computeDateScroll = function (duration) {
return this.timeAxis.computeDateScroll(duration);
};
ResourceTimelineView.prototype.queryDateScroll = function () {
return this.timeAxis.queryDateScroll();
};
ResourceTimelineView.prototype.applyDateScroll = function (scroll) {
this.timeAxis.applyDateScroll(scroll);
};
ResourceTimelineView.prototype.queryResourceScroll = function () {
var _a = this, rowComponents = _a.rowComponents, rowNodes = _a.rowNodes;
var scroll = {};
var scrollerTop = this.timeAxis.layout.bodyScroller.el.getBoundingClientRect().top; // fixed position
for (var i = 0; i < rowComponents.length; i++) {
var rowComponent = rowComponents[i];
var rowNode = rowNodes[i];
var el = rowComponent.timeAxisTr;
var elBottom = el.getBoundingClientRect().bottom; // fixed position
if (elBottom > scrollerTop) {
scroll.rowId = rowNode.id;
scroll.bottom = elBottom - scrollerTop;
break;
}
}
// TODO: what about left scroll state for spreadsheet area?
return scroll;
};
ResourceTimelineView.prototype.applyResourceScroll = function (scroll) {
var rowId = scroll.forcedRowId || scroll.rowId;
if (rowId) {
var rowComponent = this.rowComponentsById[rowId];
if (rowComponent) {
var el = rowComponent.timeAxisTr;
if (el) {
var innerTop = this.timeAxis.layout.bodyScroller.enhancedScroll.canvas.el.getBoundingClientRect().top;
var rowRect = el.getBoundingClientRect();
var scrollTop = (scroll.forcedRowId ?
rowRect.top : // just use top edge
rowRect.bottom - scroll.bottom) - // pixels from bottom edge
innerTop;
this.timeAxis.layout.bodyScroller.enhancedScroll.setScrollTop(scrollTop);
this.spreadsheet.layout.bodyScroller.enhancedScroll.setScrollTop(scrollTop);
}
}
}
};
// TODO: scrollToResource
// Hit System
// ------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.buildPositionCaches = function () {
this.timeAxis.slats.updateSize();
this.rowPositions.build();
};
ResourceTimelineView.prototype.queryHit = function (positionLeft, positionTop) {
var rowPositions = this.rowPositions;
var slats = this.timeAxis.slats;
var rowIndex = rowPositions.topToIndex(positionTop);
if (rowIndex != null) {
var resource = this.rowNodes[rowIndex].resource;
if (resource) { // not a group
var slatHit = slats.positionToHit(positionLeft);
if (slatHit) {
return {
component: this,
dateSpan: {
range: slatHit.dateSpan.range,
allDay: slatHit.dateSpan.allDay,
resourceId: resource.id
},
rect: {
left: slatHit.left,
right: slatHit.right,
top: rowPositions.tops[rowIndex],
bottom: rowPositions.bottoms[rowIndex]
},
dayEl: slatHit.dayEl,
layer: 0
};
}
}
}
};
// Resource Area
// ------------------------------------------------------------------------------------------------------------------
ResourceTimelineView.prototype.setResourceAreaWidth = function (widthVal) {
this.resourceAreaWidth = widthVal;
core.applyStyleProp(this.resourceAreaHeadEl, 'width', widthVal || '');
};
ResourceTimelineView.prototype.initResourceAreaWidthDragging = function () {
var _this = this;
var resourceAreaDividerEls = Array.prototype.slice.call(this.el.querySelectorAll('.fc-col-resizer'));
var ElementDraggingImpl = this.calendar.pluginSystem.hooks.elementDraggingImpl;
if (ElementDraggingImpl) {
this.resourceAreaWidthDraggings = resourceAreaDividerEls.map(function (el) {
var dragging = new ElementDraggingImpl(el);
var dragStartWidth;
var viewWidth;
dragging.emitter.on('dragstart', function () {
dragStartWidth = _this.resourceAreaWidth;
if (typeof dragStartWidth !== 'number') {
dragStartWidth = _this.resourceAreaHeadEl.getBoundingClientRect().width;
}
viewWidth = _this.el.getBoundingClientRect().width;
});
dragging.emitter.on('dragmove', function (pev) {
var newWidth = dragStartWidth + pev.deltaX * (_this.isRtl ? -1 : 1);
newWidth = Math.max(newWidth, MIN_RESOURCE_AREA_WIDTH);
newWidth = Math.min(newWidth, viewWidth - MIN_RESOURCE_AREA_WIDTH);
_this.setResourceAreaWidth(newWidth);
});
dragging.setAutoScrollEnabled(false); // because gets weird with auto-scrolling time area
return dragging;
});
}
};
ResourceTimelineView.needsResourceData = true; // for ResourceViewProps
return ResourceTimelineView;
}(core.View));
function hasResourceBusinessHours(resourceStore) {
for (var resourceId in resourceStore) {
var resource = resourceStore[resourceId];
if (resource.businessHours) {
return true;
}
}
return false;
}
function hasNesting(nodes) {
for (var _i = 0, nodes_1 = nodes; _i < nodes_1.length; _i++) {
var node = nodes_1[_i];
if (node.group) {
return true;
}
else if (node.resource) {
if (node.hasChildren) {
return true;
}
}
}
return false;
}
var main = core.createPlugin({
deps: [ResourceCommonPlugin__default, TimelinePlugin__default],
defaultView: 'resourceTimelineDay',
views: {
resourceTimeline: {
class: ResourceTimelineView,
resourceAreaWidth: '30%',
resourcesInitiallyExpanded: true,
eventResizableFromStart: true // TODO: not DRY with this same setting in the main timeline config
},
resourceTimelineDay: {
type: 'resourceTimeline',
duration: { days: 1 }
},
resourceTimelineWeek: {
type: 'resourceTimeline',
duration: { weeks: 1 }
},
resourceTimelineMonth: {
type: 'resourceTimeline',
duration: { months: 1 }
},
resourceTimelineYear: {
type: 'resourceTimeline',
duration: { years: 1 }
}
}
});
exports.ResourceTimelineView = ResourceTimelineView;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
// ../../../../../@fullcalendar/timeline
// ../../../../../@fullcalendar/resource-common
declare module '@fullcalendar/resource-timeline' {
import ResourceTimelineView from '@fullcalendar/resource-timeline/ResourceTimelineView';
export { ResourceTimelineView };
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/resource-timeline/ResourceTimelineView' {
import { ElementDragging, SplittableProps, PositionCache, Hit, View, ViewSpec, ComponentContext, DateProfileGenerator, DateProfile, Duration } from '@fullcalendar/core';
import { ScrollJoiner, TimelineLane, StickyScroller, TimeAxis } from '@fullcalendar/timeline';
import { GroupNode, ResourceNode, ResourceViewProps } from '@fullcalendar/resource-common';
import GroupRow from '@fullcalendar/resource-timeline/GroupRow';
import ResourceRow from '@fullcalendar/resource-timeline/ResourceRow';
import Spreadsheet from '@fullcalendar/resource-timeline/Spreadsheet';
export { ResourceTimelineView as default, ResourceTimelineView };
class ResourceTimelineView extends View {
static needsResourceData: boolean;
props: ResourceViewProps;
spreadsheet: Spreadsheet;
timeAxis: TimeAxis;
lane: TimelineLane;
bodyScrollJoiner: ScrollJoiner;
spreadsheetBodyStickyScroller: StickyScroller;
isStickyScrollDirty: boolean;
timeAxisTbody: HTMLElement;
miscHeight: number;
rowNodes: (GroupNode | ResourceNode)[];
rowComponents: (GroupRow | ResourceRow)[];
rowComponentsById: {
[id: string]: (GroupRow | ResourceRow);
};
resourceAreaHeadEl: HTMLElement;
resourceAreaWidth?: number;
resourceAreaWidthDraggings: ElementDragging[];
superHeaderText: any;
isVGrouping: any;
isHGrouping: any;
groupSpecs: any;
colSpecs: any;
orderSpecs: any;
rowPositions: PositionCache;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
renderSkeletonHtml(): string;
render(props: ResourceViewProps): void;
updateHasNesting(isNesting: boolean): void;
diffRows(newNodes: any): void;
addRow(index: any, rowNode: any): void;
removeRows(startIndex: any, len: any, oldRowNodes: any): void;
buildChildComponent(node: (GroupNode | ResourceNode), spreadsheetTbody: HTMLElement, spreadsheetNext: HTMLElement, timeAxisTbody: HTMLElement, timeAxisNext: HTMLElement): GroupRow | ResourceRow;
renderRows(dateProfile: DateProfile, fallbackBusinessHours: any, splitProps: {
[resourceId: string]: SplittableProps;
}): void;
updateSize(isResize: any, viewHeight: any, isAuto: any): void;
syncHeadHeights(): void;
updateRowSizes(isResize: boolean): number;
destroy(): void;
getNowIndicatorUnit(dateProfile: DateProfile): string;
renderNowIndicator(date: any): void;
unrenderNowIndicator(): void;
queryScroll(): any;
applyScroll(scroll: any, isResize: any): void;
computeDateScroll(duration: Duration): {
left: number;
};
queryDateScroll(): {
left: number;
};
applyDateScroll(scroll: any): void;
queryResourceScroll(): any;
applyResourceScroll(scroll: any): void;
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number): Hit;
setResourceAreaWidth(widthVal: any): void;
initResourceAreaWidthDragging(): void;
}
}
declare module '@fullcalendar/resource-timeline/GroupRow' {
import { Group } from '@fullcalendar/resource-common';
import Row from '@fullcalendar/resource-timeline/Row';
export interface GroupRowProps {
spreadsheetColCnt: number;
id: string;
isExpanded: boolean;
group: Group;
}
export { GroupRow as default, GroupRow };
class GroupRow extends Row {
spreadsheetHeightEl: HTMLElement;
timeAxisHeightEl: HTMLElement;
expanderIconEl: HTMLElement;
render(props: GroupRowProps): void;
destroy(): void;
renderCells(group: Group, spreadsheetColCnt: number): void;
unrenderCells(): void;
renderSpreadsheetContent(group: Group): HTMLElement;
renderCellText(group: Group): any;
getHeightEls(): HTMLElement[];
updateExpanderIcon(isExpanded: boolean): void;
onExpanderClick: (ev: UIEvent) => void;
}
}
declare module '@fullcalendar/resource-timeline/ResourceRow' {
import { Duration, ComponentContext, EventInteractionState, DateSpan, EventUiHash, EventStore, DateProfile } from '@fullcalendar/core';
import { TimelineLane, TimeAxis } from '@fullcalendar/timeline';
import Row from '@fullcalendar/resource-timeline/Row';
import SpreadsheetRow from '@fullcalendar/resource-timeline/SpreadsheetRow';
import { Resource } from '@fullcalendar/resource-common';
export interface ResourceRowProps {
dateProfile: DateProfile;
nextDayThreshold: Duration;
businessHours: EventStore | null;
eventStore: EventStore | null;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
colSpecs: any;
id: string;
rowSpans: number[];
depth: number;
isExpanded: boolean;
hasChildren: boolean;
resource: Resource;
}
export { ResourceRow as default, ResourceRow };
class ResourceRow extends Row {
innerContainerEl: HTMLElement;
spreadsheetRow: SpreadsheetRow;
lane: TimelineLane;
constructor(context: ComponentContext, a: any, b: any, c: any, d: any, timeAxis: TimeAxis);
destroy(): void;
render(props: ResourceRowProps): void;
updateSize(isResize: boolean): void;
getHeightEls(): HTMLElement[];
}
}
declare module '@fullcalendar/resource-timeline/Spreadsheet' {
import { Component, ComponentContext } from '@fullcalendar/core';
import { HeaderBodyLayout } from '@fullcalendar/timeline';
import SpreadsheetHeader from '@fullcalendar/resource-timeline/SpreadsheetHeader';
export interface SpreadsheetProps {
superHeaderText: string;
colSpecs: any;
}
export { Spreadsheet as default, Spreadsheet };
class Spreadsheet extends Component {
header: SpreadsheetHeader;
layout: HeaderBodyLayout;
bodyContainerEl: HTMLElement;
bodyColGroup: HTMLElement;
bodyTbody: HTMLElement;
bodyColEls: HTMLElement[];
constructor(context: ComponentContext, headParentEl: HTMLElement, bodyParentEl: HTMLElement);
destroy(): void;
render(props: SpreadsheetProps): void;
renderCells(superHeaderText: any, colSpecs: any): void;
unrenderCells(): void;
renderColTags(colSpecs: any): string;
updateSize(isResize: any, totalHeight: any, isAuto: any): void;
applyColWidths(colWidths: (number | string)[]): void;
}
}
declare module '@fullcalendar/resource-timeline/Row' {
import { Component, ComponentContext } from '@fullcalendar/core';
export { Row as default, Row };
abstract class Row extends Component {
spreadsheetTr: HTMLElement;
timeAxisTr: HTMLElement;
isSizeDirty: boolean;
constructor(context: ComponentContext, spreadsheetParent: HTMLElement, spreadsheetNextSibling: HTMLElement, timeAxisParent: HTMLElement, timeAxisNextSibling: HTMLElement);
destroy(): void;
abstract getHeightEls(): HTMLElement[];
updateSize(isResize: boolean): void;
}
}
declare module '@fullcalendar/resource-timeline/SpreadsheetRow' {
import { Component, ComponentContext } from '@fullcalendar/core';
import { Resource } from '@fullcalendar/resource-common';
export interface SpreadsheetRowProps {
colSpecs: any;
id: string;
rowSpans: number[];
depth: number;
isExpanded: boolean;
hasChildren: boolean;
resource: Resource;
}
export { SpreadsheetRow as default, SpreadsheetRow };
class SpreadsheetRow extends Component {
tr: HTMLElement;
heightEl: HTMLElement;
expanderIconEl: HTMLElement;
constructor(context: ComponentContext, tr: HTMLElement);
render(props: SpreadsheetRowProps): void;
destroy(): void;
renderRow(resource: Resource, rowSpans: number[], depth: number, colSpecs: any): void;
unrenderRow(): void;
updateExpanderIcon(hasChildren: boolean, isExpanded: boolean): void;
onExpanderClick: (ev: UIEvent) => void;
}
}
declare module '@fullcalendar/resource-timeline/SpreadsheetHeader' {
import { ElementDragging, Component, ComponentContext, EmitterMixin } from '@fullcalendar/core';
export interface SpreadsheetHeaderProps {
superHeaderText: string;
colSpecs: any;
colTags: string;
}
export { SpreadsheetHeader as default, SpreadsheetHeader };
class SpreadsheetHeader extends Component {
tableEl: HTMLElement;
resizerEls: HTMLElement[];
resizables: ElementDragging[];
thEls: HTMLElement[];
colEls: HTMLElement[];
colWidths: number[];
emitter: EmitterMixin;
constructor(context: ComponentContext, parentEl: HTMLElement);
destroy(): void;
render(props: SpreadsheetHeaderProps): void;
initColResizing(): void;
}
}
/*!
FullCalendar Resource Timeline Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core"),require("@fullcalendar/timeline"),require("@fullcalendar/resource-common")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core","@fullcalendar/timeline","@fullcalendar/resource-common"],t):t((e=e||self).FullCalendarResourceTimeline={},e.FullCalendar,e.FullCalendarTimeline,e.FullCalendarResourceCommon)}(this,function(e,t,r,o){"use strict";var i="default"in r?r.default:r,s="default"in o?o.default:o,n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)};function l(e,t){function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}var a=function(){return(a=Object.assign||function(e){for(var t,r=1,o=arguments.length;r'+(r?t.htmlEscape(r):" ")+""),i=e.spec.render;return"function"==typeof i&&(o=i(o,e.value)||o),o},r.prototype.renderCellText=function(e){var t=e.value||"",r=e.spec.text;return"function"==typeof r&&(t=r(t)||t),t},r.prototype.getHeightEls=function(){return[this.spreadsheetHeightEl,this.timeAxisHeightEl]},r.prototype.updateExpanderIcon=function(e){d(this.expanderIconEl,e)},r}(c);u.addEqualityFuncs({group:o.isGroupsEqual});var h=function(e){function r(r,o){var i=e.call(this,r)||this;return i._renderRow=t.memoizeRendering(i.renderRow,i.unrenderRow),i._updateTrResourceId=t.memoizeRendering(p,null,[i._renderRow]),i._updateExpanderIcon=t.memoizeRendering(i.updateExpanderIcon,null,[i._renderRow]),i.onExpanderClick=function(e){var t=i.props;i.calendar.dispatch({type:"SET_RESOURCE_ENTITY_EXPANDED",id:t.id,isExpanded:!t.isExpanded})},i.tr=o,i}return l(r,e),r.prototype.render=function(e){this._renderRow(e.resource,e.rowSpans,e.depth,e.colSpecs),this._updateTrResourceId(this.tr,e.resource.id),this._updateExpanderIcon(e.hasChildren,e.isExpanded)},r.prototype.destroy=function(){e.prototype.destroy.call(this),this._renderRow.unrender()},r.prototype.renderRow=function(e,r,i,s){for(var n,l=this.tr,a=this.theme,c=this.calendar,d=this.view,p=o.buildResourceFields(e),u=0;u'+(h.isMain?y(i):"")+''+(m?t.htmlEscape(m):" ")+"");"function"==typeof h.render&&(g=h.render(new o.ResourceApi(c,e),g)||g),f>1&&g.classList.add("fc-sticky");var v=t.createElement("td",{className:a.getClass("widgetContent"),rowspan:f},g);h.isMain&&(v.appendChild(this.heightEl=t.createElement("div",null,v.childNodes)),n=v),l.appendChild(v)}}this.expanderIconEl=l.querySelector(".fc-expander-space .fc-icon"),d.publiclyTrigger("resourceRender",[{resource:new o.ResourceApi(c,e),el:n,view:d}])},r.prototype.unrenderRow=function(){this.tr.innerHTML=""},r.prototype.updateExpanderIcon=function(e,t){var r,o=this.expanderIconEl,i=o.parentElement;o&&i&&(e?(i.addEventListener("click",this.onExpanderClick),i.classList.add("fc-expander"),d(o,t)):(i.removeEventListener("click",this.onExpanderClick),i.classList.remove("fc-expander"),(r=o.classList).remove("fc-icon-minus-square"),r.remove("fc-icon-plus-square")))},r}(t.Component);function y(e){for(var t="",r=0;r';return t+=''}var f=function(e){function o(o,i,s,n,l,a){var c=e.call(this,o,i,s,n,l)||this;return c._updateTrResourceId=t.memoizeRendering(p),c.spreadsheetRow=new h(o,c.spreadsheetTr),c.timeAxisTr.appendChild(t.createElement("td",{className:c.theme.getClass("widgetContent")},c.innerContainerEl=document.createElement("div"))),c.lane=new r.TimelineLane(o,c.innerContainerEl,c.innerContainerEl,a),c}return l(o,e),o.prototype.destroy=function(){this.spreadsheetRow.destroy(),this.lane.destroy(),e.prototype.destroy.call(this)},o.prototype.render=function(e){this.spreadsheetRow.receiveProps({colSpecs:e.colSpecs,id:e.id,rowSpans:e.rowSpans,depth:e.depth,isExpanded:e.isExpanded,hasChildren:e.hasChildren,resource:e.resource}),this._updateTrResourceId(this.timeAxisTr,e.resource.id),this.lane.receiveProps({dateProfile:e.dateProfile,nextDayThreshold:e.nextDayThreshold,businessHours:e.businessHours,eventStore:e.eventStore,eventUiBases:e.eventUiBases,dateSelection:e.dateSelection,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize}),this.isSizeDirty=!0},o.prototype.updateSize=function(t){e.prototype.updateSize.call(this,t),this.lane.updateSize(t)},o.prototype.getHeightEls=function(){return[this.spreadsheetRow.heightEl,this.innerContainerEl]},o}(c);f.addEqualityFuncs({rowSpans:t.isArraysEqual});var m=function(e){function r(r,o){var i=e.call(this,r)||this;return i.resizables=[],i.colWidths=[],i.emitter=new t.EmitterMixin,o.appendChild(i.tableEl=t.createElement("table",{className:i.theme.getClass("tableGrid")})),i}return l(r,e),r.prototype.destroy=function(){for(var r=0,o=this.resizables;r"+e.colTags+"";e.superHeaderText&&(i+='
/*!
FullCalendar Resource Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
import { mapHash, DateComponent, memoize, parseFieldSpecs, createPlugin } from '@fullcalendar/core';
import ResourceCommonPlugin, { VResourceSplitter, VResourceJoiner, flattenResources, ResourceDayHeader, DayResourceTable, ResourceDayTable } from '@fullcalendar/resource-common';
import DayGridPlugin, { DayGridSlicer, AbstractDayGridView, buildBasicDayTable } from '@fullcalendar/daygrid';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ResourceDayGrid = /** @class */ (function (_super) {
__extends(ResourceDayGrid, _super);
function ResourceDayGrid(context, dayGrid) {
var _this = _super.call(this, context, dayGrid.el) || this;
_this.splitter = new VResourceSplitter();
_this.slicers = {};
_this.joiner = new ResourceDayGridJoiner();
_this.dayGrid = dayGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.dayGrid.el
});
return _this;
}
ResourceDayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
ResourceDayGrid.prototype.render = function (props) {
var _this = this;
var dayGrid = this.dayGrid;
var dateProfile = props.dateProfile, resourceDayTable = props.resourceDayTable, nextDayThreshold = props.nextDayThreshold;
var splitProps = this.splitter.splitProps(props);
this.slicers = mapHash(splitProps, function (split, resourceId) {
return _this.slicers[resourceId] || new DayGridSlicer();
});
var slicedProps = mapHash(this.slicers, function (slicer, resourceId) {
return slicer.sliceProps(splitProps[resourceId], dateProfile, nextDayThreshold, dayGrid, resourceDayTable.dayTable);
});
dayGrid.allowAcrossResources = resourceDayTable.dayTable.colCnt === 1;
dayGrid.receiveProps(__assign({}, this.joiner.joinProps(slicedProps, resourceDayTable), { dateProfile: dateProfile, cells: resourceDayTable.cells, isRigid: props.isRigid }));
};
ResourceDayGrid.prototype.buildPositionCaches = function () {
this.dayGrid.buildPositionCaches();
};
ResourceDayGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.dayGrid,
dateSpan: {
range: rawHit.dateSpan.range,
allDay: rawHit.dateSpan.allDay,
resourceId: this.props.resourceDayTable.cells[rawHit.row][rawHit.col].resource.id
},
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return ResourceDayGrid;
}(DateComponent));
var ResourceDayGridJoiner = /** @class */ (function (_super) {
__extends(ResourceDayGridJoiner, _super);
function ResourceDayGridJoiner() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceDayGridJoiner.prototype.transformSeg = function (seg, resourceDayTable, resourceI) {
var colRanges = resourceDayTable.computeColRanges(seg.firstCol, seg.lastCol, resourceI);
return colRanges.map(function (colRange) {
return __assign({}, seg, colRange, { isStart: seg.isStart && colRange.isStart, isEnd: seg.isEnd && colRange.isEnd });
});
};
return ResourceDayGridJoiner;
}(VResourceJoiner));
var ResourceDayGridView = /** @class */ (function (_super) {
__extends(ResourceDayGridView, _super);
function ResourceDayGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.flattenResources = memoize(flattenResources);
_this.buildResourceDayTable = memoize(buildResourceDayTable);
_this.resourceOrderSpecs = parseFieldSpecs(_this.opt('resourceOrder'));
if (_this.opt('columnHeader')) {
_this.header = new ResourceDayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.resourceDayGrid = new ResourceDayGrid(context, _this.dayGrid);
return _this;
}
ResourceDayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.resourceDayGrid.destroy();
};
ResourceDayGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var resources = this.flattenResources(props.resourceStore, this.resourceOrderSpecs);
var resourceDayTable = this.buildResourceDayTable(this.props.dateProfile, this.dateProfileGenerator, resources, this.opt('datesAboveResources'));
if (this.header) {
this.header.receiveProps({
resources: resources,
dates: resourceDayTable.dayTable.headerDates,
dateProfile: props.dateProfile,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.resourceDayGrid.receiveProps({
dateProfile: props.dateProfile,
resourceDayTable: resourceDayTable,
businessHours: props.businessHours,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
isRigid: this.hasRigidRows(),
nextDayThreshold: this.nextDayThreshold
});
};
ResourceDayGridView.needsResourceData = true; // for ResourceViewProps
return ResourceDayGridView;
}(AbstractDayGridView));
function buildResourceDayTable(dateProfile, dateProfileGenerator, resources, datesAboveResources) {
var dayTable = buildBasicDayTable(dateProfile, dateProfileGenerator);
return datesAboveResources ?
new DayResourceTable(dayTable, resources) :
new ResourceDayTable(dayTable, resources);
}
var main = createPlugin({
deps: [ResourceCommonPlugin, DayGridPlugin],
defaultView: 'resourceDayGridDay',
views: {
resourceDayGrid: ResourceDayGridView,
resourceDayGridDay: {
type: 'resourceDayGrid',
duration: { days: 1 }
},
resourceDayGridWeek: {
type: 'resourceDayGrid',
duration: { weeks: 1 }
},
resourceDayGridMonth: {
type: 'resourceDayGrid',
duration: { months: 1 },
// TODO: wish we didn't have to C&P from dayGrid's file
monthMode: true,
fixedWeekCount: true
}
}
});
export default main;
export { ResourceDayGrid, ResourceDayGridView };
/*!
FullCalendar Resource Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core'), require('@fullcalendar/resource-common'), require('@fullcalendar/daygrid')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core', '@fullcalendar/resource-common', '@fullcalendar/daygrid'], factory) :
(global = global || self, factory(global.FullCalendarResourceDayGrid = {}, global.FullCalendar, global.FullCalendarResourceCommon, global.FullCalendarDayGrid));
}(this, function (exports, core, ResourceCommonPlugin, DayGridPlugin) { 'use strict';
var ResourceCommonPlugin__default = 'default' in ResourceCommonPlugin ? ResourceCommonPlugin['default'] : ResourceCommonPlugin;
var DayGridPlugin__default = 'default' in DayGridPlugin ? DayGridPlugin['default'] : DayGridPlugin;
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var ResourceDayGrid = /** @class */ (function (_super) {
__extends(ResourceDayGrid, _super);
function ResourceDayGrid(context, dayGrid) {
var _this = _super.call(this, context, dayGrid.el) || this;
_this.splitter = new ResourceCommonPlugin.VResourceSplitter();
_this.slicers = {};
_this.joiner = new ResourceDayGridJoiner();
_this.dayGrid = dayGrid;
context.calendar.registerInteractiveComponent(_this, {
el: _this.dayGrid.el
});
return _this;
}
ResourceDayGrid.prototype.destroy = function () {
_super.prototype.destroy.call(this);
this.calendar.unregisterInteractiveComponent(this);
};
ResourceDayGrid.prototype.render = function (props) {
var _this = this;
var dayGrid = this.dayGrid;
var dateProfile = props.dateProfile, resourceDayTable = props.resourceDayTable, nextDayThreshold = props.nextDayThreshold;
var splitProps = this.splitter.splitProps(props);
this.slicers = core.mapHash(splitProps, function (split, resourceId) {
return _this.slicers[resourceId] || new DayGridPlugin.DayGridSlicer();
});
var slicedProps = core.mapHash(this.slicers, function (slicer, resourceId) {
return slicer.sliceProps(splitProps[resourceId], dateProfile, nextDayThreshold, dayGrid, resourceDayTable.dayTable);
});
dayGrid.allowAcrossResources = resourceDayTable.dayTable.colCnt === 1;
dayGrid.receiveProps(__assign({}, this.joiner.joinProps(slicedProps, resourceDayTable), { dateProfile: dateProfile, cells: resourceDayTable.cells, isRigid: props.isRigid }));
};
ResourceDayGrid.prototype.buildPositionCaches = function () {
this.dayGrid.buildPositionCaches();
};
ResourceDayGrid.prototype.queryHit = function (positionLeft, positionTop) {
var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop);
if (rawHit) {
return {
component: this.dayGrid,
dateSpan: {
range: rawHit.dateSpan.range,
allDay: rawHit.dateSpan.allDay,
resourceId: this.props.resourceDayTable.cells[rawHit.row][rawHit.col].resource.id
},
dayEl: rawHit.dayEl,
rect: {
left: rawHit.relativeRect.left,
right: rawHit.relativeRect.right,
top: rawHit.relativeRect.top,
bottom: rawHit.relativeRect.bottom
},
layer: 0
};
}
};
return ResourceDayGrid;
}(core.DateComponent));
var ResourceDayGridJoiner = /** @class */ (function (_super) {
__extends(ResourceDayGridJoiner, _super);
function ResourceDayGridJoiner() {
return _super !== null && _super.apply(this, arguments) || this;
}
ResourceDayGridJoiner.prototype.transformSeg = function (seg, resourceDayTable, resourceI) {
var colRanges = resourceDayTable.computeColRanges(seg.firstCol, seg.lastCol, resourceI);
return colRanges.map(function (colRange) {
return __assign({}, seg, colRange, { isStart: seg.isStart && colRange.isStart, isEnd: seg.isEnd && colRange.isEnd });
});
};
return ResourceDayGridJoiner;
}(ResourceCommonPlugin.VResourceJoiner));
var ResourceDayGridView = /** @class */ (function (_super) {
__extends(ResourceDayGridView, _super);
function ResourceDayGridView(context, viewSpec, dateProfileGenerator, parentEl) {
var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this;
_this.flattenResources = core.memoize(ResourceCommonPlugin.flattenResources);
_this.buildResourceDayTable = core.memoize(buildResourceDayTable);
_this.resourceOrderSpecs = core.parseFieldSpecs(_this.opt('resourceOrder'));
if (_this.opt('columnHeader')) {
_this.header = new ResourceCommonPlugin.ResourceDayHeader(_this.context, _this.el.querySelector('.fc-head-container'));
}
_this.resourceDayGrid = new ResourceDayGrid(context, _this.dayGrid);
return _this;
}
ResourceDayGridView.prototype.destroy = function () {
_super.prototype.destroy.call(this);
if (this.header) {
this.header.destroy();
}
this.resourceDayGrid.destroy();
};
ResourceDayGridView.prototype.render = function (props) {
_super.prototype.render.call(this, props); // for flags for updateSize
var resources = this.flattenResources(props.resourceStore, this.resourceOrderSpecs);
var resourceDayTable = this.buildResourceDayTable(this.props.dateProfile, this.dateProfileGenerator, resources, this.opt('datesAboveResources'));
if (this.header) {
this.header.receiveProps({
resources: resources,
dates: resourceDayTable.dayTable.headerDates,
dateProfile: props.dateProfile,
datesRepDistinctDays: true,
renderIntroHtml: this.renderHeadIntroHtml
});
}
this.resourceDayGrid.receiveProps({
dateProfile: props.dateProfile,
resourceDayTable: resourceDayTable,
businessHours: props.businessHours,
eventStore: props.eventStore,
eventUiBases: props.eventUiBases,
dateSelection: props.dateSelection,
eventSelection: props.eventSelection,
eventDrag: props.eventDrag,
eventResize: props.eventResize,
isRigid: this.hasRigidRows(),
nextDayThreshold: this.nextDayThreshold
});
};
ResourceDayGridView.needsResourceData = true; // for ResourceViewProps
return ResourceDayGridView;
}(DayGridPlugin.AbstractDayGridView));
function buildResourceDayTable(dateProfile, dateProfileGenerator, resources, datesAboveResources) {
var dayTable = DayGridPlugin.buildBasicDayTable(dateProfile, dateProfileGenerator);
return datesAboveResources ?
new ResourceCommonPlugin.DayResourceTable(dayTable, resources) :
new ResourceCommonPlugin.ResourceDayTable(dayTable, resources);
}
var main = core.createPlugin({
deps: [ResourceCommonPlugin__default, DayGridPlugin__default],
defaultView: 'resourceDayGridDay',
views: {
resourceDayGrid: ResourceDayGridView,
resourceDayGridDay: {
type: 'resourceDayGrid',
duration: { days: 1 }
},
resourceDayGridWeek: {
type: 'resourceDayGrid',
duration: { weeks: 1 }
},
resourceDayGridMonth: {
type: 'resourceDayGrid',
duration: { months: 1 },
// TODO: wish we didn't have to C&P from dayGrid's file
monthMode: true,
fixedWeekCount: true
}
}
});
exports.ResourceDayGrid = ResourceDayGrid;
exports.ResourceDayGridView = ResourceDayGridView;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
// ../../../../../@fullcalendar/daygrid
// ../../../../../@fullcalendar/resource-common
declare module '@fullcalendar/resource-daygrid' {
import ResourceDayGridView from '@fullcalendar/resource-daygrid/ResourceDayGridView';
export { ResourceDayGridView };
export { default as ResourceDayGrid } from '@fullcalendar/resource-daygrid/ResourceDayGrid';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
declare module '@fullcalendar/resource-daygrid/ResourceDayGridView' {
import { ComponentContext, ViewSpec, DateProfileGenerator } from '@fullcalendar/core';
import { AbstractDayGridView } from '@fullcalendar/daygrid';
import { ResourceDayHeader, ResourceViewProps } from '@fullcalendar/resource-common';
import ResourceDayGrid from '@fullcalendar/resource-daygrid/ResourceDayGrid';
export { ResourceDayGridView as default, ResourceDayGridView };
class ResourceDayGridView extends AbstractDayGridView {
static needsResourceData: boolean;
props: ResourceViewProps;
header: ResourceDayHeader;
resourceDayGrid: ResourceDayGrid;
constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement);
destroy(): void;
render(props: ResourceViewProps): void;
}
}
declare module '@fullcalendar/resource-daygrid/ResourceDayGrid' {
import { Hit, DateSpan, DateComponent, DateProfile, EventStore, EventUiHash, EventInteractionState, ComponentContext, Duration } from '@fullcalendar/core';
import { DayGrid } from '@fullcalendar/daygrid';
import { AbstractResourceDayTable } from '@fullcalendar/resource-common';
export interface ResourceDayGridProps {
dateProfile: DateProfile | null;
resourceDayTable: AbstractResourceDayTable;
businessHours: EventStore;
eventStore: EventStore;
eventUiBases: EventUiHash;
dateSelection: DateSpan | null;
eventSelection: string;
eventDrag: EventInteractionState | null;
eventResize: EventInteractionState | null;
isRigid: boolean;
nextDayThreshold: Duration;
}
export { ResourceDayGrid as default, ResourceDayGrid };
class ResourceDayGrid extends DateComponent {
dayGrid: DayGrid;
constructor(context: ComponentContext, dayGrid: DayGrid);
destroy(): void;
render(props: ResourceDayGridProps): void;
buildPositionCaches(): void;
queryHit(positionLeft: number, positionTop: number): Hit;
}
}
/*!
FullCalendar Resource Day Grid Plugin v4.3.0
Docs & License: https://fullcalendar.io/scheduler
(c) 2019 Adam Shaw
*/
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@fullcalendar/core"),require("@fullcalendar/resource-common"),require("@fullcalendar/daygrid")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core","@fullcalendar/resource-common","@fullcalendar/daygrid"],r):r((e=e||self).FullCalendarResourceDayGrid={},e.FullCalendar,e.FullCalendarResourceCommon,e.FullCalendarDayGrid)}(this,function(e,r,t,o){"use strict";var i="default"in t?t.default:t,a="default"in o?o.default:o,s=function(e,r){return(s=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,r){e.__proto__=r}||function(e,r){for(var t in r)r.hasOwnProperty(t)&&(e[t]=r[t])})(e,r)};function n(e,r){function t(){this.constructor=e}s(e,r),e.prototype=null===r?Object.create(r):(t.prototype=r.prototype,new t)}var l=function(){return(l=Object.assign||function(e){for(var r,t=1,o=arguments.length;t
/*!
FullCalendar Moment Timezone Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import * as momentNs from 'moment';
import 'moment-timezone/builds/moment-timezone-with-data';
import { createPlugin, NamedTimeZoneImpl } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var moment = momentNs; // the directly callable function
var MomentNamedTimeZone = /** @class */ (function (_super) {
__extends(MomentNamedTimeZone, _super);
function MomentNamedTimeZone() {
return _super !== null && _super.apply(this, arguments) || this;
}
MomentNamedTimeZone.prototype.offsetForArray = function (a) {
return moment.tz(a, this.timeZoneName).utcOffset();
};
MomentNamedTimeZone.prototype.timestampToArray = function (ms) {
return moment.tz(ms, this.timeZoneName).toArray();
};
return MomentNamedTimeZone;
}(NamedTimeZoneImpl));
var main = createPlugin({
namedTimeZonedImpl: MomentNamedTimeZone
});
export default main;
/*!
FullCalendar Moment Timezone Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('moment'), require('moment-timezone/builds/moment-timezone-with-data'), require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', 'moment', 'moment-timezone/builds/moment-timezone-with-data', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarMomentTimezone = {}, global.moment, global.moment, global.FullCalendar));
}(this, function (exports, momentNs, momentTimezoneWithData, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var moment = momentNs; // the directly callable function
var MomentNamedTimeZone = /** @class */ (function (_super) {
__extends(MomentNamedTimeZone, _super);
function MomentNamedTimeZone() {
return _super !== null && _super.apply(this, arguments) || this;
}
MomentNamedTimeZone.prototype.offsetForArray = function (a) {
return moment.tz(a, this.timeZoneName).utcOffset();
};
MomentNamedTimeZone.prototype.timestampToArray = function (ms) {
return moment.tz(ms, this.timeZoneName).toArray();
};
return MomentNamedTimeZone;
}(core.NamedTimeZoneImpl));
var main = core.createPlugin({
namedTimeZonedImpl: MomentNamedTimeZone
});
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../moment-timezone/builds/moment-timezone-with-data
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/moment-timezone' {
import 'moment-timezone/builds/moment-timezone-with-data';
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar Moment Timezone Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("moment"),require("moment-timezone/builds/moment-timezone-with-data"),require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","moment","moment-timezone/builds/moment-timezone-with-data","@fullcalendar/core"],t):t((e=e||self).FullCalendarMomentTimezone={},e.moment,e.moment,e.FullCalendar)}(this,function(e,t,n,o){"use strict";var r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)};var i=t,u=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}(t,e),t.prototype.offsetForArray=function(e){return i.tz(e,this.timeZoneName).utcOffset()},t.prototype.timestampToArray=function(e){return i.tz(e,this.timeZoneName).toArray()},t}(o.NamedTimeZoneImpl),m=o.createPlugin({namedTimeZonedImpl:u});e.default=m,Object.defineProperty(e,"__esModule",{value:!0})});
//= require fullcalendar/core/main.js
//= require fullcalendar/daygrid/main.js
//= require fullcalendar/timegrid/main.js
//= require fullcalendar/premium/timeline/main.js
//= require fullcalendar/premium/resource-common/main.js
//= require fullcalendar/premium/resource-timeline/main.js
//= require fullcalendar/interaction/main.js
{
"name": "@fullcalendar/bootstrap",
"version": "4.3.0",
"title": "FullCalendar Bootstrap Plugin",
"description": "Bootstrap 4 theming for your calendar",
"keywords": [
"calendar",
"event",
"full-sized"
],
"homepage": "https://fullcalendar.io/",
"docs": "https://fullcalendar.io/docs/bootstrap-theme",
"bugs": "https://fullcalendar.io/reporting-bugs",
"repository": {
"type": "git",
"url": "https://github.com/fullcalendar/fullcalendar.git",
"homepage": "https://github.com/fullcalendar/fullcalendar"
},
"license": "MIT",
"author": {
"name": "Adam Shaw",
"email": "arshaw@arshaw.com",
"url": "http://arshaw.com/"
},
"copyright": "2019 Adam Shaw",
"peerDependencies": {
"@fullcalendar/core": "~4.3.0"
},
"main": "main.js",
"module": "main.esm.js",
"unpkg": "main.min.js",
"types": "main.d.ts"
}
/*!
FullCalendar Bootstrap Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
import { createPlugin, Theme } from '@fullcalendar/core';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var BootstrapTheme = /** @class */ (function (_super) {
__extends(BootstrapTheme, _super);
function BootstrapTheme() {
return _super !== null && _super.apply(this, arguments) || this;
}
return BootstrapTheme;
}(Theme));
BootstrapTheme.prototype.classes = {
widget: 'fc-bootstrap',
tableGrid: 'table-bordered',
tableList: 'table',
tableListHeading: 'table-active',
buttonGroup: 'btn-group',
button: 'btn btn-primary',
buttonActive: 'active',
today: 'alert alert-info',
popover: 'card card-primary',
popoverHeader: 'card-header',
popoverContent: 'card-body',
// day grid
// for left/right border color when border is inset from edges (all-day in timeGrid view)
// avoid `table` class b/c don't want margins/padding/structure. only border color.
headerRow: 'table-bordered',
dayRow: 'table-bordered',
// list view
listView: 'card card-primary'
};
BootstrapTheme.prototype.baseIconClass = 'fa';
BootstrapTheme.prototype.iconClasses = {
close: 'fa-times',
prev: 'fa-chevron-left',
next: 'fa-chevron-right',
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right'
};
BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome';
BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome';
BootstrapTheme.prototype.iconOverridePrefix = 'fa-';
var main = createPlugin({
themeClasses: {
bootstrap: BootstrapTheme
}
});
export default main;
export { BootstrapTheme };
/*!
FullCalendar Bootstrap Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) :
typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) :
(global = global || self, factory(global.FullCalendarBootstrap = {}, global.FullCalendar));
}(this, function (exports, core) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var BootstrapTheme = /** @class */ (function (_super) {
__extends(BootstrapTheme, _super);
function BootstrapTheme() {
return _super !== null && _super.apply(this, arguments) || this;
}
return BootstrapTheme;
}(core.Theme));
BootstrapTheme.prototype.classes = {
widget: 'fc-bootstrap',
tableGrid: 'table-bordered',
tableList: 'table',
tableListHeading: 'table-active',
buttonGroup: 'btn-group',
button: 'btn btn-primary',
buttonActive: 'active',
today: 'alert alert-info',
popover: 'card card-primary',
popoverHeader: 'card-header',
popoverContent: 'card-body',
// day grid
// for left/right border color when border is inset from edges (all-day in timeGrid view)
// avoid `table` class b/c don't want margins/padding/structure. only border color.
headerRow: 'table-bordered',
dayRow: 'table-bordered',
// list view
listView: 'card card-primary'
};
BootstrapTheme.prototype.baseIconClass = 'fa';
BootstrapTheme.prototype.iconClasses = {
close: 'fa-times',
prev: 'fa-chevron-left',
next: 'fa-chevron-right',
prevYear: 'fa-angle-double-left',
nextYear: 'fa-angle-double-right'
};
BootstrapTheme.prototype.iconOverrideOption = 'bootstrapFontAwesome';
BootstrapTheme.prototype.iconOverrideCustomButtonOption = 'bootstrapFontAwesome';
BootstrapTheme.prototype.iconOverridePrefix = 'fa-';
var main = core.createPlugin({
themeClasses: {
bootstrap: BootstrapTheme
}
});
exports.BootstrapTheme = BootstrapTheme;
exports.default = main;
Object.defineProperty(exports, '__esModule', { value: true });
}));
// Generated by dts-bundle v0.7.3-fork.1
// Dependencies for this module:
// ../../../../../@fullcalendar/core
declare module '@fullcalendar/bootstrap' {
import { Theme } from '@fullcalendar/core';
export class BootstrapTheme extends Theme {
}
const _default: import("@fullcalendar/core").PluginDef;
export default _default;
}
/*!
FullCalendar Bootstrap Plugin v4.3.0
Docs & License: https://fullcalendar.io/
(c) 2019 Adam Shaw
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):t((e=e||self).FullCalendarBootstrap={},e.FullCalendar)}(this,function(e,t){"use strict";var o=function(e,t){return(o=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var o in t)t.hasOwnProperty(o)&&(e[o]=t[o])})(e,t)};var r=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return function(e,t){function r(){this.constructor=e}o(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}(t,e),t}(t.Theme);r.prototype.classes={widget:"fc-bootstrap",tableGrid:"table-bordered",tableList:"table",tableListHeading:"table-active",buttonGroup:"btn-group",button:"btn btn-primary",buttonActive:"active",today:"alert alert-info",popover:"card card-primary",popoverHeader:"card-header",popoverContent:"card-body",headerRow:"table-bordered",dayRow:"table-bordered",listView:"card card-primary"},r.prototype.baseIconClass="fa",r.prototype.iconClasses={close:"fa-times",prev:"fa-chevron-left",next:"fa-chevron-right",prevYear:"fa-angle-double-left",nextYear:"fa-angle-double-right"},r.prototype.iconOverrideOption="bootstrapFontAwesome",r.prototype.iconOverrideCustomButtonOption="bootstrapFontAwesome",r.prototype.iconOverridePrefix="fa-";var a=t.createPlugin({themeClasses:{bootstrap:r}});e.BootstrapTheme=r,e.default=a,Object.defineProperty(e,"__esModule",{value:!0})});
Error reading file: wrong argument type nil (expected String)
// Place all the styles related to the Messages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.app-chat {
overflow: hidden;
.mdk-drawer-layout,
.mdk-header-layout,
.mdk-header-layout__content {
height: 100%;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
padding-bottom: 0;
}
&-container {
flex: 1 1 0%;
position: relative;
height: 100%;
overflow: hidden;
z-index: 0;
}
}
.trello {
overflow: hidden;
.mdk-drawer-layout,
.mdk-header-layout,
.mdk-header-layout__content {
height: 100%;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
position: relative;
padding-bottom: 0;
}
&-container {
flex: 1 1 0%;
position: relative;
height: 100%;
overflow: hidden;
}
&-board {
display: block;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
height: 100%;
&__tasks {
display: inline-flex;
align-items: start;
width: 22rem;
height: 100%;
&:not(:last-child) {
margin-right: 1.25rem;
}
.card {
width: 100%;
max-height: calc(100% - 1.5625rem);
}
.card-body {
height: 100%;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
&-list {
min-height: 10px;
}
&-item {
margin-bottom: $card-spacer-x/2;
border-color: rgba($body-color, .1) !important;
&:hover {
background: rgba($body-color, .03);
}
}
}
}
}
.layout-login {
background-size: cover;
background-image: url('../images/login/alexandre-godreau-431553-unsplash.jpg');
background-repeat: no-repeat;
position: relative;
&__overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(theme-color('dark'), .5);
z-index: -1;
pointer-events: none;
}
&__form {
padding: 2rem;
position: relative;
}
@include media-breakpoint-up(md) {
display: flex;
&__form {
min-width: calc(320px + 3rem * 2);
padding: 3rem;
}
}
}
.page-separator {
position: relative;
color: $text-muted;
display: flex;
align-items: center;
justify-content: center;
z-index: 0;
margin: .5rem 0;
&::before {
content: '';
height: 1px;
background-color: $border-color;
width: 100%;
top: 50%;
left: 0;
position: absolute;
z-index: -1;
}
&__text {
display: inline-flex;
padding: .25rem 1rem;
font-size: .75rem;
text-transform: uppercase;
letter-spacing: 1.2px;
color: $dark-gray;
line-height: .9375rem;
font-weight: bold;
background-color: white;
max-width: 90%;
@include hover {
text-decoration: none;
}
}
}
.pricing {
&__title {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: normal;
}
&__amount {
font-size: 3.4rem;
line-height: 4.2rem;
font-weight: 600;
margin-right: .466rem;
}
&__currency {
font-size: 1.4rem;
line-height: 1.5rem;
font-weight: 600;
}
&__duration {
font-size: .933rem;
letter-spacing: 1.4px;
line-height: 1.066rem;
}
&__card--popular {
background-color: #F6FAFD;
position: relative;
.pricing__card-badge {
background-color: #EE4059;
color: white;
padding: .2rem .66rem;
border-radius: .2rem .2rem 0 0;
font-weight: 700;
font-size: .66rem;
font-style: italic;
letter-spacing: 1px;
line-height: .8rem;
position: absolute;
top: -1.2rem;
right: 1.2rem;
display: inline-block;
text-transform: uppercase;
}
}
&__features {
letter-spacing: 1.4px;
line-height: 1.86rem;
li {
padding-left: 1.466rem;
position: relative;
&::after {
content: '';
width: .5rem;
height: .5rem;
background-color: $primary;
border-radius: 100%;
left: 0;
top: .6rem;
position: absolute;
}
}
}
}
.layout-login-centered-boxed {
height: initial;
&__form {
padding: 2rem;
position: relative;
max-width: 26rem;
margin: 2rem 0;
}
@include media-breakpoint-up(md) {
display: flex;
justify-content: center;
align-items: center;
&__form {
min-width: calc(320px + 3rem * 2);
padding: 3rem;
}
}
}
.card-header-tabs-basic {
padding-top: 0;
padding-bottom: 0;
display: flex;
> div {
display: flex;
}
a {
padding: .5rem .75rem;
color: rgba($body-color, .54);
font-weight: 600;
&:hover {
text-decoration: none;
color: rgba(17,43,74,.84);
}
}
.active {
color: rgba(17,43,74,.84);
border-bottom: 2px solid $primary;
}
}
.navbar {
min-height: $navbar-height;
padding-top: 0;
padding-bottom: 0;
font-size: .9rem;
.dropdown-menu {
position: absolute;
}
&.navbar-expand {
@each $breakpoint in map-keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
{$infix} {
@include media-breakpoint-up($next) {
.nav-item {
height: $navbar-height;
display: flex;
align-items: center;
}
.dropdown-menu {
max-height: calc(100vh - #{$navbar-height * 2});
}
}
}
}
}
}
.navbar-brand {
font-size: 1.5rem;
font-weight: 600;
padding-top: 0;
padding-bottom: 0;
margin-right: $navbar-item-spacing;
display: flex;
align-items: center;
// @include media-breakpoint-between(lg, xl) {
// .layout-default & {
// min-width: calc(#{$mdk-drawer-width} - #{$navbar-item-spacing});
// }
// }
// @include media-breakpoint-up(xl) {
// .layout-default & {
// min-width: calc(#{$mdk-drawer-width} + 2.6875rem - #{$navbar-padding-x} - #{$navbar-item-spacing});
// }
// }
}
.navbar-brand-icon {
margin-right: $navbar-padding-x/2;
}
.navbar-toggler {
border: none;
width: $navbar-height;
height: $navbar-height;
padding: 0;
text-align: center;
display: inline-block;
}
.navbar-toggler-right {
order: 1;
}
.navbar-toggler-custom {
display: flex;
align-items: center;
.material-icons {
font-size: 2.0625rem;
}
}
.navbar-height {
height: $navbar-height;
}
.navbar-nav .nav-link {
font-weight: 600;
}
.navbar-nav .nav-icon {
font-size: 22px;
}
.navbar-secondary {
min-height: 60px;
&.navbar-expand {
@each $breakpoint in map-keys($grid-breakpoints) {
$next: breakpoint-next($breakpoint, $grid-breakpoints);
$infix: breakpoint-infix($next, $grid-breakpoints);
{$infix} {
@include media-breakpoint-up($next) {
.nav-item + .nav-item {
margin-left: $nav-link-padding-x;
}
.navbar-nav,
.nav-item {
display: flex;
align-items: center;
}
.nav-item {
height: 60px;
}
}
}
}
}
}
.navbar-main {
&.navbar-light {
box-shadow: 0 2px 2px rgba(0,0,0, .05);
}
#account_menu {
right: 10px;
min-width: 200px;
}
}
.navbar .search-form {
max-width: 300px;
.form-control {
box-shadow: none;
&::placeholder {
letter-spacing: 1.4px;
}
}
.btn {
color: $primary;
background-color: transparent;
}
}
.navbar-notifications-indicator {
position: relative;
&::after {
width: 6px;
height: 6px;
content: "";
background: $success;
position: absolute;
top: -3px;
right: 2px;
border-radius: 100px;
}
}
.dropdown-menu.navbar-notifications-menu {
width: 320px;
min-height: 240px;
line-height: 1.5;
padding: 0;
.dropdown-item {
text-transform: none;
letter-spacing: initial;
font-size: $font-size-base;
font-weight: normal;
color: $body-color;
}
> .dropdown-item {
background: $body-bg;
border-bottom: 1px solid $border-color;
}
.navbar-notifications-menu__title {
letter-spacing: initial;
font-weight: 600;
}
.navbar-notifications-menu__content {
max-height: 220px;
.dropdown-item {
white-space: normal;
}
}
.navbar-notifications-menu__footer {
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
border-top: 1px solid $border-color;
font-weight: 600;
&:hover {
color: $link-color;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// //
// search //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////
.search-form {
display: flex;
align-items: center;
background-color: $input-bg;
border: $input-btn-border-width solid $input-border-color;
@if $enable-rounded {
border-radius: $input-border-radius;
} @else {
border-radius: 0;
}
.form-control {
border-color: transparent;
background-color: transparent;
box-shadow: none;
&:focus {
border-color: transparent;
box-shadow: none;
}
}
&:not([class^="input-group-"]) .btn {
padding: 0 .5rem;
}
.btn + .form-control {
padding-left: 0;
}
}
.search-form--light {
.btn {
background: transparent;
color: rgba(0, 0, 0, .4);
}
.form-control:focus + .btn {
color: $input-color;
@include hover {
color: $body-color;
}
}
}
.map {
width: 100%;
min-height: 300px;
position: relative;
overflow: hidden;
}
.map-pin {
width: 64px;
height: 50px;
position: absolute;
top: -25px;
left: -32px;
background-size: 32px 32px;
background-repeat: no-repeat;
text-align: center;
background-position: top center;
color: #888;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
}
.map-pin span {
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
left: 0;
}
.blue {
background-image: url(<%= asset_path 'stack/marker/blue.png' %>)
}
.jqvmap-zoomin,
.jqvmap-zoomout {
box-sizing: content-box;
background: $primary;
padding: .25rem;
}
.jqvmap-label {
z-index: 1;
border: 1px solid $border-color;
border-radius: $border-radius;
background: white;
padding: .25rem .5rem;
color: $body-color;
}
.daterangepicker td.active {
&,
&:hover {
background-color: $primary;
}
}
.daterangepicker .ranges li.active {
background-color: $primary;
}
@import 'sidebar-style-guide/sass/style';
.sidebar {
height: 100%;
text-align: initial;
&:not([data-simplebar]) {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.simplebar-content {
height: 100%;
overflow-x: hidden !important;
}
}
[data-simplebar-force-enabled="true"] .simplebar-content::-webkit-scrollbar {
display: none;
}
.sidebar-heading {
margin-bottom: $sidebar-spacing-y/2;
}
.sidebar-menu + .sidebar-heading {
margin-top: 1.625rem;
}
.sidebar-heading + .sidebar-heading {
margin-top: 2.25rem;
}
.sidebar-heading:first-child {
margin-top: 1.25rem;
}
.sidebar-menu-item.active {
&:not(.open) {
border-right: 2px solid $primary;
}
}
.sidebar-submenu > .sidebar-menu-item:last-child {
margin-bottom: 1rem;
}
.sidebar-submenu .sidebar-menu-text {
padding-left: 1rem;
margin-left: .75rem;
}
.sidebar .progress {
height: 7px;
}
.sidebar-light .sidebar-submenu {
.sidebar-menu-text {
border-left: 1px dotted rgba($dark-gray, .24);
}
}
.sidebar-dark .sidebar-submenu {
.sidebar-menu-text {
border-left: 1px dotted rgba($dark-gray, .24);
}
}
.sidebar-menu-toggle-icon {
font-weight: normal;
}
.mdk-header {
margin-bottom: $spacer;
height: auto;
&__content {
display: flex;
flex-direction: column;
}
}
@import './variables';
.select2-selection__arrow {
display: none;
}
.select2.select2-container {
width: 100% !important;
}
.select2-results__options {
margin: 0 !important;
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field,
.select2-container--bootstrap4 .select2-selection--multiple,
.select2-container .select2-selection--single {
font-size: $font-size-base;
line-height: $line-height-base;
display: block;
width: 100%;
height: calc(2.40625rem + 2px);
padding: .5rem .75rem;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
color: $input-color;
border: 1px solid $input-border-color;
border-radius: $input-border-radius;
background-color: $input-bg;
background-clip: padding-box
}
@media screen and (prefers-reduced-motion:reduce) {
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field,
.select2-container--bootstrap4 .select2-selection--multiple,
.select2-container .select2-selection--single {
transition: none
}
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple::-ms-expand,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field::-ms-expand,
.select2-container--bootstrap4 .select2-selection--multiple::-ms-expand,
.select2-container .select2-selection--single::-ms-expand {
border: 0;
background-color: transparent
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple:focus,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field:focus,
.select2-container--bootstrap4 .select2-selection--multiple:focus,
.select2-container .select2-selection--single:focus {
color: $input-focus-color;
border-color: $input-focus-border-color;
outline: 0;
background-color: $input-focus-bg;
box-shadow: $input-box-shadow, $input-focus-box-shadow;
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple::placeholder,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field::placeholder,
.select2-container--bootstrap4 .select2-selection--multiple::placeholder,
.select2-container .select2-selection--single::placeholder {
opacity: 1;
color: $input-placeholder-color;
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple:disabled,
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple[readonly],
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field:disabled,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field[readonly],
.select2-container--bootstrap4 .select2-selection--multiple:disabled,
.select2-container--bootstrap4 .select2-selection--multiple[readonly],
.select2-container .select2-selection--single:disabled, .select2-container .select2-selection--single[readonly] {
opacity: 1;
background-color: $input-disabled-bg;
}
.select2-container .select2-selection--single .select2-selection__rendered {
overflow: inherit;
padding: 0;
white-space: inherit;
text-overflow: inherit
}
.select2-container--bootstrap4 .select2-selection--single .select2-selection__rendered {
line-height: inherit;
color: inherit
}
.select2-dropdown {
padding: .5rem 0;
border: $dropdown-border-width solid $dropdown-border-color;
background-color: $dropdown-bg;
@include border-radius($dropdown-border-radius);
@include box-shadow($dropdown-box-shadow);
}
.select2-results__option {
padding: .375rem .75rem;
color: rgba($input-color, .54);
background-color: transparent;
display: flex;
align-items: center;
}
.select2-results__option:hover {
color: $input-color;
}
.select2-container--bootstrap4 .select2-results__option--highlighted[aria-selected],
.select2-container--bootstrap4 .select2-results__option[aria-selected=true] {
color: $input-color;
background-color: transparent;
font-weight: 600;
}
.select2-container--bootstrap4 .select2-results__option[aria-disabled=true] {
color: #95aac9;
}
.select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.select2-container--bootstrap4 .select2-selection--multiple {
height: auto;
min-height: calc(2.40625rem + 2px)
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__rendered {
display: block;
margin: 0 0 -.25rem -.25rem;
padding: 0
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice {
font-size: .8125rem;
line-height: 1.40625rem;
display: inline-flex;
margin: 0 0 .25rem .25rem;
padding: 0 .5rem;
// color: $input-color;
border: none;
border-radius: $border-radius-sm;
background-color: $body-bg;
color: #fff;
background-color: #17a2b8;
box-sizing: border-box;
font-weight: 600;
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove {
margin-left: .5rem;
color: rgba($input-color, .54);
order: 2;
}
.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove:hover {
color: $input-color;
}
.select2-search--dropdown {
padding: .375rem .75rem;
}
.form-control-sm + .select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.form-control-sm + .select2-container--bootstrap4 .select2-selection--multiple,
.form-control-sm + .select2-container .select2-selection--single,
.select2-container--bootstrap4 .select2-search--dropdown .select2-search__field {
font-size: .8125rem;
line-height: 1.75;
height: calc(1.67188rem + 2px);
padding: .125rem .5rem;
border-radius: .25rem
}
.form-control-sm + .select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.form-control-sm + .select2-container--bootstrap4 .select2-selection--multiple {
min-height: calc(1.67188rem + 2px)
}
.form-control-sm + .select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice {
line-height: 1.42188rem
}
.form-control-lg + .select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.form-control-lg + .select2-container--bootstrap4 .select2-selection--multiple,
.form-control-lg + .select2-container .select2-selection--single {
font-size: 1.0625rem;
line-height: 1.5;
height: calc(2.59375rem + 2px);
padding: .5rem 1rem;
border-radius: .5rem
}
.form-control-lg + .select2-container--bootstrap4.select2-container--focus .select2-selection--multiple,
.form-control-lg + .select2-container--bootstrap4 .select2-selection--multiple {
min-height: calc(2.59375rem + 2px)
}
.form-control-lg + .select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice {
line-height: 1.59375rem
}
.button-list > .btn {
margin-bottom: .75rem;
margin-left: .5rem;
}
.btn-rounded {
border-radius: 100px;
}
.btn .material-icons {
font-size:18px;
vertical-align: middle;
position:relative;
top: -1px;
}
.btn-light {
@include button-variant($background: white, $border: $border-color);
}
.btn-flush {
padding: 0;
background: none;
color: inherit;
line-height: 1;
box-shadow: none;
}
/* Material Design Icons - Setup Method 2. Self hosting
* http://google.github.io/material-design-icons/ */
$MD-icon-path-name: $material-design-icons-font-path + 'MaterialIcons-Regular' !default;
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(asset-path("#{$MD-icon-path-name}.eot")); /* For IE6-8 */
src: local("Material Icons"),
local("MaterialIcons-Regular"),
url(asset-path("#{$MD-icon-path-name}.woff2")) format('woff2'),
url(asset-path("#{$MD-icon-path-name}.woff")) format('woff'),
url(asset-path("#{$MD-icon-path-name}.ttf")) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
&.md-14 { font-size: 14px; }
&.md-18 { font-size: 18px; }
&.md-24 { font-size: 24px; }
&.md-36 { font-size: 36px; }
&.md-48 { font-size: 48px; }
&.md-128 { font-size: 128px; }
}
.comment-counter{
position: relative;
span {
position: absolute;
background: rgba(239,244,248,1);
left: 15px;
top: -5px;
width: 1rem;
text-align: center;
font-size: 1rem;
border-radius: 50%;
color:$body-color;
border:1px solid $body-color;
}
}
.md-top-1 {
position:relative;
top: -1px;
}
.material-icons {
vertical-align: middle;
}
.icon-muted {
color: rgba(#374D67, .2) !important;
}
a.icon-muted:hover .material-icons {
color: $primary;
}
.icon-16pt {
font-size: 1rem !important;
}
.icon-20pt {
font-size: 1.25rem !important;
}
.icon-30pt {
font-size: 30px !important;
}
.icon-40pt {
font-size: 40px !important;
}
.icon-60pt {
font-size: 60px !important;
}
#app-icons {
.col-auto {
padding: 1rem;
height: 120px;
width: 20%;
}
.title {
max-width: 112px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 .35rem;
}
}
.chart {
position: relative;
height: 300px;
}
.chart-legend {
display: flex;
margin-top: 2.5rem;
white-space: nowrap;
justify-content: center;
&-item {
display: flex;
align-items: center;
justify-content: start;
}
&:not(&--vertical) {
.chart-legend-item + .chart-legend-item {
margin-left: 1rem;
}
}
&--vertical {
flex-direction: column;
}
}
.chart-legend-indicator {
display: block;
width: .5rem;
height: .5rem;
margin-right: 1rem;
border-radius: 50%;
}
#chart-tooltip {
z-index: 0;
pointer-events: none;
}
#chart-tooltip .arrow {
top: 100%;
left: 50%;
transform: translateX(-50%) translateX(-.5rem);
}
.popover-header {
border-bottom: 0;
}
.popover-body-label, .popover-body-value {
margin-left: .25rem;
}
.popover-body-indicator {
display: inline-block;
width: .5rem;
height: .5rem;
border-radius: 50%;
}
@import "./_vendor-flatpickr-vars";
$calendarBackground: #fff;
$calendarBorderColor: #eee;
$monthForeground: #3c3f40;
$monthBackground: transparent;
$weekdaysBackground: transparent;
$weekdays_color: #757575;
$dayForeground: #404848;
$dayHoverBackground: #e9e9e9;
$disabled_border_color: $dayHoverBackground;
$disabled_day_color: rgba(black, 0.15);
$todayColor: #F64747;
$today_fg_color: #fff;
$selectedDayBackground: #4f99ff;
$selectedDayForeground: #fff;
.flatpickr-calendar {
width: $daysWidth;
}
.dayContainer {
padding: 0;
border-right: 0;
}
span.flatpickr-day {
&, &.prevMonthDay, &.nextMonthDay {
border-radius: 0 !important;
border: 1px solid $dayHoverBackground;
max-width: none;
border-right-color: transparent;
&:nth-child(n+8) {
border-top-color: transparent;
}
&:nth-child(7n-6) {
border-left: 0;
}
&:nth-child(n+36) {
border-bottom: 0;
}
&:nth-child(-n+7) {
margin-top: 0;
}
&.today:not(.selected) {
border-color: $dayHoverBackground;
border-right-color: transparent;
border-top-color: transparent;
border-bottom-color: $todayColor;
&:hover {
border: 1px solid $todayColor;
}
}
&.startRange, &.endRange {
border-color: $selectedDayBackground;
}
&.today, &.selected {
z-index: 2;
}
}
}
.rangeMode .flatpickr-day {
margin-top: -1px;
}
.flatpickr-weekwrapper .flatpickr-weeks {
box-shadow: none;
}
.flatpickr-weekwrapper span.flatpickr-day {
border: 0;
margin: -1px 0 0 -1px;
}
.hasWeeks .flatpickr-days {
border-right: 0;
}
@import "./_vendor-flatpickr-vars.scss";
@-webkit-keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@-moz-keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@-ms-keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@-o-keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.flatpickr-calendar {
background: transparent;
opacity: 0;
display: none;
text-align: center;
visibility: hidden;
padding: 0;
animation: none;
direction: ltr;
border: 0;
font-size: 14px;
line-height: 24px;
border-radius: 5px;
position: absolute;
width: $calendarWidth;
box-sizing: border-box;
touch-action: manipulation;
@if $noCalendarBorder {
box-shadow: 0 3px 13px rgba(black, 0.08);
} @else {
background: $calendarBackground;
box-shadow: 1px 0 0 $calendarBorderColor,
-1px 0 0 $calendarBorderColor,
0 1px 0 $calendarBorderColor,
0 -1px 0 $calendarBorderColor,
0 3px 13px rgba(black, 0.08);
}
&.open, &.inline {
opacity: 1;
max-height: 640px;
visibility: visible;
}
&.open {
display: inline-block;
z-index: 99999;
}
&.animate.open {
animation: fpFadeInDown 300ms $bezier;
}
&.inline {
display: block;
position: relative;
top: 2px;
}
&.static {
position: absolute;
top: calc(100% + 2px);
&.open {
z-index: 999;
display: block;
}
}
&.multiMonth {
.flatpickr-days .dayContainer:nth-child(n+1) {
& .flatpickr-day.inRange:nth-child(7n+7) {
box-shadow: none !important;
}
}
.flatpickr-days .dayContainer:nth-child(n+2) {
& .flatpickr-day.inRange:nth-child(7n+1) {
box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6;
}
}
}
.hasWeeks, .hasTime {
.dayContainer {
border-bottom: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
@if $noCalendarBorder {
.hasWeeks .dayContainer {
border-left: 0;
}
}
&.showTimeInput.hasTime {
.flatpickr-time {
height: $timeHeight;
border-top: 1px solid $calendarBorderColor;
}
@if $noCalendarBorder {
.flatpickr-innerContainer {
border-bottom: 0;
}
.flatpickr-time {
border: 1px solid $calendarBorderColor;
}
}
}
&.noCalendar.hasTime {
.flatpickr-time {
height: auto;
}
}
&:before, &:after {
position: absolute;
display: block;
pointer-events: none;
border: solid transparent;
content: '';
height: 0;
width: 0;
left: 22px;
}
&.rightMost {
&:before, &:after {
left: auto;
right: 22px;
}
}
&:before {
border-width: 5px;
margin: 0 -5px;
}
&:after {
border-width: 4px;
margin: 0 -4px;
}
&.arrowTop {
&:before, &:after {
bottom: 100%;
}
&:before {
border-bottom-color: $calendarBorderColor;
}
&:after {
@if $noCalendarBorder {
border-bottom-color: $monthBackground;
} @else {
border-bottom-color: $calendarBackground;
}
}
}
&.arrowBottom {
&:before, &:after {
top: 100%;
}
&:before {
border-top-color: $calendarBorderColor;
}
&:after {
@if $noCalendarBorder {
border-top-color: $monthBackground;
} @else {
border-top-color: $calendarBackground;
}
}
}
&:focus {
outline: 0;
}
}
.flatpickr-wrapper {
position: relative;
display: inline-block;
}
.flatpickr-months {
display: flex;
.flatpickr-month {
@if $noCalendarBorder {
border-radius: 5px 5px 0 0;
}
background: $monthBackground;
color: $monthForeground;
fill: $monthForeground;
height: $monthNavHeight;
line-height: 1;
text-align: center;
position: relative;
user-select: none;
overflow: hidden;
flex: 1;
}
.flatpickr-prev-month, .flatpickr-next-month {
text-decoration: none;
cursor: pointer;
position: absolute;
top: 0px;
line-height: 16px;
height: $monthNavHeight;
padding: 10px;
z-index: 3;
color: $monthForeground;
fill: $monthForeground;
&.disabled {
display: none;
}
i {
position: relative;
}
&.flatpickr-prev-month {
/*!
/*rtl:begin:ignore*/ /*
*/
left: 0;
/*!
/*rtl:end:ignore*/ /*
*/
}
&.flatpickr-next-month {
/*!
/*rtl:begin:ignore*/ /*
*/
right: 0;
/*!
/*rtl:end:ignore*/ /*
*/
}
&:hover {
color: $todayColor;
svg {
@if $arrow_hover_color {
fill: $arrow_hover_color;
} @else {
fill: $todayColor;
}
}
}
svg {
width: 14px;
height: 14px;
path {
transition: fill 0.1s;
fill: inherit;
}
}
}
}
.numInputWrapper {
position: relative;
height: auto;
input, span {
display: inline-block;
}
input {
width: 100%;
&::-ms-clear {
display: none;
}
}
span {
position: absolute;
right: 0;
width: 14px;
padding: 0 4px 0 2px;
height: 50%;
line-height: 50%;
opacity: 0;
cursor: pointer;
border: 1px solid rgba($dayForeground, 0.15);
box-sizing: border-box;
&:hover {
background: rgba($invertedBg, 0.1);
}
&:active {
background: rgba($invertedBg, 0.2);
}
&:after {
display: block;
content: "";
position: absolute;
}
&.arrowUp {
top: 0;
border-bottom: 0;
&:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid rgba($dayForeground, 0.6);
top: 26%;
}
}
&.arrowDown {
top: 50%;
&:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba($dayForeground, 0.6);
top: 40%;
}
}
svg {
width: inherit;
height: auto;
path {
fill: rgba($monthForeground, 0.5);
}
}
}
&:hover {
background: rgba($invertedBg, 0.05);
span {
opacity: 1;
}
}
}
.flatpickr-current-month {
font-size: 135%;
line-height: inherit;
font-weight: 300;
color: inherit;
position: absolute;
width: 75%;
left: 12.5%;
padding: 0.22 * $monthNavHeight 0 0 0;
line-height: 1;
height: $monthNavHeight;
display: inline-block;
text-align: center;
transform: translate3d(0px, 0px, 0px);
span.cur-month {
font-family: inherit;
font-weight: 700;
color: inherit;
display: inline-block;
margin-left: 0.5ch;
padding: 0;
&:hover {
background: rgba($invertedBg, 0.05);
}
}
.numInputWrapper {
width: 6ch;
width: unquote("7ch\0");
display: inline-block;
span.arrowUp:after {
border-bottom-color: $monthForeground;
}
span.arrowDown:after {
border-top-color: $monthForeground;
}
}
input.cur-year {
background: transparent;
box-sizing: border-box;
color: inherit;
cursor: text;
padding: 0 0 0 0.5ch;
margin: 0;
display: inline-block;
font-size: inherit;
font-family: inherit;
font-weight: 300;
line-height: inherit;
height: auto;
border: 0;
border-radius: 0;
vertical-align: initial;
&:focus {
outline: 0;
}
&[disabled], &[disabled]:hover {
font-size: 100%;
color: rgba($monthForeground, 0.5);
background: transparent;
pointer-events: none;
}
}
}
.flatpickr-weekdays {
background: $weekdaysBackground;
text-align: center;
overflow: hidden;
width: 100%;
display: flex;
align-items: center;
height: $weekdaysHeight;
.flatpickr-weekdaycontainer {
display: flex;
flex: 1;
}
}
span.flatpickr-weekday {
cursor: default;
font-size: 90%;
background: $monthBackground;
color: $weekdaysForeground;
line-height: 1;
margin: 0;
text-align: center;
display: block;
flex: 1;
font-weight: bolder;
}
.dayContainer, .flatpickr-weeks {
padding: 1px 0 0 0;
}
.flatpickr-days {
position: relative;
overflow: hidden;
display: flex;
align-items: flex-start;
width: $daysWidth;
&:focus {
outline: 0;
}
@if $noCalendarBorder {
border-left: 1px solid $calendarBorderColor;
border-right: 1px solid $calendarBorderColor;
}
}
.dayContainer {
padding: 0;
outline: 0;
text-align: left;
width: $daysWidth;
min-width: $daysWidth;
max-width: $daysWidth;
box-sizing: border-box;
display: inline-block;
display: -ms-flexbox;
display: flex;
flex-wrap: wrap;
-ms-flex-wrap: wrap;
-ms-flex-pack: justify;
justify-content: space-around;
transform: translate3d(0px, 0px, 0px);
opacity: 1;
& + .dayContainer {
box-shadow: -1px 0 0 $calendarBorderColor;
}
}
.flatpickr-day {
background: none;
border: 1px solid transparent;
border-radius: 150px;
box-sizing: border-box;
color: $dayForeground;
cursor: pointer;
font-weight: 400;
width: 14.2857143%;
flex-basis: 14.2857143%;
max-width: $daySize;
height: $daySize;
line-height: $daySize;
margin: 0;
display: inline-block;
position: relative;
justify-content: center;
text-align: center;
&, &.prevMonthDay, &.nextMonthDay {
&.inRange, &.today.inRange, &:hover, &:focus {
cursor: pointer;
outline: 0;
background: $dayHoverBackground;
border-color: $dayHoverBackground;
}
}
&.today {
border-color: $todayColor;
&:hover, &:focus {
border-color: $todayColor;
background: $todayColor;
@if $today_fg_color {
color: $today_fg_color;
} @else {
color: white;
}
}
}
&.selected, &.startRange, &.endRange {
&, &.inRange, &:focus, &:hover, &.prevMonthDay, &.nextMonthDay {
background: $selectedDayBackground;
box-shadow: none;
@if $selectedDayForeground {
color: $selectedDayForeground;
} @else {
color: white;
}
border-color: $selectedDayBackground;
}
&.startRange {
border-radius: 50px 0 0 50px;
}
&.endRange {
border-radius: 0 50px 50px 0;
}
&.startRange + .endRange:not(:nth-child(7n+1)) {
box-shadow: -5 * $dayMargin 0 0 $selectedDayBackground;
}
&.startRange.endRange {
border-radius: 50px;
}
}
&.inRange {
border-radius: 0;
box-shadow: -2.5 * $dayMargin 0 0 $dayHoverBackground, 2.5 * $dayMargin 0 0 $dayHoverBackground;
}
&.disabled, &.disabled:hover,
&.prevMonthDay, &.nextMonthDay,
&.notAllowed, &.notAllowed.prevMonthDay, &.notAllowed.nextMonthDay {
color: rgba($dayForeground, 0.3);
background: transparent;
@if $disabled_border_color {
border-color: $disabled_border_color;
} @else {
border-color: transparent;
}
cursor: default;
}
&.disabled, &.disabled:hover {
cursor: not-allowed;
color: rgba($dayForeground, 0.1);
}
&.week.selected {
border-radius: 0;
box-shadow: -2.5 * $dayMargin 0 0 $selectedDayBackground, 2.5 * $dayMargin 0 0 $selectedDayBackground;
}
&.hidden {
visibility: hidden;
}
}
.rangeMode .flatpickr-day {
margin-top: 1px;
}
.flatpickr-weekwrapper {
display: inline-block;
float: left;
.flatpickr-weeks {
padding: 0 12px;
@if $noCalendarBorder {
border-left: 1px solid $calendarBorderColor;
} @else {
box-shadow: 1px 0 0 $calendarBorderColor;
}
}
.flatpickr-weekday {
float: none;
width: 100%;
line-height: $weekdaysHeight;
}
span.flatpickr-day {
&, &:hover {
display: block;
width: 100%;
max-width: none;
color: rgba($dayForeground, 0.3);
background: transparent;
cursor: default;
border: none;
}
}
}
.flatpickr-innerContainer {
display: block;
display: flex;
box-sizing: border-box;
overflow: hidden;
@if $noCalendarBorder {
background: $calendarBackground;
border-bottom: 1px solid $calendarBorderColor;
}
}
.flatpickr-rContainer {
display: inline-block;
padding: 0;
box-sizing: border-box;
}
.flatpickr-time {
text-align: center;
outline: 0;
display: block;
height: 0;
line-height: $timeHeight;
max-height: $timeHeight;
box-sizing: border-box;
overflow: hidden;
display: flex;
@if $noCalendarBorder {
background: $calendarBackground;
border-radius: 0 0 5px 5px;
}
&:after {
content: "";
display: table;
clear: both;
}
.numInputWrapper {
flex: 1;
width: 40%;
height: $timeHeight;
float: left;
span.arrowUp:after {
border-bottom-color: $dayForeground;
}
span.arrowDown:after {
border-top-color: $dayForeground;
}
}
&.hasSeconds .numInputWrapper {
width: 26%;
}
&.time24hr .numInputWrapper {
width: 49%;
}
input {
background: transparent;
box-shadow: none;
border: 0;
border-radius: 0;
text-align: center;
margin: 0;
padding: 0;
height: inherit;
line-height: inherit;
color: $dayForeground;
font-size: 14px;
position: relative;
box-sizing: border-box;
&.flatpickr-hour {
font-weight: bold;
}
&.flatpickr-minute, &.flatpickr-second {
font-weight: 400;
}
&:focus {
outline: 0;
border: 0;
}
}
.flatpickr-time-separator, .flatpickr-am-pm {
height: inherit;
display: inline-block;
float: left;
line-height: inherit;
color: $dayForeground;
font-weight: bold;
width: 2%;
user-select: none;
align-self: center;
}
.flatpickr-am-pm {
outline: 0;
width: 18%;
cursor: pointer;
text-align: center;
font-weight: 400;
}
input, .flatpickr-am-pm {
&:hover, &:focus {
background: lighten($dayHoverBackground, 5);
}
}
}
.flatpickr-input[readonly] {
cursor: pointer;
}
.dashboard-location-tabs {
&__tab {
margin-bottom: $grid-gutter-width/3;
cursor: pointer;
&:not(.active) {
.progress-bar {
background: $text-muted !important;
}
}
}
}
.preloader {
height: 100%;
width: 100%;
top: 0px;
position: fixed;
z-index: 99999;
background: $dark;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
@import './variables';
/*
* The MIT License
* Copyright (c) 2012 Matias Meno
*/
@keyframes passing-through {
0% {
opacity: 0;
transform: translateY(40px);
}
30%, 70% {
opacity: 1;
transform: translateY(0px);
}
100% {
opacity: 0;
transform: translateY(-40px);
}
}
@keyframes slide-in {
0% {
opacity: 0;
transform: translateY(40px);
}
30% {
opacity: 1;
transform: translateY(0px);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
10% {
transform: scale(1.1);
}
20% {
transform: scale(1);
}
}
.dropzone {
// min-height: 150px;
// border: 2px solid rgba(0, 0, 0, 0.3);
// background: white;
// padding: 20px 20px;
position: relative;
display: flex;
flex-direction: column;
}
.dropzone.dz-clickable {
cursor: pointer;
}
.dropzone.dz-clickable * {
cursor: default;
}
.dropzone.dz-clickable .dz-message,
.dropzone.dz-clickable .dz-message * {
cursor: pointer;
}
.dropzone.dz-started .dz-message {
display: none;
}
.dropzone.dz-drag-hover {
border-style: solid;
}
.dropzone.dz-drag-hover .dz-message {
opacity: 0.5;
}
.dropzone .dz-message {
text-align: center;
padding: 2rem 1rem;
color: #95aac9;
border: 1px dashed $border-color;
border-radius: $border-radius;
background-color: white;
order: -1;
}
.dropzone .dz-preview:hover {
z-index: 1000;
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1;
}
.dropzone .dz-preview.dz-file-preview .dz-image {
border-radius: 20px;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd);
}
.dropzone .dz-preview.dz-file-preview .dz-details {
opacity: 1;
}
.dropzone .dz-preview.dz-image-preview {
background: white;
}
.dropzone .dz-preview.dz-image-preview .dz-details {
transition: opacity 0.2s linear;
}
.dropzone .dz-preview .dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none;
}
.dropzone .dz-preview .dz-remove:hover {
text-decoration: underline;
}
.dropzone .dz-preview:hover .dz-details {
opacity: 1;
}
.dropzone .dz-preview .dz-details {
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, 0.9);
line-height: 150%;
}
.dropzone .dz-preview .dz-details .dz-size {
margin-bottom: 1em;
font-size: 16px;
}
.dropzone .dz-preview .dz-details .dz-filename {
white-space: nowrap;
}
.dropzone .dz-preview .dz-details .dz-filename:hover span {
border: 1px solid rgba(200, 200, 200, 0.8);
background-color: rgba(255, 255, 255, 0.8);
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) {
overflow: hidden;
text-overflow: ellipsis;
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
border: 1px solid transparent;
}
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
background-color: rgba(255, 255, 255, 0.4);
padding: 0 0.4em;
border-radius: 3px;
}
.dropzone .dz-preview:hover .dz-image img {
transform: scale(1.05, 1.05);
filter: blur(8px);
}
.dropzone .dz-preview .dz-image {
border-radius: 20px;
overflow: hidden;
width: 120px;
height: 120px;
position: relative;
display: block;
z-index: 10;
}
.dropzone .dz-preview .dz-image img {
display: block;
}
.dropzone .dz-preview.dz-success .dz-success-mark {
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dropzone .dz-preview.dz-error .dz-error-mark {
opacity: 1;
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -27px;
margin-top: -27px;
}
.dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg {
display: block;
width: 54px;
height: 54px;
}
.dropzone .dz-preview.dz-processing .dz-progress {
opacity: 1;
transition: all 0.2s linear;
}
.dropzone .dz-preview.dz-complete .dz-progress {
opacity: 0;
transition: opacity 0.4s ease-in;
}
.dropzone .dz-preview:not(.dz-processing) .dz-progress {
animation: pulse 6s ease infinite;
}
.dropzone .dz-preview .dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 16px;
left: 50%;
top: 50%;
margin-top: -8px;
width: 80px;
margin-left: -40px;
background: rgba(255, 255, 255, 0.9);
-webkit-transform: scale(1);
border-radius: 8px;
overflow: hidden;
}
.dropzone .dz-preview .dz-progress .dz-upload {
background: #333;
background: linear-gradient(to bottom, #666, #444);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
transition: width 300ms ease-in-out;
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block;
}
.dropzone .dz-preview.dz-error:hover .dz-error-message {
opacity: 1;
pointer-events: auto;
}
.dropzone .dz-preview .dz-error-message {
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 8px;
font-size: 13px;
top: 130px;
left: -10px;
width: 140px;
background: #be2626;
background: linear-gradient(to bottom, #be2626, #a92222);
padding: 0.5em 1.2em;
color: white;
}
.dropzone .dz-preview .dz-error-message:after {
content: '';
position: absolute;
top: -6px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #be2626;
}
// Alternate styles with softer background color
//
// Generate contextual modifier classes for colorizing the alert.
@each $color, $value in $theme-colors {
.alert-soft-#{$color} {
@include alert-variant(
rgba(theme-color($color), .05),
theme-color-level($color, $alert-border-level),
theme-color($color)
);
}
}
.alert-light,
.alert-soft-light {
color: theme-color-level('light', $alert-color-level);
@include gradient-bg(theme-color('light'));
border-color: darken(theme-color('light'), 5%);
hr {
border-top-color: darken(theme-color('light'), 5%);
}
}
.list-group-small > .list-group-item {
padding-top: .8125rem;
padding-bottom: .8125rem;
}
.custom-checkbox-toggle {
width: 3rem;
height: 1.5rem;
padding-left: 0;
.custom-control-label {
position: relative;
width: 100%;
height: 100%;
&::after,
&::before {
position: absolute;
top: 0;
left: 0;
content: "";
transition: all .2s ease;
border-radius: 1.5rem;
}
&:before {
width: 100%;
height: 100%;
background-color: #e3ebf6;
}
&::after {
width: 1.5rem;
height: 100%;
transform: scale(.8);
background-color: #fff;
}
}
.custom-control-input:checked ~ .custom-control-label::after {
right: 0;
left: 1.5rem;
background-color: #fff;
}
}
@import './variables';
$bezier: cubic-bezier(0.23, 1, 0.32, 1);
$slideTime: 400ms;
$daySize: 39px;
$padding: $daySize / 16;
$dayMargin: 2px;
$daysWidth: $daySize * 7 + $dayMargin * 14 + $padding * 2 + 2;
$calendarWidth: $daysWidth;
$monthNavHeight: 28px !default;
$weekdaysHeight: 28px !default;
$timeHeight: 40px;
$calendarBackground: #ffffff !default;
$calendarBorderColor: #e6e6e6 !default;
$noCalendarBorder: false !default;
$monthForeground: rgba(black, 0.9) !default;
$arrow_hover_color: #f64747 !default;
$monthBackground: transparent !default;
$weekdaysBackground: transparent !default;
$weekdaysForeground: rgba(black, 0.54) !default;
$dayForeground: #393939 !default;
$dayHoverBackground: #e6e6e6 !default;
$today_fg_color: false !default;
$todayColor: #959ea9 !default;
$selectedDayBackground: $primary !default;
$selectedDayForeground: false !default;
$disabled-border-color: false !default;
$invertedBg: invert($calendarBackground);
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
.border#{$infix} { border: $border-width solid $border-color !important; }
.border-top#{$infix} { border-top: $border-width solid $border-color !important; }
.border-right#{$infix} { border-right: $border-width solid $border-color !important; }
.border-bottom#{$infix} { border-bottom: $border-width solid $border-color !important; }
.border-left#{$infix} { border-left: $border-width solid $border-color !important; }
.border#{$infix}-0 { border: 0 !important; }
.border-top#{$infix}-0 { border-top: 0 !important; }
.border-right#{$infix}-0 { border-right: 0 !important; }
.border-bottom#{$infix}-0 { border-bottom: 0 !important; }
.border-left#{$infix}-0 { border-left: 0 !important; }
.rounded#{$infix} {
border-radius: $border-radius !important;
}
.rounded-top#{$infix} {
border-top-left-radius: $border-radius !important;
border-top-right-radius: $border-radius !important;
}
.rounded-right#{$infix} {
border-top-right-radius: $border-radius !important;
border-bottom-right-radius: $border-radius !important;
}
.rounded-bottom#{$infix} {
border-bottom-right-radius: $border-radius !important;
border-bottom-left-radius: $border-radius !important;
}
.rounded-left#{$infix} {
border-top-left-radius: $border-radius !important;
border-bottom-left-radius: $border-radius !important;
}
.rounded#{$infix}-0 {
border-radius: 0 !important;
}
.rounded-top#{$infix}-0 {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.rounded-left#{$infix}-0 {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.rounded-right#{$infix}-0 {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
}
}
.badge {
box-sizing:border-box;
font-weight:600;
display:inline-flex;
align-items:center;
vertical-align:middle;
}
.badge-light-gray {
color: rgba(#66768A, .4);
}
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
.flex#{$infix} {
flex: 1 1 0% !important;
}
.flex#{$infix}-none,
.flex#{$infix}-0 {
flex: none !important;
}
.flex#{$infix}-grow {
flex: 1 1 auto !important;
}
.flex#{$infix}-shrink-0 {
flex-shrink: 0 !important;
}
}
}
@import "font-awesome";
// Variables
@import './variables';
// Material Icons
@import 'material-design-icons-iconfont/dist/material-design-icons.scss';
.list-todo {
&:last-child {
margin-bottom: 0;
}
li {
padding-left: 1rem;
&:not(:last-child) {
margin-bottom: 1rem;
}
}
.custom-control-input:checked ~ .custom-control-label {
text-decoration: line-through;
}
.custom-control {
padding-left: 2rem;
}
.custom-control-label {
&::before,
&::after {
left: -2rem;
}
}
}
.input-group-text .material-icons {
font-size: inherit;
}
.input-group.input-group-merge {
.field_with_errors {
position: relative;
flex: 1 1 auto;
width: 1%;
margin-bottom: 0;
}
.form-control {
box-shadow: none;
&:focus ~ [class*=input-group] .input-group-text {
border-color: $input-focus-border-color;
border-width: 1px;
color: $input-focus-border-color;
}
&.is-valid ~ [class*=input-group] .input-group-text,
.was-validated &:valid ~ [class*=input-group] .input-group-text {
border-color: $form-feedback-valid-color;
color: $form-feedback-valid-color;
}
&.is-invalid ~ [class*=input-group] .input-group-text,
.was-validated &:invalid ~ [class*=input-group] .input-group-text {
border-color: $form-feedback-invalid-color;
color: $form-feedback-invalid-color;
}
}
.form-control-prepended {
padding-left: 10px;
border-left-width: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
@include border-right-radius($input-border-radius);
}
.form-control-appended {
padding-right: 0;
border-right-width: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group-prepend {
order: -1;
> .input-group-text {
border-right-width: 0 !important;
@include border-left-radius($input-border-radius);
}
}
.input-group-append > .input-group-text {
border-left-width: 0 !important;
@include border-right-radius($input-border-radius);
}
&.input-group-rounded {
.form-control-prepended {
@include border-right-radius(20rem);
}
.form-control-appended {
@include border-left-radius(20rem);
}
.input-group-prepend > .input-group-text {
@include border-left-radius(20rem);
}
.input-group-append > .input-group-text {
@include border-right-radius(20rem);
}
}
}
.table thead th {
font-size: .825rem;
// color: rgba(#374D67, .54);
color: #555555;
}
.table tbody td {
vertical-align: middle;
}
.thead-border-top-0 thead th {
border-top: 0;
}
.table tbody .selected > td {
background-color: rgba(#5897E0, .05);
}
.table .sort {
color: inherit;
display: flex-inline;
align-items: center;
text-decoration: none;
&::after {
margin-left: .5rem;
content: str-replace(url("data:image/svg+xml;utf8,"), "#", "%23");
}
&.asc,
&.desc {
color: $body-color;
text-decoration: underline;
}
& + .sort {
margin-left: 1rem;
}
}
table a.rating-link {
color: theme-color("light-gray");
&:hover, &.active {
color: theme-color("warning");
}
}
.progress {
box-shadow: none;
}
.avatar {
font-size: 1rem;
display: inline-block;
width: 3rem;
height: 3rem;
position: relative;
z-index: 0;
&-img {
width: 100%;
height: 100%;
object-fit: cover;
}
&-online,
&-offline {
&::before,
&::after {
position: absolute;
right: 5%;
bottom: 5%;
width: 20%;
height: 20%;
content: "";
border-radius: 50%;
}
&::before {
width: 22%;
height: 22%;
right: 4%;
bottom: 4%;
background-color: white;
}
}
&-online::after {
background-color: $success;
}
&-offline::after {
background-color: theme-color('dark-gray');
}
&-title {
display: flex;
width: 100%;
height: 100%;
color: #fff;
background-color: theme-color('dark-gray');
align-items: center;
justify-content: center;
}
&-xl,
&-xxl {
font-size: 1.70833rem;
width: 5.125rem;
height: 5.125rem;
}
&-lg {
font-size: 1.33333rem;
width: 4rem;
height: 4rem;
}
&-sm {
width: 2.5rem;
height: 2.5rem;
font-weight: bold;
}
&-xs {
font-size: .54167rem;
width: 35px;
height: 35px;
font-weight: bold;
}
&-xxs {
font-size: .54167rem;
width: 20px;
height: 20px;
font-weight: bold;
&.avatar-online,
&.avatar-offline {
&::before,
&::after {
position: absolute;
right: -1px;
bottom: -1px;
width: 25%;
height: 25%;
content: "";
border-radius: 50%;
}
}
}
@include media-breakpoint-up(md) {
&-xxl {
font-size: 2.66667rem;
width: 8rem;
height: 8rem;
}
}
&.avatar-4by3 {
width: 4rem;
}
&-xxl.avatar-4by3 {
width: 10.66667rem;
}
&-xl.avatar-4by3 {
width: 6.83333rem;
}
&-lg.avatar-4by3 {
width: 5.33333rem;
}
&-group {
display: inline-flex;
.avatar {
// mask-image: url(../images/avatar-group-mask.svg);
// mask-size: 100% 100%;
}
.avatar:hover {
z-index: 1;
// mask-image: none;
}
.avatar-img,
.avatar-title {
border: 2px solid white;
}
.avatar+.avatar {
margin-left: -.75rem;
}
.avatar-lg+.avatar-lg {
margin-left: -1rem;
}
.avatar-xl+.avatar-xl {
margin-left: -1.28125rem;
}
.avatar-xxl+.avatar-xxl {
margin-left: -2rem;
}
.avatar-sm+.avatar-sm {
margin-left: -.625rem;
}
.avatar-xs+.avatar-xs {
margin-left: -.40625rem;
}
}
}
.avatar-list {
> .avatar,
> .avatar-group {
margin-bottom: .75rem;
margin-left: .5rem;
}
}
.flatpickr-input[readonly] {
background-color: $input-bg;
}
.flatpickr-wrapper {
display: block;
}
.flatpickr-hidden-input {
visibility: hidden;
height: 0;
}
.bg-primary-dark {
background-color: $primary-dark !important;
}
.border-style-dashed {
border-style: dashed !important;
}
.border-bottom-2 {
border-bottom: 2px solid $border-color;
}
.text-body {
color: $body-color;
&[href] {
@include hover {
color: $body-color;
}
}
}
.text-underline {
text-decoration: underline;
}
.text-underline-0 {
text-decoration: none;
@include hover {
text-decoration: none;
}
}
// .fc button {
// height: auto;
// }
.fc td {
padding: 0 !important;
}
.fc-event {
line-height: 1.5;
border-color: transparent;
border-radius: $border-radius;
padding: .25rem .5rem;
}
.fc-title {
font-weight: 300;
}
.fc-popover .fc-header {
position: relative;
padding: .25rem .5rem;
.fc-close {
position: absolute;
top: .6rem;
right: .5rem;
}
}
.external-event {
padding: .25rem .5rem;
color: white;
margin-bottom: .5rem;
display: flex;
border-radius: $border-radius;
cursor: move;
}
.appointment-message-badge { //used for showing that there's an unread message/notification on this appointment
height: 15px;
width: 25px;
background-color: #d9534f;
border-radius: 90%;
margin-left: -12px;
margin-top: -8px;
}
.fc-filterEmployeesDropdown-button {
background: none! important;
border: none! important;
padding: 0! important;
color: #069! important;
}
.card {
// box-shadow: 0 10px 25px 0 rgba(50,50,93,0.07), 0 5px 15px 0 rgba(0,0,0,0.07);
border: none;
margin-bottom: $grid-gutter-width / 2;
@include media-breakpoint-up(lg) {
margin-bottom: $grid-gutter-width;
}
}
.card-margin {
margin-bottom: $grid-gutter-width / 2;
@include media-breakpoint-up(lg) {
margin-bottom: $grid-gutter-width;
}
}
.form-row .card.form-row__card {
margin-bottom: 10px;
}
.card-list > .card:last-child {
margin-bottom: 0;
}
.card-group {
@include media-breakpoint-up(sm) {
margin-bottom: $grid-gutter-width;
box-shadow: 0 10px 25px 0 rgba(50,50,93,0.07), 0 5px 15px 0 rgba(0,0,0,0.07);
@if $enable-rounded {
@include border-radius($card-border-radius);
}
> .card {
box-shadow: none;
border: $card-border-width solid $card-border-color;
}
}
}
.card-header {
background-color: #F7F8F9;
}
.card-header__title {
font-size: 1.25rem;
font-weight: 600;
font-family: $font-family-base;
letter-spacing: 0;
color: $headings-color;
&:last-child {
margin-bottom: 0;
}
.active & {
color: $primary;
}
}
.card-header-large {
padding-top: 1.4375rem;
padding-bottom: 1.4375rem;
}
.card-header-sm {
padding-top: .85rem;
padding-bottom: .85rem;
}
.card-footer {
background-color: white;
}
.card-body-lg {
padding: 1.6875rem;
}
.card-body-x-lg {
padding-left: 1.6875rem;
padding-right: 1.6875rem;
}
///////////////////////////////////////////////////////
// CARD GROUP combined with GRID //
// //
//
.card-group-row {
display: flex;
flex-flow: row wrap;
&__col {
display: flex;
}
&__card {
flex: 1 0 0;
}
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
line-height: 1.25rem;
}
b,
strong {
font-weight: 600;
}
.bold {
font-weight: 700;
}
h1, .h1 {
font-weight: 700;
}
.headings-color {
color: $headings-color;
}
.text-15pt {
font-size: .9375rem !important;
}
.decoration-0 {
@include hover {
text-decoration: none;
}
}
.link-date {
color: $link-color;
position: relative;
overflow: hidden;
background: rgba($primary, .05);
padding: 2px 4px;
border-radius: 2px;
&::after {
content: "................................................................";
color: rgba($dark-gray, .4);
font-size: .75rem;
position: absolute;
left: 0;
overflow: hidden;
width: 100%;
top: .525rem;
white-space: nowrap;
}
@include hover {
text-decoration: none;
}
}
.text-amount {
font-size: 30px;
letter-spacing: 2.1px;
color: #112B4A;
line-height: 2.1875rem;
}
.text-stats {
font-weight: bold;
letter-spacing: 0.93px;
font-size: .8125rem;
display: flex;
align-items: center;
.material-icons {
margin-left: .125rem;
font-size: 1rem;
}
}
html,
body {
height: 100%;
position: relative;
}
.mdk-header-layout,
.mdk-drawer-layout,
.mdk-drawer-layout__content {
height: initial;
min-height: 100%;
}
.page {
padding-bottom: $grid-gutter-width;
}
.page__container,
.page__heading-container {
.layout-default &,
.layout-fluid & {
@include media-breakpoint-up(xl) {
padding-left: $grid-gutter-width;
padding-right: $grid-gutter-width;
}
}
.layout-fluid & {
max-width: 1300px;
}
.layout-default & {
max-width: 1300px;
}
}
.page__heading {
h1 {line-height: 1;}
padding-top: $grid-gutter-width /2;
padding-bottom: $grid-gutter-width/2;
@include media-breakpoint-up(xl) {
&:not(.page__heading--xl_small) {
padding-top: $grid-gutter-width;
padding-bottom: $grid-gutter-width;
}
}
}
.layout-default, .layout-fixed, .layout-mini
{
.page__heading {
border-bottom:1px solid $border-color;
margin-bottom: $grid-gutter-width/2;
@include media-breakpoint-up(xl) {
&:not(.page__heading--xl_small) {
margin-bottom: $grid-gutter-width;
}
}
}
}
.page__header {
background: white;
box-shadow: 0 5px 15px 0 rgba(227,227,227,0.50);
margin-bottom: $grid-gutter-width/2;
@include media-breakpoint-up(xl) {
margin-bottom: $grid-gutter-width;
}
z-index: 3;
& + .page__header {
z-index: 2;
}
position: relative;
}
// sticky subnav
.layout-sticky-subnav {
.mdk-header-layout {
overflow: initial;
}
.page__header {
&-nav {
position: sticky;
top: $navbar-height;
z-index: 1;
}
&:first-child {
margin-bottom: 0;
box-shadow: none;
}
}
.page__container {
z-index: 0;
position: relative;
}
}
.mdk-header--shadow::after {
box-shadow: 0 5px 15px 0 rgba(227,227,227,0.50);
bottom: 0;
height: $navbar-height;
}
.projects-item {
margin-bottom: 0 !important;
padding-bottom: .4375rem;
border-left: 1px dotted rgba(55, 77, 103, 0.2);
margin-left: 10px;
}
.bottom-spacing {
margin-bottom: $grid-gutter-width / 2;
@include media-breakpoint-up(lg) {
margin-bottom: $grid-gutter-width;
}
}
.dropdown-toggle:focus {
outline: 0;
}
[data-caret="false"] {
&::before,
&::after {
display: none;
}
}
.dropdown-menu {
visibility: hidden;
opacity: 0;
transition: opacity .3s ease, margin-top .3s ease, visibility .3s ease;
margin-top: 20px !important;
background-clip: initial;
&::before,
&::after {
border: 8px solid transparent;
border-bottom-color: $dropdown-bg;
content: "";
height: 0;
left: 10px;
opacity: 0;
transition: .1s opacity cubic-bezier(.3, .5, .5, 1);
position: absolute;
top: -16px;
width: 1px;
}
&::before {
top: -17px;
border-bottom-color: $dropdown-border-color;
}
}
.dropdown-menu-right {
&::before,
&::after {
left: initial;
right: 10px;
}
}
.dropup {
.dropdown-menu {
transform-origin: 10% bottom;
&::before,
&::after {
top: auto;
bottom: -16px;
border-bottom-color: transparent;
border-top-color: $dropdown-bg;
}
&::before {
bottom: -17px;
border-top-color: $dropdown-border-color;
}
}
.dropdown-menu-right {
transform-origin: 90% bottom;
}
}
.show > .dropdown-menu,
.dropdown-menu.show {
visibility: visible;
opacity: 1;
margin-top: $dropdown-spacer !important;
&::before,
&::after {
opacity: 1;
}
}
.dropdown-menu-caret-center {
transform-origin: 50% top;
.dropup & {
transform-origin: 50% bottom;
}
&::before,
&::after {
left: 50%;
margin-left: -4px;
}
}
@import './variables';
/*!
* Quill Editor v1.3.6
* https://quilljs.com/
* Copyright (c) 2014, Jason Chen
* Copyright (c) 2013, salesforce.com
*/
.ql-container {
background-color: white;
height: 100%;
margin: 0px;
position: relative;
border-radius: 0 0 $border-radius $border-radius;
&:not(:last-child) {
margin-bottom: 1.5rem;
}
}
.ql-container.ql-disabled .ql-tooltip {
visibility: hidden;
}
.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
pointer-events: none;
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
}
.ql-clipboard p {
margin: 0;
padding: 0;
}
.ql-editor {
box-sizing: border-box;
height: 100%;
outline: none;
overflow-y: auto;
padding: .5rem .75rem;
tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor > * {
cursor: text;
}
.ql-editor p,
.ql-editor ol,
.ql-editor ul,
.ql-editor pre,
.ql-editor blockquote,
.ql-editor h1,
.ql-editor h2,
.ql-editor h3,
.ql-editor h4,
.ql-editor h5,
.ql-editor h6 {
margin: 0;
padding: 0;
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol,
.ql-editor ul {
padding-left: 1.5em;
}
.ql-editor ol > li,
.ql-editor ul > li {
list-style-type: none;
}
.ql-editor ul > li::before {
content: '\2022';
}
.ql-editor ul[data-checked=true],
.ql-editor ul[data-checked=false] {
pointer-events: none;
}
.ql-editor ul[data-checked=true] > li *,
.ql-editor ul[data-checked=false] > li * {
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before,
.ql-editor ul[data-checked=false] > li::before {
color: #777;
cursor: pointer;
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before {
content: '\2611';
}
.ql-editor ul[data-checked=false] > li::before {
content: '\2610';
}
.ql-editor li::before {
display: inline-block;
white-space: nowrap;
width: 1.2em;
}
.ql-editor li:not(.ql-direction-rtl)::before {
margin-left: -1.5em;
margin-right: 0.3em;
text-align: right;
}
.ql-editor li.ql-direction-rtl::before {
margin-left: 0.3em;
margin-right: -1.5em;
}
.ql-editor ol li:not(.ql-direction-rtl),
.ql-editor ul li:not(.ql-direction-rtl) {
padding-left: 1.5em;
}
.ql-editor ol li.ql-direction-rtl,
.ql-editor ul li.ql-direction-rtl {
padding-right: 1.5em;
}
.ql-editor ol li {
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
counter-increment: list-0;
}
.ql-editor ol li:before {
content: counter(list-0, decimal) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-increment: list-1;
}
.ql-editor ol li.ql-indent-1:before {
content: counter(list-1, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-2 {
counter-increment: list-2;
}
.ql-editor ol li.ql-indent-2:before {
content: counter(list-2, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-2 {
counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-3 {
counter-increment: list-3;
}
.ql-editor ol li.ql-indent-3:before {
content: counter(list-3, decimal) '. ';
}
.ql-editor ol li.ql-indent-3 {
counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-4 {
counter-increment: list-4;
}
.ql-editor ol li.ql-indent-4:before {
content: counter(list-4, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-4 {
counter-reset: list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-5 {
counter-increment: list-5;
}
.ql-editor ol li.ql-indent-5:before {
content: counter(list-5, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-5 {
counter-reset: list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-6 {
counter-increment: list-6;
}
.ql-editor ol li.ql-indent-6:before {
content: counter(list-6, decimal) '. ';
}
.ql-editor ol li.ql-indent-6 {
counter-reset: list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-7 {
counter-increment: list-7;
}
.ql-editor ol li.ql-indent-7:before {
content: counter(list-7, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-7 {
counter-reset: list-8 list-9;
}
.ql-editor ol li.ql-indent-8 {
counter-increment: list-8;
}
.ql-editor ol li.ql-indent-8:before {
content: counter(list-8, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-8 {
counter-reset: list-9;
}
.ql-editor ol li.ql-indent-9 {
counter-increment: list-9;
}
.ql-editor ol li.ql-indent-9:before {
content: counter(list-9, decimal) '. ';
}
.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
padding-left: 3em;
}
.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
padding-left: 4.5em;
}
.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 3em;
}
.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 4.5em;
}
.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
padding-left: 7.5em;
}
.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 7.5em;
}
.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
padding-left: 9em;
}
.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
padding-left: 10.5em;
}
.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 9em;
}
.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 10.5em;
}
.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
padding-left: 13.5em;
}
.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 13.5em;
}
.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
padding-left: 15em;
}
.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
padding-left: 16.5em;
}
.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 15em;
}
.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 16.5em;
}
.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
padding-left: 19.5em;
}
.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 19.5em;
}
.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
padding-left: 21em;
}
.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
padding-left: 22.5em;
}
.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 21em;
}
.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 22.5em;
}
.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
padding-left: 24em;
}
.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
padding-left: 25.5em;
}
.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 24em;
}
.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 25.5em;
}
.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
padding-left: 27em;
}
.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
padding-left: 28.5em;
}
.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 27em;
}
.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 28.5em;
}
.ql-editor .ql-video {
display: block;
max-width: 100%;
}
.ql-editor .ql-video.ql-align-center {
margin: 0 auto;
}
.ql-editor .ql-video.ql-align-right {
margin: 0 0 0 auto;
}
.ql-editor .ql-bg-black {
background-color: #000;
}
.ql-editor .ql-bg-red {
background-color: #e60000;
}
.ql-editor .ql-bg-orange {
background-color: #f90;
}
.ql-editor .ql-bg-yellow {
background-color: #ff0;
}
.ql-editor .ql-bg-green {
background-color: #008a00;
}
.ql-editor .ql-bg-blue {
background-color: #06c;
}
.ql-editor .ql-bg-purple {
background-color: #93f;
}
.ql-editor .ql-color-white {
color: #fff;
}
.ql-editor .ql-color-red {
color: #e60000;
}
.ql-editor .ql-color-orange {
color: #f90;
}
.ql-editor .ql-color-yellow {
color: #ff0;
}
.ql-editor .ql-color-green {
color: #008a00;
}
.ql-editor .ql-color-blue {
color: #06c;
}
.ql-editor .ql-color-purple {
color: #93f;
}
.ql-editor .ql-font-serif {
font-family: Georgia, Times New Roman, serif;
}
.ql-editor .ql-font-monospace {
font-family: Monaco, Courier New, monospace;
}
.ql-editor .ql-size-small {
font-size: 0.75em;
}
.ql-editor .ql-size-large {
font-size: 1.5em;
}
.ql-editor .ql-size-huge {
font-size: 2.5em;
}
.ql-editor .ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-editor .ql-align-center {
text-align: center;
}
.ql-editor .ql-align-justify {
text-align: justify;
}
.ql-editor .ql-align-right {
text-align: right;
}
.ql-editor.ql-blank::before {
color: rgba(0,0,0,0.6);
content: attr(data-placeholder);
font-style: italic;
left: 15px;
pointer-events: none;
position: absolute;
right: 15px;
}
.ql-snow.ql-toolbar:after,
.ql-snow .ql-toolbar:after {
clear: both;
content: '';
display: table;
}
.ql-snow.ql-toolbar button,
.ql-snow .ql-toolbar button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
float: left;
height: 24px;
padding: 3px 5px;
width: 28px;
}
.ql-snow.ql-toolbar button svg,
.ql-snow .ql-toolbar button svg {
float: left;
height: 100%;
}
.ql-snow.ql-toolbar button:active:hover,
.ql-snow .ql-toolbar button:active:hover {
outline: none;
}
.ql-snow.ql-toolbar input.ql-image[type=file],
.ql-snow .ql-toolbar input.ql-image[type=file] {
display: none;
}
.ql-snow.ql-toolbar button:hover,
.ql-snow .ql-toolbar button:hover,
.ql-snow.ql-toolbar button:focus,
.ql-snow .ql-toolbar button:focus,
.ql-snow.ql-toolbar button.ql-active,
.ql-snow .ql-toolbar button.ql-active,
.ql-snow.ql-toolbar .ql-picker-label:hover,
.ql-snow .ql-toolbar .ql-picker-label:hover,
.ql-snow.ql-toolbar .ql-picker-label.ql-active,
.ql-snow .ql-toolbar .ql-picker-label.ql-active,
.ql-snow.ql-toolbar .ql-picker-item:hover,
.ql-snow .ql-toolbar .ql-picker-item:hover,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected {
color: #06c;
}
.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow .ql-toolbar button:hover .ql-fill,
.ql-snow.ql-toolbar button:focus .ql-fill,
.ql-snow .ql-toolbar button:focus .ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-fill,
.ql-snow .ql-toolbar button.ql-active .ql-fill,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,
.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill {
fill: #06c;
}
.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow .ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-toolbar button:focus .ql-stroke,
.ql-snow .ql-toolbar button:focus .ql-stroke,
.ql-snow.ql-toolbar button.ql-active .ql-stroke,
.ql-snow .ql-toolbar button.ql-active .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
.ql-snow .ql-toolbar button:hover .ql-stroke-miter,
.ql-snow.ql-toolbar button:focus .ql-stroke-miter,
.ql-snow .ql-toolbar button:focus .ql-stroke-miter,
.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
stroke: #06c;
}
@media (pointer: coarse) {
.ql-snow.ql-toolbar button:hover:not(.ql-active),
.ql-snow .ql-toolbar button:hover:not(.ql-active) {
color: $body-color;
}
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill {
fill: $body-color;
}
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,
.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,
.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter {
stroke: $body-color;
}
}
.ql-snow {
box-sizing: border-box;
}
.ql-snow * {
box-sizing: border-box;
}
.ql-snow .ql-hidden {
display: none;
}
.ql-snow .ql-out-bottom,
.ql-snow .ql-out-top {
visibility: hidden;
}
.ql-snow .ql-tooltip {
position: absolute;
transform: translateY(10px);
}
.ql-snow .ql-tooltip a {
cursor: pointer;
text-decoration: none;
}
.ql-snow .ql-tooltip.ql-flip {
transform: translateY(-10px);
}
.ql-snow .ql-formats {
display: inline-block;
vertical-align: middle;
}
.ql-snow .ql-formats:after {
clear: both;
content: '';
display: table;
}
.ql-snow .ql-stroke {
fill: none;
stroke: $body-color;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
}
.ql-snow .ql-stroke-miter {
fill: none;
stroke: $body-color;
stroke-miterlimit: 10;
stroke-width: 2;
}
.ql-snow .ql-fill,
.ql-snow .ql-stroke.ql-fill {
fill: $body-color;
}
.ql-snow .ql-empty {
fill: none;
}
.ql-snow .ql-even {
fill-rule: evenodd;
}
.ql-snow .ql-thin,
.ql-snow .ql-stroke.ql-thin {
stroke-width: 1;
}
.ql-snow .ql-transparent {
opacity: 0.4;
}
.ql-snow .ql-direction svg:last-child {
display: none;
}
.ql-snow .ql-direction.ql-active svg:last-child {
display: inline;
}
.ql-snow .ql-direction.ql-active svg:first-child {
display: none;
}
.ql-snow .ql-editor h1 {
font-size: 2em;
}
.ql-snow .ql-editor h2 {
font-size: 1.5em;
}
.ql-snow .ql-editor h3 {
font-size: 1.17em;
}
.ql-snow .ql-editor h4 {
font-size: 1em;
}
.ql-snow .ql-editor h5 {
font-size: 0.83em;
}
.ql-snow .ql-editor h6 {
font-size: 0.67em;
}
.ql-snow .ql-editor a {
text-decoration: underline;
}
.ql-snow .ql-editor blockquote {
border-left: 4px solid $border-color;
margin-bottom: 5px;
margin-top: 5px;
padding-left: 16px;
}
.ql-snow .ql-editor code,
.ql-snow .ql-editor pre {
background-color: #f0f0f0;
border-radius: 3px;
}
.ql-snow .ql-editor pre {
white-space: pre-wrap;
margin-bottom: 5px;
margin-top: 5px;
padding: 5px 10px;
}
.ql-snow .ql-editor code {
font-size: 85%;
padding: 2px 4px;
}
.ql-snow .ql-editor pre.ql-syntax {
background-color: #23241f;
color: #f8f8f2;
overflow: visible;
}
.ql-snow .ql-editor img {
max-width: 100%;
}
.ql-snow .ql-picker {
color: $body-color;
display: inline-block;
float: left;
font-size: 14px;
font-weight: 600;
height: 24px;
position: relative;
vertical-align: middle;
}
.ql-snow .ql-picker-label {
cursor: pointer;
display: inline-block;
height: 100%;
padding-left: 8px;
padding-right: 2px;
position: relative;
width: 100%;
}
.ql-snow .ql-picker-label::before {
display: inline-block;
line-height: 22px;
}
.ql-snow .ql-picker-options {
background-color: #fff;
display: none;
min-width: 100%;
padding: 4px 8px;
position: absolute;
white-space: nowrap;
}
.ql-snow .ql-picker-options .ql-picker-item {
cursor: pointer;
display: block;
padding-bottom: 5px;
padding-top: 5px;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label {
color: $border-color;
z-index: 2;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill {
fill: $border-color;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
stroke: $border-color;
}
.ql-snow .ql-picker.ql-expanded .ql-picker-options {
display: block;
margin-top: -1px;
top: 100%;
z-index: 1;
}
.ql-snow .ql-color-picker,
.ql-snow .ql-icon-picker {
width: 28px;
}
.ql-snow .ql-color-picker .ql-picker-label,
.ql-snow .ql-icon-picker .ql-picker-label {
padding: 2px 4px;
}
.ql-snow .ql-color-picker .ql-picker-label svg,
.ql-snow .ql-icon-picker .ql-picker-label svg {
right: 4px;
}
.ql-snow .ql-icon-picker .ql-picker-options {
padding: 4px 0px;
}
.ql-snow .ql-icon-picker .ql-picker-item {
height: 24px;
width: 24px;
padding: 2px 4px;
}
.ql-snow .ql-color-picker .ql-picker-options {
padding: 3px 5px;
width: 152px;
}
.ql-snow .ql-color-picker .ql-picker-item {
border: 1px solid transparent;
float: left;
height: 16px;
margin: 2px;
padding: 0px;
width: 16px;
}
.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
position: absolute;
margin-top: -9px;
right: 0;
top: 50%;
width: 18px;
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before {
content: attr(data-label);
}
.ql-snow .ql-picker.ql-header {
width: 98px;
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: 'Normal';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: 'Heading 1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: 'Heading 2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: 'Heading 3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: 'Heading 4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: 'Heading 5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: 'Heading 6';
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
font-size: 2em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
font-size: 1.5em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
font-size: 1.17em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
font-size: 1em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
font-size: 0.83em;
}
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
font-size: 0.67em;
}
.ql-snow .ql-picker.ql-font {
width: 108px;
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: 'Sans Serif';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
content: 'Serif';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
content: 'Monospace';
}
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
font-family: Georgia, Times New Roman, serif;
}
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
font-family: Monaco, Courier New, monospace;
}
.ql-snow .ql-picker.ql-size {
width: 98px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: 'Normal';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
content: 'Small';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
content: 'Large';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
content: 'Huge';
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
font-size: 10px;
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
font-size: 32px;
}
.ql-snow .ql-color-picker.ql-background .ql-picker-item {
background-color: #fff;
}
.ql-snow .ql-color-picker.ql-color .ql-picker-item {
background-color: #000;
}
.ql-toolbar.ql-snow {
border: 1px solid $border-color;
padding: 8px;
background-color: white;
border-radius: $border-radius $border-radius 0 0;
}
.ql-toolbar.ql-snow .ql-formats {
margin-right: 15px;
}
.ql-toolbar.ql-snow .ql-picker-label {
border: 1px solid transparent;
}
.ql-toolbar.ql-snow .ql-picker-options {
border: 1px solid transparent;
box-shadow: rgba($body-color,0.2) 0 2px 8px;
}
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label {
border-color: $border-color;
}
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
border-color: $border-color;
}
.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,
.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover {
border-color: #000;
}
.ql-toolbar.ql-snow + .ql-container.ql-snow {
border-top: 0px;
}
.ql-snow .ql-tooltip {
background-color: #fff;
border: 1px solid $border-color;
box-shadow: 0px 0px 5px #ddd;
color: inherit;
padding: 5px 12px;
white-space: nowrap;
}
.ql-snow .ql-tooltip::before {
content: "Visit URL:";
line-height: 26px;
margin-right: 8px;
}
.ql-snow .ql-tooltip input[type=text] {
display: none;
border: 1px solid $border-color;
font-size: 13px;
height: 26px;
margin: 0px;
padding: 3px 5px;
width: 170px;
}
.ql-snow .ql-tooltip a.ql-preview {
display: inline-block;
max-width: 200px;
overflow-x: hidden;
text-overflow: ellipsis;
vertical-align: top;
}
.ql-snow .ql-tooltip a.ql-action::after {
border-right: 1px solid $border-color;
content: 'Edit';
margin-left: 16px;
padding-right: 8px;
}
.ql-snow .ql-tooltip a.ql-remove::before {
content: 'Remove';
margin-left: 8px;
}
.ql-snow .ql-tooltip a {
line-height: 26px;
}
.ql-snow .ql-tooltip.ql-editing a.ql-preview,
.ql-snow .ql-tooltip.ql-editing a.ql-remove {
display: none;
}
.ql-snow .ql-tooltip.ql-editing input[type=text] {
display: inline-block;
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: 'Save';
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode=link]::before {
content: "Enter link:";
}
.ql-snow .ql-tooltip[data-mode=formula]::before {
content: "Enter formula:";
}
.ql-snow .ql-tooltip[data-mode=video]::before {
content: "Enter video:";
}
.ql-snow a {
color: $link-color;
}
.ql-container.ql-snow {
border: 1px solid $border-color;
}
.text-editor {
height: 350px;
}
.navbar-dark {
&.bg-dark [class*="border-"] {
border-color: rgba(0,0,0,.34) !important;
}
&[class*=primary] [class*="border-"] {
border-color: rgba(white,.24) !important;
}
.search-form {
border-color: transparent;
.form-control {
color: rgba(white, .84);
}
}
&.bg-dark .search-form {
background-color: rgba(0,0,0,.24);
.form-control::placeholder {
color: #66768A;
font-weight: 500;
}
.btn {
color: rgba(white, .54);
}
}
&[class*=primary] .search-form {
background-color: rgba(0,0,0,.12);
.form-control::placeholder {
color: white;
}
.btn {
color: rgba(white, .54);
}
}
.navbar-toggler-custom {
background-color: rgba(black, .24);
.material-icons {
color: white;
}
}
.nav-icon,
.navbar-notifications-indicator {
color: white;
}
}
.list-skills {
li {
display: flex;
align-items: center;
&:not(:last-child) {
margin-bottom: 1rem;
}
> div + div {
padding-left: 1rem;
}
> div:first-child {
width: 110px;
}
}
&:last-child {
margin-bottom: 0;
}
}
@import url('https://fonts.googleapis.com/css?family=Open+Sans:600,700|Source+Sans+Pro:400,400i,600,700');
// $material-design-icons-font-path: '../fonts/material-icons/' !default;
$material-design-icons-font-path: 'material-design-icons-iconfont/dist/fonts/' !default;
$body-bg: #F7F8F9 !default;
$body-color: #112b4a !default;
$link-color: #4a90e2 !default;
$text-muted: rgba(#374D67, .54) !default;
$primary: #308AF3 !default;
$primary-dark: darken($primary, 10%) !default;
$success: #48BA16 !default;
$dark: #364C66 !default;
$info: #51A7C5 !default;
$enable-shadows: false !default; //https://github.com/twbs/bootstrap/issues/32019
$gutter: 1rem;
$mdc-theme-secondary: $primary !default;
$dark-gray: #939FAD !default;
$light-gray: #ECEEF0 !default;
$theme-colors: (
"teal": #16BA71,
"purple": #5163C5,
"dark-gray": $dark-gray,
"light-gray": $light-gray,
"muted-light": #DBDFE4,
) !default;
$font-size-base: .9375rem !default; // 15px
$font-family-base: 'Source Sans Pro', sans-serif;
$headings-font-family: 'Open Sans', monospace !default;
$small-font-size: .8125rem !default;
$headings-color: rgba($body-color, .84) !default;
// $paragraph-margin-bottom: .9375rem !default;
$bottom-spacing: 0.9375rem * 2 !default;
$border-color: #efefef !default;
$badge-font-size: .675rem !default;
$badge-font-weight: bold !default;
$badge-padding-y: .25rem !default;
$badge-padding-x: .25rem !default;
$badge-border-radius: 2px !default;
$breadcrumb-padding-y: 0 !default;
$breadcrumb-padding-x: 0 !default;
$breadcrumb-bg: transparent;
$breadcrumb-divider-color: rgba(#364C66, .24) !default;
$breadcrumb-active-color: rgba(#364C66, .54) !default;
$table-border-color: $border-color !default;
$table-cell-padding: .35rem 1rem !default;
//
// Buttons + Forms
//
// Shared variables that are reassigned to `$input-` and `$btn-` specific variables.
$btn-font-weight: 600 !default;
$input-btn-padding-y: .375rem !default;
$input-btn-padding-x: .875rem !default;
$input-btn-font-size: .875rem !default;
$input-btn-padding-y-sm: .15rem !default;
$input-btn-padding-x-sm: .5rem !default;
$input-btn-font-size-sm: .65rem !default;
$input-btn-padding-y-lg: .35rem !default;
$input-btn-padding-x-lg: .85rem !default;
$input-btn-font-size-lg: 1rem !default;
//
$input-border-color: #DBDFE4 !default;
$input-placeholder-color: #A6B0BC !default;
$input-focus-border-color: $primary !default;
$input-group-addon-bg: white !default;
$input-group-addon-color: $dark-gray !default;
$navbar-height: 64px !default;
$navbar-item-spacing: 1rem !default;
$navbar-nav-link-padding-x: .75rem !default;
$navbar-light-color: rgba(#384E68, .54) !default;
$navbar-light-active-color: rgba(17,43,74,.84) !default;
$navbar-dark-color: rgba(white, .54) !default;
$mdk-drawer-width: 300px !default;
$layout-mini-drawer-width: 64px !default;
$sidebar-spacing-x: 1.25rem !default;
$sidebar-spacing-y: 1rem !default;
$sidebar-heading-font-size: .6875rem !default;
$sidebar-heading-letter-spacing: .92px !default;
$sidebar-heading-font-weight: bold !default;
$sidebar-light-heading-color: rgba(17,43,74,.84) !default;
$sidebar-dark-heading-color: rgba(255,255,255,.84) !default;
$sm-icon-spacing-x: .625rem !default;
$sm-button-font-weight: 600 !default;
$sm-button-font-size: 1rem !default;
$ssm-button-height: 2rem !default;
$ssm-button-font-size: .875rem !default;
$sm-light-button-color: $dark-gray !default;
$sm-light-open-bg: #F7F8F9 !default;
$sm-light-icon-color: rgba(#A4AFBA, .5) !default;
$sm-light-hover-icon-color: $dark-gray !default;
$sm-light-active-button-color: rgba(#354B66, .84) !default;
$sm-light-active-icon-color: $primary !default;
$sm-light-toggle-color: #BBBCBD !default;
$sm-light-hover-button-bg: transparent !default;
$ssm-light-button-color: $sm-light-button-color !default;
$sm-light-open-button-color: #384E68 !default;
$sm-light-open-icon-color: $primary !default;
$sidebar-light-container-border-color: $border-color !default;
$sidebar-light-border-color: $border-color !default;
$sidebar-light-heading-color: rgba(#364C66, .3) !default;
$sidebar-dark-heading-color: rgba(white, .18) !default;
$sm-dark-button-color: rgba($dark-gray, .84) !default;
$sm-dark-icon-color: rgba(#A4AFBA, .5) !default;
$sm-dark-active-button-color: rgba(white, .84) !default;
$sm-dark-active-icon-color: $primary !default;
$sm-toggle-font-size: 18px !default;
$sm-toggle-icon: "\e148" !default;
$sm-open-toggle-icon: "\e15d" !default;
$dropdown-padding-y: .625rem !default;
$dropdown-border-color: $border-color !default;
$dropdown-border-radius: 5px !default;
$dropdown-box-shadow: 0 5px 11px 0 rgba(0,0,0,0.07) !default;
// $dropdown-link-color: $dark-gray !default;
$dropdown-link-hover-color: #384E68 !default;
$dropdown-link-hover-bg: transparent !default;
$dropdown-link-active-color: $primary !default;
$dropdown-link-active-bg: transparent !default;
$dropdown-item-padding-y: .25rem !default;
$dropdown-item-padding-x: 1.25rem !default;
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 966px
) !default;
$card-border-color: $border-color !default;
$list-group-item-padding-y: 1rem !default;
$list-group-border-color: $border-color !default;
$pagination-color: #929EAC !default;
$pagination-border-width: 0 !default;
$pagination-line-height: 30px !default;
$pagination-bg: #ECEEF0 !default;
$pagination-active-bg: $primary;
$pagination-disabled-bg: rgba($pagination-bg, .5) !default;
$pagination-disabled-color: rgba($pagination-color, .3) !default;
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/mixins';
// Sidebar style guide (default)
@import 'sidebar-style-guide/sass/variables';
// MDK (default)
@import 'material-design-kit/src/variables';
.page-item + .page-item {
margin-left: .05rem;
}
.page-link {
min-width: $pagination-line-height;
height: $pagination-line-height;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.5;
padding-top: 0;
padding-bottom: 0;
font-weight: 600;
&:hover {
background: $pagination-active-bg;
color: #fff;
}
}
.pagination-rounded {
.page-link {
border-radius: 100px;
}
.page-item + .page-item {
margin-left: .625rem;
}
.page-item {
&:first-child .page-link,
&:last-child .page-link {
@include border-radius(100px);
}
}
}
.pagination-light {
.page-item:not(.active) .page-link {
background: transparent;
&:hover {
color: $primary;
}
}
.page-item .page-link {
border-radius: 100px;
}
.page-item {
&:first-child .page-link,
&:last-child .page-link {
@include border-radius(100px);
}
}
}
.is-loading {
position: relative;
color: transparent !important;
}
.is-loading:after,
.loader {
display: block;
width: 1.5rem;
height: 1.5rem;
animation: 1s is-loading linear infinite;
border: 3px solid $body-color;
border-bottom-color: transparent;
border-radius: 50%;
background: transparent;
}
.is-loading-sm:after, .loader-sm {
width: 1rem;
height: 1rem;
border-width: 2px;
}
.is-loading-lg:after, .loader-lg {
width: 2rem;
height: 2rem;
border-width: 5px;
}
.is-loading > * {
opacity: 0 !important;
}
.is-loading:after {
position: absolute;
top: calc(50% - 1.5rem / 2);
left: calc(50% - 1.5rem / 2);
content: "";
}
.is-loading-sm:after {
top: calc(50% - 1rem / 2);
left: calc(50% - 1rem / 2);
}
.is-loading-lg:after {
top: calc(50% - 2rem / 2);
left: calc(50% - 2rem / 2);
}
@each $color, $value in $theme-colors {
.is-loading-#{$color}:after,
.btn-outline-#{$color}.is-loading:after,
.loader-#{$color} {
border-color: $value;
border-bottom-color: transparent;
}
.btn-#{$color}.is-loading:after {
border-color: color-yiq($value);
border-bottom-color: transparent;
}
}
@keyframes is-loading {
0% {
transform: rotate(0deg);
}
to {
transform: rotate(1turn);
}
}
.loader-list {
display: flex;
align-items: center;
> .loader {
margin-bottom: .75rem;
margin-left: .5rem;
}
}
.mdk-drawer__content {
background: transparent;
}
.mdk-header-layout .mdk-drawer__content {
top: $navbar-height;
}
.layout-mini {
.mdk-drawer__content {
display: flex;
}
.sidebar-mini {
width: $layout-mini-drawer-width;
.active.show .sidebar-menu-icon {
color: $dark;
}
.sidebar-menu > li.sidebar-menu-item.active {
&:not(.open) {
border-right: 0px;
}
}
.sidebar-menu-text,
.sidebar-badge,
.sidebar-menu-toggle-icon {
display: none;
}
.sidebar-menu-button {
justify-content: center;
padding-top: .5rem;
padding-bottom: .5rem;
}
.sidebar-menu-icon {
margin: 0;
}
}
@include media-breakpoint-up(sm) {
.navbar {
padding-left: 0;
}
.navbar-brand {
width: $layout-mini-drawer-width;
min-width: $layout-mini-drawer-width;
justify-content: center;
.navbar-brand-icon {
margin: 0;
}
> span {
display: none;
}
}
& &__d-none {
display: none;
}
}
}
.dashboard-area-tabs {
&__tab {
display: flex;
flex-direction: column;
@include hover {
text-decoration: none;
}
.text-label-large {
color: theme-color('muted-light');
}
&:not(.active) {
background-color: theme-color('light');
.text-amount {
color: theme-color('dark-gray');
}
}
&.active .text-label-large {
color: $primary;
}
}
}
.navbar-light {
.navbar-brand {
color: $dark;
}
.navbar-nav {
> .active {
position: relative;
&::before {
content: "";
background-color: $primary;
height: 2px;
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
}
}
.text-light {
color: $navbar-light-active-color !important;
}
}
//** CORE
// Variables
@import 'variables';
// Bootstrap
@import 'bootstrap/scss/bootstrap';
// MDK
@import 'material-design-kit/src/style';
// SIMPLEBAR (SCROLL)
@import 'simplebar/dist/simplebar';
//** PLUGINS (NODE_MODULES)
@import "daterangepicker/daterangepicker";
@import "jqvmap/dist/jqvmap.min";
@import "toastr/toastr";
@import "toastr/toastr";
@import "vendor-select2";
@import "select2/dist/css/select2.min";
@import "dragula/dist/dragula.min";
@import "fullcalendar/dist/fullcalendar.min";
@import "fullcalendar";
@import "vendor-fontawesome-free";
@import "vendor-quill";
@import "vendor-dropzone";
//** APP
@import 'flexbox';
@import 'icons';
@import 'type';
@import 'layout';
@import 'dropdown';
@import 'header';
@import 'search-form';
@import 'navbar';
@import 'navbar-dark';
@import 'navbar-light';
@import 'drawer';
@import 'sidebar';
@import 'card';
@import 'table';
@import 'button';
@import 'breadcrumb';
@import 'form';
@import 'input-group';
@import 'progress';
@import 'tabs';
@import 'list-group';
@import 'borders';
@import 'custom-checkbox-toggle';
@import 'alert';
@import 'loader';
@import 'helpers';
@import 'badge';
@import 'avatar';
@import 'chart';
@import 'pagination';
@import 'page-separator';
@import 'apps/trello';
@import 'apps/chat';
@import 'daterangepicker';
@import 'toastr';
@import 'fullcalendar';
@import 'preloader';
//** PAGES
@import 'dashboard-area-tabs';
@import 'dashboard-location-tabs';
@import 'todo';
@import 'skills';
@import 'login';
@import 'login-centered-boxed';
@import 'stories';
@import 'pricing';
@import 'map';
.stories {
&-card {
margin-bottom: $grid-gutter-width/2;
&:last-child {
margin-bottom: 0;
}
&__content {
padding: $spacer/2;
}
&__title {
white-space: nowrap;
}
&__title,
.card-title,
.card-title > a {
max-width: 180px;
}
.card-title > a {
display: inline-block;
overflow-x: hidden;
text-overflow: ellipsis;
}
@include media-breakpoint-down(lg) {
&__meta {
margin-top: .4375rem;
padding-top: .4375rem;
border-top: 1px solid $border-color;
width: 100%;
order: 3;
align-items: flex-start;
}
}
@include media-breakpoint-up(sm) {
&__tag {
width: 100px;
}
&__date {
width: 140px;
}
&__title,
.card-title,
.card-title > a {
max-width: 300px;
}
}
}
&-cards {
margin-bottom: $card-group-margin;
}
&-card-popular {
position: relative;
overflow: hidden;
&__content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(theme-color('dark'), .125);
}
.card-img {
height: 256px;
object-fit: cover;
}
a {
color: white;
}
&__title {
position: absolute;
left: 0;
bottom: 0;
.text-muted {
color: rgba(255,255,255, .54) !important;
}
}
}
}
.custom-control-label::before {
box-shadow: none;
background-color: white;
border: 1px solid #EFEFEF;
}
.custom-control-input {
&:checked ~ .custom-control-label::before {
border: none;
}
}
%label {
font-size: .8rem;
text-transform: uppercase;
letter-spacing: 1.2px;
color: rgba($dark-gray, .84);
line-height: .9375rem;
margin-bottom: .5rem;
font-weight: bold;
}
.text-label {
@extend %label;
}
.text-label-large {
@extend %label;
font-size: $font-size-base;
letter-spacing: 1px;
font-weight: bold;
line-height: 1.5rem;
}
.card-form {
.card-body {
padding: 1.6875rem;
}
.card-body-form-group {
padding: $card-spacer-x;
padding-bottom: $card-spacer-x - $form-group-margin-bottom;
}
&__body {
background-color: #F3F5F6;
label:not([class]) {
@extend %label;
}
}
}
.form-control:focus {
box-shadow: none !important;
}
.form-control-rounded {
border-radius: 20rem;
}
.form-control-flush {
padding: 0;
border-width: 0;
box-shadow: none;
background-color: transparent;
&:focus {
background-color: transparent;
box-shadow: none;
}
}
.custom-file-naked {
cursor: pointer;
width: auto;
height: auto;
.custom-file-input {
width: auto;
height: auto;
cursor: pointer;
line-height: 1;
}
.custom-file-label {
border: none;
background: none;
padding: 0;
margin: 0;
height: auto;
cursor: pointer;
line-height: 1;
box-shadow: none;
&:after {
display: none;
}
}
}
.custom-select{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
#toast-container > div {
opacity: 1;
box-shadow: 0 10px 25px 0 rgba(50,50,93,0.07), 0 5px 15px 0 rgba(0,0,0,0.07);
}
@each $color, $value in $theme-colors {
.toast-#{$color} {
background-color: $value;
}
}
.breadcrumb-item {
text-transform: uppercase;
letter-spacing: 0.85px;
font-size: .825rem;
font-weight: 600;
i.material-icons {
position: relative;
top: -1px;
}
a {
color: #364C66;
&:hover {
color: $link-color;
}
}
}
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
& + [aria-label="breadcrumb"] {
margin-top: -.6rem;
}
}
/*!
* baguetteBox.js
* @author feimosi
* @version 1.11.0
* @url https://github.com/feimosi/baguetteBox.js
*/
#baguetteBox-overlay {
display: none;
opacity: 0;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000000;
background-color: #222;
background-color: rgba(0, 0, 0, 0.8);
-webkit-transition: opacity .5s ease;
transition: opacity .5s ease; }
#baguetteBox-overlay.visible {
opacity: 1; }
#baguetteBox-overlay .full-image {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
text-align: center; }
#baguetteBox-overlay .full-image figure {
display: inline;
margin: 0;
height: 100%; }
#baguetteBox-overlay .full-image img {
display: inline-block;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
vertical-align: middle;
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); }
#baguetteBox-overlay .full-image figcaption {
display: block;
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: 1.8;
white-space: normal;
color: #ccc;
background-color: #000;
background-color: rgba(0, 0, 0, 0.6);
font-family: sans-serif; }
#baguetteBox-overlay .full-image:before {
content: "";
display: inline-block;
height: 50%;
width: 1px;
margin-right: -1px; }
#baguetteBox-slider {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
white-space: nowrap;
-webkit-transition: left .4s ease, -webkit-transform .4s ease;
transition: left .4s ease, -webkit-transform .4s ease;
transition: left .4s ease, transform .4s ease;
transition: left .4s ease, transform .4s ease, -webkit-transform .4s ease, -moz-transform .4s ease; }
#baguetteBox-slider.bounce-from-right {
-webkit-animation: bounceFromRight .4s ease-out;
animation: bounceFromRight .4s ease-out; }
#baguetteBox-slider.bounce-from-left {
-webkit-animation: bounceFromLeft .4s ease-out;
animation: bounceFromLeft .4s ease-out; }
@-webkit-keyframes bounceFromRight {
0% {
margin-left: 0; }
50% {
margin-left: -30px; }
100% {
margin-left: 0; } }
@keyframes bounceFromRight {
0% {
margin-left: 0; }
50% {
margin-left: -30px; }
100% {
margin-left: 0; } }
@-webkit-keyframes bounceFromLeft {
0% {
margin-left: 0; }
50% {
margin-left: 30px; }
100% {
margin-left: 0; } }
@keyframes bounceFromLeft {
0% {
margin-left: 0; }
50% {
margin-left: 30px; }
100% {
margin-left: 0; } }
.baguetteBox-button#next-button, .baguetteBox-button#previous-button {
top: 50%;
top: calc(50% - 30px);
width: 44px;
height: 60px; }
.baguetteBox-button {
position: absolute;
cursor: pointer;
outline: none;
padding: 0;
margin: 0;
border: 0;
-moz-border-radius: 15%;
border-radius: 15%;
background-color: #323232;
background-color: rgba(50, 50, 50, 0.5);
color: #ddd;
font: 1.6em sans-serif;
-webkit-transition: background-color .4s ease;
transition: background-color .4s ease; }
.baguetteBox-button:focus, .baguetteBox-button:hover {
background-color: rgba(50, 50, 50, 0.9); }
.baguetteBox-button#next-button {
right: 2%; }
.baguetteBox-button#previous-button {
left: 2%; }
.baguetteBox-button#close-button {
top: 20px;
right: 2%;
right: calc(2% + 6px);
width: 30px;
height: 30px; }
.baguetteBox-button svg {
position: absolute;
left: 0;
top: 0; }
/*
Preloader
Borrowed from http://tobiasahlin.com/spinkit/
*/
.baguetteBox-spinner {
width: 40px;
height: 40px;
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px; }
.baguetteBox-double-bounce1,
.baguetteBox-double-bounce2 {
width: 100%;
height: 100%;
-moz-border-radius: 50%;
border-radius: 50%;
background-color: #fff;
opacity: .6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: bounce 2s infinite ease-in-out;
animation: bounce 2s infinite ease-in-out; }
.baguetteBox-double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s; }
@-webkit-keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
transform: scale(0); }
50% {
-webkit-transform: scale(1);
transform: scale(1); } }
@keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
-moz-transform: scale(0);
transform: scale(0); }
50% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
// Place all the styles related to the PublishedJobPages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the locations controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
@media only screen and (max-width: 400px) {
.invoice-box {
overflow-x: auto !important;
table td {
font-size: 12px;
}
.li-delete-btn {
font-size: 20px !important;
}
.li-desc {
width: 95vw;
}
.custom-font-size {
font-size: 1rem;
}
}
}
@media(max-width: 450px) {
.logo-container{
max-width: 95%;
max-height: 95%;
}
}
@media(min-width: 451px) {
.logo-container{
max-width: 75%;
max-height: 75%;
}
}
.logo-container{
position: relative;
.logo-upload {
position: absolute;
height: 40px;
width: 40px;
background: white;
box-shadow: rgba(153, 104, 104, 0.35) 0px 5px 15px;
border-radius: 50%;
top: 80%;
left: 45%;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s;
cursor: pointer;
z-index: 1040;
}
.logo-upload:hover {
box-shadow: rgba(100, 100, 111, 20%) 0px 7px 29px 0px;
}
.logo-option {
display: none;
font-size: 12px;
padding: 0;
margin: 0;
position: absolute;
list-style: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: white;
box-shadow: rgba(149, 157, 165, 20%) 0px 8px 24px;
top: 100%;
left: 55%;
border-radius: 5px;
li {
display: flex;
height: 40px;
width: 100px;
justify-content: center;
align-items: center;
padding: 0 4px;
cursor: pointer;
}
li:hover {
background: rgba(0,0,0,0.05);
}
}
}
.editable-icon {
position: absolute;
height: 25px;
width: 25px;
border-radius: 50%;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
top: 0;
right: -30px;
transition: all 0.3s;
}
.restrict-remove {
color: white;
background: rgba(255, 0, 0, 0.5);
cursor: not-allowed;
&:hover {
background: rgba(255, 0, 0, 0.5) !important;
cursor: not-allowed !important;
}
}
// Place all the styles related to the products controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the line_item_templates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the subscriptions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the AppointmentColors controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the payments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the job_statuses controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the Timesheets controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
.timesheet-card-header{
border-radius: 13px !important; //Adding !important because bootstrap.scss is overriding because we have the _card class on it..
}
.container {
max-width: 960px;
}
/*
* Custom translucent site header
*/
.site-header {
background-color: rgba(0, 0, 0, .85);
-webkit-backdrop-filter: saturate(180%) blur(20px);
backdrop-filter: saturate(180%) blur(20px);
}
.site-header a {
color: #999;
transition: ease-in-out color .15s;
}
.site-header a:hover {
color: #fff;
text-decoration: none;
}
/*
* Dummy devices (replace them with your own or something else entirely!)
*/
.product-device {
position: absolute;
right: 10%;
bottom: -30%;
width: 300px;
height: 540px;
background-color: #333;
border-radius: 21px;
-webkit-transform: rotate(30deg);
transform: rotate(30deg);
}
.product-device::before {
position: absolute;
top: 10%;
right: 10px;
bottom: 10%;
left: 10px;
content: "";
background-color: rgba(255, 255, 255, .1);
border-radius: 5px;
}
.product-device-2 {
top: -25%;
right: auto;
bottom: 0;
left: 5%;
background-color: #e5e5e5;
}
/*
* Extra utilities
*/
.border-top { border-top: 1px solid #e5e5e5; }
.border-bottom { border-bottom: 1px solid #e5e5e5; }
.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
.flex-equal > * {
-ms-flex: 1;
-webkit-box-flex: 1;
flex: 1;
}
@media (min-width: 768px) {
.flex-md-equal > * {
-ms-flex: 1;
-webkit-box-flex: 1;
flex: 1;
}
}
.overflow-hidden { overflow: hidden; }
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.container {
max-width: 960px;
}
.pricing-header {
max-width: 700px;
}
.card-deck .card {
min-width: 220px;
}
.border-top { border-top: 1px solid #e5e5e5; }
.border-bottom { border-bottom: 1px solid #e5e5e5; }
.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-200;
}
}
*/
.chat {
border: solid 1px #EEE;
}
.message-info{
color: grey;
font-size:78%;
}
.message-info-mine {
@extend .message-info;
text-align: right
}
.message-info-yours{
@extend .message-info;
text-align: left
}
.messages {
margin-top: 30px;
display: flex;
flex-direction: column;
}
.hsc-message {
border-radius: 20px;
padding: 8px 15px;
margin-top: 5px;
margin-bottom: 5px;
display: inline-block;
}
.yours {
align-items: flex-start;
}
.yours .hsc-message {
margin-right: 25%;
background-color: #eee;
position: relative;
}
.yours .hsc-message.last:before {
content: "";
position: absolute;
z-index: 0;
bottom: 0;
left: -7px;
height: 20px;
width: 20px;
background: #eee;
border-bottom-right-radius: 15px;
}
.yours .hsc-message.last:after {
content: "";
position: absolute;
z-index: 1;
bottom: 0;
left: -10px;
width: 10px;
height: 20px;
background: white;
border-bottom-right-radius: 10px;
}
.mine {
align-items: flex-end;
}
.mine .hsc-message {
color: white;
margin-left: 25%;
// background: linear-gradient(to bottom, #00D0EA 0%, #0085D1 100%);
background: #0085D1;
background-attachment: fixed;
position: relative;
}
.mine .hsc-message.last:before {
content: "";
position: absolute;
z-index: 0;
bottom: 0;
right: -8px;
height: 20px;
width: 20px;
// background: linear-gradient(to bottom, #00D0EA 0%, #0085D1 100%);
background:#0085D1;
background-attachment: fixed;
border-bottom-left-radius: 15px;
}
.mine .hsc-message.last:after {
content: "";
position: absolute;
z-index: 1;
bottom: 0;
right: -10px;
width: 10px;
height: 20px;
background: white;
border-bottom-left-radius: 10px;
}
// Place all the styles related to the Events controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the Calls controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the branches controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the text_templates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
// Place all the styles related to the Jobs controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
@import "ace";
#search-field-group {
display: inline;
font-size: 20px;
margin-bottom: 0.5rem;
position: relative;
i {
position: absolute;
left: 0;
padding: 9px 8px;
color: #666;
}
input {
display: inline;
width: 35%;
padding-left: 40px;
-webkit-transition: width 0.4s ease-in-out;
transition: width 0.6s ease-in-out;
&:focus {
width: 98%;
}
&:focus + i {
color: #333;
}
}
}
.weekly-assignment-viewer{
text-align: center;
}
.remove-mobile-longpress-menu{
-webkit-touch-callout: none !important;
-webkit-user-select: none !important;
}
.topnav {
overflow: hidden;
background-color: #ffff;
}
.topnav a {
float: left;
color: #202020;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.topnav button {
float: left;
color: #202020;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
background: none!important;
border: none;
padding: 0!important;
/*optional*/
/*input has OS specific font-family*/
cursor: pointer;
}
.topnav a:hover, button:hover {
text-decoration: none;
color: $company-red;
}
.topnav button.active {
color: $company-blue;
}
header.masthead {
position: relative;
overflow: hidden;
padding-top: calc(7rem + 72px);
padding-bottom: 7rem;
background: linear-gradient(0deg, #54504e 0%, #100a0d 100%);
background-repeat: no-repeat;
background-position: center center;
background-attachment: scroll;
background-size: cover;
.masthead-content {
z-index: 1;
position: relative;
.masthead-heading {
font-size: 4rem;
}
.masthead-subheading {
font-size: 2rem;
}
}
.bg-circle {
z-index: 0;
position: absolute;
border-radius: 100%;
background: linear-gradient(0deg, $primary 0%, $secondary 100%);
}
.bg-circle-1 {
height: 90rem;
width: 90rem;
bottom: -55rem;
left: -55rem;
}
.bg-circle-2 {
height: 50rem;
width: 50rem;
top: -25rem;
right: -25rem;
}
.bg-circle-3 {
height: 20rem;
width: 20rem;
bottom: -10rem;
right: 5%;
}
.bg-circle-4 {
height: 30rem;
width: 30rem;
top: -5rem;
right: 35%;
}
}
@media (min-width: 992px) {
header.masthead {
padding-top: calc(10rem + 55px);
padding-bottom: 10rem;
.masthead-content {
.masthead-heading {
font-size: 6rem;
}
.masthead-subheading {
font-size: 4rem;
}
}
}
}
.bg-primary {
background-color: $primary !important;
}
.btn-primary {
background-color: $primary;
border-color: $primary;
&:active,
&:focus,
&:hover {
background-color: darken($primary, 10%) !important;
border-color: darken($primary, 10%) !important;
}
&:focus {
box-shadow: 0 0 0 0.2rem fade-out($primary, 0.5);
}
}
.btn-secondary {
background-color: $secondary;
border-color: $secondary;
&:active,
&:focus,
&:hover {
background-color: darken($secondary, 10%) !important;
border-color: darken($secondary, 10%) !important;
}
&:focus {
box-shadow: 0 0 0 0.2rem fade-out($secondary, 0.5);
}
}
// Core variables and mixins
@import "variables.scss";
// Global CSS
@import "global.scss";
// Components
@import "masthead.scss";
@import "bootstrap-overrides.scss";
.btn-xl {
text-transform: uppercase;
padding: 1.5rem 3rem;
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.1rem;
}
.bg-black {
background-color: $black !important;
}
.rounded-pill {
border-radius: 5rem;
}
// Variables
// Restated Bootstrap Variables
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #b7babd !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$primary: #858282 !default;
$secondary: #868483 !default;
$company-blue: #6080b0;
$company-blue-hover: darken( $company-blue, 10% );
$company-red: #5a9404;
$company-bright-red: #b71422;
$company-red-hover: darken( $company-bright-red, 5% );
$company-grey: #626060;
$company-grey-hover: darken( $company-grey, 5% );
a {
@extend .company-link;
}
.company-header {
// color: $company-blue;
// text-transform: uppercase;
}
.btn-company-danger {
background-color: $company-bright-red;
border-color: $company-bright-red;
color:white;
}
.btn-company-danger:hover {
background-color: $company-red-hover;
border-color: $company-red-hover;
color:white;
}
.btn-company-success {
background-color: $company-bright-red;
border-color: $company-bright-red;
color:white;
}
.btn-company-success:hover {
background-color: $company-red-hover;
border-color: $company-red-hover;
color:white;
}
.btn-company-primary {
background-color: $company-blue;
border-color: $company-blue;
color:white;
}
.btn-company-primary:hover{
background-color: $company-blue-hover;
border-color: $company-blue-hover;
color:white;
}
.btn-company-warning {
color: white;
background-color: $company-grey;
border-color: $company-grey;
}
.btn-company-warning:hover {
color: white;
background-color: $company-grey-hover;
border-color: $company-grey-hover;
}
.company-link {
color: $company-blue;
:hover {
color: $company-red;
text-decoration: none;
}
}
.btn-company-link {
@extend .company-link;
border:none;
background-color:inherit;
}
.sidebar-menu-text{
color: #444444;
}
.sidebar-menu-button{
color: #444444;
}
.med-text{
font-size:1.3em;
}
.clickable-card:hover{
transform:scale(1.1);
}
/////END COMPANY SPECIFIC STUFF
/**
* The css styling for our custom context menu.
*/
.custom-menu {
display: none;
z-index: 1000;
position: absolute;
overflow: hidden;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #FFF;
color: #333;
border-radius: 5px;
padding-left: 0;
}
.custom-menu li {
padding: 8px 12px;
cursor: pointer;
list-style-type: none; //Get rid of the bullet points on our custom context menu
}
.custom-menu li:hover {
background-color: #DEF;
}
body {
// background-image: linear-gradient(to top, #ecedee 0%, #eceeef 75%, #e7e8e9 100%);
min-height: 100vh;
font: normal 16px sans-serif;
padding: 60px 0;
}
.container.gallery-container {
background-color: #fff;
color: #35373a;
min-height: 100vh;
border-radius: 20px;
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.06);
}
.gallery-container h1 {
text-align: center;
margin-top: 70px;
font-family: 'Droid Sans', sans-serif;
font-weight: bold;
}
.gallery-container p.page-description {
text-align: center;
max-width: 800px;
margin: 25px auto;
color: #888;
font-size: 18px;
}
.tz-gallery {
padding: 40px;
}
.tz-gallery .lightbox img {
width: 100%;
margin-bottom: 30px;
transition: 0.2s ease-in-out;
box-shadow: 0 2px 3px rgba(0,0,0,0.2);
}
.tz-gallery .lightbox img:hover {
transform: scale(1.05);
box-shadow: 0 8px 15px rgba(0,0,0,0.3);
}
.tz-gallery img {
border-radius: 4px;
}
.baguetteBox-button {
background-color: transparent !important;
}
@media(max-width: 768px) {
body {
padding: 0;
}
.container.gallery-container {
border-radius: 0;
}
}
// Place all the styles related to the Invoices controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.invoice-box {
max-width: 800px;
margin: auto;
padding: 30px;
border: 1px solid #eee;
box-shadow: 0 0 10px rgba(0, 0, 0, .15);
font-size: 16px;
line-height: 24px;
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
color: #555;
// overflow-x: auto !important;
}
.invoice-box table {
width: 100%;
line-height: inherit;
text-align: left;
}
.invoice-box table td {
padding: 5px;
vertical-align: top;
}
.invoice-box table tr td:nth-child(2) { //This is what was causing the line items to get justified in a funky way -- "3 or 4 spaces between words". As Spencer Townsend said.
// text-align: justify;
}
.invoice-box table tr.top table td {
padding-bottom: 20px;
}
.invoice-box table tr.top table td.title {
font-size: 45px;
line-height: 45px;
color: #333;
}
.invoice-box table tr.information table td {
padding-bottom: 40px;
}
.invoice-box table tr.heading td {
background: #eee;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.invoice-box table tr.details td {
padding-bottom: 20px;
}
.invoice-box table tr.line-item td{
border-bottom: 1px solid #eee;
}
.invoice-box table tr.line-item.last td {
border-bottom: none;
}
.invoice-box table tr.total td:nth-child(2) {
border-top: 2px solid #eee;
font-weight: bold;
}
.line-item-mobile-container {
border:#333;
}
.isDisabled {
color: currentColor;
cursor: not-allowed;
opacity: 0.5;
text-decoration: none;
}
// @media only screen and (min-width: 960px) {
// /* styles for browsers larger than 960px; */
// }
// @media only screen and (min-width: 1440px) {
// /* styles for browsers larger than 1440px; */
// }
// @media only screen and (min-width: 2000px) {
// /* for sumo sized (mac) screens */
// }
@media only screen and (max-device-width: 480px) {
/* styles for mobile browsers smaller than 480px; (iPhone) */
.line-item-desktop {
display: none;
}
.invoice-desktop {
display: none;
}
.invoice-box {
padding: 5px;
overflow-x: auto !important;
}
}
@media only screen and (min-device-width: 481px) {
/* default iPad screens */
.line-item-mobile {
display:none;
}
.invoice-mobile
{
display:none;
}
}
// /* different techniques for iPad screening */
// @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:portrait) {
// /* For portrait layouts only */
// }
// @media only screen and (min-device-width: 481px) and (max-device-width: 1024px) and (orientation:landscape) {
// /* For landscape layouts only */
// }
/** RTL **/
.rtl {
direction: rtl;
font-family: Tahoma, 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
}
.rtl table {
text-align: right;
}
.rtl table tr td:nth-child(2) {
text-align: left;
}
.line-item-price {
white-space: no-wrap;
}
.gallery_container {
max-width: 800px;
margin: 0 auto;
.tz-gallery {
padding: 40px;
img {
transition: 0.2s ease-in-out;
border-radius: 4px;
&:hover {
transform: scale(1.05);
box-shadow: 0 8px 15px #0000004d;
}
}
}
}
.edit-page-invoice-images {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
//CSS for Payment Button
.ibx-pay-button {background-image: linear-gradient(to right, #314755 0%, #26a0da 51%, #314755 100%)}
.ibx-pay-button {
display: inline-block;
font-weight: 1000;
text-align: center;
vertical-align: middle;
padding: 5px 15px;
transition: 0.5s;
background-size: 200% auto;
color: white;
box-shadow: 0 0 20px #eee;
border-radius: 5px;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
font-size: 0.875rem;
line-height: 1.5;
}
.ibx-pay-button:hover {
background-position: right center; /* change the direction of the change here */
color: #fff;
text-decoration: none;
}
.ibx-pay-button:disabled {
opacity: 0.65;
}
//------------------------------CUSTOM CSS FOR INVOICE PAY PAGE------------------------------
.gradient-buttons {
font-size: 15px;
font-weight: bold;
background-image: linear-gradient(to bottom, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.15) 51%, rgba(0,0,0,0.05));
background-repeat: repeat-x;
}
#payment-modal-shadow {
display: none;
height: 100%;
width: 100%;
background: black;
position: fixed;
top: 0;
left: 0;
opacity: 0.5;
z-index: 1040;
}
#require-signature-notice {
color: #b53416;
display: none;
}
// .modal-backdrop {
// display: none;
// }
#payment_modal_style{
.modal{
top: 15% !important;
}
.times_btn {
position: absolute;
top: 25px;
left: 5px;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.input-group-text {
border-radius: 0.25rem 0 0 0.25rem;
}
.m-head {
justify-content: center;
}
.m-title {
font-weight: bold;
}
#payment_button {
width:100%;
font-size: inherit;
font-weight: bold;
}
}
// Place all the styles related to the HomePages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the LineItems controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
body {
background-color: #fff;
color: #333;
margin: 33px;
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a {
color: #000;
&:visited {
color: #666;
}
&:hover {
color: #fff;
background-color: #000;
}
}
th {
padding-bottom: 5px;
}
td {
padding: 0 5px 7px;
}
div {
&.field, &.actions {
margin-bottom: 10px;
}
}
#notice {
color: green;
}
.field_with_errors {
padding: 2px;
background-color: red;
display: table;
}
#error_explanation {
width: 450px;
border: 2px solid red;
padding: 7px 7px 0;
margin-bottom: 20px;
background-color: #f0f0f0;
h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px -7px 0;
background-color: #c00;
color: #fff;
}
ul li {
font-size: 12px;
list-style: square;
}
}
label {
display: block;
}
/*
* bootstrap-tagsinput v0.8.0
*
*/
.bootstrap-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
color: #555;
vertical-align: middle;
border-radius: 4px;
width: 100%;
line-height: 22px;
cursor: text;
}
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0 6px;
margin: 0;
width: auto;
max-width: inherit;
}
.bootstrap-tagsinput.form-control input::-moz-placeholder {
color: #777;
opacity: 1;
}
.bootstrap-tagsinput.form-control input:-ms-input-placeholder {
color: #777;
}
.bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
color: #777;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .badge {
margin: 2px 0;
padding:5px 8px;
}
.bootstrap-tagsinput .badge [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.bootstrap-tagsinput .badge [data-role="remove"]:after {
content: "×";
padding: 0px 4px;
background-color:rgba(0, 0, 0, 0.1);
border-radius:50%;
font-size:13px
}
.bootstrap-tagsinput .badge [data-role="remove"]:hover:after {
background-color:rgba(0, 0, 0, 0.62);}
.bootstrap-tagsinput .badge [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.tt-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
background-color: #ffffff;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
cursor: pointer;
}
.tt-suggestion {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.428571429;
color: #333333;
white-space: nowrap;
}
.tt-suggestion:hover,
.tt-suggestion:focus {
color: #ffffff;
text-decoration: none;
outline: 0;
background-color: #428bca;
}
/*! Flickity v2.3.0
https://flickity.metafizzy.co
---------------------------------------------- */
.flickity-enabled {
position: relative;
}
.flickity-enabled:focus { outline: none; }
.flickity-viewport {
overflow: hidden;
position: relative;
height: 100%;
}
.flickity-slider {
position: absolute;
width: 100%;
height: 100%;
}
/* draggable */
.flickity-enabled.is-draggable {
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.flickity-enabled.is-draggable .flickity-viewport {
cursor: move;
cursor: -webkit-grab;
cursor: grab;
}
.flickity-enabled.is-draggable .flickity-viewport.is-pointer-down {
cursor: -webkit-grabbing;
cursor: grabbing;
}
/* ---- flickity-button ---- */
.flickity-button {
position: absolute;
background: hsla(0, 0%, 100%, 0.75);
border: none;
color: #333;
}
.flickity-button:hover {
background: white;
cursor: pointer;
}
.flickity-button:focus {
outline: none;
box-shadow: 0 0 0 5px #19F;
}
.flickity-button:active {
opacity: 0.6;
}
.flickity-button:disabled {
opacity: 0.3;
cursor: auto;
/* prevent disabled button from capturing pointer up event. #716 */
pointer-events: none;
}
.flickity-button-icon {
fill: currentColor;
}
/* ---- previous/next buttons ---- */
.flickity-prev-next-button {
top: 50%;
width: 44px;
height: 44px;
border-radius: 50%;
/* vertically center */
transform: translateY(-50%);
}
.flickity-prev-next-button.previous { left: 10px; }
.flickity-prev-next-button.next { right: 10px; }
/* right to left */
.flickity-rtl .flickity-prev-next-button.previous {
left: auto;
right: 10px;
}
.flickity-rtl .flickity-prev-next-button.next {
right: auto;
left: 10px;
}
.flickity-prev-next-button .flickity-button-icon {
position: absolute;
left: 20%;
top: 20%;
width: 60%;
height: 60%;
}
/* ---- page dots ---- */
.flickity-page-dots {
position: absolute;
width: 100%;
bottom: -25px;
padding: 0;
margin: 0;
list-style: none;
text-align: center;
line-height: 1;
}
.flickity-rtl .flickity-page-dots { direction: rtl; }
.flickity-page-dots .dot {
display: inline-block;
width: 10px;
height: 10px;
margin: 0 8px;
background: #333;
border-radius: 50%;
opacity: 0.25;
cursor: pointer;
}
.flickity-page-dots .dot.is-selected {
opacity: 1;
}
// Place all the styles related to the customer_contact controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#images-upload-modal {
.modal-footer {
display: none;
}
.footer-buttons{
float: right;
}
}
#image_upload {
display: none;
background-color: white;
position: fixed;
width: 50vw;
height: fit-content;
padding: 0 !important;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: #959da5 0px 8px 24px;
border-radius: 10px;
z-index: 9999;
opacity: 1;
overflow: visible;
@media only screen and (max-width: 768px) {
width: 80vw;
}
.modal-header {
align-items: center;
.container {
width: fit-content;
}
}
.modal-dialog {
transform: translate(0, 0) !important;
max-width: 100% !important;
margin: 0;
}
}
.btn-underline {
border-bottom: 1px solid #8080803d;
}
.modal-btn-grp {
width: fit-content;
}
.btn-underline-dark {
border-bottom: 1px solid gray;
}
.modal-img {
height: 150px;
width: 150px;
box-shadow: #959DA5 0px 8px 24px;
}
.menu-btn{
height: 300px;
overflow-y: auto;
div {
height: 100%;
text-align: center;
}
}
.btn-top-border {
border-top: 1px solid #8080803d;
}
// Place all the styles related to the invoice_templates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#template_name {
width: 90%;
text-align: center;
padding: 4px;
}
// Place all the styles related to the ForwardingNumbers controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the Reviews controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the JobRequests controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
:root {
--black: #1f1e1e;
--litegray: #f0f0f0;
--gray: #777777;
--night: #0f0f1e;
}
.job-request {
font-family: "Poppins", sans-serif;
.container {
max-width: 1200px;
margin: auto;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.logo123 {
width: 100%;
max-width: 250px;
max-height: 300px;
}
.wraper {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 5em;
align-items: center;
margin: 5em 0;
margin-top: 7em;
}
.header {
text-align: right;
color: var(--black);
}
.title {
font-size: 50px;
}
.sub-title {
font-size: 20px;
}
form {
width: 100%;
background: white;
padding: 3em;
border-radius: 10px;
box-shadow: -10px 20px 70px #0000002d;
}
form input,
textarea,
select {
width: 100%;
outline: none;
background: var(--litegray) !important;
padding: 0 15px;
border-radius: 5px;
height: 45px;
}
select {
margin: 1em 0;
font-size: 16px;
color: var(--black);
font-weight: 600;
border-radius: 5px;
}
textarea {
height: 150px;
resize: none;
padding: 15px;
}
textarea::placeholder {
font-size: 16px;
color: var(--gray);
font-weight: 600;
}
.row-sm {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1em;
}
.upload {
background: var(--litegray);
height: 45px;
width: 100%;
margin: 1em 0;
display: flex;
align-items: center;
border-radius: 5px;
cursor: pointer;
justify-content: center;
}
input[type="file"] {
display: none;
}
label span {
margin-right: 10px;
}
.upload label {
display: flex;
align-items: center;
justify-content: center;
color: var(--gray);
cursor: pointer;
font-weight: 500;
}
.input .lebel {
color: var(--gray);
font-size: 16px;
text-transform: uppercase;
font-weight: 500;
margin: 0;
padding: 0;
}
.input {
margin-bottom: 1em;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
border: none;
outline: none;
margin: auto;
height: 45px;
margin-top: 50px;
cursor: pointer;
color: white;
background: linear-gradient(93.36deg, #ea3223 0%, #f26a5f 98.82%);
border-radius: 5px;
font-size: 18px;
text-transform: uppercase;
}
.btn i {
margin-left: 10%;
}
.art {
margin-bottom: 5em;
}
.main {
padding: 5em 0;
padding-bottom: 8em;
}
/* indicator */
.left,
.header,
.title {
position: relative;
}
.left::before {
content: "";
left: 0;
height: 100%;
width: 2px;
background: var(--black);
position: absolute;
top: 38px;
}
.arr {
color: var(--black);
position: absolute;
bottom: -40px;
width: 100%;
}
.main img {
width: 30%;
}
.tnx {
text-transform: capitalize;
}
.s-tnx {
font-size: 25px;
color: var(--gray);
font-weight: 500;
}
.buttons {
display: grid;
grid-template-columns: 1fr 1fr;
width: 50%;
margin: auto;
gap: 1em;
margin-top: 50px;
}
.btn-2 {
display: flex;
align-items: center;
justify-content: center;
color: white;
background: linear-gradient(93.36deg, #ea3223 0%, #f26a5f 98.82%);
border: none;
height: 50px;
font-size: 18px;
border-radius: 5px;
cursor: pointer;
}
.btn-2:nth-child(1) {
background: transparent;
color: var(--black);
border: 1px solid #ea3223;
transition: 0.3s;
}
.btn-2:nth-child(1):hover {
color: white;
background: linear-gradient(93.36deg, #ea3223 0%, #f26a5f 98.82%);
border: none;
}
.btn-2 span {
margin-right: 10px;
}
footer {
background: var(--black);
padding: 1em 0;
}
footer .container {
display: flex;
align-items: center;
justify-content: space-between;
}
footer img {
width: 20%;
}
footer p {
color: white;
font-size: 16px;
}
footer a {
color: #ea3223;
text-decoration: none;
}
@media (max-width: 991px) {
.logo123 {
width: 235px;
}
.title {
font-size: 30px;
}
.sub-title {
font-size: 16px;
}
.container {
padding: 0 15px;
}
.wraper {
grid-template-columns: 1fr;
margin-top: 40%;
}
.left::before {
top: 23px;
}
.arr {
width: 50%;
bottom: -26px;
}
.row-sm {
grid-template-columns: 1fr;
}
form {
padding: 3em 1.5em;
}
.main {
display: flex;
padding: 10rem 0!important;
align-items: center;
justify-content: center;
padding: 0;
padding-bottom: 0;
}
.main img {
width: 60%;
}
.buttons {
width: 100%;
}
footer .container {
flex-direction: column;
}
footer img {
width: 50%;
}
footer p {
font-size: 12px;
text-align: center;
margin-top: 15px;
}
.buttons {
grid-template-columns: 1fr;
}
}
#detail_table_jr {
margin-top: 30px;
display:none;
th {
background-color: var(--litegray);
}
}
.button_div{
text-align:center;
grid-template-columns: auto;
button {
max-width: 550px;
margin: 0 auto;
width: 100%;
}
}
.img-gallery {
width: 80%;
margin: 2rem auto;
a img {
min-width: 150px !important;
border-radius: 5px;
transition: all 0.3s;
&:hover {
transform: scale(1.05);
}
}
.table, th{
background: var(--litegray);
}
}
.invalid-text {
color:#dc3545 !important;
}
.email-invalid-text{
@extend .invalid-text;
}
.phone-invalid-text{
@extend .invalid-text;
}
}
/* Styling for the previewContainer */
#upload-preview {
display: flex;
flex-wrap: wrap; /* This will allow items to wrap to the next row if there's not enough space in the current row */
justify-content: space-between; /* This spaces out the items evenly */
}
/* Styling for the child divs (containing images or PDFs) */
#upload-preview > div {
flex: 1 1 auto; /* This allows the div to grow and shrink according to the content and available space */
max-width: 200px; /* You can adjust this value based on your preference */
margin: 10px; /* Adding a little margin for separation */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Optional: For a subtle shadow effect */
}
/* Styling for the images */
#upload-preview img {
max-width: 100%;
height: auto;
object-fit: cover; /* This ensures that the image covers the frame without losing its aspect ratio */
}
/* Styling for the iframes (PDFs) */
#upload-preview iframe {
max-width: 100%;
height: 300px; /* Adjust based on your preference */
}
/* the norm */
#gritter-notice-wrapper {
position:fixed;
top:20px;
right:20px;
width:301px;
z-index:9999;
}
#gritter-notice-wrapper.top-left {
left: 20px;
right: auto;
}
#gritter-notice-wrapper.bottom-right {
top: auto;
left: auto;
bottom: 20px;
right: 20px;
}
#gritter-notice-wrapper.bottom-left {
top: auto;
right: auto;
bottom: 20px;
left: 20px;
}
.gritter-item-wrapper {
position:relative;
margin:0 0 10px 0;
background: image-url("ie-spacer.gif");
}
.gritter-item {
display:block;
background: image-url("gritter.png");
color:#eee;
padding:2px 11px 8px 11px;
font-size: 11px;
font-family:verdana;
}
.hover .gritter-item {
background-position:right -40px;
}
.gritter-item p {
padding:0;
margin:0;
word-wrap:break-word;
}
.gritter-close {
display:none;
position:absolute;
top:2px;
left:275px; //Used to push close button to right of gritter notification.
background: image-url("gritter.png") no-repeat left top;
cursor:pointer;
width:30px;
height:30px;
text-indent:-9999em;
}
.gritter-title {
font-size:14px;
font-weight:bold;
padding:0 0 7px 0;
display:block;
}
.gritter-image {
width:48px;
height:48px;
float:left;
}
.gritter-with-image,
.gritter-without-image {
padding:0;
}
.gritter-with-image {
width:220px;
float:right;
}
/* for the light (white) version of the gritter notice */
.gritter-light .gritter-item,
.gritter-light .gritter-bottom,
.gritter-light .gritter-top,
.gritter-light .gritter-close {
background-image: url(../images/gritter-light.png);
color: #222;
}
.gritter-light .gritter-title {
text-shadow: none;
}
#gritter-notice-wrapper {
position: fixed;
top: 20px;
right: 20px;
width: 301px;
z-index: 9999;
}
#gritter-notice-wrapper.top-left {
left: 20px;
right: auto;
}
#gritter-notice-wrapper.bottom-right {
top: auto;
left: auto;
bottom: 20px;
right: 20px;
}
#gritter-notice-wrapper.bottom-left {
top: auto;
right: auto;
bottom: 20px;
left: 20px;
}
#gritter-notice-wrapper .gritter-item-wrapper {
position: relative;
margin: 0 0 10px 0;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item {
display: block;
background: #657791;
border: 1px solid #455264;
color: #ffffff;
padding: 10px 11px;
font-size: 12px;
border-radius: 3px;
transition: all;
transition: all all ease-out;
transition-duration: 0.4s;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item:hover {
background: #72849e;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item p {
padding: 0;
margin: 0;
word-wrap: break-word;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-close {
position: absolute;
top: 5px;
right: 7px;
cursor: pointer;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-close i {
font-size: 16px;
color: #ffffff;
transition: background;
transition: all background ease-out;
transition-duration: 0.4s;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-close:hover i {
color: #f7f7f7;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-image {
width: 48px;
height: 48px;
float: left;
border-radius: 3px;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-icon {
margin-top: 0;
margin-right: 15px;
float: left;
font-size: 32px;
padding-bottom: 10px;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-with-image {
width: 220px;
float: right;
padding: 0;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-without-image {
padding: 0;
}
#gritter-notice-wrapper .gritter-item-wrapper .gritter-item .gritter-title {
font-size: 14px;
font-weight: bold;
padding: 0;
display: block;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item {
color: #0a776c;
background: #b2ede8;
border: 1px solid #63c8be;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item:hover {
background: #c7f2ee;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item .gritter-close i {
color: #0a776c;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item .gritter-close:hover i {
color: #09695f;
}
#gritter-notice-wrapper .gritter-item-wrapper.info-notice .gritter-item {
color: #2a6891;
background: #cfebff;
border: 1px solid #94bedb;
}
#gritter-notice-wrapper .gritter-item-wrapper.info-notice .gritter-item:hover {
background: #e9f6ff;
}
#gritter-notice-wrapper .gritter-item-wrapper.info-notice .gritter-item .gritter-close i {
color: #2a6891;
}
#gritter-notice-wrapper .gritter-item-wrapper.info-notice .gritter-item .gritter-close:hover i {
color: #275f85;
}
#gritter-notice-wrapper .gritter-item-wrapper.error-notice .gritter-item {
color: #933230;
background: #fddede;
border: 1px solid #cb7c7a;
}
#gritter-notice-wrapper .gritter-item-wrapper.error-notice .gritter-item:hover {
background: #fef6f6;
}
#gritter-notice-wrapper .gritter-item-wrapper.error-notice .gritter-item .gritter-close i {
color: #933230;
}
#gritter-notice-wrapper .gritter-item-wrapper.error-notice .gritter-item .gritter-close:hover i {
color: #872e2c;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item {
color: #1a410d;
background: #a1df91;
border: 1px solid #59b95e;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item:hover {
background: #fef6f6;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item .gritter-close i {
color: #080606;
}
#gritter-notice-wrapper .gritter-item-wrapper.success-notice .gritter-item .gritter-close:hover i {
color: #70b16a;
}
.survey-answer {
border: solid;
border-color: #ff5a5a;
border-radius: 12px;
background-color: white;
color: #4f4f4f
}
.survey-answer:hover{
color: #4f4f4f;
transition: 500ms;
}
.mobile-download-button{
color: #4f4f4f;
border-radius: "12px";
background-color: "#ede8e8";
padding: "15px";
margin: "15px";
border: "none"
}
.mobile-download-button:hover{
color: black;
box-shadow: rgb(204, 204, 204) -3px 3px;
transform:scale(1.025);
transition: 500ms;
}
// Place all the styles related to the Customers controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
table td input {
width:100%;
}
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*/
// Gem style assets.
@import "bootstrap";
@import "gritter";
@import "flatpickr";
// Custom style assets.
@import "base.scss";
@import "customers.scss";
@import "jobs.scss";
@import "dataviz.scss";
@import "modal.scss";
@import "react_invoices.scss";
@import "job_requests.scss";
@import "ios_messages.scss";
@import "3rdparty/bootstrap-tagsinput.scss";
@import "stack/application";
@import "ace.scss";
@import "invoices.scss";
@import "custom-context-menu.scss";
@import "invoice_templates.scss";
@import "timesheets.scss";
@import "job_history.scss";
@import "get_started.scss"
/* app/assets/stylesheets/job_history.css */
/* Table styles */
.job-history-table {
width: 100%;
border-collapse: collapse;
}
.job-history-table th,
.job-history-table td {
padding: 10px;
text-align: left;
border: 1px solid #ccc;
}
.job-history-table thead {
background-color: #f5f5f5;
font-weight: bold;
}
.job-history-table tbody tr:nth-child(even) {
background-color: #f2f2f2;
}
[contenteditable][placeholder]:empty:before {
content: attr(placeholder);
color: gray;
background-color: transparent;
width: initial;
height: initial;
padding: 0 .2em;
cursor: text;
}
.dropdown-toggle::after {
display: none;
}
@media(max-width: 768px) {
.fc-button
{
margin: .5px !important;
}
.hide-if-small-screen { //used to hide the company name on the navbar because it is causing the navbar to add an extra line on mobile devices which takes up more screen space. If we hide the company name when the device screen size is smaller, it gives them more real estate and makes it more functional
display: none;
}
}
.job-modal-overflow {
overflow: auto;
}
.title{
text-transform: uppercase;
letter-spacing: 0.25px;
}
.speech-bubble {
display: flex;
flex-direction: column;
align-items: flex-start;
background-color: #F2F2F2;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
padding: 0.5em 1em;
margin: 0.5em;
border-radius: 1em;
font-size: 1.2em;
}
.speech-bubble.right {
align-items: flex-end;
}
.speech-bubble.spk_1 {
align-items: flex-end;
}
.speech-bubbles-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.spk_0 {
background-color: lightblue;
}
.spk_1 {
background-color: pink;
}
.customer-info-container {
display: flex;
justify-content: center;
align-items: center;
}
.customer-info-item {
display: flex;
flex-direction: column;
align-items: center;
}
.customer-info-header {
font-size: 1.5em;
margin-bottom: 0.5em;
}
.customer-info-value {
font-size: 1.2em;
font-weight: bold;
}
.transcription-header {
font-size: 1.5em;
margin-bottom: 0.5em;
}
.transcription-container {
display: flex;
flex-direction: column;
align-items: center;
}
.green-checkbox {
color: green;
font-weight: bold;
font-size: 15px; /* adjust as needed */
}
.red-x {
color: red;
background-color: #FFD5D5; /* light red for circle */
border-radius: 30%; /* makes the background a circle */
padding: 3px; /* space around the X */
font-weight: bold;
font-size: 12px; /* adjust as needed */
}
.get-started{
font-family: "Poppins", sans-serif;
}
.fr-pill-button{
margin: 10px;
padding: 15px;
border-radius: 30px;
background-color: #EA3223;
color: rgb(254, 254, 254);
border: none;
font-weight: 600;
box-shadow: rgb(204, 204, 204) -1px 2px;
min-width: 150px;
}
.fr-pill-button:hover {
box-shadow: rgb(204, 204, 204) -3px 3px;
transform:scale(1.025);
color: rgb(254, 254, 254);
transition: 500ms;
}
.fr-pill-button-disabled{
margin: 10px;
padding: 15px;
border-radius: 30px;
background-color: #999393;
color: rgb(254, 254, 254);
border: none;
font-weight: 600;
box-shadow: rgb(204, 204, 204) -1px 2px;
min-width: 150px;
}
.fr-pill-button-disabled:hover {
box-shadow: rgb(204, 204, 204) -3px 3px;
transform:scale(1.025);
color: rgb(254, 254, 254);
transition: 500ms;
}
.fr-link{
text-decoration: underline;
color: rgb(255, 90, 90);
}
.fr-link:hover{
text-decoration: none;
color: rgb(255, 90, 90);
}
body {
font-family: 'Raleway', sans-serif;
/* font-family: 'Roboto', sans-serif; */
font-size: 20px;
}
@import "font-awesome";
@import "3rdparty/flickity";
/* Main content */
.main {
margin-top: 13%; /* Add a top margin to avoid content overlay */
}
p {
font-size: 18px;
font-family: "Open Sans", Helvetica, Arial, sans-serif;
color: #777;
}
.business-header {
text-align: center;
font-size: 1em !important; /* Define a base size for the business header */
padding: 5px;
margin: 5px;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: bold !important;
}
.business-header h1 h2 h3 h4 h5 h6{
font-weight: bold !important;
}
/* Descriptions & Headers of Description. */
.description-header{
text-align: center;
font-size: 1em !important; /* Define a base size for the business header */
font-weight: normal; /* Normal font weight */
padding: 5px;
margin: 5px;
}
.description-text{
width: 70%;
}
.section-header{
text-transform: uppercase;
}
#phone-header {
text-align: center;
font-size: 3em; /* 50% larger than business-header */
font-weight: bold; /* Bolder text */
}
.content {
margin: 20px;
}
.button {
background-color: #ffff;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
font-weight: bold;
text-transform: uppercase;
}
img {
width: 100px;
height: 100px;
}
.navbar {
// padding: 0 5vw; /* 5% of viewport width padding on both left and right */
overflow: hidden;
position: fixed; /* Set the navbar to fixed position */
top: 0; /* Position the navbar at the top of the page */
width: 100%; /* Full width */
background-color: #fff;
z-index: 999;
}
.navbar a {
float: left;
display: block;
color: #333;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar a:hover {
color: #f8f9fa //<%= @home_page.secondary_color%>;
}
.navbar-toggler {
// width: 1%;
// height: auto;
margin-right: 15px
}
/* NAVBAR HAMBURGER TOGGLE BUTTON */
label.hamburger {
display: block;
background: #fff;
width: 40px; /* less width = square hamburger */
height: 60px;
position: relative;
cursor: pointer;
}
input#hamburger {
display:none
}
.line {
position: absolute;
height: 5px;
width: 80%;
background: #000;
transition: 0.5s;
}
/* steps of 12 are optional, could also be 10, 13 etc */
.line:nth-child(1) { top: 12px; }
.line:nth-child(2) { top: 24px; }
.line:nth-child(3) { top: 36px; }
/* translateY must be same as */
#hamburger:checked + .hamburger .line:nth-child(1){
transform: translateY(12px) rotate(-45deg);
}
#hamburger:checked + .hamburger .line:nth-child(2){
opacity:0;
}
#hamburger:checked + .hamburger .line:nth-child(3){
transform: translateY(-12px) rotate(45deg);
}
/* END NAVBAR HAMBURGER TOGGLE BUTTON */
.nav-link {
font-size: 2vh;
font-family: 'Roboto', sans-serif;
text-transform: uppercase;
}
.logo-image{
height: auto; /* adjust this as needed */
width: 50%;
max-width: 540px; /* optional, but good practice */
overflow: hidden;
margin-right: 10px;
}
.footer {
font-weight: normal;
padding: 20px;
background-color: #f8f9fa;
/* position: absolute; */
margin-top: 20px;
bottom: 0;
width: 100%;
text-align: left;
}
/* Default style for large screens and desktops */
.social-btn {
width: 75px;
}
/* Style for medium screens (tablets, etc.) */
@media (max-width: 768px) {
.social-btn {
width: 75px;
}
}
/* Style for small screens (phones, etc.) */
@media (max-width: 480px) {
.social-btn {
width: 65px;
}
}
.contact-info {
margin: 10px 0;
}
.primary-button {
background-color: #333;//<%= @home_page.primary_color%>;
}
.contact-us-button {
background-color: #f8f9fa; //<%= @home_page.tertiary_color%>;
border: 1px solid lightgrey;
color: white; /* Text color */
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
}
.section-white {
background-color: #fff;
}
.section-gray {
background-color: #f8f9f9;
}
.section-subtitle {
color: #aaaaaa;
display: inline-block;
margin-bottom: 13px;
margin-bottom: 0.8125rem;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: bold;
}
/* Styles applied when the viewport width is less than 481px*/
@media (max-width: 480px) {
/* Main content */
.main {
margin-top: 40%; /* Add a top margin to avoid content overlay */
}
.section-subtitle {
font-size: 12px;
margin: 5px;
padding-left: 10px;
padding-right: 10px;
}
p {
font-size: 18.5px;
}
.description-text{
width: 95%;
}
.button {
font-size: 12px;
}
}
/* Styles applied when the viewport width is between 481px and 768px */
@media (min-width: 480px) and (max-width: 768px) {
.main {
margin-top: 32%; /* Add a top margin to avoid content overlay */
}
.section-subtitle {
font-size: 14px;
margin: 5px;
padding-left: 10px;
padding-right: 10px;
}
p {
font-size: 18.5px;
}
.description-text{
width: 85%;
}
}
/* Styles applied when the viewport width is between 768 and 1024 */
@media (min-width: 768px) and (max-width: 1024px){
#business-name {
font-size: 2.75rem; /* 50% larger than business-header */
}
.main {
margin-top: 25%; /* Add a top margin to avoid content overlay */
}
.section-subtitle {
font-size: 16px;
}
.description-text{
width: 75%;
}
#phone-header-number {
font-size: 110%; /* 50% larger than business-header */
}
}
/* Styles applied when the viewport width is above 1024 */
@media (min-width: 1024px){
#business-name {
font-size: 3rem; /* 50% larger than business-header */
}
#business-name {
font-size: 3.25rem; /* 50% larger than business-header */
}
.main {
margin-top: 13%; /* Add a top margin to avoid content overlay */
}
.section-subtitle {
font-size: 18px;
}
#phone-header-number {
font-size: 130%; /* 50% larger than business-header */
}
}
// Place all the styles related to the marketing_sources controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
Error reading file: U_STRING_NOT_TERMINATED_WARNING
@charset "UTF-8";.fc-timeGrid-view .fc-day-grid{position:relative;z-index:2}.fc-timeGrid-view .fc-day-grid .fc-row{min-height:3em}.fc-timeGrid-view .fc-day-grid .fc-row .fc-content-skeleton{padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px;white-space:nowrap}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-mirror-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-time-grid-event{margin-bottom:1px}.fc-time-grid-event-inset{-webkit-box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px #fff}.fc-time-grid-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-time-grid-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event .fc-content{overflow:hidden;max-height:100%}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em;white-space:nowrap}.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:" - "}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent}
@charset "UTF-8";
/* TimeGridView all-day area
--------------------------------------------------------------------------------------------------*/
.fc-timeGrid-view .fc-day-grid {
position: relative;
z-index: 2;
/* so the "more.." popover will be over the time grid */
}
.fc-timeGrid-view .fc-day-grid .fc-row {
min-height: 3em;
/* all-day section will never get shorter than this */
}
.fc-timeGrid-view .fc-day-grid .fc-row .fc-content-skeleton {
padding-bottom: 1em;
/* give space underneath events for clicking/selecting days */
}
/* TimeGrid axis running down the side (for both the all-day area and the slot area)
--------------------------------------------------------------------------------------------------*/
.fc .fc-axis {
/* .fc to overcome default cell styles */
vertical-align: middle;
padding: 0 4px;
white-space: nowrap;
}
.fc-ltr .fc-axis {
text-align: right;
}
.fc-rtl .fc-axis {
text-align: left;
}
/* TimeGrid Structure
--------------------------------------------------------------------------------------------------*/
.fc-time-grid-container,
.fc-time-grid {
/* so slats/bg/content/etc positions get scoped within here */
position: relative;
z-index: 1;
}
.fc-time-grid {
min-height: 100%;
/* so if height setting is 'auto', .fc-bg stretches to fill height */
}
.fc-time-grid table {
/* don't put outer borders on slats/bg/content/etc */
border: 0 hidden transparent;
}
.fc-time-grid > .fc-bg {
z-index: 1;
}
.fc-time-grid .fc-slats,
.fc-time-grid > hr {
/* the
.fc-time-grid .fc-content-col {
position: relative;
/* because now-indicator lives directly inside */
}
.fc-time-grid .fc-content-skeleton {
position: absolute;
z-index: 3;
top: 0;
left: 0;
right: 0;
}
/* divs within a cell within the fc-content-skeleton */
.fc-time-grid .fc-business-container {
position: relative;
z-index: 1;
}
.fc-time-grid .fc-bgevent-container {
position: relative;
z-index: 2;
}
.fc-time-grid .fc-highlight-container {
position: relative;
z-index: 3;
}
.fc-time-grid .fc-event-container {
position: relative;
z-index: 4;
}
.fc-time-grid .fc-now-indicator-line {
z-index: 5;
}
.fc-time-grid .fc-mirror-container {
/* also is fc-event-container */
position: relative;
z-index: 6;
}
/* TimeGrid Slats (lines that run horizontally)
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-slats td {
height: 1.5em;
border-bottom: 0;
/* each cell is responsible for its top border */
}
.fc-time-grid .fc-slats .fc-minor td {
border-top-style: dotted;
}
/* TimeGrid Highlighting Slots
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-highlight-container {
/* a div within a cell within the fc-highlight-skeleton */
position: relative;
/* scopes the left/right of the fc-highlight to be in the column */
}
.fc-time-grid .fc-highlight {
position: absolute;
left: 0;
right: 0;
/* top and bottom will be in by JS */
}
/* TimeGrid Event Containment
--------------------------------------------------------------------------------------------------*/
.fc-ltr .fc-time-grid .fc-event-container {
/* space on the sides of events for LTR (default) */
margin: 0 2.5% 0 2px;
}
.fc-rtl .fc-time-grid .fc-event-container {
/* space on the sides of events for RTL */
margin: 0 2px 0 2.5%;
}
.fc-time-grid .fc-event,
.fc-time-grid .fc-bgevent {
position: absolute;
z-index: 1;
/* scope inner z-index's */
}
.fc-time-grid .fc-bgevent {
/* background events always span full width */
left: 0;
right: 0;
}
/* TimeGrid Event Styling
----------------------------------------------------------------------------------------------------
We use the full "fc-time-grid-event" class instead of using descendants because the event won't
be a descendant of the grid when it is being dragged.
*/
.fc-time-grid-event {
margin-bottom: 1px;
}
.fc-time-grid-event-inset {
-webkit-box-shadow: 0px 0px 0px 1px #fff;
box-shadow: 0px 0px 0px 1px #fff;
}
.fc-time-grid-event.fc-not-start {
/* events that are continuing from another day */
/* replace space made by the top border with padding */
border-top-width: 0;
padding-top: 1px;
/* remove top rounded corners */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.fc-time-grid-event.fc-not-end {
/* replace space made by the top border with padding */
border-bottom-width: 0;
padding-bottom: 1px;
/* remove bottom rounded corners */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.fc-time-grid-event .fc-content {
overflow: hidden;
max-height: 100%;
}
.fc-time-grid-event .fc-time,
.fc-time-grid-event .fc-title {
padding: 0 1px;
}
.fc-time-grid-event .fc-time {
font-size: 0.85em;
white-space: nowrap;
}
/* short mode, where time and title are on the same line */
.fc-time-grid-event.fc-short .fc-content {
/* don't wrap to second line (now that contents will be inline) */
white-space: nowrap;
}
.fc-time-grid-event.fc-short .fc-time,
.fc-time-grid-event.fc-short .fc-title {
/* put the time and title on the same line */
display: inline-block;
vertical-align: top;
}
.fc-time-grid-event.fc-short .fc-time span {
display: none;
/* don't display the full time text... */
}
.fc-time-grid-event.fc-short .fc-time:before {
content: attr(data-start);
/* ...instead, display only the start time */
}
.fc-time-grid-event.fc-short .fc-time:after {
content: " - ";
/* seperate with a dash, wrapped in nbsp's */
}
.fc-time-grid-event.fc-short .fc-title {
font-size: 0.85em;
/* make the title text the same size as the time */
padding: 0;
/* undo padding from above */
}
/* resizer (cursor device) */
.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer {
left: 0;
right: 0;
bottom: 0;
height: 8px;
overflow: hidden;
line-height: 8px;
font-size: 11px;
font-family: monospace;
text-align: center;
cursor: s-resize;
}
.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after {
content: "=";
}
/* resizer (touch device) */
.fc-time-grid-event.fc-selected .fc-resizer {
/* 10x10 dot */
border-radius: 5px;
border-width: 1px;
width: 8px;
height: 8px;
border-style: solid;
border-color: inherit;
background: #fff;
/* horizontally center */
left: 50%;
margin-left: -5px;
/* center on the bottom edge */
bottom: -5px;
}
/* Now Indicator
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-now-indicator-line {
border-top-width: 1px;
left: 0;
right: 0;
}
/* arrow on axis */
.fc-time-grid .fc-now-indicator-arrow {
margin-top: -5px;
/* vertically center on top coordinate */
}
.fc-ltr .fc-time-grid .fc-now-indicator-arrow {
left: 0;
/* triangle pointing right... */
border-width: 5px 0 5px 6px;
border-top-color: transparent;
border-bottom-color: transparent;
}
.fc-rtl .fc-time-grid .fc-now-indicator-arrow {
right: 0;
/* triangle pointing left... */
border-width: 5px 6px 5px 0;
border-top-color: transparent;
border-bottom-color: transparent;
}
.fc-dayGridDay-view .fc-content-skeleton,.fc-dayGridWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-dayGrid-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-dayGrid-view .fc-day-number,.fc-dayGrid-view .fc-week-number{padding:2px}.fc-dayGrid-view th.fc-day-number,.fc-dayGrid-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-dayGrid-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-dayGrid-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-dayGrid-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-dayGrid-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-dayGrid-view .fc-day-top .fc-week-number{min-width:1.5em;text-align:center;background-color:#f2f2f2;color:grey}.fc-dayGrid-view td.fc-week-number{text-align:center}.fc-dayGrid-view td.fc-week-number>*{display:inline-block;min-width:1.25em}
/* DayGridView
--------------------------------------------------------------------------------------------------*/
/* day row structure */
.fc-dayGridWeek-view .fc-content-skeleton,
.fc-dayGridDay-view .fc-content-skeleton {
/* there may be week numbers in these views, so no padding-top */
padding-bottom: 1em;
/* ensure a space at bottom of cell for user selecting/clicking */
}
.fc-dayGrid-view .fc-body .fc-row {
min-height: 4em;
/* ensure that all rows are at least this tall */
}
/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */
.fc-row.fc-rigid {
overflow: hidden;
}
.fc-row.fc-rigid .fc-content-skeleton {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* week and day number styling */
.fc-day-top.fc-other-month {
opacity: 0.3;
}
.fc-dayGrid-view .fc-week-number,
.fc-dayGrid-view .fc-day-number {
padding: 2px;
}
.fc-dayGrid-view th.fc-week-number,
.fc-dayGrid-view th.fc-day-number {
padding: 0 2px;
/* column headers can't have as much v space */
}
.fc-ltr .fc-dayGrid-view .fc-day-top .fc-day-number {
float: right;
}
.fc-rtl .fc-dayGrid-view .fc-day-top .fc-day-number {
float: left;
}
.fc-ltr .fc-dayGrid-view .fc-day-top .fc-week-number {
float: left;
border-radius: 0 0 3px 0;
}
.fc-rtl .fc-dayGrid-view .fc-day-top .fc-week-number {
float: right;
border-radius: 0 0 0 3px;
}
.fc-dayGrid-view .fc-day-top .fc-week-number {
min-width: 1.5em;
text-align: center;
background-color: #f2f2f2;
color: #808080;
}
/* when week/day number have own column */
.fc-dayGrid-view td.fc-week-number {
text-align: center;
}
.fc-dayGrid-view td.fc-week-number > * {
/* work around the way we do column resizing and ensure a minimum width */
display: inline-block;
min-width: 1.25em;
}
.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item-marker,.fc-list-item-time{white-space:nowrap;width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee}
/* List View
--------------------------------------------------------------------------------------------------*/
/* possibly reusable */
.fc-event-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
/* view wrapper */
.fc-rtl .fc-list-view {
direction: rtl;
/* unlike core views, leverage browser RTL */
}
.fc-list-view {
border-width: 1px;
border-style: solid;
}
/* table resets */
.fc .fc-list-table {
table-layout: auto;
/* for shrinkwrapping cell content */
}
.fc-list-table td {
border-width: 1px 0 0;
padding: 8px 14px;
}
.fc-list-table tr:first-child td {
border-top-width: 0;
}
/* day headings with the list */
.fc-list-heading {
border-bottom-width: 1px;
}
.fc-list-heading td {
font-weight: bold;
}
.fc-ltr .fc-list-heading-main {
float: left;
}
.fc-ltr .fc-list-heading-alt {
float: right;
}
.fc-rtl .fc-list-heading-main {
float: right;
}
.fc-rtl .fc-list-heading-alt {
float: left;
}
/* event list items */
.fc-list-item.fc-has-url {
cursor: pointer;
/* whole row will be clickable */
}
.fc-list-item-marker,
.fc-list-item-time {
white-space: nowrap;
width: 1px;
}
/* make the dot closer to the event title */
.fc-ltr .fc-list-item-marker {
padding-right: 0;
}
.fc-rtl .fc-list-item-marker {
padding-left: 0;
}
.fc-list-item-title a {
/* every event title cell has an tag */
text-decoration: none;
color: inherit;
}
.fc-list-item-title a[href]:hover {
/* hover effect only on titles with hrefs */
text-decoration: underline;
}
/* message when no events */
.fc-list-empty-wrap2 {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.fc-list-empty-wrap1 {
width: 100%;
height: 100%;
display: table;
}
.fc-list-empty {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.fc-unthemed .fc-list-empty {
/* theme will provide own background */
background-color: #eee;
}
@charset "UTF-8";.fc-button:not(:disabled),.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a.fc-more,a[data-goto]{cursor:pointer}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:2px 4px}.fc-rtl .fc-popover .fc-header{flex-direction:row-reverse}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{opacity:.65;font-size:1.1em}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-bg,.fc-bgevent-skeleton,.fc-highlight-skeleton,.fc-mirror-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-mirror-skeleton{z-index:5}.fc .fc-row .fc-content-skeleton table,.fc .fc-row .fc-content-skeleton td,.fc .fc-row .fc-mirror-skeleton td{background:0 0;border-color:transparent}.fc-row .fc-content-skeleton td,.fc-row .fc-mirror-skeleton td{border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-mirror-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.4;border-radius:3px;border:1px solid #3788d8}.fc-event,.fc-event-dot{background-color:#3788d8}.fc-event,.fc-event:hover{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-event.fc-dragging.fc-selected{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-event.fc-dragging:not(.fc-selected){opacity:.75}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-mirror-skeleton tr:first-child>td>.fc-day-grid-event{margin-top:0}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-button,.fc-icon{display:inline-block;font-weight:400;text-align:center}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed td.fc-today{background:#fcf8e3}.fc-unthemed .fc-disabled-day{background:#d7d7d7;opacity:.3}@font-face{font-family:fcicons;src:url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format("truetype");font-weight:400;font-style:normal}.fc-icon{font-family:fcicons!important;speak:none;font-style:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;width:1em;height:1em}.fc-icon-chevron-left:before{content:""}.fc-icon-chevron-right:before{content:""}.fc-icon-chevrons-left:before{content:""}.fc-icon-chevrons-right:before{content:""}.fc-icon-minus-square:before{content:""}.fc-icon-plus-square:before{content:""}.fc-icon-x:before{content:""}.fc-button{overflow:visible;text-transform:none;margin:0;font-family:inherit}.fc-button::-moz-focus-inner{padding:0;border-style:none}.fc-button{-webkit-appearance:button;color:#212529;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.4em .65em;font-size:1em;line-height:1.5;border-radius:.25em}.fc-button:hover{color:#212529;text-decoration:none}.fc-button:focus{outline:0;-webkit-box-shadow:0 0 0 .2rem rgba(44,62,80,.25);box-shadow:0 0 0 .2rem rgba(44,62,80,.25)}.fc-button:disabled{opacity:.65}.fc-button-primary{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.fc-button-primary:hover{color:#fff;background-color:#1e2b37;border-color:#1a252f}.fc-button-primary:focus{-webkit-box-shadow:0 0 0 .2rem rgba(76,91,106,.5);box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc-button-primary:disabled{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.fc-button-primary:not(:disabled).fc-button-active,.fc-button-primary:not(:disabled):active{color:#fff;background-color:#1a252f;border-color:#151e27}.fc-button-primary:not(:disabled).fc-button-active:focus,.fc-button-primary:not(:disabled):active:focus{-webkit-box-shadow:0 0 0 .2rem rgba(76,91,106,.5);box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc-button .fc-icon{vertical-align:middle;font-size:1.5em}.fc-button-group{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.fc-button-group>.fc-button{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.fc-button-group>.fc-button.fc-button-active,.fc-button-group>.fc-button:active,.fc-button-group>.fc-button:focus,.fc-button-group>.fc-button:hover{z-index:1}.fc-button-group>.fc-button:not(:first-child){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-button-group>.fc-button:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-list-item:hover td{background-color:#f5f5f5}.fc-toolbar{display:flex;justify-content:space-between;align-items:center}.fc-toolbar.fc-header-toolbar{margin-bottom:1.5em}.fc-toolbar.fc-footer-toolbar{margin-top:1.5em}.fc-toolbar>*>:not(:first-child){margin-left:.75em}.fc-toolbar h2{font-size:1.75em;margin:0}.fc-view-container{position:relative}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}@media print{.fc-bg,.fc-bgevent-container,.fc-bgevent-skeleton,.fc-business-container,.fc-event .fc-resizer,.fc-highlight-container,.fc-highlight-skeleton,.fc-mirror-container,.fc-mirror-skeleton{display:none}.fc tbody .fc-row,.fc-time-grid{min-height:0!important}.fc-time-grid .fc-event.fc-not-end:after,.fc-time-grid .fc-event.fc-not-start:before{content:"..."}.fc{max-width:100%!important}.fc-event{background:#fff!important;color:#000!important;page-break-inside:avoid}.fc hr,.fc tbody,.fc td,.fc th,.fc thead,.fc-row{border-color:#ccc!important;background:#fff!important}.fc tbody .fc-row{height:auto!important}.fc tbody .fc-row .fc-content-skeleton{position:static;padding-bottom:0!important}.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td{padding-bottom:1em}.fc tbody .fc-row .fc-content-skeleton table{height:1em}.fc-more,.fc-more-cell{display:none!important}.fc tr.fc-limited{display:table-row!important}.fc td.fc-limited{display:table-cell!important}.fc-popover,.fc-timeGrid-view .fc-axis{display:none}.fc-slats,.fc-time-grid hr{display:none!important}.fc button,.fc-button-group,.fc-time-grid .fc-event .fc-time span{display:none}.fc-time-grid .fc-content-skeleton{position:static}.fc-time-grid .fc-content-skeleton table{height:4em}.fc-time-grid .fc-event-container{margin:0!important}.fc-time-grid .fc-event{position:static!important;margin:3px 2px!important}.fc-time-grid .fc-event.fc-not-end{border-bottom-width:1px!important}.fc-time-grid .fc-event.fc-not-start{border-top-width:1px!important}.fc-time-grid .fc-event .fc-time{white-space:normal!important}.fc-time-grid .fc-event .fc-time:after{content:attr(data-full)}.fc-day-grid-container,.fc-scroller,.fc-time-grid-container{overflow:visible!important;height:auto!important}.fc-row{border:0!important;margin:0!important}}
@import "ace.scss";
@charset "UTF-8";
.fc {
direction: ltr;
text-align: left;
}
.fc-rtl {
text-align: right;
}
body .fc {
/* extra precedence to overcome jqui */
font-size: 1em;
}
/* Colors
--------------------------------------------------------------------------------------------------*/
.fc-highlight {
/* when user is selecting cells */
background: #bce8f1;
opacity: 0.3;
}
.fc-bgevent {
/* default look for background events */
background: #8fdf82;
opacity: 0.3;
}
.fc-nonbusiness {
/* default look for non-business-hours areas */
/* will inherit .fc-bgevent's styles */
background: #d7d7d7;
}
/* Popover
--------------------------------------------------------------------------------------------------*/
.fc-popover {
position: absolute;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.fc-popover .fc-header {
/* TODO: be more consistent with fc-head/fc-body */
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 2px 4px;
}
.fc-rtl .fc-popover .fc-header {
flex-direction: row-reverse;
}
.fc-popover .fc-header .fc-title {
margin: 0 2px;
}
.fc-popover .fc-header .fc-close {
cursor: pointer;
opacity: 0.65;
font-size: 1.1em;
}
/* Misc Reusable Components
--------------------------------------------------------------------------------------------------*/
.fc-divider {
border-style: solid;
border-width: 1px;
}
hr.fc-divider {
height: 0;
margin: 0;
padding: 0 0 2px;
/* height is unreliable across browsers, so use padding */
border-width: 1px 0;
}
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-mirror-skeleton {
/* these element should always cling to top-left/right corners */
position: absolute;
top: 0;
left: 0;
right: 0;
}
.fc-bg {
bottom: 0;
/* strech bg to bottom edge */
}
.fc-bg table {
height: 100%;
/* strech bg to bottom edge */
}
/* Tables
--------------------------------------------------------------------------------------------------*/
.fc table {
width: 100%;
box-sizing: border-box;
/* fix scrollbar issue in firefox */
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
font-size: 1em;
/* normalize cross-browser */
}
.fc th {
text-align: center;
}
.fc th,
.fc td {
border-style: solid;
border-width: 1px;
padding: 0;
vertical-align: top;
}
.fc td.fc-today {
border-style: double;
/* overcome neighboring borders */
}
/* Internal Nav Links
--------------------------------------------------------------------------------------------------*/
a[data-goto] {
cursor: pointer;
}
a[data-goto]:hover {
text-decoration: underline;
}
/* Fake Table Rows
--------------------------------------------------------------------------------------------------*/
.fc .fc-row {
/* extra precedence to overcome themes forcing a 1px border */
/* no visible border by default. but make available if need be (scrollbar width compensation) */
border-style: solid;
border-width: 0;
}
.fc-row table {
/* don't put left/right border on anything within a fake row.
the outer tbody will worry about this */
border-left: 0 hidden transparent;
border-right: 0 hidden transparent;
/* no bottom borders on rows */
border-bottom: 0 hidden transparent;
}
.fc-row:first-child table {
border-top: 0 hidden transparent;
/* no top border on first row */
}
/* Day Row (used within the header and the DayGrid)
--------------------------------------------------------------------------------------------------*/
.fc-row {
position: relative;
}
.fc-row .fc-bg {
z-index: 1;
}
/* highlighting cells & background event skeleton */
.fc-row .fc-bgevent-skeleton,
.fc-row .fc-highlight-skeleton {
bottom: 0;
/* stretch skeleton to bottom of row */
}
.fc-row .fc-bgevent-skeleton table,
.fc-row .fc-highlight-skeleton table {
height: 100%;
/* stretch skeleton to bottom of row */
}
.fc-row .fc-highlight-skeleton td,
.fc-row .fc-bgevent-skeleton td {
border-color: transparent;
}
.fc-row .fc-bgevent-skeleton {
z-index: 2;
}
.fc-row .fc-highlight-skeleton {
z-index: 3;
}
/*
row content (which contains day/week numbers and events) as well as "mirror" (which contains
temporary rendered events).
*/
.fc-row .fc-content-skeleton {
position: relative;
z-index: 4;
padding-bottom: 2px;
/* matches the space above the events */
}
.fc-row .fc-mirror-skeleton {
z-index: 5;
}
.fc .fc-row .fc-content-skeleton table,
.fc .fc-row .fc-content-skeleton td,
.fc .fc-row .fc-mirror-skeleton td {
/* see-through to the background below */
/* extra precedence to prevent theme-provided backgrounds */
background: none;
/* in case s are globally styled */
border-color: transparent;
}
.fc-row .fc-content-skeleton td,
.fc-row .fc-mirror-skeleton td {
/* don't put a border between events and/or the day number */
border-bottom: 0;
}
.fc-row .fc-content-skeleton tbody td,
.fc-row .fc-mirror-skeleton tbody td {
/* don't put a border between event cells */
border-top: 0;
}
/* Scrolling Container
--------------------------------------------------------------------------------------------------*/
.fc-scroller {
-webkit-overflow-scrolling: touch;
}
/* TODO: move to timegrid/daygrid */
.fc-scroller > .fc-day-grid,
.fc-scroller > .fc-time-grid {
position: relative;
/* re-scope all positions */
width: 100%;
/* hack to force re-sizing this inner element when scrollbars appear/disappear */
}
/* Global Event Styles
--------------------------------------------------------------------------------------------------*/
.fc-event {
position: relative;
/* for resize handle and other inner positioning */
display: block;
/* make the tag block */
font-size: 0.85em;
line-height: 1.4;
border-radius: 3px;
border: 1px solid #3788d8;
}
.fc-event,
.fc-event-dot {
background-color: #3788d8;
/* default BACKGROUND color */
}
.fc-event,
.fc-event:hover {
color: #fff;
/* default TEXT color */
text-decoration: none;
/* if has an href */
}
.fc-event[href],
.fc-event.fc-draggable {
cursor: pointer;
/* give events with links and draggable events a hand mouse pointer */
}
.fc-not-allowed,
.fc-not-allowed .fc-event {
/* to override an event's custom cursor */
cursor: not-allowed;
}
.fc-event .fc-content {
position: relative;
z-index: 2;
}
/* resizer (cursor AND touch devices) */
.fc-event .fc-resizer {
position: absolute;
z-index: 4;
}
/* resizer (touch devices) */
.fc-event .fc-resizer {
display: none;
}
.fc-event.fc-allow-mouse-resize .fc-resizer,
.fc-event.fc-selected .fc-resizer {
/* only show when hovering or selected (with touch) */
display: block;
}
/* hit area */
.fc-event.fc-selected .fc-resizer:before {
/* 40x40 touch area */
content: "";
position: absolute;
z-index: 9999;
/* user of this util can scope within a lower z-index */
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin-left: -20px;
margin-top: -20px;
}
/* Event Selection (only for touch devices)
--------------------------------------------------------------------------------------------------*/
.fc-event.fc-selected {
z-index: 9999 !important;
/* overcomes inline z-index */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.fc-event.fc-selected:after {
content: "";
position: absolute;
z-index: 1;
/* same z-index as fc-bg, behind text */
/* overcome the borders */
top: -1px;
right: -1px;
bottom: -1px;
left: -1px;
/* darkening effect */
background: #000;
opacity: 0.25;
}
/* Event Dragging
--------------------------------------------------------------------------------------------------*/
.fc-event.fc-dragging.fc-selected {
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3);
}
.fc-event.fc-dragging:not(.fc-selected) {
opacity: 0.75;
}
/* Horizontal Events
--------------------------------------------------------------------------------------------------*/
/* bigger touch area when selected */
.fc-h-event.fc-selected:before {
content: "";
position: absolute;
z-index: 3;
/* below resizers */
top: -10px;
bottom: -10px;
left: 0;
right: 0;
}
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */
.fc-ltr .fc-h-event.fc-not-start,
.fc-rtl .fc-h-event.fc-not-end {
margin-left: 0;
border-left-width: 0;
padding-left: 1px;
/* replace the border with padding */
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.fc-ltr .fc-h-event.fc-not-end,
.fc-rtl .fc-h-event.fc-not-start {
margin-right: 0;
border-right-width: 0;
padding-right: 1px;
/* replace the border with padding */
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* resizer (cursor AND touch devices) */
/* left resizer */
.fc-ltr .fc-h-event .fc-start-resizer,
.fc-rtl .fc-h-event .fc-end-resizer {
cursor: w-resize;
left: -1px;
/* overcome border */
}
/* right resizer */
.fc-ltr .fc-h-event .fc-end-resizer,
.fc-rtl .fc-h-event .fc-start-resizer {
cursor: e-resize;
right: -1px;
/* overcome border */
}
/* resizer (mouse devices) */
.fc-h-event.fc-allow-mouse-resize .fc-resizer {
width: 7px;
top: -1px;
/* overcome top border */
bottom: -1px;
/* overcome bottom border */
}
/* resizer (touch devices) */
.fc-h-event.fc-selected .fc-resizer {
/* 8x8 little dot */
border-radius: 4px;
border-width: 1px;
width: 6px;
height: 6px;
border-style: solid;
border-color: inherit;
background: #fff;
/* vertically center */
top: 50%;
margin-top: -4px;
}
/* left resizer */
.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,
.fc-rtl .fc-h-event.fc-selected .fc-end-resizer {
margin-left: -4px;
/* centers the 8x8 dot on the left edge */
}
/* right resizer */
.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,
.fc-rtl .fc-h-event.fc-selected .fc-start-resizer {
margin-right: -4px;
/* centers the 8x8 dot on the right edge */
}
/* DayGrid events
----------------------------------------------------------------------------------------------------
We use the full "fc-day-grid-event" class instead of using descendants because the event won't
be a descendant of the grid when it is being dragged.
*/
.fc-day-grid-event {
margin: 1px 2px 0;
/* spacing between events and edges */
padding: 0 1px;
}
tr:first-child > td > .fc-day-grid-event {
margin-top: 2px;
/* a little bit more space before the first event */
}
.fc-mirror-skeleton tr:first-child > td > .fc-day-grid-event {
margin-top: 0;
/* except for mirror skeleton */
}
.fc-day-grid-event .fc-content {
/* force events to be one-line tall */
white-space: nowrap;
overflow: hidden;
}
.fc-day-grid-event .fc-time {
font-weight: bold;
}
/* resizer (cursor devices) */
/* left resizer */
.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer {
margin-left: -2px;
/* to the day cell's edge */
}
/* right resizer */
.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer {
margin-right: -2px;
/* to the day cell's edge */
}
/* Event Limiting
--------------------------------------------------------------------------------------------------*/
/* "more" link that represents hidden events */
a.fc-more {
margin: 1px 3px;
font-size: 0.85em;
cursor: pointer;
text-decoration: none;
}
a.fc-more:hover {
text-decoration: underline;
}
.fc-limited {
/* rows and cells that are hidden because of a "more" link */
display: none;
}
/* popover that appears when "more" link is clicked */
.fc-day-grid .fc-row {
z-index: 1;
/* make the "more" popover one higher than this */
}
.fc-more-popover {
z-index: 2;
width: 220px;
}
.fc-more-popover .fc-event-container {
padding: 10px;
}
/* Now Indicator
--------------------------------------------------------------------------------------------------*/
.fc-now-indicator {
position: absolute;
border: 0 solid red;
}
/* Utilities
--------------------------------------------------------------------------------------------------*/
.fc-unselectable {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/*
TODO: more distinction between this file and common.css
*/
/* Colors
--------------------------------------------------------------------------------------------------*/
.fc-unthemed th,
.fc-unthemed td,
.fc-unthemed thead,
.fc-unthemed tbody,
.fc-unthemed .fc-divider,
.fc-unthemed .fc-row,
.fc-unthemed .fc-content,
.fc-unthemed .fc-popover,
.fc-unthemed .fc-list-view,
.fc-unthemed .fc-list-heading td {
border-color: #ddd;
}
.fc-unthemed .fc-popover {
background-color: #fff;
}
.fc-unthemed .fc-divider,
.fc-unthemed .fc-popover .fc-header,
.fc-unthemed .fc-list-heading td {
background: #eee;
}
.fc-unthemed td.fc-today {
background: #fcf8e3;
}
.fc-unthemed .fc-disabled-day {
background: #d7d7d7;
opacity: 0.3;
}
/* Icons
--------------------------------------------------------------------------------------------------
from https://feathericons.com/ and built with IcoMoon
*/
@font-face {
font-family: "fcicons";
src: url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format("truetype");
font-weight: normal;
font-style: normal;
}
.fc-icon {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: "fcicons" !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.fc-icon-chevron-left:before {
content: "";
}
.fc-icon-chevron-right:before {
content: "";
}
.fc-icon-chevrons-left:before {
content: "";
}
.fc-icon-chevrons-right:before {
content: "";
}
.fc-icon-minus-square:before {
content: "";
}
.fc-icon-plus-square:before {
content: "";
}
.fc-icon-x:before {
content: "";
}
.fc-icon {
display: inline-block;
width: 1em;
height: 1em;
text-align: center;
}
/* Buttons
--------------------------------------------------------------------------------------------------
Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css
*/
/* reset */
.fc-button {
border-radius: 0;
overflow: visible;
text-transform: none;
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.fc-button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
.fc-button {
-webkit-appearance: button;
}
.fc-button:not(:disabled) {
cursor: pointer;
}
.fc-button::-moz-focus-inner {
padding: 0;
border-style: none;
}
/* theme */
.fc-button {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: 0.4em 0.65em;
font-size: 1em;
line-height: 1.5;
border-radius: 0.25em;
}
.fc-button:hover {
color: #212529;
text-decoration: none;
}
.fc-button:focus {
outline: 0;
-webkit-box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25);
box-shadow: 0 0 0 0.2rem rgba(44, 62, 80, 0.25);
}
.fc-button:disabled {
opacity: 0.65;
}
/* "primary" coloring */
.fc-button-primary {
// color: #fff;
background-color: $company-blue;
border-color: $company-blue;
color:white;
/* background-color: #2C3E50;
border-color: #2C3E50; */
}
.fc-button-primary:hover {
background-color: $company-blue-hover;
border-color: $company-blue-hover;
color:white;
}
.fc-button-primary:focus {
-webkit-box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
}
.fc-button-primary:disabled {
color: #fff;
background-color: #2C3E50;
border-color: #2C3E50;
}
.fc-button-primary:not(:disabled):active,
.fc-button-primary:not(:disabled).fc-button-active {
color: #fff;
background-color: #1a252f;
border-color: #151e27;
}
.fc-button-primary:not(:disabled):active:focus,
.fc-button-primary:not(:disabled).fc-button-active:focus {
-webkit-box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
box-shadow: 0 0 0 0.2rem rgba(76, 91, 106, 0.5);
}
/* icons within buttons */
.fc-button .fc-icon {
vertical-align: middle;
font-size: 1.5em;
}
/* Buttons Groups
--------------------------------------------------------------------------------------------------*/
.fc-button-group {
position: relative;
display: -webkit-inline-box;
display: -ms-inline-flexbox;
display: inline-flex;
vertical-align: middle;
}
.fc-button-group > .fc-button {
position: relative;
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
}
.fc-button-group > .fc-button:hover {
z-index: 1;
}
.fc-button-group > .fc-button:focus,
.fc-button-group > .fc-button:active,
.fc-button-group > .fc-button.fc-button-active {
z-index: 1;
}
.fc-button-group > .fc-button:not(:first-child) {
margin-left: -1px;
}
.fc-button-group > .fc-button:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.fc-button-group > .fc-button:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
/* Popover
--------------------------------------------------------------------------------------------------*/
.fc-unthemed .fc-popover {
border-width: 1px;
border-style: solid;
}
/* List View
--------------------------------------------------------------------------------------------------*/
.fc-unthemed .fc-list-item:hover td {
background-color: #f5f5f5;
}
/* Toolbar
--------------------------------------------------------------------------------------------------*/
.fc-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.fc-toolbar.fc-header-toolbar {
margin-bottom: 1.5em;
}
.fc-toolbar.fc-footer-toolbar {
margin-top: 1.5em;
}
/* inner content */
.fc-toolbar > * > :not(:first-child) {
margin-left: 0.75em;
}
.fc-toolbar h2 {
font-size: 1.75em;
margin: 0;
}
/* View Structure
--------------------------------------------------------------------------------------------------*/
.fc-view-container {
position: relative;
}
/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */
/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */
.fc-view-container *,
.fc-view-container *:before,
.fc-view-container *:after {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.fc-view,
.fc-view > table {
/* so dragged elements can be above the view's main element */
position: relative;
z-index: 1;
}
@media print {
.fc {
max-width: 100% !important;
}
/* Global Event Restyling
--------------------------------------------------------------------------------------------------*/
.fc-event {
background: #fff !important;
color: #000 !important;
page-break-inside: avoid;
}
.fc-event .fc-resizer {
display: none;
}
/* Table & Day-Row Restyling
--------------------------------------------------------------------------------------------------*/
.fc th,
.fc td,
.fc hr,
.fc thead,
.fc tbody,
.fc-row {
border-color: #ccc !important;
background: #fff !important;
}
/* kill the overlaid, absolutely-positioned components */
/* common... */
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-mirror-skeleton,
.fc-bgevent-container,
.fc-business-container,
.fc-highlight-container,
.fc-mirror-container {
display: none;
}
/* don't force a min-height on rows (for DayGrid) */
.fc tbody .fc-row {
height: auto !important;
/* undo height that JS set in distributeHeight */
min-height: 0 !important;
/* undo the min-height from each view's specific stylesheet */
}
.fc tbody .fc-row .fc-content-skeleton {
position: static;
/* undo .fc-rigid */
padding-bottom: 0 !important;
/* use a more border-friendly method for this... */
}
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td {
/* only works in newer browsers */
padding-bottom: 1em;
/* ...gives space within the skeleton. also ensures min height in a way */
}
.fc tbody .fc-row .fc-content-skeleton table {
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
making it look more like 3em. for other browers, it will already be this tall */
height: 1em;
}
/* Undo month-view event limiting. Display all events and hide the "more" links
--------------------------------------------------------------------------------------------------*/
.fc-more-cell,
.fc-more {
display: none !important;
}
.fc tr.fc-limited {
display: table-row !important;
}
.fc td.fc-limited {
display: table-cell !important;
}
.fc-popover {
display: none;
/* never display the "more.." popover in print mode */
}
/* TimeGrid Restyling
--------------------------------------------------------------------------------------------------*/
/* undo the min-height 100% trick used to fill the container's height */
.fc-time-grid {
min-height: 0 !important;
}
/* don't display the side axis at all ("all-day" and time cells) */
.fc-timeGrid-view .fc-axis {
display: none;
}
/* don't display the horizontal lines */
.fc-slats,
.fc-time-grid hr {
/* this hr is used when height is underused and needs to be filled */
display: none !important;
/* important overrides inline declaration */
}
/* let the container that holds the events be naturally positioned and create real height */
.fc-time-grid .fc-content-skeleton {
position: static;
}
/* in case there are no events, we still want some height */
.fc-time-grid .fc-content-skeleton table {
height: 4em;
}
/* kill the horizontal spacing made by the event container. event margins will be done below */
.fc-time-grid .fc-event-container {
margin: 0 !important;
}
/* TimeGrid *Event* Restyling
--------------------------------------------------------------------------------------------------*/
/* naturally position events, vertically stacking them */
.fc-time-grid .fc-event {
position: static !important;
margin: 3px 2px !important;
}
/* for events that continue to a future day, give the bottom border back */
.fc-time-grid .fc-event.fc-not-end {
border-bottom-width: 1px !important;
}
/* indicate the event continues via "..." text */
.fc-time-grid .fc-event.fc-not-end:after {
content: "...";
}
/* for events that are continuations from previous days, give the top border back */
.fc-time-grid .fc-event.fc-not-start {
border-top-width: 1px !important;
}
/* indicate the event is a continuation via "..." text */
.fc-time-grid .fc-event.fc-not-start:before {
content: "...";
}
/* time */
/* undo a previous declaration and let the time text span to a second line */
.fc-time-grid .fc-event .fc-time {
white-space: normal !important;
}
/* hide the the time that is normally displayed... */
.fc-time-grid .fc-event .fc-time span {
display: none;
}
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
.fc-time-grid .fc-event .fc-time:after {
content: attr(data-full);
}
/* Vertical Scroller & Containers
--------------------------------------------------------------------------------------------------*/
/* kill the scrollbars and allow natural height */
.fc-scroller,
.fc-day-grid-container,
.fc-time-grid-container {
/* */
overflow: visible !important;
height: auto !important;
}
/* kill the horizontal border/padding used to compensate for scrollbars */
.fc-row {
border: 0 !important;
margin: 0 !important;
}
/* Button Controls
--------------------------------------------------------------------------------------------------*/
.fc-button-group,
.fc button {
display: none;
/* don't display any button-related controls */
}
}
.fc-scroller-clip{overflow:hidden;position:relative}.fc-no-scrollbars{background:rgba(255,255,255,0)}.fc-no-scrollbars::-webkit-scrollbar{width:0;height:0}.fc-scroller-canvas{position:relative;box-sizing:border-box;min-height:100%}.fc-scroller-canvas>.fc-bg{z-index:1}.fc-scroller-canvas>.fc-content{z-index:2;position:relative;border-style:solid;border-width:0}.fc-scroller-canvas.fc-gutter-left>.fc-content{border-left-width:1px;margin-left:-1px}.fc-scroller-canvas.fc-gutter-right>.fc-content{border-right-width:1px;margin-right:-1px}.fc-scroller-canvas.fc-gutter-top>.fc-content{border-top-width:1px;margin-top:-1px}.fc-rtl .fc-timeline{direction:rtl}.fc-scrolled .fc-head .fc-scroller{z-index:2}.fc-timeline.fc-scrolled .fc-head .fc-scroller{box-shadow:0 3px 4px rgba(0,0,0,.075)}.fc-timeline .fc-body .fc-scroller{z-index:1}.fc-timeline .fc-scroller-canvas>div>div>table,.fc-timeline .fc-scroller-canvas>div>table{border-style:hidden}.fc-timeline .fc-scroller-canvas>.fc-content>.fc-rows>table{border-bottom-style:none}.fc-timeline td,.fc-timeline th{white-space:nowrap}.fc-timeline .fc-cell-content{overflow:hidden}.fc-timeline .fc-cell-text{display:inline-block;padding-left:4px;padding-right:4px}.fc-timeline th{vertical-align:middle}.fc-timeline .fc-head .fc-cell-content{padding-top:3px;padding-bottom:3px}.fc-timeline .fc-head .fc-time-area .fc-cell-content{overflow:visible}.fc-time-area col{min-width:2.2em}.fc-ltr .fc-time-area .fc-chrono th{text-align:left}.fc-rtl .fc-time-area .fc-chrono th{text-align:right}.fc-time-area .fc-slats{position:absolute;z-index:1;top:0;left:0;right:0;bottom:0}.fc-time-area .fc-slats table{height:100%}.fc-time-area .fc-slats .fc-minor{border-style:dotted}.fc-time-area .fc-slats td{border-width:0 1px}.fc-ltr .fc-time-area .fc-slats td{border-right-width:0}.fc-rtl .fc-time-area .fc-slats td{border-left-width:0}.fc-time-area .fc-bgevent-container,.fc-time-area .fc-highlight-container{position:absolute;z-index:2;top:0;bottom:0;width:0}.fc-ltr .fc-time-area .fc-bgevent-container,.fc-ltr .fc-time-area .fc-highlight-container,.fc-ltr .fc-time-area .fc-mirror-container{left:0}.fc-rtl .fc-time-area .fc-bgevent-container,.fc-rtl .fc-time-area .fc-highlight-container,.fc-rtl .fc-time-area .fc-mirror-container{right:0}.fc-time-area .fc-bgevent,.fc-time-area .fc-highlight{position:absolute;top:0;bottom:0}.fc-timeline .fc-now-indicator{z-index:3;top:0}.fc-time-area .fc-now-indicator-arrow{margin:0 -6px;border-width:6px 5px 0;border-left-color:transparent;border-right-color:transparent}.fc-time-area .fc-now-indicator-line{margin:0 -1px;bottom:0;border-left-width:1px}.fc-time-area .fc-event-container{position:relative;z-index:2;width:0}.fc-time-area .fc-mirror-container{position:absolute;z-index:3;top:0}.fc-time-area .fc-event-container{padding-bottom:8px;top:-1px}.fc-time-area tr:first-child .fc-event-container{top:0}.fc-no-overlap .fc-time-area .fc-event-container{padding-bottom:0;top:0}.fc-timeline-event{position:absolute;display:flex;border-radius:0;padding:2px 1px;margin-bottom:1px}.fc-no-overlap .fc-timeline-event{padding-top:5px;padding-bottom:5px;margin-bottom:0}.fc-ltr .fc-timeline-event{flex-direction:row;margin-right:1px}.fc-rtl .fc-timeline-event{margin-left:1px}.fc-timeline-event .fc-time-wrap{flex-shrink:0;min-width:0}.fc-timeline-event .fc-title-wrap{flex-grow:1;min-width:0}.fc-timeline-event .fc-time,.fc-timeline-event .fc-title{display:inline-block;vertical-align:top;max-width:100%;padding:0 2px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;white-space:nowrap;overflow:hidden}.fc-timeline-event .fc-time{font-weight:700}.fc-timeline-event.fc-not-end:after,.fc-timeline-event.fc-not-start:before{content:"";align-self:center;width:0;height:0;margin:0 1px;border:5px solid #000;border-top-color:transparent;border-bottom-color:transparent;opacity:.5}.fc-ltr .fc-timeline-event.fc-not-start:before,.fc-rtl .fc-timeline-event.fc-not-end:after{border-left:0}.fc-ltr .fc-timeline-event.fc-not-end:after,.fc-rtl .fc-timeline-event.fc-not-start:before{border-right:0}
/* Scroller
--------------------------------------------------------------------------------------------------*/
.fc-scroller-clip {
overflow: hidden;
/* for clipping scrollbars */
position: relative;
/* so things like scrollfollowers can attach to this */
}
/* supresses rendering of native scrollbars */
/* on .fc-scroller */
.fc-no-scrollbars {
background: rgba(255, 255, 255, 0);
/* hack for dynamic DOM nodes (in Chrome OSX at least) */
}
.fc-no-scrollbars::-webkit-scrollbar {
width: 0;
height: 0;
}
.fc-scroller-canvas {
position: relative;
/* origin for bg */
box-sizing: border-box;
/* so that padding (for gutter) will be part of height */
min-height: 100%;
}
.fc-scroller-canvas > .fc-bg {
z-index: 1;
/* make default? */
}
.fc-scroller-canvas > .fc-content {
z-index: 2;
/* make default? */
position: relative;
/* origin for inner content */
border-style: solid;
border-width: 0;
}
.fc-scroller-canvas.fc-gutter-left > .fc-content {
border-left-width: 1px;
margin-left: -1px;
}
.fc-scroller-canvas.fc-gutter-right > .fc-content {
border-right-width: 1px;
margin-right: -1px;
}
.fc-scroller-canvas.fc-gutter-top > .fc-content {
border-top-width: 1px;
margin-top: -1px;
}
/* content is responsible for bottom border */
/* View Structure
--------------------------------------------------------------------------------------------------*/
.fc-rtl .fc-timeline {
direction: rtl;
}
.fc-scrolled .fc-head .fc-scroller {
z-index: 2;
/* so drop shadow will go above body panes */
}
.fc-timeline.fc-scrolled .fc-head .fc-scroller {
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.075);
}
.fc-timeline .fc-body .fc-scroller {
z-index: 1;
}
/*
on most tables that expand to the edges, kill the outer border,
because the container elements take care of it.
example tables:
.fc-scroller-canvas .fc-content table
.fc-scroller-canvas .fc-bg .fc-slats table
*/
.fc-timeline .fc-scroller-canvas > div > table,
.fc-timeline .fc-scroller-canvas > div > div > table {
border-style: hidden;
}
/*
for resource rows (in both the spreadsheet and timeline areas),
undo previous rule in order to always show last border.
*/
.fc-timeline .fc-scroller-canvas > .fc-content > .fc-rows > table {
border-bottom-style: none;
}
/* Table Cell Common
--------------------------------------------------------------------------------------------------*/
.fc-timeline th,
.fc-timeline td {
white-space: nowrap;
}
.fc-timeline .fc-cell-content {
overflow: hidden;
}
.fc-timeline .fc-cell-text {
display: inline-block;
padding-left: 4px;
padding-right: 4px;
}
/*
Cells at the start of a week
TODO: figure out better styling
.fc-ltr .fc-timeline .fc-em-cell div {
border-left: 3px solid #eee;
height: 100%;
}
.fc-rtl .fc-timeline .fc-em-cell {
border-right-width: 3px;
}
*/
/* head */
.fc-timeline th {
vertical-align: middle;
}
.fc-timeline .fc-head .fc-cell-content {
padding-top: 3px;
padding-bottom: 3px;
}
.fc-timeline .fc-head .fc-time-area .fc-cell-content {
overflow: visible;
}
/* Time Area
--------------------------------------------------------------------------------------------------*/
.fc-time-area col {
min-width: 2.2em;
/* detected by JS */
}
/* head */
.fc-ltr .fc-time-area .fc-chrono th {
text-align: left;
}
.fc-rtl .fc-time-area .fc-chrono th {
text-align: right;
}
/* body slats (vertical lines) */
.fc-time-area .fc-slats {
/* fc-bg is responsible for a lot of this now! */
position: absolute;
z-index: 1;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.fc-time-area .fc-slats table {
height: 100%;
}
.fc-time-area .fc-slats .fc-minor {
border-style: dotted;
}
.fc-time-area .fc-slats td {
border-width: 0 1px;
/* need to do this. sometimes -1 margin wouldn't hide the dotted */
}
.fc-ltr .fc-time-area .fc-slats td {
border-right-width: 0;
}
.fc-rtl .fc-time-area .fc-slats td {
border-left-width: 0;
}
/* body content containers
can be within rows or directly within the pane's content
*/
.fc-time-area .fc-bgevent-container,
.fc-time-area .fc-highlight-container {
position: absolute;
z-index: 2;
/* only for directly within pane. not for row. overridden later */
top: 0;
bottom: 0;
width: 0;
}
.fc-ltr .fc-time-area .fc-mirror-container,
.fc-ltr .fc-time-area .fc-bgevent-container,
.fc-ltr .fc-time-area .fc-highlight-container {
left: 0;
}
.fc-rtl .fc-time-area .fc-mirror-container,
.fc-rtl .fc-time-area .fc-bgevent-container,
.fc-rtl .fc-time-area .fc-highlight-container {
right: 0;
}
.fc-time-area .fc-bgevent,
.fc-time-area .fc-highlight {
position: absolute;
top: 0;
bottom: 0;
}
/* Now Indicator
--------------------------------------------------------------------------------------------------*/
.fc-timeline .fc-now-indicator {
/* both the arrow and the line */
z-index: 3;
/* one above scroller's fc-content */
top: 0;
}
.fc-time-area .fc-now-indicator-arrow {
margin: 0 -6px;
/* 5, then one more to counteract scroller's negative margins */
/* triangle pointing down... */
border-width: 6px 5px 0 5px;
border-left-color: transparent;
border-right-color: transparent;
}
.fc-time-area .fc-now-indicator-line {
margin: 0 -1px;
/* counteract scroller's negative margins */
bottom: 0;
border-left-width: 1px;
}
/* Event Container
--------------------------------------------------------------------------------------------------*/
.fc-time-area .fc-event-container {
position: relative;
z-index: 2;
/* above bgevent and highlight */
width: 0;
/* for event positioning. will end up on correct side based on dir */
}
.fc-time-area .fc-mirror-container {
/* also an fc-event-container */
position: absolute;
z-index: 3;
top: 0;
}
.fc-time-area .fc-event-container {
padding-bottom: 8px;
top: -1px;
}
.fc-time-area tr:first-child .fc-event-container {
top: 0;
}
.fc-no-overlap .fc-time-area .fc-event-container {
padding-bottom: 0;
top: 0;
}
/* Time Grid Events
--------------------------------------------------------------------------------------------------*/
.fc-timeline-event {
position: absolute;
display: flex;
border-radius: 0;
padding: 2px 1px;
margin-bottom: 1px;
}
.fc-no-overlap .fc-timeline-event {
padding-top: 5px;
padding-bottom: 5px;
margin-bottom: 0;
}
.fc-ltr .fc-timeline-event {
flex-direction: row;
margin-right: 1px;
}
.fc-rtl .fc-timeline-event {
margin-left: 1px;
}
.fc-timeline-event .fc-time-wrap {
flex-shrink: 0;
min-width: 0;
}
.fc-timeline-event .fc-title-wrap {
flex-grow: 1;
min-width: 0;
}
.fc-timeline-event .fc-time,
.fc-timeline-event .fc-title {
display: inline-block;
vertical-align: top;
max-width: 100%;
padding: 0 2px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
}
.fc-timeline-event .fc-time {
font-weight: bold;
}
.fc-timeline-event.fc-not-start:before,
.fc-timeline-event.fc-not-end:after {
content: "";
align-self: center;
width: 0;
height: 0;
margin: 0 1px;
border: 5px solid #000;
border-top-color: transparent;
border-bottom-color: transparent;
opacity: 0.5;
}
.fc-ltr .fc-timeline-event.fc-not-start:before,
.fc-rtl .fc-timeline-event.fc-not-end:after {
border-left: 0;
}
.fc-ltr .fc-timeline-event.fc-not-end:after,
.fc-rtl .fc-timeline-event.fc-not-start:before {
border-right: 0;
}
.fc-timeline .fc-divider{width:3px;border-style:double}.fc-timeline .fc-head>tr>.fc-divider{border-bottom:0}.fc-timeline .fc-body>tr>.fc-divider{border-top:0}.fc-resource-area{width:30%}.fc-resource-area col{width:40%;min-width:70px}.fc-resource-area col.fc-main-col{width:60%}.fc-flat .fc-expander-space{display:none}.fc-ltr .fc-resource-area tr>*{text-align:left}.fc-rtl .fc-resource-area tr>*{text-align:right}.fc-resource-area .fc-cell-content{padding-left:4px;padding-right:4px}.fc-resource-area .fc-super th{text-align:center}.fc-resource-area th>div{position:relative}.fc-resource-area th .fc-cell-content{position:relative;z-index:1}.fc-resource-area th .fc-col-resizer{position:absolute;z-index:2;top:0;bottom:0;width:5px}.fc-timeline .fc-col-resizer{cursor:col-resize}.fc-ltr .fc-resource-area th .fc-col-resizer{right:-3px}.fc-rtl .fc-resource-area th .fc-col-resizer{left:-3px}.fc-body .fc-resource-area .fc-cell-content{padding-top:8px;padding-bottom:8px}.fc-no-overlap .fc-body .fc-resource-area .fc-cell-content{padding-top:6px;padding-bottom:6px}.fc-resource-area .fc-icon{display:inline-block;width:1em;text-align:center}.fc-resource-area .fc-expander{cursor:pointer;opacity:.65}.fc-time-area .fc-rows{position:relative;z-index:3}.fc-time-area .fc-rows td>div{position:relative}.fc-time-area .fc-rows .fc-bgevent-container,.fc-time-area .fc-rows .fc-highlight-container{z-index:1}
/* Divider between resources and time area
--------------------------------------------------------------------------------------------------*/
.fc-timeline .fc-divider {
width: 3px;
border-style: double;
/* overcome neighboring borders */
}
.fc-timeline .fc-head > tr > .fc-divider {
border-bottom: 0;
}
.fc-timeline .fc-body > tr > .fc-divider {
border-top: 0;
}
/* Resource Area
--------------------------------------------------------------------------------------------------*/
.fc-resource-area {
width: 30%;
}
.fc-resource-area col {
width: 40%;
min-width: 70px;
/* will be read by JS */
}
.fc-resource-area col.fc-main-col {
width: 60%;
/* make the first column in a nested setup bigger */
}
.fc-flat .fc-expander-space {
/* fc-flat is opposite of fc-nested */
display: none;
}
.fc-ltr .fc-resource-area tr > * {
text-align: left;
}
.fc-rtl .fc-resource-area tr > * {
text-align: right;
}
.fc-resource-area .fc-cell-content {
padding-left: 4px;
padding-right: 4px;
}
/* head */
.fc-resource-area .fc-super th {
text-align: center;
}
.fc-resource-area th > div {
position: relative;
}
.fc-resource-area th .fc-cell-content {
position: relative;
z-index: 1;
}
.fc-resource-area th .fc-col-resizer {
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
width: 5px;
}
.fc-timeline .fc-col-resizer {
cursor: col-resize;
}
.fc-ltr .fc-resource-area th .fc-col-resizer {
right: -3px;
}
.fc-rtl .fc-resource-area th .fc-col-resizer {
left: -3px;
}
/* body */
.fc-body .fc-resource-area .fc-cell-content {
/* might BE the cell */
padding-top: 8px;
padding-bottom: 8px;
}
.fc-no-overlap .fc-body .fc-resource-area .fc-cell-content {
/* might BE the cell */
padding-top: 6px;
padding-bottom: 6px;
}
.fc-resource-area .fc-icon {
/* the expander and spacers before the expander */
display: inline-block;
width: 1em;
/* ensure constant width, esp for empty icons */
text-align: center;
}
.fc-resource-area .fc-expander {
cursor: pointer;
opacity: 0.65;
}
/* body resource rows */
.fc-time-area .fc-rows {
position: relative;
z-index: 3;
}
.fc-time-area .fc-rows td > div {
position: relative;
}
.fc-time-area .fc-rows .fc-bgevent-container,
.fc-time-area .fc-rows .fc-highlight-container {
z-index: 1;
}
.fc.fc-bootstrap a{text-decoration:none}.fc.fc-bootstrap a[data-goto]:hover{text-decoration:underline}.fc-bootstrap hr.fc-divider{border-color:inherit}.fc-bootstrap .fc-today.alert{border-radius:0}.fc-bootstrap a.fc-event:not([href]):not([tabindex]){color:#fff}.fc-bootstrap .fc-popover.card{position:absolute}.fc-bootstrap .fc-popover .card-body{padding:0}.fc-bootstrap .fc-time-grid .fc-slats table{background:0 0}
.fc.fc-bootstrap a {
text-decoration: none;
}
.fc.fc-bootstrap a[data-goto]:hover {
text-decoration: underline;
}
.fc-bootstrap hr.fc-divider {
border-color: inherit;
}
.fc-bootstrap .fc-today.alert {
border-radius: 0;
}
.fc-bootstrap a.fc-event:not([href]):not([tabindex]) {
color: #fff;
}
.fc-bootstrap .fc-popover.card {
position: absolute;
}
/* Popover
--------------------------------------------------------------------------------------------------*/
.fc-bootstrap .fc-popover .card-body {
padding: 0;
}
/* TimeGrid Slats (lines that run horizontally)
--------------------------------------------------------------------------------------------------*/
.fc-bootstrap .fc-time-grid .fc-slats table {
/* some themes have background color. see through to slats */
background: none;
}
// Place all the styles related to the email_templates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
//= link_tree ../images
//= link_tree ../audios
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= link application.js
//= link_tree ../builds
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
CS
SM
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
.cls-1 { fill: #1f1e1e; }
.cls-1 { fill: #3ddc84; }
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
<![CDATA[
.fil1 {fill:#FEFEFE}
.fil0 {fill:#373435}
]]>
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
Error reading file: wrong argument type nil (expected String)
/*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}.tooltip-arrow,.tooltip-arrow:before{background:inherit;height:8px;position:absolute;width:8px}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";transform:rotate(45deg);visibility:visible}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-color:#e5e7eb;border-style:solid}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-left-width:1px;border-top-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-right-width:1px;border-top-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{background:inherit;height:8px;position:absolute;width:8px}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";transform:rotate(45deg);visibility:visible}[data-popper-arrow]:after{background:inherit;height:9px;position:absolute;width:9px}[role=tooltip]>[data-popper-arrow]:before{border-color:#e5e7eb;border-style:solid}.dark [role=tooltip]>[data-popper-arrow]:before{border-color:#4b5563;border-style:solid}[role=tooltip]>[data-popper-arrow]:after{border-color:#e5e7eb;border-style:solid}.dark [role=tooltip]>[data-popper-arrow]:after{border-color:#4b5563;border-style:solid}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-left-width:1px;border-top-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-right-width:1px;border-top-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#1c64f2;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#1c64f2;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' aria-hidden='true' viewBox='0 0 10 6'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m1 1 4 4 4-4'/%3E%3C/svg%3E");background-position:right .75rem center;background-repeat:no-repeat;background-size:.75em .75em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}:is([dir=rtl]) select:not([size]){background-position:left .75rem center;padding-left:0;padding-right:.75rem}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#1c64f2;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#1c64f2;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:.55em .55em;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' aria-hidden='true' viewBox='0 0 16 12'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M1 5.917 5.724 10.5 15 1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-size:.55em .55em;-webkit-print-color-adjust:exact;print-color-adjust:exact}.dark [type=radio]:checked,[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E");background-size:1em 1em}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' aria-hidden='true' viewBox='0 0 16 12'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M.5 6h14'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:.55em .55em;-webkit-print-color-adjust:exact;print-color-adjust:exact}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{-webkit-margin-start:-1rem;-webkit-margin-end:1rem;background:#1f2937;border:0;color:#fff;cursor:pointer;font-size:.875rem;font-weight:500;margin-inline-end:1rem;margin-inline-start:-1rem;padding:.625rem 1rem .625rem 2rem}input[type=file]::file-selector-button:hover{background:#374151}:is([dir=rtl]) input[type=file]::file-selector-button{padding-left:1rem;padding-right:2rem}.dark input[type=file]::file-selector-button{background:#4b5563;color:#fff}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{appearance:none;-moz-appearance:none;-webkit-appearance:none;background:#1c64f2;border:0;border-radius:9999px;cursor:pointer;height:1.25rem;width:1.25rem}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid #0000;outline-offset:2px}input[type=range]::-moz-range-thumb{appearance:none;-moz-appearance:none;-webkit-appearance:none;background:#1c64f2;border:0;border-radius:9999px;cursor:pointer;height:1.25rem;width:1.25rem}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#3f83f8}input[type=range]::-ms-fill-lower{background:#3f83f8}.toggle-bg:after{background:#fff;border-color:#d1d5db;border-radius:9999px;border-width:1px;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);content:"";height:1.25rem;left:.125rem;position:absolute;top:.125rem;transition-duration:.15s;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;width:1.25rem}input:checked+.toggle-bg:after{border-color:#fff;transform:translateX(100%);}input:checked+.toggle-bg{background:#1c64f2;border-color:#1c64f2}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3f83f880;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.aspect-h-9{--tw-aspect-h:9}.aspect-w-16{--tw-aspect-w:16;padding-bottom:calc(var(--tw-aspect-h)/var(--tw-aspect-w)*100%);position:relative}.aspect-w-16>*{bottom:0;height:100%;left:0;position:absolute;right:0;top:0;width:100%}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-top-1{top:-.25rem}.-top-1\.5{top:-.375rem}.bottom-0{bottom:0}.bottom-20{bottom:5rem}.bottom-24{bottom:6rem}.bottom-4{bottom:1rem}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-auto{grid-column:auto}.float-right{float:right}.m-0{margin:0}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-3{margin:.75rem}.m-5{margin:1.25rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-bottom:0;margin-top:0}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-3{margin-bottom:.75rem;margin-top:.75rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-5{margin-bottom:1.25rem;margin-top:1.25rem}.-ml-1{margin-left:-.25rem}.-mt-20{margin-top:-5rem}.-mt-4{margin-top:-1rem}.mb-0{margin-bottom:0}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.me-2{-webkit-margin-end:.5rem;margin-inline-end:.5rem}.ml-1{margin-left:.25rem}.ml-10{margin-left:2.5rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-8{margin-left:2rem}.ml-auto{margin-left:auto}.mr-0{margin-right:0}.mr-1{margin-right:.25rem}.mr-1\.5{margin-right:.375rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.table-column{display:table-column}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[576px\]{height:576px}.h-auto{height:auto}.h-full{height:100%}.max-h-0{max-height:0}.max-h-\[32px\]{max-height:32px}.max-h-\[40px\]{max-height:40px}.max-h-screen{max-height:100vh}.min-h-\[128px\]{min-height:128px}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-10\/12{width:83.333333%}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-4\/12{width:33.333333%}.w-44{width:11rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-6\/12{width:50%}.w-64{width:16rem}.w-8{width:2rem}.w-\[480px\]{width:480px}.w-auto{width:auto}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-\[10rem\]{min-width:10rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[14rem\]{min-width:14rem}.min-w-\[200px\]{min-width:200px}.min-w-\[640px\]{min-width:640px}.min-w-\[6rem\]{min-width:6rem}.min-w-\[7rem\]{min-width:7rem}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[32px\]{max-width:32px}.max-w-\[40px\]{max-width:40px}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-md{max-width:768px}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-6{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-6{--tw-translate-x:-1.5rem}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-8{--tw-translate-y:-2rem}.-translate-y-8,.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.translate-y-0{--tw-translate-y:0px}.translate-y-0,.translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-1\/2{--tw-translate-y:50%}.translate-y-\[-100\%\]{--tw-translate-y:-100%}.translate-y-\[-100\%\],.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.rotate-45{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate:45deg}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-100,.scale-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-90{--tw-scale-x:.9;--tw-scale-y:.9}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-none{transform:none}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-10{row-gap:2.5rem}.gap-y-2{row-gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.25rem*var(--tw-space-x-reverse))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-scroll{overflow-x:scroll}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-\[7px\]{border-radius:7px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-e-lg{border-end-end-radius:.5rem;border-start-end-radius:.5rem}.rounded-l-lg{border-bottom-left-radius:.5rem;border-top-left-radius:.5rem}.rounded-r-lg{border-bottom-right-radius:.5rem;border-top-right-radius:.5rem}.rounded-s-lg{border-end-start-radius:.5rem;border-start-start-radius:.5rem}.rounded-t-2xl{border-top-left-radius:1rem;border-top-right-radius:1rem}.rounded-bl-full{border-bottom-left-radius:9999px}.rounded-br-lg{border-bottom-right-radius:.5rem}.rounded-tl-full{border-top-left-radius:9999px}.rounded-tr-full{border-top-right-radius:9999px}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-b-4{border-bottom-width:4px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-solid{border-style:solid}.border-blue-600{--tw-border-opacity:1;border-color:rgb(28 100 242/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(26 86 219/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.border-green-400{--tw-border-opacity:1;border-color:rgb(49 196 141/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(249 128 128/var(--tw-border-opacity))}.border-transparent{border-color:#0000}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.border-t-transparent{border-top-color:#0000}.bg-\[\#ff6600\]{--tw-bg-opacity:1;background-color:rgb(255 102 0/var(--tw-bg-opacity))}.bg-\[\#fff0e6\]{--tw-bg-opacity:1;background-color:rgb(255 240 230/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(225 239 254/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(63 131 248/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(28 100 242/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(26 86 219/var(--tw-bg-opacity))}.bg-blue-900{--tw-bg-opacity:1;background-color:rgb(35 56 118/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-gray-900\/50{background-color:#11182780}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(222 247 236/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(188 240 218/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(243 250 247/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(14 159 110/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(253 232 232/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(253 242 242/var(--tw-bg-opacity))}.bg-red-700{--tw-bg-opacity:1;background-color:rgb(200 30 30/var(--tw-bg-opacity))}.bg-transparent{background-color:initial}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-opacity-60{--tw-bg-opacity:0.6}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-blue-600{--tw-gradient-from:#1c64f2 var(--tw-gradient-from-position);--tw-gradient-to:#1c64f200 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-100{--tw-gradient-from:#f3f4f6 var(--tw-gradient-from-position);--tw-gradient-to:#f3f4f600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-600{--tw-gradient-from:#057a55 var(--tw-gradient-from-position);--tw-gradient-to:#057a5500 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-600{--tw-gradient-from:#d03801 var(--tw-gradient-from-position);--tw-gradient-to:#d0380100 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-600{--tw-gradient-from:#d61f69 var(--tw-gradient-from-position);--tw-gradient-to:#d61f6900 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-500{--tw-gradient-from:#f05252 var(--tw-gradient-from-position);--tw-gradient-to:#f0525200 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-blue-400{--tw-gradient-to:#76a9fa var(--tw-gradient-to-position)}.to-gray-200{--tw-gradient-to:#e5e7eb var(--tw-gradient-to-position)}.to-green-400{--tw-gradient-to:#31c48d var(--tw-gradient-to-position)}.to-orange-400{--tw-gradient-to:#ff8a4c var(--tw-gradient-to-position)}.to-pink-400{--tw-gradient-to:#f17eb8 var(--tw-gradient-to-position)}.to-red-700{--tw-gradient-to:#c81e1e var(--tw-gradient-to-position)}.bg-cover{background-size:cover}.bg-clip-border{background-clip:initial}.bg-center{background-position:50%}.bg-repeat{background-repeat:repeat}.bg-no-repeat{background-repeat:no-repeat}.fill-current{fill:currentColor}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-0{padding-left:0;padding-right:0}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-bottom:0;padding-top:0}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-16{padding-bottom:4rem;padding-top:4rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-20{padding-bottom:5rem;padding-top:5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-48{padding-bottom:12rem;padding-top:12rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-10{padding-bottom:2.5rem}.pb-2{padding-bottom:.5rem}.pb-20{padding-bottom:5rem}.pb-3{padding-bottom:.75rem}.pb-32{padding-bottom:8rem}.pb-4{padding-bottom:1rem}.pb-5{padding-bottom:1.25rem}.pl-0{padding-left:0}.pl-20{padding-left:5rem}.pl-4{padding-left:1rem}.pr-0{padding-right:0}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-0{padding-top:0}.pt-10{padding-top:2.5rem}.pt-2{padding-top:.5rem}.pt-20{padding-top:5rem}.pt-3{padding-top:.75rem}.pt-40{padding-top:10rem}.pt-5{padding-top:1.25rem}.pt-60{padding-top:15rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-sans{font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-normal{line-height:1.5}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-normal{letter-spacing:0}.text-\[\#333333\]{--tw-text-opacity:1;color:rgb(51 51 51/var(--tw-text-opacity))}.text-\[\#993d00\]{--tw-text-opacity:1;color:rgb(153 61 0/var(--tw-text-opacity))}.text-\[\#ff6600\]{--tw-text-opacity:1;color:rgb(255 102 0/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(63 131 248/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(28 100 242/var(--tw-text-opacity))}.text-blue-900{--tw-text-opacity:1;color:rgb(35 56 118/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(14 159 110/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(5 122 85/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(4 108 78/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(3 84 63/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(88 80 236/var(--tw-text-opacity))}.text-purple-500{--tw-text-opacity:1;color:rgb(144 97 249/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(240 82 82/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(224 36 36/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(200 30 30/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(155 28 28/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgb(227 160 8/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.text-opacity-50{--tw-text-opacity:0.5}.text-opacity-70{--tw-text-opacity:0.7}.text-opacity-90{--tw-text-opacity:0.9}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.placeholder-gray-400::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}.placeholder-gray-400::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 #0000000d;--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.shadow-none,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-blue-500\/40{--tw-shadow-color:#3f83f866;--tw-shadow:var(--tw-shadow-colored)}.shadow-green-500\/40{--tw-shadow-color:#0e9f6e66;--tw-shadow:var(--tw-shadow-colored)}.shadow-orange-500\/40{--tw-shadow-color:#ff5a1f66;--tw-shadow:var(--tw-shadow-colored)}.shadow-pink-500\/40{--tw-shadow-color:#e7469466;--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid #0000;outline-offset:2px}.outline{outline-style:solid}.outline-0{outline-width:0}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-transform{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.before\:pointer-events-none:before{content:var(--tw-content);pointer-events:none}.before\:mr-1:before{content:var(--tw-content);margin-right:.25rem}.before\:mt-\[6\.5px\]:before{content:var(--tw-content);margin-top:6.5px}.before\:box-border:before{box-sizing:border-box;content:var(--tw-content)}.before\:block:before{content:var(--tw-content);display:block}.before\:h-1:before{content:var(--tw-content);height:.25rem}.before\:h-1\.5:before{content:var(--tw-content);height:.375rem}.before\:w-2:before{content:var(--tw-content);width:.5rem}.before\:w-2\.5:before{content:var(--tw-content);width:.625rem}.before\:rounded-tl-md:before{border-top-left-radius:.375rem;content:var(--tw-content)}.before\:border-l:before{border-left-width:1px;content:var(--tw-content)}.before\:border-t:before{border-top-width:1px;content:var(--tw-content)}.before\:transition-all:before{content:var(--tw-content);transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:ml-1:after{content:var(--tw-content);margin-left:.25rem}.after\:mt-\[6\.5px\]:after{content:var(--tw-content);margin-top:6.5px}.after\:box-border:after{box-sizing:border-box;content:var(--tw-content)}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-1\.5:after{content:var(--tw-content);height:.375rem}.after\:w-2:after{content:var(--tw-content);width:.5rem}.after\:w-2\.5:after{content:var(--tw-content);width:.625rem}.after\:flex-grow:after{content:var(--tw-content);flex-grow:1}.after\:rounded-tr-md:after{border-top-right-radius:.375rem;content:var(--tw-content)}.after\:border-r:after{border-right-width:1px;content:var(--tw-content)}.after\:border-t:after{border-top-width:1px;content:var(--tw-content)}.after\:transition-all:after{content:var(--tw-content);transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.placeholder-shown\:border:-moz-placeholder-shown{border-width:1px}.placeholder-shown\:border:placeholder-shown{border-width:1px}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-blue-800:hover{--tw-border-opacity:1;border-color:rgb(30 66 159/var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:bg-blue-50:hover{--tw-bg-opacity:1;background-color:rgb(235 245 255/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(28 100 242/var(--tw-bg-opacity))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(26 86 219/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(30 66 159/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:bg-red-800:hover{--tw-bg-opacity:1;background-color:rgb(155 28 28/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:bg-yellow-600:hover{--tw-bg-opacity:1;background-color:rgb(159 88 10/var(--tw-bg-opacity))}.hover\:bg-opacity-80:hover{--tw-bg-opacity:0.8}.hover\:bg-opacity-90:hover{--tw-bg-opacity:0.9}.hover\:from-red-600:hover{--tw-gradient-from:#e02424 var(--tw-gradient-from-position);--tw-gradient-to:#e0242400 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-red-800:hover{--tw-gradient-to:#9b1c1c var(--tw-gradient-to-position)}.hover\:text-black:hover{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.hover\:text-blue-200:hover{--tw-text-opacity:1;color:rgb(195 221 253/var(--tw-text-opacity))}.hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(63 131 248/var(--tw-text-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(28 100 242/var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 66 159/var(--tw-text-opacity))}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(249 128 128/var(--tw-text-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(224 36 36/var(--tw-text-opacity))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgb(200 30 30/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:text-opacity-70:hover{--tw-text-opacity:0.7}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-md:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.focus\:z-10:focus{z-index:10}.focus\:border-2:focus{border-width:2px}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(63 131 248/var(--tw-border-opacity))}.focus\:border-blue-600:focus{--tw-border-opacity:1;border-color:rgb(28 100 242/var(--tw-border-opacity))}.focus\:border-gray-200:focus{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.focus\:border-indigo-300:focus{--tw-border-opacity:1;border-color:rgb(180 198 252/var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(104 117 245/var(--tw-border-opacity))}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-t-transparent:focus{border-top-color:#0000}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:outline-0:focus{outline-width:0}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-0:focus,.focus\:ring:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-0:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(63 131 248/var(--tw-ring-opacity))}.focus\:ring-blue-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(26 86 219/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-indigo-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(205 219 254/var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(104 117 245/var(--tw-ring-opacity))}.focus\:ring-red-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(248 180 180/var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity:0.5}.focus\:ring-opacity-75:focus{--tw-ring-opacity:0.75}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:border-0:disabled{border-width:0}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:shadow-none:disabled{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.peer:-moz-placeholder-shown~.peer-placeholder-shown\:text-sm{font-size:.875rem;line-height:1.25rem}.peer:placeholder-shown~.peer-placeholder-shown\:text-sm{font-size:.875rem;line-height:1.25rem}.peer:-moz-placeholder-shown~.peer-placeholder-shown\:leading-\[3\.75\]{line-height:3.75}.peer:placeholder-shown~.peer-placeholder-shown\:leading-\[3\.75\]{line-height:3.75}.peer:-moz-placeholder-shown~.peer-placeholder-shown\:text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.peer:placeholder-shown~.peer-placeholder-shown\:text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.peer:-moz-placeholder-shown~.peer-placeholder-shown\:before\:border-transparent:before{border-color:#0000;content:var(--tw-content)}.peer:placeholder-shown~.peer-placeholder-shown\:before\:border-transparent:before{border-color:#0000;content:var(--tw-content)}.peer:-moz-placeholder-shown~.peer-placeholder-shown\:after\:border-transparent:after{border-color:#0000;content:var(--tw-content)}.peer:placeholder-shown~.peer-placeholder-shown\:after\:border-transparent:after{border-color:#0000;content:var(--tw-content)}.peer:focus~.peer-focus\:text-\[11px\]{font-size:11px}.peer:focus~.peer-focus\:leading-tight{line-height:1.25}.peer:focus~.peer-focus\:text-blue-500{--tw-text-opacity:1;color:rgb(63 131 248/var(--tw-text-opacity))}.peer:focus~.peer-focus\:before\:border-l-2:before{border-left-width:2px;content:var(--tw-content)}.peer:focus~.peer-focus\:before\:border-t-2:before{border-top-width:2px;content:var(--tw-content)}.peer:focus~.peer-focus\:before\:border-blue-500:before{--tw-border-opacity:1;border-color:rgb(63 131 248/var(--tw-border-opacity));content:var(--tw-content)}.peer:focus~.peer-focus\:after\:border-r-2:after{border-right-width:2px;content:var(--tw-content)}.peer:focus~.peer-focus\:after\:border-t-2:after{border-top-width:2px;content:var(--tw-content)}.peer:focus~.peer-focus\:after\:border-blue-500:after{--tw-border-opacity:1;border-color:rgb(63 131 248/var(--tw-border-opacity));content:var(--tw-content)}.peer:disabled~.peer-disabled\:text-transparent{color:#0000}.peer:disabled~.peer-disabled\:after\:border-transparent:after,.peer:disabled~.peer-disabled\:before\:border-transparent:before{border-color:#0000;content:var(--tw-content)}:is([dir=rtl] .rtl\:rotate-180){--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:space-x-reverse)>:not([hidden])~:not([hidden]){--tw-space-x-reverse:1}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(75 85 99/var(--tw-divide-opacity))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(63 131 248/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(28 100 242/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-900\/80){background-color:#111827cc}:is(.dark .dark\:bg-green-100){--tw-bg-opacity:1;background-color:rgb(222 247 236/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-100){--tw-bg-opacity:1;background-color:rgb(253 232 232/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-600){--tw-bg-opacity:1;background-color:rgb(224 36 36/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-100){--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(63 131 248/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-green-800){--tw-text-opacity:1;color:rgb(3 84 63/var(--tw-text-opacity))}:is(.dark .dark\:text-red-800){--tw-text-opacity:1;color:rgb(155 28 28/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-800){--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(26 86 219/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-700:hover){--tw-bg-opacity:1;background-color:rgb(200 30 30/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(63 131 248/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-gray-700:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(55 65 81/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-red-900:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(119 29 29/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mt-0{margin-top:0}.sm\:inline{display:inline}.sm\:h-36{height:9rem}.sm\:w-full{width:100%}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.sm\:rounded-lg{border-radius:.5rem}.sm\:px-10{padding-left:2.5rem;padding-right:2.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-5{padding-bottom:1.25rem;padding-top:1.25rem}.sm\:py-8{padding-bottom:2rem;padding-top:2rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:col-span-2{grid-column:span 2/span 2}.md\:mb-0{margin-bottom:0}.md\:mr-4{margin-right:1rem}.md\:mr-6{margin-right:1.5rem}.md\:mt-0{margin-top:0}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:w-1\/2{width:50%}.md\:w-10\/12{width:83.333333%}.md\:w-2\/3{width:66.666667%}.md\:w-4\/12{width:33.333333%}.md\:w-56{width:14rem}.md\:w-6\/12{width:50%}.md\:w-8\/12{width:66.666667%}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-\[1fr_200px\]{grid-template-columns:1fr 200px}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}.md\:gap-4{gap:1rem}.md\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.md\:p-8{padding:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:py-20{padding-bottom:5rem;padding-top:5rem}.md\:py-64{padding-bottom:16rem;padding-top:16rem}.md\:pb-20{padding-bottom:5rem}.md\:pb-32{padding-bottom:8rem}.md\:pb-48{padding-bottom:12rem}.md\:pl-10{padding-left:2.5rem}.md\:pl-20{padding-left:5rem}.md\:pl-48{padding-left:12rem}.md\:pl-8{padding-left:2rem}.md\:pt-20{padding-top:5rem}.md\:pt-72{padding-top:18rem}.md\:text-5xl{font-size:3rem;line-height:1}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1024px){.lg\:flex{display:flex}.lg\:w-1\/2{width:50%}.lg\:w-10\/12{width:83.333333%}.lg\:w-2\/3{width:66.666667%}.lg\:w-6\/12{width:50%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:px-12{padding-left:3rem;padding-right:3rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:pl-96{padding-left:24rem}}@media (min-width:1280px){.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:w-8\/12{width:66.666667%}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-\[minmax\(620px\2c _1fr\)_minmax\(min-content\2c _400px\)\]{grid-template-columns:minmax(620px,1fr) minmax(min-content,400px)}.xl\:text-6xl{font-size:3.75rem;line-height:1}}
module ApplicationCable
class Connection < ActionCable::Connection::Base
# identified_by :current_business
# def connect
# self.current_user = find_current_business
# end
private
# def find_current_business
# if verified_user = User.find_by(id: cookies.encrypted[:user_id])
# verified_user
# else
# reject_unauthorized_connection
# end
# end
end
end
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
class MessagesChannel < ApplicationCable::Channel
#To test from Rails Console: Business.first.messages.where(to: "8017973803").first.action_cable_update_messages_view_and_attach_image({})
def subscribed
@current_business = Business.find(params[:business_id])
stream_for @current_business
end
def receive(data)
end
end
// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
// like app/views/layouts/application.html.erb. All it does is render
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
const Hello = props => (
Hello.defaultProps = {
name: 'David'
}
Hello.propTypes = {
name: PropTypes.string
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
,
document.body.appendChild(document.createElement('div')),
)
})
/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.
//
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
// Support component names relative to this directory:
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)
// Support component names relative to this directory:
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [],
theme: {
extend: {},
},
plugins: [],
}
e
import React, { Component, useState, useEffect } from "react";
import ProgressBar from "../shared/ProgressBar";
import FieldRocketLogo from "../shared/FieldRocketLogo";
import FRPillButton from "../shared/Buttons/FRPillButton";
import WhatsYourNameStep from "./steps/WhatsYourNameStep";
import AskUserHighestPrioritySurveyOnRegistration from "./steps/AskUserHighestPrioritySurveyOnRegistration";
import PhoneNumberStep from "./steps/PhoneNumberStep";
import ConfirmAccessCodeStep from "./steps/ConfirmAccessCodeStep";
import DoneStep from "./steps/DoneStep";
import ArrowLeftIcon from 'images/icons/arrow-left.png'; // Adjust the path as per your directory structure
import AppleIcon from 'images/social/apple-Icon-metro.svg'; // Adjust the path as per your directory structure
import AndroidIcon from 'images/social/android-Icon-material.svg'; // Adjust the path as per your directory structure
import HowDidYouHearAboutUs from "./steps/HowDidYouHearAboutUs";
const GetStarted=() => {
const [registrationStep, setRegistrationStep] = useState(1);
const [titleText, setTitleText] = useState("Let's get started!");
const [buttonText, setButtonText] = useState("Register New Account");
//NOTE: This must match the same userOptions in the AskUserHighestPriority.jsx to make sure the server gets the right info.
const userSurveyOptions = [
"Invoicing & Payments", "Estimates & Closing", "Scheduling Customers", "Growth & Marketing", "Other"
];
const [surveyAnswer, setSurveyAnswer] = useState(-1);
const [freeFormSurveyAnswer, setFreeFormSurveyAnswer] = useState("");
const [nextButtonEnabled, setNextButtonEnabled] = useState(true);
const[ howDidYouHearAboutUsAnswer, setHowDidYouHearaboutUsAnswer] = useState("");
const LETS_GET_STARTED_STEP = 1;
const WHATS_YOUR_NAME_STEP = 2;
const ASK_USER_HIGHEST_PRIORITY_SURVEY_QUESTION = 3;
const HOW_DID_YOU_HEAR_ABOUT_US_QUESTION = 4;
const PHONE_NUMBER_STEP = 5;
const CONFIRM_ACCESS_CODE_STEP = 6;
const DONE_STEP = 7;
const [firstName, setFirstName] = useState("");
const [businessName, setBusinessName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [accessCode, setAccessCode] = useState("");
const [showBackArrow, setShowBackArrow] = useState(false); // for the user to navigate back to the previous step
// We need to make the "Next" button disabled if they need to fill out fields,
// or if a field is incorrect (like email/phone number), etc.
const [currentFormIsValid, setCurrentFormIsValid] = useState(true);
const [employeeId, setEmployeeId] = useState(null);
const [businessId, setBusinessId] = useState(null);
// const [accessCode, setAccessCode] = useState(null);
/**
* Responsible for changing the title text and button text depending on what's being rendered
*/
useEffect(() => {
//Only let the user navigate backwards if they've gone past the first page.
setShowBackArrow(registrationStep > 1);
switch(registrationStep){
case LETS_GET_STARTED_STEP:
setTitleText("Let's get started!");
setButtonText("Register New Account");
setCurrentFormIsValid(true);
break;
case WHATS_YOUR_NAME_STEP:
setTitleText("What's your name?");
setButtonText("Next");
break;
case PHONE_NUMBER_STEP:
setTitleText("What's Your Phone Number?");
setCurrentFormIsValid(false);
break;
case CONFIRM_ACCESS_CODE_STEP:
registerBusinessAndSendAccessCode();
//TODO: This is where we need to attempt to do a POST.. in between these two steps.
//TODO: This is where we will do a POST!
setTitleText("We Just Texted You a Code.");
break;
case ASK_USER_HIGHEST_PRIORITY_SURVEY_QUESTION:
setTitleText("What's Your #1 Priority for Using Field Rocket?");
setCurrentFormIsValid(false);
break;
case HOW_DID_YOU_HEAR_ABOUT_US_QUESTION:
setTitleText("How Did You Hear About Field Rocket?");
setCurrentFormIsValid(false);
break;
case DONE_STEP:
verifyAccessCodeAndFinishRegistration(); // This is where we validate the access code.. or at least do a POST to the server.
setTitleText("Done, welcome!");
break;
}
}, [registrationStep])
const getCurrentStepComponent = () => {
switch(registrationStep){
case LETS_GET_STARTED_STEP:
return (
case DONE_STEP:
return (
);
default:
return
const onBackArrowPressed = () => {
if (registrationStep > 1){ // don't let the registration step get below 1
setRegistrationStep(registrationStep - 1);
}
}
const setInputIsValidated=(isValid, invalidMessage = null)=>{
setIsInputValid(isValid);
setInvalidInputMessage(invalidMessage);
}
/**
* This request does a POST request to the server to create
* the business entity.
*
* If there is an error, then we need to display the error
* very clearly to the user so they can resolve it, and take them back
* to the appropriate step.
*
*/
const registerBusinessAndSendAccessCode = () => {
//POST /businesses - to the right route to create a business and trigger an access code to be sent via Twilio.
let readableSurveyAnswer = userSurveyOptions[surveyAnswer]
const body = {
business: {
phone: phone.replace(/[- )(]/g,''),
name: businessName,
survey_answers: readableSurveyAnswer + " - " + freeFormSurveyAnswer,
attribution_source: howDidYouHearAboutUsAnswer + " - Web App",
email,
employees_attributes: [{ phone: phone.replace(/[- )(]/g,''), first_name: firstName, email }],
},
};
$.ajax({
url: "/businesses.json",
type: "POST", // Equivalent to method in fetch API
contentType: "application/json", // Sets the Content-Type header
data: JSON.stringify(body), // The data to send in the request
success: function(response) {
console.log(response);
setEmployeeId(response.first_employee_created_id); // We set these here, and then we use this when verifying the access code.
setBusinessId(response.id);
},
error: function(jqXHR, textStatus, errorThrown) {
// This function is called if the request fails.
// 'jqXHR' is the jQuery XMLHttpRequest object.
// 'textStatus' is a string describing the type of error.
// 'errorThrown' is an optional exception object, if one occurred.
console.log("Error while registering business: " + errorThrown);
return errorThrown;
}
});
}
/**
* This request does a POST request to verify the access code and finish activating our user's account
*
* TODO: If there is an error, then we need to display the error
* very clearly to the user so they can resolve it, and take them back
* to the appropriate step.
*
*/
const verifyAccessCodeAndFinishRegistration = () => {
const body = {
access_code: "990099",
business_id: businessId
};
$.ajax({
url: "/employees/" + employeeId + "/verify_access_code.json",
type: "POST",
contentType: "application/json",
data: JSON.stringify(body),
success: function(response) {
console.log("Verification Successful", response);
//TODO: Fix. Push them through without verification
// Handle success scenario (e.g., update UI, redirect, etc.)
},
error: function(xhr, status, error) {
console.error("Verification Failed", xhr, status, error);
// Handle error scenario
}
});
};
return (
export default GetStarted;
import React, {useEffect} from "react";
import InputField from "../../shared/Forms/InputField";
//NOTE: This must match the same surveyUserOptions in the GetStarted.jsx to make sure the server gets the right info.
const userOptions = [
"Invoicing & Payments", "Estimates & Closing", "Scheduling Customers", "Growth & Marketing", "Other"
];
// const surveyUserOptions = ["Invoicing & Payments", "Estimates & Closing", "Scheduling Customers", "Growth & Marketing", "Other"];
const AskUserHighestPrioritySurveyOnRegistration = ({
surveyAnswer,
setSurveyAnswer,
freeFormSurveyAnswer,
setFreeFormSurveyAnswer,
setEnableNextButton
}) => {
useEffect(() => {
let nextButtonShouldBeEnabled = surveyAnswer != undefined && surveyAnswer != null && surveyAnswer >= 0;
if (surveyAnswer == 4) {//3 is hardcoded to be the "other" selection.
nextButtonShouldBeEnabled = !isBlank(freeFormSurveyAnswer) // is not blank
console.log(!isBlank(freeFormSurveyAnswer));
}
console.log("Survey answer is: " + userOptions[surveyAnswer])
setEnableNextButton(nextButtonShouldBeEnabled);
}, [surveyAnswer, freeFormSurveyAnswer])
return (
{userOptions.map((item, index) => (
{surveyAnswer === 4 && (
!isBlank(answer)}
/>
// {
// setFreeFormSurveyAnswer(e.target.value)
// }}
// placeholder="What would you like to accomplish?"
// className="textInput"
// />
)}
export default AskUserHighestPrioritySurveyOnRegistration;
import React, {useEffect} from "react";
import InputField from "../../shared/Forms/InputField";
const HowDidYouHearAboutUs = ({
howDidYouHearAboutUsAnswer,
setHowDidYouHearaboutUsAnswer,
setEnableNextButton
}) => {
useEffect(() => {
let nextButtonShouldBeEnabled = howDidYouHearAboutUsAnswer != undefined && howDidYouHearAboutUsAnswer != null && !isBlank(howDidYouHearAboutUsAnswer);
setEnableNextButton(nextButtonShouldBeEnabled);
}, [howDidYouHearAboutUsAnswer])
return (
export default HowDidYouHearAboutUs;
import React, {useEffect, useState} from "react";
import InputField from "../../shared/Forms/InputField";
const PhoneNumberStep = ({ phone, setPhone, setEnableNextButton }) => {
const [isValid, setIsValid] = useState(true);
const [invalidMessage, setInvalidMessage] = useState("");
/**
* This is a super annoying hack, because phone-mask.js for some reason isn't running on this page after the
* className is set properly in InputField. So I'm just re-running the code needed to add the formatting for the
* phone number here.
*/
useEffect(() => {
$('.phone_mask').mask('(000) 000-0000');
$('.phone_mask').bind('paste', (event) => { //I added this in so that you could copy and paste from iPhone phone numbers into the app.. the +1 was throwing off our phone_masking.
var pastedData = event.originalEvent.clipboardData.getData('text')
if (pastedData[0] == "+" && pastedData[1] == "1"){
let filteredPhoneNumber = pastedData.substring(2); //trim out the +1 from phone number if it exists..
console.log(`Modified Pasted phone number from: ${pastedData} to: ${filteredPhoneNumber}!`);
// event.originalEvent.clipboardData.setData('text/plain', filteredPhoneNumber);
event.target.value = filteredPhoneNumber
}
});
})
useEffect(() => {
let formattedPhoneNumber = formatPhoneNumber(phone);
let isValid = isValidPhoneNumber(formattedPhoneNumber);
setEnableNextButton(isValid);
}, [phone])
const onValidatePhoneNumber = (phoneNumber) => {
let formattedPhoneNumber = formatPhoneNumber(phoneNumber);
let isValid = isValidPhoneNumber(formattedPhoneNumber);
if (isValid) {
setInvalidMessage("");
setIsValid(true);
return true;
}
else {
setInvalidMessage("Please enter a full phone number")
setIsValid(false);
return false;
}
}
return (
export default PhoneNumberStep;
import React from "react";
import AppleIcon from 'images/social/apple-Icon-metro.svg'; // Adjust the path as per your directory structure
import AndroidIcon from 'images/social/android-Icon-material.svg'; // Adjust the path as per your directory structure
const DoneStep = ({email}) => {
return (
After setting your password, make sure to click "Already have an account?"
export default DoneStep;
import React from "react";
const ConfirmAccessCodeStep = ({ accessCode, setAccessCode }) => {
return (
export default ConfirmAccessCodeStep;
import React, {useState, useEffect} from "react";
import InputField from "../../shared/Forms/InputField";
import EmailInputField from "../../shared/Forms/EmailInputField";
const WhatsYourNameStep = ({ firstName, setFirstName, businessName, setBusinessName, email, setEmail, setEnableNextButton }) => {
useEffect(() => { //all of these need to be true in order to be valid.
setEnableNextButton(!isBlank(firstName) && !isBlank(businessName) && !isBlank(email) && isValidEmail(email));
// setEnableNextButton(true);
}, [firstName, businessName, email])
return (
export default WhatsYourNameStep;
import React, {Component} from "react"
import ConversationPreview from "./ConversationPreview"
class AllConversations extends Component{
constructor(props){
super(props);
}
render(){
const {messageThreads, businessPhoneNumber, setMessageThreadCallback, pageNumber} = this.props;
return(
export default AllConversations;
import React, {Component} from "react";
class ConversationPreview extends Component{
constructor(props){
super(props);
this.getUnreadIcon = this.getUnreadIcon.bind(this);
}
getUnreadIcon = (message)=>{
console.log(`rendering message preview for2 ${message.has_been_read}`)
if (!message.has_been_read){
return (
)
}
else {
return ()
}
}
render(){
const {messageThread,
setMessageThreadCallback,
recipient,
recipientName,
urls,
businessPhoneNumber} = this.props;
return(
const { id } = recipientName;
const noImage = $('
if(id){
$.get({
url: `/messages/${id}/customer_uploaded_images.json`,
success: function(data){
if(data.length > 0){
const imageContainer = $('
data.forEach((image)=>{
imageContainer.append(
$('', {
'src': `${image['url']}`,
'style': 'width: 150px !important; height: 150px;',
'class': 'm-1 btn p-0 modal-img',
'data-id': `${image['id']}`,
'onclick': 'imageClickHandler(event)'
})
)
})
$('#photos-to-attach-to-message').append(imageContainer);
}
},
error: function(data){
console.log("error");
console.log(data);
}
});
}
}
}>
{moment(messageThread[messageThread.length-1].sent).calendar()}
export default ConversationPreview;
import React, {Component} from "react";
class MessageBubbleSent extends Component{
constructor(props){
super(props);
}
render(){
const {content, time, imageSrc} = this.props;
if (!imageSrc && !content) {
return null
}
const image = ( imageSrc == null || imageSrc.trim() === '' ?
: return (
import React, { Component } from "react";
class NewConversationButton extends Component{
constructor(props) {
super(props);
}
render(){
const {selectNewConversationThreadCallback} = this.props;
return(
selectNewConversationThreadCallback()}>
)
}
}
export default NewConversationButton;
import React, { Component } from "react";
import MessageBubbleSent from "./MessageBubbleSent";
import MessageBubbleReceived from "./MessageBubbleReceived";
import ToggleConversations from "./ToggleConversations";
import SendMessageToolbar from "./SendMessageToolbar"
import NewConversationButton from "./NewConversationButton";
class Conversation extends Component{
constructor(props) {
super(props);
this.state = {
recipientName: props.recipientName,
recipient: props.recipient,
context: props.context,
}
// this.ajaxGetRecipientNameFromNumber = this.ajaxGetRecipientNameFromNumber.bind(this);
}
componentDidMount() {
$('#chat-window').scrollTop($('#chat-window')[0].scrollHeight)
}
componentDidUpdate(){
$('.phone_mask').mask('(000) 000-0000')
}
render(){
const { context,
messagesImageUrls,
businessPhoneNumber,
recipient,
recipientName,
addNewMessageCallback,
openNewConversationCallback } = this.props;
//This is to determine if it's a new conversation thread..
let recipientElement = null;
if (recipient == "New Conversation"){
//This is going to render a field where they type in a phone number to send to.
//Then on send, it will turn into a normal recipient.
recipientElement = ()
}
else {
recipientElement =
let recipientNameElement = null;
if (recipientName && recipientName["full_name"] != null && recipientName["id"] != null){
if (recipientName["customer_id"] != null){
recipientNameElement = ({recipientName["full_name"]});
}
else {
recipientNameElement = ({recipientName["full_name"]});
}
}
else {
recipientNameElement = "";
}
return(
export default Conversation;
import React, { Component } from "react";
import Conversation from "./Conversation";
import AllConversations from "./AllConversations"
class Index extends Component {
constructor(props) {
super(props);
this.state = {//messageThreads[0] => ['8013499925', { messages: [], urls: [urls] }, 'Bob Doe']
currentConversation: [],
currentConversationImageUrls: [],
currentRecipient: this.findRecipientFromParam(),
currentRecipientName: this.findRecipientNameFromParam(),
conversationWasSetInParamater: this.wasConversationSetInParameter(),
businessPhoneNumber: props.businessPhoneNumber,
messageThreads: props.messageThreads,
employeeName: props.employeeName
}
this.wasConversationSetInParameter = this.wasConversationSetInParameter.bind(this);
this.setMessageThread = this.setMessageThread.bind(this);
this.findRecipientNameFromParam = this.findRecipientNameFromParam.bind(this);
this.addNewMessage = this.addNewMessage.bind(this);
this.setupMessageSubscription = this.setupMessageSubscription.bind(this);
this.receivedMessageFromActionCableSubscription = this.receivedMessageFromActionCableSubscription.bind(this);
this.scrollToBottomOfCurrentConversation = this.scrollToBottomOfCurrentConversation.bind(this);
this.openNewConversation = this.openNewConversation.bind(this);
this.createLinkElement = this.createLinkElement.bind(this);
this.setCurrentThreadViewAndImageUrls = this.setCurrentThreadViewAndImageUrls.bind(this);
}
componentDidMount() {
this.setCurrentThreadViewAndImageUrls()
this.setupMessageSubscription();
this.setMessageContentFromParam();
}
// Fetching current conversation image_urls and messages
setCurrentThreadViewAndImageUrls() {
let currentThreadMessages = {};
if (this.props.threadToView == null){
currentThreadMessages = this.props.messageThreads[0][1];
}
else {
for (let i = 0; i < this.props.messageThreads.length; i++){
let currentThread = this.props.messageThreads[i];
let number = currentThread[0];
if (number == this.props.threadToView){
currentThreadMessages = this.props.messageThreads[i][1];
}
}
}
const { urls, messages } = currentThreadMessages;
this.setState({
currentConversation: messages || [],
currentConversationImageUrls: urls || []
})
}
wasConversationSetInParameter(){
return this.props.threadToView == null
}
//checks if the conversation url param was set, and sets the recipient from that param, otherwise defaults to first in the list.
findRecipientFromParam(){
if (this.props.threadToView == null){
return this.props.messageThreads[0][0]
}
else{
return this.props.threadToView
}
}
//checks if the conversation url param was set, and sets the recipient name from that param, otherwise defaults to first in the list.
findRecipientNameFromParam(){
if (this.props.threadToView == null){
return this.props.messageThreads[0][2]
}
else{
$.get({
url: '/resolve_phone',
data: {phone: this.props.threadToView},
success: function(data){
//sets the recipientName when url is set with a conversation number
let wasCustomer = data["customer"] != null;
let wasContact = data["contact"] != null;
let couldntResolve = data == [];
if (!couldntResolve){
if (wasCustomer){
var recipientNameElement = this.createLinkElement('View Customer', data["customer"]["full_name"], `/customers/${data["customer"]["id"]}`);
$('#convo-recipient-name').append(recipientNameElement);
}
else if (wasContact){
var recipientNameElement = this.createLinkElement('View Customer', data["contact"]["full_name"], `/customers/${data["contact"]["customer_id"]}`);
$('#convo-recipient-name').append(recipientNameElement);
}
}
}.bind(this),
error: function(data){
console.log("Error getting the recipent name from the phone number");
}
})
return ""
}
}
createLinkElement(title, text, href){
var a = document.createElement('a');
var linkText = document.createTextNode(text);
a.appendChild(linkText);
a.title = title;
a.href = href;
return a;
}
//if the message url param was set, it fills the message box. Useful for creating links with prefilled text messages.
setMessageContentFromParam(){
if (this.props.content != null){
$('#message_to_send').val(this.props.content);
}
}
//Received a new message from the application, so this callback adds the message button and notification.
receivedMessageFromActionCableSubscription(data){
console.log(`Received incoming message from: ${data["sent_from"]}`); //Just received a message.. We are assuming it's a received message because message.rb doesn't broadcast on actioncable when we created & sent a message out. Therefore, we know that data["to"] is who sent the message to us.
console.log(`content: ${data["content"]}`); //Just received a message.. We are assuming it's a received message because message.rb doesn't broadcast on actioncable when we created & sent a message out. Therefore, we know that data["to"] is who sent the message to us.
console.log(`to: ${data["to"]}`); //Just received a message.. We are assuming it's a received message because message.rb doesn't broadcast on actioncable when we created & sent a message out. Therefore, we know that data["to"] is who sent the message to us.
console.log(`sent on: ${data["sent"]}`); //Just received a message.. We are assuming it's a received message because message.rb doesn't broadcast on actioncable when we created & sent a message out. Therefore, we know that data["to"] is who sent the message to us.
const receivedMessage = {sent_from: data["sent_from"], to: data["to"], content: data["content"], sent: data["sent"]};
const imageUrl = data["image_url"];
if (this.state.currentRecipient === data["sent_from"]){
this.setState({
currentConversation: [ ...this.state.currentConversation, receivedMessage ],
currentConversationImageUrls: [ ...this.state.currentConversationImageUrls, imageUrl ]
});
this.scrollToBottomOfCurrentConversation();//Scroll to bottom of current convo page
}
else{
for(let i = 0; i < this.state.messageThreads.length; i++){
if (this.state.messageThreads[i][0] === data["sent_from"]){
const messageThreads = this.state.messageThreads
messageThreads[i][1]['messages'] = [...messageThreads[i][1]['messages'], receivedMessage]
messageThreads[i][1]['urls'] = [...messageThreads[i][1]['urls'], imageUrl]
this.setState({
messageThreads: messageThreads,
});
return;
}
}
//If we made it this far, we need to create a whole new thread.
const thread = [ data["to"], { messages: [receivedMessage], urls: [imageUrl] }];
this.setState({
messageThreads: [...messageThreads, thread]
})
}
}
setupMessageSubscription(){
console.log("Setting up message subscription!");
App.messages = App.cable.subscriptions.create('MessagesChannel', {
received: (data) => this.receivedMessageFromActionCableSubscription(data)
});
}
//Given a conversation & recipient, we set the message thread
setMessageThread(conversation, recipient, recipientName, urls){
this.setState({
currentConversation: conversation,
currentRecipient: recipient,
currentRecipientName: recipientName,
currentConversationImageUrls: urls || []
});
$.post({
url: '/mark_as_read.json',
data: {number: recipient},
error: function(data){
gritterError("Error", "Couldn't mark these sms messages as being seen")
}
});
$('#chat-window').scrollTop($('#chat-window')[0].scrollHeight);//Scroll to bottom of current convo pagej
}
//Callback for when you want to send a message by typing in a phone number
openNewConversation(){
console.log("Open new conversation button clicked");
this.setState({
currentRecipient: "New Conversation",
currentRecipientName: "",
currentConversation: []
});
}
//when sending a new message, this is the callback for adding the new message bubble in the conversation
addNewMessage(messagePayload, resetImageDataCallback){
const { image: { imageUrl, file, id }, message } = messagePayload;
if (isBlank(message) && isBlank(imageUrl)){ //Don't want to send a message if it's blank
return;
}
let sendTo = this.state.currentRecipient != "New Conversation" ? this.state.currentRecipient : $('#new-phone-number').val();
const messageFormData = new FormData();
messageFormData.append('file', file)
messageFormData.append('id', id)
messageFormData.append('name', this.state.employeeName)
messageFormData.append('content', message)
messageFormData.append('to', sendTo)
messageFormData.append('sent_from', this.state.businessPhoneNumber)
$.ajax({
url: '/messages.json',
type: "POST",
dataType: 'json',
data: messageFormData,
processData: false,
contentType: false,
success:function(data){
const new_message = {to: this.state.currentRecipient, sent_from: this.state.businessPhoneNumber, content: message}
this.setState({
currentConversation: [ ...this.state.currentConversation, new_message ],
currentConversationImageUrls: [ ...this.state.currentConversationImageUrls, imageUrl ],
currentRecipient: sendTo,
currentRecipientName: this.state.currentRecipientName,
})
resetImageDataCallback();
$('#chat-window').scrollTop($('#chat-window')[0].scrollHeight);//Scroll to bottom of current convo page
}.bind(this), //binding to 'this' so we can access 'this' from within the success callback.
error: function(data){
showError("Couldn't Send Message", data); //shows gritter errors
}.bind(this)
});
}
scrollToBottomOfCurrentConversation(){ //TODO: Replace all times we scroll to bottom with this function call for readability
$('#chat-window').scrollTop($('#chat-window')[0].scrollHeight);
}
render() {
const { messageThreads, businessPhoneNumber } = this.props;
//We need to show the conversation previews if they just went to messages. If they navigated to conversations with a conversation in mind, we'll hide the previews and only show the current conversation.
let shouldHideConversation = this.state.conversationWasSetInParamater //if currentRecipient is null it wasn't set.
return (
export default Index;
import React, { Component } from "react";
class SendMessageToolbar extends Component{
constructor(props) {
super(props);
this.changeHandler = this.changeHandler.bind(this);
this.resetImage = this.resetImage.bind(this);
this.handleSend = this.handleSend.bind(this);
this.resetData = this.resetData.bind(this);
}
changeHandler(event) {
let src = event.target.value;
if(src) {
$('#image_avatar_url_id').val('');
const url = window.URL.createObjectURL(event.target.files[0])
$('#attached-image').attr('src',url);
$('#attached-image-container').show();
$('#image_upload').hide();
}
}
resetImage() {
$('#image_avatar').val('')
$('#image_avatar_url_id').val('');
$("#attached-image-container").hide()
$("#attached-image").attr("src", '');
this.setState({ imageSrc: '', file: {}})
}
resetData() {
this.resetImage();
$('#message_to_send').val('');
}
handleSend() {
const imageSrc = $('#attached-image').attr('src')
const file = $('#image_avatar')[0].files[0] || {}
const uploadedImageId = $('#image_avatar_url_id').val();
const messagePayload = {
image: {
file: file,
imageUrl: imageSrc,
id: uploadedImageId
},
message: $('#message_to_send').val()
}
const { addNewMessageCallback } = this.props;
addNewMessageCallback(messagePayload, this.resetData);
}
render(){
return(
export default SendMessageToolbar;
import React, {Component} from "react";
class MessageBubbleSent extends Component{
constructor(props){
super(props);
}
render(){
const {content, time, from, imageSrc, status} = this.props;
//TODO: On click we want to display the image from Baguette Box
const image = ( imageSrc == null || imageSrc.trim() === '' ?
return (
// status == "delivered" ?
//
import React, {Component} from "react";
class ToggleConversations extends Component {
constructor(props){
super(props);
}
render(){
return (
{
$('#conversation-previews').show();
$('#current-conversation').hide();
}}>
)
}
}
export default ToggleConversations;
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.employee_name }
{ data.time_start }
{ data.time_end }
))
}
);
}
}
export default TableBody;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
export default Table;
import React from 'react';
const PublishedJobTableRow = ({ published_job_page }) => {
return (
{/*moment(published_job_page.created_at).format('MM-DD-YYYY')*/}
{formatReadableDate(published_job_page.created_at)}
{published_job_page.address}
{published_job_page.slug}
{published_job_page.target_keywords}
{published_job_page.views}
export default PublishedJobTableRow;
import React, {useState} from "react";
import ExpandableTableRow from "./ExpandableTableRow";
import ExpandedBodyRow from "./ExpandedBodyRow";
const ExpandableTableBody = ({publishableJobs}) => { //TODO: how do we handle this..
//We want our data to be.
// City -> Customer Name -> Title -> Invoice/Estimate -> Amount -> Pictures -> Status -> Pictures -> Category/Type of Work? -> Keywords? -> Number of Views ->
// Images need to be queried.? But not number of images.
// I think this needs to come from the invoices controller?
// JSON object structure?
// invoice: { published_job.? }
// }
// }
const [expandedIndex, setExpandedIndex] = useState(null);
const toggleAccordion = (index) => {
console.log("Toggle accordion: ", index);
if (expandedIndex === index) {
setExpandedIndex(null);
} else {
setExpandedIndex(index);
}
};
return(
{publishableJobs.map((publishableJob, index) => (
toggleAccordion(index)}/>
))}
)
}
export default ExpandableTableBody;
import React, {useState, useEffect} from "react";
const ExpandedBodyRow = ({publishableJob, index, isExpanded }) => {
const [images, setImages] = useState([])
useEffect(() => {
// GET /customers/1/images.json
const fetchImages = async () => {
if (isExpanded) {//lazy load only when expanded
try {
const response = await fetch(`/customers/${publishableJob.customer_id}/images.json`);
if (response.ok) {
const data = await response.json();
// debugger;
setImages(data["images"]);
} else {
console.error('Failed to fetch images:', response.status);
}
} catch (error) {
console.error('Error fetching images:', error);
}
}
};
fetchImages();
}, [isExpanded, publishableJob.customer_id]);
return (
{lineItem.description} - ${lineItem.price}
export default ExpandedBodyRow;
import React from "react";
const ExpandableTableHead = () => {
return(
export default ExpandableTableHead;
import React, {useEffect} from "react";
const ExpandableTableRow = ({publishableJob, index, toggleAccordion }) => {
useEffect(() => {
console.log("publishable Job: ", publishableJob)
}, [])
return(
export default ExpandableTableRow;
import React, {useState, useEffect} from 'react';
import PublishedJobTableRow from "./PublishedJobTableRow";
const PerformanceForPublishedJobs = ({published_job_pages}) => {
const [publishedJobs, setPublishedJobs] = useState([]);
useEffect(() => {
//TODO: Query all the published_job_pages for our current business?
}, []);
return(
{published_job_pages.length} published
Date
City
Post
Published
Keywords
Views
{published_job_pages
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.map((published_job_page, index) => (
))}
{/*
Add the New Pricing Page
$500
25%
import React, {useState, useEffect} from 'react';
import ExpandableTableHead from './PublishableJobTable/ExpandableTableHead';
import ExpandableTableBody from "./PublishableJobTable/ExpandableTableBody";
const PublishableJobsTable = () => {
const [publishableJobs, setPublishableJobs] = useState([])
useEffect(() => {
// Define a function to fetch the data
const fetchData = async () => {
try {
// Fetch data from the server
const response = await fetch('/invoices/publishable_jobs.json');
// Parse the JSON from the response
const data = await response.json();
// Update the state with the fetched data
setPublishableJobs(data);
} catch (error) {
// If there's an error, log it to the console
console.error('Failed to fetch publishable jobs:', error);
}
};
// Call the fetch function
fetchData();
}, []); // The empty array ensures this effect only runs once, after the initial render
return (
export default PublishableJobsTable;
import React from 'react';
const StatsForMarketingDashboard = () => {
return (
Today's Money
+xx% than last week
Today's Users
+x% than last month
New Clients
-x% than yesterday
Sales
+x% than yesterday
export default StatsForMarketingDashboard;
import React, { useState, useEffect } from 'react';
const NewPublishableJobPage = ({
invoice,
notes: initialNotes,
keywords: initialKeywords,
customerName: initialCustomerName,
address: initialAddress,
customTitlePrompt: initialCustomTitlePrompt,
customHtmlPrompt: initialCustomHtmlPrompt
}) => {
const [includePrice, setIncludePrice] = useState(false);
const [notes, setNotes] = useState(initialNotes);
const [notesWithoutPrice, setNotesWithoutPrice] = useState(notesWithoutPrice);
const [keywords, setKeywords] = useState(initialKeywords);
const [customerName, setCustomerName] = useState(initialCustomerName);
const [address, setAddress] = useState(initialAddress);
const [selectedPictures, setSelectedPictures] = useState([]);
const [customPrompt, setCustomPrompt] = useState(false);
const [customTitlePrompt, setCustomTitlePrompt] = useState(initialCustomTitlePrompt);
const [customHtmlPrompt, setCustomHtmlPrompt] = useState(initialCustomHtmlPrompt);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (includePrice) {
setNotes(notes);
} else {
setNotes(notesWithoutPrice);
}
}, [includePrice]);
// useEffect(() => {
// console.log(selectedPictures);
// }, [selectedPictures]);
const handleSubmit = async (event) => {
event.preventDefault();
console.log("made it!");
// # "published_job_page" => {
// # "notes" => "Job Notes: Removal of tree stump and roots\r\nItem: Stump grind small stump and roots in front flower bed leaving stump 4-6\" below grade. Clean up and backfill. (Mulch removal not included)\r\nItem: Add stump in backyard ",
// # "keywords" => "stump removal, stump grinding, stump poisoning, trunk removal",
// # "customer_name" => "Alayna",
// # "address" => "Riverton, UT",
// # "job_id" => "11921",
// # "invoice_id" => "6318"
// # },
// # "attachments" => {
// # "IMG_0232.JPG" => "9270"
// # }, ^^ ABOVE IS THE CORRECT WAY TO NEST THE FORM DATA
const form_data = {
published_job_page: {
invoice_id: invoice.id,
job_id: invoice?.job?.id,
notes: notes,
keywords: keywords,
customer_name: customerName,
address: address,
use_custom_prompt_on_generation: customPrompt,
custom_title_prompt: customTitlePrompt,
custom_html_prompt: customHtmlPrompt,
},
attachments: getAttachedImages(),
};
// debugger;
console.log(form_data);
setIsLoading(true);
try {
const response = await fetch('/published_job_pages.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(form_data)
});
if (response.ok) {
const data = await response.json();
console.log('Success:', data);
let publishedJobPageId = data.id;
document.location.href = `/published_job_pages/${publishedJobPageId}`;
// setIsLoading(false);
// Handle success (e.g., redirect to the new job page)
} else {
const errorData = await response.json();
console.error('Error:', errorData);
setIsLoading(false);
// Handle error (e.g., display error messages)
}
} catch (error) {
console.error('Error:', error);
setIsLoading(false);
// Handle network error
}
}
const getAttachedImages = () => {
const checkboxes = document.querySelectorAll('input[type="checkbox"][name^="attachments"]');
// Initialize an object to store selected images
const selectedImages = {};
// Iterate through each checkbox and add it to the object if checked
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
// Assuming the value is in the format "filename:id"
// const [filename, id] = checkbox.value.split(':');
const filename = checkbox.name.split('[')[1].replace(']', '')
const id = checkbox.value
selectedImages[filename] = id;
// debugger;
}
});
return selectedImages;
}
return (
{/* Notes */}
{/* Keywords */}
{/* Customer Name */}
{/* Address */}
{/* Custom Prompt Checkbox */}
{/* Custom Prompts */}
{customPrompt && (
{/* Submit Button */}
export default NewPublishableJobPage;
import React, {useState, useEffect} from "react"
import TableDataNotEditable from "../shared/TableDataNotEditable"
import Modal from 'react-bootstrap/Modal'
import Button from 'react-bootstrap/Button';
export default function ContactsTable({contacts, setContacts}){
const [isOpen, setIsOpen] = useState(false);
const [isSaveButtonLoading, setIsSaveButtonLoading] = useState(false);
const [activeContactId, setActiveContactId] = useState(0);
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [phone, setPhone] = useState(null);
const [email, setEmail] = useState(null);
const [notes, setNotes] = useState(null);
const [modalTitle, setModalTitle] = useState("");
const editButtonClicked = (contactId, contactData) => {
console.log("Edit button clicked for contact! About to hit debugger");
console.log("Contact Id: " + contactId);
console.log("Contact data: ");
console.log(contactData);
setActiveContactId(contactId);
const selectedContact = contacts.find(contact => contact.id == contactId);
console.log("Selected contact: ", selectedContact);
setFirstName(selectedContact.first_name);
setLastName(selectedContact.last_name);
setPhone(selectedContact.phone);
setEmail(selectedContact.email);
setNotes(selectedContact.notes);
setModalTitle("Edit Contact");
setIsOpen(true);
}; //after a timesheet gets edited.. we need to modify the state somehow.
const saveContactChangesToServer = () => {//This is a closure... I don't think it will work like this (I don't think this closure gets updated)
setIsSaveButtonLoading(true);
$.ajax({
type: 'PUT',
url: `/customer_contacts/${activeContactId}.json`,
data: {customer_contact: {id: activeContactId, first_name: firstName, last_name: lastName, phone: phone, email: email, notes: notes}},
success: (data) => {
console.log("Success: " + data);
setIsOpen(false);
gritterSuccess("Success", "Successfully Updated Contact", 10000);
const contactsWithEdits = [...contacts];
for (let index = 0; index < contacts.length; index++){
const contact = contacts[index];
const modifiedThisContact = contact["id"] == data.id;
if (modifiedThisContact){
contactsWithEdits[index] = data;
}
}
setContacts(contactsWithEdits);
setIsSaveButtonLoading(false);
},
error: (data) => {
console.log("error: " + data);
gritterError("Warning", "Couldn't Update Contact", 10000);
setIsSaveButtonLoading(false);
},
})
}
return(
[
contact.id,
contact.full_name,
formatPhoneNumber(contact.phone),
contact.email,
contact.notes])}
actions={{delete: true, edit: true}}
editModalOnClick={editButtonClicked}
/>
setIsOpen(false)} centered>
{modalTitle}
{/* TODO: customer contact fields/form. */}
First Name:
setFirstName(e.target.value)} />
Last Name:
setLastName(e.target.value)} />
Phone:
setPhone(e.target.value)} />
Email:
setEmail(e.target.value)} />
Notes:
setNotes(e.target.value)} />
setIsOpen(false)}>
Close
{isSaveButtonLoading ?
Saving...
:
Save Changes
}
)
}
import React, { Component, useState } from "react"
import ContactsTable from "./ContactsTable";
export default function ContactsIndex(props){
const [contacts, setContacts] = useState(props.contacts);
return(
)
}
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
_tools(data) {
const { permissions } = this.props;
if( permissions["destroy"] && permissions["update"] ) {
return(
{
data.deactivated_at
?
:
}
);
} else if( permissions["destroy"] ) {
return(
);
} else if( permissions["update"] ) {
return(
);
} else {
return();
}
}
}
export default TableBody;
import React, { Component } from "react";
import Table from "./Table";
import TableTop from "./TableTop";
class Index extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions, displayGroup } = this.props;
return (
export default Index;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Table;
import React, { Component } from "react";
import SortLink from "../shared/SortLink";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
return (
Avatar
Email
Phone
Color
Group
);
}
}
export default TableHeader;
import React, { Component } from "react";
import SearchField from "../shared/SearchField";
class TableTop extends Component {
constructor(props) {
super(props);
}
render() {
return (
_groupLink() {
const { displayGroup } = this.props;
if( displayGroup ) {
return();
} else {
return null;
}
}
_inviteLink() {
const { permissions } = this.props;
if( permissions["create"] ) {
return(
);
} else {
return null;
}
}
_newLink(){
const { permissions } = this.props;
if( permissions["create"] ) {
return(
);
} else {
return null;
}
}
}
export default TableTop;
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.name }
{ this._tools(data) }
))
}
);
}
_tools(data) {
const { permissions } = this.props;
if( permissions["destroy"] && permissions["update"] ) {
return(
);
} else if( permissions["destroy"] ) {
return(
);
} else if( permissions["update"] ) {
return(
);
} else {
return(
);
}
}
}
export default TableBody;
import React, { Component } from "react";
import Table from "./Table";
import TableTop from "./TableTop";
class Index extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Index;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Table;
import React, { Component } from "react";
import SortLink from "../shared/SortLink";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
return (
);
}
}
export default TableHeader;
import React, { Component } from "react";
import SearchField from "../shared/SearchField";
class TableTop extends Component {
constructor(props) {
super(props);
}
render() {
return (
_newLink() {
const { permissions } = this.props;
if( permissions["create"] ) {
return();
} else {
return null;
}
}
}
export default TableTop;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const ChatBot = ({ first_name, context }) => {
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [userInput, setUserInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
console.log("ChatBot props:", { first_name, context });
useEffect(() => {
// Add initial greeting message when component mounts
// console.log("app context is:" + context);
setMessages([
{ text: `What do you want to build?`, sender: 'bot' }
]);
}, [first_name]);
useEffect(() => {
// Add initial greeting message when component mounts
console.log("app context is:", context);
setMessages([
{ text: `What do you want to build?`, sender: 'bot' }
]);
}, [context]);
const styles = {
chatBubbleContainer: {
position: 'fixed',
bottom: '16px',
right: '16px',
zIndex: 50,
},
chatIcon: {
width: '64px',
height: '64px',
backgroundColor: '#3B82F6', // blue-500
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
},
chatIconImage: {
width: '48px',
height: '48px',
},
messageBubble: {
position: 'absolute',
bottom: '80px',
right: '0',
backgroundColor: '#1F2937', // gray-800
borderRadius: '8px',
padding: '12px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
},
messageBubbleText: {
color: 'white',
fontSize: '14px',
whiteSpace: 'nowrap',
},
messageBubbleTail: {
position: 'absolute',
bottom: '-6px',
right: '24px',
width: '12px',
height: '12px',
backgroundColor: '#1F2937', // gray-800
transform: 'rotate(45deg)',
},
chatbotContainer: {
position: 'fixed',
bottom: '90px',
right: '20px',
width: '300px',
height: '400px',
border: '1px solid #ccc',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
zIndex: 1000,
display: isOpen ? 'flex' : 'none',
flexDirection: 'column',
backgroundColor: '#fff',
},
chatbotHeader: {
padding: '10px',
backgroundColor: '#007bff',
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
closeButton: {
background: 'none',
border: 'none',
color: '#fff',
fontSize: '20px',
cursor: 'pointer',
},
chatbotMessages: {
flexGrow: 1,
overflowY: 'auto',
padding: '10px',
},
message: {
margin: '5px 0',
padding: '8px',
borderRadius: '8px',
},
botMessage: {
backgroundColor: '#e6f2ff',
alignSelf: 'flex-start',
},
userMessage: {
backgroundColor: '#dcf8c6',
alignSelf: 'flex-end',
},
inputContainer: {
display: 'flex',
padding: '10px',
},
input: {
flexGrow: 1,
marginRight: '10px',
padding: '5px',
},
sendButton: {
padding: '5px 10px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
},
};
const loadingIndicatorStyle = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '10px',
};
const toggleChat = () => {
setIsOpen(!isOpen);
};
const handleInputChange = (e) => {
setUserInput(e.target.value);
};
const handleSendMessage = async () => {
if (userInput.trim() === '') return;
const newUserMessage = { text: userInput, sender: 'user' };
setMessages([...messages, newUserMessage]);
setUserInput('');
setIsLoading(true);
try {
const response = await axios.post('/chatbot/message', { message: userInput, context: context });
const botResponse = { text: response.data.response, sender: 'bot' };
setMessages((prevMessages) => [...prevMessages, botResponse]);
} catch (error) {
console.error('Error sending message to ChatGPT:', error);
} finally {
setIsLoading(false);
}
};
return (
<>
What do you want to build?
export default ChatBot;
import React, {Component} from 'react';
class SidebarMenuItem extends Component {
constructor(props){
super(props);
}
render(){
const {title, path, icon} = this.props;
return (
export default SidebarMenuItem;
import React, { Component } from 'react';
class SidebarSubmenuItem extends Component {
constructor(props){
super(props);
}
render(){
const {title, path} = this.props;//Input Params
return ( //TODO: we need to figure out "active" vs. "non-active"
export default SidebarSubmenuItem;
import React, { Component } from "react";
import Request from "./Request"
class RequestsDraggable extends Component {
constructor(props){
super(props);
}
componentDidMount(){
let draggableContaineEl = document.getElementById('job_request_container');
new FullCalendarInteraction.Draggable(draggableContaineEl, {
itemSelector: '.draggable-el',
eventData: function(eventEl) {
return {
title: eventEl.innerText,
duration: '02:00',
extendedProps: { //extended props set's the event info fields for fullcalendar to use, pulling data from the data attribute tags
requestId: eventEl.dataset.requestid,
customerId: eventEl.dataset.customerid,
locationId: eventEl.dataset.locationid,
firstName: eventEl.dataset.firstname,
lastName: eventEl.dataset.lastname,
email: eventEl.dataset.email,
phone: eventEl.dataset.phone,
streetAddress: eventEl.dataset.streetaddress,
zip: eventEl.dataset.zip,
city: eventEl.dataset.city,
state: eventEl.dataset.state,
summary: eventEl.dataset.summary,
notes: eventEl.dataset.notes,
requestId: eventEl.dataset.requestid,
marketingSourceId: eventEl.dataset.marketingsourceid,
marketingSourceName: eventEl.dataset.marketingsourcename
}
}
}
});
}
render(){
const {requests} = this.props;
return ( //todo: each draggable.
}
export default RequestsDraggable;
import React, {Component} from "react";
class Request extends Component{
constructor(props){
super(props);
}
render(){
const {request} = this.props;
return(
export default Request;
import React, { Component } from "react";
import Select from "react-select";
import AsyncSelect from 'react-select/async';
class BillToDifferentCustomer extends Component {
constructor(props) {
super(props);
}
render() {
const { showingDiffCust, selectedDiffCustOption } = this.props;
return (
import React, { Component } from "react";
import PaidInvoices from "./PaidInvoices";
import OutstandingInvoices from "./OutstandingInvoices";
import LateInvoices from "./LateInvoices";
class InvoiceTracking extends Component {
constructor(props) {
super(props);
}
render() {
const { outstandingInvoices, lateInvoices, paidInvoices } = this.props;
return (
export default InvoiceTracking;
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.title }
{ formatCurrency(data.total_price) }
{ data.issued_on }
{ data.delivered_on }, {this._markAsDelivered(data.delivered_on != "Not Delivered", data.id) }
{ data.paid_in_full } { this._canMarkAsPaid(data.paid_in_full == "No", data.id) }
{ this._tools(data) }
))
}
);
}
_tools(data) {
const { tableData } = this.props;
return(
);
}
_canMarkAsPaid(wasntPaid, id){
if (wasntPaid){
return(
this._markInvoiceAsPaid(id, this)}>
);
}
}
_markAsDelivered(wasDelivered, id){
if (!wasDelivered){
return(
this._emailInvoiceToCustomer(id, this)}>
);
}
else {
return this._emailInvoiceToCustomer(id,this)}>
}
}
_emailInvoiceToCustomer(id){
emailInvoiceToCustomer(id);
window.location.reload();
}
_markInvoiceAsPaid(id, toReRender){
$.post({
url: `../../invoices/${id}/mark_as_paid`,
data: {id: id},
success: function(){
gritterSuccess("Successfully Marked as Paid",
`You've successfully marked invoice: ${id} as paid in full`);
window.location.reload();
},
error: function(data){
gritterError("Couldn't mark invoice as paid", "", "10000");
}
})
}
}
export default TableBody;
import React, { Component } from "react";
class LateInvoices extends Component {
constructor(props){
super(props);
this.styles = {
a: {color: "black", float: "left", padding: 0}
};
}
render(){
const { invoices } = this.props;
if (invoices == null || invoices == undefined)
return null;
return (
invoices.map((data, index) => (
{ data.title }
{data.amount_due}
{data.due_date}
))
);
}
}
export default LateInvoices;
import React, { Component } from "react";
class PaidInvoices extends Component {
constructor(props){
super(props);
this.styles = {
a: {color: "black", float: "left", padding: 0}
};
}
render(){
const { invoices } = this.props;
if (invoices == null || invoices == undefined)
return null;
return (
invoices.map((data, index) => (
{ data.title }
{data.amount_due}
{data.due_date}
))
);
}
}
export default PaidInvoices;
import React, { Component } from "react";
import InvoiceDocument from "./InvoiceDocument/InvoiceDocument";
import Select from "react-select";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import ToggleButton from "@mui/material/ToggleButton";
import BillToDifferentCustomer from "./BillToDifferentCustomer";
class InvoiceEditPage extends Component {
constructor(props) {
super(props);
this.state = {
invoiceType: this.props.invoice.invoice_type || "Invoice",
disableSave: false,
showingDiffCust: this.props.shouldInvoiceOtherCustomer,
selectedDiffCustOption: this.props.invoice.invoice_to ? {value: this.props.invoice.invoice_to_id, label: this.props.invoice.invoice_to.full_name} : {value: this.props.customerToInvoice?.id, label: this.props.customerToInvoice?.full_name},
}
this.handleInvoiceEstimateTypeSelection = this.handleInvoiceEstimateTypeSelection.bind(this);
this.onInvoiceTemplateSelected = this.onInvoiceTemplateSelected.bind(this);
this.onDifferentCustomerSelected = this.onDifferentCustomerSelected.bind(this);
this.toDifferentCustomerCheckbox = this.toDifferentCustomerCheckbox.bind(this);
this.setInvoiceType = this.setInvoiceType.bind(this);
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
this.getCustomerSearchOptions = this.getCustomerSearchOptions.bind(this);
}
handleInvoiceEstimateTypeSelection(event){
this.invoiceDocument.setInvoiceType(event);
}
onInvoiceTemplateSelected(event){
this.invoiceDocument.updateSelectedTemplateValues(event);
}
onDifferentCustomerSelected(event){
this.setState({
...this.state,
selectedDiffCustOption: {value: event.customer.id, label: event.customer.full_name},
})
this.invoiceDocument.updateServiceToData(event);
}
toDifferentCustomerCheckbox(event){
if(!event.currentTarget.checked){
this.invoiceDocument.updateInvoiceTo();
}
this.setState({
...this.state,
showingDiffCust: event.currentTarget.checked,
})
}
onSaveButtonClick(event){
if (!this.state.disableSave){
this.invoiceDocument.saveInvoiceToServer();
}
this.setState({
...this.state,
disableSave: true,
})
}
setInvoiceType(event){
this.setState({
...this.state,
invoiceType: event.target.value
})
}
getCustomerSearchOptions(input, callback){
fetch(`/customer_data/?name=${input}`)
.then(res => res.json())
.then(
(result) => {
callback(result)
},
(error) => {
console.log(error);
}
)
}
render() {
return (
toDifferentCustomerCheckbox={this.toDifferentCustomerCheckbox}
showingDiffCust={this.state.showingDiffCust}
onDifferentCustomerSelected={this.onDifferentCustomerSelected}
getCustomerSearchOptions={this.getCustomerSearchOptions}
selectedDiffCustOption={this.state.selectedDiffCustOption}
/>
);
}
}
export default InvoiceEditPage;
import React, { Component } from "react";
class MarkInvoiceAsPaidIcon extends Component {
constructor(props) {
super(props);
}
render() {
const { invoiceId } = this.props;
return (
markAsPaid(invoiceId, this)}>
Mark as Paid (Manual)
);
}
}
export default MarkInvoiceAsPaidIcon;
import React, { Component } from "react"
class SignaturePad extends Component {
constructor(props){
super(props);
}
componentDidMount(){
// var canvas = document.getElementById("signature-pad");
// console.log("Canvas is: " + canvas);
// var signaturePad = new SignaturePad(canvas);
// submittingForm = canvas.closest("form");
// canvas.addEventListener('mousedown', function() {
// console.log("Mouse down on the canvas element");
// enablePayment();
// }, false);
// canvas.addEventListener('mouseup', function() {
// console.log("Mouse up on the canvas element");
// document.getElementById("signature-pad-value").value = signaturePad.toDataURL();
// }, false);
// $('button[type="submit"]').click(function(e){//here's where I could check if the signature was present, if it's not, prevent default and alert that they need to sign.
// document.getElementById("signature-pad-value").value = signaturePad.toDataURL();
// })
// if(props.signature != null)
// {
// setSignatureToBeEdited(props.signature);
// }
// else
// {
// disablePaymentIfNoSignature(); // call this after we set the signature
// }
// setSignatureToBeEdited = function(dataPoints){ //sets the signature from an invoice that's already been signed
// signaturePad.fromDataURL(dataPoints);
// }
// document.getElementById('clear-signature').addEventListener('click', function () {
// signaturePad.clear();
// document.getElementById("signature-pad-value").value = "";
// disablePayment(); // we don't want them to be able to pay if they just cleared the signature, because there's no signature now haha.
// });
// disablePaymentIfNoSignature = function(){
// if (signaturePad.isEmpty()){
// disablePayment();
// }
// }
// function disablePayment(){
// // document.getElementById("elavon-button").disabled=true;
// if(document.getElementById("payment_gateway_name").value == 'stripe'){
// document.getElementsByClassName("stripe-button-el")[0].disabled=true;
// } else{
// document.getElementsByClassName("ibx-pay-button")[0].disabled=true;
// }
// $('#require-signature-notice').show();
// }
// function enablePayment(){
// // document.getElementById("elavon-button").disabled=false;
// if(document.getElementById("payment_gateway_name").value == 'stripe'){
// document.getElementsByClassName("stripe-button-el")[0].disabled=false;
// } else{
// document.getElementsByClassName("ibx-pay-button")[0].disabled=false;
// }
// $('#require-signature-notice').hide();
// }
}
render() {
return (
export default SignaturePad;
import React, { Component } from "react";
//** TODO: Implement. */
class Pay extends Component {
constructor(props) {
super(props);
}
render() {
const { acceptCardPayment, payWithStripe, payWithIbx, amountWithoutTransactionFee, transactionFee, invoice, } = this.props;
return (
import React, { Component } from "react";
class EmailInvoiceIcon extends Component {
constructor(props) {
super(props);
}
render() {
const { invoiceId } = this.props;
return (
emailInvoiceToCustomer(invoiceId, this)}>
);
}
}
export default EmailInvoiceIcon;
import React, {Component} from "react";
class LegalDisclaimer extends Component{
constructor(props){
super(props)
}
render(){
const { invoiceType, invoiceTerms, estimateTerms } = this.props;
return (
export default LegalDisclaimer;
import React, {Component} from "react";
class Signature extends Component{
constructor(props){
super(props)
}
render(){
const { signatureUrl } = this.props;
return (
Signature:
export default Signature;
import React, { Component, useState } from "react";
import LegalDisclaimer from "./LegalDisclaimer";
import TotalDue from "./TotalDue";
import LineItems from "./LineItems/LineItems";
import Header from "./Header/Header";
import Information from "./Information/Information";
import { Button } from "bootstrap";
import { success } from "toastr";
class InvoiceTemplateDocument extends Component {
constructor(props) {
super(props);
let oldPriceSum = 0;
const { invoice, lineItems, handlingInvoiceTemplate, logo, customerToInvoice, alternateLogoFromTemplate, business } = props;
const { amount_paid } = invoice;
lineItems.map((value)=>{ oldPriceSum += parseFloat(value.price) * parseInt(value.quantity); })
const oldTotalAmount = parseFloat(oldPriceSum).toFixed(2) ? parseFloat(oldPriceSum).toFixed(2) : 0.00;
const amountDue = parseFloat(oldTotalAmount).toFixed(2) || 0.00;
const totalAmount = parseFloat(oldTotalAmount) || 0.00;
const amountPaid = amount_paid || 0;
const businessName = invoice.business_name || business.name;
this.state = {
invoiceTemplateId: invoice.id,
customerToInvoice: customerToInvoice,
notes: invoice.description || "",
lineItems: lineItems || [],
amountDue: amountDue,
amountPaid: amountPaid,
totalPrice: totalAmount,
issuedDate: invoice.issued_on,
logo: logo,
alternateLogoFromTemplate: alternateLogoFromTemplate,
alternativeBusinessName: businessName,
handlingInvoiceTemplate: true,
invoiceTemplateDisplayNumber: '0',
invoiceType: 'Template',
file: {}
}
this.updateLineItems = this.updateLineItems.bind(this);
this.saveInvoiceTemplateToServer = this.saveInvoiceTemplateToServer.bind(this);
this.setNotes = this.setNotes.bind(this);
this.setLogo = this.setLogo.bind(this);
this.removeLogo = this.removeLogo.bind(this);
this.updateAlternateBusinessName = this.updateAlternateBusinessName.bind(this);
}
saveInvoiceTemplateToServer(redirectUrl){
const formData = new FormData();
const basePath = 'invoice_templates';
const { file, alternativeBusinessName, invoiceTemplateId, notes, lineItems } = this.state;
const { updateDisableSave, templateName, invoice } = this.props;
const url = `/${basePath}/${ invoiceTemplateId ? invoiceTemplateId : ''}`;
//url will be POST /invoices or POST /invoice_templates or PUT "/invoices/:id" or PUT "/invoice_templates/:id"
//if invoiceTemplateId == null, do a POST to create new invoice, if invoiceTemplateId == not null, do a PUT to update the invoice.
const requestType = `${invoiceTemplateId ? "PUT" : "POST"}`;
const name = invoice.id ? templateName || invoice.name : templateName
if(file) {
formData.append('logo', file)
}
// if(!this.props.templateName || !notes) {
// updateDisableSave(false);
// return gritterError(`Error`, notes ? "Notes can't be blank" : "Template name can't be blank" )
// }
const invoiceTemplate = {
'name': name,
'id': invoiceTemplateId,
'description': notes,
'line_items': lineItems,
'business_name': alternativeBusinessName,
'redirect_url': redirectUrl
}
formData.append('invoice_template', JSON.stringify(invoiceTemplate));
$.ajax({
url: url,
type: requestType,
dataType: 'json',
data: formData,
processData: false,
contentType: false,
success: function(data){
console.log("Successfully saved invoice!");
gritterSuccess("Success", "Successfully saved invoice!");
window.location.href = `/${basePath}/${data.id}`;
},
error: function(data){
console.log("Error Ocurred");
updateDisableSave(false);
gritterError(`Error`, "Something went wrong");
}
});
}
render() {
return (
setNotes(event, exactValue = null){
this.setState({
...this.state,
notes: exactValue || event.target.innerText
})
}
updateLineItems(items, notes = null) {
const priceSum = items.reduce(
(previousValue, lineItem) => previousValue + (parseFloat(lineItem.price ? lineItem.price : 0.00) * lineItem.quantity), 0.00
)
const newTotalAmount = parseFloat(priceSum).toFixed(2) ? parseFloat(priceSum).toFixed(2) : 0.00;
// const newDueAmount = newTotalAmount - parseFloat(this.props.invoice.amount_paid).toFixed(2);
this.setState({
...this.state,
lineItems: [...items],
totalPrice: newTotalAmount,
amountDue: newTotalAmount,
notes: notes || this.state.notes,
});
}
updateNotesAndLineItemsFromTemplateSelect(selection) {
this.updateLineItems([...this.state.lineItems,
...selection.line_items.map(
function(item) {
return {
id: item.id,
name: item.name,
price: item.price,
product_id: item.product_id,
quantity: item.quantity || 1} //this is to ensure there's no null quantity
})
],
selection.notes);//Hacky to tack on this notes to the updateLineItems call.
//TODO: Add invoiceTemplateId to save onto this if it was seleted from template select.
}
setLogo(event) {
console.log('Image Changed');
const src = event.target.value;
if(src) {
const file = event.target.files[0];
const path = window.URL.createObjectURL(event.target.files[0]);
this.setState({
...this.state,
alternateLogoFromTemplate: path,
file: file
});
}
}
removeLogo() {
const { alternateLogoFromTemplate, invoiceTemplateId } = this.state;
if(alternateLogoFromTemplate) {
console.log('Remove logo');
this.setState({
...this.state,
alternateLogoFromTemplate: '',
file: {}
});
$('#logoUpload').val('');
if(invoiceTemplateId) {
$.ajax({
'url': `/invoice_template/${invoiceTemplateId}/logo`,
'method': 'DELETE',
'success': function(){
gritterSuccess('Success', 'Logo removed successfully');
},
'error': function(error){
console.log(error)
gritterError('Error', 'Unable to delete logo');
}
})
}
}
}
updateAlternateBusinessName(event) {
const { business } = this.props;
const newBusinessName = event.target.innerText || business.name;
console.log(newBusinessName)
this.setState({
...this.state,
alternativeBusinessName: newBusinessName
})
}
}
export default InvoiceTemplateDocument;
import React, {Component} from "react";
import ContentEditable from "react-contenteditable"
class BusinessInformation extends Component {
constructor(props){
super(props);
}
render() {
const { business, businessPhone, handlingInvoiceTemplate, editable, updateAlternateBusinessName, alternativeBusinessName } = this.props;
const { name: defaultBusinessName, email: businessEmail, street_address: businessSreetAddress, city: businessCity, state: businessState, zip_code: businessZipCode, notes: businessNotes } = business;
const businessName = alternativeBusinessName || defaultBusinessName;
return(
Customer Rep: {this.props.invoice.owner.full_name}
Phone: { this.props.invoice.owner.phone}
Email: { this.props.invoice.owner.email }
export default BusinessInformation;
import React, {Component} from "react";
import ContentEditable from "react-contenteditable"
import BusinessInformation from "./BusinessInformation";
import CustomerInformation from "./CustomerInformation";
class Information extends Component {
constructor(props){
super(props);
this.formatHtmlForNotes = this.formatHtmlForNotes.bind(this);
}
formatHtmlForNotes(notes){
let notesArray = notes.split('\n');
let htmlString = ""
for (let i = 0; i < notesArray.length; i++){
let item = notesArray[i];
if (i == notesArray.length-1){
htmlString += item + "";
}
else {
htmlString += `${item}
`
}
}
return isBlank(htmlString) ? "" : htmlString;
}
render() {
return(
{
this.props.customerToInvoice &&
}
export default Information;
import React, {Component} from "react";
import ServiceTo from './ServiceTo';
import AddressModal from '../../../shared/AddressModal';
import AddressGenericModal from "../../../shared/AddressGenericModal";
class CustomerInformation extends Component {
constructor(props){
super(props);
const { job, customerLocations } = props
this.state = {
addressModalShow: false,
showGenericModal: false,
customerLocations: customerLocations || [],
job: job
}
this.openAddressModal = this.openAddressModal.bind(this);
this.closeLocationModal = this.closeLocationModal.bind(this);
this.openAddressGenericModal = this.openAddressGenericModal.bind(this);
this.makeServiceAddress = this.makeServiceAddress.bind(this);
this.updateAddressForm = this.updateAddressForm.bind(this);
}
openAddressModal(event){
this.setState({
...this.state,
addressModalShow: true
})
}
closeLocationModal(event){
this.setState({
...this.state,
addressModalShow: false,
})
}
openAddressGenericModal(showAddressModal) {
this.setState({ ...this.state, showGenericModal: showAddressModal })
}
makeServiceAddress(newServiceAddress){
$(".address-menu").css('display','none');
const { id } = newServiceAddress;
const { id:job_id } = this.props.job;
const that = this;
$.ajax({
method: 'PUT',
url: `/jobs/${job_id}/update_attribute`,
data: {attribute: 'location_id', value: id},
success: function(data){
that.setState({
...that.state,
job: data
})
gritterSuccess("Success", "Successfully Set Service Address");
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
updateAddressForm(){
const that = this;
const { streetAddress, city, state, zipCode, id:newLocationId } = this.genericAddressModal?.state?.addressFormData;
const locationMethod = newLocationId != '' ? 'PUT' : 'POST';
const locationUrl = newLocationId != '' ? "/locations/" + newLocationId + ".json" : "/locations.json";
const location = {
location: {
street_address: streetAddress,
city: city,
zip_code: zipCode,
state: state,
customer_id: this.state?.job?.location?.customer_id
}
};
$.ajax({
method: locationMethod,
url: locationUrl,
data: location,
success: function(data){
$.ajax({
method: 'GET',
url: `/jobs/${that.props.job.id}.json`,
success: function(data){
that.setState({
...that.state,
job: data
})
},
error: function(data){
gritterError("Error", "Service Address Not Changed", "10000");
}
});
newLocationId != ''
? that.setState({
...that.state,
customerLocations: that.state.customerLocations.map(ldata => ldata.id == data.id ? data : ldata),
})
: that.setState({
...that.state,
customerLocations: [ ...that.state.customerLocations, data ],
})
if(that.state?.job?.location?.id == data?.id) {
const updatedJob = { ...that.state.job, location: data }
that.setState({ ...that.state, job: updatedJob, })
}
that.genericAddressModal.handleAddressFormShow(false);
gritterSuccess("Success", "Address Saved Successfully");
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
render() {
const { invoiceType, handlingInvoiceTemplate, customerToInvoice, serviceTo, invoice, showServiceTo, editable } = this.props;
const { id:customerId, full_name:fullName, default_location_display:defaultLocationDisplay, email:customerEmail, phone: customerPhone, default_location: customerDefaultLocation} = customerToInvoice;
const { addressModalShow, job } = this.state;
return(
export default CustomerInformation;
import React, {Component} from "react";
class ServiceTo extends Component {
constructor(props){
super(props);
}
render() {
const { handlingInvoiceTemplate, serviceTo } = this.props;
const { full_name: serviceToFullName, email: serviceToEmail, phone: serviceToPhone, default_location_display: serviceToDefaultLocationDisplay, default_location: serviceTodisplayAddress} = serviceTo;
return(
export default ServiceTo;
import React, {Component} from "react";
import { getMobileOperatingSystem } from "../../../../../assets/javascripts/fullcalendar_helpers/FCTools";
import LineItemHeaderDesktop from "./Desktop/LineItemHeader";
import LineItemHeaderMobile from "./Mobile/LineItemHeader";
class LineItemDesktopHeader extends Component {
constructor(props){
super(props);
}
render() {
return(
);
}
}
export default LineItemDesktopHeader;
import React, {Component} from "react";
//Desktop Line Item Header
class LineItemHeader extends Component {
constructor(props){
super(props);
}
render() {
return(
Category
Description
Product ID
Quantity
Price
{this.props.enable_total_from_multiplying_by_quantity && (
Amount
)}
{
this.props.editable &&
}
);
}
}
export default LineItemHeader;
import React, {Component} from "react";
import ContentEditable from "react-contenteditable"
import Select from "react-select"
//Desktop Line Item
class LineItem extends Component {
constructor(props){
super(props);
}
render() {
const {name, description, quantity, price, productId, enable_total_from_multiplying_by_quantity} = this.props;
const total = parseFloat(price) * parseFloat(quantity);
const productSelectWidth = isBlank(name) ? "initial" : `${(8*(name || "").length) + 50}`;
return(
{ this.props.editable
?
:
{name}
}
{
if (e.which == 13) e.preventDefault()
}}
onBlur={this.props.onLineItemChange}
placeholder={this.props.editable ? "Enter Description Here.." : ""}
disabled={!this.props.editable}
/>
{enable_total_from_multiplying_by_quantity && (
)}
{this.props.editable &&
}
);
}
}
export default LineItem;
import React, {Component} from "react";
import LineItem from "./LineItem";
import LineItemHeader from "./LineItemHeader"
class LineItems extends Component {
constructor(props){
super(props);
this.addLineItem = this.addLineItem.bind(this);
}
addLineItem() {
const { updateLineItems, lineItems } = this.props;
const newLineItem = {
name: null,
product_id: null,
description: null,
quantity: 1,
price: 0,
}
updateLineItems([...lineItems, newLineItem])
}
render() {
const { lineItems, editable, updateLineItems, enable_total_from_multiplying_by_quantity } = this.props;
//Second sort for safety. This sort was originally here before the one on InvoiceDocument.jsx was added, but this one alone wasn't sufficient.
// This second sort might be redundant now, however we are leaving it in because it is currently working and we don't want to break it.
const sortedByPositionLineItems = [...lineItems].sort((a, b) => { //sorts this by position, but appears to not match up with the index. CAREFUL. This part of the code is somewhat fragile. Test well before deploying changes.
const posA = typeof a.position === "number" ? a.position : Infinity;
const posB = typeof b.position === "number" ? b.position : Infinity;
return posA - posB;
});
//Note: This is kind of hacky/bad because I don't really understand this code super well. How it is doing this based off the index and based off the position is confusing.
// In essence, this 'position' attribute modifies where it gets shown to the user, but it doesn't update the 'index' of the original array because the original array
// is based off the server's position.
return(
{
sortedByPositionLineItems.map(
(lineItem, index) =>
)
}
);
}
}
export default LineItems;
import React, {Component} from "react";
import Select from "react-select";
import ContentEditable from "react-contenteditable";
//Mobile Line Item
class LineItem extends Component {
constructor(props){
super(props); //Need an update method to edit the contents of the line item.
}
render() {
const {name, description, quantity, price, productId} = this.props; //How to handle editable???
return(
{ this.props.editable ?
:
{name}
}
{
if (e.which == 13) e.preventDefault()
}} //disable newlines
onBlur={this.props.onLineItemChange}
placeholder={this.props.editable ? "Enter Description Here.." : ""}
disabled={!this.props.editable}
className='li-desc custom-font-size'
/>
Qty:
{
this.props.enable_total_from_multiplying_by_quantity &&
Total:
}
export default LineItem;
import React, {Component} from "react";
import LineItemDesktop from "./Desktop/LineItem";
import LineItemMobile from "./Mobile/LineItem";
class LineItem extends Component {
constructor(props){
super(props);
this.removeLineItem = this.removeLineItem.bind(this);
this.onLineItemChange = this.onLineItemChange.bind(this);
this.onLineItemFocus = this.onLineItemFocus.bind(this);
this.onCategoryLineSelected = this.onCategoryLineSelected.bind(this);
this.defaultLineItemValues = {
name: null,
description: null,
quantity: 1,
price: 0,
product_id: null,
}
}
removeLineItem() {
const { updateLineItems, lineItems, index} = this.props;
const newLineItems = lineItems.filter((lineitem, itemIndex)=> itemIndex != index);
updateLineItems([...newLineItems]);
}
onLineItemFocus(event){
//if event.target === default value.?
const changedAttribute = event.currentTarget.dataset.attribute;
switch (changedAttribute) {
case "description":
if (event.target.innerHTML == this.defaultLineItemValues.description)
window.getSelection().selectAllChildren(event.target)
break;
case "name":
if (event.target.innerHTML == this.defaultLineItemValues.name)
window.getSelection().selectAllChildren(event.target)
break;
case "quantity":
window.getSelection().selectAllChildren(event.target)
break;
case "price":
window.getSelection().selectAllChildren(event.target)
break;
}
}
onLineItemChange(event) {
// debugger;
console.log("Line item changed!");
const { lineItems, index, updateLineItems } = this.props;
const changedAttribute = event.currentTarget.dataset.attribute;
const updatedLineItems = [...lineItems];
let cleanedValue = event.target.innerHTML || event.target.value;
switch (changedAttribute) {
case "quantity":
cleanedValue = parseFloat(cleanedValue) ? parseFloat(cleanedValue) : 1;
break;
case "price":
cleanedValue = cleanedValue.replace('$', '').replace(',', '')
cleanedValue = parseFloat(cleanedValue) ? parseFloat(cleanedValue) : 0.00;
break;
case "description":
cleanedValue = cleanedValue.replace(/ /g, ' '); //clear out any nbsp that comes here. This is due to calling event.target.innerHTML, it is getting HTML elements from it.
break;
}
updatedLineItems[index][changedAttribute] = cleanedValue;
updateLineItems([...updatedLineItems]);
}
onCategoryLineSelected(event){
const { lineItems, index, updateLineItems } = this.props;
const updatedLineItems = [...lineItems];
const productId = event.value;
const productName = event.label;
const description = event.data_description;
const price = event.data_price
updatedLineItems[index].product_id = productId;
updatedLineItems[index].name = productName;
if (isBlank(lineItems[index].description)){
updatedLineItems[index].description = description;
}
if (lineItems[index].price == this.defaultLineItemValues.price){
updatedLineItems[index].price = price;
}
updateLineItems([...updatedLineItems]);
}
render() {
const {name, description, quantity, price, editable, index, productId} = this.props;
return(
);
}
}
export default LineItem;
import React, {Component} from "react";
class TotalDue extends Component {
constructor(props){
super(props);
}
render() {
const { totalAmount, totalAmountPaid, amountDue, subtotalAmount, salesTaxAmount, enableSalesTaxCalculation, salesTax } = this.props;
return(
//TODO: Mike - if (invoice.subTotal > 0 && invoice.sales_tax > 0)
export default TotalDue;
import React, {Component} from "react";
class InvoiceTemplateLogo extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
}
render() {
const { onLogoChange, removeLogo, alternateLogoFromTemplate } = this.props;
return(
{
export default InvoiceTemplateLogo;
import React, {Component} from "react";
class InvoiceNumberReadOnly extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
}
//TODO: how to handle mobile..??
render() {
return(
export default InvoiceNumberReadOnly;
import React, {Component} from "react";
class InvoiceNumberEditable extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
}
render() {
return(
export default InvoiceNumberEditable;
import React, {Component} from "react";
import ContentEditable from "react-contenteditable"
class IssuedDateReadOnly extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
this.onDueDateChange = this.onDueDateChange.bind(this);
this.onIssueDateChange = this.onIssueDateChange.bind(this);
this.invoiceDueText = this.invoiceDueText.bind(this);
}
componentDidMount() {
flatpickr("#invoice-date", {
altInput: true,
altFormat: "F j, Y",
disableMobile: "true",
onChange: this.onIssueDateChange
});
flatpickr("#invoice-due-date", {
altInput: true,
altFormat: "F j, Y",
disableMobile: "true",
onChange: this.onDueDateChange
});
SetFlatpickr('invoice-date', this.props.issuedDate);
SetFlatpickr('invoice-due-date', this.props.dueDate);
}
invoiceDueText(invoiceType){
if (invoiceType == "Invoice")
return "Due"
else if (invoiceType == "Estimate")
return "Expires"
else
return ""
}
onIssueDateChange(selectedDates, date_str, instance) {
const { setDates } = this.props;
const currentInvoiceDueDate = $('#invoice-due-date').val();
setDates(date_str, false)
if(Number(new Date(date_str)) > new Date(currentInvoiceDueDate)) {
setDates(date_str, true); //Updating due date when issue date is selected beyond due date
SetFlatpickr('invoice-due-date', date_str);
}
}
onDueDateChange(selectedDates, date_str, instance) {
const { setDates } = this.props;
const currentInvoiceIssuedDate = $('#invoice-date').val();
setDates(date_str, true)
if(Number(new Date(date_str)) < new Date(currentInvoiceIssuedDate)) {
setDates(date_str, false); //Updating isseue date when due date is selected before issue date
SetFlatpickr('invoice-date', date_str);
}
}
render() {
return(
export default IssuedDateReadOnly;
import React, {Component} from "react";
import IssuedDateEditable from "./IssuedDateEditable";
import IssuedDateReadOnly from "./IssuedDateReadOnly";
import InvoiceTemplateLogo from "./InvoiceTemplateLogo";
class Header extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
}
componentDidMount(){
$('#logoOption').hide();
//Handling outside click
$(window).click(function(event){
const classes = event.target.classList.value;
const isInsideClicked = classes.includes('inside')
if(!isInsideClicked) {
$('#logoOption').hide()
}
});
}
render() {
const { defaultLogo, onLogoChange, removeLogo, editable, setDates, handlingInvoiceTemplate, alternateLogoFromTemplate} = this.props;
const imageSrc = alternateLogoFromTemplate || defaultLogo;
return(
{/* This table data column is for the logo, and it should only take up 50% of the invoice document width. */}
export default Header;
import moment from "moment";
import React, {Component} from "react";
class IssuedDateEditable extends Component {
constructor(props){
//Only needs the image src url for the logo, the invoice document type, the invoice number, (need to be able to set invoice number too), the date + due date.
super(props);
this.invoiceDueText = this.invoiceDueText.bind(this);
this.formatHumanReadableDate = this.formatHumanReadableDate.bind(this);
}
invoiceDueText(invoiceType){
if (invoiceType == "Invoice")
return "Due"
else if (invoiceType == "Estimate")
return "Expires"
else
return ""
}
formatHumanReadableDate(dateStr){
return moment(dateStr).format("MMMM Do, YYYY");
}
render() {
return(
{ this.props.handlingInvoiceTemplate ? 'Template ' : this.props.invoiceType + " "}
Number:
{ this.props.invoiceDisplayNumber }
export default IssuedDateEditable;
import React, { Component, useState } from "react";
import LegalDisclaimer from "./LegalDisclaimer";
import TotalDue from "./TotalDue";
import LineItems from "./LineItems/LineItems";
import Header from "./Header/Header";
import Information from "./Information/Information";
import { Button } from "bootstrap";
import Signature from './Signature';
import { success } from "toastr";
class InvoiceDocument extends Component {
constructor(props) {
super(props);
const {
invoice,
handlingInvoiceTemplate,
logo,
customerToInvoice,
serviceTo,
alternateLogoFromTemplate,
alternativeBusinessName,
invoiceTemplateId,
showServiceTo,
estimateNumber,
invoiceNumber,
business
} = props;
const {
line_items: notSortedByPositionLineItems,
amount_paid,
total_price,
sub_total_price,
sales_tax_amount
} = invoice;
const { enable_sales_tax_calculation, sales_tax } = business;
// const { line_items: lineItems, amount_paid, total_price } = invoice;
const amountDue = handlingInvoiceTemplate ? 0 : parseFloat(total_price - amount_paid).toFixed(2) || 0.00;
const totalAmount = parseFloat(total_price) || 0.00;
const amountPaid = amount_paid || 0;
const invoiceDisplayNumber = invoice.invoice_display_number || '0';
const subtotalAmount = parseFloat(sub_total_price) || 0.00;
const salesTaxAmount = parseFloat(sales_tax_amount) || 0.00;
const enableSalesTaxCalculation = enable_sales_tax_calculation || false;
const salesTax = sales_tax || 0;
//We need to sort the line_items by their position.
//There is an issue where if the 'position' is out of sync, then it modifies the wrong line item based on the index not matching the position.
const sortedByPositionLineItems = [...notSortedByPositionLineItems].sort((a, b) => { //sorts this by position, but doesn't update the index.??
const posA = typeof a.position === "number" ? a.position : Infinity;
const posB = typeof b.position === "number" ? b.position : Infinity;
return posA - posB;
});
this.state = {
jobId: invoice.job_id,
invoiceId: invoice.id,
customerId: invoice.customer_id,
invoiceDisplayNumber: invoiceDisplayNumber,
invoiceType: invoice.invoice_type,
customerToInvoice: customerToInvoice,
serviceTo: serviceTo,
sentOn: invoice.sent_on,
notes: invoice.notes || "",
lineItems: sortedByPositionLineItems || [],
amountDue: amountDue,
amountPaid: amountPaid,
totalPrice: totalAmount,
issuedDate: invoice.issued_on,
dueDate: invoice.due_on,
handlingInvoiceTemplate: false,
logo: logo,
alternateLogoFromTemplate: alternateLogoFromTemplate,
alternativeBusinessName: alternativeBusinessName,
invoiceTemplateId: invoiceTemplateId,
invoiceTo: invoice.invoice_to || '',
showServiceTo: showServiceTo || this.props.shouldInvoiceOtherCustomer,
subtotalAmount: subtotalAmount,
salesTaxAmount: salesTaxAmount,
enableSalesTaxCalculation: enableSalesTaxCalculation,
salesTax: salesTax,
}
this.updateLineItems = this.updateLineItems.bind(this);
this.setDates = this.setDates.bind(this);
this.saveInvoiceToServer = this.saveInvoiceToServer.bind(this);
this.setInvoiceDisplayNumber = this.setInvoiceDisplayNumber.bind(this);
this.setNotes = this.setNotes.bind(this);
this.setInvoiceType = this.setInvoiceType.bind(this);
}
// saveInvoiceTemplateToServer(){
// // saves line items, notes, invoice type, alternative business name, alternative business logo
// // Name of invoice template, etc.
// }
saveInvoiceToServer(){
// const totalfiles = $('#attach_images').prop("files").length;
const formData = new FormData();
const { handlingInvoiceTemplate } = this.state;
const basePath = handlingInvoiceTemplate ? 'invoice_templates' : 'invoices'; //Make basepath more generic by taking it as a props such as invoices and invoice_templates
const url = `/${basePath}/${ this.state.invoiceId ? this.state.invoiceId : ''}`;
const requestType = `${this.state.invoiceId ? "PUT" : "POST"}`;
//Temporarily disable images.
// for (let index = 0; index < totalfiles; index++) {
// formData.append("images[]", $('#attach_images').prop("files")[index]);
// }
let lineItemsWithPosition = this.state.lineItems.map((item, index) => ({
...item,
position: index
}));
const invoice = {
'invoice_template_id': this.state.invoiceTemplateId,
'id': this.state.invoiceId,
'job_id': this.state.jobId,
'invoice_type': this.state.invoiceType,
'invoice_display_number': this.state.invoiceDisplayNumber,
'issued_on': this.state.issuedDate,
'due_on': this.state.dueDate,
'notes': this.state.notes,
'invoice_to_id': isBlank(this.state.invoiceTo) ? '' : this.state.invoiceTo,
'line_items': lineItemsWithPosition,
}
formData.append('invoice', JSON.stringify(invoice))
//Sending data as direct json in case of put request
//else sending stringfy json where we might send images along with it
const data = this.state.invoiceId ? {"invoice": invoice} : formData;
//sending content type application/x-www-form-urlencoded for put request where json data is raw instead of string
const contentType = this.state.invoiceId ? "application/x-www-form-urlencoded" : false;
//sending process data true in case of put request and false in post request
const processData = !isBlank(this.state.invoiceId);
//url will be POST /invoices or POST /invoice_templates or PUT "/invoices/:id" or PUT "/invoice_templates/:id"
//if invoiceId == null, do a POST to create new invoice, if invoiceId == not null, do a PUT to update the invoice.
$.ajax({
url: url,
type: requestType,
dataType: 'json',
data: data,
processData: processData,
contentType: contentType,
success: function(data){
console.log("Successfully saved invoice!");
gritterSuccess("Success", "Successfully saved invoice!");
window.location.href = `/${basePath}/${data.id}`;
},
error: function(data){
console.log("Error Ocurred");
gritterError(`Error`, "Something went wrong");
}
});
}
render() {
return (
setNotes(event, exactValue = null){
this.setState({
...this.state,
notes: exactValue || event.target.innerText
})
}
setInvoiceType(event){ //also need to change the invoice display number, replace the "I" with the "E"
const wasSetToInvoice = event.target.value == "Invoice";
const wasSetToEstimate = event.target.value == "Estimate";
let newDisplayNumber = this.state.invoiceDisplayNumber;
//Note: There used to be a bug in this code when using an "InvoiceEditPage", and when editing the invoice type from an invoice to an estimate, or vice versa. These invoiceNumber and estimateNumber props were null, which meant when the user changed the document type, the invoice display number would end up as: null-E, or null-I. This was a bug and confusing to the user. The hack/workaround is by adding invoiceNumber and estimateNumber prop arguments in InvoiceEditPage, and I just set them to be the invoice.invoice_display_number, which ended up having a -I or -E in them already. That's why we strip the number of any non-numeric characters so that we don't end up with a number like 9034-E-E. Which would also be weird behavior. Kind of a hacky workaround, but I want to get this fix out for Perform Heating and Cooling.
if (wasSetToInvoice){
//See above note to explain why we have to strip letters from the invoiceNumber and estimateNumber props.
let invoiceNumberWithLetters = this.props.invoiceNumber + "" //sometimes this comes in as an integer, so we must cast to string before calling 'replace' method
let cleanedInvoiceNumberWithoutLetters = invoiceNumberWithLetters?.replace(/\D/g, ''); //In this example, \D is a regular expression that matches any non-digit character, and the g flag specifies that the expression should be applied globally to the entire string, rather than just the first match. The replace() method replaces all matches with an empty string, effectively removing all non-numeric characters from the input string.
newDisplayNumber = cleanedInvoiceNumberWithoutLetters + "-I"; //TODO: there might be a bug based off of the index, if it originaly has an index, and you change to estimate, then back to invoice, it might save over the display_number without the index and cause an issue with having the same display number for 2 invoices.
}
else if (wasSetToEstimate){
let estimateNumberWithLetters = this.props.estimateNumber + "" //sometimes this comes in as an integer, so we must cast to string before calling 'replace' method
let cleanedEstimateNumberWithoutLetters = estimateNumberWithLetters?.replace(/\D/g, ''); //In this example, \D is a regular expression that matches any non-digit character, and the g flag specifies that the expression should be applied globally to the entire string, rather than just the first match. The replace() method replaces all matches with an empty string, effectively removing all non-numeric characters from the input string.
newDisplayNumber = cleanedEstimateNumberWithoutLetters + "-E";
}
else {
// do nothing?
// This shouldn't happen because right nwo there are only 2 possible invoice types -- Invoice and Estimate
}
this.setState({
...this.state,
invoiceType: event.target.value,
invoiceDisplayNumber: newDisplayNumber
})
const parentPassedInFunctionToSetInvoiceType = this.props.setInvoiceType != undefined;
if(parentPassedInFunctionToSetInvoiceType){
this.props.setInvoiceType(event);
}
}
setInvoiceDisplayNumber(event){
this.setState({
...this.state,
invoiceDisplayNumber: event.target.innerText
});
}
updateLineItems(items, notes = null) {
const priceSum = items.reduce(
(previousValue, lineItem) => previousValue + (parseFloat(lineItem.price ? lineItem.price : 0.00) * lineItem.quantity), 0.00
)
let newSubtotalAmount = 0;
let newTotalAmount = parseFloat(priceSum).toFixed(2) ? parseFloat(priceSum).toFixed(2) : 0.00;
let newSalesTaxAmount = 0;
if (this.props.business.enable_sales_tax_calculation) {
newSubtotalAmount = parseFloat(priceSum).toFixed(2) ? parseFloat(priceSum).toFixed(2) : 0.00;
newSalesTaxAmount = parseFloat(newSubtotalAmount * this.props.business.sales_tax).toFixed(2);
newTotalAmount = parseFloat(parseFloat(newSubtotalAmount) + parseFloat(newSalesTaxAmount)).toFixed(2);
}
const newDueAmount = newTotalAmount - parseFloat(this.props.invoice.amount_paid).toFixed(2);
// Assign a "position" attribute to each line item based on the index of where it's at so the database remembers its position.
const updatedItems = items.map((item, index) => ({
...item,
position: index
})); //TODO: is this where the bug is happening.?
this.setState({
...this.state,
lineItems: [...updatedItems],
totalPrice: newTotalAmount,
amountDue: parseFloat(newDueAmount),
subtotalAmount: parseFloat(newSubtotalAmount),
salesTaxAmount: parseFloat(newSalesTaxAmount),
notes: notes || this.state.notes,
});
}
setDates(date, isDueDate = false) {
isDueDate
? this.setState({ ...this.state, dueDate: date })
: this.setState({ ...this.state, issuedDate: date })
}
updateSelectedTemplateValues(selection) {
const { value, business_name, logo, line_items } = selection;
this.updateLineItems([...this.state.lineItems,
...line_items.map(
function(item) {
return {
id: item.id,
name: item.name,
description: item.description,
price: item.price,
product_id: item.product_id,
quantity: item.quantity || 1} //this is to ensure there's no null quantity
})
],
selection.notes);//Hacky to tack on this notes to the updateLineItems call.
//TODO: Add invoiceTemplateId to save onto this if it was seleted from template select.
this.setState({
invoiceTemplateId: value,
alternateLogoFromTemplate: logo,
alternativeBusinessName: business_name,
})
}
updateServiceToData(selectedData){
this.setState({
...this.state,
showServiceTo: true,
customerToInvoice: selectedData.customer,
invoiceTo: selectedData.customer.id
});
}
updateInvoiceTo(){
this.setState({
...this.state,
showServiceTo: false,
customerToInvoice: this.state.serviceTo,
invoiceTo: ''
});
}
}
export default InvoiceDocument;
import React, { Component } from "react";
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { sortingUrl, tableData } = this.props;
return (
export default Table;
import React, { Component } from "react";
import SortLink from "../shared/SortLink";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
return (
Title
Paid
Actions
);
}
}
export default TableHeader;
import React, { Component } from "react";
class OutstandingInvoices extends Component {
constructor(props){
super(props);
this.styles = {
a: {color: "black", float: "left", padding: 0}
};
}
render(){
const { invoices } = this.props;
if (invoices == null || invoices == undefined)
return null;
return (
invoices.map((data, index) => (
{ data.title }
{data.amount_due}
{data.due_date}
))
);
}
}
export default OutstandingInvoices;
import React, { Component } from "react";
import InvoiceTemplateDocument from "./InvoiceDocument/InvoiceTemplateDocument";
class InvoiceTemplateEditPage extends Component {
constructor(props) {
super(props);
const name = props.invoice.name || '';
const redirectUrl = props.invoice.redirect_url || '';
this.state = {
templateName: name,
redirectUrl: redirectUrl,
disableSave: false,
}
this.setTemplateName = this.setTemplateName.bind(this);
this.setRedirectUrl = this.setRedirectUrl.bind(this);
this.onSaveButtonClick = this.onSaveButtonClick.bind(this);
this.updateDisableSave = this.updateDisableSave.bind(this);
}
setTemplateName(event) {
const newTemplateName = event.target.value;
this.setState({
...this.state,
templateName: newTemplateName
});
}
setRedirectUrl(event) {
const newRedirectUrl = event.target.value;
this.setState({
...this.state,
redirectUrl: newRedirectUrl
});
}
onSaveButtonClick(event){
if (!this.state.disableSave){
this.invoiceTemplateDocument.saveInvoiceTemplateToServer(this.state.redirectUrl);
}
this.updateDisableSave(true);
}
updateDisableSave(value) {
this.setState({
...this.state,
disableSave: value,
})
}
render() {
return (
export default InvoiceTemplateEditPage;
import React, {Component} from "react"
import ClickableCard from "../shared/ClickableCard";
class InvoiceCard extends Component {
constructor(props){
super(props);
}
openInvoiceModal(){
console.log("Open Invoice Modal called");
}
getInvoiceAmountColor(){
if (this.props.invoice.invoice_type == "Invoice"){
if (this.props.invoice.amount_due > 0){
return "red";
}
else {
return "green";
}
}
else{
return "blue";
}
}
getInvoiceDueDateColor(){
let due_date = moment(this.props.invoice.due_date)
// let today = moment.today()
if (due_date < moment()){ //If the due date is before today....
return "green";
}
else {
return "red";
}
//If late, make bright red.
// If within 15 days of being late, make it a dark yellow color
// Anything 15 days out, make it green.
}
sendInvoiceToCustomer(){
emailInvoiceToCustomer(this.props.invoice.id);
}
collectInvoicePayment(){
markAsPaid(this.props.invoice.id);
}
render(){
const {invoice} = this.props;
const cardContent = (
export default InvoiceCard;
import React, { Component } from "react";
import SearchField from "../shared/SearchField";
class JobsTableTop extends Component {
constructor(props) {
super(props);
}
render() {
const { customerID } = this.props;
return (
export default JobsTableTop;
import { toggleButtonGroupClasses } from '@mui/material';
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
this.onMergeCheckboxChecked = this.onMergeCheckboxChecked.bind(this);
}
onMergeCheckboxChecked(event){
console.log("Merge checked with boxes: ${}");
if (event.currentTarget.checked){
this.props.customersToMerge.add(event.currentTarget.value);
}
else {
this.props.customersToMerge.delete(event.currentTarget.value);
}
if (this.props.customersToMerge.size > 0){
this.props.setUserWantsToMerge(true);
}
else {
this.props.setUserWantsToMerge(false);
}
console.log(this.props.customersToMerge);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.full_name }
{ formatPhoneNumber(data.phone) }
{ data.email }
{ this._tools(data) }
))
}
);
}
_tools(data) {
const { tableData, permissions } = this.props;
if( permissions['destroy'] && permissions['update'] ) {
return(
);
} else if( permissions['destroy'] ) {
return(
);
} else if( permissions['update'] ) {
return(
);
} else {
return(
);
}
}
}
export default TableBody;
import React, { Component } from "react";
class CSVModal extends Component {
constructor(props) {
super(props);
}
render() {
return (
Welcome to the Lightning Service Customer Import!
To Import customers into your account, please place your customers into a .csv file.
Currently this feature requires specific columns, if you do not have these data columns,
don’t worry! You can insert empty columns. Please ensure your csv does not contain
columns that are not apart of this list: first_name, last_name,
city, phone, company, notes, state,
street_address, zip_code, and email. If you’re missing a column,
you may leave itempty but please have a header for it at the top of the file.
CSV file
export default CSVModal;
import React, { Component } from "react";
import CustomerSideBySide from "./CustomerSideBySide";
class CustomerMergeTool extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
baseCustomerRecord: {message: 'loading'},
toMergeCustomerRecord: {message: 'loading'},
}
this.mergeRecordIntoBase = this.mergeRecordIntoBase.bind(this);
}
componentDidMount(){
const { baseCustomerId, toMergeCustomerId} = this.props;
// go fetch the customer records from the server in preparation for a merge.
$.get({
url: `/customers/${baseCustomerId}.json`,
success: (data) => {
this.setState({...this.state, isLoading: false, baseCustomerRecord: data})
}})
$.get({
url: `/customers/${toMergeCustomerId}.json`,
success: (data) => {
this.setState({...this.state, isLoading: false, toMergeCustomerRecord: data})
}})
}
mergeRecordIntoBase(){
$.post({
url: "/customers_merge",
data: {base_id: this.props.baseCustomerId, other_id: this.props.toMergeCustomerId},
success: function(data){
gritterSuccess("Merged Customer", "Successfully merged the customer");
}});
}
render() {
const { baseCustomerId, toMergeCustomerId } = this.props;
return (
export default CustomerMergeTool;
import React, {Component} from "react";
class CustomerSideBySide extends Component{
constructor(props){
super(props);
}
formattedRecord(){
Object.entries(this.props.customerRecord).forEach(([key, value]) => {
console.log("Key: " + key);
console.log("Value: " + value);
})
}
render(){
const {customerRecord} = this.props;
return (
export default CustomerSideBySide;
import CSVModal from "./CSVModal";
import React, { Component } from "react";
import Table from "./Table";
import TableTop from "./TableTop";
class Index extends Component {
constructor(props) {
super(props);
this.state = {
userWantsToMerge: false,
customersToMerge: new Set(),
}
this.setUserWantsToMerge = this.setUserWantsToMerge.bind(this);
this.mergeAllRecords = this.mergeAllRecords.bind(this);
}
setUserWantsToMerge(doesUserWantToMerge){
console.log("Set user wants to merge was set to: " + doesUserWantToMerge)
this.setState({...this.state, userWantsToMerge: doesUserWantToMerge})
}
mergeAllRecords(){
let baseRecord = [...this.state.customersToMerge][0]; // we are taking first one checked to be base record
this.state.customersToMerge.delete(baseRecord);
$.post({
url: "/customers_merge_all",
data: {base_id: baseRecord, others_to_merge: [...this.state.customersToMerge]},
success: function(data){
gritterSuccess("Success", "Merged all customer records into one");
window.location.reload();
}
})
}
render() {
const { tableData, permissions, connectedToQuickbooks } = this.props;
console.log(connectedToQuickbooks)
return (
export default Index;
import React, {Component} from "react"
import JobIndex from "./../jobs/JobIndex"
import NavigationButton from "./navigation/NavigationButton"
import CustomerViews from "./CustomerViews"
import InvoiceIndex from "./InvoiceIndex"
class InfoPage extends Component{
constructor(props){
super(props);
this.handleNavigation = this.handleNavigation.bind(this);
this.state = {
activeView: CustomerViews.job,
}
}
render(){
const activeView = this.getActiveView(this.state.activeView);
return(
}
/**
* Depending on what view is selected, here is where we render the relevant views. Right now we have competing views from the app/views/shared views and the views from within our components.
* @param {*} view
* @returns
*/
getActiveView(view){
switch(view){
case(CustomerViews.job):
return
case(CustomerViews.invoice):
return
case(CustomerViews.property):
return
handleNavigation(e, data){
this.setState({activeView: e.target.dataset.viewname})
}
}
export default InfoPage;
const CustomerViews = {
job: "job",
invoice: "invoice",
property: "property",
contact: "contact"
}
export default CustomerViews;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Table;
import React, { Component } from "react";
import SortLink from "../shared/SortLink";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
return (
Phone
);
}
}
export default TableHeader;
import React, { Component } from "react";
import SearchField from "../shared/SearchField";
import QuickbooksIcon from 'images/quickbooks-assets/C2QB_green_btn_sm_hover.png' //https://stackoverflow.com/questions/46844499/how-to-handle-images-in-a-rails-webpacker-react-app
class TableTop extends Component {
constructor(props) {
super(props);
}
render() {
return (
_newLink() {
const { permissions, _ } = this.props;
if( permissions["create"] ) {
return();
} else {
return null;
}
}
_quickbooksIcon(){
const { _, connectedToQuickbooks } = this.props
if (connectedToQuickbooks){
return (
)
} else {
return null;
}
}
}
export default TableTop;
import React, {Component} from "react";
import InvoiceCard from "./InvoiceCard";
class InvoiceIndex extends Component{
constructor(props){
super(props);
}
render(){
const {invoices} = this.props;
//TODO: We'll map over all the invoices and create an InvoiceCard for each one.
return(
{
invoices.map((invoice, index) => (
))
}
)
}
}
export default InvoiceIndex;
import React, {Component} from "react";
class NavigationButton extends Component{
constructor(props){
super(props);
}
// componentDidMount(){
// const {active, text, handleNavigation, id} = this.props;
// if (active){
// document.getElementById(id).style.visibility = "visible";
// }
// else{
// document.getElementById(id).style.visibility = "hidden";
// }
// }
render(){
const {active, text, handleNavigation, viewName} = this.props;
return (
//How to set the job visibility in a smart way..??
{text}
)
}
}
export default NavigationButton;
import React, {useState} from "react";
import Modal from 'react-bootstrap/Modal'
import Button from 'react-bootstrap/Button';
import Flatpickr from "react-flatpickr";
export default function TimesheetEditModal(props){
const {activeTimesheetId,
timesheetCustomerName,
timesheetEmployeeName,
clockedInTime,
clockedOutTime,
timesheets,
setTimesheets,
isOpen,
setIsOpen} = props;
return (
setIsOpen(false)} centered>
{modalTitle}
Clocked In:
{
console.log("datetime changed: " + date)
setClockedInTime(date);
}}
/>
Clocked Out:
{
console.log("datetime changed: " + date)
setClockedOutTime(date);
}}
/>
setIsOpen(false)}>
Close
Save Changes
)
}
import React, {useState, useEffect} from "react"
import TableDataNotEditable from "../shared/TableDataNotEditable"
import Modal from 'react-bootstrap/Modal'
import Button from 'react-bootstrap/Button';
import Flatpickr from "react-flatpickr";
export default function TimesheetsTable({timesheets, setTimesheets}){
const [isOpen, setIsOpen] = useState(false);
const [activeTimesheetId, setActiveTimesheetId] = useState(0)
const [modalTitle, setModalTitle] = useState("");
const [clockedInTime, setClockedInTime] = useState(null);
const [clockedOutTime, setClockedOutTime] = useState(null);
const readableTimesheetKeys = {id: 0, customerName: 1, clockedIn: 2, clockedOut: 3, duration: 4, employeeName: 5}
const flatpickrOptions = {
enableTime: true,
altInput: true,
altFormat: 'F j, h:i K',
minTime: "6:00",
maxTime: "22:00",
}
const editButtonClicked = (timesheetId, timesheetData) => {
console.log("Edit button clicked! About to hit debugger");
console.log("Time sheet Id: " + timesheetId);
console.log("Time sheet data: ");
console.log(timesheetData);
setActiveTimesheetId(timesheetId);
// let timesheetsData = timesheetsData
let foundTimesheet = timesheetData.find(timsheet => timsheet[0] == timesheetId)
//Why is it always indexing into 0..? That's not right. I think that's where this issue is coming from.
setModalTitle(`${foundTimesheet[readableTimesheetKeys["customerName"]] == null ? "" : foundTimesheet[readableTimesheetKeys["customerName"]]} -- ${foundTimesheet[readableTimesheetKeys["employeeName"]]}`);
const clockedIn = foundTimesheet[readableTimesheetKeys["clockedIn"]]
const clockedOut = foundTimesheet[readableTimesheetKeys["clockedOut"]]
console.log('clocked in: ' + clockedIn + ' clocked out: ' + clockedOut)
setClockedInTime(clockedIn)
setClockedOutTime(clockedOut)
setIsOpen(true);
}; //after a timesheet gets edited.. we need to modify the state somehow.
const saveTimesheetChangesToServer = () => {//This is a closure... I don't think it will work like this (I don't think this closure gets updated)
$.ajax({
type: 'PUT',
url: `/timesheets/${activeTimesheetId}.json`,
data: {timesheet: {id: activeTimesheetId, clocked_in: moment(clockedInTime).format("YYYY-MM-DDTHH:mm:ssZ"), clocked_out: moment(clockedOutTime).format("YYYY-MM-DDTHH:mm:ssZ")}},
success: (data) => {
console.log("Success: " + data);
setIsOpen(false);
gritterSuccess("Success", "Successfully Updated Timesheet", 10000);
const timesheetsWithEdits = [...timesheets];
for (let index = 0; index < timesheets.length; index++){
const timesheet = timesheets[index];
const modifiedThisTimesheet = timesheet["id"] == data.timesheet.id;
if (modifiedThisTimesheet){
timesheetsWithEdits[index] = data.timesheet;
}
}
setTimesheets(timesheetsWithEdits);
},
error: (data) => {
console.log("error: " + data);
gritterError("Warning", "Couldn't Update Timesheet", 10000);
},
})
}
return(
[
timesheet.id,
timesheet.customer_name,
timesheet.clocked_in == null ? "_" : moment(timesheet.clocked_in).format("MM/DD/YY hh:mm a"),
timesheet.clocked_out == null ? "_" : moment(timesheet.clocked_out).format("MM/DD/YY hh:mm a"),
formatDuration(timesheet.duration),
timesheet.employee_name])}
actions={{delete: true, edit: true}}
editModalOnClick={editButtonClicked}
/>
setIsOpen(false)} centered>
{modalTitle}
Clocked In:
{
console.log("datetime changed: " + date)
setClockedInTime(date);
}}
/>
Clocked Out:
{
console.log("datetime changed: " + date)
setClockedOutTime(date);
}}
/>
setIsOpen(false)}>
Close
Save Changes
)
}
import React, { Component, useState } from "react"
import SimpleToggleButtonGroup from "../shared/SimpleToggleButtonGroup";
import DateRangePicker from "../shared/DateRangePicker";
import JobTimesheetHeader from "./JobTimesheetHeader";
import EmployeeTimesheetHeader from "./EmployeeTimesheetHeader";
class Index extends Component{
constructor(props){
super(props);
this.state = {
selectedTimesheetViewType: 'Job',
}
}
render(){
const {job_timesheets, employee_timesheets, start_date, end_date} = this.props; //TODO: pass these in appropriately.
const job_timesheets_array = Object.entries(job_timesheets) //Triple nested array [[job_id, [{timesheet 1 hash}, {timesheet 2 hash}, etc..]]]
const employee_timesheets_array = Object.entries(employee_timesheets) //Triple nested array [[job_id, [{timesheet 1 hash}, {timesheet 2 hash}, etc..]]]
return(
{/* Collapsible Section for Each Job */}
{this.state.selectedTimesheetViewType == "Job" && job_timesheets_array.map((entry) => (
))}
{this.state.selectedTimesheetViewType == "Employee" && employee_timesheets_array.map((entry) => (
))}
{/* for each job, sorted by most recent to least recent. Then show each time card (sorted by employee alphabetically on that job), show the date, customer name, clocked in, clocked out, duration., edit pencil, delete. */}
{/* If it's view by job toggle.. show, what? A list of the jobs, and the timesheets nested underneath them that can be edited or deleted. */}
{/* for each employee (sorted alphabetically), show a title. Then show each time card (sorted by most recent to least recent), show the date, customer name, clocked in, clocked out, duration, edit pencil, delete. */}
{/* If it's view by employee toggle.. show a list of the employees, and the timesheets nested underneath them (summed), that can be edited or deleted.
It will be a table of the employees and the timesheets will get matched to the jobs. */}
)
}
}
export default Index;
import React, {useState, useEffect} from "react";
import TimesheetsTable from "./TimesheetTable";
export default function EmployeeTimesheetHeader({employeeId, timesheets}){
const [isOpen, setIsOpen] = useState(false);
const [employeeHeaderInfo, setEmployeeHeaderInfo] = useState(null);
const [duration, setDuration] = useState("");
const [currentTimesheets, setCurrentTimesheets] = useState(timesheets);
useEffect(() => { // An AJAX request to the server to request the employeeHeaderInfo that will have all sorts of info we need to render this component.
if (employeeId == "" || employeeId == null){
setEmployeeHeaderInfo({"employee_full_name": "Unassigned"})
}
else {
$.get(
{url: `/employees/${employeeId}/info`,
success: function(data){
console.log(data);
setEmployeeHeaderInfo(data);
}
})
}
}, [])
useEffect(() => {
setDuration(formatDuration(totalDurationForTheseTimesheets()));
}, [currentTimesheets])
const totalDurationForTheseTimesheets = () => {
const durationSum = currentTimesheets.map((timesheet) => timesheet.duration).reduce((sumSoFar, duration) => sumSoFar + duration);
return durationSum;
}
return(
import React, {useState, useEffect} from "react";
import TimesheetsTable from "./TimesheetTable";
export default function JobTimesheetHeader({jobId, timesheets}){
const [isOpen, setIsOpen] = useState(false);
const [jobHeaderInfo, setJobHeaderInfo] = useState(null);
const [duration, setDuration] = useState("");
const [currentTimesheets, setCurrentTimesheets] = useState(timesheets);
useEffect(() => { // An AJAX request to the server to request the jobHeaderInfo that will have all sorts of info we need to render this component.
if (jobId == "" || jobId == null){
setJobHeaderInfo({"customer_full_name": "General Time"})
}
else {
$.get(
{url: `/jobs/${jobId}.json`,
success: function(data){
console.log(data);
setJobHeaderInfo(data);
}
})
}
}, [])
useEffect(() => {
setDuration(formatDuration(totalDurationForTheseTimesheets()));
}, [currentTimesheets])
const totalDurationForTheseTimesheets = () => {
const durationSum = currentTimesheets.map((timesheet) => timesheet.duration).reduce((sumSoFar, duration) => sumSoFar + duration);
return durationSum;
}
return(
import React, {useState, useEffect} from "react"
import TableDataNotEditable from "../shared/TableDataNotEditable"
import Modal from 'react-bootstrap/Modal'
import Button from 'react-bootstrap/Button';
export default function LocationsTable({locations, setLocations}){
const [isOpen, setIsOpen] = useState(false);
const [isSaveButtonLoading, setIsSaveButtonLoading] = useState(false);
const [activeLocationId, setActiveLocationId] = useState(0);
const [streetAddress, setStreetAddress] = useState("");
const [city, setCity] = useState("");
const [zip, setZip] = useState(null);
const [state, setState] = useState(null);
const [modalTitle, setModalTitle] = useState("");
const editButtonClicked = (locationId, locationData) => {
console.log("Edit button clicked for location! About to hit debugger");
console.log("Location Id: " + locationId);
console.log("Location data: ");
console.log(locationData);
setActiveLocationId(locationId);
const selectedLocation = locations.find(location => location.id == locationId);
console.log("Selected Location: ", selectedLocation);
setStreetAddress(selectedLocation.street_address);
setCity(selectedLocation.city);
setZip(selectedLocation.zip_code);
setState(selectedLocation.state);
setModalTitle("Edit Location");
setIsOpen(true);
}; //after a timesheet gets edited.. we need to modify the state somehow.
const saveLocationChangesToServer = () => {//This is a closure... I don't think it will work like this (I don't think this closure gets updated)
setIsSaveButtonLoading(true);
$.ajax({
type: 'PUT',
url: `/locations/${activeLocationId}.json`,
data: {location: {id: activeLocationId, street_address: streetAddress, city: city, zip_code: zip, state: state}},
success: (data) => {
console.log("Success: " + data);
setIsOpen(false);
gritterSuccess("Success", "Successfully Updated Location", 10000);
const locationsWithEdits = [...locations];
for (let index = 0; index < locations.length; index++){
const location = locations[index];
const modifiedThislocation = location["id"] == data.id;
if (modifiedThislocation){
locationsWithEdits[index] = data;
}
}
setLocations(locationsWithEdits);
setIsSaveButtonLoading(false);
},
error: (data) => {
console.log("error: " + data);
gritterError("Warning", "Couldn't Update location", 10000);
setIsSaveButtonLoading(false);
},
})
}
return(
[
location.id,
location.street_address,
location.city,
location.zip_code,
location.is_billing_address == true ? "Is Billing" : {url: `/locations/${location.id}/make_default`, icon: 'fa-map-marked'}])}
actions={{delete: true, edit: true}}
editModalOnClick={editButtonClicked}
/>
setIsOpen(false)} centered>
{modalTitle}
{/* TODO: customer location fields/form. */}
Street Address:
setStreetAddress(e.target.value)} />
City:
setCity(e.target.value)} />
Zip:
setZip(e.target.value)} />
state:
setState(e.target.value)} />
setIsOpen(false)}>
Close
{isSaveButtonLoading ?
Saving...
:
Save Changes
}
)
}
import React, { Component, useState } from "react"
import LocationsTable from "./LocationsTable";
export default function LocationsIndex(props){
const [locations, setLocations] = useState(props.locations);
return(
)
}
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.resource }
{ data.action }
{ this._toolsColumnField(data) }
))
}
);
}
_toolsColumnField(data) {
const { displayTools } = this.props;
if( displayTools ) {
return(
);
} else {
return null;
}
}
}
export default TableBody;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { displayTools, tableData } = this.props;
return (
export default Table;
import React, { Component } from "react";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
const { displayTools } = this.props;
return (
Resource
Action
{ this._toolsColumnHeader() }
);
}
_toolsColumnHeader() {
const { displayTools } = this.props;
if( displayTools ) {
return();
} else {
return null;
}
}
}
export default TableHeader;
import React, { useEffect, useState, useRef, useCallback } from 'react';
import axios from 'axios';
import Modal from 'react-modal';
function BusinessImagesToAssign() {
const [images, setImages] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedImages, setSelectedImages] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
city: '',
zipCode: '',
streetAddress: '',
jobNotes: '',
jobTitle: '',
price: '',
});
const [page, setPage] = useState(1);
const observer = useRef();
const galleryStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))',
gridGap: '15px',
padding: '15px',
};
const imgStyle = {
width: '100%',
height: 'auto',
border: '1px solid #ccc',
boxShadow: '2px 2px 6px 0px rgba(0,0,0,0.2)',
borderRadius: '4px',
objectFit: 'cover',
};
useEffect(() => {
loadImages(page);
}, [page]);
const loadImages = (page) => {
setIsLoading(true);
axios
.get(`/business_images?page=${page}`)
.then((response) => {
setImages((prevImages) => [...prevImages, ...response.data]);
setIsLoading(false);
})
.catch((error) => {
console.error('Error fetching images', error);
setIsLoading(false);
});
};
const lastImageElementRef = useCallback((node) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setPage((prevPage) => prevPage + 1);
}
});
if (node) observer.current.observe(node);
}, [isLoading]);
const imagePressed = (index) => {
const indexInSelectedImages = selectedImages.indexOf(index);
if (indexInSelectedImages !== -1) {
setSelectedImages(selectedImages.filter((_, i) => i !== indexInSelectedImages));
} else {
setSelectedImages([...selectedImages, index]);
}
};
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
const selectedImageIds = selectedImages.map((index) => images[index].attachment_id);
axios
.post('/assign_images_to_new_job', {
...formData,
selectedImages: selectedImageIds,
})
.then((response) => {
closeModal();
gritterSuccess("Success!", "Images assigned to new job. Moving on to publishing.");
let invoiceId = response.data.invoice.id;
window.location.href = `/published_job_pages/new?invoice_id=${invoiceId}`;
})
.catch((error) => {
console.error('Error assigning images to new job', error);
});
};
return (
Loading...
}export default BusinessImagesToAssign;
import React, {Component} from "react";
class EventsIndex extends Component{
constructor(props){
super(props);
this.state = {
events: props.events,
business: props.business,
}
this.fetchFilteredEvents = this.fetchFilteredEvents.bind(this);
}
fetchFilteredEvents = () => {
let selectedBusinessId = $('#select_business').val()
let url = isBlank(selectedBusinessId) ? '/events.json' : `/events.json?business_id=${selectedBusinessId}`;
$.get({
url: url,
params: {business_id: 1},
success: (data) => {
this.setState({
...this.state,
events: data
});
}
});
$.get({
url: '/businesses/business_info.json',
params: {business_id: 1},
success: (data) => {
this.setState({
...this.state,
business: data
});
}
})
}
render(){
const { events, businesses, employees } = this.props;
// Sort businesses by 'created_at' in descending order
businesses.sort((a, b) => {
if (a.created_at > b.created_at) return -1;
if (a.created_at < b.created_at) return 1;
return 0;
});
let businessOptions = businesses.map((business) =>
{business.name} - {business.id}
);
let employeeOptions = employees.map((employee) =>
{employee.full_name}
);
let eventRows = this.state.events.map((event) =>
// let eventRows = events.reverse().map((event) =>
{event.business_name}
{event.employee_name}
{ event.name}
{ event.resource}
{ event.resource_id}
{ evaluateReadableDateTime(event.created_at)} {event.created_at}
);
return(
{/* {this.state.business != null
?
: null
} */}
Business
Employee Name
Event Type
Resource
Resource ID
Time of Event
{eventRows}
);
}
}
export default EventsIndex;
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.display_address }
{ this._toolsField(data) }
))
}
);
}
_toolsField(data) {
const { permissions } = this.props;
console.log("Permissions: " + permissions);
if( permissions["destroy"] && permissions["update"]) {
return (
);
} else if( permissions["destroy"] ) {
return (
);
} else if( permissions["update"] ) {
return (
);
} else {
return (
);
}
}
}
export default TableBody;
import React, { Component } from "react";
import Table from "./Table";
import TableTop from "./TableTop";
class Index extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Index;
import React, {Component} from "react"
class InvoiceOnCard extends Component {
constructor(props){
super(props);
}
render(){
const {invoice} = this.props
return (
getColor(){
const {invoice} = this.props
if (invoice.invoice_type == "Invoice"){
if (invoice.amount_due > 0){
return "red"
} else {
return "green"
}
} else {
return "blue"
}
}
}
export default InvoiceOnCard;
import React, {Component} from "react"
class AppointmentOnCard extends Component{
constructor(props){
super(props);
}
render(){
const {appointment} = this.props;
const appointmentTitle = appointment.appointment_type_name != "No Type" ? appointment.appointment_type_name : ""
return(
export default AppointmentOnCard;
import React, { Component } from "react";
import JobCard from "./JobCard";
class JobIndex extends Component {
constructor(props) {
super(props);
}
render() {
const { data } = this.props;
const responsiveStyleWrapper = {
overflowX: "auto"
}
return (
{
data.map((jobData, index) => (
))
}
);
}
}
export default JobIndex;
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Table;
import React, {Component} from "react";
import ClickableCard from "../shared/ClickableCard";
import InvoiceOnCard from "./InvoiceOnCard"
import AppointmentOnCard from "./AppointmentOnCard"
class JobCard extends Component {
constructor(props){
super(props);
}
render(){
const { jobData } = this.props;
var invoiceSection = null;
if (jobData.invoices != null && jobData.invoices.length > 0){
invoiceSection =
{
jobData.invoices.map((invoice, index) => (
))
}
;
}
var appointmentSection = null;
if (jobData.appointments != null && jobData.appointments.length > 0){
appointmentSection =
{
jobData.appointments.map((appointment, index) => (
))
}
;
}
function openJobModal(){
console.log("Job Clicked!");
}
//Job Title Job Owner
//Address
//Invoices...
//Invoice amount (Red if owed, Green if paid, Blue if estimate) Invoice Number/Name
//Appointments..
//Appointments Appointment Type, Date, Start Time, End Time, Employee
const jobContent = (
(
{appointmentSection}
export default JobCard;
import React, { Component } from "react";
import SearchField from "../shared/SearchField";
class TableTop extends Component {
constructor(props) {
super(props);
}
render() {
return (
_newJobLink() {
const { permissions } = this.props;
if( permissions["create"] ) {
return();
} else {
return null;
}
}
}
export default TableTop;
import React, { useState, useEffect } from 'react';
const SEOForm = ({ invoice, images }) => {
const [notes, setNotes] = useState(invoice?.notes || '');
const [keywords, setKeywords] = useState('');
const [customerName, setCustomerName] = useState(invoice?.customer?.first_name || '');
const [address, setAddress] = useState(`${invoice?.customer?.location?.city}, ${invoice?.customer?.location?.state}` || '');
const [selectedImages, setSelectedImages] = useState([]);
useEffect(() => {
debugger;
}, [
])
const handleImageCheck = (image, isChecked) => {
if (isChecked) {
setSelectedImages([...selectedImages, image]);
} else {
setSelectedImages(selectedImages.filter(img => img !== image));
}
};
const handleSubmit = (e) => {
e.preventDefault();
// Here you can call the method to post data to your Rails backend
console.log({ notes, keywords, customerName, address, selectedImages });
};
return (
export default SEOForm;
import React, {Component} from "react";
import BillToDifferentCustomer from "../invoices/BillToDifferentCustomer";
class AddressModal extends Component {
constructor(props){
super(props);
}
render() {
const { addressModalShow, modalTitle } = this.props
return (
<>
{
addressModalShow &&
// Filename - Progress_bar.js
import React from 'react'
const ProgressBar = ({bgcolor,progress,height}) => {
const Parentdiv = {
height: height,
width: '80%',
backgroundColor: 'whitesmoke',
borderRadius: 40,
margin: 50
}
const Childdiv = {
height: '100%',
width: `${progress}%`,
backgroundColor: bgcolor,
borderRadius:40,
textAlign: 'right'
}
const progresstext = {
padding: 10,
color: 'black',
fontWeight: 900
}
return (
import React, { Component } from "react";
class SortLink extends Component {
constructor(props) {
super(props);
}
render() {
const { name, url, dbParamName } = this.props;
const paramName = dbParamName || this._paramName();
const direction = (this._checkURL(`${paramName}+asc`)) ? "desc" : "asc";
return (
{ `${name} ${this._arrowIcon()}` }
);
}
/**
* Returns an arrow icon if page is sorted on this field.
* Returns a blank string otherwise.
*
* @private
*/
_arrowIcon() {
const { dbParamName } = this.props;
const paramName = dbParamName || this._paramName();
if( this._checkURL(`${paramName}+asc`) ) {
return " ▲";
}
if( this._checkURL(`${paramName}+desc`) ) {
return " ▼";
}
return "";
}
/**
* Returns true if URL contains value, else returns false.
* @private
*/
_checkURL(value) {
return window.location.href.toLowerCase().includes(value);
}
/**
* Returns url parameter name for this link.
*
* @private
*/
_paramName() {
const { name } = this.props;
return name.toLowerCase().replace(" ", "_");
}
}
export default SortLink;
import React, { Component } from 'react';
class SearchField extends Component {
constructor(props) {
super(props);
}
render() {
const { action, search } = this.props;
return (
);
}
}
export default SearchField;
import React, { Component } from 'react';
import TableData from "./TableData";
import DateRangePicker from "./DateRangePicker";
class IndexPage extends Component {
constructor(props){
super(props);
this.openSearchBar = this.openSearchBar.bind(this);
}
openSearchBar() {
const search = prompt("Search");
const data = document.getElementsByClassName('tdData');
if (search != null) {
for (let d = 0; d < data.length; d++) {
const text = data[d].innerText.split(" ");
for (let i = 0; i < text.length; i++) {
let index = text[i];
let splitIndex = index.split("");
if (index.toLowerCase().includes(search.toLowerCase())) {
for (let si = 0; si < index.length; si++) {
if (search.toLowerCase().includes(index[si].toLowerCase())) {
splitIndex[si] = `${index[si]}`;
text[i] = splitIndex.join("");
}
}
}
}
data[d].innerHTML = text.join(" ");
}
}
}
render(){
const {title, attributes, data, basePath, updateProduct, removeProduct, handleProductPriceSorting, handleProductNameSorting, showNewProductButton} = this.props;
return(
);
}
}
export default IndexPage;
import React, {Component} from 'react';
class ClickableCard extends Component {
constructor(props){
super(props);
}
render(){
const { content, onClick } = this.props;
return(
export default ClickableCard;
import React, { Component } from "react";
import ContentEditable from "react-contenteditable"
/**
* The contractor who edited this recently broke the single responsibility issue and did not keep the proper level of abstraction here.
* So now, this component is tightly coupled with the "product" table, thus breaking our abstraction and introducing a whole host of issues.
* So this class should not be reused by other components until it is uncoupled and we put the correct level of abstraction back to it.
*/
class TableData extends Component{
constructor(props){
super(props)
this.addNewProduct = this.addNewProduct.bind(this);
this.saveProductsData = this.saveProductsData.bind(this);
this.removeProductsRow = this.removeProductsRow.bind(this);
this.productAttrUpdate = this.productAttrUpdate.bind(this);
}
addNewProduct(){
const { updateProduct } = this.props;
const data = [
"",
{
"title": "",
"url": "products/"
},
"",
0,
""
];
updateProduct(data);
}
removeProductsRow(event) {
const { removeProduct, data } = this.props;
const { index:productTotalIndex } = $(event.currentTarget).parents('tr')?.data()
removeProduct(productTotalIndex)
}
render(){
const {title, attributes, data, basePath, actions, showNewProductButton } = this.props;
const responsiveStyleWrapper = {
overflowX: "auto"
}
var showActions = actions;
if (showActions == null){ //These are the default actions
showActions = {delete: true, edit: true, view: true}
}
const tableHeaders = this._buildTopRow(attributes);
const tableData = this._buildTableBodyRows(data, basePath, showActions);
return(
_buildTopRow(attributes){
const { handleProductNameSorting, handleProductPriceSorting } = this.props;
const tableHeaders = [];
for (const [index, value] of attributes.entries()){
const titleizedValue = value.replace(/^\w/, c => c.toUpperCase());
tableHeaders.push(
{
value == 'name' || value == 'price'
? handleProductNameSorting()
: () => handleProductPriceSorting()
}
style={{
textDecoration: "none",
color: "#555555"
}}
>
{titleizedValue} ⇵
: titleizedValue
}
);
};
return tableHeaders;
}
_buildTableBodyRows(data, basePath, actions){
const tableData = [];
for (const [index, value] of data.entries()){
const row = [];
const productId = value[0]; //First element is always the id
for (const [objectIndex, objectValue] of value.entries()){
const lookingAtObjectsId = objectIndex == 0;
const lookingAtUrlObject = objectValue != null && objectValue["url"] != null;
let attrName = '';
if (lookingAtObjectsId){ //We don't want to add a just for the ID.
continue;
}
else if (lookingAtUrlObject){
const openInNewTab = objectValue["newtab"] != null;
const icon = objectValue["icon"] == null ? null : ( {objectValue["title"]} );
const iconFullClass = objectValue["iconclass"] == null ? null : ( {objectValue["title"]} ); // This extra one is because some icons don't start with 'fas', they might start with 'fab', like in the case with the stripe icon.
const displayText = icon == null && iconFullClass == null ? objectValue["title"] : icon || iconFullClass
const linkClass = objectValue["class"] != null ? objectValue["class"] : "";
const hover = objectValue["hover"] != null ? objectValue["hover"] : "";
const target = openInNewTab ? "_blank" : "";
// row.push( {displayText} )
row.push()
}
else {
if(objectIndex == 2){
attrName = 'description';
} else if(objectIndex == 3){
attrName = 'price';
} else if(objectIndex == 4){
attrName = 'SKU';
} else{
attrName = '';
}
row.push(
)
}
}
row.push(
this.actionsTd(productId, basePath, actions)
);
tableData.push({row})
};
return tableData;
}
actionsTd(productId, basePath, actions) {
if(!productId)
{
return
var deleteElement = null;
if (actions.delete){
deleteElement =
(
return
productAttrUpdate(event){
const { updateProduct } = this.props;
const id = $(event.currentTarget).parents('tr').attr('id')
const attr = event.currentTarget.id;
const attrVal = event.currentTarget.innerText;
const attrName = attr.charAt(0).toUpperCase() + attr.slice(1);
const { index:productTotalIndex } = $(event.currentTarget).parents('tr')?.data();
if(id != '') {
$.ajax({
url: `/products/${id}/update_attribute`,
method: "PUT",
data: {attribute: attr, value: attrVal},
success: function(data){
gritterSuccess("Success", `${attrName} Updated Successfully`);
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
})
}
const newData = [...this.props.data[productTotalIndex]];
const colIndex = $(event.currentTarget).parent('td').attr('data-index');
colIndex == 1
? newData[colIndex] = {
title: event.currentTarget.innerText,
url: '/products'
}
: newData[colIndex] = event.currentTarget.innerText;
updateProduct(newData, productTotalIndex);
}
saveProductsData(event) {
const createButton = event.currentTarget;
createButton.disabled = true;
const { updateProduct } = this.props;
const currentRow = $(createButton).parents('tr').children();
const pName = currentRow[0].innerText;
const pDesc = currentRow[1].innerText;
const pPrice = currentRow[2].innerText;
const pSku = currentRow[3].innerText;
const { index:productTotalIndex } = $(createButton).parents('tr')?.data();
const product = {name: pName, description: pDesc, price: pPrice, SKU: pSku}
$.ajax({
url: `/products.json`,
method: "POST",
data: {product: product},
success: function(data){
const newData = [
data.id,
{
"title": data.name,
"url": `products/${data.id}`
},
data.description,
data.price,
data.SKU
];
updateProduct(newData, productTotalIndex);
gritterSuccess("Success", `Product Added Successfully`);
},
error: function(data){
createButton.disabled = false;
gritterError("Error", "Something Error", "10000");
}
})
}
}
export default TableData;
import React from "react";
const FRPillButton = ({onClick, text, disabled=false}) => {
return(
disabled
?
{text}
:
{text}
)
}
export default FRPillButton;
import React, {Component} from 'react';
class InplaceEdit extends Component{
constructor(props){
super(props);
this.onFocusOut = this.onFocusOut.bind(this);
this.updateUrl = this.updateUrl.bind(this);
}
render(){
const {attribute, resource, resource_id, value, allowNewline} = this.props;
let inplaceId = `inplace-${resource}-${attribute}-${resource_id}`;
this.val = value;
return (
{e.key === 'Enter' && e.preventDefault();}}>
{value}
);
}
/**
* Will send a 'PUT' to the update attribute url with the new data
*/
onFocusOut(event){
const newVal = event.currentTarget.innerText;
if (newVal == this.val){
return
}
else {
this.val = newVal;
}
$.ajax({
type: 'PUT',
url: this.updateUrl(),
data: {attribute: this.props.attribute, value: event.currentTarget.innerText},
success: () => {
var successMessage = `Successfully set ${this.titleizeAttribute(this.props.attribute)} to: "${this.val}"`;
if (this.val.length == 0){
successMessage = `Successfully cleared ${this.titleizeAttribute(this.props.attribute)}`;
}
console.log(successMessage);
gritterSuccess("Successful Update", successMessage);
},
error: function(data){
const failureMessage = `Failed to set: ${this.titleizeAttribute(this.props.attribute)}`;
console.log(failureMessage);
gritterError("Failed Update", failureMessage);
},
})
}
/**
* Gets the update attribute URL based on the given resource and resource id.
*/
updateUrl(){
return `/${this.props.resource}/${this.props.resource_id}/update_attribute.json`;
}
/**
* Takes a rails formatted attribute and returns the titleized verison of it.
* For example: "first_name" => "First Name", "notes" => "Notes"
* @param {*} attr
*/
titleizeAttribute(attr){
if (typeof attr != "string")
return attr
return attr.split('_').map(function(val){return val.charAt(0).toUpperCase() + val.slice(1)}).join(' ');
}
}
export default InplaceEdit;
import React, { Component } from 'react';
class DateRangePicker extends Component {
constructor(props){
super(props);
const { startDate, endDate } = this.props;
this.state = {
startDate: startDate || '',
endDate: endDate || '',
isShowButtonLoading: false
}
this.onStartDateChange = this.onStartDateChange.bind(this)
this.onEndDateChange = this.onEndDateChange.bind(this)
this.onShowClick = this.onShowClick.bind(this)
}
componentDidMount() {
const { startDateId, endDateId } = this.props;
const { startDate, endDate } = this.state
flatpickr(`#${startDateId}`, {
altFormat: "Y-m-d",
onChange: this.onStartDateChange
});
flatpickr(`#${endDateId}`, {
altFormat: "Y-m-d",
onChange: this.onEndDateChange
});
if(startDate && endDate){
SetFlatpickr(startDateId, flatpickr.parseDate(startDate, "Y-m-d" ));
SetFlatpickr(endDateId, flatpickr.parseDate(endDate, "Y-m-d"));
}
}
onStartDateChange(selectedDates, date_str, instance) {
this.setState({ ...this.state, startDate: date_str })
}
onEndDateChange(selectedDates, date_str, instance) {
this.setState({ ...this.state, endDate: date_str })
}
onShowClick() {
const { path } = this.props;
const { startDate, endDate } = this.state;
this.setState({...this.state, isShowButtonLoading: true})
window.location.href = `${path}?start_date=${startDate}&end_date=${endDate}`;
}
render() {
const { enableToggle, startDateId, endDateId } = this.props;
const { startDate, endDate } = this.state;
return(
import React, {Component} from "react";
import LogoPath from "images/fieldrocketlogo.png"
const FieldRocketLogo = () => {
return(
export default FieldRocketLogo;
import React, {useState} from "react";
const InputField = ({id, label, value, setValue, isValid=true, onValidate=null, type="text", invalidMessage=""}) => {
const baseClass = `form-control ${type=="tel" ? "phone_mask" : ''}`
const [inputClassName, setInputClassName] = useState(baseClass);
const addValidCSSClassOnBlur = (isValidated) => {
if (isValidated){
setInputClassName(baseClass + " is-valid");
}
else {
setInputClassName(baseClass + " is-invalid");
}
}
return(
export default InputField;
import React, {useState} from "react";
import InputField from "./InputField";
const EmailInputField = ({id, email, setEmail}) => {
const [emailIsValid, setEmailIsValid] = useState(true);
const [invalidMessage, setInvalidMessage] = useState("");
const validateEmailInput = (userInputtedEmail) => {
if (isValidEmail(userInputtedEmail)){
console.log(`Email ${userInputtedEmail} is valid!`)
setEmailIsValid(true);
return true;
}
else {
console.log(`Email ${userInputtedEmail} is NOT valid!`)
setEmailIsValid(false);
setInvalidMessage("Email: Please enter a valid email address.");
return false;
}
}
return(
)
}
export default EmailInputField;
import React, {Component} from "react";
class AddressGenericModal extends Component {
constructor(props){
super(props);
this.state = {
showDisplayAddressOptions: false,
addressFormShow: false,
addressFormData: {
streetAddress:'',
id:'',
customerId:'',
city:'',
zipCode:'',
state:'',
}
}
this.handleAddressFormShow = this.handleAddressFormShow.bind(this);
// this.addNewAddressForm = this.addNewAddressForm.bind(this);
// this.removeNewAddressForm = this.removeNewAddressForm.bind(this);
this.openAddressMenu = this.openAddressMenu.bind(this);
this.editLocation = this.editLocation.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
// addNewAddressForm(event){
// this.setState({
// ...this.state,
// addressFormShow: true,
// })
// }
// removeNewAddressForm(event){
// this.setState({
// ...this.state,
// addressFormShow: false,
// })
// }
openAddressMenu(event){
$(".address-menu").css('display','none');
$(event.currentTarget).parent('td').find('.address-menu').toggle();
}
editLocation(data){
$(".address-menu").css('display','none');
const formData = {
streetAddress: data?.street_address || '',
id: data?.id || '',
customerId: data?.customer_id || '',
city: data?.city || '',
zipCode: data?.zip_code || '',
state: data?.state || '',
}
this.setState({
...this.state,
addressFormShow: true,
addressFormData: formData
})
}
handleInputChange(event){
const value = event?.currentTarget?.value
const { name:dataName } = event?.currentTarget?.dataset;
const updatedFormData = { ...this.state.addressFormData }
updatedFormData[dataName] = value;
this.setState({ ...this.state, addressFormData: updatedFormData })
}
handleAddressFormShow(showForm, data={}) {
const { id, street_address, customer_id, city, zip_code, state } = data;
showForm
? this.setState({
addressFormShow: true,
addressFormData: {
streetAddress: street_address || '',
id: id || '',
customerId: customer_id || '',
city: city || '',
zipCode: zip_code || '',
state: state || '',
}
})
: this.setState({
addressFormShow: false,
addressFormData: {
streetAddress:'',
id:'',
customerId:'',
city:'',
zipCode:'',
state:'',
}
})
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside(event) {
if (event.target.className == 'address-context-menu-link' || $(event.target)?.parent('ul')[0]?.className == 'custom-menu address-menu') {
} else{
$(".address-menu").css('display','none');
}
}
render() {
const { genericModalShow, modalTitle, customerLocations, serviceAddress} = this.props
const { addressFormShow, addressFormData } = this.state
const availableCutomerLocations = customerLocations?.filter( locationData =>locationData.id != serviceAddress.id)
return (
import React, {Component} from "react";
import { ToggleButtonGroup, ToggleButton } from "@mui/material";
class SimpleToggleButtonGroup extends Component{
constructor(props){
super(props);
this.state = {
selectedValue: this.props.items[0]
}
this.setSelectedValue = this.setSelectedValue.bind(this);
}
setSelectedValue = (changeEvent) => {
const newValue = changeEvent.target.value;
this.setState({...this.state, selectedValue: newValue})
if (this.props.onChange != null){
this.props.onChange(newValue);
}
}
render(){
const {items} = this.props;
return(
{items.map((item) => (
{item}
))}
)
}
}
export default SimpleToggleButtonGroup;
import React, { Component } from "react";
class TableDataNotEditable extends Component{
constructor(props){
super(props)
}
render(){
const {title, attributes, data, basePath, actions, editModalOnClick } = this.props;
const responsiveStyleWrapper = {
overflowX: "auto"
}
var showActions = actions;
if (showActions == null){ //These are the default actions
showActions = {delete: true, edit: true, view: true}
}
const tableHeaders = this._buildTopRow(attributes);
const tableData = this._buildTableBodyRows(data, basePath, showActions, editModalOnClick);
return(
_buildTopRow(attributes){
const tableHeaders = []
for (const [index, value] of attributes.entries()){
const titleizedValue = value.replace(/^\w/, c => c.toUpperCase());
tableHeaders.push({titleizedValue});
};
return tableHeaders;
}
_buildTableBodyRows(data, basePath, actions, editModalOnClick){
const tableData = [];
for (const [index, value] of data.entries()){
const row = [];
const productId = value[0]; //First element is always the id
for (const [objectIndex, objectValue] of value.entries()){
const lookingAtObjectsId = objectIndex == 0;
const lookingAtUrlObject = objectValue != null && objectValue["url"] != null;
if (lookingAtObjectsId){ //We don't want to add a just for the ID.
continue;
}
else if (lookingAtUrlObject){
const openInNewTab = objectValue["newtab"] != null;
const icon = objectValue["icon"] == null ? null : ( {objectValue["title"]} );
const iconFullClass = objectValue["iconclass"] == null ? null : ( {objectValue["title"]} ); // This extra one is because some icons don't start with 'fas', they might start with 'fab', like in the case with the stripe icon.
const displayText = icon == null && iconFullClass == null ? objectValue["title"] : icon || iconFullClass
const linkClass = objectValue["class"] != null ? objectValue["class"] : "";
const hover = objectValue["hover"] != null ? objectValue["hover"] : "";
const target = openInNewTab ? "_blank" : "";
row.push( {displayText} )
}
else {
row.push({objectValue})
}
}
row.push(
this.actionsTd(productId, basePath, actions, editModalOnClick, data)
);
tableData.push({row})
};
return tableData;
}
/**
* Builds the Actions Table Data Column with the view, edit, and delete icons.
* @param {*} productId The id of the resource that is being shown
* @param {*} basePath The path of the resource used for viewing, editing, and POSTing a delete. (for the default actions of viewing, editing, or deleting)
* @param {*} actions The actions that are enabled for this resource row (sometimes you don't want to show viewing, or editing, or delete, sometimes you want to show all, sometimes you want to show none.)
* @param {*} editModalOnClick This is a callback used to show a modal if we are going to allow editing this resource directly on this same page, using a modal.
* @param {*} data Pass in the data for this resource so we can set the data in the modal fields so the user can see it before making a change.
* @returns
*/
actionsTd(productId, basePath, actions, editModalOnClick, data) {
var viewElement = null;
if (actions.view){
viewElement =
(
var deleteElement = null;
if (actions.delete){
deleteElement =
(
return
export default TableDataNotEditable;
import React, { Component } from 'react';
class TableBody extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData } = this.props;
return (
{
tableData.map((data, index) => (
{ data.name }
{ this._tools(data) }
))
}
);
}
_tools(data) {
const { permissions } = this.props;
if( permissions["destroy"] && permissions["update"] ) {
return(
);
} else if( permissions["destroy"] ) {
return(
);
} else if( permissions["update"] ) {
return(
);
} else {
return(
);
}
}
}
export default TableBody;
import React, { Component } from "react";
import Table from "./Table";
import TableTop from "./TableTop";
class Index extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Index;
Error reading file: U_STRING_NOT_TERMINATED_WARNING
import React, { Component } from 'react';
import TableBody from "./TableBody";
import TableHeader from "./TableHeader";
class Table extends Component {
constructor(props) {
super(props);
}
render() {
const { tableData, permissions } = this.props;
return (
export default Table;
import React, { Component } from "react";
import SortLink from "../shared/SortLink";
class TableHeader extends Component {
constructor(props) {
super(props);
}
render() {
return (
Roster Name
);
}
}
export default TableHeader;
import React, { Component } from "react";
class TableTop extends Component {
constructor(props) {
super(props);
}
render() {
return (
_newLink() {
const { permissions } = this.props;
if( permissions["create"] ) {
return( );
} else {
return null;
}
}
}
export default TableTop;
import React, {Component} from 'react';
import IndexPage from '../shared/IndexPage';
class ProductsIndexPage extends Component { //this might be a good candidate for polymorphism and just inheriting from the parent IndexPage
constructor(props){
super(props);
const {title, attributes, data, basePath} = this.props;
this.state={
data: data,
paidDirection: 'asc'
}
this.updateProduct = this.updateProduct.bind(this);
this.removeProduct = this.removeProduct.bind(this);
this.handleProductPriceSorting = this.handleProductPriceSorting.bind(this);
this.handleProductNameSorting = this.handleProductNameSorting.bind(this);
}
updateProduct(newData, index = null) {
index || index == 0
? this.setState({
...this.state,
data: [
...this.state.data.slice(0, index),
newData,
...this.state.data.slice(index+1, this.state.data.length)
]
})
: this.setState({ ...this.state, data: [...this.state.data, newData]})
}
removeProduct(index) {
this.setState({
...this.state,
data: [
...this.state.data.slice(0, index),
...this.state.data.slice(index+1, this.state.data.length)
]
})
}
handleProductPriceSorting(){
const paidDirection = this.state.paidDirection;
const updatedPaidDirection = paidDirection == 'asc' ? 'desc' : 'asc'
const sortBy = (a, b) => paidDirection == 'asc' ? (a[3] - b[3]) : (b[3] - a[3])
const tableData1 = [...this.state.data].sort(sortBy);
this.setState({
...this.state,
data: [...tableData1],
paidDirection: updatedPaidDirection,
});
}
handleProductNameSorting(){
const paidDirection = this.state.paidDirection;
const updatedPaidDirection = paidDirection == 'asc' ? 'desc' : 'asc'
const sortBy = paidDirection == 'asc'
? (a, b) => (a[1].title > b[1].title ? -1 : 1)
: (a, b) => (a[1].title < b[1].title ? -1 : 1)
const tableData1 = [...this.state.data].sort(sortBy);
this.setState({
...this.state,
data: [...tableData1],
paidDirection: updatedPaidDirection,
});
}
render(){
const {title, attributes, basePath} = this.props;
const { data } = this.state;
const openModal = () => {
$('#import_products_services_modal').modal('show');
}
const importButton = (
// here is where we'll have the import modal
return(
);
}
}
export default ProductsIndexPage;
module JobsHelper
def customer_options
current_business.customers.map do |c|
[c.full_name.titleize, c.id]
end
end
def customer_search_label_style(job)
form_group_error?(job, :customer_id) ? 'color: #dc3545; display: block;' : 'display: block;'
end
def employee_options
current_business.employees.map do |e|
[e.full_name, e.id]
end
end
def roster_options
current_business.rosters.map do |e|
[e.name, e.id]
end
end
def job_status(job)
if job.job_status.nil?
'Job State Unknown'
else
job.job_status
end
end
def job_status_options()
return [
['1', 'On Site Sales Call'],
['2', 'Lost Estimate'],
['3', 'Work Order'],
['4', 'Completed'],
]
end
def invoice_to_customer_full_name(job)
job.customer.full_name.blank? ? "Company: #{job.customer.company}" : job.customer.full_name.titleize
end
end
module JobStatusesHelper
end
module SubscriptionsHelper
end
module LocationsHelper
end
module TimesheetsHelper
end
module ReviewsHelper
end
module InvoiceTemplatesHelper
def invoice_data(editable)
set_data(default_data, editable, false)
end
def invoice_template_data(editable)
set_data(default_data, editable, true)
end
def set_data(data, editable, is_invoice_template=true)
data[:issuedDate] = editable ? @invoice.issued_on.strftime("%Y-%m-%d") : @invoice.issued_on.strftime("%B %d, %Y")
data[:dueDate] = editable ? @invoice.due_on.strftime("%Y-%m-%d") : @invoice.due_on.strftime("%B %d, %Y")
data[:editable] = editable
data[:handlingInvoiceTemplate] = is_invoice_template
if current_business.present?
data[:productOptions] = current_business.products.map {|product| {value: product.id, label: product.name, data_description: product.description, data_price: product.price}}
data[:invoiceTemplates]= current_business.invoice_templates.includes([logo_attachment: :blob]).map do |template| {
value: template.id,
label: template.name,
notes: template.description,
business_name: template.business_name,
logo: template&.logo&.attached? ? url_for(template.logo) : '',
line_items: LineItemTemplate.where(invoice_template_id: template.id)}
end #line items..?? logo..? and Business Name..?
end
if is_invoice_template
data[:lineItems] = @invoice.line_items
data[:alternateLogoFromTemplate] = @invoice&.logo&.attached? ? url_for(@invoice.logo) : ''
data[:invoiceTemplateId] = @invoice.invoice_display_number
else
data[:serviceTo] = @invoice.service_to
attached_invoice_tempalte = @invoice.invoice_template
data[:alternativeBusinessName] = attached_invoice_tempalte&.business_name || ''
data[:alternateLogoFromTemplate] = attached_invoice_tempalte&.logo&.attached? ? url_for(attached_invoice_tempalte.logo) : ''
end
data
end
def default_data
customer_to_invoice = @invoice.customer_to_invoice
{
invoice: @invoice,
dueDateLabel: invoice_due_text(@invoice),
logo: @invoice.business.logo.attached? ? url_for(@invoice.business.logo) : image_url("homeservice.png"),
showDueDate: show_due_date?(@invoice),
business: @invoice.business,
businessPhone: number_to_phone(@invoice.business.phone),
owner: !@invoice.job&.owner&.nil?,
ownerFullName: @invoice.job&.owner&.full_name,
ownerEmail: @invoice.job&.owner&.email,
ownerPhone: @invoice.sales_rep_number_display,
customerToInvoice: customer_to_invoice,
shouldInvoiceOtherCustomer: @invoice.should_invoice_other_customer?,
isCustomerAddressDifferentThanJobAddress: !@invoice.is_customer_address_different_than_job_address?,
locationDisplayAddress: @invoice.job&.location&.display_address != nil,
customerToInvoiceFullName: customer_to_invoice&.full_name,
customerToInvoiceDisplayAddress: customer_to_invoice&.display_address,
customerToInvoicePhone: number_to_phone(customer_to_invoice&.phone),
invoiceJobDisplayAddress: @invoice.job&.display_address,
serviceToFullName: @invoice.service_to&.full_name,
serviceToDisplayAddress: @invoice.service_to&.display_address,
serviceToPhone: number_to_phone(@invoice.service_to&.phone),
invoiceType: @invoice.invoice_type,
job: @invoice.job,
customerLocations: @invoice.job&.customer&.locations,
estimateNumber: @estimate_number || @invoice.invoice_display_number, #invoice controller doesn't set this for the 'edit' action, so just default to invoice_display_number
invoiceNumber: @invoice_number || @invoice.invoice_display_number #view above comment
}
end
end
module InvoicesHelper
def template_options
current_business.invoice_templates.map do |template|
[template.name, template.id]
end
end
def show_due_date?(invoice)
return invoice.invoice_type == "Invoice" || invoice.invoice_type == "Estimate"
end
def invoice_due_text(invoice)
if invoice.invoice_type == "Invoice"
"Due"
elsif invoice.invoice_type == "Estimate"
"Expires"
else
return ""
end
end
end
module ForwardingNumbersHelper
end
module RostersHelper
end
module JobRequestsHelper
end
module AppointmentColorsHelper
end
module PermissionsHelper
def action_options
Permission::ACTIONS.map do |a|
[a.capitalize, a]
end
end
def resource_options
Permission::RESOURCES[1..-1].map do |r|
[r.capitalize, r]
end
end
end
module VisitorDeviceInfosHelper
end
module CustomerContactHelper
end
module TextTemplatesHelper
# //used in evaluating the text message template variable: {{start_time}}
# matches equivalent in helpers.js
def evaluate_start_time_for_text_message(start_datetime)
if start_datetime == nil || start_datetime == ""
return ""
end
return start_datetime.utc.strftime("%I:%M %P")
end
# //used in evaluating the text message template variable: {{readable_start_time}}
# matches evaluateReadableStartTimeFromAppointment in helpers.js
def evaluate_readable_start_time_for_text_message(start_datetime)
if start_datetime == nil || start_datetime == ""
return ""
end
hhmma = start_datetime.utc.strftime("%I:%M %P")
date = start_datetime.strftime("%a, %d %b %Y").to_time.to_i
is_same_day = date == (Date.today).to_time.to_i
is_next_day = date == (Date.tomorrow).to_time.to_i
is_next_week = (date > (Date.today).to_time.to_i) && (date <= (Date.today + 7).to_time.to_i)
was_yesterday = date == (Date.yesterday).to_time.to_i
was_last_week = (date > (Date.today - 7).to_time.to_i) && (date < (Date.yesterday).to_time.to_i)
if is_same_day
return "today at #{hhmma}"
elsif is_next_day
return "tomorrow at #{hhmma}"
elsif is_next_week
return "this upcoming #{start_datetime.strftime("%A")} at #{hhmma}"
elsif was_yesterday
return "yesterday, from #{hhmma}"
elsif was_last_week
return start_datetime.strftime("%A")
else
return start_datetime.strftime("%A, %b %e") + " at " + start_datetime.strftime("%I:%M %P")
end
end
# //used in evaluating the text message template variable: {{end_time}}
# matches equivalent in helpers.js
def evaluate_end_time_for_text_message(end_datetime)
if end_datetime == nil || end_datetime == ""
return ""
end
return end_datetime.utc.strftime("%I:%M %P")
end
# //used in evaluating the text message template variable: {{day}}
# matches evaluateReadableStartTimeFromAppointment in helpers.js
def evaluate_day_for_text_message(start_datetime)
if start_datetime == nil || start_datetime == ""
return ""
end
hhmma = start_datetime.utc.strftime("%I:%M %P")
date = start_datetime.strftime("%a, %d %b %Y").to_time.to_i
is_same_day = date == (Date.today).to_time.to_i
is_next_day = date == (Date.tomorrow).to_time.to_i
is_next_week = (date > (Date.today).to_time.to_i) && (date <= (Date.today + 7).to_time.to_i)
was_yesterday = date == (Date.yesterday).to_time.to_i
was_last_week = (date > (Date.today - 7).to_time.to_i) && (date < (Date.yesterday).to_time.to_i)
if is_same_day
return "today"
elsif is_next_day
return "tomorrow"
elsif is_next_week
return "this upcoming #{start_datetime.strftime("%A")}"
elsif was_yesterday
return "yesterday"
elsif was_last_week
return start_datetime.strftime("%A")
else
return start_datetime.strftime("%A")
end
end
# //used in evaluating the text message template variable: {{date}}
# matches evaluateReadableStartTimeFromAppointment in helpers.js
def evaluate_date_for_text_message(start_datetime)
if start_datetime == nil || start_datetime == ""
return ""
end
return start_datetime.strftime("%b %e")
end
end
module MessagesHelper
end
module LineItemsHelper
end
module ApplicationHelper
require 'fileutils'
require "zapier"
# Returns a label, input field, and error message (only if there are errors).
def form_group(form, resource, field_name, field_type='text_field', select_options: nil, options: {}, note: nil, label: nil, value: nil, user_added_classes: nil)
label_style = form_group_error?(resource, field_name) ? 'color: #dc3545' : ''
if label.nil?
result = form.label(field_name, style: label_style, class: "text-label")
elsif !label
# label == false (no label)
result = ''.html_safe
else
result = form.label(field_name, label, style: label_style, class: "text-label")
end
unless note.nil?
result += content_tag(:small, " (#{note})", class: 'form-text text-muted', style: 'display: inline;')
end
field_classes = form_group_error?(resource, field_name) ? "form-control is-invalid #{user_added_classes}" : "form-control #{user_added_classes}"
if field_type == :select
options = options.merge({class: field_classes, selected: value})
result += form.select(field_name, select_options, options, options)
elsif field_type == :time_select
result += tag(:br)
field_classes += ' form-time-select'
result += form.time_select(field_name, options, {class: field_classes}, value: value)
elsif field_type == :currency
options = options.merge({class: field_classes, value: value, step: 0.01})
result += form.number_field(field_name, options)
elsif field_type == :phone_field
options = options.merge({class: "#{field_classes} phone_mask", value: value})
result += form.text_field(field_name, options)
elsif field_type == :color
options = options.merge({class: "#{field_classes}", value: value})
result += form.color_field(field_name, options)
elsif field_type == :password_field
options = options.merge({class: "#{field_classes}", value: value})
result += form.password_field(field_name, options)
else
options = options.merge({class: field_classes, value: value})
result += form.send(field_type, field_name, options)
end
if resource != nil
resource&.errors[field_name].each do |message|
result += content_tag(:small, message, class: 'form-text text-muted')
end
end
result
end
def state_options
return [
# [innerHTML, value]
['', ''],
['AL', 'AL'],
['AK', 'AK'],
['AZ', 'AZ'],
['AR', 'AR'],
['CA', 'CA'],
['CO', 'CO'],
['CT', 'CT'],
['DE', 'DE'],
['FL', 'FL'],
['GA', 'GA'],
['HI', 'HI'],
['ID', 'ID'],
['IL', 'IL'],
['IN', 'IN'],
['IA', 'IA'],
['KS', 'KS'],
['KY', 'KY'],
['LA', 'LA'],
['ME', 'ME'],
['MD', 'MD'],
['MA', 'MA'],
['MI', 'MI'],
['MN', 'MN'],
['MS', 'MS'],
['MO', 'MO'],
['MT', 'MT'],
['NE', 'NE'],
['NV', 'NV'],
['NM', 'NM'],
['NJ', 'NJ'],
['NM', 'NM'],
['NY', 'NY'],
['NC', 'NC'],
['ND', 'ND'],
['OH', 'OH'],
['OK', 'OK'],
['OR', 'OR'],
['PA', 'PA'],
['RI', 'RI'],
['SC', 'SC'],
['SD', 'SD'],
['TN', 'TN'],
['TX', 'TX'],
['UT', 'UT'],
['VT', 'VT'],
['VA', 'VA'],
['WA', 'WA'],
['WV', 'WV'],
['WI', 'WI'],
['WY', 'WY'],
['DC', 'DC'],
['PR', 'PR'],
['VI', 'VI']
]
end
def marketing_sources_options
current_business.marketing_sources.map {|marketing_source| [marketing_source.name, marketing_source.id]}
end
# Returns true if there is an error for this field.
def form_group_error?(resource, field_name)
if resource != nil
resource.errors[field_name].any?
end
end
def permissions_for(resource)
{
create: (can? :create, resource),
destroy: (can? :destroy, resource),
read: (can? :read, resource),
update: (can? :update, resource)
}
end
def get_grow_header_image_path_from_domain()
# domain = request.env["HTTP_HOST"]
# domain.slice! "www."
# if domain == "myhvac.cloud" || domain == "hvac-on-cloud.com"
# return "grow-hvac-header2.png"
# elsif domain == "myplumbing.cloud"
# return "grow-plumbing-header2.png"
# elsif domain == "myelectrical.cloud"
# return "grow-electrical-header2.png"
# end
return "grow-homeservice-header2.png"
end
def get_trade()
# domain = request.env["HTTP_HOST"]
# domain.slice! "www."
# if domain == "myhvac.cloud" || domain == "hvac-on-cloud.com"
# return "HVAC"
# elsif domain == "myplumbing.cloud"
# return "Plumbing"
# elsif domain == "myelectrical.cloud"
# return "Electrical"
# end
return "Service"
end
def duration_of_interval_in_words(interval)
if interval == nil || interval == 0
return "N/A"
end
# hours, minutes, seconds = interval.split(':').map(&:to_i)
hours = 0
#should get seconds in here.
minutes = (interval / 60).abs
seconds = (60 - interval % 60)
[].tap do |parts|
parts << "#{hours} hour".pluralize(hours) unless hours.zero?
parts << "#{minutes} minute".pluralize(minutes) unless minutes.zero?
parts << "#{seconds} second".pluralize(seconds) unless seconds.zero?
end.join(', ')
end
def calculate_rows(content)
return 5 if content.blank?
lines = content.split("\n")
rows = lines.length
max_line_length = lines.map(&:length).max
rows += (max_line_length / 80.0).ceil
[rows, 5].max
end
def should_use_tailwind_instead_of_application_css?
pages_with_tailwind_enabled = [
'/published_job_pages/dashboard',
'/pictures',
'/w',
'/request_data_deletion',
'/llama_bots',
'/llama_bot',
'/source',
{ controller: 'home_pages', action: 'edit' }
]
should_page_use_tailwind = false
pages_with_tailwind_enabled.any? do |page|
if page.is_a?(String)
should_page_use_tailwind ||= current_page?(page)
elsif page.is_a?(Hash)
should_page_use_tailwind ||= controller.controller_name == page[:controller].to_s && controller.action_name == page[:action].to_s
end
end
return should_page_use_tailwind
end
def human_readable_date_time(date, business)
date = date.in_time_zone(business.time_zone)
if date.to_date == Date.today.to_date #seriously.. not saving in mountain time
date.strftime("Today, %l:%M%p")
elsif date.to_date == Date.yesterday.to_date
date.strftime("Yesterday, %l:%M%p")
elsif (date > Date.today - 7) && (date < Date.yesterday)
date.strftime("%a, %l:%M%p")
else
date.strftime("%b %-d, %l:%M%p")
end
end
def human_readable_date_time_mst(date)
date = date.in_time_zone('America/Denver')
if date.to_date == Date.today.to_date #seriously.. not saving in mountain time
date.strftime("Today, %l:%M%p")
elsif date.to_date == Date.yesterday.to_date
date.strftime("Yesterday, %l:%M%p")
elsif (date > Date.today - 7) && (date < Date.yesterday)
date.strftime("%a, %l:%M%p")
else
date.strftime("%b %-d, %l:%M%p")
end
end
def log_tracked_event(event_information)
# Creating a file
event_type = event_information[:event].downcase.gsub(' ','_')
file_name = "#{event_type}.log"
path = Rails.root.join('log', 'manual_logs')
FileUtils.mkdir_p path.to_s unless File.exists?(path.to_s)
fileobject = File.open(path.join(file_name).to_s, "a");
file_data = "\n\n**********************************************\n"
event_information.keys.each do |key|
file_data += "#{key.capitalize}: #{event_information[key]} \n"
end
fileobject.syswrite(file_data);
# Closing a file
fileobject.close();
# business_id: @business.id,
# name: @business.name.to_s,
# email: @business.email.to_s,
# phone: @business.phone,
# signed_in_at: date_time_now,
# event: 'Business Registered'
#Zapier integration if enabled.
if event_information["event"] == "Business Registered"
Zapier.PipeDriveNewRegistration(event_information["business_id"], event_information["name"], event_information["employee_name"], event_information["phone"], event_information["email"], event_information["signed_in_at"])
Zapier.GoogleSheetsNewRegistration(event_information["business_id"], event_information["name"], event_information["employee_name"], event_information["phone"], event_information["email"], event_information["signed_in_at"])
end
end
def controller_index_urls
controllers = Dir[Rails.root.join('app/controllers/*_controller.rb')].map do |file|
controller_name = file.split('/').last.gsub('_controller.rb', '')
view_folder = Rails.root.join('app/views', controller_name)
if Dir.exist?(view_folder)
{
name: controller_name.humanize,
url: "/#{controller_name.pluralize}",
created_at: File.ctime(file)
}
end
end.compact
sorted_controllers = controllers.sort_by { |controller| controller[:created_at] }.reverse
sorted_controllers.map do |controller|
{
name: controller[:name],
url: controller[:url]
}
end
end
end
module MarketingSourcesHelper
end
# app/helpers/html_sanitizer.rb
# https://chat.openai.com/c/38a68600-d48a-433b-8741-ee4fbee13696
require 'action_view'
include ActionView::Helpers::SanitizeHelper
module HtmlSanitizer
def self.strip_html_tags_and_entities(input)
return nil if input.nil?
stripped = ActionView::Helpers::SanitizeHelper.strip_tags(input)
unescaped = CGI.unescapeHTML(stripped)
unescaped_with_spaces = unescaped.gsub(/\u00A0/, ' ')
unescaped_with_spaces.squeeze(' ')
end
end
module LlamaBotsHelper
end
module WebPagesHelper
end
module EmployeeJobTypesHelper
end
module CustomersHelper
def update_job_params_if_new_customer!(params)
if params['job']['customer_name'] != ""
splittedName = params['job']['customer_name'].split(' ')
params['job'].merge!(:first_name => splittedName[0])
params['job'].merge!(:last_name => splittedName[1])
params['job'].merge!(:company => 'Test')
params['job'].merge!(:business_id => current_business.id)
puts("generating new customer record")
@customer = current_business.customers.new(customer_params)
if @customer.save
custID = ActiveRecord::Base.connection.execute("SELECT id FROM customers WHERE email = '"+params['job']['email'] +"'")
params['job'].merge!(:customer_id => custID.values[0][0])
end
end
end
def format_time_seconds(seconds)
hours = (seconds / 3600).floor()
seconds = (seconds % 3600).floor()
minutes = (seconds / 60).floor()
seconds = (seconds % 60).floor()
return "#{hours}h #{minutes}m #{seconds}s";
end
end
module EventsHelper
end
module CallsHelper
end
module EmployeesHelper
#
def employees_data
@employees
end
# Group options for edit employee form.
def group_options
# Do NOT include owner as an option.
Group.employee_groups(current_business).map do |g|
[g.name&.titleize, g.id]
end
end
def branch_options
Branch.where(:business_id => current_business.id).map do |b|
[b.name&.titleize, b.id]
end
end
end
include StripeHelper
module PaymentsHelper
def payment_type_options
Payment.payment_types.keys
end
def to_cents(amount)
StripeHelper.to_cents(amount)
end
def surcharge_amount(amount, app_percentage_fee)
StripeHelper.calculate_surcharge(amount, app_percentage_fee) - amount
end
def total_surcharge_amount(amount, app_percentage_fee)
StripeHelper.calculate_surcharge(amount, app_percentage_fee)
end
end
class ActionDispatch::IntegrationTest
include PaymentsHelper
end
module WebhooksHelper
end
module ProductsHelper
end
module WebPageHistoriesHelper
end
module PublishedJobPagesHelper
def PublishedJobPagesHelper.extract_image_urls_from_attachments_in_params(attachment_params)
# This method is responsible for taking attachment_ids, and converting them into workable S3 links
# so that we can download the images and embed them with SEO-rich information, such as geo-location and keywords.
if attachment_params.nil?
return [] #return empty array if user didn't pass in attachment_params
end
image_urls = []
attachment_params.values.each do |image_attachment_id|
image_attachment = ActiveStorage::Attachment.where(id: image_attachment_id).first
image_url = image_attachment.service.send(:object_for, image_attachment.key).public_url
image_urls << image_url
end
return image_urls
end
def PublishedJobPagesHelper.extract_image_urls_from_attachments_in_params_array(attachment_params)
# This method is responsible for taking attachment_ids, and converting them into workable S3 links
# so that we can download the images and embed them with SEO-rich information, such as geo-location and keywords.
if attachment_params.nil?
return [] #return empty array if user didn't pass in attachment_params
elsif attachment_params.all? { |param| param.is_a?(String) && param.start_with?('http') }
return attachment_params # Return the image_urls back if attachment_params are just an array of https URLs
end
image_urls = []
attachment_params.each do |image_attachment_id|
image_attachment = ActiveStorage::Attachment.where(id: image_attachment_id["image"]["id"])&.first
unless image_attachment.nil? #sometimes this is nil, due to an upload failing/being deleted out of the active storage. (Bug where not all images always get uploaded consistently)
image_url = image_attachment.service.send(:object_for, image_attachment.key).public_url
image_urls << image_url
end
end
return image_urls
end
end
module BranchesHelper
end
module EmailTemplatesHelper
end
module HomePagesHelper
end
module FormatterHelper
def format_phone_number(phone_number)
cleaned = phone_number.gsub(/\D/, '') # removes any non-digit characters
"(#{cleaned[0..2]}) #{cleaned[3..5]}-#{cleaned[6..9]}"
end
end
#http://blog.plataformatec.com.br/2019/01/custom-authentication-methods-with-devise/
class AuthenticationTokenStrategy < Warden::Strategies::Base
def valid?
authentication_token.present?
end
def authenticate!
employee = Employee.find_by(authentication_token: authentication_token)
if employee
success!(employee)
else
fail!('Invalid login credentials')
end
end
private
def authentication_token
params['authentication_token']
end
end
class AppointmentFilterValidator < ActiveModel::Validator
def validate(record)
unless record.appointment_filter =~ /^(\d,?)+$/i
record.errors[:appointment_filter] << "Couldn't set the schedule employee filter"
end
end
end
class MessagePhoneValidator < ActiveModel::Validator
include ValidatorHelper
def validate(record)
if not ValidatorHelper.valid_phone_format(record.to)
record.errors.add(:to, "The number must you're sending from must have numeric digits. No special characters are allowed.")
end
if not ValidatorHelper.valid_phone_format(record.sent_from)
record.errors.add(:from, "The number you're sending this message to must only have numeric digits. No special characters are allowed.")
end
if record.business.twilio_number&.count('0123456789') != 10
record.errors.add(:from, "The business's twilio phone number isn't set up correctly! (Should be 10 digits)")
end
if record.to.count('0123456789') != 10
record.errors.add(:to, "Phone number must be 10 digits")
end
if record.sent_from&.count('0123456789') != 10
record.errors.add(:from, "Phone number must be 10 digits")
end
end
end
class ZipCodeValidator < ActiveModel::Validator
def validate(record)
unless valid_zipcode(record.zip_code)
record.errors.add(:zip_code, "Zipcode must be either a 5-digit number for US or a valid Canadian postal code in the format 'A1A 1A1'.")
end
end
def valid_zipcode(zip)
return false if zip.nil?
us_zipcode_pattern = /\A\d{5}\Z/
canadian_postal_code_pattern = /\A[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d\Z/
us_zipcode_pattern.match?(zip) || canadian_postal_code_pattern.match?(zip)
end
end
class PaymentValidator < ActiveModel::Validator
def validate(record)
if (record.job.present? && record.invoice != nil && !(record.job.invoices.include?(record.invoice)))
record.errors.add(:invoice_id, "This invoice doesn't belong to the given job")
end
end
end
class CssHexColorValidator < ActiveModel::Validator
def validate(record)
unless record.color =~ /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/i
record.errors[:color] << "must be a valid CSS hex color code"
end
end
end
class JobValidator < ActiveModel::Validator
def validate(record)
if !job_business_id_matches_customer_business_id(record)
record.errors.add(:business_id, "The Job's Business ID and Customer's Business ID do not match! Please contact your developer.")
end
end
private
def job_business_id_matches_customer_business_id(record)
return record.business_id == record.customer.business_id
end
end
class RosterJobTimeValidator < ActiveModel::Validator
def validate(record)
time_start_end_in_five_minute_intervals(record.time_start, record.time_end, record)
time_start_end_range(record.time_start, record.time_end, record)
time_start_and_end_are_not_same_time(record)
time_start_not_before_end_time(record)
if theres_a_conflict_in_schedule(record)
record.errors.add(:date, "#{Roster.find(record.roster_id).name} already has a job scheduled for #{record.time_start.to_formatted_s(:short)} to #{record.time_end.to_formatted_s(:short)}")
end
end
## Checks the database for any employee jobs where the given time_start and time_end would conflict for the given employee's id
## If there's a conflict, return true. Otherwise, return false.
def theres_a_conflict_in_schedule(record)
jobs_already_scheduled_during_start_time = RosterJob.where(:roster_id => record.roster_id,
:time_start => record.time_start..record.time_end)
.where.not(:id=>record.id)
start_count = jobs_already_scheduled_during_start_time.count
overlapping_jobs = EmployeeJob.where(:time_start => record.time_start..record.time_end)
for employee in overlapping_jobs
if EmployeeRoster.where(:roster_id => record.roster_id).pluck(:employee_id).include?(employee.employee_id) && !job_cancelled(employee.job_id)
start_count = start_count + 1
end
end
for job in jobs_already_scheduled_during_start_time
if job_cancelled(job.job_id)
start_count = start_count-1
end
end
jobs_already_scheduled_during_end_time = RosterJob.where(:roster_id => record.roster_id,
:time_end => record.time_start..record.time_end)
.where.not(:id=>record.id)
end_count = jobs_already_scheduled_during_end_time.count
overlapping_jobs = EmployeeJob.where(:time_end => record.time_start..record.time_end)
for employee in overlapping_jobs
if EmployeeRoster.where(:roster_id => record.roster_id).pluck(:employee_id).include?(employee.employee_id) && !job_cancelled(employee.job_id)
end_count = end_count + 1
end
end
for job in jobs_already_scheduled_during_end_time
if job_cancelled(job.job_id)
end_count = end_count - 1
end
end
theres_a_conflict_in_schedule = start_count > 0 || end_count > 0
if theres_a_conflict_in_schedule
end
#Get the earliest time and latest time for the given date, then let's check if there's any conflicts in the date's schedule.
beginning_of_day = record.time_start.change(:hour => 0)
end_of_day = record.time_end.change(:hour => 24)
jobs_worked_by_employee_for_the_day = RosterJob.where(:roster_id => record.roster_id, :time_start => beginning_of_day..end_of_day).where.not(:id=>record.id)
all_jobs_of_roster_employees = EmployeeRoster.where(:roster_id => record.roster_id)
for employee in all_jobs_of_roster_employees
employees_jobs_for_roster = EmployeeJob.where(:employee_id => employee.employee_id, :time_start => beginning_of_day..end_of_day)
for job in employees_jobs_for_roster
if job_cancelled(job.job_id)
next
end
if !same_job(job, record) && job_was_scheduled_in_between_existing_job(job, record)
theres_a_conflict_in_schedule = true
break
end
end
end
#Make sure there are no jobs that are sandwiched between existing jobs, where the start i
for job in jobs_worked_by_employee_for_the_day
if job_cancelled(job.job_id)
next
end
if !same_job(job, record) && job_was_scheduled_in_between_existing_job(job, record)
theres_a_conflict_in_schedule = true
break
end
end
return theres_a_conflict_in_schedule
end
def time_start_end_in_five_minute_intervals(time_start, time_end, record)
if (time_start.min % 5 != 0 && time_start >= 0 && time_start < 60)
record.errors.add(:time_start, "Starting time must be in 5 minute increments.")
end
if (time_start.min % 5 != 0 && time_start >= 0 && time_start < 60)
record.errors.add(:time_end, "Ending time must be in 5 minute increments.")
end
end
def time_start_end_range(time_start, time_end, record)
if not between_7pm_and_8am(time_start)
record.errors.add(:time_start, "Appointment must be between 8 AM and 7 PM")
end
if not between_7pm_and_8am(time_end)
record.errors.add(:time_end, "Appointment must be between 8 AM and 7 PM")
end
end
def time_start_and_end_are_not_same_time(record)
if (record.time_start.hour == record.time_end.hour && (record.time_start.min == record.time_end.min))
# if (record.time_start == record.time_end)
record.errors.add(:time_end, "The appointment start and end time are the exact same, you can't have a job with 0 minutes")
end
end
def time_start_not_before_end_time(record)
if (record.time_start > record.time_end)
record.errors.add(:time_end, "The appointment's start time must come before the end time")
end
end
private
def between_7pm_and_8am(t)
return ((t.hour < 19 or (t.hour == 19 and t.min < 1)) and t.hour >= 8)
end
def same_job(existing_job, new_job)
return existing_job.id == new_job.id
end
def job_was_scheduled_in_between_existing_job(existing_job, new_job)
return new_job.time_start > existing_job.time_start && new_job.time_start < existing_job.time_end
end
def job_cancelled(job_id)
is_cancelled = Job.where(:id => job_id, :job_status => 'Cancelled')
if is_cancelled.count > 0
return true
else
return false
end
end
end
class InvoiceValidator < ActiveModel::Validator
def validate(record)
if not has_valid_invoice_type(record)
record.errors.add(:invoice_type, "Your invoice type must be 'Invoice', 'Receipt', 'Estimate', or 'Change Order'")
end
if not_have_business(record)
record.errors.add(:business, "You must have a business record attached to an invoice to create it!")
end
end
def has_valid_invoice_type(record)
t = record.invoice_type
return t == 'Invoice' || t == 'Estimate' || t == 'Receipt' || t == 'Change Order'
end
def not_have_business(record)
return record.business.nil?
end
end
# class PhoneValidator < ActiveModel::EachValidator
class PhoneValidator < ActiveModel::Validator
include ValidatorHelper
# def validate_each(record, attribute, value)
# record[attribute] = value&.delete('^0-9') #Is this bad to change data in a validation..?
# record.errors[attribute] << "Phone numbers must be 10 digits & only numeric digits" unless (ValidatorHelper.valid_phone_format(record[attribute]) && record[attribute]&.count == 10 rescue nil)
# end
def validate(record)
record.phone = record.phone&.delete('^0-9') #Is this bad to change data in a validation..?
if not ValidatorHelper.valid_phone_format(record.phone)
record.errors.add(:phone, "Phone number must only have numeric digits. No special characters are allowed.")
end
if record.phone&.count('0123456789') != 10
record.errors.add(:phone, "Phone number must be 10 digits")
end
end
# def valid_phone_format(phone)
# if phone == nil
# return true
# else
# return (/^[0-9]*$/.match(phone))
# end
# end
end
class InvoiceToValidator < ActiveModel::Validator
def validate(record)
end
end
class JobStatusValidator < ActiveModel::Validator
include JobsHelper
def validate(record)
end
def nil_job_status(record)
return record.job_status == nil
end
end
class EmailValidator < ActiveModel::Validator
def validate(record)
if not valid_email(record.email)
record.errors.add(:email, "Email wasn't in correct format.")
end
end
def valid_email(email)
return URI::MailTo::EMAIL_REGEXP.match(email)
end
end
class JobTimeValidator < ActiveModel::Validator
def validate(record)
# time_start_end_in_five_minute_intervals(record.time_start, record.time_end, record)
# time_start_and_end_are_not_same_time(record)
time_start_not_before_end_time(record)
# if theres_a_conflict_in_schedule(record)
# record.errors.add(:date, "#{Employee.find(record.employee_id).first_name} already has a job scheduled for #{record.time_start.to_formatted_s(:short)} to #{record.time_end.to_formatted_s(:short)}")
# end
end
## Checks the database for any employee jobs where the given time_start and time_end would conflict for the given employee's id
## If there's a conflict, return true. Otherwise, return false.
def theres_a_conflict_in_schedule(record)
jobs_already_scheduled_during_start_time = EmployeeJob.where(:employee_id => record.employee_id,
:time_start => record.time_start..record.time_end)
.where.not(:id=>record.id)
start_count = jobs_already_scheduled_during_start_time.count
overlapping_rosters = RosterJob.where(:time_start => record.time_start..record.time_end)
for roster in overlapping_rosters
if EmployeeRoster.where(:roster_id => roster.roster_id).pluck(:employee_id).include?(record.employee_id) && !job_cancelled(roster.job_id)
start_count = start_count + 1
end
end
for job in jobs_already_scheduled_during_start_time
if job_cancelled(job.job_id)
start_count = start_count-1
end
end
jobs_already_scheduled_during_end_time = EmployeeJob.where(:employee_id => record.employee_id,
:time_end => record.time_start..record.time_end)
.where.not(:id=>record.id)
end_count = jobs_already_scheduled_during_end_time.count
overlapping_rosters = RosterJob.where(:time_end => record.time_start..record.time_end)
for roster in overlapping_rosters
if EmployeeRoster.where(:roster_id => roster.roster_id).pluck(:employee_id).include?(record.employee_id) && !job_cancelled(roster.job_id)
end_count = end_count + 1
end
end
for job in jobs_already_scheduled_during_end_time
if job_cancelled(job.job_id)
end_count = end_count - 1
end
end
theres_a_conflict_in_schedule = start_count > 0 || end_count > 0
#Get the earliest time and latest time for the given date, then let's check if there's any conflicts in the date's schedule.
beginning_of_day = record.time_start.change(:hour => 0)
end_of_day = record.time_end.change(:hour => 24)
jobs_worked_by_employee_for_the_day = EmployeeJob.where(:employee_id => record.employee_id, :time_start => beginning_of_day..end_of_day)
all_of_employees_rosters = EmployeeRoster.where(:employee_id => record.employee_id)
for roster in all_of_employees_rosters
roster_jobs_by_employee_for_the_day = RosterJob.where(:roster_id => roster.roster_id, :time_start => beginning_of_day..end_of_day)
for job in roster_jobs_by_employee_for_the_day
if job_cancelled(job.job_id)
next
end
if !same_job(job, record) && job_was_scheduled_in_between_existing_job(job, record)
theres_a_conflict_in_schedule = true
break
end
end
end
#Make sure there are no jobs that are sandwiched between existing jobs, where the start i
for job in jobs_worked_by_employee_for_the_day
if job_cancelled(job.job_id)
next
end
if !same_job(job, record) && job_was_scheduled_in_between_existing_job(job, record)
theres_a_conflict_in_schedule = true
break
end
end
return theres_a_conflict_in_schedule
end
def time_start_end_in_five_minute_intervals(time_start, time_end, record)
if (time_start.min % 5 != 0 && time_start >= 0 && time_start < 60)
record.errors.add(:time_start, "Starting time must be in 5 minute increments.")
end
if (time_start.min % 5 != 0 && time_start >= 0 && time_start < 60)
record.errors.add(:time_end, "Ending time must be in 5 minute increments.")
end
end
def time_start_end_range(time_start, time_end, record)
if not between_7pm_and_8am(time_start)
record.errors.add(:time_start, "Appointment must be between 8 AM and 7 PM")
end
if not between_7pm_and_8am(time_end)
record.errors.add(:time_end, "Appointment must be between 8 AM and 7 PM")
end
end
def time_start_and_end_are_not_same_time(record)
if (record.time_start.hour == record.time_end.hour && (record.time_start.min == record.time_end.min))
# if (record.time_start == record.time_end)
record.errors.add(:time_end, "The appointment start and end time are the exact same, you can't have a job with 0 minutes")
end
end
def time_start_not_before_end_time(record)
if record != nil && record.time_start && record.time_end && (record&.time_start > record&.time_end)
record&.errors.add(:time_end, "The appointment's start time must come before the end time")
end
end
private
def between_7pm_and_8am(t)
return ((t.hour < 19 or (t.hour == 19 and t.min < 1)) and t.hour >= 8)
end
def same_job(existing_job, new_job)
return existing_job.id == new_job.id
end
def job_was_scheduled_in_between_existing_job(existing_job, new_job)
return new_job.time_start > existing_job.time_start && new_job.time_start < existing_job.time_end
end
def job_cancelled(job_id)
is_cancelled = Job.where(:id => job_id, :job_status => 'Cancelled')
if is_cancelled.count > 0
return true
else
return false
end
end
end
class FirstLastNameValidator < ActiveModel::Validator
def validate(record)
if not has_valid_name(record)
record.errors.add(:first_name, "You must have a first or last name")
end
if (record.first_name != "" && record.first_name != nil)
if not valid_first_name(record.first_name)
record.errors.add(:first_name, "You have invalid characters for your first name")
end
end
if (record.last_name != "" && record.last_name != nil)
if not valid_last_name(record.last_name)
record.errors.add(:last_name, "You have invalid characters for your last name.")
end
end
end
def valid_first_name(first_name)
return (first_name != nil and /\w*/.match(first_name))
end
def valid_last_name(last_name)
return (last_name != nil and /\w*/.match(last_name))
end
def has_valid_name(record)
if record.first_name.blank? #Not valid if both first and last name are blank. Must have either first or last name.
return !record.last_name.blank?
else
return true
end
end
end
class LineItemValidator < ActiveModel::Validator
def validate(record)
if record.price&.nan?
record.errors.add(:price, "Your line item must not be NaN!")
end
end
end
class QuickbooksPushInvoiceWorker
include Sidekiq::Worker
def perform(invoice_id, business_id)
invoice = Invoice.find(invoice_id)
business = Business.find(business_id)
if invoice.invoice_type == "Invoice" && invoice.line_items.count != 0 && business.authenticated_with_quickbooks? &&
successful_push = invoice.push_invoice_to_quickbooks!
Rails.logger.info("Just successfully pushed the invoice #{invoice.id} into Quickbooks!")
end
end
end
class QuickbooksResyncInvoiceWorker
include Sidekiq::Worker
def perform(invoice_id, business_id)
invoice = Invoice.find(invoice_id)
business = Business.find(business_id)
invoice.sync_with_quickbooks!(business)
end
end
class AutoRespondTextFromInboundCallWorker
include Sidekiq::Worker
def perform(phone_number, twilio_number, twilio_missed_call_message, business_id)
# Rails.logger.info("Auto respond to phone nubmer: #{phone_number} for business: #{business_id}")
# business = Business.find(business_id)
message_content = twilio_missed_call_message
if !twilio_missed_call_message.blank? && !twilio_number.blank?
Message.create(sent_from: twilio_number, to: phone_number, content: message_content, business_id: business_id)
# successful_push = invoice.push_invoice_to_quickbooks!
Rails.logger.info("Just successfully auto-texted #{phone_number} from #{twilio_number} the message with contents: #{message_content}")
end
end
end
module QuickbooksHelper
#Takes the XML from Quickbooks and returns a hash of the customer's information
def QuickbooksHelper.parse_customer_xml(customer_xml)
#Handling the first/last names.. This might be off a little bit.
first_name = nil
last_name = nil
if customer_xml.xpath("GivenName").text.empty? && customer_xml.xpath("FamilyName").empty? && customer_xml.xpath("CompanyName").empty?
first_name = customer_xml.xpath("DisplayName").text.split(" ")[0]
last_name = customer_xml.xpath("DisplayName").text.split(" ")[1]
else
first_name = customer_xml.xpath("GivenName").text
last_name = customer_xml.xpath("FamilyName").text
end
customer_email = customer_xml.xpath("PrimaryEmailAddr/Address").text #if multiple emails how do we handle this?
customer_hash = {:first_name => first_name,
:last_name => last_name,
# :phone => customer_xml.xpath("PrimaryPhone/FreeFormNumber").text,
:phone => customer_xml.xpath("//FreeFormNumber").first&.text,
:company => customer_xml.xpath("CompanyName").text,
:notes => "Imported from Quickbooks", #TODO: Not sure what to put here.
:email => customer_xml.xpath("PrimaryEmailAddr/Address").text,
:quickbooks_id => customer_xml.xpath("Id").text}
location_hash = { :city => customer_xml.xpath("BillAddr/City").text,
:state => customer_xml.xpath("BillAddr/CountrySubDivisionCode").text,
:street_address => customer_xml.xpath("BillAddr/Line1").text,
:zip_code => customer_xml.xpath("BillAddr/PostalCode").text}
return customer_hash, location_hash
end
#In Quickbooks, There can also be a Mobile/FreeFormNumber, or a AlternatePhone/FreeFormNumber. We should check for these too.
def QuickbooksHelper.phone_from_customer_xml(customer)
phone_types = ["PrimaryPhone", "Mobile", "AlternatePhone"]
phone_types.each do |phone_type|
phone = customer.xpath("#{phone_type}/FreeFormNumber").text
return phone unless phone.empty?
end
nil
end
end
module ExpoNotificationHelper
require 'uri'
require 'net/http'
def ExpoNotificationHelper.notify_client(device_tokens, notification_content, title, data)
client = Exponent::Push::Client.new
url = URI("https://exp.host/--/api/v2/push/send")
messages = device_tokens.map {|token| {to: token.token, sound: "default", body: notification_content, title: title, data: data.to_json}}
messages.each do |message|
Rails.logger.info("Sending message: #{message}")
url = URI("https://exp.host/--/api/v2/push/send")
response = Net::HTTP.post_form(url, message) #message already formatted as param.
Rails.logger.info("Just sent notification to expo, here is the expo server's response:")
Rails.logger.info(response)
Rails.logger.info(JSON.parse(response.body))
end
end
end
module MessagesHelper
# Gets the messaging threads for the given business and groups them by number
def self.message_threads(business, selected_conversation=nil, messages_limit = 25, page=0) #web version
numbers = most_recent_numbers(business, messages_limit, page) #only shows the 25 most recent.. how do we add pagination on these.?
if selected_conversation && !(numbers.include?(selected_conversation)) #append the phone number onto here if it's been selected. This fixes a bug where users have been getting empty conversations if it's an older conversation.
numbers.append(selected_conversation)
end
return construct_messaging_threads(business, numbers)
end
def self.most_recent_numbers(business, messages_limit=25, page=0)
#get the distinct number_threads from the business
query = "SELECT sent_from, \"to\"
FROM messages
WHERE business_id = #{ActiveRecord::Base.connection.quote(business.id)}
GROUP BY sent_from, \"to\"
ORDER BY MAX(sent) DESC
LIMIT #{messages_limit}
OFFSET #{messages_limit * page}"
all_numbers = ActiveRecord::Base.connection.execute(query).values
numbers = all_numbers.map{|n| n.first == business.twilio_number ? n.second : n.first} #This line is because depending on which direction the message came in, the sent_from would either be from our customer, or from our business. The number we want to display is just the customer's number, period. So we simply check each one to see if the "sent_from" is our twilio number (meaning we sent it outbound to a customer, and if that's the case we'll display the "to" on the message), or to see if the "to" is to our twilio number, meaning it was sent to our business from a customer, and we want to display the "sent_from" phone number since that's the customer phone number.
numbers = numbers.uniq #Remove any duplicates.
return numbers
end
def self.most_recent_unread_numbers(business, messages_limit=25, page=0)
#get the distinct number_threads from the business
query = "SELECT sent_from, \"to\"
FROM messages
WHERE business_id = #{ActiveRecord::Base.connection.quote(business.id)}
AND NOT has_been_read
GROUP BY sent_from, \"to\"
ORDER BY MAX(sent) DESC
LIMIT #{messages_limit}
OFFSET #{messages_limit * page}"
all_numbers = ActiveRecord::Base.connection.execute(query).values
numbers = all_numbers.map{|n| n.first == business.twilio_number ? n.second : n.first} #This line is because depending on which direction the message came in, the sent_from would either be from our customer, or from our business. The number we want to display is just the customer's number, period. So we simply check each one to see if the "sent_from" is our twilio number (meaning we sent it outbound to a customer, and if that's the case we'll display the "to" on the message), or to see if the "to" is to our twilio number, meaning it was sent to our business from a customer, and we want to display the "sent_from" phone number since that's the customer phone number.
numbers = numbers.uniq #Remove any duplicates.
return numbers
end
def self.most_recent_previews(business, page_number=0) #mobile version
numbers = self.most_recent_numbers(business, 25, page_number) #subtract 1 from page number because it's a 0 based index whereas the one coming from mobile app is a 1 based index
previews = numbers.map{|n| {number: n, customer: resolve_phone_number(business, n), message: get_latest_message_from_number(business, n)}}
# numbers = most_recent_numbers(business, messages_limit, page) #only shows the 25 most recent.. how do we add pagination on these.?
# if selected_conversation && !(numbers.include?(selected_conversation)) #append the phone number onto here if it's been selected. This fixes a bug where users have been getting empty conversations if it's an older conversation.
# numbers.append(selected_conversation)
# end
# return construct_messaging_threads(business, numbers)
end
def self.unread_message_previews(business)
numbers = self.most_recent_unread_numbers(business)
previews = numbers.map{|n| {number: n, customer: resolve_phone_number(business, n), message: get_latest_message_from_number(business, n)}}
end
def self.get_latest_message_from_number(business, number)
last_message_sent_to_number = business.messages.where(to: number).order(sent: :desc)
last_message_received_from_number = business.messages.where(sent_from: number).order(sent: :desc)
# last_message_sent_to_number.sent
if last_message_sent_to_number.empty?
return last_message_received_from_number.first
elsif last_message_received_from_number.empty?
return last_message_sent_to_number.first
else
last_message_sent_to_number.first.sent > last_message_received_from_number.first.sent ? last_message_sent_to_number.first : last_message_received_from_number.first
end
end
def self.message_thread_for_specific_number(business, number)
return construct_messaging_threads(business, [number])[0][1]
end
#Takes a phone number and business, and returns either the customer (if found), the contact (if no customer found but contact found), or nil (if can't find either)
def self.resolve_phone_number(business, number)
matches = business.customers.includes([:location, :marketing_source]).where(phone: number)
if matches.length < 1 #match is a contact
matches = business.customer_contacts.where(phone:number)
if matches.length >= 1
return matches.first
end
else
return matches.first #returns a customer
end
return nil
end
# Take this phone number and return a customer recrord from the given business, if there's any customers with this number.
# Only returns matching customer records, will not return any matching customer contact record.
# Returns nil if no customer record is found that has the given phone number
# Phone number must be normalized to '8013499925', format. 10 digits, no +1, no internationalization, no special symbols. See phone_validator.rb
def self.resolve_phone_number_customer_records_only(business, number)
matches = business.customers.includes([:location, :marketing_source]).where(phone: number)
if matches.length >= 1
return matches.first
end
return nil
end
def self.resolve_phone_number_readable(business, number)
readable = self.resolve_phone_number(business, number)
readable.nil? ? {} : readable
end
def self.get_uploaded_images_urls(customer)
image_urls = customer.jobs.includes([:images_attachments => :blob]).map do |job|
job.images.attachments.map do |image|
if image
{
url: image.blob.service_url,
id: image.id
}
end
end
end
image_urls.flatten
end
private
def self.get_numbers_business_sent_messages_to_but_havent_replied_yet(business, numbers_that_have_replied_to_business)
query = "SELECT \"to\"
FROM messages
WHERE business_id = #{ActiveRecord::Base.connection.quote(business.id)}
AND sent_from = #{ActiveRecord::Base.connection.quote(business.twilio_number)}"
numbers_that_have_replied_to_business.each do |n|
query << " AND \"to\" <> #{ActiveRecord::Base.connection.quote n}"
end
query << "
GROUP BY \"to\"
ORDER BY MAX(sent) DESC"
ActiveRecord::Base.connection.execute(query).values.map{|n| n.first} #these are sorted by the most recent message and it's content.
end
def self.construct_messaging_threads(business, numbers)
message_threads = []
numbers.each do |phone|
#need to combine the two of them, in the correct sending order.
messages_sent_from_this_number = business.messages.where(sent_from: phone).includes([:image_attachment => :blob]).includes([:employee]).order('sent ASC')
messages_we_sent_to_this_number = business.messages.where(to: phone).includes([:image_attachment => :blob]).includes([:employee]).order('sent ASC')
messages = messages_sent_from_this_number.or(messages_we_sent_to_this_number)
messages.order('sent ASC')
messages_info = {
messages: messages.to_a,
urls: get_message_image_url(messages).to_a
}
message_threads << [phone, messages_info]
end
return message_threads
end
def self.get_message_image_url(messages)
messages.map do |msg|
msg.image.attachment ? msg.image.attachment.blob.service_url : ''
end
end
def self.get_employee_who_sent_last_message(recipientPhoneNumber, business)
business.messages.where(to: recipientPhoneNumber).last&.employee
end
end
require 'onesignal_notifications'
module DeliverNotification
def DeliverNotification.push_text_notification(message_contents, sender_name, sender_number, device_tokens, sender_id, unread_message_count)
begin
Rails.logger.info("Now Sending a Text Message Notification!: #{message_contents}, sender_name: #{sender_name}, sender_number: #{sender_number}, device_tokens: #{device_tokens}, sender_id: #{sender_id}")
return OneSignalNotifications.send_notification_of_text_message(message_contents, sender_name, sender_number, device_tokens, sender_id, unread_message_count)
rescue Exception => e
Rails.logger.info("There has been an error trying to push a notification about this message, error: " + e.message + " " + e.backtrace.join("\n"))
return false
end
end
def DeliverNotification.push_job_request_submitted_notification(job_request_id, job_request_notes, sender_name, sender_city, device_tokens)
begin
Rails.logger.info("Now Sending a Job Request Notification! For job request: #{job_request_id}, job_request_notes: #{job_request_notes} sender_name: #{sender_name}, sender_city: #{sender_city}, device_tokens: #{device_tokens}")
return OneSignalNotifications.send_notification_of_job_request(job_request_id, job_request_notes, sender_name, sender_city, device_tokens)
rescue Exception => e
Rails.logger.info("There has been an error trying to push a notification about this message, error: " + e.message + " " + e.backtrace.join("\n"))
return false
end
end
def DeliverNotification.push_call_modified_received_event(call_id, call, customer_id, customer_name, customer_phone, url, player_ids)
begin
Rails.logger.info("Now Sending a call modified event! For call: #{call_id}, customer_id: #{customer_id} customer_name: #{customer_name}, customer_phone: #{customer_phone}, device_tokens: #{player_ids}, url: #{url}")
return OneSignalNotifications.send_notification_of_receive_call_modified(call_id, call, customer_id, customer_name, customer_phone, url, player_ids)
rescue Exception => e
Rails.logger.info("There has been an error trying to push a notification about this message, error: " + e.message + " " + e.backtrace.join("\n"))
return false
end
end
def DeliverNotification.push_signature_collected_notification(invoice_id, shortened_url, customer_name, invoice_type, player_ids)
begin
Rails.logger.info("Now Sending a signature collected event! For invoice: #{invoice_id}, shortened_url: #{shortened_url} customer_name: #{customer_name}, invoice_type: #{invoice_type}, device_tokens: #{player_ids}")
return OneSignalNotifications.send_notification_of_collected_signature(invoice_id, shortened_url, customer_name, invoice_type, player_ids)
rescue Exception => e
Rails.logger.info("There has been an error trying to push a notification about this message, error: " + e.message + " " + e.backtrace.join("\n"))
return false
end
end
end
class JsonWebToken
class << self
# https://www.pluralsight.com/guides/token-based-authentication-with-ruby-on-rails-5-api
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
def decode(token)
body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
HashWithIndifferentAccess.new body
rescue
nil
end
end
end
#This module is responsible for querying our data and generating the google chart data for our reports page
module ReportsHelper
# Generates our marketing sources data for new customers added
def ReportsHelper.marketing_report_data(start_range, end_range, business)
data_arr = []
added_customers = business.customers.where('created_at >= ? AND created_at <= ?', start_range, end_range)
business.marketing_sources.each do |ms|
data_arr.push([ms.name, added_customers.select{ |c| c.marketing_source_id == ms.id }.count])
end
data_arr.push(["Unknown", added_customers.select{ |c| c.marketing_source_id.nil? }.count])
return data_arr
end
def ReportsHelper.marketing_revenue_report_data(start_range, end_range, business)
query = "SELECT marketing_sources.name, DATE(invoices.issued_on), SUM(NULLIF(line_items.price, 'NaN'))
FROM invoices
INNER JOIN line_items ON invoices.id = line_items.invoice_id
INNER JOIN jobs ON invoices.job_id = jobs.id
INNER JOIN customers ON jobs.customer_id = customers.id
INNER JOIN marketing_sources ON marketing_sources.id = customers.marketing_source_id
WHERE issued_on >= #{ActiveRecord::Base.connection.quote(start_range)}
AND issued_on <= #{ActiveRecord::Base.connection.quote(end_range)}
AND invoice_type = 'Invoice'
AND jobs.business_id = #{ActiveRecord::Base.connection.quote(business.id)}
GROUP BY marketing_sources.name, DATE(invoices.issued_on)"
records = ActiveRecord::Base.connection.execute(query).values
#[Date, Total1, Total2, Total3]
marketing_source_to_column_index = {}
counter = 0
data_arr = ['Date']
business.marketing_sources.each do |ms|
marketing_source_to_column_index[ms.name] = counter
data_arr.append(ms.name)
counter += 1
end
final_data = {} # [{"Date" => '08/01/2020', "Marketing_Source_1" => , "Marketing_Source_2" => }]
records.each do |r|
source_name = r.first
date = DateTime.strptime(r.second, "%Y-%m-%d").strftime("%m/%d")
total = r.third
if final_data.key? date
final_data[date][marketing_source_to_column_index[source_name]] += total
else
new_array = Array.new(business.marketing_sources.count) {|i| 0} #Create array filled with 0s
new_array[marketing_source_to_column_index[source_name]] += total
final_data[date] = new_array
end
end
f = final_data.sort.map{|v| v.flatten}
f.prepend(data_arr)
f
end
def ReportsHelper.production_revenue_by_employee(start_range, end_range, business)
query = "
SELECT
CONCAT(employees.first_name, ' ', employees.last_name) AS employee_name,
SUM(
CASE
WHEN invoices.invoice_type = 'Invoice'
THEN NULLIF(line_items.price, 'NaN')
ELSE 0
END
) as invoice_line_items_total
FROM invoices INNER JOIN line_items ON invoices.id = line_items.invoice_id
INNER JOIN jobs ON invoices.job_id = jobs.id
INNER JOIN employee_jobs ON jobs.id = employee_jobs.job_id
INNER JOIN employees ON employees.id = employee_jobs.employee_id
WHERE invoices.issued_on >= #{ActiveRecord::Base.connection.quote(start_range)}
AND invoices.issued_on <= #{ActiveRecord::Base.connection.quote(end_range)}
AND (invoices.invoice_type = 'Invoice' OR invoices.invoice_type = 'Estimate')
AND jobs.business_id = #{ActiveRecord::Base.connection.quote(business.id)}
-- AND employee_jobs.employee_id <> jobs.owner_id
GROUP BY employee_name;
"
#Sort alphabetically by sales rep's first name
records_array = ActiveRecord::Base.connection.execute(query).values.sort_by {|i| i.first }
data_array = [
['Employee', 'Revenue']
]
records_array.each do |r|
data_array.append([r.first, r.second])
end
data_array
end
def ReportsHelper.timeclocked_by_employee(start_range, end_range, business)
query = "
SELECT
CONCAT(employees.first_name, ' ', employees.last_name) AS employee_name,
SUM((DATE_PART('hour', clocked_out - clocked_in ) * 60 + DATE_PART('minute', clocked_out - clocked_in))/60) as timesheets_time_total
FROM timesheets INNER JOIN employees ON employees.id = timesheets.employee_id
WHERE timesheets.clocked_in >= #{ActiveRecord::Base.connection.quote(start_range)}
AND timesheets.clocked_in <= #{ActiveRecord::Base.connection.quote(end_range)}
GROUP BY employee_name;"
#Sort alphabetically by employee's first name
records_array = ActiveRecord::Base.connection.execute(query).values.sort_by {|i| i.first }
data_array = [
['Employee', 'Time (In Hours)']
]
records_array.each do |r|
data_array.append([r.first, r.second])
end
data_array
end
# Queries and format our invoice (income) data for a certain range
def ReportsHelper.income_report_data(start_range, end_range, business)
invoices = business.invoices.where(invoice_type: "Invoice").where('issued_on >= ? AND issued_on <= ?', start_range, end_range)
data_array = [["Day", "Income"]]
query = "SELECT DATE(invoices.issued_on), SUM(NULLIF(line_items.price, 'NaN'))
FROM line_items
INNER JOIN invoices ON invoices.id = line_items.invoice_id
INNER JOIN jobs ON jobs.id = invoices.job_id
WHERE issued_on >= #{ActiveRecord::Base.connection.quote(start_range)}
AND issued_on <= #{ActiveRecord::Base.connection.quote(end_range)}
AND invoice_type = 'Invoice'
AND jobs.business_id = #{ActiveRecord::Base.connection.quote(business.id)}
GROUP BY DATE(issued_on)
ORDER BY DATE(issued_on) ASC;"
#Probably don't need to use the 'quote' on the business_id since it doesn't come from the controller params, but I'm doing it just to be safe.
#Getting it into proper format now..
records = ActiveRecord::Base.connection.execute(query).values
records.each do |r|
data_array.append([DateTime.strptime(r.first,"%Y-%m-%d").strftime("%Y-%m-%d"), r.second])
end
return data_array
end
def ReportsHelper.income_report_by_category_data(start_range, end_range, business)
#TODO: Determine if we should send the monthly, daily, or weekly view.
#Daily = time range <= 1 week
#Weekly = time range > 1 week <= 4 weeks
#Monthly = time range > 4 weeks
query =
"SELECT products.name, SUM(NULLIF(line_items.price, 'NaN'))
FROM invoices
INNER JOIN line_items ON invoices.id = line_items.invoice_id
INNER JOIN products on line_items.product_id = products.id
INNER JOIN jobs on jobs.id = invoices.job_id
WHERE issued_on >= #{ActiveRecord::Base.connection.quote(start_range)}
AND issued_on <= #{ActiveRecord::Base.connection.quote(end_range)}
AND invoices.invoice_type = 'Invoice'
AND jobs.business_id = #{ActiveRecord::Base.connection.quote(business.id)}
GROUP BY products.name ORDER BY products.name ASC;"
#Probably don't need to use the 'quote' on the business_id since it doesn't come from the controller params, but I'm doing it just to be safe.
records_array = ActiveRecord::Base.connection.execute(query)
data_array = [] #Formatting the data_array
data_array << records_array.values.map{|v| v.first}.prepend('Date')
data_array << records_array.values.map{|v| v.second}.prepend("#{start_range&.strftime("%m/%d")} - #{end_range&.strftime("%m/%d")}")
return data_array
end
def ReportsHelper.revenue_by_sales_rep_data(start_range, end_range, business)
query = "SELECT
CONCAT(employees.first_name, ' ', employees.last_name) AS employee_name,
SUM(
CASE
WHEN invoices.invoice_type = 'Invoice'
THEN NULLIF(line_items.price, 'NaN')
ELSE 0
END
) as invoice_line_items_total,
SUM(
CASE
WHEN invoices.invoice_type ='Estimate'
THEN NULLIF(line_items.price, 'NaN')
ELSE 0
END
) as estimate_line_items_total
FROM invoices INNER JOIN line_items ON invoices.id = line_items.invoice_id
INNER JOIN jobs ON invoices.job_id = jobs.id
INNER JOIN employees ON jobs.owner_id = employees.id
WHERE invoices.issued_on >= #{ActiveRecord::Base.connection.quote(start_range)}
AND invoices.issued_on <= #{ActiveRecord::Base.connection.quote(end_range)}
AND (invoices.invoice_type = 'Invoice' OR invoices.invoice_type = 'Estimate')
AND jobs.business_id = #{ActiveRecord::Base.connection.quote(business.id)}
GROUP BY employee_name;"
#Sort alphabetically by sales rep's first name
records_array = ActiveRecord::Base.connection.execute(query).values.sort_by {|i| i.first }
data_array = [
['Sales Rep', 'Sales', 'Estimate']
]
records_array.each do |r|
data_array.append([r.first, r.second, r.third])
end
data_array
end
def ReportsHelper.event_total_report_data(start_range, end_range, business, employee)
data_arr = ['Date', 'All', 'Web Sign In', 'Mobile Sign In', 'Invoice Creation', 'Job Creation', 'Job Request Creation']
final_data = []
final_data.append(data_arr)
a_range = (start_range.to_date .. end_range.to_date)
a_range.each do |date|
if !employee.nil?
final_data.append ReportsHelper.create_event_day_row_specific_employee(date, employee)
elsif !business.nil?
final_data.append ReportsHelper.create_event_day_row_specific_business(date, business)
else
final_data.append ReportsHelper.create_event_day_row_all(date)
end
end
return final_data
end
def ReportsHelper.create_event_day_row_all(date)
[date.to_s,
Event.where(created_at: date.all_day).count,
Event.where(created_at: date.all_day, name: 'Web Sign In').count,
Event.where(created_at: date.all_day, name: 'Mobile Sign In').count,
Event.where(created_at: date.all_day, resource: 'Invoice', name: 'Created').count,
Event.where(created_at: date.all_day, resource: 'Job', name: 'Created').count,
Event.where(created_at: date.all_day, resource: 'Job Request', name: 'Created').count]
end
def ReportsHelper.create_event_day_row_specific_business(date, business)
[date.to_s,
Event.where(created_at: date.all_day, business_id: business).count,
Event.where(created_at: date.all_day, name: 'Web Sign In', business_id: business).count,
Event.where(created_at: date.all_day, name: 'Mobile Sign In', business_id: business).count,
Event.where(created_at: date.all_day, resource: 'Invoice', name: 'Created', business_id: business).count,
Event.where(created_at: date.all_day, resource: 'Job', name: 'Created', business_id: business).count,
Event.where(created_at: date.all_day, resource: 'Job Request', name: 'Created', business_id: business).count]
end
def ReportsHelper.create_event_day_row_specific_employee(date, employee)
[date.to_s,
Event.where(created_at: date.all_day, employee_id: employee).count,
Event.where(created_at: date.all_day, name: 'Web Sign In', employee_id: employee).count,
Event.where(created_at: date.all_day, name: 'Mobile Sign In', employee_id: employee).count,
Event.where(created_at: date.all_day, resource: 'Invoice', name: 'Created', employee_id: employee).count,
Event.where(created_at: date.all_day, resource: 'Job', name: 'Created', employee_id: employee).count,
Event.where(created_at: date.all_day, resource: 'Job Request', name: 'Created', employee_id: employee).count]
end
end
module GoogleCalendarApi
def GoogleCalendarApi.client_options
{
client_id: "650654906628-tuhlkgl03r0lv41o8aolhub9i0100peo.apps.googleusercontent.com",
client_secret: "QldutF5WkjqmoBlgcohIWWui",
authorization_uri: 'https://accounts.google.com/o/oauth2/auth',
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
scope: Google::Apis::CalendarV3::AUTH_CALENDAR,
redirect_uri: "#{ENV["BASE_URL"]}/authorize_gcal_oauth"
}
end
end
module StripeHelper
def StripeHelper.calculate_surcharge(amount_business_needs_to_collect, application_percentage_fee)
if amount_business_needs_to_collect <= 0
return 0
else
return ((amount_business_needs_to_collect*(1+application_percentage_fee) +0.30)/0.971).round(2) #stripe takes .029 (2.9%) + 30 cents, so 1-.029 = .971
end
end
def StripeHelper.to_cents(amount)
if amount.nil?
return 0
else
return (amount * 100).round
end
end
end
module ValidatorHelper
def ValidatorHelper.valid_phone_format(phone)
if phone == nil
return true
else
return (/^[0-9]*$/.match(phone))
end
end
end
require 'uri'
require 'net/http'
require 'openssl'
module OneSignalNotifications
def OneSignalNotifications.push_test_message
url = URI("https://onesignal.com/api/v1/notifications?app_id=#{ENV["ONESIGNAL_APP_ID"]}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["accept"] = 'application/json'
request["Authorization"] = "Basic #{ENV["ONESIGNAL_REST_API_KEY"]}"
request["content-type"] = 'application/json'
# request.body = "{\"included_segments\":[\"Subscribed Users\"],\"contents\":{\"en\":\"English or Any Language Message\",\"es\":\"Spanish Message\"},\"name\":\"INTERNAL_CAMPAIGN_NAME\"}"
# kody_iphone_player_id = "7ac839a3-ac8c-4fb3-b3e0-109fbd568090"
kody_iphone_player_id = "d9969b80-c95b-49be-9f43-a70632084c39"
#
request.body = "{\"include_player_ids\":[\"#{kody_iphone_player_id}\"],\"contents\":{\"en\":\"You have a new message notification!\",\"es\":\"Spanish Message\"},\"name\":\"INTERNAL_CAMPAIGN_NAME\"}"
response = http.request(request)
puts response.read_body
end
def OneSignalNotifications.send_test_message_to_kodys_device
OneSignalNotifications.send_notification_of_text_message("Test text message from Kody", "Josh & Jennifer Stout", "8018564964", ["5a19e771-f47c-43d0-96d7-5c11aefb92d2"], "12")
end
# Sends a One Signal text message push notification to the mobile devices who have the passed in player ids, through the One Signal service.
# See: https://documentation.onesignal.com/reference/push-channel-properties
# Params:
# +message_contents+:: Displayed in the push notification, this is the contents of the text message that will display to the user before the user clicks on the push notification
# +sender_name+:: +Proc+ The name to display in the push notification, and to display as the sender in the messages screen once the push notification is opened
# +sender_number+:: +Proc+ The phone number this message was received from. Used in the messages screen when user replies to the message, and in querying and showing previous messages in the messages screen.
# +player_ids+:: +Proc+ These are the device_tokens that one signal assigned to our users when they first downloaded and opened the app. The message push notification gets sent out to all player_ids.
# +sender_id+:: +Proc+ The customer id of whoever sent this message (if we can find a customer associated with this phone number). Used in the messages screen so you can click on customer record and be taken to the customer record
# +unread_message_count+:: +Proc+ The number of unread messages this user has. Used to update the badge count on the app icon.
# Returns: True if the push notifications were successfully delivered, False if the push notifications were not successfully delivered.
def OneSignalNotifications.send_notification_of_text_message(message_contents, sender_name, sender_number, player_ids, sender_id, unread_message_count)
params = {"app_id" => "#{ENV["ONESIGNAL_APP_ID"]}",
"contents" => {"en" => message_contents || ""},
"include_player_ids" => player_ids,
"headings" => {"en" => sender_name || sender_number},
"data" => {"NotificationType" => "InboundSMS", "name": sender_name, "phoneNumber": sender_number, "customerId": sender_id},
'ios_badgeCount' => unread_message_count,
'ios_badgeType' => 'SetTo',
}
uri = URI.parse('https://onesignal.com/api/v1/notifications')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path,'Content-Type' => 'application/json;charset=utf-8')
request.body = params.as_json.to_json
response = http.request(request)
puts response.body
#TODO: Parse a response indicating that it wasn't successfully delivered, and return false.
return true
end
# Sends a One Signal push notification of a job request to the mobile devices who have the passed in player ids, through the One Signal service.
# See: https://documentation.onesignal.com/reference/push-channel-properties
# Params:
# +job_request_id+:: Used to open the job request when user clicks on the push notification
# +job_request_notes+:: Displayed in the push notification, the description passed in by the user to display in the body of the push notification
# +sender_name+:: +Proc+ The name to display in the push notification
# +sender_city+:: +Proc+ The city this customer is in, to display in the subtitle of the push notification
# +player_ids+:: +Proc+ These are the device_tokens that one signal assigned to our users when they first downloaded and opened the app. The message push notification gets sent out to all player_ids.
# Returns: True if the push notifications were successfully delivered, False if the push notifications were not successfully delivered.
def OneSignalNotifications.send_notification_of_job_request(job_request_id, job_request_notes, sender_name, sender_city, player_ids)
params = {"app_id" => "#{ENV["ONESIGNAL_APP_ID"]}",
"contents" => {"en" => "Notes: " + job_request_notes},
"include_player_ids" => player_ids, #TODO: This will now be "include_subscription_ids: 6392d91a-b206-4b7b-a620-cd68e32c3a76"
"headings" => {"en" => "Job Request: #{sender_name}"},
"subtitle" => {"en" => "In #{sender_city}"},
"data" => {"NotificationType" => "JobRequest", "job_request_id": job_request_id},
}
uri = URI.parse('https://onesignal.com/api/v1/notifications')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path,'Content-Type' => 'application/json;charset=utf-8')
request.body = params.as_json.to_json
response = http.request(request)
puts response.body
#TODO: Parse a response indicating that it wasn't successfully delivered, and return false.
return true
end
# Sends a One Signal push notification about a phone call to the mobile devices who have the passed in player ids, through the One Signal service.
# See: https://documentation.onesignal.com/reference/push-channel-properties
# Params:
# +argument+:: Argument description
# +job_request_notes+:: Displayed in the push notification, the description passed in by the user to display in the body of the push notification
# +sender_name+:: +Proc+ The name to display in the push notification
# +sender_city+:: +Proc+ The city this customer is in, to display in the subtitle of the push notification
# +player_ids+:: +Proc+ These are the device_tokens that one signal assigned to our users when they first downloaded and opened the app. The message push notification gets sent out to all player_ids.
# Returns: True if the push notifications were successfully delivered, False if the push notifications were not successfully delivered.
def OneSignalNotifications.send_notification_of_receive_call_modified(call_id, call, customer_id, caller_name, customer_phone, url, player_ids)
params = {"app_id" => "#{ENV["ONESIGNAL_APP_ID"]}",
"contents" => {"en" => "Call with #{caller_name || customer_phone} finished!"},
"include_player_ids" => player_ids,
"headings" => {"en" => "Phone Call Finished: #{caller_name || customer_phone}"},
"subtitle" => {"en" => "#{customer_phone}"},
"data" => {"NotificationType" => "ReceiveCallModifiedEvent", "call_id": call_id, "call": call, "customer_id": customer_id, "caller_name": caller_name, "customer_phone": customer_phone, "url": url},
} #TODO: there's a bug where the data is greater than 3500 bytes.
# {"errors":["Option Metadata must not exceed 3500 bytes.","Data Data must be no more than 2048 bytes long","Option Message in English language is too long to send to an iOS device. You can either make the content shorter or shorten or remove the other options."]}
uri = URI.parse('https://onesignal.com/api/v1/notifications')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path,'Content-Type' => 'application/json;charset=utf-8')
request.body = params.as_json.to_json
response = http.request(request)
puts response.body
#TODO: Parse a response indicating that it wasn't successfully delivered, and return false.
return true
end
# Sends a One Signal push notification telling the user we just collected a signature from their customer
# See: https://documentation.onesignal.com/reference/push-channel-properties
# Params:
# +argument+:: Argument description
# +invoice_id+:: the id if the invoice/estimate that was signed
# +shortened_url+:: the url that can be used to view the signed invoice
# +customer_name+:: +Proc+ the name of the customer who signed the invoice
# +invoice_type+:: +Proc+ tells the user whether this was an "Invoice" or "Estimate" that was signed
# +player_ids+:: +Proc+ These are the device_tokens that one signal assigned to our users when they first downloaded and opened the app. The message push notification gets sent out to all player_ids.
# Returns: True if the push notifications were successfully delivered, False if the push notifications were not successfully delivered.
def OneSignalNotifications.send_notification_of_collected_signature(invoice_id, shortened_url, customer_name, invoice_type, device_tokens)
params = {"app_id" => "#{ENV["ONESIGNAL_APP_ID"]}",
"contents" => {"en" => "#{customer_name} just signed their #{invoice_type}!"},
"include_player_ids" => device_tokens,
"headings" => {"en" => "Signature from #{customer_name}"},
"subtitle" => {"en" => "Collected Signature. View Now"},
"data" => {"NotificationType" => "SignatureCollected", "invoice_id": invoice_id, "url": shortened_url, "customer_name": customer_name, "invoice_type": invoice_type},
} #TODO: there's a bug where the data is greater than 3500 bytes.
# {"errors":["Option Metadata must not exceed 3500 bytes.","Data Data must be no more than 2048 bytes long","Option Message in English language is too long to send to an iOS device. You can either make the content shorter or shorten or remove the other options."]}
uri = URI.parse('https://onesignal.com/api/v1/notifications')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path,'Content-Type' => 'application/json;charset=utf-8')
request.body = params.as_json.to_json
response = http.request(request)
puts response.body
#TODO: Parse a response indicating that it wasn't successfully delivered, and return false.
return true
end
def OneSignalNotifications.view_devices
url = URI("https://onesignal.com/api/v1/players?app_id=#{ENV["ONESIGNAL_APP_ID"]}&limit=5&offset=offset")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["accept"] = 'text/plain'
request["Authorization"] = "Basic #{ENV["ONESIGNAL_REST_API_KEY"]}"
response = http.request(request)
puts response.read_body
# kody's iPhone Player Id = "7ac839a3-ac8c-4fb3-b3e0-109fbd568090"
end
def OneSignalNotifications.set_badge_count
url = URI("https://onesignal.com/api/v1/notifications?app_id=#{ENV["ONESIGNAL_APP_ID"]}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["accept"] = 'application/json'
request["Authorization"] = "Basic #{ENV["ONESIGNAL_REST_API_KEY"]}"
request["content-type"] = 'application/json'
# request.body = "{\"included_segments\":[\"Subscribed Users\"],\"contents\":{\"en\":\"English or Any Language Message\",\"es\":\"Spanish Message\"},\"name\":\"INTERNAL_CAMPAIGN_NAME\"}"
kody_iphone_player_id = "75a38e78-8647-4eca-bec2-e6f196ce9c2a"
# 75a38e78-8647-4eca-bec2-e6f196ce9c2a
# request.body = "{\"include_player_ids\":[\"#{kody_iphone_player_id}\"],\"contents\":{\"en\":\"You have a new message notification!\",\"es\":\"Spanish Message\"},\"name\":\"INTERNAL_CAMPAIGN_NAME\"}"
request.body = {
'include_player_ids' => [kody_iphone_player_id],
'app_id' => ENV["ONESIGNAL_APP_ID"],
'contents' => { 'en' => 'You have a new message notification!' },
'ios_badgeCount' => 1, #TODO: set this to unread message.
'ios_badgeType' => 'SetTo',
# 'ios' => {'badgeCount' => 1}
}.to_json
response = http.request(request)
puts response.read_body
# Process the response
# if response.code.to_i == 200
# puts "iOS badge count has been set successfully."
# else
# puts "Failed to set iOS badge count. Error: #{response.body}"
# end
end
end
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
def send_invoice(invoice)
@invoice = invoice
mail(
to: @invoice.customer_to_invoice.email,
from: @invoice.business.email,
reply_to: @invoice.business.email,
subject: @invoice.title
)
end
def no_refresh_token_error(email_that_failed)
@email = email_that_failed
mail(
to: 'kodyckendall@gmail.com',
subject: "HSC Email failed due to refresh token failure"
)
end
def send_support_ticket(employee, email, phone, message)
@phone = phone
@message = message
@email = email
mail(
to: 'kodyckendall@gmail.com',
subject: "Field Rocket support ticket: #{employee.full_name} from #{employee.business.name}"
)
end
def notify_salesrep_of_payment(payment, rep_email)
@invoice = payment.invoice
@message = "Field Rocket - #{payment.invoice&.job&.customer&.full_name} has made a payment of $#{format("%.2f", payment.amount)} towards #{payment.invoice&.title}"
mail(
to: rep_email,
subject: @message
)
end
def notify_signature_collected(invoice, rep_email)
@invoice = invoice
@message = "Field Rocket - #{invoice.customer&.full_name} has signed #{invoice.title}!. Link: https://#{ENV["BASE_URL"]}/invoices/#{invoice.id}/sign?secret_key=#{invoice.secret_key}"
mail(
to: rep_email,
subject: @message
)
end
def notify_kody_of_quickbooks_failure(invoice, rep_email)
@invoice = invoice
@message = "Field Rocket - Quickbooks Failure for #{invoice.business.name}"
mail(
to: rep_email,
subject: @message
)
end
def send_daily_report(files)
files.each do | file |
attachments[file[:name]] = File.read(file[:path]) if File.exist?(file[:path])
end
mail( to: 'kodyckendall@gmail.com', subject: "Daily Report Status" )
end
end
class ApplicationJob < ActiveJob::Base
end
# app/commands/authorize_api_request.rb
module Api
class AuthorizeApiRequest
prepend SimpleCommand
def initialize(headers = {})
@headers = headers
end
def call
user
end
private
attr_reader :headers
def user
@user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
@user || errors.add(:token, 'Invalid token') && nil
end
def decoded_auth_token
@decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
nil
end
end
end
# app/commands/authenticate_user.rb
# module Api
class AuthenticateUser
prepend SimpleCommand
def initialize(email, password) #email & password used when first authenticating & signing into a user account.
@email = email
@password = password
end
def call #makes a new web token based on the user id.
JsonWebToken.encode(user_id: user.id) if user
end
private
attr_accessor :email, :password
def user
user = Employee.where(email: email).first
return user if user && user.valid_password?(password)
errors.add :user_authentication, 'invalid credentials'
nil
end
end
# end
Basic Information
Edit your account details and settings.
Update Your Password
Change your password.
Profile Settings
Update your public profile with relevant and meaningful information.
<%
badgeLabel = badgeLabel || 'OPEN'
badgeClass= badgeClass || 'badge-light-gray'
title= title || 'Add UI Buttons Page'
comments= comments || 28
assigned= assigned || ''
avatarGroup= avatarGroup || false
avatarCount= avatarCount || 1
avatars= avatars || [
"stack/256_rsz_1andy-lee-642320-unsplash.jpg",
"stack/256_michael-dam-258165-unsplash.jpg",
"stack/256_luke-porter-261779-unsplash.jpg"
]
%>
<%= title %> ml-auto"><%= badgeLabel %>
folder_open
Stack
comment
<%= comments %> comments
A simple yet powerful datepicker powered by Flatpickr. Please read the official plugin documentation for a full list of options.
The DateRangePicker can be attached to any webpage element to pop up two calendars for selecting dates, times, or predefined ranges like "Last 30 Days". Please read the official plugin documentation for a full list of options.
<%
items= [
{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
title: "Founder and CEO"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
title: "Software Engineer"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
title: "Web Designer"
}, {
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
title: "Web Developer"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
title: "Business Analyst"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
title: "Human Resources"
}]
%>
Default Forms
Stack supports all of Bootstrap's default form styling in addition to a handful of new input types and features. Please read the official documentation for a full list of options from Bootstrap's core library.
Form Validation
Provide valuable, actionable feedback to your users with HTML5 form validation. Indicate invalid and valid form fields with .is-invalid and .is-valid classes.
Search
A dedicated form group alternative for search forms that always keeps the submit button as part of the form control.
Merge Group
A slightly modified version of the default input groups that always keeps icons as a part of the form control. Also works with validation.
Rounded
Form control with rounded corners with the .form-control-rounded modifier.
Flush
Remove paddings and borders from a form control with the .form-control-flush modifier.
Toggle
Replaces a standard checkbox input with a toggle button.
Date Picker
A simple yet powerful datepicker powered by Flatpickr. Please read the official plugin documentation for a full list of options.
Input Masking
Mask your form controls depending on the context with the jQuery Mask plugin. Please read the official plugin documentation for a full list of options.
Rich Text Editor
Powerful WYSIWYG text editor powered by Quill. Please read the official plugin documentation for a full list of options.
Some initial bold text
Custom toolbar
Hello World!
Some initial bold text
Drag & Drop File Uploads
Drag and drop file uploads with image previews powered by Dropzone.js. Please read the official plugin documentation for a full list of options.
Select
Customizable select box with support for searching, tagging, remote data sets, infinite scrolling, and many other highly used options powered by Select2. Please read the official plugin documentation for a full list of options.
A simple yet versatile animated spinner component. Add .loader-[sm|lg] or .is-loading-[sm|lg] to create loaders at different sizes.
Indicate loading state of nearly any component with an .is-loading modifier.
Using Bootstrap’s typical naming structure i.e. .loader-primary, you can create colored loaders depending on your needs.
Just add the data attribute data-toggle="dragula" to have drag and drop support in your container.
<% leftCards = [{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
title: "Founder and CEO"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
title: "Software Engineer"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
title: "Web Designer"
}] %>
<% rightCards = [{
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
title: "Web Developer"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
title: "Business Analyst"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
title: "Human Resources"
}] %>
Add the data attribute data-dragula-containers='["#cards-left", "#cards-right"]' to allow the user to drag an element in any of the containers and drop it in any other container in the list.
Add the data attribute data-dragula-moves="js-dragula-handle" to allow the user to drag an element in any of the containers and drop it in any other container in the list using a handle.
Toggle Checkboxes
A tiny plugin which adds the ability to toggle all checkboxes within a table.
Search
Add search functionality to your tables with List.js. Please read the official plugin documentation for a full list of options.
<%= render 'staff.table' %>
Sort Columns
Add sorting functionality to your tables with by List.js. Please read the official plugin documentation for a full list of options.
<% icons = [{"e84d":{"name":"3d Rotation"},"eb3b":{"name":"Ac Unit"},"e190":{"name":"Access Alarm"},"e191":{"name":"Access Alarms"},"e192":{"name":"Access Time"},"e84e":{"name":"Accessibility"},"e914":{"name":"Accessible"},"e84f":{"name":"Account Balance"},"e850":{"name":"Account Balance Wallet"},"e851":{"name":"Account Box"},"e853":{"name":"Account Circle"},"e60e":{"name":"Adb"},"e145":{"name":"Add"},"e439":{"name":"Add A Photo"},"e193":{"name":"Add Alarm"},"e003":{"name":"Add Alert"},"e146":{"name":"Add Box"},"e147":{"name":"Add Circle"},"e148":{"name":"Add Circle Outline"},"e567":{"name":"Add Location"},"e854":{"name":"Add Shopping Cart"},"e39d":{"name":"Add To Photos"},"e05c":{"name":"Add To Queue"},"e39e":{"name":"Adjust"},"e630":{"name":"Airline Seat Flat"},"e631":{"name":"Airline Seat Flat Angled"},"e632":{"name":"Airline Seat Individual Suite"},"e633":{"name":"Airline Seat Legroom Extra"},"e634":{"name":"Airline Seat Legroom Normal"},"e635":{"name":"Airline Seat Legroom Reduced"},"e636":{"name":"Airline Seat Recline Extra"},"e637":{"name":"Airline Seat Recline Normal"},"e195":{"name":"Airplanemode Active"},"e194":{"name":"Airplanemode Inactive"},"e055":{"name":"Airplay"},"eb3c":{"name":"Airport Shuttle"},"e855":{"name":"Alarm"},"e856":{"name":"Alarm Add"},"e857":{"name":"Alarm Off"},"e858":{"name":"Alarm On"},"e019":{"name":"Album"},"eb3d":{"name":"All Inclusive"},"e90b":{"name":"All Out"},"e859":{"name":"Android"},"e85a":{"name":"Announcement"},"e5c3":{"name":"Apps"},"e149":{"name":"Archive"},"e5c4":{"name":"Arrow Back"},"e5db":{"name":"Arrow Downward"},"e5c5":{"name":"Arrow Drop Down"},"e5c6":{"name":"Arrow Drop Down Circle"},"e5c7":{"name":"Arrow Drop Up"},"e5c8":{"name":"Arrow Forward"},"e5d8":{"name":"Arrow Upward"},"e060":{"name":"Art Track"},"e85b":{"name":"Aspect Ratio"},"e85c":{"name":"Assessment"},"e85d":{"name":"Assignment"},"e85e":{"name":"Assignment Ind"},"e85f":{"name":"Assignment Late"},"e860":{"name":"Assignment Return"},"e861":{"name":"Assignment Returned"},"e862":{"name":"Assignment Turned In"},"e39f":{"name":"Assistant"},"e3a0":{"name":"Assistant Photo"},"e226":{"name":"Attach File"},"e227":{"name":"Attach Money"},"e2bc":{"name":"Attachment"},"e3a1":{"name":"Audiotrack"},"e863":{"name":"Autorenew"},"e01b":{"name":"Av Timer"},"e14a":{"name":"Backspace"},"e864":{"name":"Backup"},"e19c":{"name":"Battery Alert"},"e1a3":{"name":"Battery Charging Full"},"e1a4":{"name":"Battery Full"},"e1a5":{"name":"Battery Std"},"e1a6":{"name":"Battery Unknown"},"eb3e":{"name":"Beach Access"},"e52d":{"name":"Beenhere"},"e14b":{"name":"Block"},"e1a7":{"name":"Bluetooth"},"e60f":{"name":"Bluetooth Audio"},"e1a8":{"name":"Bluetooth Connected"},"e1a9":{"name":"Bluetooth Disabled"},"e1aa":{"name":"Bluetooth Searching"},"e3a2":{"name":"Blur Circular"},"e3a3":{"name":"Blur Linear"},"e3a4":{"name":"Blur Off"},"e3a5":{"name":"Blur On"},"e865":{"name":"Book"},"e866":{"name":"Bookmark"},"e867":{"name":"Bookmark Border"},"e228":{"name":"Border All"},"e229":{"name":"Border Bottom"},"e22a":{"name":"Border Clear"},"e22b":{"name":"Border Color"},"e22c":{"name":"Border Horizontal"},"e22d":{"name":"Border Inner"},"e22e":{"name":"Border Left"},"e22f":{"name":"Border Outer"},"e230":{"name":"Border Right"},"e231":{"name":"Border Style"},"e232":{"name":"Border Top"},"e233":{"name":"Border Vertical"},"e06b":{"name":"Branding Watermark"},"e3a6":{"name":"Brightness 1"},"e3a7":{"name":"Brightness 2"},"e3a8":{"name":"Brightness 3"},"e3a9":{"name":"Brightness 4"},"e3aa":{"name":"Brightness 5"},"e3ab":{"name":"Brightness 6"},"e3ac":{"name":"Brightness 7"},"e1ab":{"name":"Brightness Auto"},"e1ac":{"name":"Brightness High"},"e1ad":{"name":"Brightness Low"},"e1ae":{"name":"Brightness Medium"},"e3ad":{"name":"Broken Image"},"e3ae":{"name":"Brush"},"e6dd":{"name":"Bubble Chart"},"e868":{"name":"Bug Report"},"e869":{"name":"Build"},"e43c":{"name":"Burst Mode"},"e0af":{"name":"Business"},"eb3f":{"name":"Business Center"},"e86a":{"name":"Cached"},"e7e9":{"name":"Cake"},"e0b0":{"name":"Call"},"e0b1":{"name":"Call End"},"e0b2":{"name":"Call Made"},"e0b3":{"name":"Call Merge"},"e0b4":{"name":"Call Missed"},"e0e4":{"name":"Call Missed Outgoing"},"e0b5":{"name":"Call Received"},"e0b6":{"name":"Call Split"},"e06c":{"name":"Call To Action"},"e3af":{"name":"Camera"},"e3b0":{"name":"Camera Alt"},"e8fc":{"name":"Camera Enhance"},"e3b1":{"name":"Camera Front"},"e3b2":{"name":"Camera Rear"},"e3b3":{"name":"Camera Roll"},"e5c9":{"name":"Cancel"},"e8f6":{"name":"Card Giftcard"},"e8f7":{"name":"Card Membership"},"e8f8":{"name":"Card Travel"},"eb40":{"name":"Casino"},"e307":{"name":"Cast"},"e308":{"name":"Cast Connected"},"e3b4":{"name":"Center Focus Strong"},"e3b5":{"name":"Center Focus Weak"},"e86b":{"name":"Change History"},"e0b7":{"name":"Chat"},"e0ca":{"name":"Chat Bubble"},"e0cb":{"name":"Chat Bubble Outline"},"e5ca":{"name":"Check"},"e834":{"name":"Check Box"},"e835":{"name":"Check Box Outline Blank"},"e86c":{"name":"Check Circle"},"e5cb":{"name":"Chevron Left"},"e5cc":{"name":"Chevron Right"},"eb41":{"name":"Child Care"},"eb42":{"name":"Child Friendly"},"e86d":{"name":"Chrome Reader Mode"},"e86e":{"name":"Class"},"e14c":{"name":"Clear"},"e0b8":{"name":"Clear All"},"e5cd":{"name":"Close"},"e01c":{"name":"Closed Caption"},"e2bd":{"name":"Cloud"},"e2be":{"name":"Cloud Circle"},"e2bf":{"name":"Cloud Done"},"e2c0":{"name":"Cloud Download"},"e2c1":{"name":"Cloud Off"},"e2c2":{"name":"Cloud Queue"},"e2c3":{"name":"Cloud Upload"},"e86f":{"name":"Code"},"e3b6":{"name":"Collections"},"e431":{"name":"Collections Bookmark"},"e3b7":{"name":"Color Lens"},"e3b8":{"name":"Colorize"},"e0b9":{"name":"Comment"},"e3b9":{"name":"Compare"},"e915":{"name":"Compare Arrows"},"e30a":{"name":"Computer"},"e638":{"name":"Confirmation Number"},"e0d0":{"name":"Contact Mail"},"e0cf":{"name":"Contact Phone"},"e0ba":{"name":"Contacts"},"e14d":{"name":"Content Copy"},"e14e":{"name":"Content Cut"},"e14f":{"name":"Content Paste"},"e3ba":{"name":"Control Point"},"e3bb":{"name":"Control Point Duplicate"},"e90c":{"name":"Copyright"},"e150":{"name":"Create"},"e2cc":{"name":"Create New Folder"},"e870":{"name":"Credit Card"},"e3be":{"name":"Crop"},"e3bc":{"name":"Crop 16 9"},"e3bd":{"name":"Crop 3 2"},"e3bf":{"name":"Crop 5 4"},"e3c0":{"name":"Crop 7 5"},"e3c1":{"name":"Crop Din"},"e3c2":{"name":"Crop Free"},"e3c3":{"name":"Crop Landscape"},"e3c4":{"name":"Crop Original"},"e3c5":{"name":"Crop Portrait"},"e437":{"name":"Crop Rotate"},"e3c6":{"name":"Crop Square"},"e871":{"name":"Dashboard"},"e1af":{"name":"Data Usage"},"e916":{"name":"Date Range"},"e3c7":{"name":"Dehaze"},"e872":{"name":"Delete"},"e92b":{"name":"Delete Forever"},"e16c":{"name":"Delete Sweep"},"e873":{"name":"Description"},"e30b":{"name":"Desktop Mac"},"e30c":{"name":"Desktop Windows"},"e3c8":{"name":"Details"},"e30d":{"name":"Developer Board"},"e1b0":{"name":"Developer Mode"},"e335":{"name":"Device Hub"},"e1b1":{"name":"Devices"},"e337":{"name":"Devices Other"},"e0bb":{"name":"Dialer Sip"},"e0bc":{"name":"Dialpad"},"e52e":{"name":"Directions"},"e52f":{"name":"Directions Bike"},"e532":{"name":"Directions Boat"},"e530":{"name":"Directions Bus"},"e531":{"name":"Directions Car"},"e534":{"name":"Directions Railway"},"e566":{"name":"Directions Run"},"e533":{"name":"Directions Subway"},"e535":{"name":"Directions Transit"},"e536":{"name":"Directions Walk"},"e610":{"name":"Disc Full"},"e875":{"name":"Dns"},"e612":{"name":"Do Not Disturb"},"e611":{"name":"Do Not Disturb Alt"},"e643":{"name":"Do Not Disturb Off"},"e644":{"name":"Do Not Disturb On"},"e30e":{"name":"Dock"},"e7ee":{"name":"Domain"},"e876":{"name":"Done"},"e877":{"name":"Done All"},"e917":{"name":"Donut Large"},"e918":{"name":"Donut Small"},"e151":{"name":"Drafts"},"e25d":{"name":"Drag Handle"},"e613":{"name":"Drive Eta"},"e1b2":{"name":"Dvr"},"e3c9":{"name":"Edit"},"e568":{"name":"Edit Location"},"e8fb":{"name":"Eject"},"e0be":{"name":"Email"},"e63f":{"name":"Enhanced Encryption"},"e01d":{"name":"Equalizer"},"e000":{"name":"Error"},"e001":{"name":"Error Outline"},"e926":{"name":"Euro Symbol"},"e56d":{"name":"Ev Station"},"e878":{"name":"Event"},"e614":{"name":"Event Available"},"e615":{"name":"Event Busy"},"e616":{"name":"Event Note"},"e903":{"name":"Event Seat"},"e879":{"name":"Exit To App"},"e5ce":{"name":"Expand Less"},"e5cf":{"name":"Expand More"},"e01e":{"name":"Explicit"},"e87a":{"name":"Explore"},"e3ca":{"name":"Exposure"},"e3cb":{"name":"Exposure Neg 1"},"e3cc":{"name":"Exposure Neg 2"},"e3cd":{"name":"Exposure Plus 1"},"e3ce":{"name":"Exposure Plus 2"},"e3cf":{"name":"Exposure Zero"},"e87b":{"name":"Extension"},"e87c":{"name":"Face"},"e01f":{"name":"Fast Forward"},"e020":{"name":"Fast Rewind"},"e87d":{"name":"Favorite"},"e87e":{"name":"Favorite Border"},"e06d":{"name":"Featured Play List"},"e06e":{"name":"Featured Video"},"e87f":{"name":"Feedback"},"e05d":{"name":"Fiber Dvr"},"e061":{"name":"Fiber Manual Record"},"e05e":{"name":"Fiber New"},"e06a":{"name":"Fiber Pin"},"e062":{"name":"Fiber Smart Record"},"e2c4":{"name":"File Download"},"e2c6":{"name":"File Upload"},"e3d3":{"name":"Filter"},"e3d0":{"name":"Filter 1"},"e3d1":{"name":"Filter 2"},"e3d2":{"name":"Filter 3"},"e3d4":{"name":"Filter 4"},"e3d5":{"name":"Filter 5"},"e3d6":{"name":"Filter 6"},"e3d7":{"name":"Filter 7"},"e3d8":{"name":"Filter 8"},"e3d9":{"name":"Filter 9"},"e3da":{"name":"Filter 9 Plus"},"e3db":{"name":"Filter B And W"},"e3dc":{"name":"Filter Center Focus"},"e3dd":{"name":"Filter Drama"},"e3de":{"name":"Filter Frames"},"e3df":{"name":"Filter Hdr"},"e152":{"name":"Filter List"},"e3e0":{"name":"Filter None"},"e3e2":{"name":"Filter Tilt Shift"},"e3e3":{"name":"Filter Vintage"},"e880":{"name":"Find In Page"},"e881":{"name":"Find Replace"},"e90d":{"name":"Fingerprint"},"e5dc":{"name":"First Page"},"eb43":{"name":"Fitness Center"},"e153":{"name":"Flag"},"e3e4":{"name":"Flare"},"e3e5":{"name":"Flash Auto"},"e3e6":{"name":"Flash Off"},"e3e7":{"name":"Flash On"},"e539":{"name":"Flight"},"e904":{"name":"Flight Land"},"e905":{"name":"Flight Takeoff"},"e3e8":{"name":"Flip"},"e882":{"name":"Flip To Back"},"e883":{"name":"Flip To Front"},"e2c7":{"name":"Folder"},"e2c8":{"name":"Folder Open"},"e2c9":{"name":"Folder Shared"},"e617":{"name":"Folder Special"},"e167":{"name":"Font Download"},"e234":{"name":"Format Align Center"},"e235":{"name":"Format Align Justify"},"e236":{"name":"Format Align Left"},"e237":{"name":"Format Align Right"},"e238":{"name":"Format Bold"},"e239":{"name":"Format Clear"},"e23a":{"name":"Format Color Fill"},"e23b":{"name":"Format Color Reset"},"e23c":{"name":"Format Color Text"},"e23d":{"name":"Format Indent Decrease"},"e23e":{"name":"Format Indent Increase"},"e23f":{"name":"Format Italic"},"e240":{"name":"Format Line Spacing"},"e241":{"name":"Format List Bulleted"},"e242":{"name":"Format List Numbered"},"e243":{"name":"Format Paint"},"e244":{"name":"Format Quote"},"e25e":{"name":"Format Shapes"},"e245":{"name":"Format Size"},"e246":{"name":"Format Strikethrough"},"e247":{"name":"Format Textdirection L To R"},"e248":{"name":"Format Textdirection R To L"},"e249":{"name":"Format Underlined"},"e0bf":{"name":"Forum"},"e154":{"name":"Forward"},"e056":{"name":"Forward 10"},"e057":{"name":"Forward 30"},"e058":{"name":"Forward 5"},"eb44":{"name":"Free Breakfast"},"e5d0":{"name":"Fullscreen"},"e5d1":{"name":"Fullscreen Exit"},"e24a":{"name":"Functions"},"e927":{"name":"G Translate"},"e30f":{"name":"Gamepad"},"e021":{"name":"Games"},"e90e":{"name":"Gavel"},"e155":{"name":"Gesture"},"e884":{"name":"Get App"},"e908":{"name":"Gif"},"eb45":{"name":"Golf Course"},"e1b3":{"name":"Gps Fixed"},"e1b4":{"name":"Gps Not Fixed"},"e1b5":{"name":"Gps Off"},"e885":{"name":"Grade"},"e3e9":{"name":"Gradient"},"e3ea":{"name":"Grain"},"e1b8":{"name":"Graphic Eq"},"e3eb":{"name":"Grid Off"},"e3ec":{"name":"Grid On"},"e7ef":{"name":"Group"},"e7f0":{"name":"Group Add"},"e886":{"name":"Group Work"},"e052":{"name":"Hd"},"e3ed":{"name":"Hdr Off"},"e3ee":{"name":"Hdr On"},"e3f1":{"name":"Hdr Strong"},"e3f2":{"name":"Hdr Weak"},"e310":{"name":"Headset"},"e311":{"name":"Headset Mic"},"e3f3":{"name":"Healing"},"e023":{"name":"Hearing"},"e887":{"name":"Help"},"e8fd":{"name":"Help Outline"},"e024":{"name":"High Quality"},"e25f":{"name":"Highlight"},"e888":{"name":"Highlight Off"},"e889":{"name":"History"},"e88a":{"name":"Home"},"eb46":{"name":"Hot Tub"},"e53a":{"name":"Hotel"},"e88b":{"name":"Hourglass Empty"},"e88c":{"name":"Hourglass Full"},"e902":{"name":"Http"},"e88d":{"name":"Https"},"e3f4":{"name":"Image"},"e3f5":{"name":"Image Aspect Ratio"},"e0e0":{"name":"Import Contacts"},"e0c3":{"name":"Import Export"},"e912":{"name":"Important Devices"},"e156":{"name":"Inbox"},"e909":{"name":"Indeterminate Check Box"},"e88e":{"name":"Info"},"e88f":{"name":"Info Outline"},"e890":{"name":"Input"},"e24b":{"name":"Insert Chart"},"e24c":{"name":"Insert Comment"},"e24d":{"name":"Insert Drive File"},"e24e":{"name":"Insert Emoticon"},"e24f":{"name":"Insert Invitation"},"e250":{"name":"Insert Link"},"e251":{"name":"Insert Photo"},"e891":{"name":"Invert Colors"},"e0c4":{"name":"Invert Colors Off"},"e3f6":{"name":"Iso"},"e312":{"name":"Keyboard"},"e313":{"name":"Keyboard Arrow Down"},"e314":{"name":"Keyboard Arrow Left"},"e315":{"name":"Keyboard Arrow Right"},"e316":{"name":"Keyboard Arrow Up"},"e317":{"name":"Keyboard Backspace"},"e318":{"name":"Keyboard Capslock"},"e31a":{"name":"Keyboard Hide"},"e31b":{"name":"Keyboard Return"},"e31c":{"name":"Keyboard Tab"},"e31d":{"name":"Keyboard Voice"},"eb47":{"name":"Kitchen"},"e892":{"name":"Label"},"e893":{"name":"Label Outline"},"e3f7":{"name":"Landscape"},"e894":{"name":"Language"},"e31e":{"name":"Laptop"},"e31f":{"name":"Laptop Chromebook"},"e320":{"name":"Laptop Mac"},"e321":{"name":"Laptop Windows"},"e5dd":{"name":"Last Page"},"e895":{"name":"Launch"},"e53b":{"name":"Layers"},"e53c":{"name":"Layers Clear"},"e3f8":{"name":"Leak Add"},"e3f9":{"name":"Leak Remove"},"e3fa":{"name":"Lens"},"e02e":{"name":"Library Add"},"e02f":{"name":"Library Books"},"e030":{"name":"Library Music"},"e90f":{"name":"Lightbulb Outline"},"e919":{"name":"Line Style"},"e91a":{"name":"Line Weight"},"e260":{"name":"Linear Scale"},"e157":{"name":"Link"},"e438":{"name":"Linked Camera"},"e896":{"name":"List"},"e0c6":{"name":"Live Help"},"e639":{"name":"Live Tv"},"e53f":{"name":"Local Activity"},"e53d":{"name":"Local Airport"},"e53e":{"name":"Local Atm"},"e540":{"name":"Local Bar"},"e541":{"name":"Local Cafe"},"e542":{"name":"Local Car Wash"},"e543":{"name":"Local Convenience Store"},"e556":{"name":"Local Dining"},"e544":{"name":"Local Drink"},"e545":{"name":"Local Florist"},"e546":{"name":"Local Gas Station"},"e547":{"name":"Local Grocery Store"},"e548":{"name":"Local Hospital"},"e549":{"name":"Local Hotel"},"e54a":{"name":"Local Laundry Service"},"e54b":{"name":"Local Library"},"e54c":{"name":"Local Mall"},"e54d":{"name":"Local Movies"},"e54e":{"name":"Local Offer"},"e54f":{"name":"Local Parking"},"e550":{"name":"Local Pharmacy"},"e551":{"name":"Local Phone"},"e552":{"name":"Local Pizza"},"e553":{"name":"Local Play"},"e554":{"name":"Local Post Office"},"e555":{"name":"Local Printshop"},"e557":{"name":"Local See"},"e558":{"name":"Local Shipping"},"e559":{"name":"Local Taxi"},"e7f1":{"name":"Location City"},"e1b6":{"name":"Location Disabled"},"e0c7":{"name":"Location Off"},"e0c8":{"name":"Location On"},"e1b7":{"name":"Location Searching"},"e897":{"name":"Lock"},"e898":{"name":"Lock Open"},"e899":{"name":"Lock Outline"},"e3fc":{"name":"Looks"},"e3fb":{"name":"Looks 3"},"e3fd":{"name":"Looks 4"},"e3fe":{"name":"Looks 5"},"e3ff":{"name":"Looks 6"},"e400":{"name":"Looks One"},"e401":{"name":"Looks Two"},"e028":{"name":"Loop"},"e402":{"name":"Loupe"},"e16d":{"name":"Low Priority"},"e89a":{"name":"Loyalty"},"e158":{"name":"Mail"},"e0e1":{"name":"Mail Outline"},"e55b":{"name":"Map"},"e159":{"name":"Markunread"},"e89b":{"name":"Markunread Mailbox"},"e322":{"name":"Memory"},"e5d2":{"name":"Menu"},"e252":{"name":"Merge Type"},"e0c9":{"name":"Message"},"e029":{"name":"Mic"},"e02a":{"name":"Mic None"},"e02b":{"name":"Mic Off"},"e618":{"name":"Mms"},"e253":{"name":"Mode Comment"},"e254":{"name":"Mode Edit"},"e263":{"name":"Monetization On"},"e25c":{"name":"Money Off"},"e403":{"name":"Monochrome Photos"},"e7f2":{"name":"Mood"},"e7f3":{"name":"Mood Bad"},"e619":{"name":"More"},"e5d3":{"name":"More Horiz"},"e5d4":{"name":"More Vert"},"e91b":{"name":"Motorcycle"},"e323":{"name":"Mouse"},"e168":{"name":"Move To Inbox"},"e02c":{"name":"Movie"},"e404":{"name":"Movie Creation"},"e43a":{"name":"Movie Filter"},"e6df":{"name":"Multiline Chart"},"e405":{"name":"Music Note"},"e063":{"name":"Music Video"},"e55c":{"name":"My Location"},"e406":{"name":"Nature"},"e407":{"name":"Nature People"},"e408":{"name":"Navigate Before"},"e409":{"name":"Navigate Next"},"e55d":{"name":"Navigation"},"e569":{"name":"Near Me"},"e1b9":{"name":"Network Cell"},"e640":{"name":"Network Check"},"e61a":{"name":"Network Locked"},"e1ba":{"name":"Network Wifi"},"e031":{"name":"New Releases"},"e16a":{"name":"Next Week"},"e1bb":{"name":"Nfc"},"e641":{"name":"No Encryption"},"e0cc":{"name":"No Sim"},"e033":{"name":"Not Interested"},"e06f":{"name":"Note"},"e89c":{"name":"Note Add"},"e7f4":{"name":"Notifications"},"e7f7":{"name":"Notifications Active"},"e7f5":{"name":"Notifications None"},"e7f6":{"name":"Notifications Off"},"e7f8":{"name":"Notifications Paused"},"e90a":{"name":"Offline Pin"},"e63a":{"name":"Ondemand Video"},"e91c":{"name":"Opacity"},"e89d":{"name":"Open In Browser"},"e89e":{"name":"Open In New"},"e89f":{"name":"Open With"},"e7f9":{"name":"Pages"},"e8a0":{"name":"Pageview"},"e40a":{"name":"Palette"},"e925":{"name":"Pan Tool"},"e40b":{"name":"Panorama"},"e40c":{"name":"Panorama Fish Eye"},"e40d":{"name":"Panorama Horizontal"},"e40e":{"name":"Panorama Vertical"},"e40f":{"name":"Panorama Wide Angle"},"e7fa":{"name":"Party Mode"},"e034":{"name":"Pause"},"e035":{"name":"Pause Circle Filled"},"e036":{"name":"Pause Circle Outline"},"e8a1":{"name":"Payment"},"e7fb":{"name":"People"},"e7fc":{"name":"People Outline"},"e8a2":{"name":"Perm Camera Mic"},"e8a3":{"name":"Perm Contact Calendar"},"e8a4":{"name":"Perm Data Setting"},"e8a5":{"name":"Perm Device Information"},"e8a6":{"name":"Perm Identity"},"e8a7":{"name":"Perm Media"},"e8a8":{"name":"Perm Phone Msg"},"e8a9":{"name":"Perm Scan Wifi"},"e7fd":{"name":"Person"},"e7fe":{"name":"Person Add"},"e7ff":{"name":"Person Outline"},"e55a":{"name":"Person Pin"},"e56a":{"name":"Person Pin Circle"},"e63b":{"name":"Personal Video"},"e91d":{"name":"Pets"},"e0cd":{"name":"Phone"},"e324":{"name":"Phone Android"},"e61b":{"name":"Phone Bluetooth Speaker"},"e61c":{"name":"Phone Forwarded"},"e61d":{"name":"Phone In Talk"},"e325":{"name":"Phone Iphone"},"e61e":{"name":"Phone Locked"},"e61f":{"name":"Phone Missed"},"e620":{"name":"Phone Paused"},"e326":{"name":"Phonelink"},"e0db":{"name":"Phonelink Erase"},"e0dc":{"name":"Phonelink Lock"},"e327":{"name":"Phonelink Off"},"e0dd":{"name":"Phonelink Ring"},"e0de":{"name":"Phonelink Setup"},"e410":{"name":"Photo"},"e411":{"name":"Photo Album"},"e412":{"name":"Photo Camera"},"e43b":{"name":"Photo Filter"},"e413":{"name":"Photo Library"},"e432":{"name":"Photo Size Select Actual"},"e433":{"name":"Photo Size Select Large"},"e434":{"name":"Photo Size Select Small"},"e415":{"name":"Picture As Pdf"},"e8aa":{"name":"Picture In Picture"},"e911":{"name":"Picture In Picture Alt"},"e6c4":{"name":"Pie Chart"},"e6c5":{"name":"Pie Chart Outlined"},"e55e":{"name":"Pin Drop"},"e55f":{"name":"Place"},"e037":{"name":"Play Arrow"},"e038":{"name":"Play Circle Filled"},"e039":{"name":"Play Circle Outline"},"e906":{"name":"Play For Work"},"e03b":{"name":"Playlist Add"},"e065":{"name":"Playlist Add Check"},"e05f":{"name":"Playlist Play"},"e800":{"name":"Plus One"},"e801":{"name":"Poll"},"e8ab":{"name":"Polymer"},"eb48":{"name":"Pool"},"e0ce":{"name":"Portable Wifi Off"},"e416":{"name":"Portrait"},"e63c":{"name":"Power"},"e336":{"name":"Power Input"},"e8ac":{"name":"Power Settings New"},"e91e":{"name":"Pregnant Woman"},"e0df":{"name":"Present To All"},"e8ad":{"name":"Print"},"e645":{"name":"Priority High"},"e80b":{"name":"Public"},"e255":{"name":"Publish"},"e8ae":{"name":"Query Builder"},"e8af":{"name":"Question Answer"},"e03c":{"name":"Queue"},"e03d":{"name":"Queue Music"},"e066":{"name":"Queue Play Next"},"e03e":{"name":"Radio"},"e837":{"name":"Radio Button Checked"},"e836":{"name":"Radio Button Unchecked"},"e560":{"name":"Rate Review"},"e8b0":{"name":"Receipt"},"e03f":{"name":"Recent Actors"},"e91f":{"name":"Record Voice Over"},"e8b1":{"name":"Redeem"},"e15a":{"name":"Redo"},"e5d5":{"name":"Refresh"},"e15b":{"name":"Remove"},"e15c":{"name":"Remove Circle"},"e15d":{"name":"Remove Circle Outline"},"e067":{"name":"Remove From Queue"},"e417":{"name":"Remove Red Eye"},"e928":{"name":"Remove Shopping Cart"},"e8fe":{"name":"Reorder"},"e040":{"name":"Repeat"},"e041":{"name":"Repeat One"},"e042":{"name":"Replay"},"e059":{"name":"Replay 10"},"e05a":{"name":"Replay 30"},"e05b":{"name":"Replay 5"},"e15e":{"name":"Reply"},"e15f":{"name":"Reply All"},"e160":{"name":"Report"},"e8b2":{"name":"Report Problem"},"e56c":{"name":"Restaurant"},"e561":{"name":"Restaurant Menu"},"e8b3":{"name":"Restore"},"e929":{"name":"Restore Page"},"e0d1":{"name":"Ring Volume"},"e8b4":{"name":"Room"},"eb49":{"name":"Room Service"},"e418":{"name":"Rotate 90 Degrees Ccw"},"e419":{"name":"Rotate Left"},"e41a":{"name":"Rotate Right"},"e920":{"name":"Rounded Corner"},"e328":{"name":"Router"},"e921":{"name":"Rowing"},"e0e5":{"name":"Rss Feed"},"e642":{"name":"Rv Hookup"},"e562":{"name":"Satellite"},"e161":{"name":"Save"},"e329":{"name":"Scanner"},"e8b5":{"name":"Schedule"},"e80c":{"name":"School"},"e1be":{"name":"Screen Lock Landscape"},"e1bf":{"name":"Screen Lock Portrait"},"e1c0":{"name":"Screen Lock Rotation"},"e1c1":{"name":"Screen Rotation"},"e0e2":{"name":"Screen Share"},"e623":{"name":"Sd Card"},"e1c2":{"name":"Sd Storage"},"e8b6":{"name":"Search"},"e32a":{"name":"Security"},"e162":{"name":"Select All"},"e163":{"name":"Send"},"e811":{"name":"Sentiment Dissatisfied"},"e812":{"name":"Sentiment Neutral"},"e813":{"name":"Sentiment Satisfied"},"e814":{"name":"Sentiment Very Dissatisfied"},"e815":{"name":"Sentiment Very Satisfied"},"e8b8":{"name":"Settings"},"e8b9":{"name":"Settings Applications"},"e8ba":{"name":"Settings Backup Restore"},"e8bb":{"name":"Settings Bluetooth"},"e8bd":{"name":"Settings Brightness"},"e8bc":{"name":"Settings Cell"},"e8be":{"name":"Settings Ethernet"},"e8bf":{"name":"Settings Input Antenna"},"e8c0":{"name":"Settings Input Component"},"e8c1":{"name":"Settings Input Composite"},"e8c2":{"name":"Settings Input Hdmi"},"e8c3":{"name":"Settings Input Svideo"},"e8c4":{"name":"Settings Overscan"},"e8c5":{"name":"Settings Phone"},"e8c6":{"name":"Settings Power"},"e8c7":{"name":"Settings Remote"},"e1c3":{"name":"Settings System Daydream"},"e8c8":{"name":"Settings Voice"},"e80d":{"name":"Share"},"e8c9":{"name":"Shop"},"e8ca":{"name":"Shop Two"},"e8cb":{"name":"Shopping Basket"},"e8cc":{"name":"Shopping Cart"},"e261":{"name":"Short Text"},"e6e1":{"name":"Show Chart"},"e043":{"name":"Shuffle"},"e1c8":{"name":"Signal Cellular 4 Bar"},"e1cd":{"name":"Signal Cellular Connected No Internet 4 Bar"},"e1ce":{"name":"Signal Cellular No Sim"},"e1cf":{"name":"Signal Cellular Null"},"e1d0":{"name":"Signal Cellular Off"},"e1d8":{"name":"Signal Wifi 4 Bar"},"e1d9":{"name":"Signal Wifi 4 Bar Lock"},"e1da":{"name":"Signal Wifi Off"},"e32b":{"name":"Sim Card"},"e624":{"name":"Sim Card Alert"},"e044":{"name":"Skip Next"},"e045":{"name":"Skip Previous"},"e41b":{"name":"Slideshow"},"e068":{"name":"Slow Motion Video"},"e32c":{"name":"Smartphone"},"eb4a":{"name":"Smoke Free"},"eb4b":{"name":"Smoking Rooms"},"e625":{"name":"Sms"},"e626":{"name":"Sms Failed"},"e046":{"name":"Snooze"},"e164":{"name":"Sort"},"e053":{"name":"Sort By Alpha"},"eb4c":{"name":"Spa"},"e256":{"name":"Space Bar"},"e32d":{"name":"Speaker"},"e32e":{"name":"Speaker Group"},"e8cd":{"name":"Speaker Notes"},"e92a":{"name":"Speaker Notes Off"},"e0d2":{"name":"Speaker Phone"},"e8ce":{"name":"Spellcheck"},"e838":{"name":"Star"},"e83a":{"name":"Star Border"},"e839":{"name":"Star Half"},"e8d0":{"name":"Stars"},"e0d3":{"name":"Stay Current Landscape"},"e0d4":{"name":"Stay Current Portrait"},"e0d5":{"name":"Stay Primary Landscape"},"e0d6":{"name":"Stay Primary Portrait"},"e047":{"name":"Stop"},"e0e3":{"name":"Stop Screen Share"},"e1db":{"name":"Storage"},"e8d1":{"name":"Store"},"e563":{"name":"Store Mall Directory"},"e41c":{"name":"Straighten"},"e56e":{"name":"Streetview"},"e257":{"name":"Strikethrough S"},"e41d":{"name":"Style"},"e5d9":{"name":"Subdirectory Arrow Left"},"e5da":{"name":"Subdirectory Arrow Right"},"e8d2":{"name":"Subject"},"e064":{"name":"Subscriptions"},"e048":{"name":"Subtitles"},"e56f":{"name":"Subway"},"e8d3":{"name":"Supervisor Account"},"e049":{"name":"Surround Sound"},"e0d7":{"name":"Swap Calls"},"e8d4":{"name":"Swap Horiz"},"e8d5":{"name":"Swap Vert"},"e8d6":{"name":"Swap Vertical Circle"},"e41e":{"name":"Switch Camera"},"e41f":{"name":"Switch Video"},"e627":{"name":"Sync"},"e628":{"name":"Sync Disabled"},"e629":{"name":"Sync Problem"},"e62a":{"name":"System Update"},"e8d7":{"name":"System Update Alt"},"e8d8":{"name":"Tab"},"e8d9":{"name":"Tab Unselected"},"e32f":{"name":"Tablet"},"e330":{"name":"Tablet Android"},"e331":{"name":"Tablet Mac"},"e420":{"name":"Tag Faces"},"e62b":{"name":"Tap And Play"},"e564":{"name":"Terrain"},"e262":{"name":"Text Fields"},"e165":{"name":"Text Format"},"e0d8":{"name":"Textsms"},"e421":{"name":"Texture"},"e8da":{"name":"Theaters"},"e8db":{"name":"Thumb Down"},"e8dc":{"name":"Thumb Up"},"e8dd":{"name":"Thumbs Up Down"},"e62c":{"name":"Time To Leave"},"e422":{"name":"Timelapse"},"e922":{"name":"Timeline"},"e425":{"name":"Timer"},"e423":{"name":"Timer 10"},"e424":{"name":"Timer 3"},"e426":{"name":"Timer Off"},"e264":{"name":"Title"},"e8de":{"name":"Toc"},"e8df":{"name":"Today"},"e8e0":{"name":"Toll"},"e427":{"name":"Tonality"},"e913":{"name":"Touch App"},"e332":{"name":"Toys"},"e8e1":{"name":"Track Changes"},"e565":{"name":"Traffic"},"e570":{"name":"Train"},"e571":{"name":"Tram"},"e572":{"name":"Transfer Within A Station"},"e428":{"name":"Transform"},"e8e2":{"name":"Translate"},"e8e3":{"name":"Trending Down"},"e8e4":{"name":"Trending Flat"},"e8e5":{"name":"Trending Up"},"e429":{"name":"Tune"},"e8e6":{"name":"Turned In"},"e8e7":{"name":"Turned In Not"},"e333":{"name":"Tv"},"e169":{"name":"Unarchive"},"e166":{"name":"Undo"},"e5d6":{"name":"Unfold Less"},"e5d7":{"name":"Unfold More"},"e923":{"name":"Update"},"e1e0":{"name":"Usb"},"e8e8":{"name":"Verified User"},"e258":{"name":"Vertical Align Bottom"},"e259":{"name":"Vertical Align Center"},"e25a":{"name":"Vertical Align Top"},"e62d":{"name":"Vibration"},"e070":{"name":"Video Call"},"e071":{"name":"Video Label"},"e04a":{"name":"Video Library"},"e04b":{"name":"Videocam"},"e04c":{"name":"Videocam Off"},"e338":{"name":"Videogame Asset"},"e8e9":{"name":"View Agenda"},"e8ea":{"name":"View Array"},"e8eb":{"name":"View Carousel"},"e8ec":{"name":"View Column"},"e42a":{"name":"View Comfy"},"e42b":{"name":"View Compact"},"e8ed":{"name":"View Day"},"e8ee":{"name":"View Headline"},"e8ef":{"name":"View List"},"e8f0":{"name":"View Module"},"e8f1":{"name":"View Quilt"},"e8f2":{"name":"View Stream"},"e8f3":{"name":"View Week"},"e435":{"name":"Vignette"},"e8f4":{"name":"Visibility"},"e8f5":{"name":"Visibility Off"},"e62e":{"name":"Voice Chat"},"e0d9":{"name":"Voicemail"},"e04d":{"name":"Volume Down"},"e04e":{"name":"Volume Mute"},"e04f":{"name":"Volume Off"},"e050":{"name":"Volume Up"},"e0da":{"name":"Vpn Key"},"e62f":{"name":"Vpn Lock"},"e1bc":{"name":"Wallpaper"},"e002":{"name":"Warning"},"e334":{"name":"Watch"},"e924":{"name":"Watch Later"},"e42c":{"name":"Wb Auto"},"e42d":{"name":"Wb Cloudy"},"e42e":{"name":"Wb Incandescent"},"e436":{"name":"Wb Iridescent"},"e430":{"name":"Wb Sunny"},"e63d":{"name":"Wc"},"e051":{"name":"Web"},"e069":{"name":"Web Asset"},"e16b":{"name":"Weekend"},"e80e":{"name":"Whatshot"},"e1bd":{"name":"Widgets"},"e63e":{"name":"Wifi"},"e1e1":{"name":"Wifi Lock"},"e1e2":{"name":"Wifi Tethering"},"e8f9":{"name":"Work"},"e25b":{"name":"Wrap Text"},"e8fa":{"name":"Youtube Searched For"},"e8ff":{"name":"Zoom In"},"e900":{"name":"Zoom Out"},"e56b":{"name":"Zoom Out Map"}}]
%>
event
Wednesday, 24 Oct
<% items = [{
time: "12:30 PM",
title: "#4813 - Fix email alignment on iPhone 7",
badge_label: "NEW",
badge_class: "badge-light-gray",
started_by: "Michael S.",
tag: "DEPLOYED",
assigned_image: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
assigned: "Sherri Cardenas"
}, {
time: "07:48 PM",
title: "#4813 - Create account profile page",
badge_label: "RESOLVED",
badge_class: "badge-success",
started_by: "Adrian D.",
tag: "CONCEPT",
assigned_image: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
assigned: "Jenell Matney"
}, {
time: "03:13 PM",
title: "#4811 - Design new Dashboard",
badge_label: "PRO",
badge_class: "badge-warning",
started_by: "John L.",
tag: "WIREFRAME",
assigned_image: "stack/256_jeremy-banks-798787-unsplash.jpg",
assigned: "Joseph Ferland"
}, {
time: "12:30 PM",
title: "#4901 - Design Pricing Table",
badge_label: "NEW",
badge_class: "badge-light-gray",
started_by: "Michael S.",
tag: "DEPLOYED",
assigned_image: "stack/256_joao-silas-636453-unsplash.jpg",
assigned: "Bryan Davis"
}] %>
<% items.each do |item| %>
<%= render 'projects_activity_item',
time: item[:time],
title: item[:title],
badge_label:item[:badge_label],
badge_class:item[:badge_class],
started_by:item[:started_by],
tag:item[:tag],
assigned_image:item[:assigned_image],
assigned: item[:assigned]
%>
<% end %>
event
Tuesday, 23 Oct
<% items = [{
time: "07:48 PM",
title: "#4923 - Fix spacings on Stories page",
badge_label: "RESOLVED",
badge_class: "badge-success",
started_by: "Adrian D.",
tag: "CONCEPT",
assigned_image: "stack/256_luke-porter-261779-unsplash.jpg",
assigned: "Elizabeth Ohara"
}, {
time: "03:13 PM",
title: "#4813 - Fix email alignment on iPhone 7",
badge_label: "NEW",
badge_class: "badge-light-gray",
started_by: "John L.",
tag: "WIREFRAME",
assigned_image: "stack/256_michael-dam-258165-unsplash.jpg",
assigned: "Kaci Langston"
}] %>
<% items.each do |item| %>
<%= render 'projects_activity_item',
time: item[:time],
title: item[:title],
badge_label:item[:badge_label],
badge_class:item[:badge_class],
started_by:item[:started_by],
tag:item[:tag],
assigned_image:item[:assigned_image],
assigned: item[:assigned]
%>
<% end %>
Drag and drop your event or click in the calendar.
<% content_for :footer do %>
<% end %>
<% pricing = [{
title: 'Personal',
amount: 29,
features: [
'HTML/SCSS/JS',
'6 Months Support'
]
}, {
title: 'Developers',
amount: 59,
popular: true,
btn_variant: 'primary',
features: [
'HTML/SCSS/JS',
'6 Months Support',
'HTML/SCSS/JS',
'6 Months Support'
]
}, {
title: 'Personal',
amount: 199,
features: [
'HTML/SCSS/JS',
'6 Months Support',
'HTML/SCSS/JS',
'6 Months Support',
'HTML/SCSS/JS',
'6 Months Support'
]
}] %>
Use button classes on an "<%=h "" %>" , "<%=h "" %>", or "<%=h "" %>" element.
Use the .btn-outline-* classes to create bordered buttons.
Add rounded to buttons to get large rounded corners.
Use the .btn-outline-* and .btn-rounded classes together to quickly create bordered buttons with large rounded corners.
Add .btn-lg or .btn-sm classes for additional button sizes.
Add icons to any button or even use Icon-only buttons.
Create block level buttons, by adding add .btn-block.
Create a group of buttons by wrapping with .btn-group.
Add the disabled attribute to disable buttons.
<% @body_class = 'layout-login-centered-boxed' %>
">
<% stories = [{
avatar: "stack/stories/256_rsz_clem-onojeghuo-193397-unsplash.jpg",
title: "Cheap Romantic Vacations",
views: 35,
tag: "future",
date: "11 Nov, 2018 07:46 AM",
avatarGroup: true,
avatarCount: 3,
avatars: [
"stack/256_rsz_1andy-lee-642320-unsplash.jpg",
"stack/256_michael-dam-258165-unsplash.jpg",
"stack/256_luke-porter-261779-unsplash.jpg"
]
}, {
avatar: "stack/stories/256_rsz_dex-ezekiel-761373-unsplash.jpg",
title: "Copper Canyon",
views: 78,
tag: "future",
date: "10 Nov, 2018 08:25 AM",
avatarGroup: true,
avatarCount: 2,
avatars: [
"stack/256_joao-silas-636453-unsplash.jpg",
"stack/256_jeremy-banks-798787-unsplash.jpg",
"stack/256_daniel-gaffey-1060698-unsplash.jpg"
]
}, {
avatar: "stack/stories/256_rsz_rhema-kallianpur-275251-unsplash.jpg",
title: "Selecting The Right Hotel",
views: 78,
tag: "video",
date: "09 Nov, 2018 07:13 PM",
avatarGroup: true,
avatarCount: 1,
avatars: [
"stack/256_rsz_1andy-lee-642320-unsplash.jpg",
"stack/256_michael-dam-258165-unsplash.jpg",
"stack/256_luke-porter-261779-unsplash.jpg"
]
}] %>
<% popular_stories = [{
image: "stack/stories/256_rsz_euan-carmichael-666378-unsplash.jpg",
category: "blog",
title: "Tremblant In Canada",
avatar: "stack/256_luke-porter-261779-unsplash.jpg"
}, {
image: "stack/stories/256_rsz_jared-rice-388260-unsplash.jpg",
category: "blog",
title: "Become A Travel Pro In One Easy Lesson",
avatar: "stack/256_michael-dam-258165-unsplash.jpg"
}, {
image: "stack/stories/256_rsz_dex-ezekiel-761373-unsplash.jpg",
category: "blog",
title: "A Guide To Rocky Mountain Vacations",
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg"
}] %>
<%
@chat = [{
avatar: "256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
date: "4 days ago"
}, {
avatar: "256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
date: "3 days ago"
}, {
avatar: "256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
date: "2 days ago"
}, {
avatar: "256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
date: "1 day ago"
}, {
avatar: "256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
date: "4 hours ago"
}, {
avatar: "256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
date: "just now"
}]
%>
<% content_for :footer_scripts do %>
(function(){
'use strict';
Charts.init()
var Views = function(id, type = 'line', options = {}) {
options = Chart.helpers.merge({
elements: {
line: {
fill: 'start',
backgroundColor: settings.charts.colors.area,
tension: 0,
borderWidth: 1
},
point: {
pointStyle: 'circle',
radius: 5,
hoverRadius: 5,
backgroundColor: settings.colors.white,
borderColor: settings.colors.primary[700],
borderWidth: 2
}
},
scales: {
xAxes: [{
gridLines: {
display: false
},
type: 'time',
time: {
unit: 'day'
}
}]
},
tooltips: {
callbacks: {
label: function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel,
r = "";
return 1 < e.datasets.length && (r += '<span class="popover-body-label mr-auto">' + t + "</span>"), r += '<span class="popover-body-value">' + o + " views</span>"
}
}
}
}, options)
var data = []
// Create a date range for the last 7 days
var start = moment().subtract(7, 'days').format('YYYY-MM-DD') // 7 days ago
var end = moment().format('YYYY-MM-DD') // today
var range = moment.range(start, end)
// Create the graph data
// Iterate the date range and assign a random value for each day
for (let moment of range.by('days')) {
data.push({
y: Math.floor(Math.random() * 300) + 10,
x: moment.toDate()
})
}
var data = {
datasets: [{
label: "All Views",
data
}]
}
Charts.create(id, type, options, data)
}
///////////////////
// Create Charts //
///////////////////
Views('#viewsChart')
})();
<% end %>
<%
avatar = avatar || "assets/images/stack/256_rsz_1andy-lee-642320-unsplash.jpg"
name = name || "Jenell D. Matney"
title = title || "Founder and CEO"
%>
<%= name %>
<%= title %>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.
<%
@chat = [{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
date: "4 days ago"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
date: "3 days ago"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
date: "2 days ago"
}, {
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
date: "1 day ago"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
date: "4 hours ago"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
date: "just now"
}]
%>
arrow_forward
Role
All Roles
<% content_for :footer_scripts do %>
// DASHBOARD CHART SAMPLES
Charts.init();
var EarningsTraffic = function(id, type = 'line', options = {}) {
options = Chart.helpers.merge({
elements: {
line: {
fill: 'start',
backgroundColor: settings.charts.colors.area
}
}
}, options)
var data = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [{
label: "Traffic",
data: [10, 2, 5, 15, 10, 12, 15, 25, 22, 30, 25, 40]
}]
}
Charts.create(id, type, options, data)
}
var Products = function(id, type = 'line', options = {}, data) {
options = Chart.helpers.merge({
elements: {
line: {
fill: 'start',
backgroundColor: settings.charts.colors.area,
tension: 0,
borderWidth: 1
},
point: {
pointStyle: 'circle',
radius: 5,
hoverRadius: 5,
backgroundColor: settings.colors.white,
borderColor: settings.colors.primary[700],
borderWidth: 2
}
},
scales: {
yAxes: [{
display: false
}],
xAxes: [{
display: false
}]
}
}, options)
data = data || {
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
datasets: [{
label: "Earnings",
data: [400, 200, 450, 460, 220, 380, 800]
}]
}
Charts.create(id, type, options, data)
}
var Courses = function(id, type = 'line', options = {}) {
options = Chart.helpers.merge({
elements: {
line: {
borderColor: settings.colors.success[700],
backgroundColor: settings.hexToRGB(settings.colors.success[100], 0.5)
},
point: {
borderColor: settings.colors.success[700]
}
}
}, options)
Products(id, type, options)
}
var LocationDoughnut = function(id, type = 'doughnut', options = {}) {
options = Chart.helpers.merge({
tooltips: {
callbacks: {
title: function(a, e) {
return e.labels[a[0].index]
},
label: function(a, e) {
var t = "";
return t += '<span class="popover-body-value">' + e.datasets[0].data[a.index] + "%</span>"
}
}
}
}, options)
var data = {
labels: ["United States", "United Kingdom", "Germany", "India"],
datasets: [{
data: [25, 25, 15, 35],
backgroundColor: [settings.colors.success[400], settings.colors.danger[400], settings.colors.primary[500], settings.colors.gray[300]],
hoverBorderColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[800] : settings.colors.white
}]
}
Charts.create(id, type, options, data)
}
///////////////////
// Create Charts //
///////////////////
EarningsTraffic('#earningsTrafficChart')
LocationDoughnut('#locationDoughnutChart')
Products('#productsChart')
Courses('#coursesChart')
<% end %>
<%
time = time || "12:30 PM"
title = title || "#4813 - Fix email alignment on iPhone 7"
badge_label = badge_label || "NEW"
badge_class = badge_class || "badge-light-gray"
started_by = started_by || "Michael S."
tag = tag || "DEPLOYED"
assigned = assigned || "Bryan Davis"
assigned_image = assigned_image || "stack/256_joao-silas-636453-unsplash.jpg"
%>
Toasts based notifications can be used to to show important alerts or information to users.
Primary Example
Info Example
Warning Example
Success Example
Danger Example
Close Button
Making them sticky
Fade/Slide animation
Alerts are available for any length of text, as well as an optional dismiss button. For proper styling, use one of the eight contextual classes (i.e. .alert-success). For background color use class .bg-* and .text-white.
Add a button and the .alert-dismissible class, which adds extra padding to the right of the alert and positions the .close button.
Alternate styles for alerts with softer background color by replacing classes like .alert-primary with .alert-soft-primary. Alerts can also contain additional HTML elements like icons and paragraphs.
Shift and pressing Enter will create a new line in your message without posting it in channel. Learn more about formatting your messages.
A small count and labeling component. Please read the official Bootstrap documentation for a full list of options.
Primary
Secondary
Success
Danger
Warning
Info
Light
Dark
Primary
Secondary
Success
Danger
Warning
Info
Light
Dark
<% content_for :page_scripts do %>
(function(){
'use strict';
$('[data-toggle="tab"]').on('hide.bs.tab', function (e) {
$(e.target).removeClass('active')
})
})()
<% end %>
Adrian Demian
959 Emerson Road
Winnfield, LA
Daniel J. Batiste
3700 George Avenue
Mobile, AL 36602
We appreciate your business. Should you need us to add VAT or extra notes let us know!
A line chart is a way of plotting data points on a line. Often, it is used to show trend data, or the comparison of two data sets.
An area chart or area graph displays graphically quantitative data. It is based on the line chart.
Excellent at showing the relational proportions between data.
A bar chart provides a way of showing data values represented as vertical bars. It is sometimes used to show trend data, and the comparison of multiple data sets side by side.
Easily toggle an additional set of data with a simple interface based on the data attributes.
<% content_for :footer_scripts do %>
(function(){
'use strict';
Charts.init()
var Performance = function(id, type = 'line', options = {}) {
options = Chart.helpers.merge({
scales: {
yAxes: [{
ticks: {
callback: function(a) {
if (!(a % 10))
return "$" + a + "k"
}
}
}]
},
tooltips: {
callbacks: {
label: function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel,
r = "";
return 1 < e.datasets.length && (r += '<span class="popover-body-label mr-auto">' + t + "</span>"), r += '<span class="popover-body-value">$' + o + "k</span>"
}
}
}
}, options)
var data = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [{
label: "Performance",
data: [0, 10, 5, 15, 10, 20, 15, 25, 20, 30, 25, 40]
}]
}
Charts.create(id, type, options, data)
}
var Orders = function(id, type = 'roundedBar', options = {}) {
options = Chart.helpers.merge({
barRoundness: 1.2,
scales: {
yAxes: [{
ticks: {
callback: function(a) {
if (!(a % 10))
return "$" + a + "k"
}
}
}]
},
tooltips: {
callbacks: {
label: function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel,
r = "";
return 1 < e.datasets.length && (r += '<span class="popover-body-label mr-auto">' + t + "</span>"), r += '<span class="popover-body-value">$' + o + "k</span>"
}
}
}
}, options)
var data = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [{
label: "Sales",
data: [25, 20, 30, 22, 17, 10, 18, 26, 28, 26, 20, 32]
}]
}
Charts.create(id, type, options, data)
}
var Devices = function(id, type = 'doughnut', options = {}) {
options = Chart.helpers.merge({
tooltips: {
callbacks: {
title: function(a, e) {
return e.labels[a[0].index]
},
label: function(a, e) {
var t = "";
return t += '<span class="popover-body-value">' + e.datasets[0].data[a.index] + "%</span>"
}
}
}
}, options)
var data = {
labels: ["Desktop", "Tablet", "Mobile"],
datasets: [{
data: [60, 25, 15],
backgroundColor: [settings.colors.primary[700], settings.colors.success[300], settings.colors.success[100]],
hoverBorderColor: "dark" == settings.charts.colorScheme ? settings.colors.gray[800] : settings.colors.white
}]
}
Charts.create(id, type, options, data)
}
///////////////////
// Create Charts //
///////////////////
Performance('#performanceChart')
Performance('#performanceAreaChart', 'line', {
elements: {
line: {
fill: 'start',
backgroundColor: settings.charts.colors.area
}
}
})
Orders('#ordersChart')
Orders('#ordersChartSwitch')
Devices('#devicesChart')
$('[data-toggle="chart"]:checked').each(function (index, el) {
Charts.add($(el))
})
})()
<% end %>
<% @body_class = 'layout-login-centered-boxed' %>
Create an account with Stack
" novalidate>
Using Bootstrap's typical naming structure, you can create a standard avatar, or scale it up to different sizes based on what's needed.
Add an online or offline status indicator to show user's availability.
Change the shape of an avatar with the default Bootstrap image classes.
Change the proportional relationship between the width and height of an avatar to 4 by 3 with an .avatar-4by3 modifier.
You won't always have an image for every user, so easily use initials instead.
Easily group avatars of any size, shape and content with a single component. Each avatar can also use an {{ '' | escape }} to link to the corresponding profile.
<%
avatar = avatar || "assets/images/256_rsz_1andy-lee-642320-unsplash.jpg"
name = name || "Jenell D. Matney"
title = title || "Founder and CEO"
%>
Role
All Roles
<% content_for :footer_scripts do %>
(function(){
'use strict';
Charts.init()
var Orders = function(id, type = 'roundedBar', options = {}) {
options = Chart.helpers.merge({
barRoundness: 1.2,
scales: {
yAxes: [{
ticks: {
callback: function(a) {
if (!(a % 10))
return "$" + a + "k"
}
}
}]
},
tooltips: {
callbacks: {
label: function(a, e) {
var t = e.datasets[a.datasetIndex].label || "",
o = a.yLabel,
r = "";
return 1 < e.datasets.length && (r += '<span class="popover-body-label mr-auto">' + t + "</span>"), r += '<span class="popover-body-value">$' + o + "k</span>"
}
}
}
}, options)
var data = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [{
label: "Sales",
data: [25, 20, 30, 22, 17, 10, 18, 26, 28, 26, 20, 32]
}]
}
Charts.create(id, type, options, data)
}
///////////////////
// Create Charts //
///////////////////
Orders('#ordersChart')
})()
<% end %>
<% tasks.each do |task| %>
<%= render 'trelloTask',
badgeLabel: task[:badgeLabel],
badgeClass: task[:badgeClass],
date: task[:date],
title: task[:title],
comments: task[:comments],
assigned: task[:assigned],
avatarGroup: task[:avatarGroup],
avatarCount: task[:avatarCount] %>
<% end %>
<% tasks.each do |task| %>
<%= render 'trelloTask',
badgeLabel: task[:badgeLabel],
badgeClass: task[:badgeClass],
date: task[:date],
title: task[:title],
comments: task[:comments],
assigned: task[:assigned],
avatarGroup: task[:avatarGroup],
avatarCount: task[:avatarCount] %>
<% end %>
<% tasks.each do |task| %>
<%= render 'trelloTask',
badgeLabel: task[:badgeLabel],
badgeClass: task[:badgeClass],
date: task[:date],
title: task[:title],
comments: task[:comments],
assigned: task[:assigned],
avatarGroup: task[:avatarGroup],
avatarCount: task[:avatarCount] %>
<% end %>
<% tasks.each do |task| %>
<%= render 'trelloTask',
badgeLabel: task[:badgeLabel],
badgeClass: task[:badgeClass],
date: task[:date],
title: task[:title],
comments: task[:comments],
assigned: task[:assigned],
avatarGroup: task[:avatarGroup],
avatarCount: task[:avatarCount] %>
<% end %>
<%
pages = pages || 2
first = first || false
prevItem = prevItem || true
nextItem = nextItem || true
last = last || false
labels = labels || false
align = align || 'left'
className= className || ''
%>
A rendered modal with header, body, and set of actions in the footer.
Show different contexual alert messages using modal component.
Examples of modals with custom content.
By default, modals will be positioned at the top of the page, but you can change this to vertically centered by adding the .modal-dialog-centered class to the <%=h ' element.
Vertically Centered Modal
<% content_for :footer do %>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt eaque explicabo, itaque iusto unde quas magni non, quae expedita eveniet, incidunt molestiae tempora maiores odit hic perspiciatis. Eveniet, porro illo.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt eaque explicabo, itaque iusto unde quas magni non, quae expedita eveniet, incidunt molestiae tempora maiores odit hic perspiciatis. Eveniet, porro illo.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt eaque explicabo, itaque iusto unde quas magni non, quae expedita eveniet, incidunt molestiae tempora maiores odit hic perspiciatis. Eveniet, porro illo.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt eaque explicabo, itaque iusto unde quas magni non, quae expedita eveniet, incidunt molestiae tempora maiores odit hic perspiciatis. Eveniet, porro illo.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Repudiandae aliquam tempora quibusdam rem facere? Nulla dolorum, velit cumque maiores architecto officiis reprehenderit adipisci magnam doloribus dolores ratione error omnis quos voluptate culpa excepturi, autem ipsum! Vitae exercitationem cumque provident, aliquid repudiandae placeat necessitatibus?
Temporibus delectus soluta necessitatibus a est dolores quos esse enim, sint maxime assumenda sapiente harum tempora eius ullam. Earum quam, explicabo magni, blanditiis sint, nam reprehenderit porro suscipit voluptates cum eius. Pariatur vel rerum, saepe, rem harum, nam ipsum deserunt vitae odio, quaerat sapiente nulla! Nulla nesciunt labore, distinctio aut, aliquam possimus sapiente qui adipisci quae fuga, consectetur hic. Facilis nostrum officiis saepe quia nemo, adipisci libero illo sint omnis. Placeat doloremque, omnis eligendi ullam fugit, aut.
Aliquam maxime nobis ut porro sit, repellendus beatae provident suscipit at soluta sapiente, cupiditate dolore similique alias doloribus aperiam, veritatis quibusdam numquam adipisci sequi quasi rerum architecto inventore vitae fugiat. Odio corporis est, temporibus earum molestias quos, labore dignissimos eligendi, laboriosam, incidunt illum!
Itaque officia repellat temporibus quis quasi ipsum reprehenderit dicta pariatur tenetur nisi officiis ad eaque veritatis velit dolore eos iste, beatae labore. Quo, incidunt vitae sint tempore, delectus sapiente pariatur labore illo veniam itaque molestiae consequatur doloribus.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
<%# REQUIRED BODY CLASS FOR SCROLL AND APP LIKE BEHAVIOUR %>
<% @body_class = 'layout-fluid app-chat' %>
<% chat_items = [{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
date: "4 days ago",
message: "Hey guys, just kicking things off here in the chat window. Hope you're all ready to tackle this great project. Let's smash some Brand Concept & Design!"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
date: "3 days ago",
message: "Nice one @Claire, we've got some killer ideas kicking about already.",
image: "stack/stories/256_rsz_dex-ezekiel-761373-unsplash.jpg"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
date: "2 days ago",
message: "Roger that boss! @Ravi and I have already started gathering some stuff for the mood boards, excited to start! 🔥"
}, {
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
date: "1 day ago",
message: "😉"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
date: "4 hours ago",
message: "Can't wait! @David how are we coming along with the Client Objective Meeting?"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
date: "just now",
message: "Coming along nicely, we've got a draft for the client questionnaire completed, take a look! 🤓",
file: {
name: "draft.doc",
size: "15kb"
}
}] %>
<% members = [{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
title: "Founder and CEO"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
title: "Software Engineer"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
title: "Web Designer"
}, {
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
title: "Web Developer"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
title: "Business Analyst"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
title: "Human Resources"
}] %>
<%
@chat = [{
avatar: "stack/256_rsz_1andy-lee-642320-unsplash.jpg",
name: "Jenell D. Matney",
date: "4 days ago"
}, {
avatar: "stack/256_daniel-gaffey-1060698-unsplash.jpg",
name: "Sherri J. Cardenas",
date: "3 days ago"
}, {
avatar: "stack/256_jeremy-banks-798787-unsplash.jpg",
name: "Joseph S. Ferland",
date: "2 days ago"
}, {
avatar: "stack/256_joao-silas-636453-unsplash.jpg",
name: "Bryan K. Davis",
date: "1 day ago"
}, {
avatar: "stack/256_luke-porter-261779-unsplash.jpg",
name: "Elizabeth J. Ohara",
date: "4 hours ago"
}, {
avatar: "stack/256_michael-dam-258165-unsplash.jpg",
name: "Kaci M. Langston",
date: "just now"
}]
%>
<% members.each do |item| %>
<%= render 'form', review: @review %>
<%= link_to 'Back', reviews_path %>
<%= notice %>
Name:
<%= @review.name %>
City:
<%= @review.city %>
Content:
<%= @review.content %>
Home page:
<%= @review.home_page %>
Business:
<%= @review.business %>
<%= link_to 'Edit', edit_review_path(@review) %> |
<%= link_to 'Back', reviews_path %>
json.extract! review, :id, :name, :city, :content, :home_page_id, :created_at, :updated_at
json.url review_url(review, format: :json)
<%= render 'form', review: @review %>
<%= link_to 'Show', @review %> |
<%= link_to 'Back', reviews_path %>
json.array! @reviews, partial: "reviews/review", as: :review
json.partial! "reviews/review", review: @review
<%= notice %>
Name
City
Content
Home page
<% @reviews.each do |review| %>
<%= review.name %>
<%= review.city %>
<%= review.content %>
<%= review.home_page %>
<%= link_to 'Show', review %>
<%= link_to 'Edit', edit_review_path(review) %>
<%= link_to 'Destroy', review, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Review', new_review_path %>
<%= form_with(model: review, local: true) do |form| %>
<% if review.errors.any? %>
<%= notice %>
json.array! @email_templates, partial: 'email_templates/email_template', as: :email_template
json.partial! "email_templates/email_template", email_template: @email_template
<%= notice %>
<%= react_component 'shared/IndexPage', {
title: "Email Templates",
attributes: [:name],
data: @email_templates.map {|email_template| [email_template.id, {title: email_template.name, url: "email_templates/#{email_template.id}"}]},
basePath: '/email_templates'} %>
<%= form_with(model: email_template, local: true) do |form| %>
<% if email_template.errors.any? %>
<%= form_group form, email_template, :name, value: @email_template.name %>
Body
<%= render partial: "shared/text_editor", locals: {id: "email_body_text_editor", placeholder: "Enter Email Body Here", content: raw(@email_template.body)}%>
<%= form.text_field :body, style:"display:none"%>
<%= form.text_field :business_id, style:"display:none", value: current_business.id %>
<%= content_for :footer_scripts do %>
$('#saveEmailTemplate').click(function(e){
$('#email_template_body').val($('#email_body_text_editor')[0].children[0].innerHTML);
$('.loader').show();
});
<% end %>
json.extract! email_template, :id, :name, :body, :created_at, :updated_at
json.url email_template_url(email_template, format: :json)
<%= render 'form', call: @call %>
<%= link_to 'Back', calls_path %>
<%= notice %>
To:
<%= @call.to %>
Call from:
<%= @call.call_from %>
Caller name:
<%= @call.caller_name %>
Recording uri:
<%= @call.recording_uri %>
Price:
<%= @call.price %>
Start time:
<%= @call.start_time %>
End time:
<%= @call.end_time %>
Duration:
<%= @call.duration %>
Status:
<%= @call.status %>
Uri:
<%= @call.uri %>
<%= link_to 'Edit', edit_call_path(@call) %> |
<%= link_to 'Back', calls_path %>
<%= content_for :footer_scripts do %>
$("#transcribeCallWithAi").click(function(e) {
e.preventDefault();
$.ajax({
url: "/calls/<%=@call.id%>/transcribe",
type: "POST"});
});
$('#parseTranscriptionWithAi').click(function(e) {
e.preventDefault();
$.ajax({
url: "/calls/<%=@call.id%>/parse_transcription",
type: "POST"});
});
<% end %>
json.array! @calls, partial: "calls/call", as: :call
json.partial! "calls/call", call: @call
<%= notice %>
Time
Call from
Call to
Caller name
Listen
Duration
Status
Notes
<% @calls.order(:created_at).each do |call| %>
<% if call.is_active %>
/edit?secret_key=<%=@business.secret_key%>">ACTIVE
<% else %>
/edit?secret_key=<%=@business.secret_key%>">Ended
<% end %>
<%= call.start_time.nil? ? "" : human_readable_date_time(call.start_time, @business) %>
<%= call.call_from %>
<%= call.to %>
<%= call.caller_name %>
">
<%= call.duration_in_words %>
<%= call.status %>
<%= call.notes %>
/edit?secret_key=<%=@business.secret_key%>">
<%#= link_to 'Destroy', call, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= render partial: 'shared/paginate', locals: {resource: @calls} %>
<%# link_to 'New Call', new_call_path %>
<%= content_for :footer_scripts do %>
let audio = null; //global for this page script.
$('.call-recording-play-stop-button').click(function(e) {
if (audio == null){
audio = new Audio($(this).data("url"));
audio.play();
toggleVisibilityOfPlayStopButton($(this))
} else{
audio.pause();
audio = null;
toggleVisibilityOfPlayStopButton($(this));
}
});
function toggleVisibilityOfPlayStopButton(element){
element.children().first().toggle();
element.children().last().toggle();
}
<% end %>
<%= form_with(model: call, local: true) do |form| %>
<% if call.errors.any? %>
<%=form_group(form, call, :caller_name, field_type= :text_field, value: call.caller_name) %>
<%=form_group(form, call, :outcome, field_type= :select, select_options: [
["", 0],
["Needs to Schedule an Appointment (Created Job Request)", 1],
["Has a Question about Previous Work or an Estimate", 2],
["Furnace Having the Same Issue Again, After we Ran Out", 3],
["Needs to Cancel/Reschedule", 4],
["Needs a Technician to Call Them", 5]
],
label: "Call Outcome",
value: call.outcome)%>
<%=form_group(form, call, :notes, field_type= :text_area, value: call.notes) %>
json.extract! call, :id, :to, :call_from, :caller_name, :recording_uri, :price, :start_time, :end_time, :duration, :status, :uri, :created_at, :updated_at, :transcription, :customer_id
json.url call_url(call, format: :json)
<%= render 'form', appointment_color: @appointment_color %>
<%= notice %>
<%= render 'form', appointment_color: @appointment_color %>
json.array! @appointment_colors, partial: "appointment_colors/appointment_color", as: :appointment_color
json.partial! "appointment_colors/appointment_color", appointment_color: @appointment_color
<%= notice %>
json.extract! appointment_color, :id, :color, :name, :business_id, :created_at, :updated_at
json.url appointment_color_url(appointment_color, format: :json)
<%# Messaging Thread UI %>
<%= stylesheet_link_tag "marketing/product.css" %>
Let the cloud run your business automatically. Less work, more done.
"
data-description="Annual Subscription to HomeService.Cloud"
data-amount="7900"
data-locale="auto">
<% else %>
Amount: $79/month, billed annually. Total = $948
"
data-description="Annual Subscription to HomeService.Cloud"
data-amount="94800"
data-locale="auto">
<% end %>
<% end %>
<%= notice %>
Email:
<%= @subscription.email %>
Key:
<%= @subscription.key %>
Activated:
<%= @subscription.activated %>
MRR:
<%= @subscription.mrr %>
Name:
<%= @subscription.name %>
SKU:
<%= @subscription.sku %>
Deactivated:
<%= @subscription.deactivated %>
<%= link_to 'Delete', subscription_path(@subscription), method: :delete, data: { confirm: 'Are you sure?' } %> |
<%= link_to 'Edit', edit_subscription_path(@subscription) %> |
<%= link_to 'Back', "/events/business?id=#{Business.find_by(subscription: @subscription.id)&.id}" %>
<%= render 'form', subscription: @subscription, form_url: "/subscriptions/create_manual?business_id=#{@business.id}" %>
<%# link_to 'Show', @subscription %>
<%= link_to 'Back', "/events/business?id=#{Business.find_by(subscription: @subscription.id)&.id}" %>
<%= render 'form', subscription: @subscription, form_url: nil %>
<%= link_to 'Show', @subscription %> |
<%= link_to 'Back', "/events/business?id=#{Business.find_by(subscription: @subscription.id)&.id}" %>
json.array! @subscriptions, partial: 'subscriptions/subscription', as: :subscription
json.partial! "subscriptions/subscription", subscription: @subscription
json.extract! subscription, :id, :email, :key, :activated, :created_at, :updated_at
json.url subscription_url(subscription, format: :json)
<%= notice %>
Email
Key
Activated
<% @subscriptions.each do |subscription| %>
<%= subscription.email %>
<%= subscription.key %>
<%= subscription.activated %>
<%= link_to 'Show', subscription %>
<%= link_to 'Edit', edit_subscription_path(subscription) %>
<%= link_to 'Destroy', subscription, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Subscription', new_subscription_path %>
<%= form_with(model: subscription, local: true, url: form_url || nil) do |form| %>
<% if subscription.errors.any? %>
<%# render 'form', subscription: @subscription %>
"
data-description="Annual Subscription to HomeService.Cloud"
data-amount="22740"
data-locale="auto">
<% end %>
<%= render "form", web_page_history: @web_page_history %>
<%= link_to 'Back to web page histories', web_page_histories_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= notice %>
<%= render @web_page_history %>
<%= link_to 'Edit this web_page_history', edit_web_page_history_path(@web_page_history), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= render "form", web_page_history: @web_page_history %>
<%= link_to "Show this web page history", @web_page_history, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to web page histories", web_page_histories_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
json.array! @web_page_histories, partial: "web_page_histories/web_page_history", as: :web_page_history
json.partial! "web_page_histories/web_page_history", web_page_history: @web_page_history
json.extract! web_page_history, :id, :content, :web_page_id, :created_at, :updated_at
json.url web_page_history_url(web_page_history, format: :json)
<%= notice %>
<%= form_with(model: web_page_history, class: "contents") do |form| %>
<% if web_page_history.errors.any? %>
Content:
<%= web_page_history.content %>
Web page:
<%= web_page_history.web_page_id %>
<% if action_name != "show" %>
<%= link_to "Show this web page history", web_page_history, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to 'Edit this web page history', edit_web_page_history_path(web_page_history), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
<%= render 'form', marketing_source: @marketing_source %>
<%= link_to 'Back', marketing_sources_path %>
json.array! @marketing_sources, partial: 'marketing_sources/marketing_source', as: :marketing_source
json.partial! "marketing_sources/marketing_source", marketing_source: @marketing_source
Name
Delete
<% @marketing_sources.each do |marketing_source| %>
"><%= marketing_source.name %>
" data-method="delete" data-confirm="Are you sure?"
title="Delete">
<% end %>
<%
@marketing_source.formatted_error_messages.each do |message|
flash[:error] = message
end
%>
json.extract! marketing_source, :id, :name, :created_at, :updated_at
json.url marketing_source_url(marketing_source, format: :json)
Error reading file: U_STRING_NOT_TERMINATED_WARNING
<%= content_for :footer_scripts do %>
$(function(){
print(); //Print this document on load
});
<% end %>
<%= render 'form', home_page: @home_page %>
<%= link_to 'Back', home_pages_path %>
<%= notice %>
Domain:
<%= @home_page.domain %>
Primary color:
<%= @home_page.primary_color %>
Secondary color:
<%= @home_page.secondary_color %>
Tertiary color:
<%= @home_page.tertiary_color %>
Facebook url:
<%= @home_page.facebook_url %>
Instagram url:
<%= @home_page.instagram_url %>
Description Header:
<%= @home_page.header_description %>
Description About Us:
<%= @home_page.about_us %>
Services Header:
<%= @home_page.services_header %>
Team Description:
<%= @home_page.our_team_description %>
Business:
<%= @home_page.business %>
Use Raw HTML:
<%= @home_page.use_raw_html %>
<%= link_to 'Edit', edit_home_page_path(@home_page) %> |
<%= link_to 'Back', home_pages_path %>
<%= render 'form', home_page: @home_page %>
<%= link_to 'Show', @home_page %> |
<%= link_to 'Back', home_pages_path %>
json.array! @home_pages, partial: "home_pages/home_page", as: :home_page
json.partial! "home_pages/home_page", home_page: @home_page
<%= notice %>
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #ddd;
}
.even {
background-color: #f9f9f9;
}
.odd {
background-color: #fff;
}
Business
Domain
Primary color
Secondary color
Tertiary color
Facebook url
Instagram url
Services Description
<% @home_pages.each_with_index do |home_page, index| %>
">
"> <%= home_page.business.name %>
<%= home_page.domain %>
<%= home_page.primary_color %>
<%= home_page.secondary_color %>
<%= home_page.tertiary_color %>
<%= home_page.facebook_url %>
<%= home_page.instagram_url %>
<%= home_page.header_description %>
<%= link_to 'View', "/w/#{home_page.business.id}" %>
<%= link_to 'Edit', edit_home_page_path(home_page) %>
<%= link_to 'Destroy', home_page, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Home Page', new_home_page_path %>
json.extract! home_page, :id, :primary_color, :secondary_color, :tertiary_color, :facebook_url, :instagram_url, :business_id, :created_at, :updated_at
json.url home_page_url(home_page, format: :json)
<% @home_page = home_page %>
<%= content_for :header do %>
<%# %>
<% end %>
<%= form_with(model: home_page, local: true, id: 'edit_form') do |form| %>
<% @home_page = home_page %>
<% if home_page.errors.any? %>
<%# raw home_page.raw_html %>
<%# raw home_page.footer_html %>
<% content_for :footer_scripts do %>
document.getElementById('toggleNavbarHtml').addEventListener('click', function(event) {
// Open a new tab to navigate to the business page
const businessId = '<%= home_page.business.id %>';
window.open(`/w/${businessId}`, '_blank');
event.preventDefault();
var navbarHtml = document.getElementById('navbarHtml');
var button = this;
if (navbarHtml.style.display === 'none') {
navbarHtml.style.display = 'block';
button.textContent = 'Hide';
} else {
navbarHtml.style.display = 'none';
button.textContent = 'Show';
}
});
//make these variables global
let generatedHtml;
let editorInstance;
function submitForm() {
console.log("POST FORM HERE!")
//TODO: we need to set the HTML field form with serialized HTML that is legitimate and works.
// $('#published_job_page_html').val()
//$('#home_page_raw_html').val(getEditorHtml());
$('#edit_form').submit(); //Here is where you can manually submit the form
}
$(document).ready(function() {
document.querySelectorAll(".saveButton").forEach(button => {
button.addEventListener("click", submitForm, false);
});
// Use .html_safe to prevent HTML escaping, and gsub to escape newline and quotation marks for JavaScript.
generatedHtml = `<%= @home_page&.raw_html&.html_safe&.gsub("\n", "\\n")&.gsub('"', '\\"')&.gsub('`', '') %>`;
console.log(generatedHtml); // To verify the output
ClassicEditor
.create(document.querySelector('#editor'))
.then(editor => {
editorInstance = editor; // Store the editor instance
})
.catch(error => {
console.error(error);
});
})
function getEditorHtml() {
const htmlContent = editorInstance.getData();
console.log(htmlContent); // Logs the HTML content to the console
return htmlContent;
}
<% end %>
2 Tile - Tile Remodeling and Handyman Services
.logo {
max-width: 50px;
height: auto;
}
We provide expert tile remodeling and handyman services in Utah County and Monroe, Utah.
Give your space a fresh new look with our tile replacement services
We specialize in replacing old and damaged tiles to give your space a fresh new look.
Keep your tiles in top condition with our professional maintenance services
Our tile maintenance services ensure your tiles stay in top condition and last longer.
Upgrade your bathroom with our complete remodeling services
We offer complete bathroom remodeling services, including tile installation and fixtures.
Transform your kitchen with our expert tile work and remodeling services
Transform your kitchen with our expert tile work and remodeling services.
2 Tile is a leading tile remodeling and handyman company serving Utah County and Monroe, Utah. Founded by Alex, an experienced professional with a passion for craftsmanship and customer satisfaction, 2 Tile has built a reputation for delivering high-quality workmanship and exceptional service.
Our team of skilled technicians is dedicated to transforming your space with beautiful and durable tile work. Whether you need tile replacement, maintenance, or a complete remodel, we have the expertise to bring your vision to life.
Ready to start your tile remodeling project? Contact us today for a free estimate and consultation.
© 2024 2 Tile. All rights reserved.
(function(){
'use strict';
// Self Initialize DOM Factory Components
domFactory.handler.autoInit()
// Connect button(s) to drawer(s)
var sidebarToggle = document.querySelectorAll('[data-toggle="sidebar"]')
sidebarToggle = Array.prototype.slice.call(sidebarToggle)
sidebarToggle.forEach(function (toggle) {
toggle.addEventListener('click', function (e) {
var selector = e.currentTarget.getAttribute('data-target') || '#default-drawer'
var drawer = document.querySelector(selector)
if (drawer) {
drawer.mdkDrawer.toggle()
}
})
})
let drawers = document.querySelectorAll('.mdk-drawer')
drawers = Array.prototype.slice.call(drawers)
drawers.forEach((drawer) => {
drawer.addEventListener('mdk-drawer-change', (e) => {
if (!e.target.mdkDrawer) {
return
}
document.querySelector('body').classList[e.target.mdkDrawer.opened ? 'add' : 'remove']('has-drawer-opened')
let button = document.querySelector('[data-target="#' + e.target.id + '"]')
if (button) {
button.classList[e.target.mdkDrawer.opened ? 'add' : 'remove']('active')
}
})
})
// SIDEBAR COLLAPSE MENUS
$('.sidebar .collapse').on('show.bs.collapse', function (e) {
e.stopPropagation()
var parent = $(this).parents('.sidebar-submenu').get(0) || $(this).parents('.sidebar-menu').get(0)
$(parent).find('.open').find('.collapse').collapse('hide');
$(this).closest('li').addClass('open');
});
$('.sidebar .collapse').on('hidden.bs.collapse', function (e) {
e.stopPropagation()
$(this).closest('li').removeClass('open');
});
// ENABLE TOOLTIPS
$('[data-toggle="tooltip"]').tooltip()
// PRELOADER
window.addEventListener('load', function() {
$('.preloader').fadeOut()
domFactory.handler.upgradeAll()
})
})()
Stack-Rails - Rails 5 & Bootstrap 4 Admin Dashboard
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
">
<% if employee_signed_in? %>
<%= render 'layouts/stack/drawer_left' %>
<% end %>
Stack-Rails - Rails 5 & Bootstrap 4 Admin Dashboard
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
">
<%= yield %>
<% if employee_signed_in? %>
<%= link_to(destroy_employee_session_path, class: "dropdown-item", title: 'Logout', method: :delete) do %>
Log Out
<% end %>
<% if !current_business.nil? %>
<%= content_for :footer_scripts do %>
$(function(){
$('#search_selected_model').on('change', function(event){
let action = event.currentTarget.value;
let ransackQuery = $('#search_selected_model option:selected').data('ransack');
let ransackUrl = `/${action}`;
//Now need to change the form that's used for searching.
//nav_search_form for url on action attribute
document.getElementById('nav_search_form').setAttribute("action", ransackUrl)
//nav_search_input for ransack params: on name attribute
document.getElementById('nav_search_input').setAttribute("name", ransackQuery)
//Set the cookie for this session so that the navbar can keep the same search dropdown across pages
Cookies.set("navbar_search", action);
});
$("#nav_search_form").on("submit", function(){
$('#search-loader').show();
});
if (Cookies.get("navbar_search")){
$('#search_selected_model').val(Cookies.get("navbar_search")).trigger('change');
};
});
<% end %>
<% end %>
<%# Confusing syntax. Breadcrumb is an array [name, route], so calling .first is getting the name. %>
<% default_title = "Field Rocket | #{employee_signed_in? ? get_breadcrumbs&.last&.first : 'Launch, Grow, & Manage Your Service Business'}" %>
<%= @title || default_title %>
<% if @use_business_logo_as_favicon && @business.logo.attached? %>
<%= favicon_link_tag url_for(@business.logo), :rel => 'icon', :type=> 'image/png' if @business.logo.attached?%>
<% else %>
<%= favicon_link_tag asset_path('fieldrocketlogo.png'), :rel => 'icon', :type => 'image/png' %>
<% end %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<% if should_use_tailwind_instead_of_application_css? %>
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
<% else %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<% end %>
<%= action_cable_meta_tag %>
<%= audio_tag "sms_notification.wav", id: "sms-notification-audio" %>
<%= content_for :header %>
">
<% if !should_use_tailwind_instead_of_application_css? %>
<% if employee_signed_in? %>
<% if !current_page?('/published_job_pages/dashboard') %>
<% if employee_signed_in? %>
<%= render 'layouts/stack/drawer_left' %>
<% end %>
<% if Rails.env.production? %>
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "n7zmxvqsqx");
<% end %>
// if (window.innerWidth + window.innerHeight >= 1500){
// if (window.innerWidth + window.innerHeight >= 1500){
//(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/k9jifvav';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
// }
// We pre-filled your app ID in the widget URL: 'https://widget.intercom.io/widget/k9jifvav'
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
Llama Press - Build Anything with AI
body {
font-family: 'Poppins', sans-serif;
}
.footer-banner {
display: flex;
justify-content: center;
align-items: center;
background-color: #4a4a4a;
color: white;
padding: 20px;
/* position: absolute; */
bottom: 0;
width: 100%;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
}
.footer-text {
font-size: 18px;
font-weight: bold;
}
.feature-icon {
width: 24px;
height: 24px;
font-size: 24px;
}
<%# react_component('chatbot/ChatBot', props: { first_name: 'Tyson' }) %>
<%= render partial: 'shared/chatbot/chatbot' %>
<%# render partial: 'shared/chatbot/history_modal' %>
<%= yield %>
<%#= react_component 'chatbot/ChatBot', { first_name: 'User' }, { prerender: false } %>
<%= yield %>
<%= js add_gritter(flash[:notice], title: 'Notice!', time: 5000),
add_gritter(flash[:success], title: 'Success!', time: 5000, class_name: "success-notice"),
add_gritter(flash[:error], title: 'Error!', time: 5000, class_name: "error-notice") %>
<%= image_tag("homeservice.png", height: '50', width: '50') %>
<% if current_employee.is_owner? %>
<%= link_to invoice_templates_path, :class => "nav-link dropdown-item" do %>
Invoice Templates
<% end %>
<% end %>
<% if current_employee.is_owner? %>
<%= link_to(marketing_sources_path, class: "nav-link dropdown-item", title: 'Marketing Sources') do %>
Marketing Sources
<% end %>
<% end %>
<%= link_to(edit_employee_registration_path, class: "nav-link dropdown-item", title: 'Account' ) do %>
Account
<% end %>
<%= link_to(destroy_employee_session_path, class: "nav-link dropdown-item", title: 'Logout', method: :delete) do %>
Log Out
<% end %>
<%= yield %>
/* Email styles need to be inline */
<%= yield %>
LLama Template
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%# stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%# javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= yield %>
<%= render partial: 'shared/chatbot/chatbot' %>
<%= render "shared/topNavbar" %>
<%= render "shared/popup" %>
<% @body_class = 'layout-login-centered-boxed' %>
<% if notice %>
<%= notice %>
<%= alert %>
">
<%= content_for :footer_scripts do %>
function clearColor(){
$('#employee_color').val('#ffffff');
}
<% end %>
Hello <%= @email %>!
<% if @resource.try(:unconfirmed_email?) %>
We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
Hello <%= @resource.email %>!
You have a request to set a new password so you can sign into Field Rocket on your computer.
You can do this through the link below.
<%= link_to 'Set a new password', "#{ENV["BASE_URL"]}/employees/password/edit?reset_password_token=#{@token}" %>
If you didn't request this, please ignore this email.
Your password won't change until you click the above link and create a new one.
<%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>
<%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>
<%= accept_invitation_url(@resource, :invitation_token => @token) %>
<% if @resource.invitation_due_at %>
<%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>
<% end %>
<%= t("devise.mailer.invitation_instructions.ignore") %>
Welcome <%= @email %>!
You can confirm your account email through the link below:
<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
Hello <%= @resource.email %>!
Your account has been locked due to an excessive number of unsuccessful sign in attempts.
Click the link below to unlock your account:
<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
<%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>
<%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>
<%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token) %>
<% if @resource.invitation_due_at %>
<%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>
<%= t("devise.mailer.invitation_instructions.ignore") %>
Hello <%= @resource.email %>!
We're contacting you to notify you that your password has been changed.
<%= notice %>
<%= render 'shared/calendarEditable' %>
<%= render partial: 'employee_table' , locals: { path: employees_path, link_text: "Show Active Employees" } %>
<%= content_for :modal_header, :flush => true do %>
<%= content_for :modal_body, :flush => true do %>
<%# branches = current_employee.business.branches.include(:employees) %>
<% branches = Branch.includes([:employees]).where(business_id: current_business.id)%>
<% branches.each do |branch| %>
<% employees = branch.employees.load %>
<% if employees.size > 0 %>
<%= render partial: "shared/generic_modal", locals: {modal_id: "appointment_filter_modal"} %>
<%= content_for :footer_scripts do %>
//This is where we send an update to the server every time something is checked to modify the appointment filter
$('.employee-appointment-filter-checkbox').on('click', function(event){
let formattedEmployeeFilter = getFormattedEmployeeFilter();
$.post({
url: '/employees/filter_settings',
data: {employees: formattedEmployeeFilter},
success: function(data){
console.log("Successfully set the employee filter.");
calendar.refetchEvents();
},
failure: function(data){
gritterSuccess('Error', "Couldn't set the employee filter", 10000);
}
});
});
function getFormattedEmployeeFilter(){
var filteredEmployees = [];
$('.employee-appointment-filter-checkbox').each(function(index, element){
if (element.checked){
filteredEmployees.push(element.name);
}
});
return filteredEmployees.join();
}
<% end %>
<% @body_class = 'layout-login-centered-boxed' %>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<% @body_class = 'layout-login-centered-boxed' %>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<%= f.hidden_field :reset_password_token %>
<%= render partial: 'employee_table' , locals: { path: deactivated_employees_path, link_text: "Show Deactivated Employees" } %>
<% @employee_jobs.each do |employee_job|%>
<% @employee_job = employee_job %>
<%= render "employee_jobs/show"%>
<% end %>
<% end %>
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
<%= devise_error_messages! %>
<%= render "employees/shared/links" %>
<%- if controller_name != 'sessions' %>
<%= link_to "Log in", new_session_path(resource_name) %>
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Register", new_business_path %>
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Set a New Password", new_password_path(resource_name) %>
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
<% end -%>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
<% end -%>
<% end -%>
console.log("Made it to javascript that will be sent back to server.");
updateScheduleViewer('<%= j @employee_weekly_schedule.to_json.html_safe %>',
'<%= j @weeks_days_to_date_dictionary.to_json.html_safe %>',
'<%= j @weeks_days_to_month_dictionary.to_json.html_safe %>');
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<%= render "employees/shared/links" %>
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= devise_error_messages! %>
<%= render "employees/shared/links" %>
Field Rocket Purchase Subscription
<%= stylesheet_link_tag "marketing/product.css" %>
Let the cloud run your business automatically. Less work, more done.
<% if employee_signed_in? %>
<%=
#react_component 'invoices/InvoiceTracking', {lateInvoices: @business.get_late_invoices, outstandingInvoices: @business.get_outstanding_invoices, paidInvoices: @business.get_paid_invoices(1) }
%>
<% else %>
<%# render "shared/landingpage" %> <%# Temporary, just using the login page for landing page for now. We might not even want to do marketing on this page. %>
<%= render "stack/login" %>
<% end %>
Pricing example for Bootstrap
<%= stylesheet_link_tag "marketing/pricing.css" %>
Holder.addTheme('thumb', {
bg: '#55595c',
fg: '#eceeef',
text: 'Thumbnail'
});
Error reading file: U_STRING_NOT_TERMINATED_WARNING
<%= render 'form', group: @group %>
<%= render 'form', group: @group %>
<%= content_for :modal_header, :flush => true do %>
Print Branch Summaries
<% end %>
<%= content_for :modal_body, :flush => true do %>
<%= render partial: "shared/generic_modal", locals: {modal_id: "branch_daily_summary"} %>
<%= content_for :footer_scripts do %>
new QuerySelect('select_branch_for_summary',
'/branches.json',
null,
SelectResultsTransform('name'),
function(event){
$('#open_branch_summary_button').click(function(event){
window.location.replace(`/branches/${$('#select_branch_for_summary').val()}/daily_summary`);
});
});
<% end %>
<%= render 'form', branch: @branch %>
<%= link_to 'Back', branches_path %>
json.array! @branches, partial: 'branches/branch', as: :branch
json.extract! branch, :id, :name, :created_at, :updated_at
json.url branch_url(branch, format: :json)
json.partial! "branches/branch", branch: @branch
<%
@branch.formatted_error_messages.each do |message|
flash[:error] = message
end
%>
<%= render 'invoices/invoice', invoice: @invoice, editable: false%>
An email failed to send because a refresh token wasn't saved.
Email: <%= @email %>
Date: <%= DateTime.now %>
Return Address: <%= @email %>
Phone Number: <%= @phone %>
Date: <%= DateTime.now %>
Message: <%= @message %>
Daily report email regarding business registration and employee sign in
Email: <%= @email %>
Date: <%= DateTime.now %>
An invoice failed to push into quickbooks due to an error
Company: <%= @invoice.business.name %>
Invoice: "> <%= @invoice.long_title %>
Error: <%= @invoice.quickbooks_error %>
Date: <%= DateTime.now %>
<%# render 'invoices/invoice.html.erb', editable: true%>
<%= react_component 'invoices/InvoiceTemplateEditPage', invoice_template_data(true)%>
Error reading file: U_STRING_NOT_TERMINATED_WARNING
<%# render partial: "invoices/invoice", locals: {editable: true} %>
<%= react_component 'invoices/InvoiceTemplateEditPage', invoice_template_data(true)%>
<%= content_for :footer_scripts do %>
function validateInputForms(){
if ($('#template_name').text().trim() == "Template Name Here.." || $('#template_name').text().trim() == ""){
gritterError("Invalid Template Name", "You must enter a template name!");
}
else {
updateInvoice('invoice_templates',<%=@invoice.id%>, updatingInvoiceTemplate=true)
}
}
<% end %>
json.array! @invoice_templates, partial: 'invoice_templates/invoice_template', as: :invoice_template
gritterError("Error adding invoice template", data.responseJSON[key]);
<%= render partial: "invoices/customer_info_for_invoice", locals: {customer: customer}%>
json.partial! "invoice_templates/invoice_template", invoice_template: @invoice_template
Name
Description
Delete
<% @invoice_templates.each do |template| %>
"><%= template.name %>
<%= template.description %>
" data-method="delete" data-confirm="Are you sure?" title="Delete">
<% end %>
<%= form_with(model: invoice_template, local: true) do |form| %>
<% if @invoice.errors.any? %>
<%= content_for :card_body, flush: true do %>
<%= content_for :footer_scripts do %>
$(document).ready(function(){
<% if @job&.id != nil %>
SetSelect2Option('job_select', "<%=@job.title%>", "<%=@job.id%>")
<% end %>
<% if @invoice&.id != nil %>
SetSelect2Option('invoice_select', "<%=@invoice.title%>", "<%=@invoice.id%>")
<% end %>
});
<% end %>
<%= content_for :card_body do %>
Amount:
<%= format("$%.2f", @payment.amount) %>
Paid on:
<%= @payment.paid_on&.strftime("%B %d, %Y") %>
Payment type:
<%= @payment.payment_type %>
Job:
<% if @payment.job %>
"> <%= @payment.job_id %>
<% else %>
No job associated with this payment
<% end%>
<% if !@payment.invoice.nil?%>
Invoice:
"> <%= @payment.invoice.display_number %>
<%= link_to 'Edit', edit_payment_path(@payment) %> |
<%= link_to 'Back', job_path(@payment.job) %>
<% end %>
<%= render :partial => "shared/card"%>
<%= content_for :card_body, flush: true do %>
json.array! @payments, partial: 'payments/payment', as: :payment
json.partial! "payments/payment", payment: @payment
<%= react_component 'shared/IndexPage', {
title: "Payments",
showDatePicker: true,
dateData: {
startDateId: 'payments_start',
endDateId: 'payments_end',
startDate: @start_date,
endDate: @end_date,
path: payments_path,
},
attributes: ["Amount", "Paid On", "Type", "Invoice", "Notes"],
data: @payments.map {|payment| [payment.id, payment.amount.nil? ? "$0.00" : format("$%.2f", payment.amount), payment.paid_on.nil? ? nil : payment.paid_on.strftime("%B %d, %Y"), payment.payment_type, !payment.invoice.nil? ? {title: payment.invoice.title, url: url_for(payment)} : nil, payment.notes]},
basePath: '/payments'} %>
<%= render partial: 'shared/paginate', locals: {resource: @payments} %>
<%= form_with(model: payment, local: true) do |form| %>
<% if payment.errors.any? %>
<% content_for :footer_scripts do %>
$(document).ready(function(){
flatpickr('#payment_paid_on',
{
altInput: true,
altFormat: 'F j',
}
);
if ($('#payment_job_id') == null){
$('#invoice_select_container').hide();
}
else {
new QuerySelect("invoice_select",
`/jobs/${$('#payment_job_id').val()}/search_invoices`,
function(params) {return {q: {invoice_display_number_eq: params.term}}},
function (data) {
// Transforms the top-level key of the response object from 'items' to 'results'
return { results: $.map(data, function (val) { return { "id": val.id, "text": val.title }})}
},
function (evt){
if (evt.currentTarget.value != null && evt.currentTarget.value.trim !== "")
$('#payment_invoice_id').val(evt.currentTarget.value);
});
}
new QuerySelect("job_select",
'/jobs.json',
function(params) {return {q: {id_eq: params.term}}},
function (data) {
// Transforms the top-level key of the response object from 'items' to 'results'
return { results: $.map(data, function (val) { return { "id": val.id, "text": val.title }})}
},
function (evt){
$('#payment_job_id').val(evt.currentTarget.value);
if (evt.currentTarget.value){
$('#invoice_select_container').show();
}
else{
$('#invoice_select_container').hide();
}
$('#invoice_select').val(null).trigger('change');
new QuerySelect("invoice_select",
`/jobs/${evt.currentTarget.value}/search_invoices`,
function(params) {return {q: {invoice_display_number_eq: params.term}}},
function (data) {
// Transforms the top-level key of the response object from 'items' to 'results'
return { results: $.map(data, function (val) { return { "id": val.id, "text": val.title }})}
},
function (evt){
if (evt.currentTarget.value != null && evt.currentTarget.value.trim !== "")
$('#payment_invoice_id').val(evt.currentTarget.value);
});
});
let payment_job_id = '<%=@payment.job_id%>'
let invoice_job_id = '<%=@payment.invoice_id%>'
if (payment_job_id != null && payment_job_id.trim() !== "")
$("#payment_job_id").val(payment_job_id)
if (invoice_job_id != null && invoice_job_id.trim() !== "")
$("#payment_invoice_id").val(invoice_job_id)
});
<% end %>
json.extract! payment, :id, :paid_on, :payment_type, :created_at, :updated_at
json.url payment_url(payment, format: :json)
create post
<%= react_component 'seo/SEOForm', invoice: @invoice.to_json, images: @invoice&.customer&.get_images %>
<%# TODO: Determine if there's an invoice template attached to this invoice, see if that invoice template has an image attached so that we can pass in that image as logo instead of the default business logo %>
<%= react_component 'invoices/InvoiceEditPage', invoice_data(true)%>
<%= content_for :footer_scripts do %>
$(function(){
<%= render partial: "invoice_to_visibility.js.erb"%>
})
<% end %>
<% end %>
<%= @invoice.invoice_type %> was delivered on: <%= @invoice.sent_on %>
<% unless @invoice.quickbooks_error.blank? %>
Quickbooks Error: "<%= @invoice.quickbooks_error %>"
<%# TODO: Determine if there's an invoice template attached to this invoice, see if that invoice template has an image attached so that we can pass in that image as logo instead of the default business logo %>
<%= react_component 'invoices/InvoiceDocument/InvoiceDocument', invoice_data(false)%>
<% content_for :footer do %>
<%= render :partial => 'invoices/emailmodal', :locals => {invoice: @invoice }%>
<% end %>
<% content_for :footer_scripts do %>
$(function(){
$('#sync_with_quickbooks_button').on('click', function(){
$.post({url: "/invoices/<%=@invoice.id%>/push_to_quickbooks",
success: function(data){
gritterSuccess("Attempting..", "Invoice Attempted to Push to Quickbooks");
location.reload();
},
error: function(data){
console.log(data);
gritterError("Couldn't Push to Quickbooks", "Invoice Couldn't Be Pushed to Quickbooks");
}});
});
});
<% end %>
Add Product/Service
<%# TODO: Determine if there's an invoice template attached to this invoice, see if that invoice template has an image attached so that we can pass in that image as logo instead of the default business logo %>
<%= react_component 'invoices/InvoiceEditPage', invoice_data(true)%>
json.array! @invoices, partial: 'invoices/invoice', as: :invoice
<%# render :template => 'invoices/_invoice.html.erb', :locals => { :editable => false, :show_payment_link => false } %>
<%= react_component 'invoices/InvoiceDocument/InvoiceDocument', invoice_data(false)%>
<%# TODO: When we submit this form, we're not getting the signature param coming through %>
<%= form_for @invoice, :url => url_for(:controller => 'invoices', :action => 'accept_signature'), method: :post do |form| %>
<%= content_for :footer_scripts do %>
<% unless @invoice.business.visitor_device_info_auth_token.blank? %>
const atk = "<%=@invoice.business.visitor_device_info_auth_token%>";
const dmn = "<%=ENV["BASE_URL"]%>";
// Check if fr_guid exists in localStorage
let frGuid = localStorage.getItem('fr_guid');
if (!frGuid) {
// Generate a new GUID using crypto.randomUUID() and append "-42"
frGuid = `${crypto.randomUUID()}-42`;
// Set the new GUID in localStorage with the key fr_guid
localStorage.setItem('fr_guid', frGuid);
console.log('New fr_guid:', frGuid);
} else {
console.log('Existing fr_guid:', frGuid);
}
const data = {
user_agent_raw: navigator.userAgent,
referrer: document.referrer,
url: window.location.href,
device_width: window.screen.width.toString(),
device_height: window.screen.height.toString(),
device_os: navigator.platform,
os_version: navigator.userAgentData ? navigator.userAgentData.platform : 'Unknown',
browser_version: navigator.userAgentData ? navigator.userAgentData.brands.map(brand => `${brand.brand} ${brand.version}`).join(', ') : 'Unknown',
browser: navigator.userAgentData ? navigator.userAgentData.brands[0].brand : 'Unknown',
user_cookie_guid: frGuid,
auth_token: atk,
};
// console.log("Sending data: " + data);
// console.log(JSON.stringify(data));
fetch(`${dmn}/visitor_device_infos.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
})
.then(response => {
console.log("FR - made it");
// Handle the response from the server
})
.catch(error => {
console.log("FR - didn't make it");
// Handle any errors
});
<% end %>
<%= javascript_include_tag "pay-invoice-signature.js" %>
<% if !@invoice.signature.nil? %>
$(function(){
// Uncaught ReferenceError: setSignatureToBeEdited is not defined
const checkFunctionInterval = setInterval(() => {
if (typeof setSignatureToBeEdited === 'function') {
setSignatureToBeEdited('<%=@invoice.signature%>');
clearInterval(checkFunctionInterval);
}
}, 100); // Check every 100ms
// document.getElementById('submit-signature').addEventListener('click', function () {
//Here is where we can submit a POST request??
// signaturePad.clear();
// disablePayment(); // we don't want them to be able to pay if they just cleared the signature, because there's no signature now haha.
// });
})
<% else %>
$(function(){
disableSignButtonIfNoSignature(); // call this after we set the signature
});
<% end %>
<% end %>
<%= react_component 'shared/IndexPage', {
title: "Payments",
attributes: ["Amount", "Paid On", "Type", "Invoice", "Notes"],
data: @payments.map {|payment| [payment.id, payment.amount.nil? ? "$0.00" : format("$%.2f", payment.amount), payment.paid_on.nil? ? nil : payment.paid_on.strftime("%B %d, %Y"), payment.payment_type, !payment.invoice.nil? ? {title: payment.invoice.title, url: url_for(payment)} : nil, payment.notes]},
basePath: '/payments'} %>
<%=customer.full_name%>
<% if !customer.default_location&.display_address&.blank? && !customer.default_location&.display_address&.tr(',', '').blank? %> <%# We don't want to display locations that have only commas in them.%>
<%= customer.default_location&.display_address %>
<% end %>
<% if !customer.email&.blank? %>
<%= customer.email %>
<% end %>
<% if !customer.phone&.blank?%>
<%= number_to_phone(customer.phone) %>
<% end %>
json.(@invoice, :id, :invoice_type, :invoice_display_number, :notes, :invoice_to_id)
<% invoice = defined?(invoice) ? invoice : nil %>
<% if invoice.class.reflect_on_all_associations.map(&:name).include?(:images_attachments) %>
<% @amount_without_surcharge = @amount_to_pay.nil? ? @invoice.amount_due : @amount_to_pay.to_f %>
<%
if @business.stripe_enabled
@amount = @surcharge_customer ? total_surcharge_amount(@amount_without_surcharge, @app_percent_fee) : @amount_without_surcharge #Stripe Amount
@payment_gateway_name = 'stripe'
else
@amount = @amount_without_surcharge #* 1.03 + 0.50 #IBX Amount
@payment_gateway_name = 'ibx'
end
%>
<% if @invoice.business.has_setup_stripe_payments? %>
<% if !@invoice.paid_in_full? && @invoice.amount_due >= 0.50 || (@amount_without_surcharge >= 0.50)%> <%# The 0.50 cents cut off is because stripe can't process anything less than 50 cents.%>
<%= form_for @invoice, :url => url_for(:controller => 'invoices', :action => 'accept_payment'), method: :post do |form| %>
<%= react_component 'invoices/InvoiceDocument/InvoiceDocument', invoice_data(false)%>
<% if !@secret_key.blank? %>
<%= hidden_field_tag 'secret_key', @secret_key %>
<% end %>
<% if @surcharge_customer %>
* By paying online, you agree to paying an additional amount of approximately ~<%= format("%.2f", (2.9 + (@app_percent_fee*100))) %>% for our software's convenience fee.
** Towards Invoice: <%= format("$%.2f", @amount_without_surcharge) %>
<%# TODO: Re-enable this so users can see pictures attached to specific invoices. %>
<%#
<%= content_for :footer_scripts do %>
<% unless @invoice.business.visitor_device_info_auth_token.blank? %>
const atk = "<%=@invoice.business.visitor_device_info_auth_token%>";
const dmn = "<%=ENV["BASE_URL"]%>";
// Check if fr_guid exists in localStorage
let frGuid = localStorage.getItem('fr_guid');
if (!frGuid) {
// Generate a new GUID using crypto.randomUUID() and append "-42"
frGuid = `${crypto.randomUUID()}-42`;
// Set the new GUID in localStorage with the key fr_guid
localStorage.setItem('fr_guid', frGuid);
console.log('New fr_guid:', frGuid);
} else {
console.log('Existing fr_guid:', frGuid);
}
const data = {
user_agent_raw: navigator.userAgent,
referrer: document.referrer,
url: window.location.href,
device_width: window.screen.width.toString(),
device_height: window.screen.height.toString(),
device_os: navigator.platform,
os_version: navigator.userAgentData ? navigator.userAgentData.platform : 'Unknown',
browser_version: navigator.userAgentData ? navigator.userAgentData.brands.map(brand => `${brand.brand} ${brand.version}`).join(', ') : 'Unknown',
browser: navigator.userAgentData ? navigator.userAgentData.brands[0].brand : 'Unknown',
user_cookie_guid: frGuid,
auth_token: atk,
};
// console.log("Sending data: " + data);
// console.log(JSON.stringify(data));
fetch(`${dmn}/visitor_device_infos.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
})
.then(response => {
console.log("FR - made it");
// Handle the response from the server
})
.catch(error => {
console.log("FR - didn't make it");
// Handle any errors
});
<% end %>
<% end %>
<% if @invoice.business.force_signature_on_payment %>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag "pay-invoice-signature.js" %>
<% if !@invoice.signature.nil? %>
$(function(){
setSignatureToBeEdited('<%=@invoice.signature%>');
})
<% else %>
$(function(){
disablePaymentIfNoSignature(); // call this after we set the signature
});
<% end %>
<% end %>
<% end %>
<% if @business.payroc_ibx_enabled && !@business.ibx_merchant_token.nil? %>
<% if Rails.env.production? && !ENV["IBX_TEST"] %>
<% else %>
<% end %>
var options = {
merchantToken: "<%=@business.ibx_merchant_token%>",
name: '<%= @business.name || "Field Rocket"%>',
description: 'Pay for this invoice',
onToken: function(data) {
$("#ibx-button-outer").addClass("is-loading");
$('#custom-button').prop('disabled','true');
$('#ibx_payment_amount').val('<%= @amount %>');
$('#ibx_card_token').val(data.tokens[0]);
document.getElementById("signature-pad-value").value = document.getElementById("signature-pad").toDataURL();
$('#edit_invoice_<%= @invoice.id %>').submit();
}
}
var handler = ibx.Checkout.init(options)
document.getElementById('custom-button').addEventListener('click', function (event) {
handler.render({
amount: "<%= to_cents(@amount) %>"
})
event.preventDefault()
})
<% end %>
<%= react_component 'shared/DateRangePicker', {
enableToggle: true,
startDateId: 'invoices_start',
endDateId: 'invoices_end',
startDate: @start_date,
endDate: @end_date,
path: invoices_path,
}
%>
<%# react_component 'invoices/InvoiceTracking', {outstandingInvoices: @invoices.map {|i| i.get_invoice_preview} } %>
<%= react_component 'invoices/Table', {sortingUrl: 'invoices', tableData: @invoices.map {|i| i.get_invoice_preview}}%>
<%= render partial: 'shared/paginate', locals: {resource: @invoices} %>
<%= content_for :footer_scripts do %>
function dateRangePicker() {
$('#date_range_picker').toggle();
}
<% end %>
<% if controller.request.format.symbol != :html %>
<%= @invoice.title %>
@media (max-width:500px) {
.invoice-box {
float: left;
}
}
.invoice-box {
max-width: 900px;
font-size: 16px;
line-height: 24px;
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
color: #555;
}
.invoice-box table {
width: 100%;
line-height: inherit;
text-align: left;
}
.invoice-box table td {
vertical-align: top;
}
.invoice-box table tr td:nth-child(2) {
text-align: left;
}
.invoice-box table tr.top table td {
padding-bottom: 20px;
}
.invoice-box table tr.top table td.title {
font-size: 45px;
line-height: 45px;
color: #333;
}
p {
margin-bottom: 0;
}
.invoice-box table tr.information table td {
padding-bottom: 40px;
}
.invoice-box table tr.heading td {
background: #eee;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.invoice-box table tr.details td {
padding-bottom: 20px;
}
.invoice-box table tr.line-item td {
border-bottom: 1px solid #eee;
text-align: left;
width:1%%;
}
.invoice-box table tr.line-item.last td {
border-bottom: none;
}
.invoice-box table tr.total td:nth-child(2) {
border-top: 2px solid #eee;
font-weight: bold;
}
/** RTL **/
/* .rtl {
direction: rtl;
font-family: Tahoma, 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
}
.rtl table {
text-align: right;
}
.rtl table tr td:nth-child(2) {
text-align: left;
} */
.line-item-price {
white-space: no-wrap;
}
/* Bootstrap CSS necessary for email. */
.btn-company-success {
color: #fff;
background-color: #28a745;
border-color: #28a745;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.btn-dark {
color: #fff;
background-color: #343a40;
border-color: #343a40;
}
.btn {
text-decoration: none;
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.text-center {
text-align: center !important;
}
.text-label {
font-size: .9rem;
text-transform: uppercase;
color: #939FAD;
margin-bottom: .5rem;
font-weight: bold;
}
<% end %>
<%= @invoice&.job&.id %>
<%= @invoice&.business_id %>
<%= @invoice&.my_index %>
<%
#These help us not have to put each local in the render template call (can have undefined locals without crashing).
editable = defined?(editable) ? editable : nil
show_payment_link = defined?(show_payment_link) ? show_payment_link : nil
show_download_link = defined?(show_download_link) ? show_download_link : nil
%>
<%= @invoice.invoice_type %>
Number:
contenteditable="true"<% end %>>
<%= @invoice.invoice_display_number %>
Customer Rep: <%= @invoice.job&.owner.full_name%>
Phone: <%= @invoice.sales_rep_number_display %>
Email: <%= @invoice.job&.owner&.email %>
<% if @invoice.should_invoice_other_customer? && @invoice.service_to %>
<% if @invoice.is_customer_address_different_than_job_address? && @invoice.job&.location&.display_address != nil %>
<% unless @invoice.job.nil? %>
Category
Description
Product ID
Quantity
Price
<% if @invoice.business.enable_total_from_multiplying_by_quantity %>
Amount
<% end %>
<% if editable %>
<% end %>
<%# IMPORTANT: line-item-anchor-row class used in invoices.js function addLineItem() as an element used to add line items in the right place on the page. %>
<%# Warning: Be very careful when applying updates here. The table row and table data structure must very closely mirror the structure defined in addLineItem in invoices.js %>
<%# @invoice.line_items.order(position: :desc).each do |li, i| %> <%# Doing these in reverse order because otherwise they show up reverse to the user. This is because rails orders them as the most recently added first in the list. %>
<%# @invoice.line_items.reverse.each do |li, i| %>
<% @invoice.line_items.order(position: :desc).each do |li, i| %>
"> <%# IMPORTANT: line-item class used in invoices.js for creating the line-items JSON array that will be sent to server, and for calculating the total of the invoice to show on page.%>
">
<% if editable %>
"> "><%=li.name%>
<% else %>
<%= li.name %>
<% end %>
contenteditable="true" <%end%> class="line-item-description" id="line-item-description-<%=i%>">
<%= li.description %>
contenteditable="true" <%end%> style="text-align:right" class="line-item-quantity" id="line-item-quantity-<%=i%>">
<%= li.quantity %>
$ "<%if editable%> contenteditable="true" <%end%>>
<%=
if li.price != nil
format("%.2f",li.price)
end
%>
<% if @invoice.business.enable_total_from_multiplying_by_quantity %>
$ ">
<%= format("%.2f", li.price * li.quantity) %>
<% end %>
<% if editable %>
<% end %>
<% end %>
<%# if (!editable && (@invoice.invoice_type == "Invoice" || @invoice.invoice_type == "Work Order")) %>
Paid: <%= format("$%.2f",@invoice.amount_paid) %>
Original Total: <%= format("$%.2f",@invoice.total_price)%>
<%# end %>
Signature:
<% end %>
<% if editable %>
<%= content_for :footer_scripts do %>
$(function(){ //hate this duplicate logic.
$('.line-item-description').keypress((function(e) {return e.which != 13}));
$('.line-item-description').keypress(maxCharacters);
$('.line-item-quantity').keypress(onlyIntegersAllowed);
$('.line-item-quantity').keyup(sumLineItemTotal);
$('.line-item-price').keypress(onlyNumbersAllowed);
$('.line-item-price').keyup(sumLineItemTotal);
$('.line-item-price').keyup(function(e) {console.log("Keyup")});
lineItemEventHandlers();
calibrateLineItemIds();
setupProductQuerySelectForEachLineItem();
})
<%= render :partial => "jobs/job-images.js.erb" %>
<% end %>
<% end %>
<%= form_with(model: invoice, local: true) do |form| %>
<% if invoice.errors.any? %>
Sample mail sent using smtp.
var jobCustomerId = "<%=@invoice.job&.customer&.id%>";
var customerId = "<%=@invoice.invoice_to&.id %>" == "" ? null : "<%=@invoice.invoice_to&.id %>";
var jobId = "<%=@invoice.job&.id%>";
setInvoiceToFields(jobCustomerId);
setServiceToFields(customerId);
$('#documentToDifferentCustomer').change(function(){
if (this.checked){
new CustomerSearchSelect("select_different_customer_to_invoice", function (e) {
let selectedCustomerId = e.currentTarget.value;
if (selectedCustomerId == jobCustomerId){
setInvoiceToFields(jobCustomerId);
setServiceToFields(null);
}
else {
setInvoiceToFields(selectedCustomerId);
setServiceToFields(jobCustomerId);
}
});
if(<%= @invoice.have_customer%>){
let whichIdToUse = customerId == null ? jobCustomerId : customerId; //We want to autopopulate the selection based on whether invoice_to is null or not. If it is null, use the invoice's job's customer id. If it's not null, use the invoice_to customer id.
$.get({
url: `/customers/${whichIdToUse}.json`,
success: function(data){
SetSelect2Option('select_different_customer_to_invoice', data["full_name"], whichIdToUse)
}
})
}
$('#select_different_customer_to_invoice').show();
$('#invoice-service-address').show();
}
else {
$('#select_different_customer_to_invoice').hide();
$('#select_different_customer_to_invoice').val(null).trigger('change');
setInvoiceToFields(jobCustomerId);
setServiceToFields(null);
$("#select_different_customer_to_invoice").select2('destroy');
}
});
if (customerId != null){ // <%# If invoice_to is already set, (most likely because we're editing an invoice where invoice_to is different than service_to), then we want to make sure the checkbox and search bar have the correct value. %>
$('#documentToDifferentCustomer').trigger('click');
}
function setInvoiceToFields(customerId){
if (customerId == null || customerId == ""){
return;
}
console.log(`Setting invoiceTo fields with given customer id ${customerId}`);
$.get({
url: `/customers/${customerId}.json`,
success: function(data){
$('#invoice-to-name').text(data["full_name"]);
$('#invoice-to-display-address').text(data["default_location"]["display_address"]);
$('#invoice-to-email').text(data["email"]);
$('#invoice-to-phone').text(formatPhoneNumber(data["phone"]));
}
});
}
function setServiceToFields(customerId){
if (customerId == null || customerId == ""){
console.log("Hiding visibility of all serviceToFields");
$('#service-to').hide();
}
else {
$('#service-to').show();
console.log(`Setting serviceTo fields with given customer ${customerId}`);
$.get({
url: `/customers/${customerId}.json`,
success: function(data){
$('#service-to-name').text(data["full_name"]);
$('#service-to-display-address').text(data["default_location"]["default_address"]);
$('#service-to-email').text(data["email"]);
$('#service-to-phone').text(formatPhoneNumber(data["phone"]));
//TODO: Eventually, we'll have it set in the service to section, however for now we're going to keep the service address section in _invoice.html.erb setServiceAddressToJobAddress(jobId); //It needs to be set here, because somewhere else the code is changing it to the service_to customer's default address, which is incorrect. We want it to be the place of service address, IE, the job address
}
});
}
}
/**
* This function sets the 'service to' section's address to what the job's address is.
*/
function setServiceAddressToJobAddress(jobId){
console.log("setServiceAddressToJobAddress called!");
$.get({
url: `/jobs/${jobId}.json`,
success: function(data){
$('#service-to-display-address').text(data["display_address"]);
}
})
}
Total Overdue Amount:
<%= react_component 'invoices/Table', {sortingUrl: 'overdue_invoices', tableData: @invoices.map {|i| i.get_invoice_preview}}%>
<%= render partial: 'shared/paginate', locals: {resource: @invoices} %>
<%= content_for :footer_scripts do %>
$(document).ready(function () {
$('#total_overdue_amount').html(formatCurrency(<%= @total_overdue_amount %>));
});
<% end %>
<%= render partial: "invoice", locals: {id: "email_body_text_editor", placeholder: "Enter Email Body Here", content: raw(@email_template.body)}%>
Error reading file: U_STRING_NOT_TERMINATED_WARNING
<% invoice_type = defined?(invoice) ? invoice.invoice_type : nil %>
<% content_for :modal_header, :flush => true do %>
<% content_for :modal_body, :flush => true do %>
<%= form_with url: deliver_invoice_path(:format => :json), remote: true, :id => 'email_form' do %>
<%= label_tag(:email_subject, "Subject:", :class => "text-label") %>
<%= text_field_tag(:email_subject, nil, :class => 'form-control') %>
<%= label_tag(:editor, "Body:", :class => "text-label") %>
<%# WARNING: This image selection code is now duplicated in shared/_image_selection.html.erb. TODO: We should render image_selection.html.erb here, rather than having duplicate code. We need to keep these in sync to follow DRY principle. %>
<%="#{img.blob.filename.to_s[0,4]}...#{img.blob.filename.to_s.split('.').last}"%>
<%# render :partial => 'jobs/job_img_gallery', locals: {job: @invoice.job}%>
<%= content_for :footer_scripts do %>
<%= stylesheet_link_tag 'gallery-clean', media: 'all' %>
<%= stylesheet_link_tag 'baguetteBox', media: 'all' %>
<%= javascript_include_tag 'baguetteBox' %>
$(document).ready(function () {
$('#email_subject').val("<%= @invoice.title %>");
$('#email_recipients_select').select2({
tags: true,
tokenSeparators: [',', ' '],
selectOnClose: true
});
new QuerySelect('email_template_select',
'/email_templates.json',
RansackSearch('name_cont'),
SelectResultsTransform('name'),
function(e){
let selectedEmailTemplateId = e.currentTarget.value;
$.get({
url: `/email_templates/${selectedEmailTemplateId}.json`,
success: function(data){
let body = data["body"].replace('{{half_payment_link}}', '<a href="<%="#{ENV["BASE_URL"]}/invoices/#{@invoice.id}/pay?amount=half&secret_key=#{@invoice.secret_key}"%>">Payment Link</a>');
body = body.replace('{{payment_link}}', '<a href="<%="#{ENV["BASE_URL"]}/invoices/#{@invoice.id}/pay?secret_key=#{@invoice.secret_key}"%>">Payment Link</a>');
body = body.replace('{{name}}', '<%=@invoice.customer_name%>')
body = body.replace('{{signature_link}}', '<a href="<%="#{ENV["BASE_URL"]}/invoices/#{@invoice.id}/sign?secret_key=#{@invoice.secret_key}"%>">Signature Link</a>')
SetQuillHtml('editor', body);
}
});
}
);
$('#email_recipients_select').on('change', function(e){
$('#email_recipients').val($('#email_recipients_select').val());
});
$('#email_ccs_select').select2({
tags: true,
tokenSeparators: [',', ' '],
selectOnClose: true
});
$('#email_ccs_select').on('change', function(e){
$('#email_ccs').val($('#email_ccs_select').val());
});
var quill = new Quill('#editor', {
theme: 'snow'
});
$('#submitEmail').click(function(e){
$('#email_body').val($('#editor')[0].children[0].innerHTML);
$('.loader').show();
});
$("form").on("ajax:success", function(event){
var detail = event.detail;
var data = detail[0], status = detail[1], xhr = detail[2];
$('.loader').hide();
ForceCloseModal('send_email_modal');
gritterSuccess("Success", "Sent Email");
});
$("form").on("ajax:error", function(event){
// Was having an issue getting the data and everything out when submitting the form this way, found this page:
// https://stackoverflow.com/questions/43827441/how-to-get-data-from-response-of-event-ajaxerror-in-rails-5-1
var detail = event.detail;
var data = detail[0], status = detail[1], xhr = detail[2];
$('.loader').hide();
var needToAuthenticateGmail = xhr.status == 511
if (needToAuthenticateGmail) {
gritterError("Need to authenticate Gmail", "Redirecting you to authenticate gmail", "10000");
var uri = data["response"]["google_redirect_uri"];
console.log(`Redirecting to ${uri}`);
window.location = uri;
}
else {
console.log(data);
showErrorForm(event);
gritterError("Couldn't send email", "Does the customer have a valid e-mail address? Check the email and try again", "10000");
}
});
});
<% end %>
$('.edit-page-invoice-images').html(`<%= render :partial => 'invoices/invoice_img_gallery', locals: {invoice: @invoice, editable: true} %>`);
<%= render :partial => "jobs/job-images.js.erb" %>
$('#img-upload-form')[0].reset();
$('#images-upload-modal').modal('hide');
<%
@customer = Customer.new
@customer.locations.build #Create location for the form.
%>
<%= content_for :modal_header, flush: true do %>
<%= render :partial => "shared/generic_modal", locals: {modal_id: 'new_customer_modal'} %>
<% @customer_contact = CustomerContact.new %>
<%# https://stackoverflow.com/a/31281336 %>
<%# Have to call content_for before rendering partial, which I don't understand fully. %>
<% content_for :modal_header, :flush => true do %>
<% content_for :modal_body, :flush => true do %>
<%= form_with model: @customer_contact, id: :new_customer_contact_form, remote: true, format: :json do |f| %>
<%= form_group(f, @customer_contact, :first_name) %>
<%= form_group(f, @customer_contact, :last_name) %>
<%= form_group(f, @customer_contact, :phone, :phone_field) %>
<%= form_group(f, @customer_contact, :email) %>
<%= form_group(f, @customer_contact, :notes) %>
<%= render :partial => 'shared/generic_modal', :locals => {modal_id: "new_customer_contact_modal"}%>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag "customer_contacts.js" %>
<% end %>
<%= render 'form', customer: @customer%>
<%= react_component "contacts/ContactsIndex", contacts: @customer.customer_contacts%>
<%= content_for :accordian_section_title, flush: true do %>
<%= react_component "locations/LocationsIndex", locations: @customer.locations%>
<%= content_for :accordian_section_title, flush: true do %>
<%= content_for :accordian_section_content, flush: true do %>
<%# react_component "jobs/JobIndex", data: @customer.jobs%>
<%= react_component "shared/TableDataNotEditable",
title: "Jobs",
attributes: ["Sales Rep", "Date", "Address", "Job Summary", "Time Clocked"],
data: @customer.jobs.includes([:owner, :location]).order({created_at: :desc}).map {|j| [
j.id,
j.owner&.full_name,
j.employee_jobs&.first&.start_short,
{url: "/jobs/#{j.id}", title: j.location&.display_address},
j.notes&.split('\n')&.first,
format_time_seconds(j.total_time_tracked_towards_this_job),
]},
basePath: '/jobs'%>
<%# react_component "customers/InfoPage", customer: @customer, jobs: @customer.jobs, invoices: @customer.invoices, properties: @customer.locations, contacts: @customer.customer_contacts%>
<%# Navigation Menu: Jobs, Invoices & Estimates, Properties, Contacts.. %>
<%= render :partial => 'shared/customer_img_gallery', locals: {customer: @customer} %>
<%= content_for :footer_scripts do %>
<%= stylesheet_link_tag 'gallery-clean', media: 'all' %>
<%= stylesheet_link_tag 'baguetteBox', media: 'all' %>
<%= javascript_include_tag 'baguetteBox' %>
$(function(){
baguetteBox.run('.tz-gallery',{
"buttons":true
});
})
<%= render :partial => "jobs/job-images.js.erb" %>
<% end %>
<%= render partial: 'form', customer: @customer, locals: {remote: false, include_location_form: false} %>
json.array! @customers, partial: 'customers/customer', as: :customer
<%#
@customer = Customer.new
@customer.locations.build #Create location for the form.
%>
<%= content_for :modal_header, flush: true do %>
/*function urlChangeHandler() {
let src = event.target.value;
if(src) {
$('#attached-image').attr('src',url);
$('#attached-image-container').show();
$('#image_upload').hide();
}
}*/
function changeHandler(event) {
let src = event.target.value;
if(src) {
let url = window.URL.createObjectURL(event.target.files[0])
$('#image_avatar_url_id').val('');
$('#attached-image').attr('src',url);
$('#attached-image-container').show();
$('#image_upload').hide();
}
}
function resetImage(event) {
$("#attached-image-container").hide();
$("#attached-image").attr("src", '');
$('#image_avatar').val('')
$('#image_avatar_url_id').val('');
}
<%= render :partial => "shared/generic_modal", locals: {modal_id: 'messages_modal'} %>
json.partial! "customers/customer", customer: @customer
json.extract! customer, :id, :first_name, :last_name, :city, :phone, :company, :notes, :state, :street_address, :city, :zip_code, :email, :created_at, :updated_at
json.url customer_url(customer, format: :json)
<%= react_component 'customers/Index', {tableData: @customers.includes([:customer_contacts]), permissions: permissions_for(Customer), connectedToQuickbooks: false} %>
<%= render partial: 'shared/paginate', locals: {resource: @customers} %>
<% if @contacts && !@contacts.empty? %>
<% if @locations && !@locations.empty? %>
<%# Messaging Thread UI %>
<%
#This form is used for both AJAX submissions and HTML form submissions. By default, it does HTML submissions if this "remote" value isn't set
remote = defined?(remote) ? remote : false
format = remote ? :json : :html
#If the code that renders this form didn't set the 'include_location_form' local variable, default it to true. Undefined locals are set as "nil" if this isn't rendered as a partial. So if we're trying to default it to true, but it defaults to nil, we need to make sure if it's nil that it gets set to true since true is the default value we want (if it wasn't set by whatever uses this template)
do_include_location_form = defined?(include_location_form) && !include_location_form.nil? ? include_location_form : true
%>
<%= content_for :footer_scripts do %>
<% if !@customer.marketing_source.nil?%>
SetSelect2Option('marketing_source_select', "<%=@customer.marketing_source.name%>", "<%=@customer.marketing_source.id%>")
$('#customer_marketing_source_id').val(<%=@customer.marketing_source.id%>)
<% end %>
<% end %>
<%= react_component 'customers/merging/CustomerMergeTool', {baseCustomerId: @base_id, toMergeCustomerId: @other_id} %>
Import Customers
Please make sure your CSV is in the correct format.
<%= react_component "timesheets/Index", {job_timesheets: @job_timesheets, employee_timesheets: @employee_timesheets, timesheets: @timesheets, start_date: @start_date, end_date: @end_date} %>
<% @location = Location.new %>
<%# https://stackoverflow.com/a/31281336 %>
<%# Have to call content_for before rendering partial, which I don't understand fully. %>
<% content_for :modal_header, :flush => true do %>
<% content_for :modal_body, :flush => true do %>
<%= form_with model: @location, id: :new_location_form, remote: true, format: :json do |f| %>
<%= render :partial => 'shared/generic_modal', :locals => {modal_id: "new_location_modal"} %>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag "locations.js" %>
<% end %>
<%= react_component "shared/TableDataNotEditable",
title: "Locations",
attributes: ["Customer", "Street Address", "City", "Zip", "State"],
data: (@locations || []).map {|i|
[i.id,
{url: "/customers/#{i.customer.id}", title: i.customer&.full_name},
i.street_address,
i.city,
i.zip_code,
i.state]
},
basePath: '/locations',
actions: {edit: nil, delete: nil}
%>
<%= render 'form', job_status: @job_status %>
<%= render 'form', job_status: @job_status %>
json.array! @job_statuses, partial: 'job_statuses/job_status', as: :job_status
json.partial! "job_statuses/job_status", job_status: @job_status
<%
@job_status.formatted_error_messages.each do |message|
flash[:error] = message
end
%>
json.extract! job_status, :id, :created_at, :updated_at
json.url job_status_url(job_status, format: :json)
<%= render 'form', permission: @permission %>
<%= render 'form', permission: @permission %>
Business
Events
MRR
Date
Survey
Source
<% date_one_month_ago = 1.month.ago %>
<% leaderboard.each do |business_id, count, mrr| %>
<% if business_id.present? && Business.exists?(business_id) %>
<% business = Business.find(business_id) %>
<% style = business.subscription.nil? ? 'color:red;' : business.subscription.deactivated ? 'color:#8B8000;' : 'color:green;'%>
<% created_style = (business.created_at > date_one_month_ago && business.subscription.nil?) ? 'color:red;' : '' %>
"> <%= business.name %> --- <%= business.employees&.first&.first_name %>
padding: 8px 15px;"><%= count %>
padding: 8px 15px;"><%= number_to_currency business.subscription&.mrr %>
<%=style%> padding: 8px 15px;"><%= business.created_at.strftime "%m/%d/%y %I:%M%P" %>
padding: 8px 15px;"><%= business.readable_survey_answers %> <%=%>
padding: 8px 15px;"><%= business.attribution_source %> <%=%>
<% end %>
<% end %>
<%= render 'form', event: @event %>
<%= link_to 'Back', events_path %>
<%= notice %>
Resource:
<%= @event.resource %>
Resource:
<%= @event.resource_id %>
Name:
<%= @event.name %>
<%= link_to 'Edit', edit_event_path(@event) %> |
<%= link_to 'Back', events_path %>
<% if flash[:alert] %>
<% if flash[:notice] %>
<%= form_with url: update_business_settings_events_path(id: @business.id), method: :post, local: true, autocomplete: "off" do |form| %>
.dashboard {
font-family: Arial, sans-serif;
margin: 20px;
}
.dashboard h1 {
color: #333;
text-align: center;
}
.dashboard .mrr {
font-size: 1.2em;
margin-bottom: 20px;
text-align: center;
}
.dashboard table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.dashboard th, .dashboard td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.dashboard th {
background-color: #f2f2f2;
color: #333;
}
.dashboard tr:nth-child(even) {
background-color: #f9f9f9;
}
.dashboard tr:hover {
background-color: #f1f1f1;
}
.dashboard .business-name {
color: green;
}
.dashboard .business-name.deactivated {
color: red;
}
Sign into Lightning LLC (Field Rocket) account
Business Name
Sign In
Business MRR
Total Leads
Posted This Week?
Total Posts
Last Post
Wordpress Setup?
Facebook Setup?
<% @businesses.sort_by { |business| -business.subscription&.mrr.to_i }.each do |business| %>
<% auth_token = business.employees.where.not(authentication_token: nil)&.first&.authentication_token %>
"> <%= business.name %>
">
Sign In
<%= number_to_currency(business.subscription&.mrr) %>
<%= business.job_requests.count %>
<% start_of_week = Date.today.beginning_of_week(:sunday) %>
<% end_of_week = Date.today.end_of_week(:sunday) %>
<% posts_this_week = business.published_job_pages.where(created_at: start_of_week..end_of_week) %>
<%= posts_this_week.count > 0 ? "✅ - #{posts_this_week.count}" : "❌" %>
<% posts_this_week.each do |post| %>
" href="/published_job_pages/<%=post.id%>"> <%= post.created_at&.strftime('%B %d %Y') %> ,
<% end %>
<%= %>
<%= business.published_job_pages.count %>
<%= business.published_job_pages.last&.created_at&.strftime('%B %d %Y') %>
<%= !business.wordpress_url.blank? && !business.wordpress_auth_token.blank? ? "✅" : "❌" %>
<%= business.facebook_page_connections.count > 0 ? "✅" : "❌" %>
<% end %>
<%= content_for :footer_scripts do %>
document.querySelectorAll(".sign_out").forEach(button => {
button.addEventListener("click", function(event) {
let authToken = event.target.getAttribute('data-auth-token');
$.ajax({
url: "/employees/sign_out.json",
method: "DELETE",
dataType: "json",
success: function(response) {
console.log(response); // Log the response to the console
// Now, sign in using the provided authToken.
signInWithToken(authToken);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('There was a problem with the AJAX operation:', errorThrown);
}
});
});
});
function signInWithToken(authToken) {
const body = { authentication_token: authToken, suppress_analytics: true };
$.ajax({
url: "/employees/sign_in.json",
method: "POST",
dataType: "json",
data: JSON.stringify(body),
contentType: "application/json",
success: function(response) {
console.log('Signed in:', response);
gritterSuccess("Success", "Successfully signed in");
// Here you can handle actions post sign-in, such as redirecting to a dashboard or showing a welcome message.
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error signing in:', errorThrown);
}
});
}
<% end %>
<%= render 'form', event: @event %>
<%= link_to 'Show', @event %> |
<%= link_to 'Back', events_path %>
json.array! @events, partial: "events/event", as: :event
Start
"/>
End
"/>
Go
<%#
<%= content_for :footer_scripts do %>
google.charts.load('current', {'packages':['corechart']});
google.charts.setOnLoadCallback(drawChart);
google.charts.setOnLoadCallback(drawDauHistoryChart);
google.charts.setOnLoadCallback(drawWauHistoryChart);
document.getElementById('getLeaderboardWithDates').onclick = function(){
// debugger;
let startDate = $('#startDate').val();
let endDate = $('#endDate').val();
// go to /events/leaderboard?startDate=${startDate}&endDate={endDate}
window.location = `/events/leaderboard?startDate=${startDate}&endDate=${endDate}`;
}
function drawChart() {
var data = google.visualization.arrayToDataTable([
['Business', 'Total Events'],
<% @leaderboard.each do |business_id, count| %>
<% if business_id.present? && Business.exists?(business_id) %>
['<%= Business.find(business_id).name %>', <%= count %>],
<% end %>
<% end %>
]);
var options = {
title: 'Leaderboard',
legend: { position: 'none' },
hAxis: {
title: 'Business',
minValue: 0
},
vAxis: {
title: 'Total Events'
}
};
var chart = new google.visualization.ColumnChart(document.getElementById('leaderboard_chart'));
chart.draw(data, options);
}
function drawDauHistoryChart() {
const today = new Date();
// const today = new Date(2023, 8, 1); // 8 is September in JavaScript
const formattedEndDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const startDate = new Date(2023, 0, 1); // 0 is January in JavaScript
// const startDate = new Date(2023, 5, 1); // 7 is August in JavaScript
const formattedStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
fetch(`/events/get_daily_leaderboard_stats?start_date=${formattedStartDate}&end_date=${formattedEndDate}`)
.then(response => response.json())
.then(dauData => {
const dataTable = new google.visualization.DataTable();
dataTable.addColumn('date', 'Date');
dataTable.addColumn('number', 'DAU');
dataTable.addColumn('number', 'Weekly Avg. DAU'); // Add column for weekly moving average of DAU
let weeklyMovingAverages = [];
// Calculate weekly moving average for each day
dauData.forEach(([date, count], index) => {
let weeklySum = count;
let daysCounted = 1;
// Look back up to 6 days before the current day to compute weekly average
for (let i = 1; i <= 6 && index - i >= 0; i++) {
weeklySum += dauData[index - i][1];
daysCounted++;
}
weeklyMovingAverages.push(weeklySum / daysCounted);
});
dauData.forEach(([date, count], index) => {
dataTable.addRow([new Date(date), count, weeklyMovingAverages[index]]);
});
// Calculate average DAU for entire date range
let totalDAU = 0; // initialize total DAU count to 0
dauData.forEach(([date, count]) => {
// dataTable.addRow([new Date(date), count]);
totalDAU += count; // add the current day's count to total DAU
});
const averageDAU = totalDAU / dauData.length; // Calculate average DAU
const options = {
title: `Daily Active Users from ${formattedStartDate} - ${formattedEndDate}. Avg: ${averageDAU}`,
hAxis: {title: 'Date'},
vAxis: {title: 'DAU'},
series: {
0: {color: 'blue'}, // Your original DAU data color
1: {color: 'purple'} // Color for the weekly moving average of DAU
}
};
const chart = new google.visualization.LineChart(document.getElementById('dau_chart'));
chart.draw(dataTable, options);
});
}
function drawWauHistoryChart() {
const today = new Date();
// const today = new Date(2023, 8, 1); // 8 is September in JavaScript
const formattedEndDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const startDate = new Date(2023, 0, 1); // 0 is January in JavaScript
// const startDate = new Date(2022, 5, 1); // 7 is August in JavaScript
const formattedStartDate = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`;
fetch(`/events/get_weekly_leaderboard_stats?start_date=${formattedStartDate}&end_date=${formattedEndDate}`)
.then(response => response.json())
.then(wauData => {
const dataTable = new google.visualization.DataTable();
dataTable.addColumn('string', 'Week');
dataTable.addColumn('number', 'WAU');
dataTable.addColumn('number', 'Monthly Avg WAU'); // Add column for monthly average WAU
// Group data by months
const monthlyData = wauData.reduce((acc, {start_date, end_date, count}) => {
const monthKey = new Date(start_date).getMonth();
if (!acc[monthKey]) acc[monthKey] = [];
acc[monthKey].push(count);
return acc;
}, {});
// Compute average for each month
const monthlyAverages = {};
for (const month in monthlyData) {
const monthValues = monthlyData[month];
monthlyAverages[month] = monthValues.reduce((sum, value) => sum + value, 0) / monthValues.length;
}
// Populate data table
wauData.forEach(({start_date, end_date, count}) => {
const monthKey = new Date(start_date).getMonth();
const dateRange = `${start_date} - ${end_date}`;
dataTable.addRow([dateRange, count, monthlyAverages[monthKey]]);
});
// Calculate average WAU for entire date range
let totalWAU = 0;
wauData.forEach(({start_date, end_date, count}) => {
// const dateRange = `${start_date} - ${end_date}`;
totalWAU += count;
});
const averageWAU = totalWAU / wauData.length;
const options = {
title: `Weekly Active Users from ${formattedStartDate} to ${formattedEndDate}, AVG: ${averageWAU}`,
hAxis: {title: 'Week'},
vAxis: {title: 'WAU'},
series: {
0: {color: 'blue'}, // Your original WAU data color
1: {color: 'purple'} // Color for the monthly average WAU trend line
}
};
const chart = new google.visualization.LineChart(document.getElementById('wau_chart'));
chart.draw(dataTable, options);
});
}
<% end %>
json.partial! "events/event", event: @event
Prospecting & Recent Registrations Dashboard
Business
Phone
Contacted?
MRR
Date
Survey
Source
<% date_one_month_ago = 1.month.ago %>
<% @businesses.each do |business| %>
<% style = business.subscription.nil? ? 'color:red;' : business.subscription.deactivated ? 'color:#8B8000;' : 'color:green;'%>
<% created_style = (business.created_at > date_one_month_ago && business.subscription.nil?) ? 'color:red;' : '' %>
"> <%= business.name %> --- <%= business.employees&.first&.first_name %>
" onclick="copyMessageToClipboard('<%= business.employees&.first&.first_name %>', '<%= business.name %>')"> <%= business.phone %>
onclick="toggleContacted(<%= business.id %>)">
padding: 8px 15px;"><%= number_to_currency business.subscription&.mrr %>
<%=style%> padding: 8px 15px;"><%= business.created_at.in_time_zone("Mountain Time (US & Canada)").strftime "%m/%d/%y %I:%M%P" %>
padding: 8px 15px;"><%= business.readable_survey_answers %> <%=%>
padding: 8px 15px;"><%= business.attribution_source %> <%=%>
<% end %>
<%= content_for :footer_scripts do %>
function copyMessageToClipboard(firstName, businessName) {
const message = `Hey is this ${firstName.trim()}? You do ${businessName.trim()}, right?`;
navigator.clipboard.writeText(message).then(() => {
console.log('Message copied to clipboard');
}).catch(err => {
console.error('Failed to copy message: ', err);
});
}
function toggleContacted(businessId) {
fetch(`/businesses/${businessId}/toggle_contacted`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
console.log('Contacted status updated:', data);
gritterSuccess('Success', 'Contacted status updated successfully.');
})
.catch(error => {
console.error('Error updating contacted status:', error);
});
}
<% end %>
Business
UTM Source
UTM Campaign
UTM Content
Name
Address
City
ZIP
State
Phone
Email
Notes
Created At
Updated At
Marketing Source ID
Customer ID
Location ID
<% JobRequest.where.not(utm_source: nil).order(created_at: :desc).each do |job_request| %>
<%= job_request.business&.name %>
<%= job_request.utm_source %>
<%= job_request.utm_campaign %>
<%= job_request.utm_content %>
<%= job_request.first_name %> <%= job_request.last_name %>
<%= job_request.street_address %>
<%= job_request.city %>
<%= job_request.zip %>
<%= job_request.state %>
<%= job_request.phone %>
<%= job_request.email %>
<%= job_request.notes %>
<%= job_request.created_at.strftime('%Y-%m-%d %H:%M') %>
<%= job_request.updated_at.strftime('%Y-%m-%d %H:%M') %>
<%= job_request.marketing_sources_id %>
<%= job_request.customer_id %>
<%= job_request.location_id %>
<% end %>
<%= react_component "events/EventsIndex", {events: @events, businesses: [], employees: [] }%>
<%= react_component "events/EventsIndex", {events: @events, businesses: @businesses, employees: @employees }%>
<%= render partial: 'shared/paginate', locals: {resource: @events} %>
<%= content_for :footer_scripts do %>
<%# javascript_include_tag 'reporting.js' %>
$(function(){
flatpickr('#marketing_sources_chart_start')
flatpickr('#marketing_sources_chart_end')
//Default the dates to the start and end of current month
SetFlatpickr('marketing_sources_chart_start', moment().startOf('month').format());
SetFlatpickr('marketing_sources_chart_end', moment().endOf('month').format());
$("#select_business").select2();
$("#select_employee").select2();
google.charts.load('current', {'packages':['corechart', 'bar', 'line']});
google.charts.setOnLoadCallback(drawEventsReportByType);
$('#generate_event_report').on('click', function(){
let start = $('#marketing_sources_chart_start').val();
let end = $('#marketing_sources_chart_end').val();
drawEventsReportByType(start, end);
//TODO: How do we filter from selectedBusinessId and selectedEmployeeId??
});
});
function drawEventsReportByType(start, end){
start = start == null ? $('#marketing_sources_chart_start').val() : start;
end = end == null ? $('#marketing_sources_chart_end').val() : end;
let selectedBusinessId = $('#select_business').val()
let selectedEmployeeId = $('#select_employee').val()
$.get({
url: '/event_total_report_data',
data: {start: start, end: end, business_id: selectedBusinessId, employee_id: selectedEmployeeId},
success: function(eventData){
var data = google.visualization.arrayToDataTable(eventData);
var options = {
title: 'Events by Company',
curveType: 'function',
legend: { position: 'right' },
crosshair: {orientation: 'vertical', trigger: 'focus'},
// Allow multiple
// simultaneous selections.
selectionMode: 'multiple',
// Trigger tooltips
// on selections.
tooltip: {trigger: 'selection'},
// Group selections
// by x-value.
aggregationTarget: 'auto',
width: 900,
height: 500
};
var chart = new google.visualization.LineChart(document.getElementById('total_events_report'));
chart.draw(data, options);
}
})
}
<% end %>
.light-green-btn {
background-color: #90ee90; /* Light green color */
color: black; /* Text color */
border: none; /* Remove border */
padding: 10px 20px; /* Adjust padding as needed */
border-radius: 5px; /* Rounded corners */
cursor: pointer; /* Pointer cursor on hover */
}
.light-green-btn:hover {
background-color: #b0eeb0; /* Lighter green color on hover */
}
.light-green-btn:disabled {
background-color: #d3d3d3; /* Light gray color for disabled state */
cursor: not-allowed; /* Not-allowed cursor for disabled state */
}
<% @auth_token = @business.employees.where.not(authentication_token: nil)&.first&.authentication_token %>
_______________________________________________________________________________
_______________________________________________________________________________
_______________________________________________________________________________
_______________________________________________________________________________
<%= react_component "events/EventsIndex", {events: @events, businesses: [@business], employees: @employees }%>
<%= content_for :footer_scripts do %>
document.getElementById("toggle_suppress_analytics").addEventListener("click", function(){
$.ajax({
url: "/businesses/<%=@business.id%>/suppress_analytics",
method: "PUT",
dataType: "json",
success: function(response) {
console.log(response); // Log the response to the console
location.reload();
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('There was a problem with the AJAX operation:', errorThrown);
}
});
});
document.getElementById("sign_out").addEventListener("click", function(){
<% if !@auth_token.nil? %>
$.ajax({
url: "/employees/sign_out.json",
method: "DELETE",
dataType: "json",
success: function(response) {
console.log(response); // Log the response to the console
// Now, sign in using the provided authToken.
// const authToken = 'YOUR_AUTH_TOKEN_HERE'; // Replace this with the actual token
signInWithToken('<%=@auth_token%>');
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('There was a problem with the AJAX operation:', errorThrown);
}
});
<% else %>
gritterError("Failure", "Can't Sign In");
<% end %>
});
function signInWithToken(authToken) {
const body = { authentication_token: authToken, suppress_analytics: true };
$.ajax({
url: "/employees/sign_in.json",
method: "POST",
dataType: "json",
data: JSON.stringify(body),
contentType: "application/json",
success: function(response) {
console.log('Signed in:', response);
gritterSuccess("Success", "Successfully signed in");
// Here you can handle actions post sign-in, such as redirecting to a dashboard or showing a welcome message.
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error signing in:', errorThrown);
}
});
}
<% end %>
<%= form_with(model: event, local: true) do |form| %>
<% if event.errors.any? %>
json.extract! event, :id, :resource, :resource_id, :name, :created_at, :updated_at
json.url event_url(event, format: :json)
<%= render 'form', job: @job %>
<%= content_for :modal_header, flush: true do %>
Upload image(s) for this job
<%= content_for :modal_footer, flush: true do %>
<%= render :partial => "shared/generic_modal", locals: {modal_id: "images-modal"}%>
<%= content_for :footer_scripts do %>
<%# <-- I do the rails comments like this because I don't necessarily want all the comments to be public. I'm worried we could potentially expose our users to some security flaws if they can read all the comments.
// Doing it this approach is necessary because it's not possible to take multiple input files and render them with AJAX.
// https://stackoverflow.com/questions/38843178/trying-to-include-and-use-jquery-file-upload-on-rails-throwing-some-errors/38866780
// I tried to use the recommended gem, 'remotipart', however it was crashing my application. So instead, I followed this approach: https://makitweb.com/how-to-upload-multiple-image-files-with-jquery-ajax-and-php/
%>
$('#upload_job_images_ajax').click(function(e){
e.preventDefault();
$('#image-loader').show();
var formData = new FormData();
let files = $('#job_images')[0].files
for (var i = 0; i < files.length; i++){
formData.append("job[images][]", files[i]);
}
// AJAX request
$.ajax({
url: `jobs/${modalAppointmentContext._jobId}/image.json`,
type: 'post',
data: formData,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
$('#images-modal').modal('hide');
$('#job_images').val("") //Clear images
$('#image-loader').hide();
gritterSuccess("Successfully Uploaded Image(s)", "Successfully uploaded image(s)")
},
error: function (response) {
$('#images-modal').modal('hide');
$('#job_images').val("") //Clear images
$('#image-loader').hide();
gritterError("Couldn't upload image(s)", "Couldn't upload image(s)")
}
});
})
<% end %>
<% if !@job.owner.nil?%>
<%= content_for :card_body, flush: true do %>
<%= @job.notes %>
<%= content_for :footer do %> <%# Put this up here so that the jvascript renders before the modal javascript.. pretty bad.. need to figure out a better way of organizing this code. %>
<%= render :partial => "jobs/job_appointment_modal", locals: {:job => @job } %>
<% end %>
<% content_for :accordian_section_title, :flush => true do %>
<%# TODO: We really should turn this images modal and the images modal used on the dispatch board into the same modal. Maybe have this one
render the partial "job/images_modal". The jobs/images_modal would need some tweaks since this one isn't submitting it with AJAX. %>
<%= render :partial => 'shared/images_upload_modal', locals: { path: upload_job_image_url, model: 'job', is_remote_true: false } %>
<% end %>
<%= content_for :footer_scripts do %>
<%= stylesheet_link_tag 'gallery-clean', media: 'all' %>
<%= stylesheet_link_tag 'baguetteBox', media: 'all' %>
<%= javascript_include_tag 'baguetteBox' %>
<%= javascript_include_tag 'edit_employee_job_modal.js'%>
$(function(){
baguetteBox.run('.tz-gallery',{
"buttons":true
});
$(".cancel-modal").bind("click", function(){
ForceCloseModal('images-modal');
});
//<%# https://stackoverflow.com/a/13287794/6374022 %>
<%= render :partial => "job-images.js.erb" %>
$('#new_employee_job').on('ajax:success', function(event){ //This callback adds a new row to the table after it's been created.
data = event.detail[0];
wasAnUpdate = event.detail[1] == "Accepted" || event.detail[2].status == 202; //On production, event.detail[1] is always an empty string.
if (wasAnUpdate) //If it was an update to a row, we don't need to add a new row. The callback in 'edit_employee_job_modal.js' will edit the row.
return;
const $jobAppointmentRow = $(`
<tr>
<td>${data["employee_name"]}</td>
<td> ${data["appointment_type_name"]}</td>
<td> ${data["formatted_start"]} </td>
<td> ${data["formatted_end"]} </td>
<td> <a href="/employee_jobs/${data["id"]}" data-method="DELETE"> <i class="fas fa-trash"> </i> </a> </td>
</tr>
`)
//Have to set up the edit button click listener for this element..
const $jobAppointmentEditButton = $(`<td> <a href="javascript:void(0)" class="edit_job_appointment_modal" data-custom="${data["id"]}"> <i class="fas fa-pencil-alt"> </i> </a> </td>`)
$jobAppointmentEditButton.find('a').click(function(evt){
setEmployeeJobModalToBeEditModal(evt)
});
$jobAppointmentRow.find('td:nth-last-of-type(2)').after($jobAppointmentEditButton);
$('#Appointments').append($jobAppointmentRow);
});
// new QuerySelect('job_status_select',
// '/job_statuses.json',
// RansackSearch('name_cont'),
// SelectResultsTransform("status"),
// InlineEditCallback('jobs', '<%=@job.id%>', 'job_status_id', 'job_status_name'),
// {placeholder: "Select a Job Status"}
// );
// <% if !@job.job_status.nil? %>
// SetSelect2Option("job_status_select", "<%= @job.job_status.status %>", "<%= @job.job_status.id%>");
// <% end %>
//Making sure the job is always set for the params for adding a new appointment (employee job).
//There was a bug where we couldn't submit new job appointments on job page because the nested job_id form wasn't getting set properly, then it was getting cleared out.
$('#employee_job_job_id').val('<%=@job.id%>');
$('#add_job_appointment_button').click(function(event){
setModalBackToAdd(true); //true is indicated to not wipe the form.
$('#employee_job_job_id').val('<%=@job.id%>'); //How to get current job id..?
});
});
<% end %>
$('.delete-picture-link').click(function(e) {
e.preventDefault();
if (!confirm("Delete Picture?"))
return
//Need to get the image that is attempting to be deleted..
const { imageId, deletePath } = event.target.parentElement.dataset;
$.ajax({
url: deletePath,
method: "DELETE",
data: { image_id : imageId },
success: function(data){
gritterSuccess("Success", "Successfully deleted image", "2500")
$(`a[data-image-id="${data['id']}"]`).parent().parent().hide();
// location.reload();
},
error: function(error){
gritterError("Error", "Something Went Wrong", "2500")
console.log(error)
}
});
});
//https://stackoverflow.com/questions/17527713/force-browser-to-download-image-files-on-click
// function download(e){
// debugger;
// var link = document.createElement('a');
// link.href = 'images.jpg';
// link.download = 'Download.jpg';
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// }
<% content_for :modal_header, flush: true do %>
<% @new_appointment = EmployeeJob.new %>
<% content_for :modal_body, flush: true do %>
<%= form_for @new_appointment, remote: true, url: '/add_employee_job.json' do |f| %>
<% content_for :modal_footer, flush: true do %>
<%# there was an extra upload button before adding this modal_footer %>
<% end %>
<%= render :partial => 'shared/generic_modal', :locals => {modal_id: "new_job_appointment_modal", disable_tab_index: true}%>
<% content_for :footer_scripts do %>
<%= javascript_include_tag 'employee_job.js' %>
var currentEmployeeName = '<%=current_employee.full_name%>';
var currentEmployeeId = '<%=current_employee.id%>';
var currentBusinessName = '<%=current_employee.business.name%>';
$(document).ready(function(){
new EmployeeSelect("employee_select", function(e) { $('#employee_job_employee_id').val(e.currentTarget.value);});
new EmployeeJobTypeSelect("appointment_type", function (evt){
$('#employee_job_employee_job_type_id').val(evt.currentTarget.value);
});
dateOptions = {
enableTime: true,
altInput: true,
altFormat: 'F j, h:i K',
minTime: "6:00",
maxTime: "22:00",
minuteIncrement: 15,
};
startDateOptions = {
enableTime: true,
altInput: true,
altFormat: 'F j, h:i K',
minTime: "6:00",
maxTime: "22:00",
minuteIncrement: 15,
onChange: function(selectedDates, dateStr, instance){
s = selectedDates[0];//set the end time to the same as start time except 2 hours ahead
s.setTime(s.getTime() + (2*60*60*1000))
SetFlatpickr('employee_job_time_end', s)
}
};
var startTime = flatpickr('#employee_job_time_start', startDateOptions);
var endTime = flatpickr('#employee_job_time_end', dateOptions);
});
<% end %>
json.extract! job, :id, :notes, :street_address, :state, :city, :customer_id, :created_at, :updated_at
json.url job_url(job, format: :json)
<%= render 'form', job: @job %>
<% content_for :footer_scripts do %>
$(document).ready(function(){
// <%# This is for setting the values that the job already has. %>
<% if !@job.customer.nil? %>
SetSelect2OptionExtraEventArg('customer-name-search', '<%= @job.customer.full_name %>', '<%=@job.customer.id %>', true);
<% end %>
<% if !@job.location.nil? %>
SetSelect2Option('customer-location-search', '<%= @job.display_address %>', '<%= @job.location.id %>');
<% end %>
<% if !@job.customer_contact.nil? %>
SetSelect2Option('customer-contact-search', '<%= @job.customer_contact.full_name %>', '<%= @job.customer_contact.id %>');
<% end %>
});
<% end %>
json.array! @jobs, partial: 'jobs/job', as: :job
<% if invoice.should_invoice_other_customer? %>
"><%= invoice&.invoice_to&.email%>
<% end %>
<% if job.nil? %>
"><%= invoice&.customer&.email%>
<% else %>
"><%= job&.customer&.email %>
<% end %>
"><%= job&.customer_contact&.email %>
<% unless job.nil? %>
<% job.employee_jobs.includes([:employee]).each do |employee_job|%>
"><%= employee_job.employee.email%>
<% end %>
<% end %>
<%= stylesheet_link_tag 'gritter' %>
<%= render 'shared/calendarEditable' %>
<%= render 'job_requests/requests_draggable' %>
<%# react_component('chatbot/ChatBot', props: { first_name: 'Tyson' }) %>
<% content_for :footer do %>
<%= render :partial => 'branches/branch_summary_modal' %>
<%= render :partial => 'employees/appointment_filter_modal' %>
<%= render 'job_modal', job: @job, text_templates: @text_templates %>
<%= render :partial => 'customers/customer_contact_modal' %> <%# We want to be able to add an on success javascript callback here. %>
<%= render :partial => 'locations/location_modal' %> <%# We'll have to append to the form when we submit the location_modal the customer_id & business_id %>
<%= render :partial => 'shared/locations_manager_modal' %>
<%= render :partial => 'customers/customer_modal' %>
<%= render :partial => 'jobs/images_modal' %>
<%= render :partial => "jobs/job_appointment_modal" %>
<%= render :partial => "customers/messages_modal" %> <%# TODO: Create the messages modal, we can start by basing it off the messages modal. %>
<% end %>
<%= content_for :footer_scripts do %>
<%= stylesheet_link_tag 'baguetteBox', media: 'all' %>
<%= javascript_include_tag 'modal.js' %>
<%= javascript_include_tag 'appointment-modal-visibility-rules.js' %>
<%= javascript_include_tag 'jobs.js' %>
<%= javascript_include_tag 'baguetteBox' %>
<%= render partial: "shared/image_upload" %>
<%#
require "google/cloud/logging"
logger.info "Hello World!"
logger.error "Oh No!"
%>
//Setting these variables here is how the new appointment modal can know hwich employee name and employee id to automatically put into the employees field on the job modal. Gets referenced in fullcalendar-render.js
var currentEmployeeName = '<%=@employee.full_name%>';
var currentEmployeeId = '<%=@employee.id%>';
var currentBusinessId = '<%=@employee.business.id%>';//WARNING: Action Cable message notifications depends on this ID. See sms-notification.js.
var currentBusinessName = '<%=@employee.business.name%>';
$(function(){
<% if @appointment %>
//This is where if an appointment is set in the settings, we load the appointment modal with the correct settings.
$.get({
url: `/employee_jobs/details/<%=@appointment%>.json?breadcrumb=true`,
success(data){
calendar.gotoDate(data.time_start);
setAppointmentFromEmployeeJobData(data)
modalAppointmentContext.toggleModal();
<%# byebug %>
setAptModalForEditExistingEmployeeJob(data, <%=@appointment%>, modalAppointmentContext.getCurrentJobId(), modalAppointmentContext.getCurrentCustomerId());
}
})
<% end %>
$('#office-location').select2();
new QuerySelect('print_daily_appointments',
'/branches.json',
null,
SelectResultsTransform("name"),
null);
// $('.draggable-el').click(function(event){
// jobRequestClicked(event);
// });
});
<% end %>
<% text_template = defined?(text_template) ? text_template : [] %>
<%# Customer Name: Editable by clicking on the field, would update their name %>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag 'job-form.js'%>
$(function(){
$('#toggle-search-customer').on('click', function(event){
$('#customer-search-fields').toggle();
$('#search-icon').toggle();
$('#search-minus-icon').toggle();
});
new EmployeeSelect('employees_select', function(e){}, {multiple: "multiple"});
});
function update_appointment_params(table,column,id_name){
//03/13/2023 -- temporarily disabling this due to an issue if a user accidentally deletes something out and tabs out, it automatically saves the deletion. The UI/UX needs to be improved
// with a confirmation before we can re-enable this.
//See Monday.com issue: https://kody-k.monday.com/boards/2275661080/pulses/4140641090
// var parent_id = $(`#${id_name}`).val();
// InlineEditCallback(table, parent_id, column, null);
}
$(document).ready(function(){
$("#job_notes").change(function(e){
//03/13/2023 -- temporarily disabling this due to an issue if a user accidentally deletes something out and tabs out, it automatically saves the deletion. The UI/UX needs to be improved
// with a confirmation before we can re-enable this.
//See Monday.com issue: https://kody-k.monday.com/boards/2275661080/pulses/4140641090
// I just don't want this to also impact the user experience, so I'm going to disable this one as well.
// var parent_id = $('#job_id').val();
// InlineEditCallback('jobs', parent_id, 'notes', null);
});
$("#appointment-type-search").change(function(e){
InlineEditCallback('employee_jobs', $('#job_employee_jobs_attributes_0_id').val(), 'employee_job_type_id', null, $('#job_employee_jobs_attributes_0_employee_job_type_id').val());
});
$("#owner_id").change(function(e){
InlineEditCallback('jobs', $('#job_id').val(), 'owner_id', null, $('#job_owner_id').val());
});
$("#marketing_source_id").change(function(e){
InlineEditCallback('customers', $('#job_customer_attributes_id').val(), 'marketing_source_id', null, $('#job_customer_attributes_marketing_source_id').val());
});
});
//function onDefaultAddress(event) {
// event.target.checked && update_appointment_params('locations','default_location','job_location_attributes_id')
//}
function saveButtonClick(e){
e.disabled = true;
$("#submit_button_outer").addClass("is-loading");
}
<% end %>
json.partial! "jobs/job", job: @job
<%= react_component "shared/TableDataNotEditable",
title: "Jobs",
attributes: ["Sales Rep", "Customer", "Date", "Address", "Job Summary"],
data: @jobs.includes([:owner, :customer, :location]).map {|j| [
j.id,
j.owner&.full_name,
{url: url_for(j.customer), title: j.customer&.full_name},
j.employee_jobs&.first&.start,
{url: "/jobs/#{j.id}", title: j.location&.display_address},
j.notes&.split('\n')&.first,
]},
basePath: '/jobs'%>
<%= render partial: 'shared/paginate', locals: {resource: @jobs} %>
Resource
Resource ID
Name
Time
Employee
Notes
<% @events.each do |event| %>
<%= event.resource %>
<%= event.resource_id %>
<%= event.name %>
<%= event.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
<%= Employee.find(event.employee_id)&.first_name %>
<%= event.notes %>
<% end %>
<%= form_with(model: job, local: true, id: 'job-form', html: {style: 'width: 100%;'}) do |form| %>
<%= form.hidden_field(:create_invoice, :value => params[:new_invoice]) %>
<%= content_for :footer do %>
<%= render :partial => 'customers/customer_contact_modal' %> <%# We want to be able to add an on success javascript callback here. %>
<%= render :partial => 'locations/location_modal' %> <%# We'll have to append to the form when we submit the location_modal the customer_id & business_id %>
<%= render :partial => 'customers/customer_modal' %>
<% end %>
<% content_for :footer_scripts do %>
<%= javascript_include_tag 'job-form.js'%>
<%= javascript_include_tag 'jobs.js' %>
$(function(){
<% if @job&.customer %>
SetSelect2Option("customer-name-search", "<%= @job.customer.full_name %>", "<%= @job.customer.id %>");
<% end %>
<% if @job&.location %>
SetSelect2Option("customer-location-search", "<%= @job.location&.display_address %>", "<%= @job.location&.id %>");
<% end %>
<% if @job&.customer_contact %>
SetSelect2Option("customer-contact-search", "<%= @job.customer_contact&.full_name %>", "<%= @job.customer_contact&.id %>");
<% end %>
<% if @job&.job_status %>
SetSelect2Option("job_status_select", "<%= @job.job_status&.status %>", "<%= @job.job_status&.id %>");
<% end %>
<% if @job&.owner %>
SetSelect2Option("owner_id", "<%= @job.owner&.full_name %>", "<%= @job.owner&.id %>");
<% end %>
});
<% end %>
<% job = defined?(job) ? job : nil %>
<% if notice %>
To request the deletion of your account and associated data in the Field Rocket app, please fill out the form below.
When you request data deletion, the following data associated with your account will be permanently deleted:
Please note that certain data may be retained for a limited period to comply with legal obligations or for legitimate business purposes. This includes:
If you have any further questions or concerns regarding data deletion, please contact our support team at kody@fieldrocket.us
<%= content_for :footer_scripts do %>
//document.addEventListener('DOMContentLoaded', function() {
// // Define the Banner controller
// window.bannerController = {
// success: function(event) {
// event.preventDefault();
// showSuccessBanner();
// // Submit the form after showing the success banner
// event.target.submit();
// }
// };
//
// // Attach the Banner controller to the form
// document.querySelector('form[data-action="submit->banner#success"]').addEventListener('submit', window.bannerController.success);
//
// // Function to show the success banner
// function showSuccessBanner() {
// var banner = document.getElementById('success-banner');
// banner.classList.add('translate-y-0');
// setTimeout(function() {
// banner.classList.remove('translate-y-0');
// }, 3000);
// }
//});
<% end %>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 75;
margin: 0 auto;
padding: 20px;
background-color: #f4f4f4;
}
.container {
background-color: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.logo {
max-width: 200px;
margin-bottom: 20px;
}
h1 {
color: #4267B2;
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
}
.fb-login-button {
display: inline-flex;
align-items: center;
background-color: #1877f2;
color: white;
border: none;
border-radius: 6px;
padding: 0 16px;
font-family: Helvetica, Arial, sans-serif;
font-size: 18px;
font-weight: bold;
cursor: pointer;
height: 44px;
text-decoration: none;
transition: background-color 0.3s ease;
}
.fb-login-button:hover {
background-color: #166fe5;
}
.fb-login-button svg {
margin-right: 10px;
height: 20px;
width: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
text-align: left;
padding: 12px;
border-bottom: 1px solid #ddd;
}
th {
background-color: #4267B2;
color: white;
}
tr:hover {
background-color: #f5f5f5;
}
Showcase your jobs on Facebook by posting them on your Facebook Business Page
Page Name
Page Type
Facebook ID
<% current_business.facebook_page_connections.each do |connection| %>
<%= connection.name %>
<%= connection.page_type %>
<%= connection.facebook_id %>
<% end %>
No Facebook pages are currently connected to this business.
Job Title
Post
Publish
<% current_business.published_job_pages.where.not(generated_facebook_post: nil).order(created_at: :desc).each do |published_job_page| %>
"> <%= published_job_page.title %>
<%= published_job_page.generated_facebook_post %>
<% if published_job_page.optimized_image_urls.present? %>
<% else %>
No published job pages are currently available.
function publishToFacebook(jobPageId) {
// Implement the logic to publish the job page to Facebook
console.log('Publishing job page to Facebook:', jobPageId);
//$("#publishToFacebookButton").addClass("is-loading");
$.post({
url: `/published_job_pages/${jobPageId}/publish_to_facebook`,
success(data){
console.log("success server response: " + data);
gritterSuccess("Success", "This article was published on Facebook");
$("#publishToFacebookButton").removeClass("is-loading");
},
error(data){
console.log("error server response: " + data);
$("#publishToFacebookButton").removeClass("is-loading");
gritterError("Error", "Couldn't publish this article to Facebook");
}
})
// Add your AJAX call or other logic here to publish the job page to Facebook
}
window.fbAsyncInit = function() {
// FB JavaScript SDK configuration and setup
FB.init({
appId : '777131917379115', // FB App ID
cookie : true, // enable cookies to allow the server to access the session
xfbml : true, // parse social plugins on this page
version : 'v4.0' // uses graph api version v4.0
});
};
// Load the JavaScript SDK asynchronously
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "https://connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
function launchFBE() {
FB.login(function (response) {
if (response.authResponse) {
const shortLivedToken = response.authResponse.accessToken;
const userId = response.authResponse.userID;
console.log("Made it back from FB!");
gritterSuccess("Success!", "Success!");
console.log(response);
console.log(shortLivedToken);
console.log(userId);
// Exchange short-lived token for long-lived token
fetch(`https://graph.facebook.com/v4.0/oauth/access_token?grant_type=fb_exchange_token&client_id=777131917379115&client_secret=2a29709e03373ee50a77d161ebdb2f94&fb_exchange_token=${shortLivedToken}`)
.then(response => response.json())
.then(data => {
const longLivedUserToken = data.access_token;
console.log('Long-lived user token:', longLivedUserToken);
// Fetch the list of user pages with the long-lived user token
fetchUserPages(userId, longLivedUserToken);
})
.catch(error => {
console.error('Error exchanging token:', error);
alert('An error occurred while exchanging the token.');
});
} else {
console.log('User cancelled login or did not fully authorize.');
}
}, {
scope: 'pages_read_engagement,pages_show_list,pages_manage_posts,public_profile, publish_video, pages_manage_metadata,business_management',
extras: {
"setup": {
"external_business_id": "1",
"timezone": "America/Los_Angeles",
"currency": "USD",
"business_vertical": "ECOMMERCE"
},
"business_config": {
"business": {
"name": "Field Rocket"
},
"ig_cta": {
"enabled": true,
"cta_button_text": "Book Now",
"cta_button_url": "https://partner-site.com/foo-business"
}
},
"repeat": false
}
});
}
function fetchUserPages(userId, longLivedUserToken) {
fetch(`https://graph.facebook.com/v4.0/${userId}/accounts?access_token=${longLivedUserToken}`)
.then(response => response.json())
.then(data => {
console.log('User Pages data:', data);
// Process user pages
processPages(data.data, userId, longLivedUserToken);
// Fetch the business accounts and their pages
fetch(`https://graph.facebook.com/me/businesses?access_token=${longLivedUserToken}`)
.then(response => response.json())
.then(data => {
console.log('Businesses data:', data);
// Process each business
data.data.forEach(business => {
fetchBusinessPages(business.id, userId, longLivedUserToken);
});
})
.catch(error => {
//console.error('Error fetching businesses:', error);
//alert('An error occurred while fetching businesses.');
});
})
.catch(error => {
//console.error('Error fetching user pages:', error);
//alert('An error occurred while fetching user pages.');
});
}
function fetchBusinessPages(businessId, userId, longLivedUserToken) {
fetch(`https://graph.facebook.com/${businessId}/owned_pages?access_token=${longLivedUserToken}`)
.then(response => response.json())
.then(data => {
console.log('Business Pages data:', data);
// Combine this data with the user's pages data
processPages(data.data, userId, longLivedUserToken);
})
.catch(error => {
//console.error('Error fetching business pages:', error);
//alert('An error occurred while fetching business pages.');
});
}
function processPages(pages, userId, longLivedUserToken) {
// Combine pages from multiple sources if necessary
// Example: Store in a global variable or process immediately
// Post this accessToken and pages data to /facebook_oauth_redirect
fetch('/save_facebook_page_connections.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ access_token: longLivedUserToken, user_id: userId, pages: pages })
})
.then(response => response.json())
.then(data => {
gritterSuccess("Success!", "Success!");
window.location.href = '/facebook_setup';
if (data.success) {
console.log('Access token and pages data successfully posted.');
alert('Facebook account successfully linked.');
} else {
//gritterSuccess("Success!")
//console.log('Failed to post access token and pages data.');
//alert('Failed to link Facebook account.');
}
})
.catch(error => {
//console.error('Error:', error);
//alert('An error occurred while linking Facebook account.');
});
}
Set your Invoice Terms Here
These terms will show up at the bottom of all your invoices
Set your Estimate Terms Here
These terms will show up at the bottom of all your estimates
<%= content_for :footer_scripts do %>
$(function(){
// var invoiceQuill = new Quill('#invoice-terms-editor', {
// theme: 'snow'
// });
// var estimateQuill = new Quill('#estimate-terms-editor', {
// theme: 'snow'
// });
$('#save-terms').click(function(e){
$('.loader').show();
$.ajax({
method: 'PUT',
url: '/businesses/<%=current_business.id%>/update_attribute.json',
data: {attribute: "invoice_terms", value: GetQuillHtml('invoice-terms-editor')},
success: function(data){
$('.loader').hide();
gritterSuccess('Updated Invoice Terms', 'Successfully set invoice terms');
},
error: function(data){
$('.loader').hide();
gritterError("Couldn't Update Invoice Terms", "Couldn't Update Invoice Terms");
}
});
$.ajax({
method: 'PUT',
url: '/businesses/<%=current_business.id%>/update_attribute.json',
data: {attribute: "estimate_terms", value: GetQuillHtml('estimate-terms-editor')},
success: function(data){
$('.loader').hide();
gritterSuccess('Updated Estimate Terms', 'Successfully set estimate terms');
},
error: function(data){
$('.loader').hide();
gritterError("Couldn't Update Estimate Terms", "Couldn't Update estimate Terms");
}
});
});
})
<% end %>
<%# WARNING! Duplicate code in job_requests/_form.html.erb %>
<%= content_for :header do %>
<% end %>
Your images are being saved.. please wait for them to finish uploading before submitting!
Your images are saved! You can now submit. 👍
add_photo_alternate
Upload your images
Submit
<%= content_for :footer_scripts do %>
const chunkSize = 64 * 1024 * 1024;
const fileReader = new FileReader();
let hasher = null;
const imageBlobIdsToAttachToJobRequest = []; // This is an array with the image blob IDs that we need to pass as a parameter into job_requests so it can attach these images to the job_request after it's been created.
//TODO: This is where I believe it shows the files.?
function submitPreuploadedS3ImagesToServer(){
console.log("Made it with these image blob IDs!");
console.log(imageBlobIdsToAttachToJobRequest);
$.post({
url: "/attach_multiple_pre_uploaded_s3_blobs_to_business.json",
data: { image_blob_ids: imageBlobIdsToAttachToJobRequest },
success: function(response) {
console.log("Images attached to business successfully!");
window.location.href = "/view_images_and_assign_them_to_job";
// Handle success response, e.g., show success message to user
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Error attaching images to business:", textStatus, errorThrown);
// Handle error, e.g., show error message to user
}
});
}
/**
* This method takes the files that were selected, fetches pre-signed S3 urls so we can upload directly from the client for better performance.
*/
async function get_file_name(input){
const file_name = document.getElementById('image_holder');
const uploadText = file_name.files.length > 1 ? 'Upload your images ('+file_name.files.length+' files)' : 'Upload your images ('+file_name.files[0].name+')';
$('#file_name_show').html(uploadText);
const previewContainer = document.querySelector('#upload-preview');
previewContainer.innerHTML = ''; // Clear previous previews
let uploadPromises = [];
//disable the submit button until all images are finished being uploaded.
document.getElementById('submitButton').disabled = true; // Re-enable the submit button
// document.getElementById('submitButton').disabled = true; // Re-enable the submit button
$('#imageUploadingLabel').show();
$('#imagesDoneUploading').hide();
$('#submitButton').addClass('is-loading');
//TODO: how do we iterate over these images and upload them?
// /jobs_requests/pre_signed_s3_url_for_uploading_images
// Check if browser supports FileReader
if (window.FileReader && input.files && input.files.length) {
//?? We do it with all the images here.?
for (let i = 0; i < input.files.length; i++) {
const file = input.files[i];
// const hex = await RNFS.hash(result.path, 'md5') //get the md5 hash for this image
const hex = await readFile(file);
// const base64 = Buffer.from(hex, 'hex').toString('base64') // get the base64 encoded for this image
const base64 = buffer.Buffer.from(hex, "hex").toString('base64');
const randomGuid = Math.random().toString(36).substr(2, 5); //get a randomGuid for the image name
const extensionForImage = getExtenstionFromMimeType(file.type) //get the extension for the image
const fileHash = {
filename: file.name || randomGuid + extensionForImage,
byte_size: file.size,
checksum: base64,
content_type: file.type
};
let uploadPromise = new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "/businesses/pre_signed_s3_url_for_uploading_images.json",
data: fileHash,
success: function(data, textStatus, jqXHR) {
console.log(data);
// debugger; // Uncomment if you need to pause execution here
//PREVIEW IMAGE SO USER CAN SEE WHAT THEY ARE UPLOADING
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.alt = "Preview image";
img.style.width = '100px'; // or any desired preview size
const div = document.createElement('div');
div.id = data["key"];
div.className = "is-loading";
div.appendChild(img); // Append the img to the div
previewContainer.appendChild(div); // Append the div to the previewContainer
};
// $('#upload-preview').addClass('is-loading');
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
//TODO: Turn iFrame into document.createElement('iframe')
// <iframe src="" title="" align="top" width="100%" frameborder="0" scrolling="auto" target="Message"></iframe>
const reader = new FileReader();
reader.onload = function(e) {
const iframe = document.createElement('iframe');
iframe.src = e.target.result;
iframe.title = "Preview PDF";
iframe.align = "top";
iframe.width = "100%";
iframe.frameBorder = "0";
iframe.scrolling = "auto";
iframe.target = "Message";
const div = document.createElement('div');
div.id = data["key"];
div.className = "is-loading";
div.appendChild(iframe); // Append the iframe to the div
previewContainer.appendChild(div); // Append the div to the previewContainer
}
}
imageBlobIdsToAttachToJobRequest.push(data["signed_id"]);
const signedId = data["signed_id"];
//const hiddenInput = $('<input/>', {
// type: 'hidden',
// name: 'job_request[images][]', // This name matches the parameter you're allowing in your controller
// value: signedId
//});
uploadToS3(data, file, resolve, reject);
},
error: function(jqXHR, textStatus, errorThrown) {
console.log("Error uploading s3 blob!");
console.log(textStatus, errorThrown);
}
});
});
uploadPromises.push(uploadPromise);
}
// Wait for all the images to be uploaded before re-enabling the submit button
// await Promise.all(uploadPromises);
// document.getElementById('submitButton').disabled = false; // Re-enable the submit button
await Promise.all(uploadPromises)
.then(() => {
console.log('All uploads complete!');
document.getElementById('submitButton').disabled = false; // Re-enable the submit button
$('#submitButton').removeClass('is-loading');
$('#imageUploadingLabel').hide();
$('#imagesDoneUploading').show();
})
.catch(error => {
console.error('Some uploads failed:', error);
document.getElementById('submitButton').disabled = false; // Re-enable the submit button
$('#submitButton').removeClass('is-loading');
$('#imageUploadingLabel').hide();
$('#imagesDoneUploading').show();
// Handle the error - maybe inform the user that some uploads failed.
});
}
}
function uploadToS3(presignedData, file, resolveCallback, failureCallback) {
$.ajax({
url: presignedData.url,
type: 'PUT',
contentType: presignedData.headers["Content-Type"],
headers: {
"Content-MD5": presignedData.headers["Content-MD5"]
},
processData: false, // Important, do not process data
data: file,
success: function() {
console.log("Successfully uploaded to S3");
$(`#${presignedData.key}`).removeClass("is-loading"); //done uploading.
resolveCallback();
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Error uploading to S3:", textStatus, errorThrown);
failureCallback();
}
});
}
// This is how you would call the function inside your success callback:
// uploadToS3(data, file);
function hashChunk(chunk) {
return new Promise((resolve, reject) => {
fileReader.onload = async(e) => {
const view = new Uint8Array(e.target.result);
hasher.update(view);
resolve();
};
fileReader.readAsArrayBuffer(chunk);
});
}
const readFile = async(file) => {
if (hasher) {
hasher.init();
} else {
hasher = await hashwasm.createMD5();
}
const chunkNumber = Math.floor(file.size / chunkSize);
for (let i = 0; i <= chunkNumber; i++) {
const chunk = file.slice(
chunkSize * i,
Math.min(chunkSize * (i + 1), file.size)
);
await hashChunk(chunk);
}
const hash = hasher.digest();
return Promise.resolve(hash);
};
function getExtenstionFromMimeType(mimeType) {
const mimeToExtensionMap = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
'image/tiff': '.tiff',
'image/bmp': '.bmp',
// Add other mappings as needed
};
return mimeToExtensionMap[mimeType] || ''; // return empty string if mimeType is not in the map
}
<% end %>
<% @body_class = 'layout-login-centered-boxed' %>
<%= content_for :footer_scripts do %>
$(function(){
var formPageIndex = 0;
let numFormPages = 2;
for (i = formPageIndex + 1; i <= numFormPages; i++){
hideFormPage(i);
}
function showFormPage(num){
$(`.form-page-${num}`).show();
}
function hideFormPage(num){
$(`.form-page-${num}`).hide();
}
function nextPage(){
hideFormPage(formPageIndex);
formPageIndex < numFormPages ? formPageIndex++ : formPageIndex;
showFormPage(formPageIndex);
}
function previousPage(){
hideFormPage(formPageIndex);
formPageIndex > 0 ? formPageIndex-- : formPageIndex;
showFormPage(formPageIndex);
}
showFormPage(formPageIndex);
$('#next-form-field').click(nextPage);
$('#back-form-field').click(previousPage);
$('#submitAccount').click(function(e){
//confirm password matches and they agree to terms & services
error = false;
let password_confirmed = $('#business_employees_attributes_0_password').val() == $('#password_2').val();
if (!password_confirmed){
error = true;
gritterError("Password didn't match!", "Make sure your confirmed password matches your given password");
}
if (!$('#terms').is(':checked')){
error = true;
gritterError("Terms not agreed", "You must agree to the terms and services to sign up");
}
if (error)
{
e.preventDefault(); //Don't submit if there was an error.
}
else {
$('.loader').show();
}
});
//If the user hits enter, but their not on the last page, don't submit the form, just move to the next form page.
$('.form-control').keypress(function(e){
let enterKeyWasPressed = e.keyCode === 13;
let userHasntNavigatedToLastPage = formPageIndex != numFormPages;
if(enterKeyWasPressed && userHasntNavigatedToLastPage){
e.preventDefault(); // Otherwise the form will be submitted
nextPage(); //Just put the user on the next page instead of submitting the form
return;
}
});
})
<% end %>
Email
Name
First Name
Last Name
<% @businesses.each do |business| %>
<%= business.email %>
<%= business.name %>
<%= business.employees&.first&.first_name %>
<%= business.employees&.first&.last_name %>
<% end %>
State of Utah
Rev. 133EF48
END USER LICENSE AGREEMENT
This End-User License Agreement (this “EULA”) is a legal agreement between you (“Licensee”) and Lightning LLC (“Licensor”), the author of Field Rocket, including all HTML files, XML files, Java files, graphics files, animation files, data files, technology, development tools, scripts and programs, both in object code and source code (the “Software”), the deliverables provided pursuant to this EULA, which may include associated media, printed materials, and “online” or electronic documentation.
By installing, copying, or otherwise using the Software, Licensee agrees to be bound by the terms and conditions set forth in this EULA. If Licensee does not agree to the terms and conditions set forth in this EULA, then Licensee may not download, install, or use the Software.
1. Grant of License
A) Scope of License. Subject to the terms of this EULA, Licensor hereby grants to Licensee a royalty-free, non-exclusive license to possess and to use a copy of the Software.
B) Installation and Use. Licensee may install and use an unlimited number of copies of the Software solely for Licensee's business use.
2. Description of Rights and Limitations
A) Limitations. Licensee and third parties may not reverse engineer, decompile, or disassemble the Software, except and only to the extent that such activity is expressly permitted by applicable law notwithstanding the limitation.
B) Update and Maintenance. Licensor shall provide updates and maintenance on the Software on an as needed basis.
C) Separation of Components. The Software is licensed as a single product. Its components may not be separated for use on more than one computer.
3. Title to Software. Licensor represents and warrants that it has the legal right to enter into and perform its obligations under this EULA, and that use by the Licensee of the Software, in accordance with the terms of this EULA, will not infringe upon the intellectual property rights of any third parties.
4. Intellectual Property. All now known or hereafter known tangible and intangible rights, title, interest, copyrights and moral rights in and to the Software, including but not limited to all images, photographs, animations, video, audio, music, text, data, computer code, algorithms, and information, are owned by Licensor. The Software is protected by all applicable copyright laws and international treaties.
5. No Support. Licensor has no obligation to provide support services for the Software.
6. Duration. This EULA is perpetual or until:
A) Automatically terminated or suspended if Licensee fails to comply with any of the terms and conditions set forth in this EULA; or
B) Terminated or suspended by Licensor, with or without cause.
In the event this EULA is terminated, you must cease use of the Software and destroy all copies of the Software.
7. Jurisdiction. This EULA shall be deemed to have been made in, and shall be construed pursuant to the laws of the State of Utah, without regard to conflicts of laws provisions thereof. Any legal action or proceeding relating to this EULA shall be brought exclusively in courts located in Salt Lake City, UT, and each party consents to the jurisdiction thereof. The prevailing party in any action to enforce this EULA shall be entitled to recover costs and expenses including, without limitation, attorneys’ fees. This EULA is made within the exclusive jurisdiction of the United States, and its jurisdiction shall supersede any other jurisdiction of either party’s election.
8. Non-Transferable. This EULA is not assignable or transferable by Licensee, and any attempt to do so would be void.
9. Severability. No failure to exercise, and no delay in exercising, on the part of either party, any privilege, any power or any rights hereunder will operate as a waiver thereof, nor will any single or partial exercise of any right or power hereunder preclude further exercise of any other right hereunder. If any provision of this EULA shall be adjudged by any court of competent jurisdiction to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that this EULA shall otherwise remain in full force and effect and enforceable.
10. warranty disclaimer. licensor, AND AUTHOR OF THE SOFTWARE, HEREBY EXPRESSLY DISCLAIM ANY WARRANTY FOR THE SOFTWARE. THE SOFTWARE AND ANY RELATED DOCUMENTATION IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. LICENSEE ACCEPTS ANY AND ALL RISK ARISING OUT OF USE OR PERFORMANCE OF THE SOFTWARE.
11. LIMITATION OF LIABILITY. LICENSOR SHALL NOT BE LIABLE TO LICENSEE, OR ANY OTHER PERSON OR ENTITY CLAIMING THROUGH LICENSEE ANY LOSS OF PROFITS, INCOME, SAVINGS, OR ANY OTHER CONSEQUENTIAL, INCIDENTAL, SPECIAL, PUNITIVE, DIRECT OR INDIRECT DAMAGE, WHETHER ARISING IN CONTRACT, TORT, WARRANTY, OR OTHERWISE. THESE LIMITATIONS SHALL APPLY REGARDLESS OF THE ESSENTIAL PURPOSE OF ANY LIMITED REMEDY. UNDER NO CIRCUMSTANCES SHALL LICENSOR’S AGGREGATE LIABILITY TO LICENSEE, OR ANY OTHER PERSON OR ENTITY CLAIMING THROUGH LICENSEE, EXCEED THE FINANCIAL AMOUNT ACTUALLY PAID BY LICENSEE TO LICENSOR FOR THE SOFTWARE.
12. Entire Agreement. This EULA constitutes the entire agreement between Licensor and Licensee and supersedes all prior understandings of Licensor and Licensee, including any prior representation, statement, condition, or warranty with respect to the subject matter of this EULA.
<% if @business.twilio_number.nil? || @business.twilio_number.empty? %>
Search by Area Code
Search for phone numbers based on the area code you enter here.
Phone Number
City
Zip
Purchase
Phone Call Tree Settings
This is where you'll define which phone numbers get forwarded to from your phone menu tree.. For example. if the user presses 1, then it will get forwarded to the first phone number entered here.
<%= content_for :footer_scripts do %>
$("#savePhoneTreeSettings").submit(function(e) {
e.preventDefault(); // avoid to execute the actual submit of the form.
var form = $(this);
var actionUrl = form.attr('action');
$.ajax({
type: "POST",
url: "/savePhoneCallTreeSettings",
data: form.serialize(), // serializes the form's elements.
success: function(data)
{
gritterSuccess("Success", "Just successfully set the phone call tree settings.")
},
error: function(data)
{
gritterError("Error", "Could not set the phone call tree settings.")
},
});
});
$('#findAvailableTwilioPhoneNumbersButton').on('click', function(e){
let areaCode = trimNonNumericValue($("#areaCodeToSearch").val());
getTwilioAvailablePhoneNumbers(areaCode, function(data){
console.log(data);
available_phone_numbers = data["numbers"];
gritterSuccess("Success", `Just successfully found ${available_phone_numbers.length} numbers matching the area code`)
for (let i = 0; i < available_phone_numbers.length; i++){
let phone_number = available_phone_numbers[i];
let zip = phone_number.zip != null ? phone_number.zip : "Not Listed"
console.log("Found Found Number:")
console.log(phone_number);
$('#availablePhoneNumbersTable > tbody').append(`<tr><td>${phone_number.number}</td><td>${phone_number.city}</td><td>${zip}</td><td> <button onclick="purchaseTwilioPhoneNumber('${phone_number.number}')"> Select </button> </td></tr>`);
}
});
});
function getTwilioAvailablePhoneNumbers(areaCode, successCallback){
$.get({
url: `/available_twilio_phone_numbers?area_code=${areaCode}`,
success(data){
successCallback(data);
},
error(data){
console.log(data);
}
});
}
function purchaseTwilioPhoneNumber(selectedPhoneNumber){
if (confirm(`Are you sure you want to select ${selectedPhoneNumber}?`) == true) {
$.post({
url: `/purchase_twilio_phone_number`, //TODO: finish URL route, controller, and unit test for purchasing a phone number using Twilio API.
data: {
number_to_purchase: selectedPhoneNumber,
},
success(data){
gritterSuccess("Successfully selected phone number", `Successfully selected ${selectedPhoneNumber}`);
},
error(data){
gritterError("Couldn't select phone number", `Couldn't select ${selectedPhoneNumber}. Contact support.`);
}
});
} else {
//Do nothing for now
}
}
<% end %>
You are now registered to receive customer payments through stripe!
<% if @import_customers %>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag 'reporting.js' %>
$(function(){
flatpickr('#marketing_sources_chart_start')
flatpickr('#marketing_sources_chart_end')
//Default the dates to the start and end of current month
SetFlatpickr('marketing_sources_chart_start', moment().startOf('month').format());
SetFlatpickr('marketing_sources_chart_end', moment().endOf('month').format());
$('#generate_marketing_report').on('click', function(){
let start = $('#marketing_sources_chart_start').val();
let end = $('#marketing_sources_chart_end').val();
drawMarketingPieChart(start, end);
drawIncomeChart(start, end);
drawIncomeByCategoryChart(start, end);
drawRevenueBySalesRepChart(start, end);
drawRevenueByMarketingSourceChart(start, end);
drawProductionRevenueByEmployee(start, end); //exclude if they're the sales rep
drawTimeClockedByEmployee(start, end); //exclude if they're the sales rep
});
$('#download_invoices').click(function(){//changes the href value before the click goes through
let start = $('#marketing_sources_chart_start').val();
let end = $('#marketing_sources_chart_end').val();
$('#download_invoices').attr('href', `/reports/invoices?start=${start}&end=${end}`)
$('#download_appointments').attr('href', `/reports/appointments?start=${start}&end=${end}`)
});
$('#download_appointments').click(function(){ //changes the href value before the click goes through
let start = $('#marketing_sources_chart_start').val();
let end = $('#marketing_sources_chart_end').val();
$('#download_appointments').attr('href', `/reports/appointments?start=${start}&end=${end}`)
});
$('#download_marketing').click(function(){ //changes the href value before the click goes through
let start = $('#marketing_sources_chart_start').val();
let end = $('#marketing_sources_chart_end').val();
$('#download_marketing').attr('href', `/reports/marketing?start=${start}&end=${end}`)
});
});
<% end %>
<% content_for :card_body do %>
Get Help
Something not working right? Please fill out your support request with your phone number and email, and we'll get back in touch as soon as possible!
<%= content_for :footer_scripts do %>
$('#submitTicket').click(function(e){
$('#support_message').val($('#support-ticket-message')[0].children[0].innerText);
if (!isValidEmail($('#email').val())){
gritterError("Email Invalid", "You must put a valid email in the email address")
e.preventDefault();
}
else {
$('.loader').show();
}
});
<% end %>
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); /* Adjust the minmax values to change the image sizes */
grid-gap: 15px; /* Adjust the spacing between images */
padding: 15px;
}
.gallery img {
width: 100%;
height: auto;
border: 1px solid #ccc; /* A light grey border around each image */
box-shadow: 2px 2px 6px 0px rgba(0,0,0,0.2); /* A subtle shadow for depth */
border-radius: 4px; /* Slightly rounded corners for a softer look */
object-fit: cover; /* This will cover the area of the grid item, cropping the image if necessary */
}
<%#= will_paginate @grouped_attachments %>
<% if @home_page.use_raw_html %>
<%= raw @home_page.raw_html %>
<% else %>
<% if @home_page.business.logo.attached? %>
<%= favicon_link_tag url_for(@home_page.business.logo), :rel => 'icon', :type=> 'image/png' if @home_page.business.logo.attached?%>
<% end %>
<%=@home_page.business.name%> | Website
.logo {
max-width: 50px;
height: auto;
}
<%= raw @home_page.navbar_html %>
<%= raw @home_page.raw_html %>
<%# render 'form', job_request: @job_request %>
<%= raw @home_page.footer_html %>
<%# render :partial => "public_website/footer_content" %>
<%# javascript_include_tag '3rdparty/flickity.pkgd.min.js' %>
<%= content_for :footer_scripts do %>
document.addEventListener('DOMContentLoaded', function() {
var menuButton = document.getElementById('menu-button');
var mobileMenu = document.getElementById('mobile-menu');
menuButton.addEventListener('click', function() {
mobileMenu.classList.toggle('hidden');
});
// Initialize AOS
AOS.init({
duration: 1000,
once: true,
offset: 100
});
});
<% end %>
document.addEventListener('DOMContentLoaded', function() {
var menuButton = document.getElementById('menu-button');
var mobileMenu = document.getElementById('mobile-menu');
menuButton.addEventListener('click', function() {
mobileMenu.classList.toggle('hidden');
});
// Initialize AOS
AOS.init({
duration: 1000,
once: true,
offset: 100
});
})
<% end %>
<% if can? :read, Roster %>
<% if current_employee.is_owner? %>
<%= react_component 'business_images/BusinessImagesToAssign' %>
<% if @business&.stripe_access_code == nil%>
&scope=read_write&state=123456789"> Click here to register your business to accept payments from customers
<% else %>
<% if !current_business.authenticated_with_quickbooks?%>
Connect Again?
Welcome to the onboarding page!
<%# content_for :header do %>
<%# stylesheet_link_tag 'get_started' %>
<%# end %>
<%= react_component 'register/GetStarted' %>
<%= content_for :footer do %>
gtag('event', 'conversion', {'send_to': 'AW-634701749/FFwbCLf7ns8YELWP064C'});
<% end %>
Name
Connected?
Connect Now
<%# Quickbooks %>
Quickbooks
<%= current_business.authenticated_with_quickbooks? ? "Connected" : "Not Connected"%>
Connect Now
<%# Gmail %>
Gmail
<%= current_employee.gmail_token.nil? ? 'Not Connected' : "Connected" %>
<%= react_component 'invoices/EmailInvoiceIcon', {invoiceId: current_business.invoices.first.id } %>
In order for you to send invoices, informational messages, and marketing emails to your customers, we need access to your gmail account.
This is to provide you with the best service possible. Generally, we've found that customers like to see invoices come directly from your businesses's email account,
and not from a random email account like: "LightningService@gmail.com" since they don't know what Lightning Service is or does.
When a customer receives an email directly from your own business account, they know exactly who the email is coming from.
To do that, however, we need permission to draft and send emails from your businesses account. With the permissions we request from Google, we don't any access to read or view any emails that you have in your inbox, we can only send them out.
To always see what is being sent from your gmail account, you can view the "send" tab when logged into gmail. From there, you can see all invoices, informational, and marketing emails that we've sent to customers on your behalf.
<%= render "form", llama_bot: @llama_bot %>
<%= link_to 'Back to llama bots', llama_bots_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= notice %>
<%= render @llama_bot %>
<%= link_to 'Edit this llama_bot', edit_llama_bot_path(@llama_bot), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%#= File.read(path) %>
<%#= simple_format(File.read(path, encoding: 'UTF-8', invalid: :replace, undef: :replace, replace: '').gsub("\t", ' ').gsub(' ', ' ')) %>
<%
#require 'charlock_holmes'
require 'rouge'
begin
file_content = File.read(path)
detection = CharlockHolmes::EncodingDetector.detect(file_content)
utf8_content = CharlockHolmes::Converter.convert(file_content, detection[:encoding], 'UTF-8')
rescue => e
utf8_content = "Error reading file: #{e.message}"
end
# TODO: Syntax highlighting for LLamaBot
#formatter = Rouge::Formatters::HTML.new
#lexer = Rouge::Lexer.guess_by_filename(path)
#highlighted_content = formatter.format(lexer.lex(utf8_content))
%>
bhexteriordesign
body {
font-family: 'Open Sans', sans-serif;
}
.container {
max-width: 1230px;
padding-left: 15px;
padding-right: 15px;
margin-left: auto;
margin-right: auto;
}
.bgNavyBlue {
background-color: #001f3f;
}
.bgHoverNavyBlue:hover {
background-color: #001f3f;
}
.textNavyBlue {
color: #001f3f;
}
.textPrimary {
color: #F7C244;
}
.textTitle {
color: #001f3f;
}
.hoverColorNavyBlue:hover {
color: #001f3f;
}
.hoverBg:hover {
background-color: #F7C244 !important;
}
.hoverColor:hover {
color: #F7C244 !important;
}
.bgPrimary {
background-color: #F7C244;
}
h1,
h2,
h3,
h4 {
font-family: 'Manrope';
}
Has time aged your home's exterior? Cracked stucco, faded siding, and drafty windows can steal
your
curb appeal and energy efficiency. BH Exterior and Design is your trusted partner for
transformative
exterior remodels that beautify Utah homes.
We're not your typical contractors, we're exterior
design specialists who craft stunning, modern spaces tailored to your vision and budget.
From Roof to Rock: Your One-Stop Shop for Utah's Most Beautiful Home Exterior Remodels.
Ditch the leaks and embrace weatherproof beauty with our diverse range of
styles and budget-friendly options.
Say goodbye to peeling vinyl and hello to
the modern, low-maintenance
appeal of Hardie® siding. Durable, stunning, and Utah-weather-ready.
We're the stucco experts! Breathe new life
into tired facades with expert
repairs or a complete refresh using our advanced, Utah-climate-defying blend.
Let the light and energy savings flood in
with top-quality,
energy-efficient window replacements. We have styles for every taste and budget.
Protect your foundation and landscaping from
Utah's unpredictable weather
with seamless gutter installation and repairs.
Rock & Brick: Update your stone or brick
facade for a timeless, elegant
look. We specialize in repairs, repointing, and full replacements for a cohesive, lasting
finish
Expand your living space and create an
outdoor oasis with our expert porch
and deck construction services. Imagine summer evenings on your new Utah retreat!
Breathtaking Results: Why BH is Your Exterior Remodel Partner.
We understand the unique challenges and opportunities of Utah's climate and building codes.
Your
project will be built to withstand anything Mother Nature throws its way.
We've transformed countless Utah homes, and our stunning portfolio is proof of our
expertise.
See projects similar to yours!
We work with your budget to create the exterior of your dreams, offering flexible financing
options and value-driven solutions.
We keep you in the loop every step of the way, from initial consultation to final
inspection. No
surprises, just smooth sailing.
We handle everything, from design and permits to construction and clean-up. You just relax
and
envision your new Utah haven.
Below, you will find several illustrative examples demonstrating the remarkable transformations
our
skilled contractors and thoughtful design approaches have brought to various home exteriors,
enhancing
both their aesthetic appeal and functional utility.
Yes, we specialize in exterior design remodels.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse porro autem qui hic quia
delectus enim fugit sapiente in laborum ipsam quaerat maxime sit veritatis incidunt,
ratione, nemo voluptate harum!
Our services are charged based on the scope of the project.
Yes, we have a dedicated team of in-house employees.
Absolutely, we provide comprehensive design services.
We offer a wide range of exterior services including painting, siding, and more.
Yes, we can paint over existing stucco depending on its
condition. Yes, we can paint over existing stucco depending on its
condition.Yes, we can paint over existing stucco depending on its
condition.
Witness the dramatic transformations of our exterior renovations and see
how we turn dated exteriors into stunning, modern masterpieces.
Explore some of our recent
jobs.
BH Exterior & Design Free Estimate A Look at Our Skim
Coat,
Fiber
Cement Siding Exterior Home Renovation A...
BH Exterior & Design Free Estimate Transforming Homes in Lindon, UT: A
Journey with BH Exterior & Design From Stucco...
BH Exterior & Design Free Estimate Salt Lake City
Homes: BH
Exterior &
Design's Chimney Repair, Waterproofing, and Brick Installation...
BH Exterior & Design Free Estimate Salt Lake City
Homes: BH
Exterior &
Design's Chimney Repair, Waterproofing, and Brick Installation...
BH Exterior & Design Free Estimate Salt Lake City
Homes: BH
Exterior &
Design's Chimney Repair, Waterproofing, and Brick Installation...
BH Exterior & Design Free Estimate Salt Lake City
Homes: BH
Exterior &
Design's Chimney Repair, Waterproofing, and Brick Installation...
Please fill out the form below and let us know how we
can
help. We'll respond the same day.
Location
615 W 9400 S #110, Sandy, UT 84070
Serving Northern Utah from Salt Lake City to Santaquin.
Boxer Homes
BH Exterior & Design is a division of Boxer Homes.
$(document).ready(function () {
$(".owl-carousel").owlCarousel({
loop: true,
margin: 10,
nav: true,
dots: true,
autoplay: true,
autoplayTimeout: 3000,
autoplayHoverPause: true,
responsive: {
0: {
items: 1
},
600: {
items: 2
},
1000: {
items: 3
}
}
});
});
function toggleAccordion(id) {
const accordion = document.getElementById(`accordion${id}`);
accordion.style.maxHeight = accordion.style.maxHeight ? null : `${accordion.scrollHeight}px`;
}
<%= render "form", llama_bot: @llama_bot %>
<%= link_to "Show this llama bot", @llama_bot, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to llama bots", llama_bots_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
json.array! @llama_bots, partial: "llama_bots/llama_bot", as: :llama_bot
YC - Welcome!
You're in a sandboxed enviroment. (No scaffolding, sorry). But, you can still use llamabot to redesign any of the following pages:
<%= notice %>
<%= web_page.business.name %>
<%= web_page.slug %>
<%= link_to 'New Web Page', new_web_page_path, class: "mt-6 inline-block bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
Costs to develop software are going to 0. This is because of Open Source and LLMs. We're going to beat everyone there by combining an open source CRM/CMS with LLMs.
Deep Network Effects when people build plugins on top of your open source ecosystem (like WordPress).
Ruby on Rails is unequivocally the most superior framework for LLM code generation, bar none. Its design philosophy and features make it the perfect match for AI-driven development:
Rails' "convention over configuration" principle and Ruby's expressive syntax results in dramatically reduced code verbosity. Fewer tokens allows the LLM to "see" more of the code or context at once.
Ruby's natural language-like syntax aligns perfectly with LLMs' language understanding capabilities.
Rails' scaffolding and generator tools are tailor-made for quick iterations - a crucial advantage when working with LLMs.
Rails' opinionated nature embeds industry best practices into its core. This frees up the LLM to focus on higher level abstractions, rather than getting bogged down in non-standard implementations
The vast Ruby gems ecosystem provides a rich set of pre-built solutions.
ActiveRecord's powerful ORM capabilities allow LLMs to generate database interactions without getting bogged down in SQL specifics.
Rails' integrated testing framework allows LLMs to simultaneously generate application code and corresponding tests.
Rails' built-in support for RESTful design principles enables LLMs to generate well-structured, scalable APIs effortlessly.
Rails' full-stack nature allows LLMs to generate cohesive, end-to-end solutions within a single framework.
The extensive Rails community and documentation serve as a rich knowledge base for LLMs.
Ruby's powerful metaprogramming capabilities enable LLMs to generate highly dynamic and adaptable code.
Ruby on Rails isn't just a good choice for LLM code generation - it's the optimal choice. Its concise syntax, semantic clarity, and robust ecosystem create a synergy with AI that no other framework can match. As we move further into the age of AI-assisted development, Ruby on Rails stands out as the framework that will unlock the full potential of LLM-driven coding, revolutionizing the way we build and scale applications. Any other choice is simply settling for less.
We cater to less-technical users who need help building customizable software for cheap:
Our Expert AI Agent, LlamaBot, that is Trained to be Your Expert CTO. LlamaBot is a Highly Opinionated Ruby on Rails and LlamaPress Expert. LlamaBot guides you through any LLamaPress project, and even acts as your very own custom software developer.
Even the most non-technical users can still build their own customized software solution on top of LlamaPress, using LlamaBot.
LlamaPress is optimized for end-to-end user experience — from the Least Technical User to the Most Technical.
Why Field Rocket? | Field Rocket
Our app offers an array of fantastic free features. Only pay if you require multiple users or need advanced features. Learn more about our free offering.
Get started with Field Rocket for free, and upgrade when you need to. If you're not satisfied with one of our paid options, take us up on our money-back guarantee. Discover our free websites for contractors.
If you have any questions or special requests feel free to contact us. Email us or call at (385) 340-3738. Learn about gathering online reviews.
// <![CDATA[ <-- For SVG support
if ('WebSocket' in window) {
(function () {
function refreshCSS() {
var sheets = [].slice.call(document.getElementsByTagName("link"));
var head = document.getElementsByTagName("head")[0];
for (var i = 0; i < sheets.length; ++i) {
var elem = sheets[i];
var parent = elem.parentElement || head;
parent.removeChild(elem);
var rel = elem.rel;
if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
}
parent.appendChild(elem);
}
}
var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
var address = protocol + window.location.host + window.location.pathname + '/ws';
var socket = new WebSocket(address);
socket.onmessage = function (msg) {
if (msg.data == 'reload') window.location.reload();
else if (msg.data == 'refreshcss') refreshCSS();
};
if (sessionStorage && !sessionStorage.getItem('IsThisFirstTime_Log_From_LiveServer')) {
console.log('Live reload enabled.');
sessionStorage.setItem('IsThisFirstTime_Log_From_LiveServer', true);
}
})();
}
else {
console.error('Upgrade your browser. This Browser is NOT supported WebSocket for Live-Reloading.');
}
// ]]>
2-tile - Beautiful Tiling & Remodeling
tailwind.config = {
theme: {
extend: {
colors: {
'custom-blue': '#2596be',
}
}
}
}
window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'AW-16612261287');
Unlock The Beauty In Your Space. Your Home, Reimagined.
At 2-Tile, we believe that every homeowner has the potential to transform their space into a masterpiece. We don't just lay tiles; we transform your home into a masterpiece of design and comfort. Specializing in bathroom remodels and beyond, we promise not just a service, but a complete metamorphosis of your living spaces.
Transform your everyday routine into an indulgent experience with bespoke tiling that reflects your unique taste.
Elevate the heart of your home with stunning tile work that combines functionality with flair.
We handle every part of your project, from floors to backsplashes, let's reimagine your space with tiles that tell your story.
Shower Remodel
Custom Bathroom Remodel
Full Bathroom Remodel and Tiling
We're On Our Way. Or, do you just have a question?
This company is legendary. 5 stars. Showed up on time every day and got job done ahead time and under budget. Would recommend to any of my friends or family.
Raymond
Alex went above and beyond the call of duty. He was utmost professional, knowledgeable, and efficient. And is good at what he does. He was prompt on answering calls. Alex was on the jobsite the same day. With reasonable prices. With exceptional business owners like Alex. 5 stars.
Jacob York
Alex did an excellent job for a fair price. He was very professional when coming to meet us and obtain the information and measurements needed for a bid. He helped make sure we chose the right tile and suggested solutions that helped keep the cost of the job down while maintaining quality. When the job was done there were no surprise expenses, the bid was perfectly accurate. As stated in previous reviews he has a wide range of well honed skills. We would highly reccomend him to anyone and we will be doing so to our friends and family.
Jace St. Pierre
© 2024 2-tile. All rights reserved.
// Initialize AOS
AOS.init({
duration: 800,
once: true
});
document.getElementById('menu-toggle').addEventListener('click', function() {
document.getElementById('mobile-menu').classList.toggle('hidden');
});
<%
main_tables = %w[businesses customers invoices employees employee_jobs messages web_pages]
@database_tables = main_tables
@foreign_keys = @database_tables.flat_map do |table|
ActiveRecord::Base.connection.foreign_keys(table)
end
%>
const canvas = document.getElementById('schema-graph');
const ctx = canvas.getContext('2d');
// Set canvas size dynamically
canvas.width = Math.max(800, window.innerWidth - 100);
canvas.height = Math.max(600, window.innerHeight - 200);
// Define tables and relationships
const tables = <%= @database_tables.to_json.html_safe %>;
const relationships = <%= @foreign_keys.map { |fk| { from: fk.from_table, to: fk.to_table, column: fk.options[:column] } }.to_json.html_safe %>;
// Calculate positions
const tablePositions = {};
const padding = 40;
const tableWidth = 150;
const tableHeight = 60;
const columns = Math.ceil(Math.sqrt(tables.length));
const rows = Math.ceil(tables.length / columns);
tables.forEach((table, index) => {
const col = index % columns;
const row = Math.floor(index / columns);
tablePositions[table] = {
x: padding + col * (canvas.width / columns),
y: padding + row * (canvas.height / rows)
};
});
// Draw relationships first
ctx.beginPath();
relationships.forEach(rel => {
const from = tablePositions[rel.from];
const to = tablePositions[rel.to];
if (from && to) {
ctx.moveTo(from.x + tableWidth / 2, from.y + tableHeight / 2);
ctx.lineTo(to.x + tableWidth / 2, to.y + tableHeight / 2);
}
});
ctx.stroke();
// Draw arrowheads and labels
relationships.forEach(rel => {
const from = tablePositions[rel.from];
const to = tablePositions[rel.to];
if (from && to) {
// Draw arrowhead
const angle = Math.atan2(to.y - from.y, to.x - from.x);
ctx.save();
ctx.translate(to.x + tableWidth / 2, to.y + tableHeight / 2);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-10, -5);
ctx.lineTo(-10, 5);
ctx.closePath();
ctx.fill();
ctx.restore();
// Draw label
const labelX = (from.x + to.x) / 2 + tableWidth / 2;
const labelY = (from.y + to.y) / 2 + tableHeight / 2;
ctx.fillText(rel.column, labelX, labelY);
}
});
// Draw tables last
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
Object.entries(tablePositions).forEach(([table, position]) => {
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(position.x, position.y, tableWidth, tableHeight);
ctx.strokeRect(position.x, position.y, tableWidth, tableHeight);
ctx.fillStyle = '#000';
ctx.fillText(table, position.x + tableWidth / 2, position.y + tableHeight / 2);
});
// Add this to log the relationships for debugging
console.log('Relationships:', relationships);
json.partial! "llama_bots/llama_bot", llama_bot: @llama_bot
Source Code
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
margin: 0;
padding: 20px;
}
h1 {
text-align: center;
color: #4f4f4f;
}
.button {
display: inline-block;
padding: 10px 20px;
margin: 20px 0;
background-color: #f2ad56;
color: white;
text-decoration: none;
border-radius: 5px;
text-align: center;
}
.button:hover {
background-color: #5b3300;
}
ul {
list-style-type: none;
padding: 0;
}
.directory, .file {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 5px;
border-radius: 5px;
background-color: white;
}
.directory {
font-weight: bold;
background-color: #e7f3e7;
}
.file {
cursor: pointer;
}
.file-content {
display: none;
margin-left: 20px;
padding: 10px;
border-left: 2px solid #ddd;
background-color: #f9f9f9;
}
function toggleFileContent(id) {
var content = document.getElementById(id);
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
<%= notice %>
<%= form_with(model: llama_bot, class: "contents") do |form| %>
<% if llama_bot.errors.any? %>
json.extract! llama_bot, :id, :created_at, :updated_at
json.url llama_bot_url(llama_bot, format: :json)
$(function(){
<% if !allow_newlines %>
$("#<%=inplace_id%>").keypress(function(e){ return e.which != 13; });
<% end %>
$("#<%=inplace_id%>").on("focusout", function(evt){
attrInfo = evt.currentTarget.dataset;
const url = `/${attrInfo.resource}/${attrInfo.id}/update_attribute.json`;
$.ajax({
type: 'PUT',
url: url,
data: {attribute: attrInfo.attribute, value: evt.currentTarget.innerText},
success: function(data){
const successMessage = `Successfully set ${attrInfo.attribute} to ${evt.currentTarget.innerText}`;
console.log(successMessage);
gritterSuccess("Successful Update", successMessage);
},
error: function(data){
const failureMessage = `Failed to set to ${evt.currentTarget.innerText}`;
console.log(failureMessage);
gritterError("Failed Update", failureMessage);
},
})
});
})
<%= content_for :modal_header, :flush => true do %>
<%= content_for :modal_body, :flush => true do %>
<%= content_for :modal_footer, :flush => true do %>
Cancel
<% end %>
<%= content_for :footer_scripts do %>
function imageClickHandler(event) {
const imageUrl = event.target.src;
const id = event.target.dataset.id;
//Setting the input tags for the image urls
$('#image_avatar').val('')
$('#image_avatar_url_id').val(id);
//Displaying selected image
$('#attached-image').attr('src',imageUrl);
$('#attached-image-container').show();
$('#image_upload').hide();
}
function selectButtonGrp(id) {
$(`#btn-grp-${id}`).css('border-bottom', '1px solid gray');
$(`#btn-grp-${id ? 0 : 1}`).css('border-bottom', 'none');
$(`#btn-menu-${id}`).css('display', 'flex');
$(`#btn-menu-${id ? 0 : 1}`).css('display', 'none');
}
$(document).ready(function(){
$("#image_upload .close").click(function(){
$("#image_upload").hide();
});
})
<%end%>
<%= render partial: "shared/generic_modal", locals: {modal_id: "image_upload"} %>
<% inplace_id = "inplace-#{resource}-#{attribute}-#{resource_id}"%>
<% allow_newlines = defined?(allow_newlines)? allow_newlines : nil %>
" data-attribute="<%=attribute%>" contenteditable="true" data-resource="<%=resource%>" data-id="<%=resource_id%>"><%=value%>
<%= content_for :footer_scripts do %>
<%= render partial: 'shared/inplace_edit.js.erb', locals: {inplace_id: inplace_id, allow_newlines: allow_newlines}%>
<% end %>
<% customer = defined?(customer) ? customer : nil %>
<%= j.location&.display_address %>
job images
%>job request images
%>customer images
%> <% customer.invoices.each do |invoice| %>
<% invoice.images.includes(:blob).each do |img| %>
<%#
invoice images
%><%= content_for :footer_scripts do %>
<%# TODO: Figure out how to move all these stylesheet_link_tag's into fullcalendar/activate %>
<%# stylesheet_link_tag "fullcalendar/activate" %>
<%= stylesheet_link_tag "fullcalendar/core/main.css" %>
<%= stylesheet_link_tag "fullcalendar/daygrid/main.css" %>
<%= stylesheet_link_tag "fullcalendar/timegrid/main.css" %>
<%= stylesheet_link_tag "fullcalendar/premium/timeline/main.css" %>
<%= stylesheet_link_tag "fullcalendar/premium/resource-timeline/main.css" %>
<%= javascript_include_tag "fullcalendar/activate.js"%> <%# https://stackoverflow.com/a/9453470/6374022 %>
<%# Main rendering logic is in this script: %>
<%= javascript_include_tag "fullcalendar_helpers/activate.js" %> <%# https://stackoverflow.com/a/9453470/6374022 %>
$(document).ready(function () {
showEditableCalendar();
<% if current_employee.branch.nil?%>
<% current_employee.business.branches.each do |branch|%>
setFullCalendarBranchResource("<%=branch.name%>"); //<%# We want to show all branches if an employee isn't assigned a branch %>
<% end %>
<% else %>
setFullCalendarBranchResource("<%=current_employee&.branch&.name%>"); //<%# We want the employee to only see his branch as default%>
<% end %>
});
<%= javascript_include_tag '3rdparty/mobile-jquery-draggable.js' %>
<% end %>
<%= form_tag do %>
Sunday
<%= content_for :footer_scripts do %>
<%= javascript_include_tag 'job-scheduling-ui.js' %>
<%= javascript_include_tag 'job-customer-search-autofill.js' %>
<% end %>
<% unless resource.total_pages == 1 %>
<% id = defined?(id) ? id : "" %>
<% placeholder = defined?(placeholder) ? placeholder : nil %>
<% content = defined?(content) ? content : nil %>
<%= content_for :footer_scripts do %>
var quill = new Quill('#<%=id%>', {
<% if placeholder %>
placeholder: "<%= placeholder%>",
<% end %>
theme: 'snow'
});
<% if content %>
SetQuillHtml('<%=id%>', `<%=content%>`);
<% end %>
<% end %>
Login
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-133433427-1');
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '327167911228268');
fbq('track', 'PageView');
Login to access your Stack Account
document.addEventListener('DOMContentLoaded', () => {
const chatIcon = document.getElementById('chatIcon');
const chatbotContainer = document.getElementById('chatbotContainer');
const closeButton = document.getElementById('closeButton');
const chatbotMessages = document.getElementById('chatbotMessages');
const userInput = document.getElementById('userInput');
const sendButton = document.getElementById('sendButton');
const modal = document.getElementById('historyModal');
const closeModal = document.getElementById('closeModal');
const leftArrow = document.getElementById('leftArrow');
const rightArrow = document.getElementById('rightArrow');
closeModal.addEventListener('click', () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
});
leftArrow.addEventListener('click', () => {
// Implement left navigation logic
});
rightArrow.addEventListener('click', () => {
// Implement right navigation logic
});
// Example: Open modal on button click
// const openHistoryModalButton = document.getElementById('openHistoryModalButton');
// openHistoryModalButton.addEventListener('click', openHistoryModal);
let isOpen = false;
let isLoading = false;
let messages = [];
function toggleChat() {
isOpen = !isOpen;
chatbotContainer.classList.toggle('hidden', !isOpen);
}
function addMessage(text, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `mb-2 p-2 rounded ${sender === 'bot' ? 'bg-blue-100 self-start' : 'bg-green-100 self-end'}`;
messageDiv.textContent = text;
chatbotMessages.appendChild(messageDiv);
chatbotMessages.scrollTop = chatbotMessages.scrollHeight;
}
function showLoadingIndicator() {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'loadingIndicator';
loadingDiv.className = 'text-center p-2';
loadingDiv.textContent = 'Thinking...';
chatbotMessages.appendChild(loadingDiv);
}
function hideLoadingIndicator() {
const loadingDiv = document.getElementById('loadingIndicator');
if (loadingDiv) {
loadingDiv.remove();
}
}
async function sendMessage() {
if (userInput.value.trim() === '' || isLoading) return;
const userMessage = userInput.value;
addMessage(userMessage, 'user');
userInput.value = '';
isLoading = true;
showLoadingIndicator();
let selectedElement = document.getElementById("selectedElementContent").innerHTML;
try {
const response = await fetch('/chatbot/message.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: userMessage,
context: '<%=@view_path%>',
selectedElement: selectedElement,
webPageId: '<%=@web_page&.id%>'
})
});
const data = await response.json();
addMessage(data.response, 'bot');
} catch (error) {
console.error('Error sending message to ChatGPT:', error);
} finally {
isLoading = false;
hideLoadingIndicator();
}
}
chatIcon.addEventListener('click', toggleChat);
closeButton.addEventListener('click', toggleChat);
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
function loadChatHistory() {
let webPageId = '<%=@web_page&.id%>';
if (webPageId) {
//TODO: fetch Chat History from /web_page_histories IF this web_page_id is present in the URL
fetch(`/web_pages/${webPageId}/histories.json`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
web_page_histories = data.web_page_histories;
web_page_histories.forEach(history => {
addWebPageHistoryToHistoryDrawer(history);
//addMessage(history.text, history.sender);
});
});
}
}
/**
* Add entry to web page history to the history drawer.
* @param {object} webPageHistory - The web page history to add.
*/
function addWebPageHistoryToHistoryDrawer(webPageHistory) {
const historyDrawer = document.getElementById('historyDrawer');
const historyItem = document.createElement('div');
historyItem.className = 'history-item cursor-pointer hover:bg-gray-100 p-2';
//historyItem.textContent = `${webPageHistory.user_message} - ${new Date(webPageHistory.created_at).toLocaleString('en-GB', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', hour12: false })}`;
//historyItem.textContent = `${webPageHistory.user_message} - ${new Date(webPageHistory.created_at).toLocaleString('en-GB', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit', hour12: false })}`;
historyItem.textContent = new Date(webPageHistory.created_at).toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', month: '2-digit', day: '2-digit', hour12: false });
historyItem.onclick = function() {
const modal = document.getElementById('historyModal'); // Get modal element
const mainIframe = document.getElementById('mainIframe');
mainIframe.innerHTML = atob(webPageHistory.content);
document.getElementById('userPrompt').innerHTML = webPageHistory.user_message;
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('restoreButton').setAttribute('llama-data-web-page-history-id', webPageHistory.id);
};
historyDrawer.appendChild(historyItem);
}
document.getElementById('restoreButton').addEventListener('click', function() {
console.log('restoreButton clicked');
// we need to do a POST with the webPageHistory.id to /web_pages/restore
let webPageId = '<%=@web_page&.id%>';
//QUESTION: how do I get this webpagehistory id?
// we save it to the restoreButton so that when the user clicks on the history item.. we have the ID.
const webPageHistoryId = document.getElementById('restoreButton').getAttribute('llama-data-web-page-history-id');
fetch(`/web_pages/${webPageId}/restore.json?web_page_history_id=${webPageHistoryId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
web_page_history_id: webPageHistoryId
})
}).then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Couldn\'t restore');
}
}).then(data => {
alert('Web page restored');
console.log('Web page restored:', data);
location.reload();
});
});
/**
* Return Summary of Prompt so user understand chat history better.
* @param {string} prompt - The original prompt from the user.
* @returns {string} - A summary of the prompt.
*/
function summarizePrompt(prompt){
// Split the text into sentences
const sentences = prompt.split('.');
// Take the first two sentences
const firstTwo = sentences.slice(0, 2);
// Join them back together and trim whitespace
let summary = firstTwo.join('.').trim() + '.';
// Truncate to a maximum of 50 words
const words = summary.split(/\s+/);
if (words.length > 50) {
summary = words.slice(0, 50).join(' ') + '...';
}
return summary;
}
// Initial bot message
loadChatHistory();
addMessage('What do you want to build?', 'bot');
});
document.addEventListener('DOMContentLoaded', () => {
const menuToggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
menuToggle.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
});
let inSelectionMode = false;
let selectedElementHTML = null;
function toggleSelectionMode() {
inSelectionMode = !inSelectionMode;
setExpandedToFalse(); // decrease chatbot size so that user can see more of the elements on the page.
document.body.classList.toggle('selection-mode');
console.log('Selection mode:', inSelectionMode ? 'ON' : 'OFF');
}
function handleElementSelection(event) {
if (!inSelectionMode) return;
event.preventDefault();
event.stopPropagation();
const selectedElement = event.target;
console.log('Selected element:', selectedElement);
// Remove the selection class from any previously selected element
const previouslySelected = document.querySelector('.selected-element');
if (previouslySelected) {
previouslySelected.classList.remove('selected-element');
}
// Add selection class to the newly selected element
selectedElement.classList.add('selected-element');
// Here you can do whatever you want with the selected element
window.lastSelectedElement = selectedElement;
// Trigger a custom event
const customEvent = new CustomEvent('elementSelected', { detail: selectedElement });
document.dispatchEvent(customEvent);
// Turn off selection mode after selecting an element
toggleSelectionMode();
}
// Add necessary event listeners
document.addEventListener('click', handleElementSelection, true);
// Add some basic styles if they haven't been added yet
if (!document.getElementById('selection-mode-styles')) {
const style = document.createElement('style');
style.id = 'selection-mode-styles';
style.textContent = `
.selection-mode * {
cursor: pointer !important;
}
.selection-mode *:hover {
outline: 2px solid red !important;
}
.selected-element {
position: relative;
}
.selected-element::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 4px solid #3B82F6;
background-color: rgba(59, 130, 246, 0.1);
pointer-events: none;
z-index: 9999;
}
`;
document.head.appendChild(style);
}
// Listen for custom elementSelected event
document.addEventListener('elementSelected', (e) => {
selectedElementHTML = e.detail;
let selectedElementContainer = document.getElementById('selectedElementContainer');
let selectedElementContent = document.getElementById('selectedElementContent');
if (selectedElementHTML != null && selectedElementContainer != null) {
selectedElementContainer.style.display = 'block';
selectedElementContent.innerHTML = selectedElementHTML.outerHTML;
setSelectedElementContainerBackground(selectedElementHTML);
}
console.log('Element selected event:', selectedElementHTML);
});
// Add this to the end of the script
document.getElementById('closeSelectedElement').addEventListener('click', function() {
console.log('Selected element closed');
document.getElementById('selectedElementContainer').style.display = 'none';
document.getElementById('selectedElementContent').innerHTML = '';
});
let isExpanded = false; // Change this to true
const chatbotContainer = document.getElementById('chatbotContainer');
const expandButton = document.getElementById('expandButton');
// Add this function to set the expanded size
function setExpandedSize() {
isExpanded = true;
chatbotContainer.style.width = '800px';
chatbotContainer.style.height = '800px';
}
function setExpandedToFalse() {
isExpanded = false;
chatbotContainer.style.width = '480px';
chatbotContainer.style.height = '576px';
}
// Call this function immediately after the page loads
expandButton.addEventListener('click', function() {
isExpanded = !isExpanded;
if (isExpanded) {
setExpandedSize();
} else {
chatbotContainer.style.width = '480px';
chatbotContainer.style.height = '576px';
}
});
// Add this function to your existing JavaScript // this is for readability of the selected snippet
function setSelectedElementContainerBackground(selectedElement) {
const container = document.getElementById('selectedElementContainer');
const content = document.getElementById('selectedElementContent');
// Get the background color of the parent element
let bgColor = window.getComputedStyle(selectedElement.parentElement).backgroundColor;
// If the background is transparent, try to find the nearest parent with a background
let parent = selectedElement.parentElement;
while (bgColor === 'rgba(0, 0, 0, 0)' && parent.parentElement) {
parent = parent.parentElement;
bgColor = window.getComputedStyle(parent).backgroundColor;
}
// Set the background color of the container
container.style.backgroundColor = bgColor;
// Adjust text color for better contrast
const rgb = bgColor.match(/\d+/g);
if (rgb) {
const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;
content.style.color = brightness > 125 ? 'black' : 'white';
}
}
// Add this near the end of the script
document.addEventListener('DOMContentLoaded', () => {
const toggleImagesButton = document.getElementById('toggleImagesForUserToSelect');
const imagesContainer = document.getElementById('imagesForUserToSelect');
// Initially hide the images container
imagesContainer.style.display = 'none';
toggleImagesButton.addEventListener('click', () => {
if (imagesContainer.style.display === 'none') {
imagesContainer.style.display = 'block';
} else {
imagesContainer.style.display = 'none';
}
});
});
document.getElementById('historyButton').addEventListener('click', function() {
// Toggle the history drawer
const historyDrawer = document.getElementById('historyDrawer');
if (historyDrawer.classList.contains('hidden')) {
historyDrawer.classList.remove('hidden');
} else {
historyDrawer.classList.add('hidden');
}
});
document.getElementById('closeHistoryDrawer').addEventListener('click', function() {
document.getElementById('historyDrawer').classList.add('hidden');
});
""
— User's request
<%="#{img.blob.filename.to_s[0,4]}...#{img.blob.filename.to_s.split('.').last}"%>
<% elsif img.blob.filename.to_s.end_with?('.pdf') %>
" title="<%= img.blob.filename %>" align="top" width="100%" frameborder="0" scrolling="auto" target="Message">
<% else %>
<%= image_tag(img, class: 'w-100') %>
<% end %>
<%= content_for :footer_scripts do %>
<%= stylesheet_link_tag 'gallery-clean', media: 'all' %>
<%= stylesheet_link_tag 'baguetteBox', media: 'all' %>
<%= javascript_include_tag 'baguetteBox' %>
<% end %>
document.addEventListener("DOMContentLoaded", function(){
$("#card").mask( "0000 0000 0000 0000" );
$("#exp").mask( "00/00", { placeholder: "MM/YY" });
$("#cvv").mask( "000" );
$('.close.times_btn').click(() => {
$('#payment-modal-shadow').hide()
});
$(window).click(function(e){
const classValue = e.target.classList.value;
if(classValue.includes('modal fade') || classValue.includes('payment-modal-shadow')){
$('#payment-modal-shadow').hide();
}
})
});
function initiateCheckoutJS() {
$('#payment-modal-shadow').show()
$('#payment_modal').modal();
$("#card").focus();
$('body').css('overflow', 'auto');
// Fetching the session to token to proceed with the payment
console.log('Fetching token to initiate payment')
$.get({
"url": "<%= generate_payment_session_token_url(@invoice)%>?secret_key=<%= @invoice.secret_key%>",
"success": function(response) {
const { body:token } = response;
document.getElementById('token').value = token;
transactionToken = token;
},
"error": function(error) {
gritterError("Error", 'Something went wrong');
console.log("Error", "Not able to generate token");
console.log(error);
}
})
return false;
}
function pay() {
$('#payment_button').prop('disabled', true);
const token = document.getElementById('token').value;
const card = document.getElementById('card').value;
const exp = document.getElementById('exp').value;
const cvv = document.getElementById('cvv').value;
const gettoken = document.getElementById('gettoken').value;
const addtoken = document.getElementById('addtoken').value;
const firstname = "<%= invoice.customer_to_invoice&.first_name %>";
const lastname = "<%= invoice.customer_to_invoice&.last_name %>";
const address = "<%= invoice.customer_to_invoice&.default_location&.street_address %>";
const zip = "<%= invoice.customer_to_invoice&.default_location&.zip_code %>";
const business_name = "<%= invoice.business.name %> Payment for Home Services";
const merchanttxnid = document.getElementById('merchanttxnid').value;
const invoice_id = <%= @invoice.id %>;
const paymentData = {
ssl_txn_auth_token: token,
ssl_card_number: card,
ssl_exp_date: exp,
ssl_get_token: gettoken,
ssl_add_token: addtoken,
ssl_first_name: firstname,
ssl_last_name: lastname,
ssl_cvv2cvc2: cvv,
ssl_merchant_txn_id: merchanttxnid,
ssl_avs_address: address,
ssl_avs_zip: zip,
ssl_dynamic_dba: business_name
};
const callback = {
onError: function (error) {
gritterError("Error!", error);
$('#payment_button').prop('disabled', false);
},
onDeclined: function (response) {
gritterError("Declined!", JSON.stringify(response));
console.log("Result Message=" + response['ssl_result_message']);
$('#payment_button').prop('disabled', false);
},
onApproval: function (response) {
$("#payment_button").removeClass("btn-primary");
$("#payment_button").addClass("btn-success");
$("#payment_button").text("Paid");
console.log("Approval Code=" + response['ssl_approval_code']);
const message = 'Payment Complete.';
gritterSuccess("Success!", message);
$("form#edit_invoice_"+invoice_id).submit();
}
};
ConvergeEmbeddedPayment.pay(paymentData, callback);
return false;
}
<%# Input: Customer ID, Job ID. %>
<%# Input: Show all locations for the customer. Show the job location, and the billing location at the top. %>
<%= content_for :modal_header, :flush => true do %>
<%= content_for :modal_body, :flush => true do %>
Address
Notes
<% end %>
<%= render partial: "shared/generic_modal", locals: {modal_id: "manage_properties_modal"} %>
<%= content_for :footer_scripts do %>
function setPropertyManageModal(locations, job_location_id){ //Locations that aren't service address or billing address.
$('#manage_properties_table').html('');
$('#selected_locations_table').html('');
$.each(locations, function( index, value ) { //for each location, add the <li> item for the menu.
let valueIcon = '';
if (value.is_default_location){
valueIcon = valueIcon.concat('<i class="fas fa-house"></i>');
}
if (value.id == job_location_id || value.is_default_location) {
// Do Nothing. If it's service address, don't even show it on the table.
let icon = '<i class="fas fa-van-shuttle"></i>'
if (value.id == job_location_id && value.is_default_location){
icon = ('<i class="fas fa-house"></i>').concat(icon);
$('#selected_locations_table').append(`<tr data-is_default_location="${value.is_default_location}" data-id="${value.id}" id="addresses" >
<td> ${icon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${value.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${value.id} </td>
</tr>`);
} else if (value.is_default_location){
icon = ('<i class="fas fa-house"></i>');
$('#selected_locations_table').append(`<tr data-is_default_location="${value.is_default_location}" data-id="${value.id}" id="billing_address" >
<td> ${icon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${value.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${value.id} </td>
</tr>`);
} else{
$('#selected_locations_table').append(`<tr data-is_default_location="${value.is_default_location}" data-id="${value.id}" id="default_address" >
<td> ${icon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${value.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${value.id} </td>
</tr>`);
}//onclick = Set Location ID for the job.
}
else {
//onclick = Set Location ID for the job.
$('#manage_properties_table').append(`<tr id="${value.id}" >
<td> ${valueIcon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${value.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${value.id} </td>
</tr>`);
}
});
};
// (function($) {
// let houseIcons = $('.fa-house');
// for (i = 0; i<houseIcons.length; i++){
// new Tooltip(houseIcons[0], {
// title: 'The Billing Address for this Customer',
// placement: 'bottom',
// trigger: 'hover',
// container: 'body'
// });
// }
// let vanIcons = $('.fa-van-shuttle')
// for (i = 0; i<vanIcons.length; i++){
// new Tooltip(vanIcons[0], {
// title: 'The Service Address for this Job',
// placement: 'bottom',
// trigger: 'hover',
// container: 'body'
// });
// }
// }
// );
function toggleAddIcon() {
$('#new_address_icon i')[0]?.classList?.value?.includes('fa-plus')
? $('#new_address_icon').html('<i class="fas fa-minus fa-2x"></i>')
: $('#new_address_icon').html('<i class="fas fa-plus fa-2x"></i>')
}
function newAddress(){
toggleAddIcon()
$('#add_new_location_in_manager_modal').toggle();
if($('#add_new_location_in_manager_modal').attr("style") != ''){
$('#new_location_id, #new_location_address, #new_location_city, #new_location_zip, #new_location_state').val('');
}
}
function newLocationFormSubmit(){
const newLocationId = $('#new_location_id').val();
var locationMethod = 'POST';
var locationUrl = "/locations.json";
const location = {
location: {
street_address: $('#new_location_address').val(),
city: $('#new_location_city').val(),
zip_code: $('#new_location_zip').val(),
state: $('#new_location_state').val(),
customer_id: $('#job_location_attributes_customer_id').val()
}
};
if(newLocationId != ''){
locationMethod = 'PUT';
locationUrl = "/locations/" + newLocationId + ".json";
}
$.ajax({
method: locationMethod,
url: locationUrl,
data: location,
success: function(data){
let valueIcon = '';
if (data.is_default_location){
valueIcon = valueIcon.concat('<i class="fas fa-house"></i>');
}
if(newLocationId){
//Updating target address Row
const targetElementRow = $(`#${newLocationId}`)
if(targetElementRow?.length){
targetElementRow.children('td:eq(1)').children('a').text(data.display_address)
} else{
$('#selected_locations_table').find(`[data-id=${newLocationId}]`).children('td:eq(1)').children('a').text(data.display_address);
updateJobLocationParams(data);
}
//location-menu
const job_location_option = $('#location-menu').find(`[data-id=${data.id}]`);
job_location_option.attr('data-street_address',data.street_address);
job_location_option.attr('data-city',data.city);
job_location_option.attr('data-zip_code',data.zip_code);
job_location_option.attr('data-state',data.state);
job_location_option.attr('data-id',data.id);
job_location_option.attr('data-is_default_location',data.is_default_location);
var defaultIcon = ""
data.is_default_location
? defaultIcon = '<i class="fa fa-map-marker" aria-hidden="true"></i>'
: defaultIcon = ""
job_location_option.html(defaultIcon + data.display_address);
} else{
$('#manage_properties_table').append(`<tr id="${data.id}" >
<td> ${valueIcon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${data.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${data.id} </td>
</tr>`);
var defaultIcon = ""
data.is_default_location
? defaultIcon = '<i class="fa fa-map-marker" aria-hidden="true"></i>'
: defaultIcon = ""
$.ajax({
method: "GET",
url: `/customers/${data.customer_id}.json`,
success: function(data){
FCTool.setLocationMenu(data);
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
$('#add_new_location_in_manager_modal').toggle();
$('#new_address_icon').html('<i class="fas fa-plus fa-2x"></i>');
$('#new_location_id, #new_location_address, #new_location_city, #new_location_zip, #new_location_state').val('');
gritterSuccess("Success", "Address Saved Successfully");
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
function addressContextMenuLink(event){
$(".address-menu").css('display','none');
$(event).parents('tr').find('#address-menu').toggle();
}
function editLocation(event){
toggleAddIcon()
const currentRow = $(event).parents('tr');
currentRow.find('#address-menu').toggle();
const locationData = currentRow.find('#address-context-menu-link').text().trim().split(', ');
const id = currentRow.children('td')[2].innerText.trim();
$('#new_location_id').val(id);
$('#new_location_address').val(locationData[0]);
$('#new_location_city').val(locationData[1]);
$('#new_location_zip').val(locationData[3]);
$('#new_location_state').val(locationData[2]);
$('#add_new_location_in_manager_modal').css('display','block');
}
function makeServiceAddress(event){
$(event).parents('tr').find('#address-menu').toggle();
const parentTr_id = $(event).parents('tr').attr('id');
const addressRow = event.currentTarget
const id = $(event).parents('tr').children('td')[2].innerText.trim();
const job_id = $('#job_id').val();
$.ajax({
method: 'PUT',
url: `/jobs/${job_id}/update_attribute`,
data: {attribute: 'location_id', value: id},
success: function(data){
gritterSuccess("Success", "Successfully Set Service Address");
const currentDefaultRow = $('#default_address');
const currentDefaultAddress = currentDefaultRow.children('td:eq(1)').children('a').text().trim()
const currentDefaultId = currentDefaultRow.children('td:eq(2)').text().trim()
if(parentTr_id == 'billing_address'){
$('#manage_properties_table').append(`<tr id="${currentDefaultId}" >
<td></td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${currentDefaultAddress}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${currentDefaultId} </td>
</tr>`);
let icon = ('<i class="fas fa-house"></i> <i class="fas fa-van-shuttle"></i>')
$('#selected_locations_table').html('');
$('#selected_locations_table').append(`<tr data-is_default_location="${data.location.is_default_location}" data-id="${data.location.id}" id="addresses" >
<td> ${icon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${data.location.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${data.location.id} </td>
</tr>`);
} else if($('#addresses')?.length){
const addressesRow = $('#addresses');
const isDefaultLocation = addressesRow.attr('data-is_default_location');
const commonAddress = addressesRow.children('td:eq(1)').children('a').text().trim()
const addressesDefaultId = addressesRow.children('td:eq(2)').text().trim()
const targetElementRow = $(`#${data?.location?.id}`)
const targetElementAddress = targetElementRow.children('td:eq(1)').children('a').text().trim()
const targetElementId = targetElementRow.children('td:eq(2)').text().trim()
targetElementRow.remove();
$('#selected_locations_table').html('');
$('#selected_locations_table').append(`<tr data-is_default_location="${isDefaultLocation}" data-id="${addressesDefaultId}" id="billing_address" >
<td> <i class="fas fa-house"></i> </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${commonAddress}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${addressesDefaultId} </td>
</tr>
<tr data-is_default_location="${data.location.is_default_location}" data-id="${data.location.id}" id="default_address" >
<td><i class="fas fa-van-shuttle" aria-hidden="true"></i></td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${data.location.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${data.location.id} </td>
</tr>`);
} else{
const targetElementRow = $(`#${data?.location?.id}`)
const targetElementAddress = targetElementRow.children('td:eq(1)').children('a').text().trim()
const targetElementId = targetElementRow.children('td:eq(2)').text().trim()
//Updating target address Row
var addressIcon = "";
currentDefaultRow.attr('data-is_default_location') == 'true'
? addressIcon = '<i class="fas fa-house" aria-hidden="true"></i>'
: addressIcon = ''
targetElementRow.children('td:eq(0)').html(addressIcon)
targetElementRow.children('td:eq(1)').children('a').text(currentDefaultAddress)
targetElementRow.children('td:eq(2)').text(currentDefaultId)
targetElementRow.attr('id', currentDefaultId)
//Updating default address Row
var defaultIcon = "";
data.location.is_default_location
? defaultIcon = '<i class="fas fa-house" aria-hidden="true"></i> <i class="fas fa-van-shuttle" aria-hidden="true"></i>'
: defaultIcon = '<i class="fas fa-van-shuttle" aria-hidden="true"></i>'
currentDefaultRow.attr('data-is_default_location', data.location.is_default_location)
currentDefaultRow.children('td:eq(0)').html(defaultIcon)
currentDefaultRow.children('td:eq(1)').children('a').text(targetElementAddress)
currentDefaultRow.children('td:eq(2)').text(targetElementId)
}
//TODO: Update the actual location information in the job modal
updateJobLocationParams(data.location);
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
function updateJobLocationParams(data){
$("#job_location_attributes_id").val(data.id);
$("#job_location_attributes_street_address").val(data.street_address);
$("#job_location_attributes_city").val(data.city);
$("#job_location_attributes_zip_code").val(data.zip_code);
$("#job_location_attributes_state").val(data.state);
data.is_default_location
? document.getElementById('job_location_attributes_default_location').checked = true
: document.getElementById('job_location_attributes_default_location').checked = false
}
//$('#close-generic-modal-div').click(function(){
// console.log("The paragraph was clicked.");
//});
$( "#close-generic-modal-div" ).on( "click", function() {
console.log("The paragraph was clicked.");
});
$(document).click(function(e){
if (e.target.className != 'address-context-menu-link') {
$(".address-menu").css('display','none');
}
});
// When the user clicks anywhere outside of the modal, close it
//TODO: We also need to add it for the 'x' on the modal.
//window.onclick = function (event) {
//console.log('m here');
//$(".address-menu").css('display','none');
//if (event.target == document.getElementById("manage_properties_modal")) {
// $('#manage_properties_table').empty();
// $('#selected_locations_table').empty();
//}
//}
function makeBillingAddress(event){
$(event).parents('tr').find('#address-menu').toggle();
const parentTr_id = $(event).parents('tr').attr('id');
const customerId = $('#job_customer_attributes_id').val();
const id = $(event).parents('tr').children('td')[2].innerText.trim();
$.ajax({
method: 'PUT',
url: `/customers/${customerId}/update_attribute`,
data: {attribute: 'location_id', value: id},
success: function(data){
gritterSuccess("Success", "Successfully Set Billing Address");
const currentDefaultRow = $('#billing_address');
const currentDefaultAddress = currentDefaultRow.children('td:eq(1)').children('a').text().trim()
const currentDefaultId = currentDefaultRow.children('td:eq(2)').text().trim()
if(parentTr_id == 'default_address'){
$('#manage_properties_table').append(`<tr id="${currentDefaultId}" >
<td></td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${currentDefaultAddress}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${currentDefaultId} </td>
</tr>`);
let icon = ('<i class="fas fa-house"></i> <i class="fas fa-van-shuttle"></i>')
$('#selected_locations_table').html('');
$('#selected_locations_table').append(`<tr data-is_default_location="${data.default_location.is_default_location}" data-id="${data.default_location.id}" id="addresses" >
<td> ${icon} </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${data.default_location.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${data.default_location.id} </td>
</tr>`);
} else if($('#addresses')?.length){
const addressesRow = $('#addresses');
const isDefaultLocation = addressesRow.attr('data-is_default_location');
const commonAddress = addressesRow.children('td:eq(1)').children('a').text().trim()
const addressesDefaultId = addressesRow.children('td:eq(2)').text().trim()
const targetElementRow = $(`#${data?.default_location?.id}`)
const targetElementAddress = targetElementRow.children('td:eq(1)').children('a').text().trim()
const targetElementId = targetElementRow.children('td:eq(2)').text().trim()
targetElementRow.remove();
$('#selected_locations_table').html('');
$('#selected_locations_table').append(`<tr data-is_default_location="${data.default_location.is_default_location}" data-id="${data.default_location.id}" id="billing_address" >
<td> <i class="fas fa-house"></i> </td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${data.default_location.display_address}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeServiceAddress(this)" >Make Service Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${data.default_location.id} </td>
</tr>
<tr data-is_default_location="${isDefaultLocation}" data-id="${addressesDefaultId}" id="default_address" >
<td><i class="fas fa-van-shuttle" aria-hidden="true"></i></td>
<td>
<a href="javascript:void(0)" class="address-context-menu-link" onclick="addressContextMenuLink(this)" title="Address" id="address-context-menu-link">
${commonAddress}
</a>
<ul class="custom-menu address-menu" id="address-menu">
<li onclick="makeBillingAddress(this)" >Make Billing Address</li>
<li onclick="editLocation(this)" >Edit Location</li>
</ul>
</td>
<td style="display:none"> ${addressesDefaultId} </td>
</tr>`);
} else{
const targetElementRow = $(`#${data?.default_location?.id}`)
const targetElementAddress = targetElementRow.children('td:eq(1)').children('a').text().trim()
const targetElementId = targetElementRow.children('td:eq(2)').text().trim()
//Updating target address Row
targetElementRow.children('td:eq(1)').children('a').text(currentDefaultAddress)
targetElementRow.children('td:eq(2)').text(currentDefaultId)
targetElementRow.attr('id', currentDefaultId)
//Updating Billing address Row
var defaultIcon = '<i class="fas fa-house" aria-hidden="true"></i>';
currentDefaultRow.attr('data-is_default_location', data.default_location.is_default_location)
currentDefaultRow.children('td:eq(0)').html(defaultIcon)
currentDefaultRow.children('td:eq(1)').children('a').text(targetElementAddress)
currentDefaultRow.children('td:eq(2)').text(targetElementId)
}
},
error: function(data){
gritterError("Error", "Something Error", "10000");
}
});
}
<% end %>
$(function() {
showReadOnlyCalendar();
});
<%= content_for :modal_header, flush: true do %>
<%= content_for :modal_body, flush: true do %>
Upload image
<%= get_trade %> Cloud Image Upload
<%= render :partial => "shared/generic_modal", locals: {modal_id: "images-upload-modal"}%>
<% disable_tab_index = defined?(disable_tab_index) ? disable_tab_index : false %>
<%= content_for :footer_scripts do %>
$(document).click(function(e){
if (e.target == document.getElementById('<%= modal_id %>')) {
$("#<%= modal_id %>").modal("hide");
}
});
<% end %>
<% collapse_id = defined?(collapse_id) ? collapse_id : "collapseOne" %>
<%= stylesheet_link_tag "landingpage-scss/one-page-wonder" %>
<%= image_tag("landing-page/#{get_grow_header_image_path_from_domain()}", :class => "img-fluid" )%>
Is your <%= get_trade %> business still stuck using a filing cabinet? Are you manually drafting bids, proposals, and invoices for your customers? There's a better way. Run your business from the cloud. Here's now.
In the age of self-driving cars and 3D printers, you shouldn't have to manually send invoices and bids at the end of your day. Automate the paperwork and focus more on doing what you do best: helping your customers
Run your business instead of letting your business run you. Analyze your data and see the bird's eye view of your business. Your data paints a picture and tells a story, unlock that story with our business data analytics.
Copyright © Lightning Service 2019
<%= content_for :card_body do %>
Name:
<%= @employee_job_type.name %>
Description:
<%= @employee_job_type.description %>
<%= link_to 'Edit', edit_employee_job_type_path(@employee_job_type) %> |
<%= link_to 'Back', employee_job_types_path %>
<% end %>
<%= render partial: "shared/card" %>
<%= render 'form', employee_job_type: @employee_job_type %>
<%= link_to 'Show', @employee_job_type %> |
<%= link_to 'Back', employee_job_types_path %>
json.array! @employee_job_types, partial: 'employee_job_types/employee_job_type', as: :employee_job_type
json.partial! "employee_job_types/employee_job_type", employee_job_type: @employee_job_type
json.extract! employee_job_type, :id, :string, :string, :created_at, :updated_at
json.url employee_job_type_url(employee_job_type, format: :json)
<%= react_component 'shared/IndexPage', {
title: "Appointment Types",
attributes: ["Name", "Description"],
data: @employee_job_types.map {|appointment| [appointment.id, {url: url_for(appointment), title: appointment.name}, appointment.description]},
basePath: '/employee_job_types'} %>
<%= form_with(model: @employee_job_type, local: true) do |form| %>
<% if @employee_job_type.errors.any? %>
<%= render 'form', roster: @roster %>
json.extract! roster, :id, :created_at, :updated_at
json.url roster_url(roster, format: :json)
<%= render 'form', customer: @roster %>
json.array! @rosters, partial: 'rosters/roster', as: :roster
json.partial! "rosters/roster", roster: @roster
<%= form.fields_for :employee_roster do |add_employee_roster|%>
<%= render "form", web_page: @web_page %>
<%= link_to 'Back to web pages', web_pages_path %>
<%= notice %>
<%= render @web_page %>
<%= link_to 'Edit this web_page', edit_web_page_path(@web_page), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%#= link_to "Show this web page", web_page, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%#= link_to 'Edit this web page', edit_web_page_path(web_page), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
<%# end %>
<%= render "form", web_page: @web_page %>
<%= link_to "Show this web page", @web_page, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to web pages", web_pages_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
json.array! @web_pages, partial: "web_pages/web_page", as: :web_page
json.partial! "web_pages/web_page", web_page: @web_page
json.extract! web_page, :id, :title, :slug, :content, :is_home_page, :business_id, :page_category_id, :meta_description, :meta_keywords, :position, :created_at, :updated_at
json.url web_page_url(web_page, format: :json)
YC -- here's your playground. Click a page and rebrand it.
<%= notice %>
<%= web_page.business.name %>
<%= web_page.slug %>
<%= link_to 'New Web Page', new_web_page_path, class: "mt-6 inline-block bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
<%= form_with(model: web_page, local: true) do |form| %>
<% if web_page.errors.any? %>
Find me in app/views/webhooks/pipedrive.html.erb
Find me in app/views/line_items/create.html.erb
Find me in app/views/line_items/destroy.html.erb
Find me in app/views/line_items/update.html.erb
<%= render "form", visitor_device_info: @visitor_device_info %>
<%= link_to 'Back to visitor device infos', visitor_device_infos_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= notice %>
<%= render @visitor_device_info %>
<%= link_to 'Edit this visitor_device_info', edit_visitor_device_info_path(@visitor_device_info), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
json.extract! visitor_device_info, :id, :user_agent_raw, :referrer, :url, :ip_address, :device_width, :device_height, :device_os, :os_version, :browser_version, :browser, :business_id, :user_cookie_guid, :auth_token, :created_at, :updated_at
json.url visitor_device_info_url(visitor_device_info, format: :json)
<%= render "form", visitor_device_info: @visitor_device_info %>
<%= link_to "Show this visitor device info", @visitor_device_info, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to visitor device infos", visitor_device_infos_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
json.array! @visitor_device_infos, partial: "visitor_device_infos/visitor_device_info", as: :visitor_device_info
Date & Time:
<%= visitor_device_info.created_at.in_time_zone("Mountain Time (US & Canada)").strftime("%B %d, %Y %I:%M %p") %>
User agent raw:
<%= visitor_device_info.user_agent_raw %>
Referrer:
<%= visitor_device_info.referrer %>
Url:
<%= visitor_device_info.url %>
Ip address:
"><%= visitor_device_info.ip_address %>
Device width:
<%= visitor_device_info.device_width %>
Device height:
<%= visitor_device_info.device_height %>
Device os:
<%= visitor_device_info.device_os %>
Os version:
<%= visitor_device_info.os_version %>
Browser version:
<%= visitor_device_info.browser_version %>
Browser:
<%= visitor_device_info.browser %>
Business:
<%= visitor_device_info.business_id %>
User cookie guid:
"><%= visitor_device_info.user_cookie_guid %>
Auth token:
<%= visitor_device_info.auth_token %>
Hostname:
<%= visitor_device_info.hostname %>
City:
<%= visitor_device_info.city %>
Zip:
<%= visitor_device_info.zip %>
Region:
<%= visitor_device_info.region %>
Coordinates:
<%= visitor_device_info.coordinates %>
ISP:
<%= visitor_device_info.isp %>
json.partial! "visitor_device_infos/visitor_device_info", visitor_device_info: @visitor_device_info
<%= notice %>
<%# content_for :footer_scripts do %>
<%# end %>
<%= form_with(model: visitor_device_info, class: "contents") do |form| %>
<% if visitor_device_info.errors.any? %>
<%= render 'form', forwarding_number: @forwarding_number %>
<%= link_to 'Back', forwarding_numbers_path %>
<%= notice %>
Phone:
<%= @forwarding_number.phone %>
Description:
<%= @forwarding_number.description %>
Business:
<%= @forwarding_number.business %>
<%= link_to 'Edit', edit_forwarding_number_path(@forwarding_number) %> |
<%= link_to 'Back', forwarding_numbers_path %>
<%= render 'form', forwarding_number: @forwarding_number %>
<%= link_to 'Show', @forwarding_number %> |
<%= link_to 'Back', forwarding_numbers_path %>
json.array! @forwarding_numbers, partial: "forwarding_numbers/forwarding_number", as: :forwarding_number
json.partial! "forwarding_numbers/forwarding_number", forwarding_number: @forwarding_number
<%= notice %>
Phone
Description
Business
<% @forwarding_numbers.each do |forwarding_number| %>
<%= forwarding_number.phone %>
<%= forwarding_number.description %>
<%= forwarding_number.business %>
<%= link_to 'Show', forwarding_number %>
<%= link_to 'Edit', edit_forwarding_number_path(forwarding_number) %>
<%= link_to 'Destroy', forwarding_number, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Forwarding Number', new_forwarding_number_path %>
<%= form_with(model: forwarding_number, local: true) do |form| %>
<% if forwarding_number.errors.any? %>
json.extract! forwarding_number, :id, :phone, :description, :business_id, :created_at, :updated_at
json.url forwarding_number_url(forwarding_number, format: :json)
<%# render 'form', published_job_page: @published_job_page %>
<%= react_component "marketing/CreateContent/NewPublishableJobPage", {
published_job_page: @published_job_page,
invoice: @invoice,
job_summary_without_price: @job_summary_without_price,
job_summary_with_price: @job_summary_with_price,
notes: @notes,
keywords: @keywords,
customerName: @customer_name,
address: @address,
company_name: @company_name,
owner_name: @owner_name,
customTitlePrompt: @custom_title_prompt,
customHtmlPrompt: @custom_html_prompt
} %>
<%= render partial: 'shared/image_selection', locals: {invoice: @invoice} %>
<%# react_component("marketing/CreateContent/NewPublishableJobPage", {
invoice: @invoice,
notes: @job_summary_without_price,
notesWithPrice: @job_summary_with_price,
keywords: @keywords,
customerName: @customer_name,
address: @address,
customTitlePrompt: @custom_title_prompt,
customHtmlPrompt: @custom_html_prompt
}) %>
<%# react_component 'seo/SEOForm', invoice: @invoice.to_json, images: @invoice&.customer&.get_images %>
<%#= link_to 'Back', published_job_pages_path %>
<%= content_for :header do %>
<% end %>
<%= content_for :header do %>
body {
background-color: #f9f4f4;
}
img {
max-width: 50%; /* ensures the image is responsive */
height: auto; /* maintain aspect ratio */
border: 1px solid #ddd; /* light border */
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* subtle shadow */
border-radius: 5px; /* rounded corners */
transition: transform 0.3s ease; /* smooth transition for hover effect */
}
img:hover {
transform: scale(1.05); /* slightly enlarge on hover */
}
.fr-responsive-image {
max-width: 50%; /* ensures the image is responsive */
height: auto; /* maintain aspect ratio */
border: 1px solid #ddd; /* light border */
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* subtle shadow */
border-radius: 5px; /* rounded corners */
transition: transform 0.3s ease; /* smooth transition for hover effect */
}
.fr-responsive-image:hover {
transform: scale(1.05); /* slightly enlarge on hover */
}
/* h1 {
font-family:'Roboto';
font-weight: 700;
letter-spacing: 0.07813rem;
color: #333333;
}
body {
font-family: "poppins";
line-height: 150%;
}
h3 {
font-family:'Roboto';
font-weight: 700;
font-size: 1.25rem;
color: #FF5A5A;
} */
h1 {
margin: 10px;
padding: 20px;
text-align: center;
}
h2 {
margin: 10px;
padding: 20px;
text-align: center;
}
h3 {
margin: 10px;
padding: 20px;
text-align: center;
}
/* hr {} */
p {
font-size: 125%;
}
#bar {
position: sticky;
height: 70px;
background: white;
top: 10px;
bottom: calc(100% - 70px);
z-index: 10;
border-radius: 10px;
}
.city-seo-header-button {
position: sticky;
background-color: #fefefe;
color: #333333;
padding: 10px 20px; /* Adjust padding as needed for button size */
border: none; /* Assuming you don't want a border */
right: 10px; /* Or however much space you want from the right */
top: 10%; /* Center vertically */
margin: 10px;
/* transform: translateY(-50%); /* Adjust vertical alignment */
z-index: 10; /* Ensure it's above other elements */
float: right;
margin-left: auto;
display: inline;
margin-right: 30px;
border-radius: 10px;
/* font-family: "inherit"; */
}
#bar h3{
/* font-family: "roboto"; */
color: #333333;
padding-left: 20px;
margin: 15px;
float: left;
display: inline;
background-color: white;
padding: 5px;
border-radius: 10px;
}
@media only screen and (max-width: 768px) {
#bar h3{
font-size: 1.25rem;
}
.city-seo-header-button{
margin: none;
}
}
@media only screen and (max-width: 365px){
#bar h3{
display: none;
}
}
.city-seo-header-button:hover{
background-color: #fefefe;
/* color: #4f4f4f; */
border-radius: 10px;
cursor:pointer;
color: #eb3323;
}
.city-seo-header-button:focus{
background-color: #fefefe;
color: #eb3323;
}
a.fr-button {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
}
#wrapper {
display: inline;
}
.prompt-content {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
margin-bottom: 1rem;
white-space: pre-wrap;
}
<% end %>
<%= notice %>
<% unless @wordpress_categories.nil? %>
<% if @published_job_page.slug.nil? %>
<%# Add Facebook Share Link %>
<% unless current_business.facebook_page_connections.empty? %>
<%# Add Facebook Share Link %>
<%# Read only version of the HTML. %>
<% unless @published_job_page.job.nil? %>
">Job
<% end %>
<%= link_to 'Back', published_job_pages_path %>
<%= content_for :footer_scripts do %>
function publishToWordpress(){
console.log("Publish pressed!");
var categoryIds = [];
var checkboxes = document.querySelectorAll('input[name="wp_categories[]"]:checked');
checkboxes.forEach(function(checkbox) {
categoryIds.push(checkbox.value);
})
console.log("Category ids: " + categoryIds);
//var featuredImageUrl = "https://fieldrocket-seo-optimized-images.s3.amazonaws.com/Salt-Lake-City-,-Ut--Salt-Lake-City-Tree-Removal-7.jpg"; //TODO: Hardcoded for now.
var featuredImageUrl = $('#featuredImageUrl').val();
$("#publishToWordpressButton").addClass("is-loading");
$.post({
url: `/published_job_pages/<%=@published_job_page.id%>/publish_job_to_wordpress`,
data: {wp_categories: categoryIds, featured_image_url: featuredImageUrl},
success(data){
console.log("success server response: " + data);
gritterSuccess("Success", "This article was published on Wordpress");
$("#publishToWordpressButton").removeClass("is-loading");
window.location.reload();
},
error(data){
console.log("error server response: " + data);
$("#publishToWordpressButton").removeClass("is-loading");
gritterError("Error", "Couldn't publish this article");
}
})
}
function copyFacebookContentToClipboard(){
console.log("made it");
let facebookPostText = $('#facebook-post-content').text()
navigator.clipboard.writeText(facebookPostText)
gritterSuccess("Success", "Copied to Clipboard")
}
function copyNextDoorContentToClipboard(){
console.log("made it");
let NextDoorPostText = $('#nextdoor-post-content').text()
navigator.clipboard.writeText(NextDoorPostText)
gritterSuccess("Success", "Copied to Clipboard")
}
function copyInstagramContentToClipboard(){
console.log("made it");
let instagramPostText = $('#instagram-post-content').text()
navigator.clipboard.writeText(instagramPostText)
gritterSuccess("Success", "Copied to Clipboard")
}
function copyGoogleMyBusinessContentToClipboard(){
console.log("made it");
let googleMyBusinessPostText = $('#google-my-business-post-content').text()
navigator.clipboard.writeText(googleMyBusinessPostText)
gritterSuccess("Success", "Copied to Clipboard")
}
function publishToFacebookApi(){
console.log("Publish to Facebook pressed!");
$("#publishToFacebookButton").addClass("is-loading");
$.post({
url: `/published_job_pages/<%= @published_job_page.id %>/publish_to_facebook`,
success(data){
console.log("success server response: " + data);
gritterSuccess("Success", "This article was published on Facebook");
$("#publishToFacebookButton").removeClass("is-loading");
},
error(data){
console.log("error server response: " + data);
$("#publishToFacebookButton").removeClass("is-loading");
gritterError("Error", "Couldn't publish this article to Facebook");
}
})
}
$(document).ready(function() {
var publishToWordpressButton = document.getElementById("publishToWordpressButton");
if (publishToWordpressButton) {
publishToWordpressButton.addEventListener("click", publishToWordpress, false);
}
var publishToFacebookButton = document.getElementById("publishToFacebookButton");
if (publishToFacebookButton) {
publishToFacebookButton.addEventListener("click", publishToFacebookApi, false);
}
// Add toggle functionality for prompts section
document.getElementById("togglePromptsButton").addEventListener("click", function() {
console.log("made it");
var promptsSection = document.getElementById("promptsSection");
if (promptsSection.style.display === "none") {
promptsSection.style.display = "block";
this.textContent = "Hide Prompts";
} else {
promptsSection.style.display = "none";
this.textContent = "Show Prompts";
}
});
});
<% end %>
<%= content_for :header do %>
<%# %>
<% end %>
<%= content_for :header do %>
body {
background-color: #f9f4f4;
}
.fr-responsive-image {
max-width: 50%; /* ensures the image is responsive */
height: auto; /* maintain aspect ratio */
border: 1px solid #ddd; /* light border */
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); /* subtle shadow */
border-radius: 5px; /* rounded corners */
transition: transform 0.3s ease; /* smooth transition for hover effect */
}
.fr-responsive-image:hover {
transform: scale(1.05); /* slightly enlarge on hover */
}
/* h1 {
font-family:'Roboto';
font-weight: 700;
letter-spacing: 0.07813rem;
color: #333333;
}
body {
font-family: "poppins";
line-height: 150%;
}
h3 {
font-family:'Roboto';
font-weight: 700;
font-size: 1.25rem;
color: #FF5A5A;
} */
h1 {
margin: 10px;
padding: 20px;
text-align: center;
}
h2 {
margin: 10px;
padding: 20px;
text-align: center;
}
h3 {
margin: 10px;
padding: 20px;
text-align: center;
}
/* hr {} */
p {
font-size: 125%;
}
#bar {
position: sticky;
height: 70px;
background: white;
top: 10px;
bottom: calc(100% - 70px);
z-index: 10;
border-radius: 10px;
}
.city-seo-header-button {
position: sticky;
background-color: #fefefe;
color: #333333;
padding: 10px 20px; /* Adjust padding as needed for button size */
border: none; /* Assuming you don't want a border */
right: 10px; /* Or however much space you want from the right */
top: 10%; /* Center vertically */
margin: 10px;
/* transform: translateY(-50%); /* Adjust vertical alignment */
z-index: 10; /* Ensure it's above other elements */
float: right;
margin-left: auto;
display: inline;
margin-right: 30px;
border-radius: 10px;
/* font-family: "inherit"; */
}
#bar h3{
/* font-family: "roboto"; */
color: #333333;
padding-left: 20px;
margin: 15px;
float: left;
display: inline;
background-color: white;
padding: 5px;
border-radius: 10px;
}
@media only screen and (max-width: 768px) {
#bar h3{
font-size: 1.25rem;
}
.city-seo-header-button{
margin: none;
}
}
@media only screen and (max-width: 365px){
#bar h3{
display: none;
}
}
.city-seo-header-button:hover{
background-color: #fefefe;
/* color: #4f4f4f; */
border-radius: 10px;
cursor:pointer;
color: #eb3323;
}
.city-seo-header-button:focus{
background-color: #fefefe;
color: #eb3323;
}
a.fr-button {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
}
#wrapper {
display: inline;
}
<% end %>
<%= notice %>
json.array! @published_job_pages, partial: "published_job_pages/published_job_page", as: :published_job_page
json.extract! published_job_page, :id, :job_id, :title, :html, :slug, :created_at, :updated_at, :generated_facebook_post, :generated_nextdoor_post, :generated_instagram_post, :generated_google_my_business_post
json.url published_job_page_url(published_job_page, format: :json)
json.partial! "published_job_pages/published_job_page", published_job_page: @published_job_page
<%= notice %>
Job
Title
Slug
Keywords
<% @published_job_pages.each do |published_job_page| %>
<%= published_job_page.job %>
<%= published_job_page.title %>
"><%= published_job_page.slug %>
<%=published_job_page.target_keywords%>
<%= link_to 'Show', published_job_page %>
<%= link_to 'Edit', edit_published_job_page_path(published_job_page) %>
<%= link_to 'Destroy', published_job_page, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Published Job Page', new_published_job_page_path %>
<%= render partial: 'shared/image_selection', locals: {invoice: @invoice} %>
<%# hidden_field_tag 'picture_urls', @images %>
<%= content_for :footer_scripts do %>
const notesWithPrice = `<%= raw @job_summary_with_price&.gsub(/\n/, '\\n') %>`;
const notesWithoutPrice = `<%= raw @job_summary_without_price&.gsub(/\n/, '\\n') %>`;
document.getElementById("include_price_checkbox").addEventListener("click", function(element){
console.log(event.target.checked);
let shouldIncludePricing = event.target.checked;
if (shouldIncludePricing){
$("#published_job_page_notes").val(notesWithPrice);
}
else {
$("#published_job_page_notes").val(notesWithoutPrice);
}
console.log("Include price checkbox checked! Value is: " + shouldIncludePricing);
});
document.getElementById("custom_prompt_checkbox").addEventListener("change", function() {
document.getElementById("custom_prompts").style.display = this.checked ? "block" : "none";
});
document.addEventListener("DOMContentLoaded", function() {
if (document.getElementById("custom_prompt_checkbox").checked) {
document.getElementById("custom_prompts").style.display = "block";
}
});
<% end %>
<%# form.label :title, class: 'form-label' %>
<%# form.text_field :title, class: 'form-control' %>
<%# hidden_field_tag :html, value: @published_job_page.html %>
<%= form.text_area :html, class: 'form-control', style: "display:none", value: @published_job_page.html %>
<% else %>
**This Article Has Been Published And Can't Be Edited.
<%= content_for :footer_scripts do %>
<%# %>
// var easyMDE = new EasyMDE({ element: document.getElementById('editor') });
// let markdown = `<%= @published_job_page.html.html_safe.gsub("\n", "\\n").gsub('"', '\\"') %>`;
// easyMDE.value(markdown);
//make these variables global
let generatedHtml;
let editorInstance;
function submitForm() {
console.log("POST FORM HERE!")
//TODO: we need to set the HTML field form with serialized HTML that is legitimate and works.
// $('#published_job_page_html').val()
$('#published_job_page_html').val(getEditorHtml());
$('#edit_form').submit(); //Here is where you can manually submit the form
}
$(document).ready(function() {
document.getElementById ("saveButton").addEventListener("click", submitForm, false);
// Use .html_safe to prevent HTML escaping, and gsub to escape newline and quotation marks for JavaScript.
generatedHtml = `<%= @published_job_page.html.html_safe.gsub("\n", "\\n").gsub('"', '\\"').gsub('`', '') %>`;
console.log(generatedHtml); // To verify the output
ClassicEditor
.create(document.querySelector('#editor'))
.then(editor => {
editorInstance = editor; // Store the editor instance
})
.catch(error => {
console.error(error);
});
})
function getEditorHtml() {
const htmlContent = editorInstance.getData();
console.log(htmlContent); // Logs the HTML content to the console
return htmlContent;
}
<% end %>
<%= render 'form', text_template: @text_template %>
<%= link_to 'Back', text_templates_path %>
<%= notice %>
Title:
<%= @text_template.title %>
json.extract! text_template, :id, :title, :body, :business_id, :created_at, :updated_at
json.url text_template_url(text_template, format: :json)
<%= render 'form', text_template: @text_template %>
<%= link_to 'Show', @text_template %> |
<%= link_to 'Back', text_templates_path %>
json.array! @text_templates, partial: 'text_templates/text_template', as: :text_template
json.partial! "text_templates/text_template", text_template: @text_template
<%= notice %>
<%= react_component "shared/TableDataNotEditable",
title: "Text Template",
attributes: ["Name", "Body", "Created On"],
data: @text_templates.map {|i| [i.id,
{url: "/text_templates/#{i.id}", title: i.title},
i.body,
i.created_at.strftime("%B %d, %Y, %A")]},
basePath: '/text_templates'
%>
<%= form_with(model: text_template, local: true) do |form| %>
<% if text_template.errors.any? %>
<%= render 'container' do%>
<%#
<%= content_for :footer_scripts do %>
$('#header').remove();
$('.mdk-header-layout__content')[0].style.paddingTop
'109px'
<% end %>
<%= render 'container', job_request: @job_request do%>
<% unless @job_request.business.visitor_device_info_auth_token.blank? %>
const atk = "<%=@job_request.business.visitor_device_info_auth_token%>";
const dmn = "<%=ENV["BASE_URL"]%>";
// Check if fr_guid exists in localStorage
let frGuid = localStorage.getItem('fr_guid');
if (!frGuid) {
// Generate a new GUID using crypto.randomUUID() and append "-42"
frGuid = `${crypto.randomUUID()}-42`;
// Set the new GUID in localStorage with the key fr_guid
localStorage.setItem('fr_guid', frGuid);
console.log('New fr_guid:', frGuid);
} else {
console.log('Existing fr_guid:', frGuid);
}
const data = {
user_agent_raw: navigator.userAgent,
referrer: document.referrer,
url: window.location.href,
device_width: window.screen.width.toString(),
device_height: window.screen.height.toString(),
device_os: navigator.platform,
os_version: navigator.userAgentData ? navigator.userAgentData.platform : 'Unknown',
browser_version: navigator.userAgentData ? navigator.userAgentData.brands.map(brand => `${brand.brand} ${brand.version}`).join(', ') : 'Unknown',
browser: navigator.userAgentData ? navigator.userAgentData.brands[0].brand : 'Unknown',
user_cookie_guid: frGuid,
auth_token: atk,
};
// console.log("Sending data: " + data);
// console.log(JSON.stringify(data));
fetch(`${dmn}/visitor_device_infos.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
})
.then(response => {
console.log("FR - made it");
// Handle the response from the server
})
.catch(error => {
console.log("FR - didn't make it");
// Handle any errors
});
<% end %>
<% if @job_request.business.id == 433 && current_employee.nil? %>
//redirect to the bh exterior thank you page!
//TODO: Test this
window.location.href = "https://bhexteriordesign.com/thank-you/";
<% end %>
<% end %>
<%= render 'form', job_request: @job_request %>
<%= link_to 'Show', @job_request %> |
<%= link_to 'Back', job_requests_path %>
json.array! @job_requests, partial: 'job_requests/job_request', as: :job_request
json.extract! job_request, :id, :business_id, :first_name, :last_name, :street_address, :city, :zip, :state, :phone, :email, :notes, :created_at, :updated_at
json.url job_request_url(job_request, format: :json)
json.partial! "job_requests/job_request", job_request: @job_request
<%= react_component 'calendar/RequestsDraggable', {requests: @requests}%>
.page { padding-bottom: 0px;}
<%= notice %>
Business
First name
Last name
Street address
City
Zip
State
Phone
Email
Notes
Customer
Job
<% @job_requests.each do |job_request| %>
<%= job_request.business %>
<%= job_request.first_name %>
<%= job_request.last_name %>
<%= job_request.street_address %>
<%= job_request.city %>
<%= job_request.zip %>
<%= job_request.state %>
<%= job_request.phone %>
<%= job_request.email %>
<%= job_request.notes %>
<%= link_to 'Show', job_request %>
<%= link_to 'Edit', edit_job_request_path(job_request) %>
<%= link_to 'Destroy', job_request, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
<%= link_to 'New Job Request', new_job_request_path %>
<%= form_with(model: job_request, local: true, id: "job_request_form") do |form| %>
<%= form.hidden_field :secret_key, :value => job_request.business.secret_key %>
<%= form.hidden_field :customer_id %>
<%= form.hidden_field :location_id %>
<%= form.hidden_field :create_new_location_for_job_request, :value => false %>
first name*
last name*
Phone number*
email*
we will never share this with anyone
<% unless job_request.business.hide_job_request_address_fields %>
full ADDRESS*
city*
zip code*
state*
How did you hear about us?
<%# form.file_field :images, accept:'image/*, application/pdf', multiple: true, onchange: "previewImages(this)" %>
<% unless job_request.business.extra_instructions_for_job_request.nil? %>
<%= job_request.business.extra_instructions_for_job_request.html_safe %>
<%#
Please Click the "Upload your Images" button above, and attach:
<% if !job_request.business.placeholder_for_job_request_notes.nil? %>
<%# form.text_area :notes, value: job_request.notes, placeholder: 'Please include rough dimensions here (width and height), and any other details important to you about your project!*'%>
<%= form.text_area :notes, value: job_request.notes, placeholder: job_request.business.placeholder_for_job_request_notes %>
<% else %>
<%= form.text_area :notes, value: job_request.notes, placeholder: 'How can we help you?*'%>
<% end %>
Submit
Your images are being saved.. please wait for them to finish uploading before submitting!
Your images are saved! You can now submit. 👍
<%= form.hidden_field :marketing_sources_id, value: job_request.marketing_sources_id %>
<%= form.hidden_field :utm_source, value: job_request.utm_source %>
<%= form.hidden_field :utm_campaign, value: job_request.utm_campaign %>
<%= form.hidden_field :utm_content, value: job_request.utm_content %>
<% end %>
<%# WARNING! Duplicate code in businesses/new_images_upload %>
<%= content_for :header do %>
<% end %>
<%= content_for :footer_scripts do %>
<%= javascript_include_tag "job_requests.js" %>
<% end %>
// let job_request_secret_key = <%=@secret_key%>
document.addEventListener("DOMContentLoaded", function() {
var submitButton = document.getElementById("submitButton");
var submitIcon = document.getElementById("submitIcon");
submitButton.addEventListener("click", function() {
// console.log("Submit button clicked!");
submitButton.classList.add("is-loading");
// submitIcon.classList.add("is-loading");
});
});
const chunkSize = 64 * 1024 * 1024;
const fileReader = new FileReader();
let hasher = null;
const imageBlobIdsToAttachToJobRequest = []; // This is an array with the image blob IDs that we need to pass as a parameter into job_requests so it can attach these images to the job_request after it's been created.
//TODO: This is where I believe it shows the files.?
/**
* This method takes the files that were selected, fetches pre-signed S3 urls so we can upload directly from the client for better performance.
*/
async function get_file_name(input){
const file_name = document.getElementById('image_holder');
const uploadText = file_name.files.length > 1 ? 'Upload your images ('+file_name.files.length+' files)' : 'Upload your images ('+file_name.files[0].name+')';
$('#file_name_show').html(uploadText);
const previewContainer = document.querySelector('#upload-preview');
previewContainer.innerHTML = ''; // Clear previous previews
let uploadPromises = [];
//disable the submit button until all images are finished being uploaded.
document.getElementById('submitButton').disabled = true; // Re-enable the submit button
// document.getElementById('submitButton').disabled = true; // Re-enable the submit button
$('#imageUploadingLabel').show();
$('#imagesDoneUploading').hide();
$('#submitButton').addClass('is-loading');
//TODO: how do we iterate over these images and upload them?
// /jobs_requests/pre_signed_s3_url_for_uploading_images
// Check if browser supports FileReader
if (window.FileReader && input.files && input.files.length) {
//?? We do it with all the images here.?
for (let i = 0; i < input.files.length; i++) {
const file = input.files[i];
// const hex = await RNFS.hash(result.path, 'md5') //get the md5 hash for this image
const hex = await readFile(file);
// const base64 = Buffer.from(hex, 'hex').toString('base64') // get the base64 encoded for this image
const base64 = buffer.Buffer.from(hex, "hex").toString('base64');
const randomGuid = Math.random().toString(36).substr(2, 5); //get a randomGuid for the image name
const extensionForImage = getExtenstionFromMimeType(file.type) //get the extension for the image
const fileHash = {
filename: file.name || randomGuid + extensionForImage,
byte_size: file.size,
checksum: base64,
content_type: file.type
};
let uploadPromise = new Promise((resolve, reject) => {
$.ajax({
type: "POST",
url: "/job_requests/pre_signed_s3_url_for_uploading_images.json",
data: fileHash,
success: function(data, textStatus, jqXHR) {
console.log(data);
// debugger; // Uncomment if you need to pause execution here
//PREVIEW IMAGE SO USER CAN SEE WHAT THEY ARE UPLOADING
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.alt = "Preview image";
img.style.width = '100px'; // or any desired preview size
const div = document.createElement('div');
div.id = data["key"];
div.className = "is-loading";
div.appendChild(img); // Append the img to the div
previewContainer.appendChild(div); // Append the div to the previewContainer
};
// $('#upload-preview').addClass('is-loading');
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
//TODO: Turn iFrame into document.createElement('iframe')
// <iframe src="" title="" align="top" width="100%" frameborder="0" scrolling="auto" target="Message"></iframe>
const reader = new FileReader();
reader.onload = function(e) {
const iframe = document.createElement('iframe');
iframe.src = e.target.result;
iframe.title = "Preview PDF";
iframe.align = "top";
iframe.width = "100%";
iframe.frameBorder = "0";
iframe.scrolling = "auto";
iframe.target = "Message";
const div = document.createElement('div');
div.id = data["key"];
div.className = "is-loading";
div.appendChild(iframe); // Append the iframe to the div
previewContainer.appendChild(div); // Append the div to the previewContainer
}
}
// imageBlobIdsToAttachToJobRequest.push(data["signed_id"]);
const signedId = data["signed_id"];
const hiddenInput = $('<input/>', {
type: 'hidden',
name: 'job_request[images][]', // This name matches the parameter you're allowing in your controller
value: signedId
});
$(input.form).append(hiddenInput);
uploadToS3(data, file, resolve, reject);
},
error: function(jqXHR, textStatus, errorThrown) {
console.log("Error uploading s3 blob!");
console.log(textStatus, errorThrown);
}
});
});
uploadPromises.push(uploadPromise);
}
// Wait for all the images to be uploaded before re-enabling the submit button
// await Promise.all(uploadPromises);
// document.getElementById('submitButton').disabled = false; // Re-enable the submit button
await Promise.all(uploadPromises)
.then(() => {
console.log('All uploads complete!');
document.getElementById('submitButton').disabled = false; // Re-enable the submit button
$('#submitButton').removeClass('is-loading');
$('#imageUploadingLabel').hide();
$('#imagesDoneUploading').show();
})
.catch(error => {
console.error('Some uploads failed:', error);
document.getElementById('submitButton').disabled = false; // Re-enable the submit button
$('#submitButton').removeClass('is-loading');
$('#imageUploadingLabel').hide();
$('#imagesDoneUploading').show();
// Handle the error - maybe inform the user that some uploads failed.
});
}
}
function uploadToS3(presignedData, file, resolveCallback, failureCallback) {
$.ajax({
url: presignedData.url,
type: 'PUT',
contentType: presignedData.headers["Content-Type"],
headers: {
"Content-MD5": presignedData.headers["Content-MD5"]
},
processData: false, // Important, do not process data
data: file,
success: function() {
console.log("Successfully uploaded to S3");
$(`#${presignedData.key}`).removeClass("is-loading"); //done uploading.
resolveCallback();
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Error uploading to S3:", textStatus, errorThrown);
failureCallback();
}
});
}
// This is how you would call the function inside your success callback:
// uploadToS3(data, file);
function hashChunk(chunk) {
return new Promise((resolve, reject) => {
fileReader.onload = async(e) => {
const view = new Uint8Array(e.target.result);
hasher.update(view);
resolve();
};
fileReader.readAsArrayBuffer(chunk);
});
}
const readFile = async(file) => {
if (hasher) {
hasher.init();
} else {
hasher = await hashwasm.createMD5();
}
const chunkNumber = Math.floor(file.size / chunkSize);
for (let i = 0; i <= chunkNumber; i++) {
const chunk = file.slice(
chunkSize * i,
Math.min(chunkSize * (i + 1), file.size)
);
await hashChunk(chunk);
}
const hash = hasher.digest();
return Promise.resolve(hash);
};
function getExtenstionFromMimeType(mimeType) {
const mimeToExtensionMap = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'image/webp': '.webp',
'image/tiff': '.tiff',
'image/bmp': '.bmp',
// Add other mappings as needed
};
return mimeToExtensionMap[mimeType] || ''; // return empty string if mimeType is not in the map
}
This Link Has Expired, Please contact the company
<%= @home_page.about_us %>
<%# We provide prompt, professional service. We take pride in the quality of our work, and the cleanliness of your home.
We offer full bathroom remodels. We can camera your sewer line. Drain cleaning. Water and sewer line repair. Water heater replacement. We offer whole house replumbing.%>
<%# @home_page.about_us_description %>
Elite Plumbing Services will keep your water flowing right. We are one of the top plumbing companies servicing along the Southern Wyoming Region. We provide a comprehensive range of services to homes and commercial properties in South Wyoming.
<%# TODO: Implement service cards similar to Elite Plumbing. %>
<%= review.content %>
<%= review.name %>
Error reading file: U_STRING_NOT_TERMINATED_WARNING
<%= @home_page.services_header %>
<%# Elite Plumbing Services will keep your water flowing right. We are one of the top plumbing companies servicing along the Southern Wyoming Region. We provide a comprehensive range of services to homes and commercial properties in South Wyoming.%>
<%# TODO: Implement service cards similar to Elite Plumbing. %>
/* Override button colors based on the company's @home_page */
.button {
background-color: <%= @home_page.primary_color%>;
}
.navbar a:hover {
color: <%= @home_page.secondary_color%>;
}
.primary-button {
background-color: <%= @home_page.primary_color%>;
}
.contact-us-button {
background-color: <%= @home_page.tertiary_color%>;
}
<%# render :partial => "public_website/styles/about_us_style"%>
<%= @home_page.our_team_description %>
<%# Elite Plumbing Services will keep your water flowing right. We are one of the top plumbing companies servicing along the Southern Wyoming Region. We provide a comprehensive range of services to homes and commercial properties in South Wyoming.%>
<%# TODO: Implement service cards similar to Elite Plumbing. %>
<%= content_for :modal_header, :flush => true do %>
Import Products and Services
<% end %>
<%= content_for :modal_body, :flush => true do %>
<%= render partial: "shared/generic_modal", locals: {modal_id: "import_products_services_modal"} %>
<%= content_for :footer_scripts do %>
// new QuerySelect('select_branch_for_summary',
// '/branches.json',
// null,
// SelectResultsTransform('name'),
// function(event){
// $('#open_branch_summary_button').click(function(event){
// window.location.replace(`/branches/${$('#select_branch_for_summary').val()}/daily_summary`);
// });
// });
<% end %>
<%= content_for :card_body, flush: true do %>
Name:
<%= @product.name %>
Description:
<%= @product.description %>
Price:
<%= @product.price %>
Sku:
<%= @product.SKU %>
<%= content_for :card_body, flush: true do %>
json.array! @products, partial: 'products/product', as: :product
json.partial! "products/product", product: @product
<%= react_component 'products/ProductsIndexPage', {
title: "Products and Services",
attributes: [:name, :description, :price, :sku],
data: @products.sort_by(&:name).map {|product| [product.id, {title: product.name, url: "products/#{product.id}"}, product.description, product.price, product.SKU]},
basePath: '/products'} %>
<%# hidden form for the csv file upload %>
<%= render "products/products_import_modal" %>
<%= form_with(model: product, local: true) do |form| %>
<% if product.errors.any? %>
json.extract! product, :id, :name, :description, :price, :SKU, :created_at, :updated_at
json.url product_url(product, format: :json)
class DeviceToken < ApplicationRecord
belongs_to :employee
#We want the device tokens to be unique..
#and if the device token is NOT unique, we want to update the
#employee id, or just not save it to the database.
#In other words, we don't want to send two notifications to the same phone, ever.
before_create :delete_any_duplicates
#This deletes any duplicates, then creates the new device token with the new employee id.
#This is so that if a device logs off one account, and logs into another account, they can still
#register their same device token for that different account, and get notifications from the new account.
def delete_any_duplicates
@duplicates = DeviceToken.where(token: self.token)
if !@duplicates.empty?
@duplicates.delete_all
end
end
end
class Product < ApplicationRecord
belongs_to :business
def get_quickbooks_id
self.quickbooks_id.nil? ? resolve_quickbooks_id_on_sku : self.quickbooks_id
end
def resolve_quickbooks_id_on_sku
Nokogiri::XML(self.business.query_quickbooks("SELECT Id FROM Item WHERE SKU = '#{self.SKU}'"))&.remove_namespaces!&.xpath("//Id")&.children&.text
end
end
class MarketingSource < ApplicationRecord
belongs_to :business
end
class FacebookPageConnection < ApplicationRecord
belongs_to :business
validates :facebook_id, presence: true, uniqueness: true
validates :access_token, :name, :page_type, presence: true
def post_to_facebook!(content, image_urls = [])
#Got the following error: invalid access token. need to figure out how to store refresh + refresh the token when it expires.
# ] ↳ app/models/published_job_page.rb:97:in `publish_to_facebook!'
# [employee-357] Failed to upload image 1: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:41 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"AE4BJ4UPDYGr19MD79UtAu1"}}
# [employee-357] Failed to upload image 2: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:41 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"AQJ-UwqcfMd9fUxjrhPQgJ7"}}
# [employee-357] Failed to upload image 3: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:41 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"ArSvt9K-_H8IMfFtUFVFlcl"}}
# [employee-357] Failed to upload image 4: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:41 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"AolsWZkEXp50PGIJL4mnMo6"}}
# [employee-357] Failed to upload image 5: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:42 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"AgeWOzU8cb4TZ8vD9j87IgH"}}
# [employee-357] Failed to upload image 6: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:42 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"ARnFsD6zmijPEL0XfAJd6yu"}}
# [employee-357] Failed to post to Facebook: {"error"=>{"message"=>"Error validating access token: Session has expired on Tuesday, 11-Jun-24 14:00:00 PDT. The current time is Tuesday, 11-Jun-24 16:21:42 PDT.", "type"=>"OAuthException", "code"=>190, "error_subcode"=>463, "fbtrace_id"=>"A180kn6D_iQaztJ-0jEdP5C"}}
uri = URI.parse("https://graph.facebook.com/v12.0/#{facebook_id}/feed")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
form_data = {
"message" => content,
"access_token" => access_token
}
if image_urls.any?
form_data["attached_media"] = image_urls.map.with_index do |url, index|
{ "media_fbid" => upload_image(url, index) }
end
end
request.body = form_data.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
response_body = JSON.parse(response.body)
if response.code.to_i == 200
Rails.logger.info("Successfully posted to Facebook: #{response_body}")
return response_body
else
Rails.logger.error("Failed to post to Facebook: #{response_body}")
return response_body
end
rescue => e
Rails.logger.error("Error posting to Facebook: #{e.message}")
return response_body
end
def post_video_to_facebook(video_url, description = "")
video_fbid = upload_video(video_url)
return unless video_fbid
uri = URI.parse("https://graph.facebook.com/v12.0/#{facebook_id}/videos")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
form_data = {
"message" => description,
"access_token" => access_token,
"attached_media" => [{ "media_fbid" => video_fbid }]
}
request.body = form_data.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
response_body = JSON.parse(response.body)
if response.code.to_i == 200
Rails.logger.info("Successfully posted video to Facebook: #{response_body}")
else
Rails.logger.error("Failed to post video to Facebook: #{response_body}")
end
rescue => e
Rails.logger.error("Error posting video to Facebook: #{e.message}")
end
def valid_access_token?
uri = URI.parse("https://graph.facebook.com/v12.0/me?access_token=#{access_token}")
response = Net::HTTP.get_response(uri)
response_body = JSON.parse(response.body)
response.code.to_i == 200 && response_body["id"].present?
rescue => e
Rails.logger.error("Error validating access token: #{e.message}")
false
end
private
def upload_image(image_url, index)
uri = URI.parse("https://graph.facebook.com/v12.0/#{facebook_id}/photos")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.set_form_data({
"url" => image_url,
"access_token" => access_token,
"published" => false
})
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
response_body = JSON.parse(response.body)
if response.code.to_i == 200
response_body["id"]
else
Rails.logger.error("Failed to upload image #{index + 1}: #{response_body}")
nil
end
rescue => e
Rails.logger.error("Error uploading image #{index + 1}: #{e.message}")
nil
end
def upload_video(video_url)
uri = URI.parse("https://graph.facebook.com/v12.0/#{facebook_id}/videos")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.set_form_data({
"file_url" => video_url,
"access_token" => access_token,
"published" => false
})
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
response_body = JSON.parse(response.body)
if response.code.to_i == 200
response_body["id"]
else
Rails.logger.error("Failed to upload video: #{response_body}")
nil
end
rescue => e
Rails.logger.error("Error uploading video: #{e.message}")
nil
end
end
class Timesheet < ApplicationRecord
belongs_to :employee
belongs_to :job, optional: :true
belongs_to :business
attribute :is_clocked_in, :boolean, default: false
def clock_in
if clocked_out.nil?
self.update(clocked_in: DateTime.now, is_clocked_in: true)
end
end
def clock_out
self.update(clocked_out: DateTime.now, is_clocked_in: false)
end
# Returns the difference in time of when this timesheet was clocked in, and when it was clocked out.
# If it hasn't clocked out yet, returns 0.
def duration
if (clocked_out)
(clocked_in - clocked_out).abs
else
return 0
end
end
def as_json(*)
super.tap do |hash|
hash["employee"] = employee_id
hash["employee_name"] = self.employee.full_name
hash["job"] = employee_id
hash["customer_name"] = job&.customer&.full_name
hash["duration"] = duration
end
end
def self.sum_duration_of_timesheets(timesheets)
duration = 0
timesheets.each do |ts|
duration += ts.duration
end
return duration
end
end
class Business < ApplicationRecord
include Quickbooks
include ActionView::Helpers::NumberHelper
require "twilio"
require "zapier"
require "mixmax"
require 'aws'
require "pipedrive"
require "claude"
has_many :branches
has_many :jobs
has_many :payments, through: :jobs
has_many :job_statuses
has_many :employee_jobs, through: :jobs
has_many :customers
has_many :employees
has_many :timesheets
has_many :notifications_lists
has_many :groups
has_many :rosters
has_many :job_invoices, class_name: "Invoice", source: :invoices, through: :jobs
has_many :invoice_templates
has_many :marketing_sources
has_many :locations, {:through=>:customers, :source=>"location"} #TODO: This is an issue when using ransack to search current_business.locations because it will only search for default locations
has_many :customer_contacts
has_many :employee_job_types
has_many :products
has_many :email_templates
has_many :messages
has_many :text_templates
has_many :job_requests
has_many :appointment_colors
has_many :calls
has_many :forwarding_numbers
has_many :events
has_many :reviews
has_many :published_job_pages
has_one :home_page
belongs_to :subscription, optional: true
#TODO: Temporarily disabling mandatory subscriptions
# validates_presence_of :subscription
belongs_to :link_shortener, optional: true
has_one_attached :logo
has_one_attached :phone_tree_greeting_mp3
has_many_attached :images #these are images that are uploaded directly to the business, then later they will be associated with a customer or job.
has_many :facebook_page_connections, dependent: :destroy
require 'uri'
# Warning!
# Order of statements changes order that models are created.
# Business must be created before employee for logic to work.
# Do NOT reorder the 3 following statements.
before_create :generate_secret_key, :assign_empty_name_if_no_name #secret key is for when our companies go to take new job requests, it's a little more security (not just complete open access to anyone that knows the business id). This way they'll need a special link.
after_create :create_default_groups, :add_conventions_to_new_business, :generate_shortened_url, :send_employee_access_code_via_twilio, :add_demo_data_on_new_registration, :add_auto_responder_sms_for_inbound_calls, :add_to_pipedrive_and_mixmax_welcome_sequence
has_many :employees
accepts_nested_attributes_for :employees
# validates_with EmailValidator
validates_with PhoneValidator #We want to make sure phone is included.. #1 thing to include.
# validates_with ZipCodeValidator
# validates :name, presence: true
# validates :email, uniqueness: true
validates :visitor_device_info_auth_token, uniqueness: true, allow_nil: true
before_save :clean_twilio_number
before_create :default_to_surcharge, if: :empty_surcharge?
before_save :default_app_percentage_fee, if: :empty_app_percentage_fee?
# Creates the default groups (owner and employee) for this business.
def create_default_groups
ActiveRecord::Base.transaction do
# Owners can manage all resources.
owner = Group.create business: self, name: :owner
Permission.create group: owner, resource: :all, action: :manage
# Employees can manage customers, jobs, and line items.
employee = Group.create business: self, name: :Manager
Permission.create group: employee, resource: :Customer, action: :manage
Permission.create group: employee, resource: :Job, action: :manage
Permission.create group: employee, resource: :Group, action: :manage
Permission.create group: employee, resource: :Employee, action: :manage
Permission.create group: employee, resource: :Roster, action: :manage
end
end
def add_conventions_to_new_business
# Conventions: 1 branch for newly created business.
b = self.branches.new(name: self.city&.titleize || "Home")
if b.save
self.employees.first&.update(branch_id: b.id)
end
self.marketing_sources.new(name: "A Friend/Family Member Referred Me").save
self.marketing_sources.new(name: "One of Your Employees Referred Me").save
self.marketing_sources.new(name: "I Googled for Companies and Found You").save
self.marketing_sources.new(name: "I Saw you on Facebook").save
self.marketing_sources.new(name: "Other").save
self.job_statuses.new(status: "Open").save
self.job_statuses.new(status: "Called Back").save
self.job_statuses.new(status: "Closed").save
self.employee_job_types.new(name: "Estimate of Work", description: "This appointment is when we show up to a customer location to estimate the amount of work and price for a potential job").save
self.employee_job_types.new(name: "Work Order", description: "This appointment is when we've agreed to provide work at a certain location for a customer").save
self.employee_job_types.new(name: "Warranty/Callback", description: "This appointment is for when a customer has asked us to come back after a work order has been completed").save
#This is where we can add text message templates to them..
self.text_templates.create(title: "15 Minutes Out", body: "This is {{employee}} from #{self.name}! Just wanted to let you know I'm about 15 minutes away from arriving at {{location}}. See you soon!")
self.text_templates.create(title: "30 Minutes Out", body: "This is {{employee}} from #{self.name}! Just wanted to let you know I'm about 30 minutes away from arriving at {{location}}. See you soon!")
self.text_templates.create(title: "45m-1h Out", body: "This is {{employee}} from #{self.name}! Just wanted to let you know I'm about 45 minutes to 1 hour away from arriving at {{location}}. See you soon!")
self.text_templates.create(title: "Confirm Info", body: "Hi {{contact_first_name}}! This is {{user}} from #{self.name}, just wanted to confirm our appointment at {{location}} for {{day}} {{date}}, somewhere around {{start_time}} to {{end_time}}, does that work for you?")
self.text_templates.create(title: "Reminder", body: "Hi {{contact_first_name}} this is {{user}} with #{self.name}, just wanted to remind you our our appointment for {{readable_start_time}} to {{end_time}} at {{location}}")
self.text_templates.create(title: "Updated Time", body: "Hi {{contact_first_name}}, our schedule has updated today, and your new ETA is: {{readable_start_time}} to {{end_time}}. Does this work for you?")
self.text_templates.create(title: "Google Review Link", body: "Thank you for being our customer. Would you be willing to leave us a Google review? This helps us out a lot. You can Google our name: #{self.name} and leave us a Google review")
self.email_templates.create(name: "Payment Request", body: "
Hi {{name}}, we've attached your invoice here.
To make a payment of your invoice, please follow this link:
{{payment_link}}
You can also pay through writing a check to #{self.name} and mailing it to #{self.street_address} #{self.city}, #{self.zip_code}, #{self.state} to avoid paying a card processing fee.
Best wishes,
#{self.employees.first&.first_name} from #{self.name}
")Thank you for choosing #{self.name}. Attached is your estimate to be approved.
To approve the estimate and proceed with the specified work, please follow this link and submit your endorsement at the bottom of the page.
{{signature_link}}
After approving, we can move forward with scheduling the work!
Thanks,
#{self.employees.first&.first_name} #{self.employees.first&.last_name}
#{self.name}
#{self.employees.first&.email}
#{self.employees.first&.phone}
")Hi {{name}}. Our records indicate that the following invoice has not been paid: {{payment_link}}
To make a payment, please follow the above link and pay with a card.
If our records are incorrect, and this invoice has been paid, please respond to this email directly.
We're hoping to close out this invoice as soon as possible.
Best wishes,
#{self.employees.first&.first_name}
#{self.name}
") self.appointment_colors.create(name: "Estimate", color: "#d8e887")
self.appointment_colors.create(name: "Need to Invoice", color: "#f94343")
self.appointment_colors.create(name: "Invoiced, Unpaid", color: "#fa809e")
self.appointment_colors.create(name: "Invoiced, Paid", color: "#6ef2d1")
# Quickbooks Configuration: Products/Services & Pull in Customers
# Products/Services & Customers must be done manually by authenticating with Quickbooks.
self.products.create(name: "Parts", description: "This is for the parts & supplies used on this job", price: 200.0)
self.products.create(name: "Service Work", description: "This is for the labor used on this job.", price: 200.0)
self.products.create(name: "Discount", description: "Discount Applied to Total", price: 50.0)
unless self.employees.first.nil?
self.employees.first.update(color: "#B0E0E6")
self.update(phone_tree_branch_settings: "[\"#{self.employees.first.phone}\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"]")
end
add_first_customer_and_invoice #This is useful and important for our /integrations page, as we need at least 1 customer with 1 invoice in order to send an email and authenticate gmail. You will get an error if the user doesn't have any invoices on their account and they go to the /integrations page.
set_up_notification_standards
end
def add_demo_data_on_new_registration
# ody = self.customers.new(first_name: "Kason", last_name: "Kendall", phone: "8013499925")
# job_request_ody = self.job_requests.create(business_id: self.id, email: "kasonckendall@gmail.com", first_name: ody.first_name, last_name: ody.last_name, notes: "I am getting ready to sell this rental I have in Woods Cross, but I'm doing a quick remodel before listing it. We need to rip up our upstairs and downstairs carpet and replace it with hardwood flooring. We're looking to move as fast as possible, we want to list the house by the end of May. Can you please provide me an quote? The house is about 2100 square feet", phone: ody.phone, state: "UT", street_address: "1309 S 675 W", zip: "84087", city: "Woods Cross")
add_demo_customers
# add_demo_text_message
# add_demo_phonecalls
# add_demo_jobs_and_appointments
# add_demo_invoices_and_estimates
# # add_demo_messages
# add_demo_job_requests
end
def add_demo_customers
ody = self.customers.create(first_name: "Kody", last_name: "Example", email: "kodykc@gmail.com", phone: "8013499925")
# ike = self.customers.create(first_name: "Mike", last_name: "Example", email: "mike@gmail.com", phone: "8015553333")
# ames = self.customers.create(first_name: "James", last_name: "Example", phone: "8015553331", email: "James@gmail.com")
# osephine = self.customers.create(first_name: "Josephine", last_name: "Example", phone: "8015553332", email: "Josephine@gmail.com")
ody.locations.create(street_address: "130 S 1300 E", city: "Salt Lake", zip_code: "84102", state: "UT")
# ike.locations.create(street_address: "130 S 1300 E", city: "Salt Lake", zip_code: "84102", state: "UT")
# ames.locations.create(street_address: "130 S 1300 E", city: "Orem", zip_code: "84054", state: "UT")
# osephine.locations.create(street_address: "130 S 1300 E", city: "Orem", zip_code: "84054", state: "UT")
j_ody = ody.jobs.create(notes: "You can use this space to enter job notes.", business_id: self.id, location_id: ody.default_location.id)
# j_ike = ike.jobs.create(notes: "You can use this space to enter job notes.", business_id: self.id, location_id: ike.default_location.id)
# j_ames = ames.jobs.create(notes: "You can use this space to enter job notes.", business_id: self.id, location_id: ames.default_location.id)
# j_osephine = osephine.jobs.create(notes: "You can use this space to enter job notes.", business_id: self.id, location_id: osephine.default_location.id)
j_ody_start_time = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_ike_start_time = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_ames_start_time = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_osephine_start_time = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
j_ody_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_ike_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_ames_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_osephine_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(7...14).hours
# j_ody_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_ike_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_ames_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_osephine_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_rt_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_enna_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_onette_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_imona_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_itsue_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_eota_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_age_start_time = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_ody_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_ike_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_ames_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_osephine_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_rt_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_enna_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_onette_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_imona_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_itsue_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_eota_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
# j_age_start_time_2 = (Date.today + rand(-4...3).days).to_time + rand(2...8).hours
j_ody.employee_jobs.create(time_start: j_ody_start_time, time_end: j_ody_start_time + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_ike.employee_jobs.create(time_start: j_ike_start_time, time_end: j_ike_start_time + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_ames.employee_jobs.create(time_start: j_ames_start_time, time_end: j_ames_start_time + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_osephine.employee_jobs.create(time_start: j_osephine_start_time, time_end: j_osephine_start_time + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
j_ody.employee_jobs.create(time_start: j_ody_start_time_2, time_end: j_ody_start_time_2 + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_ike.employee_jobs.create(time_start: j_ike_start_time_2, time_end: j_ike_start_time_2 + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_ames.employee_jobs.create(time_start: j_ames_start_time_2, time_end: j_ames_start_time_2 + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# j_osephine.employee_jobs.create(time_start: j_osephine_start_time_2, time_end: j_osephine_start_time_2 + 2.hours, employee_id: self.employees.first&.id, appointment_color_id: self.appointment_colors.sample.id)
# line_items: [{"name":" ","description":"Test","price":"10.00","product_id":"","quantity":"1"}]
# create_table "payments", force: :cascade do |t|
# t.datetime "paid_on"
# t.integer "payment_type"
# t.bigint "invoice_id"
# t.string "notes"
# t.datetime "created_at", null: false
# t.datetime "updated_at", null: false
# t.float "amount"
# { due_on: @invoice.due_on, issued_on: @invoice.issued_on, sent_on: @invoice.sent_on, invoice_type: @invoice.invoice_type, job_id: @invoice.job_id}}
#add_demo_invoices_and_estimates
j_ody_invoice = j_ody.invoices.create(due_on: j_ody_start_time + 1.week, issued_on: j_ody_start_time, sent_on: j_ody_start_time - 1.day, invoice_type: "Invoice", job_id: j_ody.id)
j_ody_invoice_price = rand(100...1000)
j_ody_invoice.line_items.create(description: j_ody.notes, price: j_ody_invoice_price, quantity: 1)
j_ody_invoice.payments.create(amount: j_ody_invoice_price, paid_on: j_ody_start_time)
# j_ike_invoice = j_ike.invoices.create(due_on: j_ike_start_time + 1.week, issued_on: j_ike_start_time, sent_on: j_ike_start_time - 1.day, invoice_type: "Invoice", job_id: j_ike.id)
# j_ike_invoice_price = rand(100...1000)
# j_ike_invoice.line_items.create(description: j_ody.notes, price: j_ike_invoice_price, quantity: 1)
# # j_ike_invoice.payments.create(amount: j_ike_invoice_price, paid_on: j_ike_start_time)
# j_ames_invoice = j_ames.invoices.create(due_on: j_ames_start_time + 1.week, issued_on: j_ames_start_time, sent_on: j_ames_start_time - 1.day, invoice_type: "Invoice", job_id: j_ames.id)
# j_ames_invoice_price = rand(100...1000)
# j_ames_invoice.line_items.create(description: j_ody.notes, price: j_ames_invoice_price, quantity: 1)
# j_ames_invoice.payments.create(amount: j_ames_invoice_price, paid_on: j_ames_start_time, payment_type: 1)
# j_osephine_invoice = j_osephine.invoices.create(due_on: j_osephine_start_time + 1.week, issued_on: j_osephine_start_time, sent_on: j_osephine_start_time - 1.day, invoice_type: "Invoice", job_id: j_osephine.id)
# j_osephine_invoice_price = rand(100...1000)
# j_osephine_invoice.line_items.create(description: j_ody.notes, price: j_osephine_invoice_price, quantity: 1)
# j_osephine_invoice.payments.create(amount: j_osephine_invoice_price, paid_on: issued_on, payment_type: 1)
# { job_request: { business_id: @job_request.business_id, city: @job_request.city, email: @job_request.email, first_name: @job_request.first_name, last_name: @job_request.last_name, notes: @job_request.notes, phone: @job_request.phone, state: @job_request.state, street_address: @job_request.street_address, zip: @job_request.zip } }
job_request_ody = self.job_requests.create(business_id: self.id, email: ody.email, first_name: ody.first_name, last_name: ody.last_name, notes: "Example — This is what a job request from your customer will look like: (Your customer will leave their notes about the job here).", phone: ody.phone, state: ody.default_location.state, street_address: ody.default_location.street_address, zip: ody.default_location.zip_code, city: ody.default_location.city, customer_id: ody.id)
# job_request_ike = self.job_requests.create(business_id: self.id, email: ody.email, first_name: ike.first_name, last_name: ike.last_name, notes: "Example — This is what a job request from your customer will look like: I was referred to you from a friend who you've done a lot of work for. I wanted to see what it would take to help me with this project?", phone: ike.phone, state: ike.default_location.state, street_address: ike.default_location.street_address, zip: ike.default_location.zip_code, city: ike.default_location.city, customer_id: ike.id)
# job_request_ames = self.job_requests.create(business_id: self.id, email: ody.email, first_name: ames.first_name, last_name: ames.last_name, notes: "Example — This is what a job request from your customer will look like: I have been having issues at my house again, can you please come and help me figure out what we need to fix it?", phone: ames.phone, state: ames.default_location.state, street_address: ames.default_location.street_address, zip: ames.default_location.zip_code, city: ames.default_location.city, customer_id: ames.id)
# job_request_osephine = self.job_requests.create(business_id: self.id, email: ody.email, first_name: osephine.first_name, last_name: osephine.last_name, notes: "Example — This is what a job request from your customer will look like: You provided me a quote 2 weeks ago, and I'm ready to move forward with the project. Can you please get me scheduled to get the work done?", phone: osephine.phone, state: osephine.default_location.state, street_address: osephine.default_location.street_address, zip: osephine.default_location.zip_code, city: osephine.default_location.city, customer_id: osephine.id)
end
def add_demo_jobs_and_appointments
# add 15 demo jobs and appointments, spreading them out over the next 2 weeks
# add an appointment for TODAY'S date, just to try and showcase what the app can do for them.
end
def add_demo_invoices_and_estimates
# add 5 demo invoices and estimates, 2 of each
end
def add_demo_messages
ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8013499925', 'This is an example text message. When you use Field Rocket to text your customers, you easily have access to their customer record, including their job notes, pictures, past and future appointments, estimates, invoices, and more. ', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8013499925', 'This is an example text message: You were out here last week to fix the issue, but theres anothing thing going on now that I need help with. Can you please come out and provide me an quote?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553333', 'This is an example text message: I was referred to you from a friend who youve done a lot of work for. I wanted to see what it would take to help me with this project?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553331', 'This is an example text message: I have been having issues at my house again, can you please come and help me figure out what we need to fix it?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553332', 'This is an example text message: You provided me a quote 2 weeks ago, and Im ready to move forward with the project. Can you please get me scheduled to get the work done?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553333', 'This is an example text message: General Contractor Job: Work with the homeowners to help with your portion of the project, we are trying to get this taken care of as quickly as possible.v98 svv. ', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553334', 'This is an example text message: Hey! Me again. Thanks for your help last week, would you be able to help with another one?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553335', 'This is an example text message: Evaluate & Repair. If the repair is less than $500, then do it. Otherwise, submit a written proposal to the landlord', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553336', 'This is an example text message: Estimate to Rework Entire House.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553337', 'This is an example text message: Hi #{self.employees.first&.first_name}, my Dad recommended you and said youve helped him out a lot. Can you come and take a look at my house, I wanted to see what your opinion was and wanted to move forward with the issue.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553338', 'This is an example text message: Can you please come out and help me fix this issue Im having at my house? Thanks', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '8015553339', 'This is an example text message: Big Job. Needs help with remodeling work as they prepare to sell the home.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Yes! Absolutely. I will get you on my scheudle.', '8013499925', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Yes, what times work for you?', '8015553333', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Ok I am wrapping up a job and on my way soon.', '8015553338', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Yes, I have one more job in front of you today then I will head right out', '8015553331', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Ok', '8015553335', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: What was your address again?', '8015553332', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Hey Im sorry, Im out at least 3 or 4 days. Can I come out next Tuesday?', '8015553339', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, \"to\", sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'This is an example text message: Yes I can do that.', '8015553337', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# # add 10 demo messages, 40 of each with conversations and pictures
end
def add_demo_phonecalls
#7/02/2023: disabling for now because this just confuses the customer:
# add 10 demo phone calls, 20 of each with conversations and pictures
# ActiveRecord::Base.connection.execute("INSERT INTO calls (business_id, \"to\", call_from, caller_name, ai_parsed_customer_name, is_active, ai_parsed_summary, ai_parsed_address, ai_parsed_city, ai_parsed_zip, transcription, start_time, end_time, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', '8015553337', 'Paul English', 'Paul English', false, 'Paul English needs his furnace repaired and has scheduled an appointment for this afternoon between 1 and 3 PM. The furnace is 15 years old, and he provided the technician with his contact information, phone number, and email. The technician will call Paul when he is on his way.', '121 Claremont', 'Bountiful', '84010', 'perform heating and cooling, this is spencer, how can I help you today? Hi I just wanted to see if anybody was available for furnace repair. Um Yeah we can definitely get you scheduled for that. Is your heater not working or just making noise. Yeah um Its got three flashes on the control board says its the pressure switches, open. Uh The pressure switch seems to actually but I dont know if its like stuck in like if it just stays on one side that its not supposed to or not and then the the motor makes a nice like wine when it tries to kick on but it just doesnt really turn over anything. Im just making notes here. Sure can it is like you are getting some heat out of it though? No no heat no heat, yep. Okay how old is the friend is uh 15 years. Okay. It might just be getting time to think about replacing that one because they usually last about 15. Uh whats your name Paul? Oh last name English, like the language. Uh yep. And uh what is your phone number? Okay. Uh Whats a good email to send an invoice for estimates too if we if you do decide to replace that one. Sure its Just my last name with novels. Okay. And where are you located at Paul? Uh One oh in Bountiful. Alright ZIP code 84010 yep. Okay um we could get you in this morning. No hang on a second actually Um we could have you we could get to you this afternoon between noon and three Or we could do come out and get that repaired for you tomorrow between eight and 8:30 which would work best for you. Uh I can do the afternoon afternoon. Yeah. Okay. Okay mm We actually be able to move it closer to 3:00, I just realized that technician has something scheduled at the same time. Uh Okay yeah I mean if it falls back a little bit thats fine too. Okay and he will give you a call when hes on his way. Okay. Alright Paul weve got you down this afternoon between one and three and can will be the one that comes out and uh takes a look at that for you. Alright. Sounds good. Okay anything else for you nope thats good. All right. Have a good day. Yeah thank you. Mhm. Bye bye.', '#{Time.now - 5.minutes}', '#{Time.now}', '#{Time.now - 1.day}', '#{Time.now}')")
# insert_query = <<-SQL
# INSERT INTO calls (business_id, \"to\", call_from, caller_name, ai_parsed_customer_name, is_active, ai_parsed_summary, ai_parsed_address, ai_parsed_city, ai_parsed_zip, transcription, start_time, end_time, created_at, updated_at)
# VALUES (
# #{self.id}, '#{self.twilio_number}', '8015553337', 'Sam Marrs', 'Sam Marrs', false,
# ' Samuel Marrs needs to have a fan of their furnace cleaned. They had a technician come out to do a tune-up earlier this year and he had mentioned that their fan needed to be cleaned. Merrick needs to get the service on Wednesday morning, and the technician Chris will call ahead when he s on his way.',
# '121 Claremont', 'Bountiful', '84010',
# 'perform heating and cooling. This is spencer. How can I help you today? I had someone come out and do a tune up this year and he mentioned that I should have the ban and everything cleaned and so I wanted to get an appointment for that. Okay. Alright, tune up and your name Sam? Marrs? I m sorry I didn t get the first time you broke up Sam and not working at all anymore. Um He is, I m just running the shop while he s out working with the customers. Okay so we can get you in today, probably a little bit later in the afternoon or we could do it tomorrow morning even do you have a preference? Um Hold on a minute. What about Wednesday morning. Good morning. Yeah we can do on Wednesday as well. What time? Uh we d probably get you in between eight and 8:30 AM. Okay. You ll be done when how long does that take um a tune up usually takes about an hour at most. Okay. Sounds good. He s making notes here. This isn t a tune up, this is a cleaning. Oh do you want this service call? Okay this is he s already tuned it up but he said the fan was really dirty and that it should be cleaned the technician that came out and this is a fan for your furnace. Yeah. Okay. And is this still do you still live at 1236 South 400, yep. Okay. Alright I ve got you down uh your technician will probably chris and he ll give you a call when he s on his way. Okay. Thank you, yep. You re welcome. Have a good day.',
# '#{Time.now - 1.day - 3.minutes}', '#{Time.now - 1.day}', '#{Time.now - 1.day}', '#{Time.now}')
# SQL
# ActiveRecord::Base.connection.execute(insert_query)
insert_query = <<-SQL
INSERT INTO calls (business_id, \"to\", call_from, caller_name, ai_parsed_customer_name, is_active, ai_parsed_summary, ai_parsed_address, ai_parsed_city, ai_parsed_zip, transcription, start_time, end_time, created_at, updated_at)
VALUES (
#{self.id}, '#{self.twilio_number}', '8015553337', 'Rachel Example', 'Rachel Example', false,
'This is an Example of a Phone Call from a Customer. Field Rocket records, transcribes, and organizes all your call history so that you never have to worry about missing any details about a job or customer',
'123 Example Drive', 'Salt Lake', '84102',
'This is an Example of a Phone Call from a Customer. Field Rocket records, transcribes, and organizes all your call history so that you never have to worry about missing any details about a job or customer',
'#{Time.now - 2.days - 2.minutes}', '#{Time.now - 2.days}', '#{Time.now - 1.day}', '#{Time.now}')
SQL
ActiveRecord::Base.connection.execute(insert_query)
end
def add_demo_job_requests
# add 10 demo job requests, 5 each with pictures
end
def add_demo_text_message
#TODO: This is challenging because we don't want to trigger callbacks and send a whole bunch of messages out to fake phone numbers when we first create these messages...
# sql = "INSERT INTO messages VALUES "
# sql_values = []
# users.each do |user|
# sql_values << "(#{user.values.join(", ")})"
# end
# sql += sql_values.join(", ")
# ActiveRecord::Base.connection.insert_sql(sql)
# ody = self.customers.find_by(phone: "8013499925")
# ike = self.customers.find_by(phone: "8015553333")
# ames = self.customers.find_by(phone: "8015553331")
# osephine = self.customers.find_by(phone: "8015553332")
# rt = self.customers.find_by(phone: "8015553333")
# enna = self.customers.find_by(phone: "8015553334")
# onette = self.customers.find_by(phone: "8015553335")
# imona = self.customers.find_by(phone: "8015553336")
# itsue = self.customers.find_by(phone: "8015553337")
# eota = self.customers.find_by(phone: "8015553338")
# age = self.customers.find_by(phone: "8015553339")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{ody.phone}', 'You were out here last week to fix the issue, but there's anothing thing going on now that I need help with. Can you please come out and provide me an quote?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{ike.phone}', 'I was referred to you from a friend who you've done a lot of work for. I wanted to see what it would take to help me with this project?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{ames.phone}', 'I have been having issues at my house again, can you please come and help me figure out what we need to fix it?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{osephine.phone}', 'You provided me a quote 2 weeks ago, and I'm ready to move forward with the project. Can you please get me scheduled to get the work done?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{rt.phone}', 'General Contractor Job: Work with the homeowners to help with your portion of the project, we are trying to get this taken care of as quickly as possible.v98 svv. ', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{enna.phone}', 'Hey! Me again. Thanks for your help last week, would you be able to help with another one?', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{onette.phone}', 'Evaluate & Repair. If the repair is less than $500, then do it. Otherwise, submit a written proposal to the landlord', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{imona.phone}', 'Estimate to Rework Entire House.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{itsue.phone}', 'Hi #{self.employees.first&.first_name}, my Dad recommended you and said you've helped him out a lot. Can you come and take a look at my house, I wanted to see what your opinion was and wanted to move forward with the issue.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{eota.phone}', 'Can you please come out and help me fix this issue I'm having at my house? Thanks', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{age.phone}', 'Big Job. Needs help with remodeling work as they prepare to sell the home.', '#{self.twilio_number}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# self.messages.create(business_id: self.id, sent_from: ody.phone, content: "You were out here last week to fix the issue, but there's anothing thing going on now that I need help with. Can you please come out and provide me an quote?"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: ike.phone, content: "I was referred to you from a friend who you've done a lot of work for. I wanted to see what it would take to help me with this project?"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: ames.phone, content: "I have been having issues at my house again, can you please come and help me figure out what we need to fix it?"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: osephine.phone, content: "You provided me a quote 2 weeks ago, and I'm ready to move forward with the project. Can you please get me scheduled to get the work done?"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: rt.phone, content: "General Contractor Job: Work with the homeowners to help with your portion of the project, we are trying to get this taken care of as quickly as possible.v98 svv. "
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: enna.phone, content: "Hey! Me again. Thanks for your help last week, would you be able to help with another one?"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: onette.phone, content: "Evaluate & Repair. If the repair is less than $500, then do it. Otherwise, submit a written proposal to the landlord"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: imona.phone, content: "Estimate to Rework Entire House."
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: itsue.phone, content: "Hi #{self.employees.first&.first_name}, my Dad recommended you and said you've helped him out a lot. Can you come and take a look at my house, I wanted to see what your opinion was and wanted to move forward with the issue."
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: eota.phone, content: "Can you please come out and help me fix this issue I'm having at my house? Thanks"
# self.twilio_number, sent: Time.now - 1.day)
# self.messages.create(business_id: self.id, sent_from: age.phone, content: "Big Job. Needs help with remodeling work as they prepare to sell the home."
# self.twilio_number, sent: Time.now - 1.day)
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Yes! Absolutely. I will get you on my scheudle.', '#{ody.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Yes, what times work for you?', '#{ike.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Ok I am wrapping up a job and on my way soon.', '#{ames.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'I have one more job in front of you today', '#{osephine.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Ok', '#{rt.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'What was your address again?', '#{enna.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Hey I'm sorry, I'm out at least 3 or 4 days. Can I come out next Tuesday?', '#{onette.phone}', '#{Time.now}', '#{Time.now}', '#{Time.now}')")
# ActiveRecord::Base.connection.execute("INSERT INTO messages (business_id, sent_from, content, to, sent, created_at, updated_at) VALUES (#{self.id}, '#{self.twilio_number}', 'Yes I can do that.', '#{eota.phone}', '#{Time.now - 1.day}', '#{Time.now}', '#{Time.now}')")
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: ody.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: ike.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: ames.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: osephine.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: rt.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: enna.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: onette.phone, content: ""
# self.messages.create(business_id: self.id, sent_from: self.twilio_number, to: eota.phone, content: ""
end
def add_first_customer_and_invoice
c = self.customers.create(first_name: "Kody", last_name: "Kendall", email: "kodyckendall@gmail.com")
c.locations.create(street_address: "130 S 1300 E", city: "Salt Lake", zip_code: "84102", state: "UT")
c.direct_invoices.create(notes: "Your first invoice example. Add line items with descriptions and a price!", business_id: self.id)
end
def add_auto_responder_sms_for_inbound_calls
self.update(twilio_missed_call_message: "Hi, this is #{self.employees.first&.first_name} with #{self.name}. If you'd like to set an appointment, click the link to fill in your info: https://rocketr.us#{self.shortened_url}. \r\n\r\nIf you add some detailed notes, I will be able to get you scheduled and helped faster!")
end
def set_up_notification_standards
end
def add_hvac_invoice_templates
end
def add_hvac_products_services
end
# The problem comes from trying to search multiple attributes on a table, (or multiple tables) when you're not sure what the user is intending to search through.
# this method is a catch all to search through all customers and return the results, regardless of what the user input is.
def search_customers(search_term)
search_term = Customer.strip_whitespace_from_numbers_but_not_names(search_term)
full_name = search_term
if full_name == "" || full_name == nil
return []
end
full_name = full_name.upcase
split_name = full_name.split(' ') # This splits the name into first name and last name.. so for "Me Too Inc" where there are multiple spaces in the name, it doesn't work very well.
#we'd have to do multiple combinations with the split_name. If there are spaces in it.
#like -- [0] and [1] and [2] search for all of those first_name LIKE or last_name LIKE or company LIKE
# then -- [0] and [1] and [2]...
#Narrowly search for first name, last name, and company based on order.
targeted_name_search_results = []
has_first_name = split_name[0] != nil
has_last_name = split_name[1] != nil
if has_first_name
if has_last_name
#had two names separated by a space that got pulled out from the full_name.
targeted_name_search_results = self.customers.includes([:locations, :customer_contacts]).where('UPPER(first_name) LIKE ? and UPPER(last_name) LIKE ?', "%#{split_name[0]}%", "%#{split_name[1]}%").limit(10)
else
#only had one name that got broken out..
targeted_name_search_results = self.customers.includes([:locations, :customer_contacts]).where('UPPER(first_name) LIKE ? or UPPER(last_name) LIKE ? or UPPER(company) LIKE ?', "%#{split_name[0]}%", "%#{split_name[0]}%", "%#{split_name[0]}%").limit(10)
end
end
#Broadly search the entire search term for first name, last name and company
agressive_search_name_fields_results = []
if targeted_name_search_results.count == 0
agressive_search_name_fields_results = self.customers.includes([:locations, :customer_contacts]).where('UPPER(first_name) LIKE ? or UPPER(last_name) LIKE ? or UPPER(company) LIKE ?', "%#{full_name}%", "%#{full_name}%", "%#{full_name}%").limit(10)
else
return targeted_name_search_results
end
phone_and_email_search_results = []
couldnt_find_names_so_lets_search_phone_and_email = agressive_search_name_fields_results.count == 0
if couldnt_find_names_so_lets_search_phone_and_email
phone_and_email_search_results = self.customers.includes([:locations, :customer_contacts]).where('UPPER(phone) LIKE ? OR UPPER(email) LIKE ?', "%#{full_name}%", "%#{full_name}%").limit(10)
else
return agressive_search_name_fields_results
end
return phone_and_email_search_results
end
def get_outstanding_invoices
return self.invoices.where('was_paid = 0 and invoice_type = ? and due_on > ?', "Invoice", Date.today).map{|i| i.get_invoice_preview}
end
## Returns all late invoices
def get_late_invoices
return self.invoices.where('was_paid = 0 and invoice_type = ? and due_on <= ?', "Invoice", Date.today).map {|i| i.get_invoice_preview}
end
## Returns the number of paid invoices within the last "weeks_of_paid_invoices" weeks
def get_paid_invoices(weeks_of_paid_invoices)
return self.invoices.where('was_paid > 0 and invoice_type = ? and due_on <= ?', "Invoice", Date.today - weeks_of_paid_invoices.week).map {|i| i.get_invoice_preview}
end
def get_stripe_user_id
if self.stripe_access_code != nil
return JSON.parse(self.stripe_access_code)["stripe_user_id"]
else
return nil
end
end
def has_setup_stripe_payments?
return get_stripe_user_id != nil
end
#Params: auth_code_from_stripe -- the auth code stripe's server sent us after user granted permission on their stripe account
def authenticate_stripe_user_account! (auth_code_from_stripe)
url = URI("https://connect.stripe.com/oauth/token")
params = {"client_secret"=>Rails.configuration.stripe[:secret_key], "code"=>auth_code_from_stripe, "grant_type"=>"authorization_code"}
token_response = Net::HTTP.post_form(url, params)
response = JSON.parse(token_response.body)
if response["access_token"] == nil || response["stripe_user_id"] == nil || response["stripe_user_id"] == nil
return false
else
return self.update(:stripe_access_code => {"access_token" => response["access_token"], "stripe_user_id" => response["stripe_user_id"], "refresh_token" => response["refresh_token"]}.to_json, :stripe_enabled => true)
end
end
def authenticate_quickbooks_account(quickbooks_auth_code, realm_id, csrf_token)
if self.quickbooks_csrf_token != csrf_token
return false
else
quickbooks_response = Quickbooks.exchange_auth_code_for_access_token(quickbooks_auth_code)
return self.update(quickbooks_access_token: quickbooks_response.access_token, quickbooks_realm_id: realm_id, quickbooks_refresh_token: quickbooks_response.refresh_token)
end
end
def use_quickbooks_refresh_token(update_unit_test_scaffold = false)
quickbooks_response = Quickbooks.exchange_refresh_token(self.quickbooks_refresh_token)
puts quickbooks_response
if update_unit_test_scaffold
#overwrite YML business scaffold for the quickbooks unit test.
business_fixture_path = Rails.root + "test/fixtures/businesses.yml"
data = YAML.load_file business_fixture_path
data["one"]["quickbooks_access_token"] = quickbooks_response.access_token
data["one"]["quickbooks_realm_id"] = self.quickbooks_realm_id
data["one"]["quickbooks_refresh_token"] = quickbooks_response.refresh_token
# data["quickbooks_csrf_token"] = quickbooks_response.csrf_token
File.open(business_fixture_path, 'w') { |f| YAML.dump(data, f) }
end
if quickbooks_response && quickbooks_response.access_token && quickbooks_response&.refresh_token
self.update!(quickbooks_access_token: quickbooks_response.access_token, quickbooks_realm_id: self.quickbooks_realm_id, quickbooks_refresh_token: quickbooks_response.refresh_token)
else
Rails.logger.info("Quicbooks response didn't have an access token, or it didn't have a refresh token, or both")
Rails.logger.info("Quickbooks response: #{quickbooks_response}")
end
end
def authenticated_with_quickbooks?(update_business_yml = false)
if self.quickbooks_access_token == nil
return false
elsif Quickbooks.have_access?(self.quickbooks_access_token, self.quickbooks_realm_id)
return true
else
self.use_quickbooks_refresh_token(update_business_yml)
return Quickbooks.have_access?(self.quickbooks_access_token, self.quickbooks_realm_id) #try a second time after refreshing.
end
end
def query_quickbooks(query)
if authenticated_with_quickbooks?
return Quickbooks.raw_query(self.quickbooks_realm_id, self.quickbooks_access_token, query)
end
#queries:
#Business.find(514).quickbooks_query("SELECT * FROM Customer WHERE DisplayName LIKE '%Minko%'") #used to find a customer that didn't get synced in from Joe's
end
def query_quickbooks_tax_codes
if authenticated_with_quickbooks?
Quickbooks.query_tax_codes(self.quickbooks_realm_id, self.quickbooks_access_token)
end
end
# Used to set the tax code ref id that is used for setting the TaxCodeRef when pushing invoice in to Quickbooks.
# Canadian Quickbooks won't work unless this is set for "HST ON".
def set_quickbooks_tax_code_ref_id(id)
self.update(quickbooks_tax_code_ref_id: id)
end
def push_payment_to_quickbooks(payment)
if authenticated_with_quickbooks?
if payment.invoice.customer_to_invoice.quickbooks_id.nil?
payment.invoice.customer_to_invoice.push_customer_to_quickbooks!
end
return Quickbooks.push_payment(payment.invoice, payment.amount, self.quickbooks_realm_id, self.quickbooks_access_token)
end
end
def import_quickbooks_customers!
if !self.authenticated_with_quickbooks? #Don't try to import customers if this self isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return #TODO: A similar guard is being used multiple times. It would be good to extract this into a helper.
end
s3_xml_filename = save_quickbooks_customers_xml_to_s3
import_qb_customers_xml_from_s3_to_quickbooks(s3_xml_filename)
end
def save_quickbooks_customers_xml_to_s3
if !self.authenticated_with_quickbooks? #Don't try to import customers if this self isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return #TODO: A similar guard is being used multiple times. It would be good to extract this into a helper.
end
Rails.logger.info "Fetching customers from quickbooks.. for #{self.name}"
quickbooks_customers = Quickbooks.fetch_all_customers(self.quickbooks_realm_id, self.quickbooks_access_token)
filename = "#{self.name}_#{self.id}_#{Date.today.strftime("%H:%M-%m-%d-%y")}.xml"
if quickbooks_customers
xml_content = quickbooks_customers.to_xml # Convert the Nokogiri::XML::Document to a string
AWS.upload_xml_to_s3(xml_content, filename)
return filename
else
return nil
end
end
def import_qb_customers_xml_from_s3_to_quickbooks(s3_filename)
return unless s3_filename
# 1. Fetch the XML from S3
xml_content = AWS.fetch_xml_from_s3(s3_filename)
# Check if the content was fetched successfully
return unless xml_content
# 2. Parse the XML
parsed_xml = Nokogiri::XML(xml_content, &:noblanks).remove_namespaces!
# 3. Iterate through the XML and create customers in quickbooks
parsed_xml.xpath("//Customer").each do |customer|
# Create the customer in Quickbooks using the extracted attributes
create_customer_from_quickbooks_xml(customer)
end
end
def create_customer_from_quickbooks_xml(customer)
#Handling the first/last names.. This might be off a little bit.
first_name = nil
last_name = nil
if customer.xpath("GivenName").text.empty? && customer.xpath("FamilyName").empty? && customer.xpath("CompanyName").empty?
first_name = customer.xpath("DisplayName").text.split(" ")[0]
last_name = customer.xpath("DisplayName").text.split(" ")[1]
else
first_name = customer.xpath("GivenName").text
last_name = customer.xpath("FamilyName").text
end
c = self.customers.new(:first_name => first_name,
:last_name => last_name,
:phone => phone_from_customer_xml(customer),
:company => customer.xpath("CompanyName").text,
:notes => "Imported from Quickbooks", #TODO: Not sure what to put here.
:email => customer.xpath("PrimaryEmailAddr/Address").text,
:quickbooks_id => customer.xpath("Id").text)
c.instance_variable_set(:@skip_quickbooks_update, true)
unless c.valid? #don't save phone or email if it's causing validation errors and not saving correctly.
c.phone = nil if c.errors.include?(:phone)
c.email = nil if c.errors.include?(:email)
end
if c.save
c.locations.create(
:city => customer.xpath("BillAddr/City").text,
:state => customer.xpath("BillAddr/CountrySubDivisionCode").text,
:street_address => customer.xpath("BillAddr/Line1").text,
:zip_code => customer.xpath("BillAddr/PostalCode").text,
)
else
Rails.logger.error "Failed to save customer with email #{c.email} and phone: #{c.phone}. Errors: #{c.errors.full_messages}. XML: #{customer.to_xml}"
# raise "Failed to save customer with email #{c.email} and phone: #{c.phone}. Errors: #{c.errors.full_messages}. XML: #{customer.to_xml}"
end
end
#In Quickbooks, There can also be a Mobile/FreeFormNumber, or a AlternatePhone/FreeFormNumber. We should check for these too.
def phone_from_customer_xml(customer)
phone_types = ["PrimaryPhone", "Mobile", "AlternatePhone"]
phone_types.each do |phone_type|
phone = customer.xpath("#{phone_type}/FreeFormNumber").text
return phone unless phone.empty?
end
nil
end
def import_unsynced_quickbooks_customers
business_to_sync = self
# business_to_sync = Business.find(471)
# All of the customer's Quickbooks IDs
customers_1_in_qb = Nokogiri::XML(business_to_sync.query_quickbooks("SELECT * from Customer MaxResults 1000")).remove_namespaces!
customers_2_in_qb = Nokogiri::XML(business_to_sync.query_quickbooks("SELECT * from Customer STARTPOSITION 1000 MaxResults 1000")).remove_namespaces!
qb_ids = customers_1_in_qb.xpath('//Customer/Id').map(&:text) + customers_2_in_qb.xpath('//Customer/Id').map(&:text)
# Customer's Quickbooks ID for customers in Field Rocket:
customers_in_fieldrocket = business_to_sync.customers.where.not(quickbooks_id:nil).map{|c| c.quickbooks_id}
ids_in_qb_but_not_in_fr = qb_ids - customers_in_fieldrocket
# Combine both XML data for ease of processing
combined_qb_customers = customers_1_in_qb.xpath('//Customer') + customers_2_in_qb.xpath('//Customer')
ids_in_qb_but_not_in_fr.each do |id|
# Find the customer node with the specified ID
customer_node = combined_qb_customers.find { |node| node.at_xpath('./Id').text == id }
# Extract additional customer data. Adjust this part according to the data structure and what you need.
customer_data, location_data = QuickbooksHelper.parse_customer_xml(customer_node)
puts customer_data
puts location_data
#multiple emails?
email = customer_data[:email]
#split email on ,
#if multiple emails, then create a customer for each email
#if single email, then create a single customer
if email.include?(",")
emails = email.split(",")
first_email = emails.first
customer_data[:email] = first_email
end
customer = business_to_sync.customers.create(customer_data)
if customer.valid?
customer.locations.create(location_data)
end
end
end
def import_quickbooks_products_services!
if !self.authenticated_with_quickbooks? #Don't try to import products if this self isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return #TODO: A similar guard is being used multiple times. It would be good to extract this into a helper.
end
s3_xml_filename = save_quickbooks_products_services_xml_to_s3
import_qb_products_xml_from_s3_to_quickbooks(s3_xml_filename)
end
def save_quickbooks_products_services_xml_to_s3
if !self.authenticated_with_quickbooks? #Don't try to import customers if this self isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return #TODO: A similar guard is being used multiple times. It would be good to extract this into a helper.
end
Rails.logger.info "Fetching products/services from quickbooks.. for #{self.name}"
quickbooks_products = Quickbooks.fetch_all_items(self.quickbooks_realm_id, self.quickbooks_access_token)
filename = "#{self.name}_#{self.id}_products-services-#{Date.today.strftime("%H:%M-%m-%d-%y")}.xml"
if quickbooks_products
xml_content = quickbooks_products.to_xml # Convert the Nokogiri::XML::Document to a string
AWS.upload_xml_to_s3(xml_content, filename)
return filename
else
return nil
end
end
def import_qb_products_xml_from_s3_to_quickbooks(s3_filename)
return unless s3_filename
# 1. Fetch the XML from S3
xml_content = AWS.fetch_xml_from_s3(s3_filename)
# Check if the content was fetched successfully
return unless xml_content
# 2. Parse the XML
parsed_xml = Nokogiri::XML(xml_content, &:noblanks).remove_namespaces!
# 3. Iterate through the XML and create customers in quickbooks
parsed_xml.xpath("//Item").each do |item|
product = self.products.new(
name: item.xpath("Name").text,
description: item.xpath("Description")&.text,
price: item.xpath("UnitPrice")&.text&.to_f,
SKU: item.xpath("Sku")&.text,
quickbooks_id: item.xpath("Id")&.text)
product.save
end
end
def clear_quickbooks_credentials
self.update(quickbooks_realm_id: nil, quickbooks_access_token: nil, quickbooks_csrf_token: nil, quickbooks_refresh_token: nil)
end
def default_to_surcharge #default we should surcharge. Learned this the hard way with Kyler. (had to pay $53.73 refund because we told him it would surcharge his customer, then it didn't.)
self.surcharge_customer = true #don't pass stripe + app fee onto customer by default, make business pay it
end
def default_app_percentage_fee
self.app_percentage_fee = 0.01 #charge 0.1 percent on all transactions for Field Rocket by default
end
def empty_surcharge?
self.surcharge_customer.blank?
end
def empty_app_percentage_fee?
self.app_percentage_fee.blank?
end
def employees_to_notify_for_job_requests
notifications_lists.where(:notification_type => "JobRequest").map{|notification| notification.employee} #Get all the employees
end
def invoices
Invoice.where("business_id = #{self.id} or job_id in (select id from jobs where jobs.business_id = #{self.id})")
end
def enable_seo_posts
# Check if the 'enable_seo_posts' column exists in the 'businesses' table
if ActiveRecord::Base.connection.column_exists?(:businesses, :enable_seo_posts)
# Return the value of 'enable_seo_posts' if the column exists
self[:enable_seo_posts]
else
# Return false if the column does not exist
false
end
end
def toggle_contacted!
if contacted_at.nil?
update(contacted_at: Time.current)
else
update(contacted_at: nil)
end
end
#This is for the job request url
def full_notification_url
"#{ENV["BASE_URL"]}/job_requests/new?secret_key=#{self.secret_key}"
end
def shortened_url #rocktr.co/l/1
if link_shortener
"/l/#{self.link_shortener_id}"
else
nil
end
end
def settings
{"gps_prompts_user_to_clock_in": self.gps_prompts_user_to_clock_in,
"is_processing_enabled": self.payroc_ibx_enabled,
"user_needs_tutorial": self.employees.first&.user_needs_tutorial,
"id": self.id,
"gmail_enabled": self.employees.any? { |employee| !employee.gmail_token.nil? }, #If this company isn't authenticated with gmail, then we want to default to sending an invoice email via text message, and not sending it through gmail. Our logic here is if there's any employees who are authenticated with gmail, then let's have the whole company marked as sending invoices via the Gmail API.
"poll_server_to_refresh_unread_message_count": self.poll_server_to_refresh_unread_message_count || false,
"manually_outbound_call": self.manually_outbound_call_feature_flag || false,
"enable_sales_tax_calculation": self.enable_sales_tax_calculation || false,
"hide_address_fields": self.hide_job_request_address_fields || false
}
end
def as_json(*)
super.tap do |hash|
hash["new_employee_access_code"] = new_employee_access_code
hash["first_employee_created_id"] = self.employees.first&.id
hash["shortened_url"] = shortened_url
hash["is_processing_enabled"] = self.payroc_ibx_enabled
hash["is_premium"] = self.subscription_id != nil
hash["logo_url"] = self.logo.attached? ? self.logo.service_url : nil
hash["stripe_access_code"] = "FILTERED"
hash["quickbooks_realm_id"] = "FILTERED"
hash["quickbooks_access_token"] = "FILTERED"
hash["quickbooks_refresh_token"] = "FILTERED"
hash["quickbooks_csrf_token"] = "FILTERED"
hash["ibx_merchant_token"] = "FILTERED"
hash["payroc_password"] = "FILTERED"
hash["payroc_user"] = "FILTERED"
hash["app_percentage_fee"] = "FILTERED"
hash["wordpress_username"] = "FILTERED"
hash["wordpress_auth_token"] = "FILTERED"
hash["wordpress_url"] = "FILTERED"
hash["settings"] = self.settings
end
end
#DRY -- Don't Repeat Yourself
def new_employee_access_code #Only good for first verification process, then expires.
self.employees.first&.access_token
end
def time_zone
if self["timezone"].blank?
"America/Denver"
else
self["timezone"]
end
end
#This is for the job request id.
def generate_shortened_url
l = LinkShortener.create(url: full_notification_url) #Leave key off for now.
self.update(link_shortener_id: l.id)
end
# pass in an array of valid phone numbers.
def set_new_phone_tree_branch_settings(phone_numbers_in_order)
serialized_phone_tree_settings = phone_numbers_in_order.to_json
Rails.logger.info("Now setting #{self.name}'s phone tree settings as: #{serialized_phone_tree_settings}")
self.update(phone_tree_branch_settings: serialized_phone_tree_settings)
end
def get_phone_number_from_branch_input_gather(gathered_input_number)
if self.phone_tree_branch_settings.nil?
return ""
end
if gathered_input_number <= 0
gathered_input_number = 1
end
deserialized_phone_tree_branch_settings = JSON::parse(self.phone_tree_branch_settings)
if gathered_input_number > deserialized_phone_tree_branch_settings.size
gathered_input_number = deserialized_phone_tree_branch_settings.size
end
converted_index = gathered_input_number - 1 # if user types in 1, then we want to get the first number in the settings, but the first number would be 0 since we're working with an array., etc.
found_number = deserialized_phone_tree_branch_settings[converted_index]
if found_number.nil? || found_number.empty?
return nil
end
return found_number
end
def get_default_forward_number
first_number = get_phone_number_from_branch_input_gather(1)
if not ValidatorHelper.valid_phone_format(first_number)
#get first employee's phone number and forward it there.
return self.employees.first.phone
else
return first_number
end
end
def display_address
return "#{self.street_address}, #{self.city}, #{self.state}, #{self.zip_code}"
end
def get_unread_messages_count
self.messages.where(has_been_read: false).count
end
def toggle_suppress_analytics
self.update(suppress_analytics: !self.suppress_analytics)
end
def readable_survey_answers
# 0 = "Invoicing"
# 1 = "Estimates"
# 2 = "Scheduling"
# 3 = "Other"
answer = self.survey_answers
answer&.gsub!("0", "Invoicing")
answer&.gsub!("1", "Estimates")
answer&.gsub!("2", "Scheduling")
answer&.gsub!("3", "Other - ")
return answer
# return self.survey_answers.map{|answer| ["Invoicing", "Estimates", "Scheduling", "Other"][answer]}
end
def get_appointments_within_24_hours
# we want to make sure only to send it to the customer once.
#We're actually going to query for 30 hours in advance, because if this message gets sent out at 7PM EST, we want it to include appointments up to 1AM (in case they have an appointment scheduled for after 7PM the next day.)
#Or if it goes out at 5PM MST, (because it's -7 UTC), then we want it to get any appointments up to 11PM the next day, etc.
# appointments = EmployeeJob.where(business_id: self.id, time_start: Time.current..(Time.current + 30.hours)) #see above comment to explain why we're doing 30 hours in advance.
# upcoming_appointments = EmployeeJob.includes(job: :customer).where(business_id: self.id, time_start: Time.current..(Time.current + 30.hours))
upcoming_appointments = EmployeeJob.includes(:job => :customer)
.joins(:job)
.where(jobs: {business_id: self.id}, time_start: Time.current.getlocal("-06:00")..(Time.current.getlocal("-06:00") + 30.hours))
# merge appointments that have the same customer ID, so we don't accidentally send multiple reminder messages to the same customer
unique_appointments = upcoming_appointments.uniq { |appointment| appointment.job.customer_id }
end
def send_daily_appointment_reminders_through_sms!
appointments = self.get_appointments_within_24_hours
appointments.each do |appointment|
appointment.send_appointment_reminder!
end
end
def add_to_pipedrive
#if Rails.env.production?
begin
Pipedrive.add_contact_and_organization_to_pipedrive(self)
rescue => e
# Handle the exception
Rails.logger.error "Error adding to Pipedrive: #{e.message}"
# Optionally, you can retry the operation or take additional actions
end
#end
end
def pipedrive_check_auth
Pipedrive.check_authentication
end
def pipedrive_get_users
Pipedrive.get_users
end
#new_registration stage id => 13
def pipedrive_get_stages
Pipedrive.get_stages
end
# Helper method to extract image metadata and URL
def extract_image_data(image)
{
image: image,
content_type: image.blob.content_type,
id: image.id, # it's often useful to also send the ID
filename: image.blob.filename.to_s,
content_type: image.blob.content_type,
url: image.service.send(:object_for, image.key).public_url
}
end
private
def assign_empty_name_if_no_name
if self.name == nil
self.name = "" #assign empty name.
end
end
def generate_secret_key
self.secret_key = SecureRandom.uuid
self.twilio_mobile_client_name = self.secret_key.tr("-", "") #Set the client_name to the same as the secret key, and let's see if it works.
end
def clean_twilio_number
self.twilio_number = self.twilio_number&.delete('^0-9')
end
def send_employee_access_code_via_twilio
if Rails.env.production? #Don't want to send access codes unless we're in production.
Twilio.send_verification_token(self.phone)
# Twilio.send_text(self.phone, "Welcome to Field Rocket! Let's get you started. Your access code is: #{self.new_employee_access_code}") #Sends via universal twilio number
#Send to Kody
Twilio.send_text('8013499925', "New Mobile Sign Up, phone: #{self.phone}, business: #{self.name}, first name: #{self.employees.first&.full_name}, survey: #{self.readable_survey_answers}") #sends text message to Kody to notify of new mobile registration.
#Send to Mike Phelps
Twilio.send_text('8019063575', "New Mobile Sign Up, phone: #{self.phone}, business: #{self.name}, first name: #{self.employees.first&.full_name}, survey: #{self.readable_survey_answers}") #sends text message to Mike to notify of new mobile registration.
#Send to Darren David Spencer
Twilio.send_text('3472859599', "New Mobile Sign Up, phone: #{self.phone}, business: #{self.name}, first name: #{self.employees.first&.full_name}, survey: #{self.readable_survey_answers}") #sends text message to Kody to notify of new mobile registration.
# TODO: We want to enroll them in an automatic SMS campaign.. how do we do this?
# We'd have to build in a feature to add in an SMS campaign, and then enroll them in it.?
# What about a generic campaign?
end
end
def add_to_pipedrive_and_mixmax_welcome_sequence
if Rails.env.production? #Don't want to enroll a new user unless we're in production.
Mixmax.enroll_in_welcome_campaign(self.email)
self.add_to_pipedrive
end
end
def create_homepage_header_with_claude_ai
# I have existing HTML that we need to modify for a different company. Please keep everything the same other than the following information: Business name: Kite Lawn & Landscape. Phone Number: (765) 422-2544. Owner Name: Blake. His business does Landscaping Services. Please keep all images the same OTHER than the logo with the URL: https://app.fieldrocket.us/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBb1FsIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--6ce4981fa6a65341c8a45bbe62a6f19b4e9dae2d/Screenshot%202024-06-18%20at%203.52.58%20PM.png
# Here is the HTML:
#
#
#
#
# prompt = """please turn this navbar HTML to work for a company named: VP Construction, his name is Brock, his phone number is: (832) 586-6432.
#
#
We provide expert lawn care and landscaping services throughout Indiana.
Maintain a healthy, manicured lawn with our reliable mowing services
We offer regular lawn mowing services to keep your grass looking its best all season long.
Transform your outdoor space with our expert landscaping services
Our landscaping services include design, installation, and maintenance of plants, trees, and hardscapes.
Enjoy a tidy, debris-free yard with our thorough cleanup services
We offer yard cleanup services to remove leaves, branches, and other debris, keeping your property looking its best.
Ensure your lawn and plants receive the proper hydration with our irrigation services
Our irrigation services include installation, maintenance, and repair of sprinkler systems to keep your lawn and plants healthy.
Walk the Line Lawncare is a leading lawn care and landscaping company serving the state of Indiana. Founded by Nate, an experienced professional with a passion for creating beautiful outdoor spaces and delivering exceptional customer service, Walk the Line Lawncare has built a reputation for providing high-quality workmanship and reliable service.
Our team of skilled technicians is dedicated to enhancing your property's curb appeal and ensuring your lawn and landscape thrive. Whether you need regular lawn mowing, landscaping design and installation, or irrigation services, we have the expertise to meet your needs.
Ready to enhance your property's appearance? Contact us today for a free estimate and consultation.
class Event < ApplicationRecord
after_create :assign_business_id
belongs_to :employee, optional: true
def assign_business_id
if !employee_id.nil?
self.update(business_id: Employee.find(employee_id).business_id)
end
end
def as_json(*)
super.tap do |hash|
hash["business_name"] = employee_id.blank? ? (business_id.blank? ? '' : Business.find(business_id).name) : Employee.find(employee_id).business.name
hash["employee_name"] = employee_id.blank? ? '' : Employee.find(employee_id).full_name
end
end
end
class Employee < ApplicationRecord
include Rails.application.routes.url_helpers
require 'digest/md5'
include ApplicationHelper
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :invitable, :database_authenticatable, :registerable, :recoverable, :rememberable #:validatable
belongs_to :business
belongs_to :group, optional: true # Hack to make accepts_nested work.
belongs_to :branch, optional: true
has_many :employee_jobs
has_many :jobs, through: :employee_jobs
has_many :rosters, through: :employee_rosters
has_many :messages
has_many :device_tokens
has_many :timesheets
has_many :events
has_one_attached :avatar
# validates_with FirstLastNameValidator
validates_with AppointmentFilterValidator, if: -> {!self.appointment_filter.blank?}
validates_with PhoneValidator
validates :color, :css_hex_color => true, if: -> { !color.nil? }
# validates :email, uniqueness: true
# From Devise module Validatable: https://github.com/heartcombo/devise/blob/main/lib/devise/models/validatable.rb
#https://stackoverflow.com/a/24678462/6374022
validates_presence_of :email, if: :email_required?
validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
validates_format_of :email, with: /\A[^@\s]+@[^@\s]+\z/, allow_blank: true, if: :email_changed?, message: "Email is not in the correct format.. make sure it is in the format of 'youremail@email.com'"
validates_presence_of :password, if: :password_required?
validates_confirmation_of :password, if: :password_required?
validates_length_of :password, within: 6..128, allow_blank: true
#end devise validatable
# Here is where we need to figure out how to handle the phone number. They either need a phone number, or an email, but they don't need both.
before_create :assign_owner, if: :first_employee?
# before_create :assign_empty_name, if: -> {self.name.blank?}
before_save :set_color_to_nil, if: -> {self.color&.downcase == "#ffffff" || self.color&.downcase == "#000000" }
after_create :create_access_token
after_create :assign_to_default_group, if: -> {self.group_id == nil}
scope :workers, -> (business) { joins(:group).where(business: business).where.not("groups.name = 'owner'") }
scope :active, -> { where(deactivated_at: nil) }
scope :not_active, -> { where.not(deactivated_at: nil) }
# WARNING: This wsa a failed attempt to use Rails scoping feature for the marketing_va, but it's not working as intended. When used in the employees#index controller, it's converting the SQL statement to be employees.marketing_va != TRUE, which is wrong.
# That statement would exclude values where it's null. I don't know why the combination of these scopes and Ransack are causing the SQL statement to be wrong, but for now
# we are going to manually query it from the employees#index controller
# scope :not_marketing_va, -> { where("employees.marketing_va = FALSE OR employees.marketing_va IS NULL") }
# #TODO: Using this scope would be cleaner, but it was causing an issue with the employees_controller route .json for some reason.
# scope :not_marketing_va, -> { where(marketing_va: [nil, false]) } #This is being set up because we will be hiring out of country marketing VA's from the phillipines, and we want to make sure they don't have unfettered access to our user's data with ability to delete data, edit data, or otherwise harm our system if they go rogue.
# # we want them to have read only access to the marketing dashboard /marketing, the list of invoices/jobs with pictures, read only access to customer info, view and edit ability for published job SEO pages, etc.
def after_database_authentication
#Tracking employee login event
date_time_now = DateTime.now
Analytics.identify( user_id: self.id,
traits: { name: self.full_name, email: self.email, created_at: date_time_now }
)
Analytics.track( user_id: self.id, event: 'Signed In', properties: { plan: 'Employee' })
event_information = {
user_id: self.id,
name: self.full_name,
email: self.email,
signed_in_at: date_time_now,
event: 'Signed In'
}
log_tracked_event(event_information)
end
def full_name
"#{first_name} #{last_name}".titleize
end
def as_json(*)
super.tap do |hash|
hash["full_name"] = self.full_name
hash["avatar_path"] = avatar_path
hash["authentication_token"] = "FILTERED"
end
end
def avatar_path
self.avatar.attached? ? rails_blob_path(self.avatar.blob, only_path: true) : ActionController::Base.helpers.asset_path('avatar.png')
end
# Finds all employee jobs from range startt to endt.
# Returns as JSON formatted object, with fields necessary for
# full calendar.
# startt: Date that is formatted as string.
# endt: Date that is formatted as string.
# returns: JSON list of jobs this employee is scheduled for between the startt and endt
def get_employee_dates_from_range(startt, endt)
employee_ids_to_render = get_non_filtered_employees
start_date = Date.parse(startt)
end_date = Date.parse(endt)
events = EmployeeJob.includes([:appointment_color, :employee, :employee_job_type]).includes(job:[:customer, :job_status, :location]).where(employee_id:[employee_ids_to_render]).where("time_start >= ?", start_date).where("time_end <= ?", end_date).load
events = events.map {|s| {
id: s.id,
title: s.job.customer.full_name,
start: s.time_start.strftime('%Y-%m-%dT%H:%M'),
end: s.time_end.strftime('%Y-%m-%dT%H:%M'),
resourceId: s.employee_id,
customerId: s.job.customer.id,
jobId: s.job.id,
employeeId: s.employee_id,
color: s.appointment_color&.color || s.employee.color || s.job.job_status&.color || '#7ec2cc',#Here's where we look at the appointment color.
borderColor: s.border_color || "",
eventHoverText: s.appointment_type_name == 'No Type' ? "#{s.job.customer.full_name} at #{s.job.display_address}" : "#{s.appointment_type_name} for #{s.job.customer.full_name} at #{s.job.display_address}"
}
}
end
def is_owner?
group.owner?
end
def set_appointment_filter(idsToView)
self.appointment_filter = idsToView.to_s
self.save!
end
def should_employee_show_on_filter?(employee)
if employee.branch_id.nil?
return false
end
return (self.appointment_filter.blank? || self.appointment_filter.split(',').include?(employee.id.to_s))
end
#Gets the employees that are saved as a filter here.
def get_non_filtered_employees
if self.appointment_filter.blank?
return business.employees.where.not(branch_id: nil)
else
employee_ids = self.appointment_filter.split(',').sort()
employees = []
employee_ids.each do |eid|
if business.employee_ids.include? eid.to_i #We are doing this check because if an employee is deleted and not cleared out of every filter, then this throws an error when the database can't find the given employee id for this business.
employees << self.business.employees.where.not(branch_id: nil).find(eid) #we don't want to render employees that don't have a branch
end
end
return employees
end
end
def gmail_oauth_authorized?
return self.gmail_token != nil
end
def remove_employee_id_from_filter(id_to_remove)
id_to_remove = id_to_remove&.to_i
ids_from_filter = self.appointment_filter.split(',').sort().map{|id| id&.to_i}
# remove specific id from filter, and re-save
ids_from_filter.delete(id_to_remove)
set_appointment_filter(ids_from_filter.join(','))
end
def set_refreshed_gmail_access_token!(refreshed_access_token)
gmail_token = JSON.parse(self.gmail_token)
gmail_token["access"] = refreshed_access_token #Overwrite old access token with new one
self.update(:gmail_token => gmail_token.to_json)
end
# Finds the phone number who last responded to a sms from this employee, or the phone number this employee last texted (if no customer responded to an sms from this employee).
# Used to route an inbound sms sent FROM the employee's phone into our system, and now needs to be forwarded to the appropriate phone number who last was messaged/messaged back with this employee.
# If this returns nil, it means that this employee has not yet texted this customer, or that the employee hasn't messaged anyone yet.
def phone_number_who_last_texted_this_employee
number = self.messages.where.not(sent_from: business.twilio_number).last&.sent_from #find this employees messages that were not sent OUT to a customer, but sent FROM a customer, and find the last one.
if number == nil
number = self.messages.last&.to
end
return number
end
def register_for_job_request_notifications
self.business.notifications_lists.create(employee_id: self.id, notification_type: "JobRequest")
end
def register_for_call_received_notification
self.business.notifications_lists.create(employee_id: self.id, notification_type: "ReceiveCallModifiedEvent")
end
def register_for_all_messages_received_notification
self.business.notifications_lists.create(employee_id: self.id, notification_type: "AnyInboundSMSReceivedNotification")
end
def unregister_for_job_request_notifications
self.business.notifications_lists.where(employee_id: self.id, notification_type: "JobRequest").delete_all
end
def unregister_for_call_received_notification
self.business.notifications_lists.where(employee_id: self.id, notification_type: "ReceiveCallModifiedEvent").delete_all
end
def unregister_for_all_messages_received_notification
self.business.notifications_lists.where(employee_id: self.id, notification_type: "AnyInboundSMSReceivedNotification").delete_all
end
def registered_notifications_summary
self.business.notifications_lists.where(employee_id: self.id)
end
def active_for_authentication?
super and self.is_active?
end
def is_active?
self.deactivated_at.blank?
end
def set_active!
self.update!(deactivated_at: nil)
end
def set_deactive!
self.update!(deactivated_at: Time.now.strftime("%Y-%m-%d %H:%M:%S"))
end
# Business.find(188).employees.first.create_authentication_token_and_activate_account!
def create_authentication_token_and_activate_account!
srand.to_s.last(30) #Super weird, but when you first run this line, it only generates 5 digits, regardless of the last(n) param. So let's run it once, so that we clear out that issue, then we actually get 6 digits on our next generation.
authentication_token = srand.to_s.last(30)
self.update(authentication_token: authentication_token, verified: DateTime.now)
create_access_token #we consumed this access token, so we need to recreate it.
end
private
#generate a 6 digit numerical string
def create_access_token
srand.to_s.last(6) #Super weird, but when you first run this line, it only generates 5 digits, regardless of the last(n) param. So let's run it once, so that we clear out that issue, then we actually get 6 digits on our next generation.
access_token = srand.to_s.last(6)
self.update(access_token: access_token)
end
def assign_to_default_group
self.update(group_id: self.business.groups.first.id)
end
def email_regexp
/\A[^@\s]+@[^@\s]+\z/
end
def password_required?
false
end
def email_required?
false
end
# def assign_empty_name_if_no_name
# self.name = ""
# end
def assign_owner
# First group is the owner group.
self.group = business.groups.first
end
def first_employee?
business.employees.first == self
end
def set_color_to_nil
self.update(color: nil)
end
end
class Subscription < ApplicationRecord
# require 'twilio'
include Twilio
validates_presence_of :email
validates_presence_of :key
has_one :business
# after_create :notify_kody, if: -> {Rails.env.production?} # Only send Kody a text if someone on a production environment signs up for a subscription.
# def notify_kody # I want to know if/when people sign up so I can send them a welcome email and help onboard them.
# Twilio.send_text("8013499925", "#{self.email} just subscribed to HomeService.Cloud with key: #{self.key}")
# end
end
class NotificationsList < ApplicationRecord
#current notifications are:
# ReceiveCallModifiedEvent
# JobRequest
# AnyInboundSMSReceivedNotification
# SignatureCollected
belongs_to :business
belongs_to :employee
end
class Job < ApplicationRecord
belongs_to :business
belongs_to :customer
belongs_to :customer_contact, optional: true
belongs_to :location, optional: true
belongs_to :owner, class_name: "Employee", optional: true
belongs_to :job_request, optional: true
belongs_to :job_status, optional: true
has_many :employee_jobs
has_many :employees, through: :employee_jobs
has_many_attached :images, dependent: :detach #WARNING: This must have dependent: :detatch, or else not all images that are uploaded will be saved. See: https://stackoverflow.com/questions/66373019/is-it-possible-to-prevent-activestorage-from-deleting-file-on-service-storage, https://github.com/rails/rails/issues/36374, and: https://claude.ai/chat/d13db9fa-d533-4457-a2ad-90abc4d38cf4,
has_many :invoices, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :timesheets, dependent: :nullify
accepts_nested_attributes_for :employee_jobs
accepts_nested_attributes_for :customer
accepts_nested_attributes_for :location
after_create :set_default_job_status
validates_with JobValidator
def update_job_and_time(to_update)
employee_job_base = to_update["job"]["employee_job_attributes"]["0"]
employee_job = employee_jobs.where(:employee_id => employee_job_base["employee_id"], :time_start => employee_job_base["time_start"], :time_end => employee_job_base["time_end"])
end
def total_time_tracked_towards_this_job
return Timesheet.sum_duration_of_timesheets(self.timesheets)
end
def title
self["title"] || "Job #{self.id}"
end
def first_name
self.customer.first_name
end
def last_name
self.customer.last_name
end
def delete_all_appointments
self.employee_jobs.each do |ej|
ej.destroy
end
end
def google_maps_link
self.location&.google_maps_link
end
def get_time_clocked_into_this_specific_job(employee_id) #MUST NOT INCLUDE the duration of any timesheets STILL CURRENTLY CLOCKED IN.
#Has to be in UTC because that's what the time gets saved in the database.
timesheets_for_this_job_today = Timesheet.where(clocked_in: DateTime.now.utc().to_date.all_day, job_id: self.id, employee_id: employee_id) #if clocked out is nil, it means it's an active timesheet which we don't want to include!
timesheets_for_this_job_today = timesheets_for_this_job_today.where.not(clocked_out: nil)
return Timesheet.sum_duration_of_timesheets(timesheets_for_this_job_today)
end
def get_clock_in_time_for_this_job_if_active(employee_id)
ts = timesheets_for_this_job_today = Timesheet.where(clocked_in: DateTime.now.utc().to_date.all_day, clocked_out: nil, job_id: self.id, employee_id: employee_id)
if !ts.empty?
return ts.first.clocked_in
else
return nil
end
end
def is_employee_tracking_time_against_this_job_currently?(employee_id)
tsheets = Timesheet.where(employee_id: employee_id, job_id: self.id, clocked_out: nil)
return tsheets.count != 0
end
def as_json(*)
# @job = Job.includes([{customer: :marketing_source}, :location, :customer_contact, :owner, :job_status, :invoices, :employee_jobs, :business]).where(id: self.id)
# byebug #this as_json is just a lot.
super.tap do |hash|
hash["title"] = self.title
hash["customer_first_name"] = self.customer&.first_name
hash["customer_last_name"] = self.customer&.last_name
hash["customer_full_name"] = self.customer&.full_name
hash["display_address"] = self.display_address
hash["street_address"] = self.location&.street_address
hash["city"] = self.location&.city
hash["zip_code"] = self.location&.zip_code
hash["state"] = self.location&.state
hash["customer_contact_id"] = self.customer_contact&.id
hash["customer_contact_full_name"] = self.customer_contact&.full_name
hash["owner_full_name"] = self.owner&.full_name
hash["job_status_name"] = self.job_status&.status
hash["invoices"] = self.invoices.includes([:invoice_to, :job_business, :direct_customer, :line_items, :link_shortener])
hash["invoices_for_customer"] = get_latest_three_invoices
hash["appointments"] = self.employee_jobs.includes([:appointment_color, :employee, :employee_job_type])
hash["location"] = self.location
hash["customer_job_index"] = find_customer_job_index
hash["job_title"] = job_title
hash["request_id"] = job_request_id
hash["marketing_source_id"] = self.customer.marketing_source_id
hash["marketing_source_name"] = self.customer.marketing_source&.name
hash["business_twilio_number"] = self.business.twilio_number
end
end
def as_json_lite
{'id' => id, 'location_id' => location_id, 'customer_id' => customer_id}.to_json
end
def get_latest_three_invoices
# byebug
return customer.invoices.includes([:invoice_to, :job_business, :direct_customer, :line_items, :link_shortener]).last(3)
end
def find_customer_job_index
customer.jobs.each_with_index do |value, index|
if value == self
return index + 1
end
end
return nil
end
def job_title
return "Job #{customer.id}-#{find_customer_job_index}"
end
#TODO: Is there a way to move these methods to some other base class so customer.rb and job.rb can extend the class and share this code?
def display_address
return self.location&.display_address
end
def street_address
return self.location&.street_address
end
def city
return self.location&.city
end
def zip_code
return self.location&.zip_code
end
def state
return self.location&.state
end
def set_border_color!
amount_owed, number_invoices = self.amount_owed_and_number_invoices
this_job_has_no_invoices = number_invoices == 0
if this_job_has_no_invoices
self.update(border_color: nil) #if there's no invoices, then let's keep the default color.
elsif (amount_owed > 0)
self.update(border_color: "red") #red if there's invoices/outstanding balance that hasn't been paid yet
else
self.update(border_color: "#009e28") #red if there's invoices/outstanding balance that hasn't been paid yet
end
end
def amount_owed
amount_owed = self.invoices.where(invoice_type: "Invoice").sum{|invoice| invoice.amount_due}
end
def amount_owed_and_number_invoices
invoices_for_job = self.invoices.where(invoice_type: "Invoice").load
amount_owed = invoices_for_job.sum{|invoice| invoice.amount_due}
return amount_owed, invoices_for_job.size
end
def set_default_job_status
if self.job_status.nil? && !self.business.job_statuses.first.nil?
self.update(:job_status_id => self.business.job_statuses&.first&.id)
end
end
def notify_customer_of_appointment_time(employee_notifying)
sms_body = sms_appointment_notification_body(employee_notifying)
if customer.phone
business.messages.create(to: customer.phone, sent_from: business.twilio_number, content: sms_body, has_been_read: false, employee_id: employee_notifying&.id)
Rails.logger.info("Now notifying customer #{customer.first_name} #{customer.last_name} #{customer.id} of their appointment. To: #{customer.phone}. From #{business.twilio_number}. Body: #{sms_body}")
end
end
def sms_appointment_notification_body(employee_notifying)
sms_body = "Hi #{customer.first_name}, this is #{employee_notifying&.first_name} from #{business.name}, just wanted to let you know that I put you in my schedule for an appointment on #{employee_jobs.first.time_start.strftime("%A %B %d")} between #{employee_jobs.first.time_start.strftime("%l:%M%p")} to #{employee_jobs.first.time_end.strftime("%l:%M%p")}"
end
# def optimize_image_for_seo(image_blob, keywords)
# 1. fetch entire blob from S3
# 2. determine it's current file format
# 3. convert to .jpg format (especially if it's an .heic file)
# 4. fetch this job's address information, and get the latitude and longitude
# 5. geo-encode the metadata with the latitude and longitude
# 6. choose smart keywords, insert keywords as tags into the image metadata
# 7. choose a smart, seo optimized name and alt-tag
# 8. re-upload to S3 with all the seo-optimizations
# 9. return the https URL to be displayed
# end
def convert_to_job_request!
if self.job_request.nil?
job_request_parmas = {
customer_id: self.customer_id,
location_id: self.location_id,
notes: self.notes,
business_id: self.business_id,
phone: self.customer&.phone,
email: self.customer&.email,
first_name: self.customer&.first_name,
last_name: self.customer&.last_name,
marketing_sources_id: self.customer&.marketing_source_id,
street_address: self.location&.street_address,
city: self.location&.city,
zip: self.location&.zip_code,
state: self.location&.state
}
@job_request = self.business.job_requests.new(job_request_parmas)
@job_request.save!(validate: false) # This skips the validations, but not the callbacks
self.destroy!
return @job_request
else
@job_request = self.job_request
self.destroy!
return @job_request
end
end
end
class AppointmentColor < ApplicationRecord
belongs_to :business
end
class EmployeeJob < ApplicationRecord
include ActionView::Helpers::DateHelper
include GoogleCalendarApi
require "google/apis/calendar_v3"
require 'action_view'
include ApplicationHelper
belongs_to :employee
belongs_to :job
belongs_to :employee_job_type, optional: true
belongs_to :appointment_color, optional: true
validates_with JobTimeValidator
after_create :push_to_google_calendar, if: :should_push_to_google_calendar?
before_update :handle_employee_change_for_google_calendar_events, if: :will_save_change_to_employee_id?
after_update :update_google_calendar_event, if: :should_update_to_google_calendar?
after_destroy :delete_google_calendar_event_with_current_employee, if: :should_delete_google_calendar_event_after_destroy?
def end
format_datetime(time_end)
end
def start
format_datetime(time_start)
end
def end_short
short_format_datetime(time_end)
end
def start_short
short_format_datetime(time_start)
end
def as_json(*)
super.tap do |hash|
hash["appointment_type_name"] = self.appointment_type_name
hash["appointment_type_id"] = self.employee_job_type&.id
hash["employee_name"] = self.employee_name
hash["employee_first"] = self.employee&.first_name
hash["formatted_start"] = self.start
hash["formatted_end"] = self.end
hash["contact_first_name"] = self.job.customer_contact&.first_name
hash["contacts_phone"] = self.job.customer_contact&.phone || self.job.customer.phone
hash["customer_phone"] = self.job.customer.phone
hash["color"] = self.appointment_color&.color || self.employee&.color || self.job&.job_status&.color
hash["color_id"] = self.appointment_color&.id
hash["customer_email"] = self.job.customer.get_best_email_from_customer
hash["twilio_enabled"] = !self.job.business.twilio_number.nil?
hash["google_maps_link"] = self.google_maps_link
end
end
def appointment_type_name
self.employee_job_type&.name.blank? ? "No Type" : self.employee_job_type&.name
end
def duration
return time_end - time_start
end
def duration_readable
distance_of_time_in_words(duration)
end
def employee_name
self.employee&.full_name
end
def sales_rep
self.job.owner&.full_name
end
def title
self.job&.title
end
def status
self.job&.job_status&.status
end
def customer_name
self.job&.customer&.full_name
end
def address
self.job&.location&.display_address
end
def border_color
job&.border_color || nil
end
def invoice_total
job.invoices.where(invoice_type: "Invoice").sum(:total_price)
end
def self.to_csv
attributes = %w{appointment_type_name title status employee_name sales_rep duration_readable start customer_name address invoice_total end}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |user|
csv << attributes.map{ |attr| user.send(attr) }
end
end
end
def push_to_google_calendar #Here is where we can push a job into Google Calendar..
Thread.new do
Rails.application.executor.wrap do
client, service = get_google_calendar_client_service(employee)
result = service.insert_event('primary', google_calendar_event_from_this_appointment)
Rails.logger.info("Added Google Calendar Event to User's Calendar")
update_column :google_calendar_id, result.id #Using update_column prevents it from triggering an after_update of before_update callback. Doing it this way to prevent infinite triggering.
rescue Google::Apis::AuthorizationError
refresh_access_token(employee, client)
retry
end
end
end
def update_google_calendar_event
Thread.new do
Rails.application.executor.wrap do
client, service = get_google_calendar_client_service(employee)
service.update_event('primary', google_calendar_id, google_calendar_event_from_this_appointment)
Rails.logger.info("Updated Google Calendar Event")
rescue Google::Apis::AuthorizationError
refresh_access_token(employee, client)
retry
end
end
end
def google_maps_link
if employee.use_apple_maps
Rails.logger.info("Using apple maps!")
return self.job&.location&.apple_maps_link
else
return self.job&.location&.google_maps_link
end
end
def format_readable_date
# return "#{format_datetime(self.time_start)} to #{short_format_datetime(self.time_end)}"
# return "#{human_readable_date_time(self.time_start, self.job.business)}"
if time_start.to_date == Date.today.to_date #seriously.. not saving in mountain time
time_start.strftime("Today, %l:%M%p")
elsif time_start.to_date == Date.yesterday.to_date
time_start.strftime("Yesterday, %l:%M%p")
elsif time_start.to_date == Date.tomorrow.to_date
time_start.strftime("Tomorrow, %l:%M%p")
elsif (date > Date.today - 7) && (date < Date.yesterday)
time_start.strftime("%a, %l:%M%p")
else
time_start.strftime("%b %-d, %l:%M%p")
end
end
def send_appointment_reminder!
business = self.job.business
#create a message.
customer_phone_number = self.job.customer&.phone
business_twilio_number = business.twilio_number
#Now, do variable evaluation with this appointment.
#Replace anything with {{}} with the variable name
if !business_twilio_number.blank? && !customer_phone_number.blank?
message_content = "Upcoming appointment reminder from #{business.name}, you have a scheduled appointment on #{format_date(self.time_start)}"
unless business.automated_appointment_sms_reminder_template.blank? #let the business customize their own message if they want.
#evaluate message_content
message_content = evaluate_appointment_reminder_message_with_variables
end
# Hello {{customer_first_name}} start_time
# This is a reminder that you are scheduled for an appointment on {Date} {Time} As a reminder, we do not require you to be home just ensure that all materials needed for the appointment are out, gate is unlocked, and the bill has been paid. Service will not happen if payment isn’t made when you are not home. Check your email for the invoice.
# Thank You
# Cincinnati Pool Divers
# human_readable_date_time(time_start)
# human_readable_date_time(time_end)
# b.update(automated_appointment_sms_reminder_template: "Hello {{customer_first_name}}. This is a reminder that you are scheduled for an appointment on {{date}}, around {{start_time}}. As a reminder, we do not require you to be home just ensure that all materials needed for the appointment are out, gate is unlocked, and the bill has been paid. Service will not happen if payment isn’t made when you are not home. Check your email for the invoice. Thank You, Cincinnati Pool Divers")
unless customer_phone_number.blank?
Message.create(business_id: business.id, sent_from: business_twilio_number, to: customer_phone_number, content: message_content)
end
end
end
def evaluate_appointment_reminder_message_with_variables
#{{start_time}} #{{customer_first_name}} #{{day}} #{{date}}
message = self.job&.business&.automated_appointment_sms_reminder_template
message = message.gsub("{{customer_first_name}}", self.job&.customer&.first_name)
message = message.gsub("{{date}}", format_date(self.time_start))
message = message.gsub("{{start_time}}", time_start.strftime("%l:%M %P"))
return message
# return self.job&.business&.automated_appointment_sms_reminder_template&.gsub!("{{customer_first_name}}", self.job&.customer&.first_name)&.gsub!("{{date}}", format_date(self.time_start))&.gsub!("{{start_time}}", time_start.strftime("%l:%M %P"))#format_readable_date)#human_readable_date_time(time_start, self.job.business))
end
private
# Day, Month/Day/Year at Hour:Minute AM/PM
def format_datetime(dt)
dt&.strftime('%a, %B %d, %Y, %l:%M %P')
end
def short_format_datetime(dt)
dt&.strftime('%a %m/%d/%y, %l:%M %P')
end
def format_date(dt)
dt&.strftime('%m/%d/%y')
end
def google_calendar_event_from_this_appointment
Google::Apis::CalendarV3::Event.new({
start: Google::Apis::CalendarV3::EventDateTime.new(date_time: (time_start + utah_timezone_adjustment).rfc3339),
end: Google::Apis::CalendarV3::EventDateTime.new(date_time: (time_end + utah_timezone_adjustment).rfc3339),
summary: "#{title} for #{job.customer.full_name}",
location: "#{job.location&.display_address}",
description: "#{job.notes} | #{ENV["BASE_URL"]}/schedule?appointment=#{id}"
})
end
# If it hasn't been pushed to Google Calendar yet
# If the employee is authenticated with Google Calendar
def should_push_to_google_calendar?
google_calendar_id.nil? && !employee.google_calendar_token.nil?
end
# If this event has been pushed to google calendar
def should_update_to_google_calendar?
#If this event exists on a Google Calendar, and if the current assigned employee is signed into Google Calendar
!google_calendar_id.nil? && !employee.google_calendar_token.nil?
end
def handle_employee_change_for_google_calendar_events
previous_employee = job.business.employees.find(employee_id_was)
Rails.logger.info("The employee ID was changed on an employee_job from #{previous_employee.full_name} to #{employee.full_name}")
#Deleting Portion
if !previous_employee.google_calendar_token.nil? # If this event was never pushed into Google Calenadr, no need to delete it at all.
if previous_employee.google_calendar_token.nil? # If previous employee didn't have Google Calendar, no need to delete the event from their calendar.
pass
else #The previous employee has this calendar on their schedule, so we need to remove it for them.
delete_google_calendar_event(previous_employee)
end
end
if !employee.google_calendar_token.nil? #If new employee isn't authenticated with Google Calendar, don't push the event to their calendar
#If new employee is authenticated with Google Calendar, push the event to their calendar.
push_to_google_calendar
end
end
def should_delete_google_calendar_event_after_destroy?
!google_calendar_id.nil? && !employee.google_calendar_token.nil?
end
def delete_google_calendar_event_with_current_employee
Rails.logger.info("Google Calendar, deleting the Google Calendar event that was previoius assinged to: #{self.employee.full_name}")
delete_google_calendar_event(self.employee)
end
def delete_google_calendar_event(employee)
#Delete the google calendar event if it exists
if !google_calendar_id.nil? && !employee.google_calendar_token.nil?
Thread.new do
Rails.application.executor.wrap do
client, service = get_google_calendar_client_service(employee)
service.delete_event('primary', google_calendar_id)
rescue Google::Apis::AuthorizationError
refresh_access_token(employee, client)
retry
end
end
end
end
def get_google_calendar_client_service(employee)
client = Signet::OAuth2::Client.new(GoogleCalendarApi.client_options)
client.update!(JSON.parse(employee.google_calendar_token))
service = Google::Apis::CalendarV3::CalendarService.new
service.authorization = client
return client, service
end
def utah_timezone_adjustment
# TODO: These are being saved as UTC times.. time_start and time_end are off because they're being saved as UTC in GMT timezone.
# adjusting for UTC to GMT. This is a bad hack. Will break our software in other time zones. We would have to do this adjustment for all Time Zones depending on what the company's timezone is..
# If it's MST, we adjust by adding + 7. If it's MDT, we adjust by adding + 6.
utah_current_time_zone = DateTime.new(2000, Date.today.month, Date.today.day) > DateTime.new(2000, 4, 14) && DateTime.new(2000, 11, 14) ? "MDT" : "MST"
utah_timezone_adjustment = "MDT" ? 6.hours : 7.hours
end
def refresh_access_token(employee, client)
response = client.refresh! #Authorization Error... Doesn't return a refresh token!!
merged_google_calendar_token = JSON.parse(employee.google_calendar_token).merge(response) #Because .refresh! doesn't return a refresh token, we have to merge in with the old JSON to keep the refresh token for future use, we can't just simply update it.
employee.update(google_calendar_token: merged_google_calendar_token.to_json)
end
end
class Customer < ApplicationRecord
include Quickbooks
include QuickbooksHelper
require 'csv'
# ASSOCIATIONS
belongs_to :business
belongs_to :marketing_source, optional: true
belongs_to :location, optional: true
has_many :jobs
has_many :job_requests
has_many :locations
has_many :customer_contacts
has_many :job_invoices, class_name: "Invoice", source: :invoices, through: :jobs
has_many_attached :images
# _______________________________________
# VALIDATIONS:
validates_with FirstLastNameValidator, if: -> { self.company.blank? } # Don't need to have first or last name if the customer has a company.
validates_with EmailValidator, if: -> { !self.email.blank? }
validates_with PhoneValidator, if: -> { !self.phone.blank? }
validates :company, presence: true, if: -> { self.full_name.blank? }
validates_numericality_of :quickbooks_id, if: -> {!self.quickbooks_id.nil?}
validate :phone_number_cannot_be_same_as_twilio_number
# _______________________________________
# CALLBACKS:
after_create :add_default_contact_as_themself, :push_customer_to_quickbooks!
after_update :update_quickbooks_customer
before_validation :strip_email_white_space
after_initialize :init
after_save :set_location_record_as_default, if: :saved_change_to_location_id
# _______________________________________
# OTHERS
accepts_nested_attributes_for :locations, reject_if: lambda {|attributes| attributes['street_address'].blank?}
# _______________________________________
def init
self.quickbooks_sync_token ||= 0
end
def full_name
if "#{first_name} #{last_name}".blank?
return company
else
return "#{first_name} #{last_name}"
end
end
def as_json(*) #https://github.com/rails/rails/pull/2200#issuecomment-3131652
super.tap do |hash|
hash["full_name"] = self.full_name
hash["first_name"] = self.first_name
hash["last_name"] = self.last_name
hash["default_location_display"] = self.default_location&.display_address
hash["default_location"] = self.default_location
hash["default_location_id"] = self.default_location&.id
hash["default_customer_contact"] = self.default_contact&.full_name
hash["default_customer_contact_id"] = self.default_contact&.id
hash["marketing_source"] = MarketingSource.find_by(id: self.marketing_source_id) || "Unknown"
hash["marketing_source_name"] = self.marketing_source&.name
hash["locations"] = self.locations
#hash["jobs"] = self.jobs.includes([:customer_contact, :job_status, :location, :owner, :business, :employee_jobs])
#hash["invoices"] = self.invoices.includes([:invoice_to, :job_business, :line_items, :payments, :link_shortener, :direct_customer])
hash["email"] = self.get_best_email_from_customer #will sometimes return a a contact's email if the current customer email is blank
end
end
def as_json_lite
{'id' => id, 'default_location' => default_location, 'default_location_display' => default_location&.display_address, 'locations' => locations, 'phone' => phone, 'full_name' => full_name, 'email' => email, 'first_name' => first_name, 'last_name' => last_name}.to_json
end
def get_customer_info_for_page
return {
invoices: self.invoices.includes([:invoice_to, :job_business, :line_items, :link_shortener, :direct_customer]),
jobs: self.jobs.includes([:customer_contact, :job_status, :location, :owner, :business]),
images: self.get_image_paths
}
end
def get_image_paths
images_paths = []
self.images.each do |i|
images_paths << i.service.send(:object_for, i.key).public_url
end
self.jobs.each do |j|
j.images.includes(:blob).each do |i|
images_paths << i.service.send(:object_for, i.key).public_url
end
end
self.jobs.includes([:job_request]).each do |j|
if j.job_request.present?
j.job_request&.images.includes(:blob).each do |i|
images_paths << i.service.send(:object_for, i.key).public_url
end
end
end
self.invoices.each do |invoice|
invoice.images.includes(:blob).each do |i|
images_paths << i.service.send(:object_for, i.key).public_url
end
end
return images_paths
end
#This function only gets called for the get_customer_info_for_page.
def get_images
images_paths = []
self.images.each do |i|
images_paths << extract_image_data(i)
end
self.jobs.each do |j|
j.images.includes(:blob).each do |i|
images_paths << extract_image_data(i)
end
end
self.jobs.includes([:job_request]).each do |j|
if j.job_request.present?
j.job_request&.images.includes(:blob).each do |i|
images_paths << extract_image_data(i)
end
end
end
self.invoices.each do |invoice|
invoice.images.includes(:blob).each do |i|
images_paths << {image: i, url: i.service.send(:object_for, i.key).public_url}
end
end
return images_paths
end
# Helper method to extract image metadata and URL
def extract_image_data(image)
{
image: image,
content_type: image.blob.content_type,
id: image.id, # it's often useful to also send the ID
filename: image.blob.filename.to_s,
content_type: image.blob.content_type,
url: image.service.send(:object_for, image.key).public_url
}
end
def all_images
images = self.images
self.jobs.each do |j|
images << j.images
if j.job_request
images << j.job_request.images
end
end
self.invoices.each do |i|
images << i.images
end
return images
end
def attach_image_from_message_to_customer_record(message_id)
# message_id = params[:message_id]
msg = Message.find(message_id)
self.images.attach(msg.image.blob)
end
def default_contact
self.customer_contacts&.first
end
def default_location
if self.location.nil?
self.locations.first
else
self.location
end
end
#TODO: Is there a way to move these methods to some other base class so customer.rb and job.rb can extend the class and share this code?
def display_address
return self.default_location&.display_address
end
def street_address
return self.default_location&.street_address
end
def city
return self.default_location&.city
end
def zip_code
return self.default_location&.zip_code
end
def state
return self.default_location&.state
end
def self.import(file, business_id)
CSV.foreach(file.path, headers: true) do |row|
puts row.to_s
location_attributes = {
street_address: row["street_address"],
city: row["city"],
zip_code: row["zip_code"],
state: row["state"]
}
# Create the new customer
customer_attributes = row.to_hash
new_customer = Business.find(business_id).customers.create(first_name: row["first_name"], last_name: row["last_name"], phone: row["phone"], email: row["email"], company: row["company"], notes: row["notes"])
#add location to customer
if new_customer.valid?
new_customer.locations.create(location_attributes)
end
end
end
def google_maps_link
return self.default_location&.google_maps_link
end
def turn_empty_state_to_nil
if self.state.blank?
self.state = nil
end
end
def find_latest_employee_job
jobs.reverse().each do |j|
ej = j.employee_jobs&.last
if !ej.nil?
return ej
end
end
end
def all_invoices
return self.invoices.or(self.business.invoices.where(invoice_to_id: self.id))
end
def invoice_total
all_invoices.where(invoice_type: 'Invoice').sum(:total_price)
end
def estimate_total
all_invoices.where(invoice_type: 'Estimate').sum(:total_price)
end
def all_locations
locations.map{|location| location.display_address}
end
def marketing_name
self.marketing_source&.name
end
def self.to_csv
attributes = %w{first_name last_name city phone company notes state street_address zip_code email marketing_name invoice_total estimate_total all_locations}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |user|
csv << attributes.map{ |attr| user.send(attr) }
end
end
end
def push_customer_to_quickbooks!
if self.quickbooks_id != nil || !self.business.authenticated_with_quickbooks? #Don't try to push customer if this business isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return
end
Rails.logger.info "Pushing customer: '#{full_name}' to quickbooks"
customer_quickbook_id = Quickbooks.push_customer(self, self.business.quickbooks_realm_id, self.business.quickbooks_access_token)
if customer_quickbook_id != nil
return self.update(quickbooks_id: customer_quickbook_id)
else
return false
end
end
def update_quickbooks_customer
return if @skip_quickbooks_update
Rails.logger.info "Updating customer to quickbooks called!"
if !self.business.authenticated_with_quickbooks? || self.quickbooks_id == nil #Don't try to push customer if this business isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
Rails.logger.info "Wasn't successfully authenticated with Quickbooks"
return
end
Rails.logger.info "Updating customer: '#{full_name}, #{id}' to quickbooks"
Quickbooks.update_customer(self)
end
#This method queries for the customer record inside Quickbooks, and then checks the phone number and address that's in Quickbooks, and updates if needed.
def resync_quickbooks_customer
if !self.business.authenticated_with_quickbooks? || self.quickbooks_id == nil #Don't try to push customer if this business isn't set up for quickbooks, also we check for nil because don't to add a duplicate customer into quickbooks.
return
end
Rails.logger.info "Resyncing customer: '#{full_name}, #{id}' to quickbooks"
customer_xml = self.business.query_quickbooks("SELECT * FROM Customer WHERE Id = '#{self.quickbooks_id}'")
parsed_customer_xml = Nokogiri::XML(customer_xml, &:noblanks).remove_namespaces!.xpath("//QueryResponse").xpath("Customer")
customer_hash, location_hash = QuickbooksHelper.parse_customer_xml(parsed_customer_xml)
self.update(customer_hash)
unless self.customer_contacts.first&.nil?
self.customer_contacts.first.update(phone: customer_hash[:phone])
end
if self.location.nil?
self.locations.create(location_hash)
else
self.location.update(location_hash)
end
end
# This updates an attribute on the location record to set it as "default" location, which helps us prevent an n+1 loading error
# by not having to query the customers database for each location.
def set_location_record_as_default
unless location_id_before_last_save.nil?
Location.find(location_id_before_last_save).update(is_billing_address: false)
end
unless location.nil?
location.update(is_billing_address: true)
end
end
# This is so that ransacker can search on this "full_name" attribute. Using Arel node for database.
ransacker :full_name do |parent|
Arel::Nodes::NamedFunction.new('CONCAT_WS', [
Arel::Nodes.build_quoted(' '), parent.table[:first_name], parent.table[:last_name]
])
end
# CALLBACKS
def add_default_contact_as_themself
self.customer_contacts.new(:first_name => self.first_name,
:last_name => self.last_name,
:phone => self.phone,
:email => self.email,
:business_id => self.business.id
).save!
end
def strip_email_white_space
self.email = email&.strip
end
def invoices #TODO: We need to find a better way to do this. Too much data is coming through here.
Invoice.where("customer_id = #{self.id} or job_id in (select id from jobs where jobs.customer_id = #{self.id})")
# invoices = Invoice.joins([:job]).joins([:job_business]).where(customer_id: self.id)
# my_invoices = Invoice.where(customer_id: self.id).includes(:job)
# job_invoices = Job.where(customer_id: self.id).joins(:invoices)
# return my_invoices << job_invoices
# my_invoices = self.direct_invoices
# job_invoices = Job.includes(:invoices).where("customer_id = ?", self.id)
# my_invoices<
class WebPageHistory < ApplicationRecord
belongs_to :web_page
end
class VisitorDeviceInfo < ApplicationRecord
belongs_to :business
#TODO: How do we attribute this properly? Link to customer record? Link to job post, etc?
def get_ip_addresses_from_job_request(job_request_id)
VisitorDeviceInfo.where("url LIKE ?", "%job_requests/#{job_request_id}%").map{|info| info.ip_address}
end
end
class LineItemTemplate < ApplicationRecord
has_one :business, through: :line_item_template
before_save :sanitize_description
def sanitize_description
self.description = HtmlSanitizer.strip_html_tags_and_entities(self.description)
end
def sanitize
sanitize_description
save
end
end
class EmployeeRoster < ApplicationRecord
belongs_to :employee
belongs_to :roster
end
class Roster < ApplicationRecord
belongs_to :business
has_many :roster_jobs
has_many :jobs, through: :roster_jobs
has_many :employee_rosters
has_many :employees, through: :employee_rosters
accepts_nested_attributes_for :employee_rosters
# Given an integer, get a deterministic color as a string that is compatible with full calendar colors.
def get_color_from_id(num)
colors = ["#CEE5F2", "#ACCBE1", "#7C98B3", "#637081", "#536B78", "#75B8C8", "#77CBB9", "#506C64", "#CDD3D5", "#A6B1E1", "#DCD6F7"]
return colors[num % colors.length]
end
def color()
return get_color_from_id(self.id)
end
def get_roster_dates_from_range(startt, endt)
start_date = Date.parse(startt)
end_date = Date.parse(endt)
schedule = RosterJob.where(:roster_id => self.id, :date => start_date..end_date)
schedule_json = []
schedule.each do |s|
job = Job.find(s.job_id)
if job.job_status == 'Cancelled'
next
end
customer = Customer.find(job.customer_id);
json = JSON.parse(s.to_json)
json["title"] = "#{customer.first_name} :#{s.id}"
json["color"] = get_color_from_id(self.id)
json["start"] = json.delete("time_start")#rename "time_start" to "start"
json["end"] = json.delete("time_end")#rename "time_end" to "end"
json["customer_id"] = job.customer_id
json["job_id"] = job.id
json["event_type"] = "roster"
json = json.to_json
schedule_json.append(json)
end
return schedule_json
end
end
class JobStatus < ApplicationRecord
belongs_to :business
validates :status, presence: true
validates :color, :css_hex_color => true
before_validation :assign_default_color, if: :color_nil?
#Used if the user didn't specify a color
def assign_default_color
self.color = "#" + Digest::MD5.hexdigest(self.status)[0..5]
end
def color_nil?
self.color.nil?
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def formatted_error_messages
return self.errors.full_messages.map{|e| "#{self.class.name} Error: #{e}"}
end
end
class ApiKey < ActiveRecord::Base
before_create :generate_access_token
before_create :set_expiration
belongs_to :employee
def expired?
DateTime.now >= self.expires_at
end
private
def generate_access_token
begin
self.access_token = SecureRandom.hex
end while self.class.exists?(access_token: access_token)
end
def set_expiration
self.expires_at = DateTime.now+30
end
end
class Group < ApplicationRecord
before_destroy :check_for_employees
belongs_to :business
has_many :employees
has_many :permissions, dependent: :destroy
validates :business_id, presence: :true
validates :name, uniqueness: {scope: :business_id}
scope :employee_groups, -> (business) { where(business: business).where.not(name: :owner) }
def owner?
name == 'owner'
end
private
def check_for_employees
return true unless employees.any?
errors.add(:base, 'Employees are assigned to this group.')
false
throw :abort
end
end
class HomePage < ApplicationRecord
belongs_to :business
has_many :reviews
# https://chatgpt.com/c/67b03f72-5558-4b6c-a9cc-3b1d3642905d
def chatbot
# ChatGPT Prompts:
# You are a chatbot living on the site of 2-Tile, a Utah based handyman who specializes in tiling work, especially Kitchen and Bathroom Remodel jobs. Your job is to engage with website visitors in the chat interface, to help "capture the lead". Your goal is first to capture their phone number and name, then their city, and then to ask them questions to fully understand the type of job they need help with, along with getting them to upload pictures of their job. You will then move to schedule an appointment to come out to their house in order to get a fuller measurement/understanding of their job. Please begin by sending them a message trying to engage them by offering value, such as answering any tiling questions they might have. You start.
# ChatGPT
# Hi there! Looking to update your kitchen or bathroom tiles? I'm here to help with any questions you have! What's your project about?
# User
# Kitchen Remodel
# ChatGPT
# Great choice! A kitchen remodel can really transform your space. Do you have a specific style or tile type in mind? Also, could I get your name and phone number to help us keep in touch as we discuss your project?
# User
# My name is Kody, phone is 801-349-9925. I'm thinking of redoing my countertop and thinking about doing a backsplash with tile
# ChatGPT
# Thanks for sharing that, Kody! Redoing your countertop and adding a tiled backsplash sounds fantastic. What city are you located in? This will help us plan for a possible site visit to get a better sense of the space and what might work best.
# User
# I'm in Bountifiul
# ChatGPT
# Perfect, Bountiful is a beautiful area! For your countertop and backsplash, do you have any colors or materials in mind? Also, it would be great if you could upload pictures of your current kitchen setup. This helps us understand the space better and tailor our suggestions to your needs.
end
def display_info
"#{business_id} - #(#{domain})"
end
end
class CustomerContact < ApplicationRecord
belongs_to :customer
belongs_to :business
has_many :jobs
validates :customer_id, presence: true
validates_with PhoneValidator, if: -> { !self.phone.blank? }
def as_json(*)
super.tap do |hash|
hash["full_name"] = self.full_name
end
end
def full_name
return "#{self.first_name} #{self.last_name}"
end
# This is so that ransacker can search on this "full_name" attribute. Using Arel node for database.
ransacker :full_name do |parent|
Arel::Nodes::NamedFunction.new('CONCAT_WS', [
Arel::Nodes.build_quoted(' '), parent.table[:first_name], parent.table[:last_name]
])
end
end
class Invoice < ApplicationRecord
require 'gmail_sender'
require 'securerandom'
include GmailSender
include ActionView::Helpers::NumberHelper
belongs_to :job, optional: true
belongs_to :link_shortener, optional: true
belongs_to :invoice_to, class_name: "Customer", optional: true #Sometimes, we need to invoice a different customer still, like for when the customer is a contractor that subcontracted a job out but our business wants the homeowner/resident to be a customer in our system.
belongs_to :direct_customer, class_name: "Customer", foreign_key: :customer_id, optional: true
belongs_to :direct_business, class_name: "Business", foreign_key: :business_id, optional: true
belongs_to :invoice_template, optional: true
has_one :job_customer, class_name: "Customer", source: :customer, through: :job
has_one :job_business, class_name: "Business", source: :business, through: :job
has_one :published_job_page
has_many :line_items, dependent: :destroy
has_many :payments, dependent: :destroy
has_many_attached :images
after_initialize :set_default_dates, if: :new_record?
after_create :generate_secret_key, :assign_shortened_url, :assign_invoice_number_if_not_assigned
after_save :auto_increment_business_to_next_invoice_number, if: :claimed_last_invoice_number?
after_save :auto_increment_business_to_next_estimate_number, if: :claimed_last_estimate_number?
# before_save :check_job_and_customer_are_same
before_save :set_logical_association_ids
after_initialize :assign_invoice_type_as_default_if_no_invoice_type_set
accepts_nested_attributes_for :line_items
validates_with InvoiceValidator
before_destroy :delete_from_quickbooks
after_destroy :update_job_border_color
after_save :update_job_border_color, if: :saved_change_to_total_price
after_save :update_job_border_color, if: :saved_change_to_amount_paid
after_save :notify_of_signature, if: :saved_change_to_signature
#Gets the invoice display number for this job
def display_number
return self.invoice_display_number
end
def customer_name
return self.have_customer ? self.customer.full_name : ''
end
def email_to_customer(pdf, additional_options, current_employee, attachments=nil, send_without_refresh=false, skip_authentication_on_google_oauth_response=false)
invoice = self
if !current_employee.gmail_token.blank? && JSON.parse(current_employee.gmail_token)["refresh"].blank? && !send_without_refresh
Rails.logger.error "Sending warning email to kodyckendall@gmail.com"
ApplicationMailer::no_refresh_token_error(current_employee.email).deliver_now #Send a warning to kody so he knows an account didn't have a refresh token.
current_employee.update(gmail_token: nil) #Clear this out so it makes them reauthenticate.
end
subject = additional_options["email_subject"].blank? ? "#{invoice.title}" : additional_options["email_subject"]
recipient = additional_options["email_recipients"].blank? && additional_options[:email_recipients].blank? ? invoice.customer_to_invoice.email : additional_options["email_recipients"]&.split(',') || additional_options[:email_recipients].split(',') #Sometimes this options dictionary gets "email_recipients" with string as the key, and sometimes :email_recipients with a symbol as the key. Unfortunately, they are not recognized as the same so we have to check for both.
cc = additional_options["email_ccs"]&.split(',')
payment_link_html = invoice.business.has_setup_stripe_payments? ? " View Online " : nil
ddhranch_id = 670 #bad! one off for DDH Ranch. We could add in a business attribute that has this custom email_template so any business can set any business's default email message that gets sent from invoice email quick-send (paper airplane button).
default_email_body_for_quick_send = "Here is a copy of the #{invoice.invoice_type&.downcase}. You can view this invoice and pay online, if you haven't already. Thank you! #{payment_link_html}"
if current_employee.business_id == ddhranch_id
default_email_body_for_quick_send = """
Thanks for your business! To move forward with us, please sign this invoice, and pay online.
Tyson Davis
Double Diamond Hill Ranch
tyson@ddhranch.com
(801) 810-9252
email_body = additional_options["email_body"].blank? ? default_email_body_for_quick_send : additional_options["email_body"]
mail = Mail.new do
to recipient #Could add multiple people here,what about cc'ing people?
cc cc
from "#{invoice.business.name} <#{invoice.business.email}>"
subject subject
html_part do
content_type 'text/html; charset=UTF-8'
body email_body
end
end
if !attachments.nil?
attachments.each_pair do |key, value|
attachment = ActiveStorage::Attachment.find(value)
mail.attachments[key] = attachment&.blob&.download
end
end
if !pdf.nil?
mail.attachments["#{invoice.title}.pdf"] = pdf
end
Rails.logger.info("We are now attempting to either send the email, or get a redirect_uri from Google (which will happen if we're not authenticated.")
Rails.logger.info("Do mobile oauth = #{skip_authentication_on_google_oauth_response}")
redirect_uri = GmailSender.send_email(mail, current_employee, self.id, additional_options, skip_authentication_on_google_oauth_response)
successfully_delivered_email = redirect_uri == nil
if successfully_delivered_email
self.mark_as_delivered!
elsif redirect_uri == "NO REFRESH TOKEN" #TODO: Magic strings are bad.
self.errors.add(:base, "Can't authenticate because no refresh token is set. Must go into gmail settings to reset for this app. Contact IT support for this application")
Rails.logger.error "Sending another warning email to kodyckendall@gmail.com"
ApplicationMailer::no_refresh_token_error(current_employee.email).deliver_now #Send a warning to kody so he knows an email failed to send.
return redirect_uri
end
#need to follow redirect_uri if it returns..
return redirect_uri
end
def amount_to_charge
return (total_price * 1.01 + 0.30)/0.971
end
def amount_to_charge_in_cents
return (self.amount_to_charge * 100).round
end
def total_price_in_cents
return (self.total_price * 100).round
end
def our_processing_fee
return total_price * 0.01
end
def our_processing_fee_in_cents #We take 1% application fee from our users. Stipe will take an additional ~3%.
return (our_processing_fee * 100).round
end
def link_to_payment
return "#{self.id}/pay"
end
def public_link
return "#{ENV["BASE_URL"]}/invoices/#{self.id}/pay?secret_key=#{self.secret_key}"
end
def signature_link
return "#{ENV["BASE_URL"]}/invoices/#{self.id}/sign?secret_key=#{self.secret_key}"
end
def notify_of_signature
users_to_notify = self.business.notifications_lists.where(notification_type: "SignatureCollected")
self.business.notifications_lists.where(notification_type: "SignatureCollected").each do |notification_subscriber|
employee_we_need_to_notify_id = notification_subscriber.employee_id # we need to notify this employee because they're subscribed to this notification type.
employee_we_need_to_notify = business.employees.find(employee_we_need_to_notify_id)
#TODO: put into a try catch block.
ApplicationMailer::notify_signature_collected(self, employee_we_need_to_notify.email).deliver_now
Rails.logger.info("Sent email to #{employee_we_need_to_notify.email} notifying them that a signature was collected on invoice #{self.id}")
# DeliverNotification.push_text_notification(content, sender_name, sent_from, employee_we_need_to_notify.device_tokens.map{|device_token| device_token.token})
DeliverNotification.push_signature_collected_notification(self.id, "#{ENV["BASE_URL"]}#{shortened_url}", self.customer&.full_name, self.invoice_type, employee_we_need_to_notify.device_tokens.map{|device_token| device_token.token})
end
end
def shortened_url
return link_shortener&.shortened_url
end
def mark_as_delivered!
self.was_delivered = true;
self.sent_on = DateTime.now.to_date;
self.save!
end
def set_total_price
# summed_price = line_items.reduce(0) {|a, i| a += (i.price.nan? ? 0.0 : i.price) * i.quantity } #TODO: Use this one line reduce function.
current_line_items = LineItem.where(invoice_id: self.id)
summed_price = current_line_items.reduce(0) {|a, i| a += (i.price.nan? ? 0.0 : i.price) * i.quantity }
processing_amount = summed_price * 0.03 + 0.50 #IBX 3% + 50 cents.
if self.business.enable_sales_tax_calculation #originally built for HST sales tax in Canada. "HST ON"
# (calculate the percentage of the sales tax after the total price is generated)
sales_tax_amount = summed_price * self.business.sales_tax
self.update(
sub_total_price: summed_price,
total_price: (summed_price + sales_tax_amount).round(2), # we must round to nearest two decimal places to match Quickbooks rounding for HST sales tax calculation
sales_tax_amount: sales_tax_amount,
processing_due: processing_amount
)
else
self.update(total_price: summed_price, processing_due: processing_amount)
end
return total_price
end
def set_amount_paid
total_amount_paid = Payment.where(invoice_id: self.id).sum(:amount)
self.update(amount_paid: total_amount_paid)
end
def amount_due
if invoice_type == "Invoice" || invoice_type == "Change Order" || invoice_type == "Estimate" #No amount due if it's not an invoice
return (total_price || 0) - (amount_paid || 0) #adding these || 0 because when migrating this gets called before the total_price or amount_paid gets set when migrating old data.
else
return 0
end
end
def paid_in_full?
return amount_due <= 0
end
def paid_in_full!
self.payments.create(amount: amount_due, paid_on: DateTime.now, payment_type: Payment.payment_types[:Cash], job_id: self.job&.id, notes: "Employee clicked the mark as paid button")
end
def get_invoice_preview
preview = {title: self.title, number: self.invoice_display_number, customer_name: self.customer_name, total_price: self.total_price, amount_due: self.amount_due, delivered_on: self.was_delivered ? self.sent_on : "Not Delivered", id: self.id, type: self.invoice_type, paid_in_full: paid_in_full_display, due_date: self.due_on, issued_on: self.issued_on.strftime('%F')}
end
def paid_in_full_display
display_text = "N/A"
if was_delivered
paid_in_full? ? display_text = "Yes" : display_text = "No"
end
return display_text
end
def customer_to_invoice
if self.invoice_to.nil?
return self.customer
else
return self.invoice_to
end
end
def sales_rep_number_display
if self.job&.owner&.hide_number_on_sales_rep_form #if they have that hide_number_on_sales_rep_form set then we'll return the business phone number instead
return number_to_phone(business.phone, area_code: true)
end
number_to_phone(job&.owner&.phone, area_code: true)
end
def is_customer_address_different_than_job_address?
return self.job&.location_id != self.service_to&.default_location&.id
end
def check_for_job
if self.job == nil && self.customer == nil
raise ArgumentError.new("You must initialize an invoice with a job or customer")
end
end
def check_job_and_customer_are_same
if self.job != nil && self.customer_id != nil && (self.job&.customer_id != self.customer_id)
raise ArgumentError.new("You can't assign a different customer than the attached Job's Customer!")
end
end
#If we save a job id to this invoice, then the logical conclusion is that the invoice should have that job's customer and business set as it's ID. If one of these gets deleted, then we will need to delete the other ones, etc.
def set_logical_association_ids
current_job_isnt_nil = !self.job_id.nil?
user_attempting_to_set_job_id_to_nil = self.changed_attribute_names_to_save().include?("job_id") && self.attribute_change_to_be_saved("job_id")&.second.nil?
job_id_was_set = current_job_isnt_nil && !user_attempting_to_set_job_id_to_nil
if job_id_was_set #if the job id is set, then it's logical that customer_id and business_id match the job id
self.customer_id = self.job.customer_id
self.business_id = self.job.business_id
return
end
current_customer_isnt_nil = !self.customer_id.nil?
user_attempting_to_set_customer_to_nil = self.changed_attribute_names_to_save().include?("customer_id") && self.attribute_change_to_be_saved("customer_id")&.second.nil?
customer_was_set_and_job_isnt_set = current_customer_isnt_nil && !user_attempting_to_set_customer_to_nil # we know job isn't set because we made it here and didn't return due to above conditional
if customer_was_set_and_job_isnt_set #if the customer id is set, then it's logical for the business_id to match the customer_id.
self.business_id = self.customer.business_id
return
end
business_id_is_set = !self.business_id.nil?
user_attempting_to_set_business_to_nil = self.changed_attribute_names_to_save().include?("business_id") && self.attribute_change_to_be_saved("business_id")&.second.nil?
if business_id_is_set && !user_attempting_to_set_business_to_nil
self.customer_id = nil #Clear these out in case
self.job_id = nil
else # Business can't be nil if everything else is nil.
raise ArgumentError.new("The invoice must have a job, customer, or business assigned to it")
end
end
def title
return "#{invoice_type} #{invoice_display_number}#{filter_title(business&.name)}#{filter_title(customer_to_invoice&.full_name)}#{filter_title(updated_at&.to_date&.strftime('%b %e, %Y'))}"
end
def filter_title(str)
str.blank? ? '' : ", #{str.to_s}"
end
def short_title
"#{invoice_type} #{invoice_display_number.nil? ? id : invoice_display_number}"
end
#for getting the service_to name if it's not the same
def long_title
if self.service_to.nil?
return short_title
else
return "#{short_title} - #{service_to&.full_name}"
end
end
# Increments the business's current invoice number and assigns the job that invoice number.
# Business.invoice_number only increments, never decrements. Should be set when creating the account
# Can also be set from the settings, however that comes with the risk that you could get jobs with duplicate invoice numbers.
def handle_invoice_num_assignment #TODO: Write Unit Test for this requirement
invoice_type_prefix = self.invoice_type.nil? ? "I" : self.invoice_type[0]
if self.invoice_type.nil?
invoice_type_prefix = "I"
end
if business.invoice_number == 1 #if the business invoice number is 1, it means it's at it's default setting and was never changed by the business owner.
index = my_index == 0 ? "" : my_index.to_s
self.invoice_display_number = "#{self.job&.id.to_s}-#{invoice_type_prefix} #{index}" #+ the amount of invoices.. if I'm the second invoice, then I want to to put-2, if i'm the first, I want to put -3, etc.
return self.job&.id.to_s
else
self.invoice_display_number = "#{self.business.invoice_number}-#{invoice_type_prefix}"
return self.business.invoice_number
end
end
def my_index
i = 0
return i if job.nil?
final = ""
job.invoices.each do |inv|
i+=1
if inv == self
final=i
end
end
i
end
def generate_secret_key
self.update(:secret_key => SecureRandom.uuid)
end
def assign_shortened_url
shortened_url = LinkShortener.create(url: public_link)
self.update(link_shortener_id: shortened_url.id)
end
# def assign_shortened_signature_url
# shortened_url = LinkShortener.create(url: signature_link)
# self.update(signature_link_shortener_id: shortened_url.id)
# end
def assign_invoice_number_if_not_assigned
if self.invoice_display_number.blank?
self.handle_invoice_num_assignment
end
end
def set_default_dates
if self.issued_on.nil?
self.assign_attributes({:issued_on => DateTime.now}) #Use assign attributes to avoid triggering a save callback before we're ready to actually save. This was causing a validation error because .update() was triggering the validation.
end
if self.due_on.nil?
self.assign_attributes({:due_on => DateTime.now}) #Use assign attributes to avoid triggering a save callback before we're ready to actually save. This was causing a validation error because .update() was triggering the validation.
end
end
def push_invoice_to_quickbooks!
if have_neither_job_or_customer || !self.business.authenticated_with_quickbooks? || self.quickbooks_id != nil #nil check is because we don't want duplicate invoices in quickbooks.
return false
end
if customer_to_invoice.quickbooks_id == nil
if customer_to_invoice.push_customer_to_quickbooks!
Rails.logger.info("Successfully pushed: #{self.customer&.full_name} into quickbooks")
else
Rails.logger.error("Failed to push customer: #{self.customer&.full_name} to quickbooks")
end
else
Rails.logger.info("customer already in quickbooks system, now moving on to invoice.")
end
if self.quickbooks_id == nil
id = Quickbooks.push_invoice(self, self.business&.quickbooks_realm_id, self.business&.quickbooks_access_token)
if id != nil
Rails.logger.info("Successfully pushed invoice #{self.invoice_display_number} into quickbooks")
self.update!(quickbooks_id: id)
else
Rails.logger.error("Failed to push invoice: #{self.invoice_display_number} into quickbooks")
#Send email to Kody telling me that an invoice getting pushed into Quickbooks failed.
ApplicationMailer::notify_kody_of_quickbooks_failure(self, "kodyckendall@gmail.com").deliver_now
self.errors.add(:base, "Couldn't push the invoice to quickbooks")
end
end
self.payments.each do |payment|
payment.push_payment_to_quickbooks
end
return true
end
def delete_from_quickbooks #TODO: Clear out quickbooks ID after this gets deleted.
if self.quickbooks_id != nil
Quickbooks.delete_invoice(self, self.business.quickbooks_realm_id, self.business.quickbooks_access_token)
end
end
def refresh_qb_sync_token #Could be an error if not careful..
if self.quickbooks_id != nil
response = self.business.query_quickbooks("SELECT SyncToken FROM Invoice WHERE id ='#{self.quickbooks_id}'")
actual_sync_token = Nokogiri::XML(response).remove_namespaces!.xpath("//SyncToken").text
Rails.logger.info("Previous sync token was: " + self.quickbooks_sync_token + ". The sync token quickbooks had was: " + actual_sync_token)
success = self.update(quickbooks_sync_token: actual_sync_token)
return success
end
end
def resync_qb
refresh_qb_sync_token
return push_updated_invoice_to_quickbooks!
end
# Ensures this invoice stays in sync properly with Quickbooks so the user maintains accurate bookkeeping within Quickbooks.
def sync_with_quickbooks!(business)
if business.authenticated_with_quickbooks?
if self.invoice_type == "Invoice"
unless self.push_invoice_to_quickbooks! #Attempt to push this invoice into Quickbooks if it's new, otherwise let's push the updated changes into Quickbooks.
self.push_updated_invoice_to_quickbooks!
end
elsif self.invoice_type == "Estimate" #If this invoice type is an estimate, and it's been synced with quickbooks in the past (has a quickbooks id), then the user most likely first created this as an invoice, which got pushed into Quickbooks as an invoice. Since the user is now changing this to an estimate, we don't want the extra invoice in Quickbooks throwing off their reporting, so we need to make sure we delete the associated invoice in Quickbooks since the user most likely just accidentally created it as an invoice first, and then changed it to an estimate after realizing their mistake.
unless self.quickbooks_id.nil?
self.delete_from_quickbooks #If this was edited from an invoice into an Estimate, and it was previously pushed into Quickbooks, delete it out so that the user doesn't have an invoice in their Quickbooks that was intended to be an estimate.
self.update(quickbooks_id: nil) #We have to set this to nil so that we know it's no longer linked to a Quickbooks Invoice record.
end
end
end
end
def push_updated_invoice_to_quickbooks!
Rails.logger.info "Updating innvoice to quickbooks called!"
if !self.business.authenticated_with_quickbooks? || self.quickbooks_id == nil #Don't try to push updates to this invoice if this business isn't set up for quickbooks, also we check for nil because don't to add a duplicate invoice info to quickbooks.
Rails.logger.info "Wasn't successfully authenticated with Quickbooks"
return
end
Rails.logger.info "Updating invoice: '#{self.id}' to quickbooks"
id = Quickbooks.push_invoice(self, self.business&.quickbooks_realm_id, self.business&.quickbooks_access_token, self.quickbooks_id, self.quickbooks_sync_token)
if id != nil
Rails.logger.info("Successfully pushed invoice #{self.invoice_display_number} into quickbooks")
self.update!(quickbooks_id: id)
else
Rails.logger.info("Failed to push invoice: #{self.invoice_display_number} into quickbooks")
#Send email to Kody telling me that an invoice getting pushed into Quickbooks failed.
ApplicationMailer::notify_kody_of_quickbooks_failure(self, "kodyckendall@gmail.com").deliver_now
end
end
def update_job_border_color
self.job&.set_border_color!
end
def copy!(convert_to_invoice = false, copied_invoice_display_number = nil)
copied = self.dup
if convert_to_invoice
copied.update(invoice_type: "Invoice", was_converted_from_estimate: self.id)
self.update(was_converted_to_invoice: copied.id)
if !copied_invoice_display_number.nil? #We need to change the new invoice display number for a quickbooks unit test.
puts "Giving copied invoice new display number: #{copied_invoice_display_number}"
copied.update(invoice_display_number: copied_invoice_display_number)
end
end
if !copied_invoice_display_number
copied.handle_invoice_num_assignment
end
if !copied.quickbooks_id.nil? #Must set this to nil or this new invoice won't be pushed into quickbooks.
copied.update(quickbooks_id: nil)
end
copied.save
self.line_items.each do |li|
copy = li.dup
copy.invoice_id = copied.id
copy.save
end
if convert_to_invoice #set these things before pushing to quickbooks so that it's all accurate info
copied.update(issued_on: DateTime.now, due_on: DateTime.now + 30.days, sent_on: nil, was_delivered: nil)
end
r = copied.push_invoice_to_quickbooks!
if convert_to_invoice
self.reassign_payments(copied) #We want to reassign the payments especially when converting an estimate to an invoice. If a customer has already made a payment for an estimate, we just want the invoice that it was copied from to take that payment and attach it to itself. This reduces overhead for office workers to have to go in and update the payment to point towards the invoice that was just created from the estimate.
copied.reload #Have to reload the copied version after we swap all payments over to it, otherwise the association won't be updated even after changing the invoice_id's on the associated payments.
copied.payments.each do |p|
r = p.push_payment_to_quickbooks
end
end
return copied
end
def should_invoice_other_customer?
customer_to_invoice&.id != service_to&.id
end
def have_neither_job_or_customer
self.job.nil? && !have_customer
end
def have_customer
self.customer.present?
end
def service_to
if !customer_id.nil?
return customer
elsif !job&.customer_id.nil?
return job.customer
else
return nil
end
end
#TODO: line items update?
def reassign_payments(invoice_to_reassign_to)
self.payments.update(:invoice_id => invoice_to_reassign_to.id)
end
def sales_rep
return job&.owner&.full_name
end
def display_address
unless job.nil?
customer&.default_location&.display_address
else
job&.location&.display_address
end
end
def lite_as_json #used for the mobile app to get a leaner version of the invoice.
return {id: self.id,
title: self.title,
short_title: self.short_title,
total_price: self.total_price,
amount_due: self.amount_due,
customer_name: self.customer_name,
invoice_display_number: self.invoice_display_number,
notes: self.notes,
invoice_type: self.invoice_type,
line_items: self.line_items,
customer_id: self.customer_id,
public_link: self.public_link,
signature_link: self.signature_link,
job_id: self.job_id,
}
end
def as_json(*) #This is a pretty heavy method call, so it's very inefficient if we serialize an invoices array to json.
super.tap do |hash|
# hash["business_logo_uri"] = self.business.logo.attached? ? self.business.logo.service_url : nil
hash["title"] = self.title
hash["short_title"] = self.short_title
hash["total_price"] = self.total_price
hash["amount_due"] = self.amount_due
hash["customer_name"] = self.customer_name
hash["customer_to_invoice"] = self.customer_to_invoice
hash["service_to"] = self.service_to
hash["display_address"] = self.display_address
hash["shortened_url"] = self.shortened_url
hash["line_items"] = self.line_items
hash["public_link"] = self.public_link
hash["signature_link"] = self.signature_link
hash["job_id"] = self.job_id
hash["invoice_type"] = self.invoice_type
hash["job_address"] = self.job&.location
end
end
def appointments
appointments = []
employee_job_from_database = self.job&.employee_jobs&.includes([:appointment_color, :employee, :employee_job_type])
if employee_job_from_database.blank?
return appointments
end
self.job&.employee_jobs&.includes([:appointment_color, :employee, :employee_job_type]).each do |appointment|
appointments << "#{appointment&.employee_job_type&.name}, #{appointment&.employee&.first_name}, #{appointment&.time_start&.to_date}"
end
return appointments
end
def invoice_link
"#{ENV["BASE_URL"]}/invoices/#{self.id}"
end
def self.to_csv
attributes = %w{invoice_type display_number sent_on total_price notes sales_rep appointments invoice_link}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |user|
csv << attributes.map{ |attr| user.send(attr) }
end
end
end
def customer
if !customer_id.nil?
return self.direct_customer
elsif !job&.customer_id.nil?
return self.job.customer
end
end
def business
self.job_business.nil? ? Business.find_by(id: self.business_id) : self.job_business
end
def summarize(include_pricing=false)
summary = []
# Add job notes if job exists
summary << "Job Notes: #{job.notes}" if job
# Add invoice notes
summary << "Invoice Notes: #{notes}" unless notes.blank?
# Add each line item's description and price
line_items.each do |item|
if include_pricing
summary << "Item: #{item.description}, Price: $#{item.price&.round()}"
else
summary << "Item: #{item.description}"
end
end
# Append total price
if include_pricing
summary << "Total Price: $#{total_price.round()}"
end
# Join all parts into a single text string
summary.join("\n")
end
private
def assign_invoice_type_as_default_if_no_invoice_type_set
if (self.invoice_type == nil)
self.invoice_type = "Invoice"
# self.update(invoice_type: "Invoice")
end
end
#Moves the business number to +1 from what our invoice number is.
def auto_increment_business_to_next_invoice_number
business.update(invoice_number: business.invoice_number + 1)
end
#Moves the business number to +1 from what our invoice number is.
def auto_increment_business_to_next_estimate_number
business.update(estimate_number: business.estimate_number + 1)
end
#checks if this invoice number matches the businesses invoice number.. knowing that we claimed it.
def claimed_last_invoice_number?
if invoice_type == "Invoice"
invoice_number = invoice_display_number&.tr('^0-9', '')&.to_i
return invoice_number == business.invoice_number
else
return false
end
end
#checks if this estimate number matches the businesses invoice number.. knowing that we claimed it.
def claimed_last_estimate_number?
if invoice_type == "Estimate"
estimate_number = invoice_display_number&.tr('^0-9', '')&.to_i
return estimate_number == business.estimate_number
else
return false
end
end
end
class Ability
include CanCan::Ability
def initialize(employee)
if employee.nil?
# No abilities, restrict all resources.
else
employee.group.permissions.each do |p|
resource = (p.resource == 'all') ? p.resource.to_sym : p.resource.constantize
action = p.action.to_sym
can action, resource
# If groups can be updated, then permissions can be managed.
if resource == Group && (action == :all || action == :update)
can :manage, Permission
end
end
end
end
end
class WebPage < ApplicationRecord
extend FriendlyId
friendly_id :slug, use: :slugged
validates :slug, presence: true, uniqueness: true
belongs_to :business
belongs_to :home_page, optional: true
has_many :web_page_histories, dependent: :destroy
def restore(web_page_history)
self.save_history
self.content = web_page_history.content
self.save
end
def save_history()
web_page_history = WebPageHistory.new
web_page_history.content = self.content
web_page_history.user_message = "Restore to previous version"
web_page_history.web_page_id = self.id
web_page_history.save
end
end
class Call < ApplicationRecord
include ExpoNotificationHelper
include ApplicationHelper
include DeliverNotification
belongs_to :business
belongs_to :employee, optional: true
belongs_to :customer, optional: true
require "aws"
require "chatgpt"
# How to mock a notification handler..?..?? We should create some type of interface that implements ways to notify
after_update :notify_owner_of_call_ended
#this should be called when notes or call outcome gets changed.
def notify_owner_of_call_ended
# if (saved_change_to_notes? || saved_change_to_outcome?)
Rails.logger.info("The call rep updated a call with info!")
#Get the employee id that we need to notify for this business, in my case it will be Kody & Dad.
#Notification using Expo Device Notification
#Check out notification list.. go through all employees and owners registered for 'ReceiveCallModifiedEvent', if the employee exists in that list, then we will send out an expo push message.
#Go through each employee that has a notification listener for this ReceiveCallModifiedevent.
self.business.notifications_lists.where(notification_type: 'ReceiveCallModifiedEvent').each do |notification_subscriber|
employee = notification_subscriber.employee
Rails.logger.info("Now looking at employee #{employee.full_name} to push out expo notifications for the ReceiveCallModifiedEvent!")
#Here, we notify the employee using their device tokens.
if employee.device_tokens.empty?
Rails.logger.info("This employee didn't have any device tokens!")
next
end
device_tokens = employee.device_tokens.map{|device_token| device_token.token}
Rails.logger.info("Now sending out the expo notifications for employee #{employee.id}'s device tokens: #{device_tokens}")
DeliverNotification.push_call_modified_received_event(id, self.as_json, customer_id, caller_name, call_from, "#{ENV["BASE_URL"]}/calls", device_tokens)
end
# end
end
def ai_parse_info_from_transcript
customer_name, address, city, zip, summary = ChatGPT.transcribe(transcription)
# byebug
self.update(ai_parsed_customer_name: customer_name)
if self.caller_name.blank?
self.update(caller_name: customer_name)
end
self.update(ai_parsed_address: address)
self.update(ai_parsed_city: city)
self.update(ai_parsed_zip: zip)
self.update(ai_parsed_summary: summary)
end
def duration_in_words
if (self.start_time.nil?) #guard clause
return ""
end
duration_diff = ((self.end_time&.to_datetime || DateTime.now) - (self.start_time.to_datetime))
duration_in_mins = (duration_diff * 1.days).to_i
return duration_of_interval_in_words(duration_in_mins)
end
def as_json(*)
super.tap do |hash|
hash["duration_in_words"] = self.duration_in_words
hash["notes"] = self.notes
end
end
def upload_to_s3
#1. FETCH the file from the recording_uri.
#2. Download that file to the server temporarily
require 'open-uri'
tmp_file_name = s3_file_name
begin
open(tmp_file_name, 'wb') do |file|
file << open(recording_uri).read
end
# s3 = Aws::S3::Client.new(region: ENV["AWS_REGION"])
AWS.get_s3_client().put_object(
:bucket => ENV["AWS_BUCKET"],
:key => tmp_file_name,
:body => IO.read(tmp_file_name)
)
ensure
File.delete(tmp_file_name)
end
#TODO: Delete this temporary file from the server.
end
def transcribe_to_text!
Rails.logger.info("Now transcribing this call to text using AWS transcription services!")
#1. upload Twilio's recording_URI to S3 bucket.
#Q: How do we upload the recording_URI to S3 bucket??
# after_update :save_to_s3_if_needed
upload_to_s3
sleep 10
#2. Generate transcription job
unique_identifier = transcription_job_name
AWS.get_transcribe_client().start_transcription_job({ ##start_call_analytics_job(params = {})
transcription_job_name: unique_identifier, # required, must be unique.
media_format: "wav",
media: { # required
media_file_uri: "https://service-jobs-images.s3.us-east-2.amazonaws.com/#{s3_file_name}",
},
language_code: "en-US", # accepts af-ZA, ar-AE, ar-SA, cy-GB, da-DK, de-CH, de-DE, en-AB, en-AU, en-GB, en-IE, en-IN, en-US, en-WL, es-ES, es-US, fa-IR, fr-CA, fr-FR, ga-IE, gd-GB, he-IL, hi-IN, id-ID, it-IT, ja-JP, ko-KR, ms-MY, nl-NL, pt-BR, pt-PT, ru-RU, ta-IN, te-IN, tr-TR, zh-CN, zh-TW, th-TH, en-ZA, en-NZ
identify_language: false,
output_bucket_name: ENV["AWS_BUCKET"],
settings: {
show_speaker_labels: true,
max_speaker_labels: 2,
}
})
Thread.new do
Rails.application.executor.wrap do
80.times do |i| #tries for 20 minutes max. 15*80 = 800 + 400 = 20 minutes.
sleep 15
if transcription_job_has_finished
sleep 15 #give it another 15 seconds to load from transcription job to S3 file.
raw_transcription, transcription_by_speaker = get_transcription_job
self.update(transcription: raw_transcription, transcription_by_speaker: transcription_by_speaker.to_json)
if self.business.parse_call_transcripts_using_ai
ai_parse_info_from_transcript
end
break
end
end
end
end
end
def get_transcription_job
unique_identifier = transcription_job_name
resp = AWS.get_transcribe_client().get_transcription_job({
transcription_job_name: unique_identifier, # required
})
tmp_file_name = "#{unique_identifier}.json"
tmp_results_of_transcription = "#{s3_file_name}-transcription-results"
begin
File.open(tmp_results_of_transcription, 'wb') do |file|
reap = AWS.get_s3_client().get_object({bucket:ENV["AWS_BUCKET"], key:tmp_file_name }, target: file)
end
content = File.read(tmp_results_of_transcription)
results = JSON.parse(content)
parsed_speaker_results = parse_speaker_content_into_readable_text(results)
return results["results"]["transcripts"][0]["transcript"], parsed_speaker_results
ensure
File.delete(tmp_results_of_transcription)
end
end
def parse_speaker_content_into_readable_text(results)
#https://towardsdatascience.com/a-quick-tutorial-to-aws-transcribe-with-python-53bbf6605a55
#TODO:
labels = results["results"]["speaker_labels"]["segments"]
speaker_start_times = {}
labels.each do |label|
label["items"].each do |item|
speaker_start_times[item["start_time"]] = item["speaker_label"]
end
end
items = results["results"]["items"]
lines = []
line = ''
time = 0
speaker = 'null'
i = 0
end_results = []
items.each do |item|
i=i+1
content = item['alternatives'][0]['content']
if !item['start_time'].nil?
content = item['alternatives'][0]['content']
elsif item['type'] == 'punctuation'
line = line + content
end
current_speaker = item["speaker_label"] #what is the speaker here..
if current_speaker != speaker #speaker changed, so let's update it.
lines << {'speaker':speaker, 'line':line, 'time':time, 'content': content} #flush the content from previous speaker into a new line.
speaker = current_speaker
if !speaker.empty?
line = content
time = item['start_time']
lines << {'speaker':speaker, 'line':line, 'time':time, 'content': content}
elsif item['type'] != 'punctuation'
line = line + ' ' + content
time = item['start_time']
lines << {'speaker':speaker, 'line':line, 'time':time, 'content': content}
end
else #If it's the same speaker, then we will simply append the current content into this existing lines.??
if item['type'] != 'punctuation' #item isn't punctuation.
line = line + ' ' + content
time = item['start_time']
else #item is punctuation.
#TODO: What do we do if it is punctuation?
end
end
end
# sort the results by the time
sorted_lines = lines.sort_by { |k| k[:time].to_f }
return sorted_lines
end
def transcription_job_has_finished
unique_identifier = transcription_job_name
resp = AWS.get_transcribe_client().get_transcription_job({
transcription_job_name: unique_identifier, # required
})
return resp["transcription_job"]["transcription_job_status"] == "COMPLETED"
end
def s3_file_name
sanitized_business_name = business.name #We have to remove all special characters because AWS api rejects it if it has any non-allowed special characters
sanitized_business_name.gsub!(/[^A-Za-z]/, '')
return "tmp#{sanitized_business_name}-call-#{id}-8"
end
def transcription_job_name
sanitized_business_name = business.name #We have to remove all special characters because AWS api rejects it if it has any non-allowed special characters
sanitized_business_name.gsub!(/[^A-Za-z]/, '')
return "#{sanitized_business_name}-transcription-#{id}-8"
end
def self.to_csv
# to call_from caller_name recording_uri start_time end_time duration created_at business_id call_sid notes is_outbound phone_number_employee_is_calling_from_in_outbound_call customer_phone_number_in_outbound_call employee_id customer_id transcription
attributes = %w{to call_from caller_name recording_uri start_time end_time duration created_at business_id call_sid notes is_outbound phone_number_employee_is_calling_from_in_outbound_call customer_phone_number_in_outbound_call employee_id customer_id transcription}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |user|
csv << attributes.map{ |attr| user.send(attr) }
end
end
end
end
class InvoiceTemplate < ApplicationRecord
belongs_to :business
has_many :line_item_template
has_one :invoice
has_one_attached :logo
# has_one_attached :logo
accepts_nested_attributes_for :line_item_template
validates :name, presence: true #, uniqueness: true #uniqueness creating issue when updating
def total
total = 0.0
self.LineItem.each do |i|
total += i.price
end
return total
end
#NOTE: All of these methods are here so that we can re-use "_invoices.html.erb" for creating and editing our invoice templates.
def title
"Invoice Template"
end
def job
self.business.jobs.first
end
def my_index
nil
end
def invoice_type
"Template"
end
def invoice_display_number
"0"
end
def amount_due
"0"
end
def amount_paid
"0"
end
def issued_on
DateTime.now
end
def due_on
DateTime.now
end
def customer_to_invoice
self.business.customers.first
end
def service_to
return self.business.customers.first
end
def should_invoice_other_customer?
false
end
def is_customer_address_different_than_job_address?
false
end
def notes
description
end
def line_items
line_item_template
end
def payments
[]
end
def signature
nil
end
def sales_rep_number_display
nil
end
def total_price
line_item_template.map {|i| i.price}.reduce(:+) || 0
end
end
class RosterJob < ApplicationRecord
belongs_to :roster
belongs_to :job
validates_with RosterJobTimeValidator
def end
format_datetime(time_end)
end
def start
format_datetime(time_start)
end
private
# Day, Month/Day/Year at Hour:Minute AM/PM
def format_datetime(dt)
dt.strftime('%a, %m/%d/%Y at %I:%M %p')
end
end
class Branch < ApplicationRecord
has_many :employees
belongs_to :business
end
class TextTemplate < ApplicationRecord
belongs_to :business
#this method encodes our text_template as a url so it can be used safely a parameter in a url
#also evaluates the following variables: {{business_name}}
def evaluated_url(user_first_name)
ERB::Util.url_encode(self.body.gsub(/{{user}}/, user_first_name))
end
end
class Permission < ApplicationRecord
belongs_to :group
ACTIONS = %w(create destroy manage read update).freeze # manage gives full access to all
RESOURCES = %w(all Customer Employee Group Job Roster).freeze #EmployeeJob Message Call Invoice Payment Roster).freeze
validates :action, presence: true, inclusion: {in: ACTIONS}
validates :group_id, presence: true, uniqueness: {scope: [:action, :resource]}
validates :resource, presence: true, inclusion: {in: RESOURCES}
validate :all_restricted_to_owner_group
private
# Ensure that only the owner group can manage "all" resources.
def all_restricted_to_owner_group
if resource == 'all'
errors.add(:resource, 'Only owners can manage all resources.') unless group.owner?
end
end
end
class Review < ApplicationRecord
belongs_to :home_page
belongs_to :business
end
class EmployeeJobType < ApplicationRecord
end
class LlamaBot < ApplicationRecord
end
class Payment < ApplicationRecord
belongs_to :invoice, optional: true #invoice optional so a customer can pay company without an invoice
has_one :job, through: :invoice
enum payment_type: { Card: 0, Check: 1, Cash: 2, Online: 3 } #Online would be through a service like paypal, venmo, wire transfer, etc. # https://naturaily.com/blog/ruby-on-rails-enum
validates_presence_of :payment_type
validates_presence_of :amount
validates_with PaymentValidator
require 'ibx'
after_create :push_payment_to_quickbooks, if: :invoice_in_quickbooks?
after_save :notify_sales_rep, if: :havent_notified_salesrep
after_save :update_invoice_total_due, :set_border_color, if: :invoice_id
after_destroy :update_invoice_total_due, :set_border_color, if: :invoice_id
def push_payment_to_quickbooks
self.business.push_payment_to_quickbooks(self)
end
def business
self.invoice.business
end
def invoice_in_quickbooks?
return !self.invoice&.quickbooks_id.nil?
end
def notify_sales_rep #TODO: How do we make this more modern.?
# for each employee that is subscribed to getting payment requests...
# notifications_lists.create(notification_type: "CustomerPaymentReceived", business_id: business.id, employee_id: employee.id)
begin
# Add a notification icon that says: "get notified when payments are made." --> I'm also worried about spamming everyone with payment notifications.
employee_ids_to_notify = self.business.notifications_lists.where(notification_type: "CustomerPaymentReceived").map{|notification_subscription| notification_subscription.employee_id}
emails_to_notify = ['kodyckendall@gmail.com']
employee_ids_to_notify.each do |eid|
emails_to_notify << Employee.find(eid).email
end
ApplicationMailer::notify_salesrep_of_payment(self, emails_to_notify).deliver_now
self.update(has_notified_payment: true)
rescue Exception => e
Rails.logger.info("There has been an error trying to push a notification about this payment, error: " + e.message + " " + e.backtrace.join("\n"))
end
end
def amount_to_refund_with_processing
self.amount + (self.amount*0.03+0.50)
end
def refund_ibx_payment
if (self.pn_ref == 0 || self.pn_ref.nil?) #That means this payment wasn't processed with IBX.
return false
end
Rails.logger.info("Attempting to refund the payment: #{self.id} from invoice #{self.invoice&.id} for customer #{self.invoice&.customer_name}")
response = nil
if within_voidable_period?
Rails.logger.info("We are within the period to void this transaction, so we are going to void this payment rather than refund!")
response = Ibx.issue_void("fiel6738", "Partner123", self.card_token, self.amount, self.pn_ref)
else
Rails.logger.info("We are NOT within the period to void this transaction, so we are going to fully refund this payment, AND refund the processing amount!")
# response = Ibx.issue_refund("fiel6738", "Partner123", self.card_token, self.amount + (self.amount*0.03+0.50), self.pn_ref)
response = Ibx.issue_refund("fiel6738", "Partner123", self.card_token, self.amount, self.pn_ref)
end
ibx_response = JSON.parse(response.body)
Rails.logger.info("Response code issuing a refund for the payment: #{response.code}")
Rails.logger.info("Response to issuing a refund for the payment: #{ibx_response}")
if response.code == "200"
previous_notes = self.notes
self.update(notes: "#{previous_notes} | A card refund was issued on #{Date.today} for #{self.amount}, setting new amount to 0.", amount: 0)
end
return response.code == "200"
end
def havent_notified_salesrep
!self.has_notified_payment
end
def update_invoice_total_due
self.invoice.set_amount_paid
end
def set_border_color
unless self.invoice.job&.nil?
self.invoice.job&.set_border_color!
end
end
# Tests to see if the created_at day for this payment is within the voidable period (if we attempt to void it the same day, then we can void it. If not, we need to run a refund instead of void)
# Needed to determine if we should issue a "void" or a "refund" request for IBX.
def within_voidable_period?
Payment.within_same_day?(created_at.to_date, Date.today, business.time_zone)
end
# Given two dates (not date times), determine if they are within the same day.
def self.within_same_day?(date_one, date_two, time_zone)
Payment.distance_between_two_dates(date_one, date_two, time_zone) == 0
end
def self.distance_between_two_dates(date_one, date_two, time_zone)
(date_one.in_time_zone(time_zone).to_date - date_two.in_time_zone(time_zone).to_date).abs
end
def as_json(*) #https://github.com/rails/rails/pull/2200#issuecomment-3131652
super.tap do |hash|
hash["amount_to_refund_with_processing"] = self.amount_to_refund_with_processing
end
end
end
class LineItem < ApplicationRecord
default_scope {order(position: :asc)} #WARNING: this was breaking the migration: AddDefaultQuantityToLineItems.
belongs_to :invoice
belongs_to :product, optional: true
validates_with LineItemValidator
after_initialize :set_default_quantity
before_save :strip_html_tags_and_entities
after_save :update_invoice_total_due, :set_border_color
after_destroy :update_invoice_total_due
before_destroy :set_border_color
def set_default_quantity
if self.quantity.nil?
self.update(:quantity => 1)
end
end
def update_invoice_total_due
self.invoice.set_total_price
end
def set_border_color
Invoice.find(self.invoice.id).job&.set_border_color!
end
def strip_html_tags_and_entities
self.description = HtmlSanitizer.strip_html_tags_and_entities(self.description)
end
def sanitize
strip_html_tags_and_entities
save
end
end
class ForwardingNumber < ApplicationRecord
#ASSOCIATIONS
belongs_to :business
# _______________________________________
# VALIDATIONS:
validates_with PhoneValidator, if: -> { !self.phone.blank? }
validates :phone, presence: true
end
class EmailTemplate < ApplicationRecord
belongs_to :business
end
class Location < ApplicationRecord
has_many :jobs
belongs_to :customer
validates_presence_of :customer
validates_with ZipCodeValidator, if: -> { !self.zip_code.blank? }
before_save :turn_empty_state_to_nil
after_save :set_customer_default_location
def as_json(*)
super.tap do |hash|
hash["display_address"] = self.display_address
hash["street_address"] = self.street_address
hash["city"] = self.city
hash["zip_code"] = self.zip_code
hash["state"] = self.state
hash["google_maps_link"] = self.google_maps_link
hash["is_default_location"] = self.is_default?
end
end
def google_maps_link
return "https://maps.google.com/?q=#{self.street_address} #{self.city} #{self.state} #{self.zip_code}"
end
def apple_maps_link
return "https://maps.apple.com/?address=#{self.street_address} #{self.city} #{self.state} #{self.zip_code}&t=m"
end
def display_address
return "#{self.street_address}, #{self.city}, #{self.state}, #{self.zip_code}"
end
def turn_empty_state_to_nil
if self.state.blank?
self.state = nil
end
end
def set_customer_default_location
if self.customer.location.nil?
self.set_customer_default_location!
end
end
def set_customer_default_location!
self.customer.update(:location_id => self.id)
end
def is_default? #TODO: Causing an n+1 error.?
# Customer.find_by(id: self.customer_id)&.location_id == self.id
is_billing_address
end
# This is so that ransacker can search on this "full_name" attribute. Using Arel node for database.
ransacker :full_address do |parent|
Arel::Nodes::NamedFunction.new('CONCAT_WS', [
Arel::Nodes.build_quoted(' '), parent.table[:street_address], parent.table[:city], parent.table[:zip_code], parent.table[:state]
])
end
end
class JobRequest < ApplicationRecord
require "twilio"
include DeliverNotification
belongs_to :business
has_one :job
belongs_to :customer, optional: true
has_one :marketing_source
has_one :location
after_create :notify_business_of_submitted_form
before_validation :strip_whitespace_from_email
validates_presence_of :email, message: "Email is needed for sending our invoices and quotes to you"
validates_presence_of :phone
validates_with EmailValidator
validates_with PhoneValidator
has_many_attached :images
# fixing the issue where marketing source isn't getting carried from the job request marketing_source_id.
# customers_with_bad_marketing_source = b.jobs.joins(:customer, :job_request).where(customers: {business_id: b.id, marketing_source_id: nil}).where.not(job_requests: {marketing_sources_id: nil})
#
# ## Assuming jobs_with_specific_conditions is your ActiveRecord relation from the previous query
# customers_with_bad_marketing_source.each do |job|
# # Access the job's associated customer and job_request
# customer = job.customer
# job_request = job.job_request
# # Check if both customer and job_request are present to avoid errors
# if customer.present? && job_request.present?
# # Update the customer's marketing_source_id with the job_request's marketing_source_id
# customer.update(marketing_source_id: job_request.marketing_source_id)
# end
# end
# customers_with_bad_marketing_source.includes(:customer, :job_request).find_each { |job| job.customer.update(marketing_source_id: job.job_request.marketing_sources_id) if job.customer && job.job_request }
# formats all info
def summary
"#{first_name} #{last_name} \n phone: #{phone}. email: #{email}. \n At #{street_address} in #{city}, #{zip}. \n #{notes}"
end
def as_json(*)
super.tap do |hash|
hash["summary"] = summary()
hash["marketing_sources_name"] = marketing_source&.name
hash["images"] = images&.map{|image| image.service.send(:object_for, image.key).public_url}
end
end
def marketing_source
self.marketing_sources_id == nil ? nil : business.marketing_sources.find(self.marketing_sources_id)
end
def notify_business_of_submitted_form
employees_to_notify = business.employees_to_notify_for_job_requests
employees_to_notify.each do |employee|
#TODO: This is where we could send each contractor a job request notification.. If they don't have the mobile app set up.
if employee.device_tokens.empty? && self.set_up_with_twilio
Twilio.send_text(employee&.phone, "A job request has been submitted by: #{first_name} #{last_name} at #{street_address} in #{city}. Phone is '#{phone}', email is '#{email}'. | #{ENV["BASE_URL"]}", business.twilio_number)
else
device_tokens = employee.device_tokens.map{|device_token| device_token.token}
DeliverNotification.push_job_request_submitted_notification(id, notes, "#{first_name} #{last_name}", city, device_tokens)
end
end
end
def set_up_with_twilio
Rails.env.production? && !business.twilio_number.blank?
end
def strip_whitespace_from_email
self.email = self.email.strip unless self.email.nil?
end
end
# customers_with_bad_marketing_source[0].customer.update(marketing_source_id: customers_with_bad_marketing_source[0].job_request.marketing_sources_id)
class LinkShortener < ApplicationRecord
def shortened_url
"/l/#{self.id}"
end
end
class Message < ApplicationRecord
require "twilio"
require 'uri'
require 'net/http'
include DeliverNotification
belongs_to :business
belongs_to :employee, optional: true
after_create :deliver_on_create, if: Proc.new{ |message| !message.skip_deliver_on_create }
# after_create :action_cable_update_messages_view_and_attach_image
has_one_attached :image, dependent: :destroy
after_create :notify_business_on_response #Send text message to business line to let them know someone responded
before_validation :clean_phone_numbers
validates_with MessagePhoneValidator #TODO: How will I validate these numbers? Twilio requires +1 for our international numbers.. I need to translate them from international to our normal phone number. I also don't want to reuse our phone validator logic.
#Cleans the phone numbers so that it has no special characters
def clean_phone_numbers
self.to = self.to&.delete('^0-9')
self.sent_from = self.sent_from&.delete('^0-9')
end
#This broadcasts out through our action cable that a message was received. Then it updates in the conversation component
def action_cable_update_messages_view_and_attach_image( image_params )
self.update(sent: DateTime.now)
if self.sent_from != self.business.twilio_number
Rails.logger.info("Twilio: Message was an inbound message from: #{self.sent_from}. Content: #{self.content}")
# ActionCable.server.broadcast 'messages', content: self.content, sent_from: self.sent_from, to: self.to, sent: self.sent, image_url: image_params[:media_url].to_s
# ActionCable.server.broadcast "messages_#{self.business_id}", content: self.content, sent_from: self.sent_from, to: self.to, sent: self.sent, image_url: image_params[:media_url].to_s
MessagesChannel.broadcast_to(self.business, self)
end
self.image.attach( io:open(image_params[:media_url]), filename: image_params[:file_name], content_type: image_params[:content_type] ) if image_params[:media_url]
end
#TODO: When we want the response to be sent to the employee who sent the last message, this is where we'll find the employee who last sent this number a message. That way, we can send a text message to that employee's phone as well.
#Sends a text message to the business that someone has messaged the business.
def notify_business_on_response
if is_inbound?
if Rails.env.production?
notify_all_relevant_users_of_inbound_sms
end
end
end
# I created this to start notifying all users who want to be notified of all incoming messages coming into the business.
# Because users are unhappy that they are losing text messages from customers and not being notified.
def notify_all_relevant_users_of_inbound_sms
# all users that are enrolled in this list, AND if an employee was the last one to notify them.
customer = MessagesHelper.resolve_phone_number(self.business, self.sent_from)
customer_name = customer&.full_name
customer_id = nil
if(customer.is_a? Customer)
customer_id = customer&.id
elsif(customer.is_a? CustomerContact)
customer_id = customer&.customer_id
else
customer_id = ''
end
#TODO: Include the number of currently unread_messages so that we can set the badge count number on the mobile app icon
number_of_unread_messages = self.business.get_unread_messages_count
business.notifications_lists.where(notification_type: "AnyInboundSMSReceivedNotification").each do |notification_subscriber|
employee_we_need_to_notify_id = notification_subscriber.employee_id # we need to notify this employee because they're subscribed to this notification type.
employee_we_need_to_notify = business.employees.find(employee_we_need_to_notify_id)
skip_this_employee_so_we_dont_double_notify_them = employee_we_need_to_notify_id == self.employee_id #The employee_id for self is going to get notified, so let's make sure to not notify them here as well (this prevents two push notifications from going out to an employee that is subscribed to all new text messages coming in, AND was also the last employee to message this customer).
if employee_we_need_to_notify && !skip_this_employee_so_we_dont_double_notify_them
DeliverNotification.push_text_notification(content, customer_name, sent_from, employee_we_need_to_notify.device_tokens.map{|device_token| device_token.token}, customer_id, number_of_unread_messages)
end
end
unless self.employee.nil? #notify the employee that sent the last message to this specific customer, if there is one. We won't accidentally double notify because we we "skip_this_employee_so_we_dont_double_notify_them" above.
notify_employee_of_response(number_to_notify_on_inbound, customer_name, customer_id, number_of_unread_messages)
end
end
#Send a text to the employee that sent the last message a notification that the customer has texted back. Defaults to notify the business if an employee hasn't messaged anyone yet.
def notify_employee_of_response(number_to_notify, customer_name, customer_id, number_of_unread_messages)
Rails.logger.info("Now notifying our employee with number #{number_to_notify} of the inbound message that came to his business")
#Could check to see if that employee has a device token, if not we will send the SMS.
if employee.device_tokens.empty?
Twilio.send_text(number_to_notify, formatted_response_message_to_employee, self.business.twilio_number)
else
Rails.logger.info("Employee has registered device token(s), sending notifications to devices now.")
#TODO: Put this into a helper class, maybe the Twilio class, or into a Mobile API Helper Class.
DeliverNotification.push_text_notification(content, customer_name, sent_from, employee.device_tokens.map{|device_token| device_token.token}, customer_id, number_of_unread_messages)
#TODO: What happens if there is a bad device token in there?
# begin
# # client = Exponent::Push::Client.new
# # device_tokens = employee.device_tokens
# # sender_name = MessagesHelper.resolve_phone_number(self.business, self.sent_from)&.full_name
# # ExpoNotificationHelper.notify_client(device_tokens, self.content, sender_name, {notification_type: "InboundMessage", name: sender_name, phoneNumber: self.sent_from, id: self.id, sent: self.sent, is_inbound: self.is_inbound?})
# rescue Exception => e
# Rails.logger.info("There has been an error trying to push a notification about this message, error: " + e.class + " " + e.message + " " + e.backtrace.join("\n"))
# end
# messages = [{
# to: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
# sound: "default",
# body: "Hello world!"
# }, {
# to: "ExponentPushToken[yyyyyyyyyyyyyyyyyyyyyy]",
# badge: 1,
# body: "You've got mail"
# }]
# MAX 100 messages at a time
# handler = client.send_messages(messages)
# Array of all errors returned from the API
# puts handler.errors
# you probably want to delay calling this because the service might take a few moments to send
# I would recommend reading the expo documentation regarding delivery delays
# client.verify_deliveries(handler.receipt_ids)
end
end
#actually sends the text message if it's an outbound message
def deliver_on_create # Only send Kody a text if someone on a production environment signs up for a subscription.
if is_outbound? && self.sent.nil? #If it's an outbound message we send it out through twilio
Rails.logger.info("Twilio: Outbound Message just created, if we're in production, sending message now.")
self.update(has_been_read: true, sent: DateTime.now)
if Rails.env.production? || Rails.env.development? #Don't send when running automated tests. We really shouldn't send even when in development, however we are developing our MMS capabilities and want to test it live with developer.
image_url = self.image.attachment.blank? ? '' : self.image.attachment.blob.service_url.to_s
twilio_response = Twilio.send_text(self.to, self.content, self.business.twilio_number, image_url)
self.update_column(:twilio_sid, twilio_response&.sid)
Rails.logger.info("Twilio: Just delivered our sms message #{self.content} to: #{self.to}")
end
end
end
def skip_deliver_on_create(value=false)
@skip_deliver_on_create = @skip_deliver_on_create ? @skip_deliver_on_create : value
end
def is_inbound?
# return self.sent_from != self.business.twilio_number && self.to == self.business.twilio_number #This would be technically more correct. However, I'm worried about it being fragile, and if it's not done right it could break the messaging function for companies and be difficult to debug.
return self.sent_from != self.business.twilio_number #If it's not sent from the business, it was sent from someone else.
end
def is_outbound?
return !is_inbound?
end
#this is the phone number that will be sent a text when a customer sends an inbound message
def number_to_notify_on_inbound #For now, we will assume we're just sending the message to the first & (hopefully only) employee in the company.
#This needs to be the employee that last messaged
if self.employee.nil?
return business.employees.first.phone #send it to the first employee in the business if we don't have
else
return self.employee.phone
end
end
def employee_who_sent_last_message?
MessagesHelper::get_employee_who_sent_last_message(sent_from, business)
end
def formatted_response_message_to_employee
readable_sent_from = MessagesHelper::resolve_phone_number(business, sent_from)&.full_name || sent_from
"#{readable_sent_from}: '#{self.content}'" #Temporarily removing URL to try and prevent it from getting picked up from spam filter.
#"#{readable_sent_from}: '#{self.content}' | #{ENV["BASE_URL"]}/messages?conversation=#{self.sent_from}"
end
def should_assign_employee?
return self.is_outbound? && self.employee.nil?
end
def self.mark_all_as_read_from_number(business, number_to_mark_as_read)
business.messages.where(sent_from: number_to_mark_as_read).update_all(has_been_read: true) && business.messages.where(to: number_to_mark_as_read).update_all(has_been_read: true)
end
def set_sent
self.sent = DateTime.now
end
def sent_nil?
sent.nil?
end
def as_json(*)
super.tap do |hash|
# hash["name"] = self.employee.nil? ? self.business.name : "#{self.employee&.first_name} #{self.employee&.last_name[0,1]}" #exclude last name because new users don't enter last name and this was causing an error when sending outbound messages.
hash["name"] = self.employee.nil? ? self.business.name : "#{self.employee&.first_name}"
hash["is_outbound"] = self.is_outbound?
hash["is_inbound"] = self.is_inbound?
end
end
end
require 'seo'
class PublishedJobPage < ApplicationRecord
#MVP: We want to be hyper-targeted here, and to keep it really simple.
belongs_to :job, optional: true
belongs_to :business, optional: true
def publish!(wp_category_ids=[], featured_image_url=nil)
wordpress_auth_token = self.business.wordpress_auth_token.nil? ? ENV["WORDPRESS_AUTH_TOKEN"] : self.business.wordpress_auth_token
wordpress_domain = self.business.wordpress_url.nil? ? ENV["WORDPRESS_URL"] : self.business.wordpress_url
wordpress_username = self.business.wordpress_username.nil? ? "api" : self.business.wordpress_username
begin
created_post = Wordpress.post_job_page(title, final_html, wordpress_auth_token, wordpress_domain, wordpress_username, wp_category_ids, featured_image_url)
created_url = created_post['link']
created_post_id = created_post['id']
unless created_url.blank?
self.update(slug: created_url, wp_post_id: created_post_id)
if self.business.google_service_account_auth_json.nil?
# Google.submit_url_to_search_console(created_url, nil)
else
# Google.submit_url_to_search_console(created_url, self.business.google_service_account_auth_json)
end
else
Rails.logger.error("There was an error publishing the article!")
end
return created_url
rescue => e
Rails.logger.error("Error publishing article: #{e.message}")
Rails.logger.error(e.backtrace.join("\n"))
return nil
end
end
def publish_update_to_wordpress_post
wordpress_auth_token = self.business.wordpress_auth_token.nil? ? ENV["WORDPRESS_AUTH_TOKEN"] : self.business.wordpress_auth_token
wordpress_domain = self.business.wordpress_url.nil? ? ENV["WORDPRESS_URL"] : self.business.wordpress_url
wordpress_username = self.business.wordpress_username.nil? ? "api" : self.business.wordpress_username
begin
updated_post = Wordpress.update_wordpress_post(self.wp_post_id, title, final_html, wordpress_auth_token, wordpress_domain, wordpress_username)
updated_url = updated_post['link'] # Is there even going to be a post here.?
unless updated_url.blank?
self.update(slug: updated_url)
if self.business.google_service_account_auth_json.nil?
# Google.submit_url_to_search_console(updated_url, nil)
else
# Google.submit_url_to_search_console(updated_url, self.business.google_service_account_auth_json)
end
else
Rails.logger.error("There was an error updating the article!")
end
return updated_url
rescue => e
Rails.logger.error("Error updating article: #{e.message}")
Rails.logger.error(e.backtrace.join("\n"))
return nil
end
end
def number_of_views
if self.slug.nil?
return 0
end
return VisitorDeviceInfo.where(business_id: self.business_id, url: self.slug)&.count || nil
end
def final_html
#append the div and bars for the CTA for the estimate.
processed_html = add_responsiveness_to_images_by_appending_class(html)
processed_html = add_schema_markup_to_html(processed_html)
processed_html = add_estimate_cta(processed_html)
return processed_html
end
def add_responsiveness_to_images_by_appending_class(html_to_process)
# Assuming @published_job_page is your model instance and it has an attribute 'html' containing the HTML content
html_content = Nokogiri::HTML::DocumentFragment.parse(html_to_process)
# Find all elements and add 'fr-responsive-image' class if it doesn't already exist
html_content.css('img').each do |img|
existing_classes = img['class'].to_s.split(' ')
unless existing_classes.include?('fr-responsive-image')
img['class'] = existing_classes.push('fr-responsive-image').join(' ')
end
unless existing_classes.include?('aligncenter')
img['class'] = existing_classes.push('aligncenter').join(' ')
end
end
# Update the HTML content in @published_job_page with modified HTML
return html_content.to_html
end
def add_schema_markup_to_html(html_to_process)
#We will eventually add schema.org content for Google to process.
return html_to_process
end
def add_estimate_cta(html_to_process)
booking_link="https://rocketr.us#{self.business&.shortened_url}"
# booking_link = self.business.booking_link
# utm_formatted_booking_link = SEO.generate_utm_formatted_job_request_link(booking_link, html_encoded_title)
html_cta_banner = "
def extract_address_from_title_using_gpt!
address = ChatGPT.extract_address_from_published_job_page_title(self.title)
self.update(address: address)
end
def get_optimized_images
image_urls_to_post = self.optimized_image_urls.nil? ? [] : self.optimized_image_urls.split('|;|')
return image_urls_to_post
end
def publish_to_facebook!
#by default, we will publish the first one that's hooked into the business
facebook_page = self.business.facebook_page_connections.first
# Turn it into an array of image strings by splitting
image_urls_to_post = self.optimized_image_urls.nil? ? [] : self.optimized_image_urls.split('|;|')
facebook_post = facebook_page.post_to_facebook!(self.generated_facebook_post, image_urls_to_post)
#Save the facebook post ID so that we can reference back to it easily and click on a link to view it in Facebook.
self.update(facebook_post_id: facebook_post["id"])
end
end
require 'openai'
require "twilio"
require "llama_bot"
class ChatbotController < ApplicationController
skip_before_action :authenticate_employee!, only: [:message]
def message
#TODO: We want to save this out to the database, and notify us that someone is messaging us.
user_message = params[:message]
context = params[:context]
selectedElement = params[:selectedElement].present? && !params[:selectedElement].empty? ? params[:selectedElement] : nil
webPageId = params[:webPageId]
#if context is a web_page, then we need to inject the webpage into the database instead of the default llama_bot.
#I also need to encapsulate this logic better into the Chatbotmodel
#TODO: Determine if this context is looking at a web_page.
# begin
# # Twilio.send_text('8013499925', "LLamaBot: #{context}: #{user_message}") #sends text message to Kody to notify of new mobile registration.
# rescue => e
# puts "Error sending text message: #{e.message}"
# end
# Use the ChatGPT module to get a response
# chatgpt_response = OpenAI.completion("You are a Chatbot Helping consult with a User to Build Features on top of LLamaPress for their Ruby on Rails Software Project. There are three scenarios that you need to respond differently to: If the user is telling you 'Yes', 'Sure', 'Sounds Good', or any other confirmation message, please respond with, 'Great! We can have it done in 3 days. How should we contact you when we're done?' If the user is giving you contact information such as a phone number or email, respond with: 'We'll have it done in 3 days. We will contact you when we're done.' If the user says Thank you or Goodbye, just respond with '👍'. For anything else, please respond to the user giving them a clear, concise outline on how you would build what their message says, and THEN ask them: 'Do you want me to build it for you?' You will now begin your conversation with the user. The User Says: '#{user_message}'")
# chatgpt_response = "msg: #{user_message} context: #{context} element: #{selectedElement} webPageId: #{webPageId}"
chatgpt_response = LLamaBot.completion(user_message, context, selectedElement, webPageId)
render json: { response: chatgpt_response }
end
end
class ReportsController < ApplicationController
include ReportsHelper
before_action :set_ranges
# GET /marketing_report.json
def marketing_sources_report
render json: ReportsHelper::marketing_report_data(@start_range, @end_range, current_business).to_json
end
# GET /income_report.json
def income_report
render json: ReportsHelper.income_report_data(@start_range, @end_range, current_business).to_json
end
# GET /income_category_report.json
def income_report_by_category
render json: ReportsHelper.income_report_by_category_data(@start_range, @end_range, current_business).to_json
end
# GET /revenue_by_sales_rep_report.json
def revenue_by_sales_rep_report
render json: ReportsHelper.revenue_by_sales_rep_data(@start_range, @end_range, current_business).to_json
end
# GET /revenue_by_marketing_source.json
def revenue_by_marketing_source
render json: ReportsHelper.marketing_revenue_report_data(@start_range, @end_range, current_business).to_json
end
def production_revenue_by_employee
render json: ReportsHelper.production_revenue_by_employee(@start_range, @end_range, current_business).to_json
end
def timeclocked_by_employee
render json: ReportsHelper.timeclocked_by_employee(@start_range, @end_range, current_business).to_json
end
def marketing_csv
@customers = current_business.customers.where('customers.created_at >= ? AND customers.created_at <= ?', @start_range, @end_range)
send_data @customers.to_csv, filename: "#{current_business.name} invoices & estimates #{Date.today}.csv"
end
def invoices_csv
@invoices = current_business.invoices.where('invoices.created_at >= ? AND invoices.created_at <= ?', @start_range, @end_range)
send_data @invoices.to_csv, filename: "#{current_business.name} invoices & estimates #{Date.today}.csv"
end
def appointments_csv
@appointments = current_business.employee_jobs.where('employee_jobs.time_start >= ? AND employee_jobs.time_end <= ?', @start_range, @end_range)
send_data @appointments.to_csv, filename: "#{current_business.name} appointments #{@start_range} - #{@end_range}.csv"
end
def set_ranges
@start_range = params[:start]&.to_datetime
@end_range = params[:end]&.to_datetime
end
end
class StackController < ApplicationController
layout 'stack/application'
def index; end
def show()
params[:page] = 'index' unless params[:page]
render 'stack/'+ params[:page]
end
end
class CallsController < ApplicationController
# load_and_authorize_resource
skip_before_action :authenticate_employee!, only: [:record_call, :update, :index, :edit, :active, :record_outbound_call]
before_action :authenticate_secret_key, only: [:update, :index, :edit, :active]
before_action :set_call, only: %i[ show edit update destroy ]
include MessagesHelper
require 'twilio'
# GET /calls or /calls.json
# GET /customers.csv
def index
@p = @business.calls.ransack(params[:q])
@calls = @p.result(distinct: true).order(created_at: :desc).paginate(page: params[:page], per_page: 35)
respond_to do |format|
format.html
format.json do
render :json => @calls.to_json
end
format.csv {send_data current_business.calls.to_csv, filename: "#{current_business.name} calls #{Date.today}.csv"}
end
end
# GET /calls/1 or /calls/1.json
def show
respond_to do |format|
format.html
format.json do
render :json => @call.to_json
end
end
end
# GET /calls/new
def new
@call = Call.new
end
# GET /calls/1/edit
def edit
end
# POST /calls or /calls.json
def create
@call = current_business.calls.new(call_params)
respond_to do |format|
if @call.save
format.html { redirect_to @call, notice: "Call was successfully created." }
format.json { render :show, status: :created, location: @call }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @call.errors, status: :unprocessable_entity }
end
end
end
#This route is so we can save the number that we are getting ready to connect to
# This gets called by the mobile app, before outbound dialing our dedicated Twilio number.
def create_pending_outbound_call
number_to_call = params[:phone]
@call = current_business.calls.new(
to: number_to_call,
call_from: current_business.twilio_number,
phone_number_employee_is_calling_from_in_outbound_call: current_employee.phone,
start_time: DateTime.now,
business_id: current_business.id,
is_outbound: true,
customer_phone_number_in_outbound_call: params[:phone],
employee_id: current_employee.id)
respond_to do |format|
if @call.save
format.html { redirect_to @call, notice: "Call was successfully created." }
format.json { render :show, status: :created, location: @call }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @call.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /calls/1 or /calls/1.json
def update
respond_to do |format|
if @call.update(call_params)
format.html { redirect_to "/calls?secret_key=#{@call.business.secret_key}", notice: "Call was successfully updated." }
format.json { render :show, status: :ok, location: @call }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @call.errors, status: :unprocessable_entity }
end
end
end
# GET /calls/active
def active
#TODO: There's a bug right now where the 'active' attribute is not properly being set. Especially for calls that aren't actually active.
active_calls = @business.calls.where(is_active: true)
if (active_calls.size == 0)
redirect_to "/calls?secret_key=#{@business.secret_key}", notice: "There are no active calls right now"
else
redirect_to "/calls/#{active_calls.first.id}/edit?secret_key=#{@business.secret_key}", notice: "Found this current active call"
end
end
# DELETE /calls/1 or /calls/1.json
def destroy
@call.destroy
respond_to do |format|
format.html { redirect_to calls_url, notice: "Call was successfully destroyed." }
format.json { head :no_content }
end
end
# POST /record_call
def record_call # This gets hit on both a new phone call coming in, and after a phone call gets closed
call_info = Twilio.fetch_call_info(params[:CallSid])
call_in_progress = call_info.status == "in-progress" #How to figure out if it's the first call or the 2nd call
call_from = call_info.from #The customer number.
call_to = call_info.to #The twilio number.
business, business_twilio_number, customer_number = Twilio.parse_incoming_request(call_to, call_from)
if business.nil?
Rails.logger.info("Just received an incoming call to a phone number we don't recognize as belonging to any business! Call to: #{call_to}, call_from: #{call_from}")
end
#So you can send caller id..? In a push notification, and take them to the customer record. AND save the call recording URL, create a new call object, etc.
#Q: Do we want to save the call as an S3 object? I would say yes, so that it's in our system and we have that control. S3 is dirt cheap.
#Why do we want business?
caller_customer_record = MessagesHelper.resolve_phone_number_customer_records_only(business, customer_number) #Why? So we can add the call info into the system.
if call_in_progress #added a call in progress!
added_call = business.calls.create(
to: call_to,
call_from: call_from,
start_time: DateTime.now,
caller_name: caller_customer_record&.full_name,
customer_id: caller_customer_record&.id,
recording_uri: params[:RecordingUrl],
call_sid: params[:CallSid],
is_outbound: false,
is_active: true)
else #Is a call they're closing out.
closed_call = business.calls.find_by(call_sid: params[:CallSid])
if (closed_call.nil?)
Rails.logger.info("Tried to update an existing call recording in calls/record_call, but it looks like the call wasn't saved correctly the first time!")
else
closed_call.update(is_active: false, end_time: DateTime.now)
closed_call.transcribe_to_text!
end
end
end
def record_outbound_call
call_info = Twilio.fetch_call_info(params[:CallSid])
call_from = call_info.from #The twilio (business) number.
call_to = call_info.to #The customer's phone number.
business, business_twilio_number, employee_number = Twilio.parse_incoming_request(call_from, call_to)
existing_call = business.calls.where(is_active: true, phone_number_employee_is_calling_from_in_outbound_call: employee_number, is_outbound: true)
if existing_call.empty?
Rails.logger.info("ERROR! !! Couldn't find the existing call!")
return
#TODO: What do we do if this outbound call wasn't first created..?
# Well, then we can't save the recording to the server, and should add it to the error log
else
existing_call = existing_call.last
end
customer_number = existing_call.customer_phone_number_in_outbound_call
if business.nil?
Rails.logger.info("OUTBOUND CALL: Just received an outbound request to record an outbound call to a phone number we don't recognize as belonging to any business! Call to: #{call_to}, call_from: #{call_from}")
end
caller_customer_record = MessagesHelper.resolve_phone_number_customer_records_only(business, customer_number) #Why? So we can add the call info into the system.
existing_call.update(
to: customer_number,
call_from: business_twilio_number,
end_time: DateTime.now,
customer_id: caller_customer_record&.id,
caller_name: caller_customer_record&.full_name,
recording_uri: params[:RecordingUrl],
call_sid: params[:CallSid],
is_active: false)
end
# POST /calls/1/transcribe
def ai_transcription
@call = Call.find(params[:id])
@call.transcribe_to_text!
end
# POST /calls/1/parse_transcription
def parse_transcription_with_ai
@call = Call.find(params[:id])
@call.ai_parse_info_from_transcript
end
private
# Use callbacks to share common setup or constraints between actions.
def set_call
@business = @business || current_business
@call = @business.calls.find(params[:id])
end
def authenticate_secret_key
if current_employee.nil?
@business = Business.where(secret_key: params[:secret_key]).first
if @business.nil?
access_denied_redirect
end
else
@business = current_employee.business
authenticate_employee!
end
end
# Only allow a list of trusted parameters through.
def call_params
params.require(:call).permit(:to, :call_from, :caller_name, :recording_uri, :price, :start_time, :end_time, :duration, :status, :uri, :notes, :outcome)
end
end
class PublishedJobPagesController < ApplicationController
include PublishedJobPagesHelper
require "wordpress"
require "seo"
before_action :set_published_job_page, only: %i[ show edit update destroy publish_to_facebook ]
skip_before_action :authenticate_employee!, :only => [:show]
def dashboard
@published_job_pages = current_business.published_job_pages
#@stats = 1 #TODO:
end
# GET /published_job_pages or /published_job_pages.json
def index
@published_job_pages = PublishedJobPage.where(business_id: current_business.id)
# @published_job_pages =
end
# GET /published_job_pages/1 or /published_job_pages/1.json
def show
handle_breadcrumb @published_job_page.title, url_for(@published_job_page)
if @published_job_page.slug.nil? #not published
@wordpress_categories = Wordpress.get_categories(current_business.wordpress_auth_token, current_business.wordpress_url)
else
@wordpress_categories = nil
end
end
# GET /published_job_pages/new
def new
@published_job_page = PublishedJobPage.new
# @published_job_page.custom_title_prompt, @published_job_page.custom_html_prompt = ChatGPT.create_SEO_prompts(
# keywords, address, notes, current_business.name, current_employee.first_name, customer_name, optimized_image_urls
# )
handle_breadcrumb "Publish Job", "/published_job_pages/new?invoice_id=#{params[:invoice_id]}"
unless params[:invoice_id].nil?
@invoice = current_business.invoices.find(params[:invoice_id])
@job_summary_without_price = @invoice.summarize
@job_summary_with_price = @invoice.summarize(include_price=true)
@notes = @job_summary_without_price
@keywords = @invoice&.business&.target_keywords || ''
@customer_name = @invoice&.customer&.first_name || ''
@address = "#{@invoice&.job&.location&.city}, #{@invoice&.job&.location&.state}" || ''
@company_name = current_business.name
@owner_name = current_employee.first_name
@custom_title_prompt = "Given the keywords '#{@keywords}', the city name '#{@address}', and key highlights from invoice notes: #{@notes},
mentioning a call to action, generate a compelling and SEO-optimized article title. The company name is: #{@company_name}.
The title should incorporate these elements naturally, focusing on local searches for #{@keywords} in #{@address}.
Highlight the affordable pricing, warranty, or free consultation as a unique benefit.
The title should only be 60 characters or less. Use action-oriented language to encourage clicks, address common homeowner concerns,
and ensure clarity and conciseness for Google search visibility. Do not enclose the title in quotation marks."
@custom_html_prompt = "Create an SEO-optimized HTML blog article about your service company's job, using the following details. The content should be nested inside a
# GET /published_job_pages/1/edit
def edit
end
def create_without_existing_job
address = params[:city]
job_description = params[:description]
job_cost = params[:cost]
job_special_notes = params[:specialNotes]
#still need to decide if we are going to ask for the customer name in the form about the job
customer_name = "homeowner"
#handle empty cost and empty notes... need to decdide what the default values would be
image_urls = PublishedJobPagesHelper.extract_image_urls_from_attachments_in_params_array(params[:attachments])
@published_job_page = current_business.published_job_pages.new
#***Temporary... we should find a way to set these ourselves... the user should not have to pass in keywords
#keywords = params[:published_job_page][:keywords].blank? ? current_business.target_keywords&.split(',') : params[:published_job_page][:keywords]&.split(',')
keywords = [address]
#**We aren't using jobs anymore...so a lot of this isr
optimized_image_urls = SEO.get_optimized_images(image_urls, address, keywords)
#TODO: This is bad practice to store a list of image urls as a comma delimited list. Woudl be better to use active storage pointing directly to the AWS fr optimized seo bucket, but we can't do that on rails 6.0
optimized_image_urls_string = optimized_image_urls.join('|;|')
#set by serializing them
@published_job_page.optimized_image_urls = optimized_image_urls_string
@published_job_page.save!
#TODO: Eventually we will break this up so that we can save the prompts, and create an intermediary step before publishing the job.
title_prompt, html_prompt = ChatGPT.create_SEO_prompts(
keywords,
address,
job_description + ". " + job_special_notes,
current_business.name,
current_employee.first_name,
customer_name,
optimized_image_urls)
# , current_employee.first_name, "https://rocketr.us#{current_business.shortened_url}",
@published_job_page.prompt_for_title = title_prompt
@published_job_page.prompt_for_html_page = html_prompt
# @published_job_page.prompt_for_image_seo = #TODO:
# ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"Lowo3kGH0an6IHgQtLmj6cBC2qfKT1TW3fmOlgT51aCdrADgJKy2hwiU/+85IuZGRfqHiptmm3AjwI8ahtEwuw==", "published_job_page"=>"Job Notes: Underground drains (2) clogged, Ace Gutter is doing the Rain Gutter Replacement\r\nItem: Hydrojet first underground drain and diagnostic , Price: $200.0\r\nItem: Hydrojet 9 underground drains. All drains pass drain inspection , Price: $100.0\r\nItem: Material: Ads adapter x 6. 6” riser pipe , Clean out basin x 2, Price: $175.0\r\nItem: 30 day drain guarantee. If any drain does not function in the next 30 days, feel free to give us a call and we will gladly come back to get it cleared out. Anything beyond 30 days is not warranteed. , Price: $0.0\r\nTotal Price: $1275.0", "keywords"=>"affordable, stump grinding, drain clearing", "customer_name"=>"Charles", "address"=>"Sandy, UT"} permitted: false>, "attachments"=>"7498", "IMG_8074.JPG"=>"7501", "IMG_8073.JPG"=>"7499"} permitted: false>, "commit"=>"Create Published job page", "controller"=>"published_job_pages", "action"=>"create"} permitted: false>
title, html_content = ChatGPT.create_content(title_prompt, html_prompt)
# @published_job_page.slug = created_url We don't know the URL since we're not created it any more.
@published_job_page.html = html_content
@published_job_page.title = title
@published_job_page.target_keywords = keywords
@published_job_page.address = address
# We want to save the prompt that we push into ChatGPT, in order to track the peformance of these prompts.
# This will help us iterate on the prompts and see what's working best, vs. what's not.
@published_job_page.prompt_for_title = title_prompt
@published_job_page.prompt_for_html_page = html_prompt
# @published_job_page.prompt_for_image_seo = #TODO: eventually we will use ChatGPT to come up with better image names, alt tags, etc.
# Generate prompts for each platform
facebook_post_prompt, nextdoor_post_prompt, instagram_post_prompt, google_my_business_post_prompt = ChatGPT.create_social_media_prompts(
keywords,
address,
job_description + ". " + job_special_notes,
current_business.name,
current_employee.first_name,
customer_name,
optimized_image_urls
)
@published_job_page.prompt_for_facebook_post = facebook_post_prompt
@published_job_page.prompt_for_nextdoor_post = nextdoor_post_prompt
@published_job_page.prompt_for_instagram_post = instagram_post_prompt
@published_job_page.prompt_for_google_my_business_post = google_my_business_post_prompt
@published_job_page.business_id = current_business.id
# Generate content for each platform
facebook_post, nextdoor_post, instagram_post, google_my_business_post = ChatGPT.create_social_media_content(
facebook_post_prompt,
nextdoor_post_prompt,
instagram_post_prompt,
google_my_business_post_prompt
)
@published_job_page.generated_facebook_post = facebook_post
@published_job_page.generated_nextdoor_post = nextdoor_post
@published_job_page.generated_instagram_post = instagram_post
@published_job_page.generated_google_my_business_post = google_my_business_post
respond_to do |format|
if @published_job_page.save
# format.html { redirect_to created_url, notice: "Success! Published Job Post #{title}." }
format.html { redirect_to published_job_page_url(@published_job_page), notice: "Success! Published Job Post #{title}." }
format.json { render :show, status: :created, location: @published_job_page }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @published_job_page.errors, status: :unprocessable_entity }
end
end
end
# POST /published_job_pages or /published_job_pages.json
def create
# {
# "utf8" => "✓",
# "authenticity_token" => "vSPpc/NfWYRoAUItC3RxdJ/Xj4vyZFcqXnxU6dsqHLWkN/MyOeX3wvjqPA4VqAncTuhmot0XqyYqZIhH+uyxag==",
# "published_job_page" => {
# "notes" => "Job Notes: Removal of tree stump and roots\r\nItem: Stump grind small stump and roots in front flower bed leaving stump 4-6\" below grade. Clean up and backfill. (Mulch removal not included)\r\nItem: Add stump in backyard ",
# "keywords" => "stump removal, stump grinding, stump poisoning, trunk removal",
# "customer_name" => "Alayna",
# "address" => "Riverton, UT",
# "job_id" => "11921",
# "invoice_id" => "6318"
# },
# "attachments" => {
# "IMG_0232.JPG" => "9270"
# },
# "commit" => "Create Published job page",
# "controller" => "published_job_pages",
# "action" => "create"
# }
image_urls = PublishedJobPagesHelper.extract_image_urls_from_attachments_in_params(params[:attachments])
#TODO: Is this saving under the business ID? Answer: no.
@published_job_page = current_business.published_job_pages.new(published_job_page_params) #TODO.. then from here, I want to call the SEO POST
keywords=params[:published_job_page][:keywords]&.split(',')
optimized_image_urls = SEO.get_optimized_images(image_urls, params[:published_job_page][:address], keywords)
#TODO: This is bad practice to store a list of image urls as a comma delimited list. Woudl be better to use active storage pointing directly to the AWS fr optimized seo bucket, but we can't do that on rails 6.0
optimized_image_urls_string = optimized_image_urls.join('|;|')
#set by serializing them
@published_job_page.optimized_image_urls = optimized_image_urls_string
@published_job_page.save!
title_prompt, html_prompt = nil, nil
if params[:published_job_page][:use_custom_prompt_on_generation]
#TODO: if _use_custom_prompt_on_generation:
title_prompt = params[:published_job_page][:custom_title_prompt]
html_prompt = params[:published_job_page][:custom_html_prompt]
html_prompt << optimized_image_urls.join(',')
else
# this immediately creates the prompts and sets them in here.
title_prompt, html_prompt = ChatGPT.create_SEO_prompts(
keywords,
params[:published_job_page][:address],
params[:published_job_page][:notes],
current_business.name,
current_employee.first_name,
params[:published_job_page][:customer_name],
optimized_image_urls)
end
# byebug #TODO: take the params[:attachments], and auto attach to this new page.
# Attach images to the published job page
# optimized_image_blobs.each do |blob|
# # blob_id = blob.id
# # blob = ActiveStorage::Blob.find_by(id: blob_id)
# # byebug
# io = StringIO.new(blob.download) #not working?
# filename = blob.filename
# @published_job_page.images.attach(blob, filename: filename, io: io, service_name: :amazon_fr_seo_optimized_images) if blob.present?
# end
# optimized_image_urls = SEO.get_optimized_images(image_urls, params[:published_job_page][:address], keywords)
# TODO: Can we just attach the images right here so that they stay carried along with this published job page post?
# (This is setting it up so they don't have to select the images again.)
#current_employee.first_name, "https://rocketr.us#{current_business.shortened_url}",
@published_job_page.prompt_for_title = title_prompt
@published_job_page.prompt_for_html_page = html_prompt
# @published_job_page.prompt_for_image_seo = #TODO:
# ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"Lowo3kGH0an6IHgQtLmj6cBC2qfKT1TW3fmOlgT51aCdrADgJKy2hwiU/+85IuZGRfqHiptmm3AjwI8ahtEwuw==", "published_job_page"=>"Job Notes: Underground drains (2) clogged, Ace Gutter is doing the Rain Gutter Replacement\r\nItem: Hydrojet first underground drain and diagnostic , Price: $200.0\r\nItem: Hydrojet 9 underground drains. All drains pass drain inspection , Price: $100.0\r\nItem: Material: Ads adapter x 6. 6” riser pipe , Clean out basin x 2, Price: $175.0\r\nItem: 30 day drain guarantee. If any drain does not function in the next 30 days, feel free to give us a call and we will gladly come back to get it cleared out. Anything beyond 30 days is not warranteed. , Price: $0.0\r\nTotal Price: $1275.0", "keywords"=>"affordable, stump grinding, drain clearing", "customer_name"=>"Charles", "address"=>"Sandy, UT"} permitted: false>, "attachments"=>"7498", "IMG_8074.JPG"=>"7501", "IMG_8073.JPG"=>"7499"} permitted: false>, "commit"=>"Create Published job page", "controller"=>"published_job_pages", "action"=>"create"} permitted: false>
@published_job_page.business_id = current_business.id
# right now this immediatley creates the content in this CREATE action. In the future, we might want to break this out so it doesn't take as long.
# AD
title, html_content = ChatGPT.create_content(title_prompt, html_prompt)
# @published_job_page.slug = created_url We don't know the URL since we're not created it any more.
@published_job_page.html = html_content
@published_job_page.title = title
@published_job_page.target_keywords = params[:published_job_page][:keywords]
@published_job_page.address = params[:published_job_page][:address]
# We want to save the prompt that we push into ChatGPT, in order to track the peformance of these prompts.
# This will help us iterate on the prompts and see what's working best, vs. what's not.
@published_job_page.prompt_for_title = title_prompt
@published_job_page.prompt_for_html_page = html_prompt
# @published_job_page.prompt_for_image_seo = #TODO: eventually we will use ChatGPT to come up with better image names, alt tags, etc.
# Generate prompts for each platform
facebook_post_prompt, nextdoor_post_prompt, instagram_post_prompt, google_my_business_post_prompt = ChatGPT.create_social_media_prompts(
keywords,
params[:published_job_page][:address],
params[:published_job_page][:notes],
current_business.name,
current_employee.first_name,
params[:published_job_page][:customer_name],
image_urls
)
@published_job_page.prompt_for_facebook_post = facebook_post_prompt
@published_job_page.prompt_for_nextdoor_post = nextdoor_post_prompt
@published_job_page.prompt_for_instagram_post = instagram_post_prompt
@published_job_page.prompt_for_google_my_business_post = google_my_business_post_prompt
@published_job_page.business_id = current_business.id
# Generate content for each platform
facebook_post, nextdoor_post, instagram_post, google_my_business_post = ChatGPT.create_social_media_content(
facebook_post_prompt,
nextdoor_post_prompt,
instagram_post_prompt,
google_my_business_post_prompt
)
@published_job_page.generated_facebook_post = facebook_post
@published_job_page.generated_nextdoor_post = nextdoor_post
@published_job_page.generated_instagram_post = instagram_post
@published_job_page.generated_google_my_business_post = google_my_business_post
respond_to do |format|
if @published_job_page.save
# format.html { redirect_to created_url, notice: "Success! Published Job Post #{title}." }
format.html { redirect_to published_job_page_url(@published_job_page), notice: "Success! Published Job Post #{title}." }
format.json { render :show, status: :created, location: @published_job_page }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @published_job_page.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /published_job_pages/1 or /published_job_pages/1.json
def update
# if the html content has changed, then we need to re-publish to wordpress
should_update_wordpress_post = @published_job_page.html != published_job_page_params[:html] || @published_job_page.title != published_job_page_params[:title]
respond_to do |format|
if @published_job_page.update(published_job_page_params)
if should_update_wordpress_post
@published_job_page.publish_update_to_wordpress_post
end
format.html { redirect_to published_job_page_url(@published_job_page), notice: "Published job page was successfully updated." }
format.json { render :show, status: :ok, location: @published_job_page }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @published_job_page.errors, status: :unprocessable_entity }
end
end
end
def publish_job_to_wordpress
@published_job_page = current_business.published_job_pages.find(params[:id])
wp_categories = params[:wp_categories] || []
featured_image_url = params[:featured_image_url] || nil
if featured_image_url.blank?
featured_image_url = @published_job_page.get_optimized_images.first
end
respond_to do |format|
if @published_job_page.publish!(wp_categories, featured_image_url)
format.html { redirect_to published_job_page_url(@published_job_page), notice: "Published to Wordpress!" }
format.json { render :show, status: :ok, location: @published_job_page }
else
format.html { render :edit, status: :unprocessable_entity, notice: "Failed to publish" }
format.json { render json: @published_job_page.errors, status: :unprocessable_entity}
end
end
end
# POST /published_job_pages/:id/publish_to_facebook
def publish_to_facebook
@published_job_page.publish_to_facebook!
end
# DELETE /published_job_pages/1 or /published_job_pages/1.json
def destroy
@published_job_page.destroy
respond_to do |format|
format.html { redirect_to published_job_pages_url, notice: "Published job page was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_published_job_page
@published_job_page = PublishedJobPage.find(params[:id])
end
# Only allow a list of trusted parameters through.
def published_job_page_params
params.require(:published_job_page).permit(:job_id, :business_id, :invoice_id, :title, :html, :slug, :address)
end
end
class EmployeeJobTypesController < ApplicationController
before_action :set_employee_job_type, only: [:show, :edit, :update, :destroy]
# GET /employee_job_types
# GET /employee_job_types.json
def index
@employee_job_types = current_business.employee_job_types.ransack(params[:q]).result
respond_to do |format|
format.html
format.json {render json: @employee_job_types}
end
end
# GET /employee_job_types/1
# GET /employee_job_types/1.json
def show
end
# GET /employee_job_types/new
def new
@employee_job_type = current_business.employee_job_types.new
end
# GET /employee_job_types/1/edit
def edit
end
# POST /employee_job_types
# POST /employee_job_types.json
def create
@employee_job_type = current_business.employee_job_types.new(employee_job_type_params)
respond_to do |format|
if @employee_job_type.save
format.html { redirect_to @employee_job_type, notice: 'Employee job type was successfully created.' }
format.json { render :show, status: :created, location: @employee_job_type }
else
format.html { render :new }
format.json { render json: @employee_job_type.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /employee_job_types/1
# PATCH/PUT /employee_job_types/1.json
def update
respond_to do |format|
if @employee_job_type.update(employee_job_type_params)
format.html { redirect_to @employee_job_type, notice: 'Employee job type was successfully updated.' }
format.json { render :show, status: :ok, location: @employee_job_type }
else
format.html { render :edit }
format.json { render json: @employee_job_type.errors, status: :unprocessable_entity }
end
end
end
# DELETE /employee_job_types/1
# DELETE /employee_job_types/1.json
def destroy
@employee_job_type.destroy
respond_to do |format|
format.html { redirect_to employee_job_types_url, notice: 'Employee job type was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_employee_job_type
@employee_job_type = current_business.employee_job_types.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def employee_job_type_params
params.require(:employee_job_type).permit(:name, :description)
end
end
class JobRequestsController < ApplicationController
before_action :set_job_request, only: [:edit, :update, :destroy, :delete_image]
skip_before_action :authenticate_employee!, :only => [:new, :create, :show, :business_not_found, :get_signed_s3_url_for_uploading_images] #For customers scheduling appointments with our business
before_action :set_business_from_secret_key, only: [:new, :create, :show]
# GET /job_requests
# GET /job_requests.json
def index
@job_requests = current_business.job_requests.all
respond_to do |format|
format.html {
@job_requests
}
format.json {
render json: @job_requests.to_json
}
end
end
# GET /job_requests/unscheduled.json
def unscheduled
request_ids_that_have_been_assigned = current_business.jobs.includes([:job_request]).where.not(job_request_id: nil).map {|j| j&.job_request&.id}
@requests = current_business.job_requests.where.not(id: [request_ids_that_have_been_assigned]).order('created_at DESC')
render json: @requests.to_json
end
# GET /job_requests/1
# GET /job_requests/1.json
def show
@job_request = @business.job_requests.find(params[:id])
end
# GET /job_requests/new
def new
#set business
@job_request = @business.job_requests.new
@job_request.first_name = params[:first_name]
@job_request.last_name = params[:last_name]
@job_request.street_address = params[:street_address]
@job_request.city = params[:city]
@job_request.email = params[:email]
@job_request.zip = params[:zip]
@job_request.phone = params[:phone]
@job_request.state = params[:state] || @business.state
@title = "#{@business.name} | Submit Request"
@use_business_logo_as_favicon = true
@secret_key = @business.secret_key
#TODO: Parse UTM here that will be useful.? Or will there be a type of redirection anyways?
# https://rocketr.us/l/45?utm_source=fieldrocket.us&utm_content=&utm_campaign=fr_seo
# params["utm_campaign"]: # "fr_seo"
# params["utm_content"]: # "stump_x_drain_system_installation"
# params["utm_source"]: # "fieldrocket.us"
@job_request.utm_source = params["utm_source"]
@job_request.utm_campaign = params["utm_campaign"]
@job_request.utm_content = params["utm_content"]
if params["utm_source"]
utm_json_object = {"utm_source": params["utm_source"], "utm_content": params["utm_content"], "utm_campaign": params["utm_campaign"]}
Event.create(resource: "Job Link Clicked", name: utm_json_object.to_json, business_id: @business&.id, notes: utm_json_object.to_json)
end
end
# GET /job_requests/1/edit
def edit
end
# POST /job_requests
# POST /job_requests.json
def create
@job_request = @business.job_requests.new(job_request_params)
#TODO: clean up zip code, so it ignores spaces and everything like that (had an issue where a user submitted a job request with a space in the zip code, and it caused issues).
respond_to do |format|
if @job_request.save
# This event was breaking the job request page because THERE IS NO CURRENT_EMPLOYEE in here.
# Event.create(resource: "JobRequest", name: "Created", resource_id: @job_request.id, employee_id: current_employee.id)
format.html { redirect_to job_request_url(@job_request, params: {secret_key: @job_request.business.secret_key}), notice: 'Job request was successfully created.'}
format.json { render :show, status: :created, location: @job_request }
else
format.html { render :new }
format.json { render json: @job_request.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /job_requests/1
# PATCH/PUT /job_requests/1.json
def update
respond_to do |format|
if @job_request.update(job_request_params)
format.html { redirect_to @job_request, notice: 'Job request was successfully updated.' }
format.json { render :show, status: :ok, location: @job_request }
else
format.html { render :edit }
format.json { render json: @job_request.errors, status: :unprocessable_entity }
end
end
end
# DELETE /job_requests/1
# DELETE /job_requests/1.json
def destroy
@job_request.destroy
respond_to do |format|
format.html { redirect_to job_requests_url, notice: 'Job request was successfully destroyed.' }
format.json { head :no_content }
end
end
def business_not_found
end
#DELETE /job_requests/1/image
def delete_image
delete_uploaded_image(@job_request, params[:image_id])
end
#POST /jobs_requests/pre_signed_s3_url_for_upload
def get_signed_s3_url_for_uploading_images #TODO:! Duplicate code in businesses_controller
@blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: params[:filename],
byte_size: params[:byte_size],
checksum: params[:checksum],
content_type: params[:content_type]
)
render json: { url: @blob.service_url_for_direct_upload(expires_in: 30.minutes), headers: @blob.service_headers_for_direct_upload, signed_id: @blob.signed_id, key: @blob.key }, status: 200
end
private
# Use callbacks to share common setup or constraints between actions.
def set_job_request
@job_request = current_business.job_requests.find(params[:id])
end
#Check if secret key is set, and if it is, set @business
def set_business_from_secret_key
if !current_business.nil?
@business = current_business
return
end
businesses = Business.where(secret_key: params[:secret_key])
if businesses.count == 0
if params[:job_request].nil? #need to flash a message that it was incorrect..
# flash[:error] = "Looks like this business's online booking link has expired" #TODO: Currently flashing this message liks this doesn't actually display it..
redirect_to action: :business_not_found
return
end
businesses = Business.where(secret_key: params[:job_request][:secret_key])
end
if businesses.count > 0 || current_business != nil
@business = businesses.first
else
access_denied_redirect
end
end
# Never trust parameters from the scary internet, only allow the white list through.
def job_request_params
params.require(:job_request).permit(:first_name, :last_name, :street_address, :city, :zip, :state, :phone, :email, :notes, :marketing_sources_id, :customer_id, :location_id, :utm_source, :utm_campaign, :utm_content, images: [])
end
def job_request_link_couldnt_map_to_business_redirect
redirect_to
end
end
class AppointmentColorsController < ApplicationController
before_action :set_appointment_color, only: %i[ show edit update destroy ]
# GET /appointment_colors or /appointment_colors.json
def index
@appointment_colors = current_business.appointment_colors.all
end
# GET /appointment_colors/1 or /appointment_colors/1.json
def show
end
# GET /appointment_colors/new
def new
@appointment_color = current_business.appointment_colors.new
end
# GET /appointment_colors/1/edit
def edit
end
# POST /appointment_colors or /appointment_colors.json
def create
@appointment_color = current_business.appointment_colors.new(appointment_color_params)
respond_to do |format|
if @appointment_color.save
format.html { redirect_to @appointment_color, notice: "Appointment color was successfully created." }
format.json { render :show, status: :created, location: @appointment_color }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @appointment_color.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /appointment_colors/1 or /appointment_colors/1.json
def update
respond_to do |format|
if @appointment_color.update(appointment_color_params)
format.html { redirect_to @appointment_color, notice: "Appointment color was successfully updated." }
format.json { render :show, status: :ok, location: @appointment_color }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @appointment_color.errors, status: :unprocessable_entity }
end
end
end
# DELETE /appointment_colors/1 or /appointment_colors/1.json
def destroy
@appointment_color.destroy
respond_to do |format|
format.html { redirect_to appointment_colors_url, notice: "Appointment color was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_appointment_color
@appointment_color = current_business.appointment_colors.find(params[:id])
end
# Only allow a list of trusted parameters through.
def appointment_color_params
params.require(:appointment_color).permit(:color, :name)
end
end
class LinkShortenerController < ApplicationController
skip_before_action :authenticate_employee!, :only => [:shorten]
before_action :set_link_shortener, only: [:shorten]
# GET /l
# GET /l.json
def shorten
domain = request.env["HTTP_HOST"].dup
domain.slice! "www."
Rails.logger.info("Just got a shortened URL request with domain of: #{domain}")
if domain[0] == "h"
# root to: redirect(path: '/pages/home')
redirect_to "https://perform.homeservice.cloud/l/#{params[:id]}"
return
elsif domain[0] == "t"
redirect_to "https://utahtreeco.homeservice.cloud/l/#{params[:id]}"
return
elsif domain[0] == "m"
redirect_to "https://app.homeservice.cloud/l/#{params[:id]}"
return
elsif domain[0] == "s"
redirect_to "https://qa.homeservice.cloud/l/#{params[:id]}"
return
end
if @link.nil?
flash[:error] = "The link has expired"
redirect_to '/'
return
end
redirect_to @link.url
end
private
# Use callbacks to share common setup or constraints between actions.
def set_link_shortener
@link = LinkShortener.where(key: params[:id])&.first
if @link.nil?
@link = LinkShortener.where(id: params[:id])&.first
end
end
end
class EmployeesController < ApplicationController
#TODO: These permissions are an issue. Right now, I'm just disabling the permission checking for these actions, when I really should figure out how to allow/disallow actions based on the permissions. Maybe I can ask Owen for help on this issue
include PopulateEmployeeOptions
load_and_authorize_resource :except => [:get_filter_settings, :update_filter_settings, :schedule_json, :update_employee_job_times, :save_device_token, :verify_access_code, :timesheets]
before_action :set_employee, only: [:destroy, :edit, :update, :daily_summary]
skip_before_action :authenticate_employee!, only: [:verify_access_code]
# GET /employees
def index
Rails.logger.info "Ransack Parameters: #{params[:q].inspect}"
query = current_business.employees
.where(deactivated_at: nil)
.where(marketing_va: [nil, false])
.ransack(params[:q]).result(distinct: true)
puts query.to_sql
respond_to do |format|
format.html {
@query = query
@employees = @query.paginate(page: params[:page], per_page: 20)
}
format.json {
@employees = query
render :json => @employees
}
end
end
# GET /employees/:id/info
def show
render json: current_business.employees.find(params[:id]).to_json
end
# GET /employee/new
def new
@employee = current_business.employees.new
end
# POST /employees/manual_create
# POST /employees/manual_create
def create
@employee = current_business.employees.new(employee_params)
@employee.avatar.attach(employee_params[:avatar])
respond_to do |format|
if @employee.save
#Tracking employee sign up event
Analytics.identify(
user_id: @employee.id,
traits: { name: @employee.full_name, email: @employee.email, created_at: DateTime.now }
)
Analytics.track( user_id: @employee.id, event: 'Employee Signed Up', properties: { plan: 'Employee' })
format.html { redirect_to employees_path, flash: {success: 'Employee was successfully created.'} }
format.json { render :show, status: :created, location: @employee}
else
@employee.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :new }
format.json { render json: @employee.errors, status: :unprocessable_entity }
end
end
end
# GET /employees/:id/edit
def edit
end
# DELETE /employees/:id
def destroy
if @employee.set_deactive!
flash[:success] = 'Employee deactivated'
redirect_to employees_url
else
flash[:error] = 'Failed to deactivated employee.'
redirect_to employees_url
end
end
def fullcalendar_resources
render :json => current_business.employees.includes([:branch]).where(:business_id => current_business.id).reject{|e| e.branch&.name.blank? || !e.is_active?}.map{|e| {id: e.id, title: e.full_name, branch: e.branch&.name}}
end
#GET /employees/filter_settings
def get_filter_settings
#TODO: For now, just return the current employee as the filter settings.
if current_employee.appointment_filter == nil
render :json => {"employees": current_employee.id.to_s}
else
render :json => {"employees": current_employee.appointment_filter.sub('[','').sub(']','')}
end
end
#POST /employees/filter_settings
def update_filter_settings
current_employee.set_appointment_filter(params["employees"])
render :json => {:response => "Successfully updated employee's appointent filter"}, :status => 200
end
# GET /employees/schedule
def schedule
# TODO: Redirects to the jobs/new path. This should be temporary until we can get the schedule.html.erb working with the fullcalendar editable version
# (Currently not working because it doesn't have the necessary modal form partials in it's class, and I didn't
# Just want to copy & paste them)
redirect_to :controller => 'jobs', :action => 'new'
end
def schedule_json
Rails.logger.info("Made it to schedule_json")
if params[:start] and params[:end]
events = current_employee.get_employee_dates_from_range(params[:start], params[:end])
Rails.logger.info("Employees_Controller schedule_json response: #{events.to_json}")
render :json => events
return
end
Rails.logger.info("Returning empty array!!")
Rails.logger.info("Params == #{params}")
render :json => JSON[{}] #return empty json if params weren't what we expected.
end
#PUT /update_employee_job_times
def update_employee_job_times
ej = EmployeeJob.update(params["employee_job_id"], :time_start => params["time_start"], :time_end => params["time_end"], :date => params["date"])
bad_request = ej.errors.count > 0;
if bad_request
render :json => {:response => ej.formatted_error_messages}, :status => 409
else
render :json => {:response => "Success"}, :status => 200
end
end
# PUT /employees/:id
def update
if !employee_params[:avatar].nil?
if @employee.avatar.attached?
@employee.avatar.purge
@employee.avatar.attach(employee_params[:avatar])
else
@employee.avatar.attach(employee_params[:avatar])
end
end
if @employee.update employee_params
flash[:success] = 'Employee updated successfully.'
redirect_to employees_path
else
flash[:error] = 'Failed to edit employee.'
render :edit
end
end
# GET /check_authenticated
def check_if_authenticated
#See if we are currently authenticated or not..
render :json => {:response => "success"}, :status => 200
end
def daily_summary
@employee_jobs = @employee.employee_jobs.where('time_start BETWEEN ? AND ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
end
# POST /save_device_token
def save_device_token
# Device tokens are so that we can send push notifications to the correct devices that are associated with certain employees.
device_token = current_employee.device_tokens.create(device_token_params)
render :json => device_token
end
def deactivated_employees
@query = current_business.employees.not_active.ransack(params[:q]).result(distinct: true)
@employees = @query.paginate(page: params[:page], per_page: 20)
end
def restore_user
if @employee.set_active!
flash[:success] = 'Employee restored!'
redirect_to employees_url
else
flash[:error] = 'Failed to restore employee.'
redirect_to employees_url
end
end
# POST /employees/:id/verify_access_code
# params : {access_code: , business_id: }
def verify_access_code
respond_to do |format|
format.json{
master_access_code = "990099"
access_code = params[:access_code]
@business = Business.find(params[:business_id])
@employee = @business.employees.find_by(access_token: params[:access_code])
# verified = Twilio.verify_access_code(@business.phone_number, access_code)
#TEMPORARILY PUTTING TRUE here so that any access code verifies. I just want to make sure the user can verify every time for now.
if true || @employee || access_code == master_access_code #backdoor master access code for ease of dev.
@employee = @employee == nil ? @business.employees.last : @employee
@employee.create_authentication_token_and_activate_account!
render status: :created, json: {employee: @employee, authentication_token: @employee.authentication_token}
else
render status: :unauthorized, json: {error: "The access token did not match!"}
end
}
end
end
def timesheets
@employee.timesheets
end
def kill_user_session
reset_session
end
def register_for_notification
notification_type = params[:notification_type]
if current_business.notifications_lists.create(employee_id: current_employee.id, notification_type: notification_type)
render :json => {:response => "Successfully registered for notification"}, :status => 200
else
render :json => {:response => "Couldn't register for notification"}, :status => 500
end
end
def unregister_for_notification
notification_type = params[:notification_type]
# notification_subscriptions = current_business.notifications_lists.where(employee_id: current_employee.id, notification_type: notification_type)
if current_business.notifications_lists.where(employee_id: current_employee.id, notification_type: notification_type).delete_all
render :json => {:response => "Successfully unregistered from notification"}, :status => 200
else
render :json => {:response => "Couldn't unregister from notification"}, :status => 500
end
end
def notifications_summary
render json: current_employee.registered_notifications_summary.to_json
end
def tutorial_complete
current_employee.update(user_needs_tutorial: false)
render json: {response: current_employee}
end
private
def employee_params
params.require(:employee).permit(:first_name, :last_name, :email, :phone, :password, :group_id, :branch_id, :avatar, :color)
end
def device_token_params
params.permit(:token)
end
def set_employee
@employee = current_business.employees.find params[:id]
end
end
class WebPageHistoriesController < ApplicationController
before_action :set_web_page_history, only: %i[ show edit update destroy ]
skip_before_action :authenticate_employee!
layout 'llama_bot'
# GET /web_page_histories or /web_page_histories.json
def index
@web_page_histories = WebPageHistory.all
end
# GET /web_page_histories/1 or /web_page_histories/1.json
def show
@web_page_history = WebPageHistory.find(params[:id])
end
# GET /web_page_histories/new
def new
@web_page_history = WebPageHistory.new
end
# GET /web_page_histories/1/edit
def edit
end
# POST /web_page_histories or /web_page_histories.json
def create
@web_page_history = WebPageHistory.new(web_page_history_params)
respond_to do |format|
if @web_page_history.save
format.html { redirect_to web_page_history_url(@web_page_history), notice: "Web page history was successfully created." }
format.json { render :show, status: :created, location: @web_page_history }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @web_page_history.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /web_page_histories/1 or /web_page_histories/1.json
def update
respond_to do |format|
if @web_page_history.update(web_page_history_params)
format.html { redirect_to web_page_history_url(@web_page_history), notice: "Web page history was successfully updated." }
format.json { render :show, status: :ok, location: @web_page_history }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @web_page_history.errors, status: :unprocessable_entity }
end
end
end
# DELETE /web_page_histories/1 or /web_page_histories/1.json
def destroy
@web_page_history.destroy
respond_to do |format|
format.html { redirect_to web_page_histories_url, notice: "Web page history was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_web_page_history
@web_page_history = WebPageHistory.find(params[:id])
end
# Only allow a list of trusted parameters through.
def web_page_history_params
params.require(:web_page_history).permit(:content, :web_page_id)
end
end
class HomePagesController < ApplicationController
before_action :set_home_page, only: %i[ show edit update destroy ]
# GET /home_pages or /home_pages.json
def index
@home_pages = HomePage.all
end
# GET /home_pages/1 or /home_pages/1.json
def show
end
# GET /home_pages/new
def new
@home_page = HomePage.new(business_id: current_business.id)
end
# GET /home_pages/1/edit
def edit
end
# POST /home_pages or /home_pages.json
def create
@home_page = HomePage.new(home_page_params)
respond_to do |format|
if @home_page.save
format.html { redirect_to home_page_url(@home_page), notice: "Home page was successfully created." }
format.json { render :show, status: :created, location: @home_page }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @home_page.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /home_pages/1 or /home_pages/1.json
def update
respond_to do |format|
if @home_page.update(home_page_params)
format.html { redirect_to "/home_pages/#{@home_page.id}/edit", notice: "Home page was successfully updated." }
format.json { render :show, status: :ok, location: @home_page }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @home_page.errors, status: :unprocessable_entity }
end
end
end
# DELETE /home_pages/1 or /home_pages/1.json
def destroy
@home_page.destroy
respond_to do |format|
format.html { redirect_to home_pages_url, notice: "Home page was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_home_page
@home_page = HomePage.find(params[:id])
end
# Only allow a list of trusted parameters through.
def home_page_params
params.require(:home_page).permit(:domain, :primary_color, :secondary_color, :tertiary_color, :facebook_url, :instagram_url, :header_description, :about_us, :services_header, :our_team_description, :business_id, :raw_html, :navbar_html, :footer_html, :use_raw_html)
end
end
class InvoiceTemplatesController < ApplicationController
before_action :set_invoice_template, only: [:show, :edit, :update, :destroy, :remove_logo]
#skip_before_action :verify_authenticity_token, :only => [:create]
# GET /invoice_templates
# GET /invoice_templates.json
def index
@invoice_templates = current_business.invoice_templates.all
respond_to do |format|
format.html {
handle_breadcrumb "Templates", invoice_templates_path
# @invoice_templates = current_business.invoice_templates.all
}
format.json do
render :json => @invoice_templates.to_json
end
end
end
# GET /invoice_templates/1
# GET /invoice_templates/1.json
def show
handle_breadcrumb "Template #{@invoice.name}", invoice_template_path(@invoice)
respond_to do |format|
format.html
format.json do
render :json => @invoice.to_json(:include => :line_item_template)
end
end
end
# GET /invoice_templates/new
def new
handle_breadcrumb "New Template", new_invoice_template_path
@invoice = current_business.invoice_templates.new
end
# GET /invoice_templates/1/edit
def edit
handle_breadcrumb "Edit Template #{@invoice.name}", edit_invoice_template_path
end
# POST /invoice_templates
# POST /invoice_templates.json
def create
is_converted_params = params[:invoice_template].class == String
_params = is_converted_params ? converted_invoice_template_params : invoice_template_params
@invoice = current_business.invoice_templates.new(_params)
line_items = is_converted_params ? JSON.parse(params[:invoice_template])["line_items"] : params[:invoice_template][:line_items]
line_items = Hash[line_items.each_with_index.map{ |a,i| [i, a] }] if line_items.class.to_s.include?('Array')
respond_to do |format|
if @invoice.save
logo_upload(params[:logo]) if params[:logo].class != String
line_items&.each do |key, value|
new_item = @invoice.line_item_template.new(description: value["description"], price: value["price"].to_d, product_id: value["product_id"], name: value["name"], quantity: value["quantity"])
new_item.save! #TODO: We'll want to eventually use the accepts_nested_attributes to not have to manually parse this as it's kind of brittle
end
format.html { redirect_to @invoice, flash: {success: 'Invoice template was successfully created.' }}
# format.json { render :show, status: :created, location: @invoice }
format.json { render json: @invoice, status: :ok}
else
@invoice.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :new, flash: {error: "Invoice template couldn't be created." }}
format.json { render json: @invoice.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /invoice_templates/1
# PATCH/PUT /invoice_templates/1.json
def update
is_converted_params = params[:invoice_template].class == String
_params = is_converted_params ? converted_invoice_template_params : invoice_template_params
line_items = is_converted_params ? JSON.parse(params[:invoice_template])["line_items"] : params[:invoice_template][:line_items]&.to_unsafe_h&.to_a
#The reverse is necessary or the order will get all flipped.
reverse_line_items = line_items&.reverse
reverse_line_items = Hash[reverse_line_items.each_with_index.map{ |a,i| [i, a] }] if reverse_line_items.class.to_s.include?('Array')
respond_to do |format|
if @invoice.update!(_params)
logo_upload(params[:logo]) if params[:logo].class != String
@invoice.line_items.delete_all
reverse_line_items&.each do |key, value|
new_item = @invoice.line_item_template.new(description: value["description"], price: value["price"].to_d, product_id: value["product_id"], name: value["name"], quantity: value["quantity"])
new_item.save! #TODO: We'll want to eventually use the accepts_nested_attributes to not have to manually parse this as it's kind of brittle
end
format.html { redirect_to @invoice, flash: {success: 'Invoice template was successfully updated.' }}
format.json { render json: @invoice, status: :ok}
# format.json { render :show, status: :ok, location: @invoice }
else
@invoice.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @invoice.errors, status: :unprocessable_entity }
end
end
end
# DELETE /invoice_templates/1
# DELETE /invoice_templates/1.json
def destroy
@invoice.destroy
respond_to do |format|
format.html { redirect_to invoice_templates_url, flash: {success: 'Invoice template was successfully destroyed.' }}
format.json { head :no_content }
end
end
def logo_upload(img)
if File.size(img.tempfile) <= 25000000 #Don't let people upload files larger than 25 Mb.
@invoice.logo.attach(img)
else
flash[:error] = "logo file is too large (20 Megabytes limit)" #We say 20 MB to play it safe. Real limit is 25 MB.
end
end
def remove_logo
@invoice.logo.purge
respond_to do |format|
if !@invoice.logo.attached?
format.json { render json: {}, status: :ok}
else
format.json { render json: {}, status: :unprocessable_entity}
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_invoice_template
@invoice = current_business.invoice_templates.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def invoice_template_params
params.require(:invoice_template).permit(
:name,
:description,
:business_name,
:redirect_url,
line_item_template: [:name, :product_id, :quantity, :description, :price])
end
def converted_invoice_template_params
ActionController::Parameters.new(JSON.parse(params[:invoice_template])).permit(
:name,
:description,
:business_name,
:redirect_url,
line_item_template: [:name, :product_id, :quantity, :description, :price]
)
end
end
class RostersController < ApplicationController
load_and_authorize_resource
before_action :set_roster, only: [:edit, :update, :destroy, :show]
# GET /rosters
# GET /rosters.json
def index
respond_to do |format|
format.html {
@rosters = current_business.rosters
}
format.json {
business = current_business
business_id = business.id
rosters = Roster.where(:business_id => business_id)
render :json => rosters
}
end
end
def schedule_json #TODO: Rename. This is for the fullcalendar.
if params[:start] and params[:end] and params[:roster]
@roster_whose_schedule_to_find = Roster.find(params[:roster])
@roster_weekly_schedule = @roster_whose_schedule_to_find.get_roster_dates_from_range(params[:start], params[:end])
render :json => @roster_weekly_schedule
return
end
render :json => JSON[{}] #return empty json if params weren't what we expected.
end
# GET /rosters/1
# GET /rosters/1.json
def show
#s@rotser = current_business.rosters.find(params[:id])
end
# GET /rosters/new
def new
@roster = Roster.new
end
# GET /rosters/1/edit
def edit
end
# POST /rosters
# POST /rosters.json
def create
@roster = Roster.new
@roster.name = roster_params["name"]
@roster.business_id = current_business.id
params["roster"]["employees"].each do |emp|
if emp != ""
employee_to_add = current_business.employees.find(emp)
@roster.employees << employee_to_add
end
end
respond_to do |format|
if @roster.save
format.html { redirect_to @roster, notice: 'Roster was successfully created.' }
format.json { render :show, status: :created, location: @roster }
else
format.html { render :new }
format.json { render json: @roster.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /rosters/1
# PATCH/PUT /rosters/1.json
def update
@roster.employees.destroy_all
@roster.name = roster_params["name"]
@roster.business_id = current_business.id
params["roster"]["employees"].each do |emp|
if emp != ""
employee_to_add = current_business.employees.find(emp)
@roster.employees << employee_to_add
end
end
respond_to do |format|
if @roster.save
format.html { redirect_to @roster, notice: 'Roster was successfully updated.' }
format.json { render :show, status: :ok, location: @roster }
else
format.html { render :edit }
format.json { render json: @roster.errors, status: :unprocessable_entity }
end
end
end
#PUT /update_employee_job_times
def update_roster_job_times
ej = RosterJob.update(params["employee_job_id"], :time_start => params["time_start"], :time_end => params["time_end"], :date => params["date"])
bad_request = ej.errors.count > 0;
if bad_request
render :json => {:response => ej.formatted_error_messages}, :status => 409
else
render :json => {:response => "Success"}, :status => 200
end
end
# DELETE /rosters/1
# DELETE /rosters/1.json
def destroy
EmployeeRoster.where(roster_id: @roster.id).destroy_all
RosterJob.where(roster_id: @roster.id).destroy_all
@roster.destroy
respond_to do |format|
format.html { redirect_to rosters_url, notice: 'Roster was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_roster
@roster = current_business.rosters.find params[:id]
end
# Never trust parameters from the scary internet, only allow the white list through.
def roster_params
params.require(:roster).permit(:name, employees: [:id])
end
end
class PaymentsController < ApplicationController
# load_and_authorize_resource
before_action :set_payment, only: [:show, :edit, :update, :destroy]
before_action :set_invoice, only: [:edit, :new]
before_action :set_job, only: [:edit, :new, :create]
# GET /payments
# GET /payments.json
def index
@start_date = params[:start_date]
@end_date = params[:end_date]
if @start_date && @end_date
@payments = current_business.payments.includes(invoice:[:job, :job_business, :invoice_to]).where("paid_on >= ? AND paid_on <= ?", @start_date, @end_date).paginate(page: params[:page], per_page: 10)
else
@payments = current_business.payments.includes(invoice:[:job, :job_business, :invoice_to, :direct_customer]).paginate(page: params[:page], per_page: 10)
end
end
# GET /payments/1
# GET /payments/1.json
def show
end
# GET /payments/new
def new
@payment = current_business.payments.new
end
# GET /payments/1/edit
def edit
end
# POST /payments
# POST /payments.json
def create
@payment = current_business.payments.new(payment_params)
respond_to do |format|
if @payment.save
format.html { redirect_to @payment, notice: 'Payment was successfully created.' }
format.json { render :show, status: :created, location: @payment }
else
format.html { render :new, flash: {error: "Couldn't save"}}
format.json { render json: @payment.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /payments/1
# PATCH/PUT /payments/1.json
def update
@payment.job_id = @payment.job_id
@payment.invoice_id = @payment.invoice_id
respond_to do |format|
if @payment.update(payment_params)
format.html { redirect_to @payment, notice: 'Payment was successfully updated.' }
format.json { render :show, status: :ok, location: @payment }
else
format.html { render :edit, flash: {:error => "Couldn't Update"}}
format.json { render json: @payment.errors, status: :unprocessable_entity }
end
end
end
# DELETE /payments/1
# DELETE /payments/1.json
def destroy
invoice = @payment.invoice
@payment.destroy
respond_to do |format|
format.html { redirect_to "/invoices/#{invoice&.id}/payments", notice: 'Payment was successfully destroyed.' }
format.json { head :no_content }
end
end
# POST /payments/1/refund
# POST /payments/1/refund.json
def refund
Rails.logger.info("Now issuing a refund for the payment: #{@payment.id} of #{@payment.amount} for customer #{@payment.invoice&.customer_name} invoice #{@payment.invoice&.id}")
respond_to do |format|
if @payment.refund_ibx_payment
format.html { redirect_to @payment.invoice, notice: 'Payment was successfully refunded.' }
format.json { render :show, status: :ok, location: @payment.invoice }
else
format.html { render :edit, flash: {:error => "Couldn't refund"}}
format.json { render json: @payment.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_payment
@payment = Payment.find(params[:id])
end
def set_invoice
if params[:invoice_id]
@invoice = current_business.invoices.find(params[:invoice_id])
elsif !@payment&.invoice.nil?
@invoice = @payment.invoice
end
end
def set_job
if params[:job_id]
@job = current_business.jobs.find(params[:job_id])
elsif !@payment&.job.nil?
@job = @payment.job
end
end
# Never trust parameters from the scary internet, only allow the white list through.
def payment_params
params.require(:payment).permit(:paid_on, :payment_type, :invoice_id, :amount, :notes, :job_id)
end
end
# frozen_string_literal: true
class Employees::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation/new
# def new
# super
# end
# POST /resource/confirmation
# def create
# super
# end
# GET /resource/confirmation?confirmation_token=abcdef
# def show
# super
# end
# protected
# The path used after resending confirmation instructions.
# def after_resending_confirmation_instructions_path_for(resource_name)
# super(resource_name)
# end
# The path used after confirmation.
# def after_confirmation_path_for(resource_name, resource)
# super(resource_name, resource)
# end
end
# frozen_string_literal: true
class Employees::SessionsController < Devise::SessionsController
# before_action :configure_sign_in_params, only: [:create]
# GET /resource/sign_in
def new
@title = "Field Rocket | Sign In"
super
end
# POST /resource/sign_in
# Copied the code from the Devise repo here: https://github.com/heartcombo/devise/blob/9f5b83750eef89710b8f2f1e0969c308023e2f05/app/controllers/devise/sessions_controller.rb
def create
respond_to do |format|
format.html {
super
Event.create(resource: "Employee", name: "Web Sign In", resource_id: resource.id, employee_id: resource.id)
cookies.signed[:employee_id] = resource.id
}
format.json{
#Allow authentication with the id and authentication_token if the auth token is present and correct.
#http://blog.plataformatec.com.br/2019/01/custom-authentication-methods-with-devise/
if params[:authentication_token]
self.resource = warden.authenticate!(:authentication_token_strategy)
else
self.resource = warden.authenticate!(auth_options)
end
sign_in(resource_name, resource)
employee = Employee.find(resource.id)
render :status => 200, :json => { :message => "Success" , employeeId: resource.id, business: employee.business, employeeEmail: resource.email }
unless params[:suppress_analytics] #there are times we don't want to track an event we signed in (like if we signed in for them to troubleshoot something)
Event.create(resource: "Employee", name: "Mobile Sign In", resource_id: resource.id, employee_id: resource.id)
end
cookies.signed[:employee_id] = resource.id
}
end
end
# DELETE /resource/sign_out
def destroy
respond_to do |format|
format.html {
super
}
format.json{
signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
if signed_out
render :json => { :message => "Sign Out Successfully", employeeId: "", business: "" }
end
}
end
end
# POST /resource/sign_in
# def create
# super
# end
# DELETE /resource/sign_out
# def destroy
# super
# end
# protected
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_in_params
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
# end
end
# frozen_string_literal: true
class Employees::UnlocksController < Devise::UnlocksController
# GET /resource/unlock/new
# def new
# super
# end
# POST /resource/unlock
# def create
# super
# end
# GET /resource/unlock?unlock_token=abcdef
# def show
# super
# end
# protected
# The path used after sending unlock password instructions
# def after_sending_unlock_instructions_path_for(resource)
# super(resource)
# end
# The path used after unlocking the resource
# def after_unlock_path_for(resource)
# super(resource)
# end
end
# frozen_string_literal: true
class Employees::PasswordsController < Devise::PasswordsController
# GET /resource/password/new
def new
@title = "Field Rocket | Reset Password"
super
end
# POST /resource/password
# def create
# super
# end
# GET /resource/password/edit?reset_password_token=abcdef
# def edit
# super
# end
# PUT /resource/password
# def update
# super
# end
# protected
# def after_resetting_password_path_for(resource)
# super(resource)
# end
# The path used after sending reset password instructions
# def after_sending_reset_password_instructions_path_for(resource_name)
# super(resource_name)
# end
end
# frozen_string_literal: true
class Employees::RegistrationsController < Devise::RegistrationsController
# before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
# def new
# super
# end
# POST /resource
# def create
# super
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
# protected
# If you have extra params to permit, append them to the sanitizer.
# def configure_sign_up_params
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
# end
# If you have extra params to permit, append them to the sanitizer.
# def configure_account_update_params
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
# end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
class Employees::InvitationsController < Devise::InvitationsController
before_action :config_permitted_invite_params, only: :create
before_action :config_permitted_accept_params, only: :update
private
# This is called when creating invitation.
# Returns an instance of resource class.
def invite_resource
# Set invited employee's business to current business.
super do |u|
u.business = current_business
end
end
def config_permitted_accept_params
devise_parameter_sanitizer.permit(:accept_invitation, keys: [:first_name, :last_name, :phone])
end
def config_permitted_invite_params
devise_parameter_sanitizer.permit(:invite, keys: [:group_id])
end
end
# frozen_string_literal: true
class Employees::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# You should configure your model like this:
# devise :omniauthable, omniauth_providers: [:twitter]
# You should also create an action method in this controller like this:
# def twitter
# end
# More info at:
# https://github.com/plataformatec/devise#omniauth
# GET|POST /resource/auth/twitter
# def passthru
# super
# end
# GET|POST /users/auth/twitter/callback
# def failure
# super
# end
# protected
# The path used when OmniAuth fails
# def after_omniauth_failure_path_for(scope)
# super(scope)
# end
end
class TwilioController < ApplicationController
# before_action :set_branch, only: [:show, :edit, :update, :destroy, :daily_summary]
before_action :turn_off_304_not_modified, only: [:businessmp3url, :get_phone_call_tree_settings]
skip_before_action :authenticate_employee!, only: [:message_status_from_twilio, :user_is_making_outbound_call_from_dialing_twilio_number]
require 'twilio'
# Returns TWIML xml that tells Twilio how to handle this call.
def call_resource_url
# byebug
end
# GET /available_twilio_phone_numbers?area_code=801
def get_available_twilio_phone_numbers_for_purchase
#Only handle JSON requests.
#Paramaters need to include the area code:
area_code = params[:area_code]
available_phone_numbers = Twilio.find_available_phone_numbers(area_code)
render :json => {:response => "Success", numbers: available_phone_numbers}, :status => 200
end
# POST /businessmp3?phone_tree_greeting_mp3=
def upload_business_mp3
current_business.phone_tree_greeting_mp3.attach(params[:phone_tree_greeting_mp3])
render :json => {:url => current_business.phone_tree_greeting_mp3.service_url}, :status => 200
end
# GET /businessmp3url
def get_uploaded_business_mp3_service_url
if current_business.phone_tree_greeting_mp3.attached?
render :json => {:url => current_business.phone_tree_greeting_mp3.service_url}, :status => 200
else
render :json => {:url => nil}, :status => 200
end
end
# POST /message_status_from_twilio
# Twilio POSTs to this route in order to update us about the message status after we attempt to send it.
# This is so we can tell the user if a message was unsuccessful in sending.
def message_status_from_twilio
sid = params[:SmsSid]
status = params[:SmsStatus]
error = params[:ErrorCode] #TODO: Can we combine this into a different error message.?
# error_message = ? #TODO: we should get the message status and try to read it..
message = Message.find_by(twilio_sid: sid)
message.update_columns(status: status, error_message: error)
render json: message
end
# POST /purchase_twilio_phone_number
def purchase_available_phone_number
number_to_purchase = params[:number_to_purchase]
#this should be a boolean
test_purchase_number = !params[:test_number_to_purchase].nil? ? ActiveModel::Type::Boolean.new.cast(params[:test_number_to_purchase]) : nil
hardcoded_test_number = "3853655596" #This is a static number that
if (test_purchase_number)
Business.where(twilio_number: hardcoded_test_number).update_all(twilio_number: nil) #Remove the hardcoded test number from all other businesses first, since it's being claimed by this one and we can't have two businesses with the same twilio number.
end
#remove special characters in order to adhere to databse phone number style convention
cleaned_number_for_database = test_purchase_number ? hardcoded_test_number : number_to_purchase.tr('^0-9', '')
number_to_purchase = test_purchase_number ? Twilio.internationalize(hardcoded_test_number) : Twilio.internationalize(number_to_purchase) #add the +1 to the phone number..
was_successful = Twilio.purchase_available_phone_number(number_to_purchase, current_business.name, test_purchase_number)
if (was_successful)
current_business.update(twilio_number: cleaned_number_for_database)
#Now that they've purchased a number, let's go and add all those demo messages and phone calls into the database.
current_business.add_demo_messages
current_business.add_demo_phonecalls
render :json => {:response => "Success", number: cleaned_number_for_database}, :status => 200
else
render :json => {:response => "Couldn't Register Phone Number"}, :status => 422
end
end
#POST /savePhoneCallTreeSettings
def save_phone_call_tree_settings
phone_numbers_in_order = []
["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"].each do |i|
val = params["phoneTreeForwardSetting#{i}"]
phone_numbers_in_order.append(val.tr('^0-9', '')) #https://stackoverflow.com/a/35603648/6374022
end
if current_business.set_new_phone_tree_branch_settings(phone_numbers_in_order)
render :json => {:response => "Success", message: "Successfully updated phone number forwarding settings"}, :status => 200
else
render :json => {:response => "Error", message: "Couldn't update phone number forwarding settings"}, :status => 400
end
end
# GET /phoneCallTreeSettings
def get_phone_call_tree_settings
render :json => {:phone_tree_settings => current_business.phone_tree_branch_settings}
end
#This route is for when a user is making an outbound call by directly dialing our '(801) 405-3170' Twilio number
# POST /twilio/connect_user_outbound_call
# params: :phone
def user_is_making_outbound_call_from_dialing_twilio_number
# We have to find the latest call for this user, so that we can find.
number_employee_is_calling_from = Twilio.strip_internationalize(params["From"])
time_threshold = 60.seconds.ago
call = Call.where(
is_outbound: true,
phone_number_employee_is_calling_from_in_outbound_call: number_employee_is_calling_from,
created_at: time_threshold..Time.now
).last
business = Business.find(call.business_id)
if call.nil? || business.nil?
#render a Twilio XML telling them of an error.
#ERROR!
else
customer_phone_number_to_call = call.customer_phone_number_in_outbound_call
twiml_xml = Twilio.employee_is_making_outbound_call_from_dialing_twilio_number(Twilio.internationalize(customer_phone_number_to_call), Twilio.internationalize(business.twilio_number))
render xml: twiml_xml
end
end
private
end
# class MessagesController < ApiController
# include MessagesHelper
# require 'twilio'
# # GET /messages
# # GET /messages.json
# def index #Shows all message threads
# @message_threads = MessagesHelper.message_threads(current_business)
# @business_phone_number = current_business.twilio_number
# @business = current_business
# @thread_to_view = params[:conversation]
# @content = params[:content]
# @employee = current_employee
# end
# # POST /messages
# # def create
# # @message = current_business.messages.new(message_params)
# # if @message.should_assign_employee?
# # @message.employee = current_employee
# # end
# # if @message.save
# # respond_to do |format|
# # format.json {render json: @message, status: :ok}
# # end
# # else
# # Rails.logger.info @message.formatted_error_messages
# # respond_to do |format|
# # format.json { render json: @message.errors, status: :unprocessable_entity }
# # end
# # end
# # end
# #POST /mark_as_read.json
# # def mark_all_messages_as_read
# # #Marks all messages for this phone number as read
# # number = params[:number]
# # if current_business.messages.where(sent_from: number).update_all(has_been_read: true) && current_business.messages.where(to: number).update_all(has_been_read: true)
# # respond_to do |format|
# # format.json {render json: {message: 'accepted'}, status: :accepted}
# # end
# # else
# # respond_to do |format|
# # format.json { render json: {message: 'error'}, status: :internal_server_error}
# # end
# # end
# # end
# def find_name_from_number
# number = params[:phone]
# match = MessagesHelper.resolve_phone_number(current_business, number)
# if match.instance_of? Customer
# render json: {customer: match}, status: :ok
# elsif match.instance_of? CustomerContact
# render json: {contact: match}, status: :ok
# else
# render json: [], status: :ok
# end
# end
# # DELETE /branches/1
# # DELETE /branches/1.json
# # def destroy #Destroys all message threads
# # @branch.destroy
# # respond_to do |format|
# # format.html { redirect_to branches_url, notice: 'Branch was successfully destroyed.' }
# # format.json { head :no_content }
# # end
# # end
# # POST /inbound_sms
# # def inbound_sms
# # Rails.logger.info("Just received twilio sms message with params: #{params}")
# # #Our authentication method for making sure it's actually Twilio sending a message
# # if !Twilio.verify_account_sid(params[:AccountSid])
# # #TODO: This is going to require some custom security measures since we're not authenticated properly.
# # render json: {message: "Unauthenticated"}, status: :unauthorized
# # return
# # end
# # destination_phone_number = params[:To].sub(/^\+1/, '') #removes the first two digits, which should be '+' and '1' for international codes.
# # #We're currently not supporting international codes, and our phone number validator doesn't take them, so we're going to remove these.
# # number_that_inbound_sms_was_sent_from = params[:From].sub(/^\+1/, '')
# # business = Business.where(twilio_number: destination_phone_number).first
# # if business.nil?
# # #business not found.
# # render json: {message: "The inbound phone number does not exist"}, status: :unprocessable_entity
# # return
# # end
# # body = params['Body']
# # #See if the message who sent is is an employee for this business. If it was, it might be a response and we might need to forward the message appropriately.
# # was_inbound_sms_sent_from_employee, employee = was_this_inbound_message_sent_from_employee(number_that_inbound_sms_was_sent_from, business)
# # if was_inbound_sms_sent_from_employee #An employee replied. Let's see who this employee last messaged so we can route it appropriately.
# # Rails.logger.info("It was an inbound sms from employee #{employee.id}, #{employee.full_name}")
# # customer_who_last_messaged_this_employee = employee.phone_number_who_last_texted_this_employee #and what if this is nil...? and the employee messaged into our system? Then we just drop the message....?
# # Rails.logger.info("The last phone number to message this employee was #{customer_who_last_messaged_this_employee}")
# # if customer_who_last_messaged_this_employee.present?
# # business.messages.create(to: customer_who_last_messaged_this_employee, sent_from: business.twilio_number, content: body, sent: DateTime.now, has_been_read: false, employee_id: employee.id)
# # end
# # else #was a customer messaging into our system.. so we don't want to add the message into the system if it's an employee who sent it, because then we'll get duplicate messages.
# # last_employee_who_messaged_this_customer = business.messages.where(to: number_that_inbound_sms_was_sent_from).last&.employee || business.employees.first
# # #We want to see if an employee messaged this customer first, so that we can set that this message was in response to that employee that messaged them in the first place, and so we can route the text message notification to the right employee phone.
# # m = business.messages.create(to: destination_phone_number, sent_from: number_that_inbound_sms_was_sent_from, content: body, sent: DateTime.now, has_been_read: false, employee_id: last_employee_who_messaged_this_customer&.id)
# # end
# # #This is where we could programatically add some logic, bad example
# # # Build response based on given body param
# # twiml = Twilio::TwiML::MessagingResponse.new do |resp|
# # #This is an example of an auto-reply from Twilio. This is if we were detecting certain messages to auto-reply to.
# # if body == 'hello123' #If body contains "ONLINEBOOKING", we know it's an online booking for us. Create a job_request with customer "Online Booking" with the notes as the body of the submission form.
# # true #for syntax reasons.
# # # resp.message body: 'Hi!'
# # elsif body == 'bye123'
# # true #for syntax reasons.
# # # resp.message body: 'Goodbye'
# # end
# # end
# # #This is where we would activate an actioncable push to push a message notification to the application for the business
# # render xml: twiml.to_xml
# # end
# #POST /connect_twilio_call
# # def connect_twilio_call
# # #params need to have the customer phone number, then we'll use the employee that's currently signed in.
# # Twilio.connect_employee_to_customer_through_call(Twilio.internationalize(current_employee.phone), Twilio.internationalize(params[:phone]), current_business.twilio_number)
# # end
# private
# # Never trust parameters from the scary internet, only allow the white list through.
# def message_params
# params.require(:message).permit(:to, :sent_from, :content, :employee_id)
# end
# # def was_this_inbound_message_sent_from_employee(from, business)
# # employee = business.employees.where(phone: from).first
# # return employee != nil, employee
# # end
# end
# class ApiController < ActionController::API
# before_action :authenticate_request
# attr_reader :current_employee
# skip_before_action :authenticate_request, only: [authenticate]
# def authenticate #Route used just to login and get a authentication token for an API user.
# command = AuthenticateUser.call(params[:email], params[:password])
# if command.success?
# render json: { auth_token: command.result, twilio_cient_name: @current_employee.business.twilio_mobile_client_name}
# else
# render json: { error: command.errors }, status: :unauthorized
# end
# end
# private
# def authenticate_request #This is where our API will get authenticated each time.
# @current_employee = AuthorizeApiRequest.call(request.headers).result
# render json: { error: 'Not Authorized' }, status: 401 unless @current_employee
# end
# end
module API
module V1
module Defaults
extend ActiveSupport::Concern
included do
prefix "api"
version "v1", using: :path
default_format :json
format :json
formatter :json,
Grape::Formatter::ActiveModelSerializers
helpers do
def permitted_params
@permitted_params ||= declared(params,
include_missing: false)
end
def logger
Rails.logger
end
def authenticate!
error!('Unauthorized. Invalid or expired token.', 401) unless current_employee
end
def current_employee
# find token. Check if valid.
token = ApiKey.where(access_token: params[:token]).first
if token && !token.expired?
@current_employee = Employee.find(token.employee_id)
else
false
end
end
def current_business
user = current_employee
if user
return user.business
end
end
end
rescue_from ActiveRecord::RecordNotFound do |e|
error_response(message: e.message, status: 404)
end
rescue_from ActiveRecord::RecordInvalid do |e|
error_response(message: e.message, status: 422)
end
end
end
end
end
module API
module V1
class Base < Grape::API
mount API::V1::Employees
mount API::V1::Auth
end
end
end
module API #Authentication? How do we handle authentication? Should we have it inherit from the base controller?
module V1
class Employees < Grape::API
include API::V1::Defaults
resource :employees do
desc "Return all users"
get "" do
authenticate!
current_business.employees.all
end
desc "Return a user"
params do
requires :id, type: String, desc: "ID of the user"
end
get ":id" do
authenticate!
current_business.employees.where(id: permitted_params[:id]).first!
end
end
end
end
end
# https://mikecoutermarsh.com/rails-grape-api-key-authentication/
# /api/auth
module API
module V1
class Auth < Grape::API
include API::V1::Defaults
resource :auth do
desc "Creates and returns access_token if valid login"
params do
requires :email, type: String, desc: "Email address"
requires :password, type: String, desc: "Password"
end
post :login do
user = Employee.where(email: params[:email].downcase).first
if user && user.valid_password?(params[:password])
key = ApiKey.create(employee_id: user.id)
{token: key.access_token}
else
error!('Unauthorized.', 401)
end
end
desc "Returns pong if logged in correctly"
params do
requires :token, type: String, desc: "Access token."
end
get :ping do
authenticate!
{ message: "pong" }
end
end
end
end
end
module API
class Base < Grape::API
mount API::V1::Base #Note: the mount keyword tells your rails app that another application (in this case the Grape API) exists here.
end
end
class MessagesController < ApplicationController
# load_and_authorize_resource
skip_before_action :authenticate_employee!, only: [:inbound_sms, :find_name_from_number]
# skip_before_action :verify_authenticity_token, only: [:inbound_sms]
before_action :turn_off_304_not_modified, only: [:message_thread_from_number, :unread_messages_count, :unread_messages_previews]
include MessagesHelper
require 'twilio'
# GET /messages
# GET /messages.json
def index #Shows all message threads
@page_number = params[:page].blank? ? 0 : params[:page].to_i
respond_to do |format|
format.html {
handle_breadcrumb("Messages", messages_path)
@thread_to_view = params[:conversation]
@message_threads = MessagesHelper.message_threads(current_business, @thread_to_view, 25, @page_number) # Make sure we load the messages from a selected conversation, even if those messages are older than the cut off date.
@business_phone_number = current_business.twilio_number
@business = current_business
@content = params[:content]
@employee = current_employee
}
format.json {
turn_off_304_not_modified
render json: MessagesHelper.most_recent_previews(current_business, @page_number)
}
end
end
# POST /messages
def create #if a sent_from isn't defined, let's go ahead and just set it to the current business's twilio phone number.
@message = current_business.messages.new(message_params)
if message_params[:sent_from].blank?
@message.sent_from = current_business.twilio_number
end
if message_params[:employee_id].blank?
@message.employee_id = current_employee.id
end
if @message.should_assign_employee?
@message.employee = current_employee
end
image_present = !params[:id].blank? || params[:file].class != String
if image_present
@message.skip_deliver_on_create(true) #we don't want to deliver until after the image is attached
end
if @message.save
if image_present
if params[:id].blank? #if the user is sending a new image that wasn't uploaded to a customer record
@message.image.attach(params[:file])
else #user is sending an image that's already been saved to the customer record
attachment = ActiveStorage::Attachment.find_by(id: params[:id])
file = open(attachment.service_url)
filename = attachment.filename.to_s
content_type = attachment.content_type
@message.image.attach( io:file, filename: filename, content_type: content_type) if attachment
end
@message.deliver_on_create #Deliver after we attach the image correctly
end
respond_to do |format|
format.json {render json: @message, status: :ok}
end
else
Rails.logger.info @message.formatted_error_messages
respond_to do |format|
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
#POST /mark_as_read.json
def mark_all_messages_as_read
#Marks all messages for this phone number as read
number = params[:number]
if current_business.messages.where(sent_from: number).update_all(has_been_read: true) && current_business.messages.where(to: number).update_all(has_been_read: true)
respond_to do |format|
format.json {render json: {message: 'accepted'}, status: :accepted}
end
else
respond_to do |format|
format.json { render json: {message: 'error'}, status: :internal_server_error}
end
end
end
def find_name_from_number
number = params[:phone]
match = nil
if current_business.nil? && !params[:secret_key].nil? #This means it's coming from the job request page.
business = Business.find_by(secret_key: params[:secret_key])
if business.nil?
return access_denied_redirect
end
match = MessagesHelper.resolve_phone_number(business, number)
else
match = MessagesHelper.resolve_phone_number(current_business, number)
end
if match.instance_of? Customer
render json: {customer: match}, status: :ok
elsif match.instance_of? CustomerContact
render json: {contact: match}, status: :ok
else
render json: [], status: :ok
end
end
# DELETE /branches/1
# DELETE /branches/1.json
def destroy #Destroys all message threads
@branch.destroy
respond_to do |format|
format.html { redirect_to branches_url, notice: 'Branch was successfully destroyed.' }
format.json { head :no_content }
end
end
# POST /inbound_sms
def inbound_sms
Rails.logger.info("Just received twilio sms message with params: #{params}")
#Our authentication method for making sure it's actually Twilio sending a message
if !Twilio.verify_account_sid(params[:AccountSid])
#TODO: This is going to require some custom security measures since we're not authenticated properly.
render json: {message: "Unauthenticated"}, status: :unauthorized
Rails.logger.info("The message failed when verifying the twilio account sid, responding with unauthorized response")
return
end
business, destination_phone_number, number_that_inbound_sms_was_sent_from = Twilio.parse_incoming_request(params[:To], params[:From])
if business.nil? #business not found.
render json: {message: "The inbound phone number does not exist"}, status: :unprocessable_entity
return
end
body = params['Body']
num_media = params['NumMedia'].to_i
image_params = Hash.new
inbound_message_was_mms = num_media > 0 #This NumMedia param is how Twilio tell us how many attached mms are in this message, so if there's more than 0, we know that this message is an mms
if inbound_message_was_mms
#CHECK FOR MULTIPLE MMS's
if(num_media > 1)
for a in 0..(num_media - 1) do
#Send each MMS individually
image_params[:media_url] = params["MediaUrl#{a}"]
image_params[:content_type] = params["MediaContentType#{a}"]
image_params[:file_name] = image_params[:media_url].split('/').last
last_employee_who_messaged_this_customer = business.messages.where(to: number_that_inbound_sms_was_sent_from).last&.employee || business.employees.first
new_message = business.messages.create(to: destination_phone_number, sent_from: number_that_inbound_sms_was_sent_from, content: (a == num_media -1 ? body : ""), has_been_read: false, employee_id: last_employee_who_messaged_this_customer&.id)
new_message.action_cable_update_messages_view_and_attach_image( image_params )
end
else
#otherwise there is only 1 MMS
Rails.logger.info("The inbound message was an mms message!")
# Prepare the file information for attachment.
image_params[:media_url] = params["MediaUrl#{0}"]
image_params[:content_type] = params["MediaContentType#{0}"]
image_params[:file_name] = image_params[:media_url].split('/').last
last_employee_who_messaged_this_customer = business.messages.where(to: number_that_inbound_sms_was_sent_from).last&.employee || business.employees.first
new_message = business.messages.create(to: destination_phone_number, sent_from: number_that_inbound_sms_was_sent_from, content: body, has_been_read: false, employee_id: last_employee_who_messaged_this_customer&.id)
new_message.action_cable_update_messages_view_and_attach_image( image_params )
end
else
#See if the message who sent is is an employee for this business. If it was, it might be a response and we might need to forward the message appropriately.
was_inbound_sms_sent_from_employee, employee = was_this_inbound_message_sent_from_employee(number_that_inbound_sms_was_sent_from, business)
if was_inbound_sms_sent_from_employee #An employee replied. Let's see who this employee last messaged so we can route it appropriately.
Rails.logger.info("It was an inbound sms from employee #{employee.id}, #{employee.full_name}")
customer_who_last_messaged_this_employee = employee.phone_number_who_last_texted_this_employee #and what if this is nil...? and the employee messaged into our system? Then we just drop the message....?
Rails.logger.info("The last phone number to message this employee was #{customer_who_last_messaged_this_employee}")
if customer_who_last_messaged_this_employee.present?
new_message = business.messages.create(to: customer_who_last_messaged_this_employee, sent_from: business.twilio_number, content: body, has_been_read: false, employee_id: employee.id)
new_message.action_cable_update_messages_view_and_attach_image( image_params )
end
else #was a customer messaging into our system.. so we don't want to add the message into the system if it's an employee who sent it, because then we'll get duplicate messages.
last_employee_who_messaged_this_customer = business.messages.where(to: number_that_inbound_sms_was_sent_from).last&.employee || business.employees.first
Rails.logger.info("It was an inbound SMS from a customer, with last employee: #{last_employee_who_messaged_this_customer.id}, #{last_employee_who_messaged_this_customer.full_name}")
#We want to see if an employee messaged this customer first, so that we can set that this message was in response to that employee that messaged them in the first place, and so we can route the text message notification to the right employee phone.
new_message = business.messages.create(to: destination_phone_number, sent_from: number_that_inbound_sms_was_sent_from, content: body, has_been_read: false, employee_id: last_employee_who_messaged_this_customer&.id)
new_message.action_cable_update_messages_view_and_attach_image( image_params )
end
end
#This is where we could programatically add some logic, bad example
# Build response based on given body param
twiml = Twilio::TwiML::MessagingResponse.new do |resp|
#This is an example of an auto-reply from Twilio. This is if we were detecting certain messages to auto-reply to.
if body == 'hello123' #If body contains "ONLINEBOOKING", we know it's an online booking for us. Create a job_request with customer "Online Booking" with the notes as the body of the submission form.
true #for syntax reasons.
# resp.message body: 'Hi!'
elsif body == 'bye123'
true #for syntax reasons.
# resp.message body: 'Goodbye'
end
end
#This is where we would activate an actioncable push to push a message notification to the application for the business
render xml: twiml.to_xml
end
# This is a callback URL for Twilio sending us updates about an SMS that we just sent out. This will set a message's status and communicate to the user if there were
# Any issues with delivery, etc.
def status_of_sent_sms
end
#POST /connect_twilio_call
def connect_twilio_call
#TODO: Duplicate code.. bad...
current_business.calls.create(
is_active: true,
is_outbound: true,
phone_number_employee_is_calling_from_in_outbound_call: Twilio.internationalize(current_employee.phone),
customer_phone_number_in_outbound_call: Twilio.internationalize(params[:phone]))
Twilio.connect_employee_to_customer_through_call(Twilio.internationalize(current_employee.phone), Twilio.internationalize(params[:phone]), current_business.twilio_number)
end
def message_thread_from_number
number = params[:customer_number]
render json: MessagesHelper.message_thread_for_specific_number(current_business, number) #go into the first element in the array
Message.mark_all_as_read_from_number(current_business, number)
end
def customer_uploaded_images
customer = Customer.find_by(id: params[:id])
image_urls = [];
if customer
image_urls = MessagesHelper.get_uploaded_images_urls(customer) || []
end
respond_to do |format|
format.json {render json: image_urls, status: :ok}
end
end
#GET /unread_messages_count.json for business, used for mobile API badge.
def unread_messages_count
render json: {count: current_business.get_unread_messages_count}
end
# GET /unread_messages.json
def unread_messages_previews
@page_number = params[:page].blank? ? 0 : params[:page].to_i
respond_to do |format|
format.html {
handle_breadcrumb("Messages", messages_path)
@thread_to_view = params[:conversation]
@message_threads = MessagesHelper.message_threads(current_business, @thread_to_view, 25, @page_number) # Make sure we load the messages from a selected conversation, even if those messages are older than the cut off date.
@business_phone_number = current_business.twilio_number
@business = current_business
@content = params[:content]
@employee = current_employee
}
format.json {
render json: MessagesHelper.unread_message_previews(current_business)
}
end
end
private
# Never trust parameters from the scary internet, only allow the white list through.
def message_params
params.permit(:to, :sent_from, :content, :employee_id)
end
def was_this_inbound_message_sent_from_employee(from, business)
employee = business.employees.where(phone: from).first
return employee != nil, employee
end
end
# require 'zip'
require 'charlock_holmes'
class LlamaBotsController < ApplicationController
before_action :set_llama_bot, only: %i[ show edit update destroy ]
skip_before_action :authenticate_employee!
layout :determine_layout
# GET /llama_bots or /llama_bots.json
def index
@llama_bots = LlamaBot.all
# @controller_index_urls = ControllerModel.order(created_at: :desc).map do |controller|
# { name: controller.name, url: controller_url(controller) }
# end
end
# GET /llama_bots/1 or /llama_bots/1.json
def show
end
# GET /llama_bots/new
def new
@llama_bot = LlamaBot.new
end
# GET /llama_bots/1/edit
def edit
end
# POST /llama_bots or /llama_bots.json
def create
@llama_bot = LlamaBot.new(llama_bot_params)
respond_to do |format|
if @llama_bot.save
format.html { redirect_to llama_bot_url(@llama_bot), notice: "Llama bot was successfully created." }
format.json { render :show, status: :created, location: @llama_bot }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @llama_bot.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /llama_bots/1 or /llama_bots/1.json
def update
respond_to do |format|
if @llama_bot.update(llama_bot_params)
format.html { redirect_to llama_bot_url(@llama_bot), notice: "Llama bot was successfully updated." }
format.json { render :show, status: :ok, location: @llama_bot }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @llama_bot.errors, status: :unprocessable_entity }
end
end
end
# DELETE /llama_bots/1 or /llama_bots/1.json
def destroy
@llama_bot.destroy
respond_to do |format|
format.html { redirect_to llama_bots_url, notice: "Llama bot was successfully destroyed." }
format.json { head :no_content }
end
end
def see_all_source_code
@directory_structure = generate_directory_structure(Rails.root.join('app'))
render 'see_all_source_code'
end
def download_source_code
# zip_file_path = Rails.root.join('tmp', 'app_source_code.zip')
# create_zip_file(zip_file_path)
# send_file zip_file_path, type: 'application/zip', disposition: 'attachment', filename: 'app_source_code.zip'
end
def show_file
file_path = Rails.root.join(params[:path])
if File.exist?(file_path) && !File.directory?(file_path)
@file_content = File.read(file_path)
render plain: @file_content
else
render plain: 'File not found', status: :not_found
end
end
def home
@web_pages = WebPage.all
render 'home'
end
def templates
render 'templates'
end
# def show_template
# template_name = params[:name]
# if template_exists?(template_name)
# render "templates/#{template_name}"
# else
# render plain: 'Template not found', status: :not_found
# end
# end
def template_bhexterior
render 'template_bhexterior'
end
def template_fieldrocket
render 'template_fieldrocket'
end
def template_twotile
render 'template_twotile'
end
def show_database
render 'show_database'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_llama_bot
@llama_bot = LlamaBot.find(params[:id])
end
# Only allow a list of trusted parameters through.
def llama_bot_params
params.fetch(:llama_bot, {})
end
def create_zip_file(zip_file_path)
# Zip::File.open(zip_file_path, Zip::File::CREATE) do |zipfile|
# Dir[Rails.root.join('app', '**', '**')].each do |file|
# zipfile.add(file.sub(Rails.root.to_s + '/', ''), file)
# end
# end
end
def generate_directory_structure(path)
Dir.glob("#{path}/**/*").map do |file|
{
path: file.sub(Rails.root.to_s + '/', ''),
type: File.directory?(file) ? 'directory' : 'file'
}
end
end
def determine_layout
if ['template_bhexterior', 'template_fieldrocket', 'template_twotile'].include?(action_name)
'llama_template'
else
'llama_bot'
end
end
end
class VisitorDeviceInfosController < ApplicationController
skip_before_action :authenticate_employee!, only: [:create]
# skip_before_action :verify_authenticity_token, only: [:create]
before_action :set_visitor_device_info, only: %i[ show edit update destroy ]
# GET /visitor_device_infos or /visitor_device_infos.json
def index
if !params[:user_cookie_guid].nil?
@visitor_device_infos = VisitorDeviceInfo.where(user_cookie_guid: params[:user_cookie_guid], business_id: current_business.id).order(created_at: :desc)
elsif !params[:ip_address].nil?
@visitor_device_infos = VisitorDeviceInfo.where(ip_address: params[:ip_address], business_id: current_business.id).order(created_at: :desc)
else
@visitor_device_infos = VisitorDeviceInfo.where(business_id: current_business.id).order(created_at: :desc)
end
end
# GET /visitor_device_infos/1 or /visitor_device_infos/1.json
def show
end
# GET /visitor_device_infos/new
def new
@visitor_device_info = VisitorDeviceInfo.new
end
# GET /visitor_device_infos/1/edit
def edit
end
# POST /visitor_device_infos or /visitor_device_infos.json
def create
auth = params[:visitor_device_info][:auth_token]
#Guard clause to handle auth token authentication:
@business = Business.find_by(visitor_device_info_auth_token: auth)
if @business.nil?
render status: :unauthorized, json: {error: "You are not authorized"}
return
end
@visitor_device_info = VisitorDeviceInfo.new(visitor_device_info_params)
@visitor_device_info.business_id = @business.id
@visitor_device_info.ip_address = request.remote_ip
# Retrieve geolocation information using Geocoder
location_data = Geocoder.search(request.remote_ip).first
# {"ip"=>"136.38.156.240", "hostname"=>"136-38-156-240.googlefiber.net", "city"=>"Salt Lake City", "region"=>"Utah", "country"=>"US", "loc"=>"40.7608,-111.8911", "org"=>"AS16591 Google Fiber Inc.", "postal"=>"84101", "timezone"=>"America/Denver", "readme"=>"https://ipinfo.io/missingauth"}
@visitor_device_info.hostname = location_data.data["hostname"]
@visitor_device_info.city = location_data.data["city"]
@visitor_device_info.zip = location_data.data["postal"]
@visitor_device_info.region = location_data.data["region"]
@visitor_device_info.coordinates = location_data.data["loc"]
@visitor_device_info.isp = location_data.data["org"]
respond_to do |format|
if @visitor_device_info.save
format.html { redirect_to visitor_device_info_url(@visitor_device_info), notice: "Visitor device info was successfully created." }
format.json { render :show, status: :created, location: @visitor_device_info }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @visitor_device_info.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /visitor_device_infos/1 or /visitor_device_infos/1.json
def update
respond_to do |format|
if @visitor_device_info.update(visitor_device_info_params)
format.html { redirect_to visitor_device_info_url(@visitor_device_info), notice: "Visitor device info was successfully updated." }
format.json { render :show, status: :ok, location: @visitor_device_info }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @visitor_device_info.errors, status: :unprocessable_entity }
end
end
end
# DELETE /visitor_device_infos/1 or /visitor_device_infos/1.json
def destroy
@visitor_device_info.destroy
respond_to do |format|
format.html { redirect_to visitor_device_infos_url, notice: "Visitor device info was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_visitor_device_info
@visitor_device_info = VisitorDeviceInfo.find(params[:id])
end
# Only allow a list of trusted parameters through.
def visitor_device_info_params
params.require(:visitor_device_info).permit(:user_agent_raw, :referrer, :url, :ip_address, :device_width, :device_height, :device_os, :os_version, :browser_version, :browser, :business_id, :user_cookie_guid, :auth_token)
end
end
class ForwardingNumbersController < ApplicationController
before_action :set_forwarding_number, only: %i[ show edit update destroy ]
# GET /forwarding_numbers or /forwarding_numbers.json
def index
@forwarding_numbers = current_business.forwarding_numbers
end
# GET /forwarding_numbers/1 or /forwarding_numbers/1.json
def show
end
# GET /forwarding_numbers/new
def new
@forwarding_number = current_business.forwarding_numbers.new
end
# GET /forwarding_numbers/1/edit
def edit
end
# POST /forwarding_numbers or /forwarding_numbers.json
def create
@forwarding_number = ForwardingNumber.new(forwarding_number_params)
respond_to do |format|
if @forwarding_number.save
format.html { redirect_to forwarding_number_url(@forwarding_number), notice: "Forwarding number was successfully created." }
format.json { render :show, status: :created, location: @forwarding_number }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @forwarding_number.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /forwarding_numbers/1 or /forwarding_numbers/1.json
def update
respond_to do |format|
if @forwarding_number.update(forwarding_number_params)
format.html { redirect_to forwarding_number_url(@forwarding_number), notice: "Forwarding number was successfully updated." }
format.json { render :show, status: :ok, location: @forwarding_number }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @forwarding_number.errors, status: :unprocessable_entity }
end
end
end
# DELETE /forwarding_numbers/1 or /forwarding_numbers/1.json
def destroy
@forwarding_number.destroy
respond_to do |format|
format.html { redirect_to forwarding_numbers_url, notice: "Forwarding number was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_forwarding_number
@forwarding_number = ForwardingNumber.find(params[:id])
end
# Only allow a list of trusted parameters through.
def forwarding_number_params
params.require(:forwarding_number).permit(:phone, :description, :business_id)
end
end
#This controller is for the client to send debug & log information to the server so we can store client-side info in the server logs
class ClientLogsController < ApplicationController
def log
begin
Rails.logger.info("Client just sent us debug information to log! User: #{current_employee.full_name}")
# if params["logs"] == nil
# raise exception
# end
#Reply with status 200 if we are successful
Rails.logger.info(params["logs"]["modal_context"])
Rails.logger.info(params["logs"]["data"])
render :json => {:response => "Success"}, :status => 200
rescue
Rails.logger.info("Log attempt failed! Raw params:#{params}")
render :json => {:response => "Error"}, :status => 400
end
end
private
def prettify
Nokogiri::XML(xml, &:noblanks)
end
end
class InvoicesController < ApplicationController
# load_and_authorize_resource
before_action :set_invoice, only: [:edit, :update, :destroy, :mark_as_paid, :deliver_invoice, :push_invoice_to_quickbooks, :upload_image, :payments, :payments_json, :refund_ibx_payment, :amount_that_will_be_refunded_ibx, :overdue_invoices, :resync_qb_token, :update_attribute, :create_seo_post]
before_action :set_public_invoice, only: [:show, :accept_payment, :pay, :sign, :accept_signature]
#skip_before_action :verify_authenticity_token, :only => [:create]
skip_before_action :authenticate_employee!, :only => [:pay, :accept_payment, :show, :sign, :accept_signature, :generate_session_token] #For customers who need acess so they can pay
require 'stripe'
include StripeHelper
include InvoiceTemplatesHelper
require 'twilio'
require 'ibx'
# GET /invoices
# GET /invoices.json
def index
handle_breadcrumb "Invoices", "/invoices"
@start_date = params[:start_date]
@end_date = params[:end_date]
Rails.logger.info("Invoices.json request with page number: #{params[:page]}")
# if !params[:only_show_invoices].nil? #The caller passes along this param if they want to render only invoices, or only estimates. If the caller of this route wants both estimates and invoices mixed, the caller shouldn't include this parameter at all.
only_return_invoices = !params[:only_show_invoices].nil? ? ActiveModel::Type::Boolean.new.cast(params[:only_show_invoices]) : nil
only_return_estimates = only_return_invoices != nil ? !only_return_invoices : nil
@invoice_type = "Invoices"
if (only_return_estimates)
@invoice_type = "Estimates"
end
respond_to do |format|
if @start_date && @end_date #user wants invoices/estimates by date range.
format.html {
if only_return_invoices
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where("issued_on >= ? AND issued_on <= ? AND invoice_type = ?", @start_date, @end_date, "Invoice").order(created_at: :desc).paginate(page: params[:page], per_page: 50)
elsif only_return_estimates
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where("issued_on >= ? AND issued_on <= ? AND invoice_type = ?", @start_date, @end_date, "Estimate").order(created_at: :desc).paginate(page: params[:page], per_page: 50)
else
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where("issued_on >= ? AND issued_on <= ?", @start_date, @end_date).order(created_at: :desc).paginate(page: params[:page], per_page: 50)
end
}
format.json {
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where("issued_on >= ? AND issued_on <= ?", @start_date, @end_date)
render :json => @invoices
}
else #user doesn't want invoices/estimates by date range.
format.html {
if only_return_invoices
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where(invoice_type: "Invoice").ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
elsif only_return_estimates
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where(invoice_type: "Estimate").ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
else
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
end
}
format.json {
if !params[:page].nil?
Rails.logger.info("Invoices.json request with page number: #{params[:page]}")
if !params[:only_show_invoices].nil? #The caller passes along this param if they want to render only invoices, or only estimates. If the caller of this route wants both estimates and invoices mixed, the caller shouldn't include this parameter at all.
only_return_invoices = ActiveModel::Type::Boolean.new.cast(params[:only_show_invoices])
only_return_estimates = !only_return_invoices
if (only_return_invoices)
@invoices = current_business.invoices.where(invoice_type: "Invoice").includes([:job, :job_business, :invoice_to, :direct_customer]).ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
elsif(only_return_estimates)
@invoices = current_business.invoices.where(invoice_type: "Estimate").includes([:job, :job_business, :invoice_to, :direct_customer]).ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
end
else
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
end
else
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).ransack(params[:q]).result()
end
lite_json_invoices = @invoices.map{|inv| inv.lite_as_json}
render :json => lite_json_invoices
}
end
end
end
# GET /invoices/1
# GET /invoices/1.pdf
# GET /invoices/1.json
def show
handle_breadcrumb "#{@invoice.short_title}", url_for(@invoice)
if request.format != "pdf" && !user_is_authenticated
access_denied_redirect
end
respond_to do |format|
format.html
format.pdf do
render :pdf => @invoice.title, :template => 'invoices/_invoice.html.erb'
end
format.json do
render :json => @invoice.to_json(:include => :line_items) #TODO: There is an n+1 error
end
end
end
def preview_pdf
@invoice = current_business.invoices.find(params[:id])
render partial: 'invoices/invoice', invoice: @invoice, :locals => { :editable => false, :show_payment_link => true, :show_download_link => true, :invoice => @invoice }
# :layout => false,
# respond_to do |format|
# format.html do
# render :html => @invoice.title, :template => 'invoices/_invoice.html.erb'
# end
# end
end
def sign
@secret_key = params[:secret_key] #Used so people with right link can view and make payments
# not sure what to do here....
end
def accept_signature #Ok, so we have it so that they
#Take the signature parameter and attach it to the invoice.
if params[:signature] #attach the signature to the invoice.
@invoice.update(signature: params[:signature])
end
if @invoice.invoice_template&.redirect_url.present?
redirect_to @invoice.invoice_template.redirect_url
else
flash[:success] = "Successfully signed!"
redirect_to sign_invoice_path(@invoice, secret_key: @invoice.secret_key)
end
end
# GET /invoices/new
# TODO: How would we turn this into a method that could be used without the user having to be signed in..???
def new
handle_breadcrumb "New Invoice/Estimate", "/jobs/#{params[:id]}/generate_invoice"
@invoice = Invoice.new(job_id: params[:id], business_id: current_business&.id) #Interesting.. We are so dependent on a job id here. But not a customer id.
@invoice_number = @invoice.handle_invoice_num_assignment
@estimate_number = current_business.estimate_number
@invoice.invoice_type = "Invoice" #TODO: Magic string
@job = @invoice.job
@business = current_business
@customer = @job&.customer
end
# POST /invoices/1/deliver_invoice
def deliver_invoice
invoice_html_as_str = render_to_string(:template => 'invoices/_invoice.html.erb', :layout => false, :locals => { :editable => false, :show_payment_link => true, :show_download_link => true })
pdf = WickedPdf.new.pdf_from_string(invoice_html_as_str)
# pdf=nil
Rails.logger.info("Deliver invoice called! with params: #{params}")
Rails.logger.info("from_mobile param = #{params[:from_mobile]}")
is_user_delivering_invoice_from_mobile_app = params[:from_mobile].nil? ? false : ActiveModel::Type::Boolean.new.cast(params[:from_mobile])
Rails.logger.info("is_user_delivering_invoice_from_mobile_app = #{is_user_delivering_invoice_from_mobile_app}")
#TODO: This is pretty bad logic flow. Shouldn't authenticate on this email_to_customer call
email_params_h = email_params.to_h
#email_params from mobile and from email_with_options modal get passed here
redirect_uri = @invoice.email_to_customer(pdf, email_params_h, current_employee, attachments=params[:attachments], send_without_refresh=false, skip_authentication_on_google_oauth_response=is_user_delivering_invoice_from_mobile_app)
if redirect_uri != nil
Rails.logger.info("We were given a redirect uri '#{redirect_uri}' to follow, so we'll now do that.")
end
if redirect_uri != nil && !@invoice.errors.any?
render :json => {:response => {"google_redirect_uri" => redirect_uri.to_s}}, :status => 511 # Need to follow redirection to authenticate
elsif @invoice.errors.any?
render json: @invoice.errors, status: :unprocessable_entity
else
render :json => {:response => "Success"}, :status => 200, flash: {success: 'Email successfully sent'}
end
end
# GET /invoices/1/edit
def edit
handle_breadcrumb "Edit #{@invoice.short_title}", edit_invoice_path(@invoice)
end
# POST /invoices
# POST /invoices.json
def create
#TODO: Need to handle marking an existing invoice as paid, or keeping track of unpaid invoices.
#TODO: if the invoice number is null, or empty, then let's set the invoice number here.
#Fetching the right params to not break the previous functionality
is_converted_params = params[:invoice].class == String
_params = is_converted_params ? converted_invoice_params : invoice_params
merged_parmas = _params.merge(business_id: current_business.id) #Adding the business_id here as it is mandatory in case of without job or customer #wasn't working before.
@invoice = current_business.invoices.create(merged_parmas)
image_uploader(@invoice, { images: params[:images] }, false) if params[:images].present?
line_items = is_converted_params ? converted_line_items_params : params[:invoice][:line_items]
line_items = Hash[line_items.each_with_index.map{ |a,i| [i, a] }] if line_items.class.to_s.include?('Array')
# if @invoice.invoice_display_number.blank?
# @invoice.handle_invoice_num_assignment
# end
respond_to do |format|
if @invoice.save
# 10/20/2022, temporarily disabling saving an event into the database to try and decrease server + network latency when an invoice is created on mobile app.
# Event.create(resource: "Invoice", name: "Created", resource_id: @invoice.id, employee_id: current_employee.id)
#Tracking First invoice creating event
# Analytics.track( user_id: current_employee.id, event: 'Invoice Created', properties: { plan: 'Invoice' } ) if current_employee.business.invoices.count == 1
line_items&.each do |key, item|
@invoice.line_items.create(description: item["description"], price: item["price"]&.to_d || 0.0, name: item["name"], quantity: item["quantity"], product_id: item["product_id"], position: item["position"] )
end
Rails.logger.info("Just successfully saved our invoice!")
Rails.logger.info("Kicking off a sidekiq worker to push this invoice into Quickbooks!")
QuickbooksPushInvoiceWorker.perform_async(@invoice.id, current_business.id)
format.html { redirect_to @invoice, flash: { success: "#{@invoice.invoice_type} was successfully created." }}
format.json { render json: @invoice, status: :ok }
else
Rails.logger.info("Couldn't save our invoice!")
@invoice.errors.messages do |attr_name, message|
Rails.logger.info("Error: #{attr_name}: #{message}")
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :new }
format.json { render json: @invoice.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /invoices/1
# PATCH/PUT /invoices/1.json
def update
#TODO: Need to handle marking an existing invoice as paid, or keeping track of unpaid invoices.
# line_items = params[:invoice][:line_items] #Does this work too? I'm worried this update method might not be working. Need to test.
is_converted_params = params[:invoice].class == String
line_items = is_converted_params ? converted_line_items_params : params[:invoice][:line_items]
line_items = Hash[line_items.each_with_index.map{ |a,i| [i, a] }] if line_items.class.to_s.include?('Array')
if @invoice.update(invoice_params)
@invoice.line_items.delete_all #This is much easier to keep these line items synced this way.. we just destroy and recreate them.
line_items&.each do |key, item|
@invoice.line_items.create(description: item["description"], price: item["price"]&.to_d || 0.0, name: item["name"], quantity: item["quantity"], product_id: item["product_id"], position: item["position"])
end
QuickbooksResyncInvoiceWorker.perform_async(@invoice.id, current_business.id) #calls invoice.sync_with_quickbooks! to ensure changes made on Field Rocket are synced with Quickbooks Version.
flash[:success] = "Successfully updated #{@invoice.invoice_type}"
respond_to do |format|
format.html { redirect_to @invoice, flash: {success: "#{@invoice.invoice_type} was successfully updated."}}
format.json { render :success, status: :ok, json: @invoice.to_json }
end
else
@invoice.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
respond_to do |format|
format.html { redirect_to :action => :edit, flash: {error: "Couldn't update #{@invoice.invoice_type}"} }
format.json { render json: @invoice.errors, status: :unprocessable_entity }
end
end
end
# Ensures this invoice stays in sync properly with Quickbooks so the user maintains accurate bookkeeping within Quickbooks.
def sync_with_quickbooks!
if current_business.authenticated_with_quickbooks?
if @invoice.invoice_type == "Invoice"
unless @invoice.push_invoice_to_quickbooks! #Attempt to push this invoice into Quickbooks if it's new, otherwise let's push the updated changes into Quickbooks.
@invoice.push_updated_invoice_to_quickbooks!
end
elsif @invoice.invoice_type == "Estimate" #If this invoice type is an estimate, and it's been synced with quickbooks in the past (has a quickbooks id), then the user most likely first created this as an invoice, which got pushed into Quickbooks as an invoice. Since the user is now changing this to an estimate, we don't want the extra invoice in Quickbooks throwing off their reporting, so we need to make sure we delete the associated invoice in Quickbooks since the user most likely just accidentally created it as an invoice first, and then changed it to an estimate after realizing their mistake.
unless @invoice.quickbooks_id.nil?
@invoice.delete_from_quickbooks #If this was edited from an invoice into an Estimate, and it was previously pushed into Quickbooks, delete it out so that the user doesn't have an invoice in their Quickbooks that was intended to be an estimate.
@invoice.update(quickbooks_id: nil) #We have to set this to nil so that we know it's no longer linked to a Quickbooks Invoice record.
end
end
end
end
def push_invoice_to_quickbooks
pushed_to_quickbooks = @invoice.push_invoice_to_quickbooks!
Rails.logger.info("Successfully pushed invoice to quickbooks from manual push button = #{pushed_to_quickbooks}}")
render :json => {:response => "Success"}, :status => 200, flash: {success: 'Successfully pushed invoice into quickbooks'}
end
def resync_qb_token
if @invoice.resync_qb
render :json => {:response => "Success"}, :status => 200, flash: {success: 'Successfully resynced invoice qb token'}
else
render :json => {:response => "Failure"}, :status => 404, flash: {success: "Couldn't resync invoice qb token"}
end
end
# DELETE /invoices/1#{@invoice.invoice_type}
# DELETE /invoices/1.json
def destroy
path = @invoice.job_id.nil? ? invoices_path : job_url(@invoice.job_id)
@invoice.destroy
respond_to do |format|
format.html { redirect_to path, flash: {success: "#{@invoice.invoice_type} was successfully destroyed."}}
format.json { head :no_content }
end
end
# GET /invoices/1/copy
def copy
original = current_business.invoices.find_by(id: params[:id])
@invoice = original.copy!(convert_to_invoice: params[:convert_to_invoice])
if params[:convert_to_invoice]
redirect_to invoice_url(@invoice), flash: {success: "#{original.invoice_display_number} was converted to #{@invoice.invoice_display_number}"}
else
render :edit
end
end
# POST /invoices/1/convert_to_invoice
def convert_to_invoice
original = current_business.invoices.find_by(id: params[:id])
@invoice = original.copy!(convert_to_invoice: params[:convert_to_invoice])
render :json => {:response => "Success", :invoice => @invoice}, :status => 200, flash: {success: 'Successfully Marked as Paid'}
# if params[:convert_to_invoice]
# redirect_to invoice_url(@invoice), flash: {success: "#{original.invoice_display_number} was converted to #{@invoice.invoice_display_number}"}
# else
# render :edit
# end
end
# POST /invoices/1/mark_invoice_as_paid
def mark_as_paid
if @invoice.paid_in_full!
render :json => {:response => "Success"}, :status => 200, flash: {success: 'Successfully Marked as Paid'}
else
render json: @invoice.errors, status: :unprocessable_entity
end
end
# GET /invoices/1/pay
def pay
handle_breadcrumb "Pay #{@invoice.short_title}", pay_invoice_path(@invoice)
@signed_in = !current_employee.nil?
@business_name = @invoice.business.name
@business = @invoice.business
@secret_key = params[:secret_key] #Used so people with right link can view and make payments
@amount_to_pay = params[:amount] == "half" ? @invoice.total_price/2 : params[:amount] #Used to easily allow half down payments
@surcharge_customer = @invoice.business.surcharge_customer #Surcharge must be set by IT using rails console. Default is to not surcharge customer.
@app_percent_fee = @invoice.business.app_percentage_fee #App percent fee must be set by IT using rails console. Default is 0.01%
@force_signature = params[:signature] #Will be used in the future when/if we require a signature along with payment.
end
def payments
@payments = @invoice.payments
end
def payments_json
@payments = @invoice.payments
render json: @payments
end
# POST /invoices/1/pay
def accept_payment #TODO: This could be where there's an issue with Stripe somehow.
require 'net/http'
require 'uri'
require 'json'
process_using_stripe = @invoice.business.stripe_enabled
if !process_using_stripe && !@invoice.business.payroc_ibx_enabled
flash[:error] = "This business hasn't setup online payments yet!"
redirect_to pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
end
if params[:signature] #attach the signature to the invoice.
@invoice.update(signature: params[:signature])
end
if process_using_stripe
Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
stripe_user_id = @invoice.business.get_stripe_user_id
begin
results = Stripe::Charge.create({
amount: @invoice.business.surcharge_customer ? StripeHelper.to_cents(StripeHelper.calculate_surcharge(params[:amount_to_charge]&.to_f, @invoice.business.app_percentage_fee)) : StripeHelper.to_cents(params[:amount_to_charge]&.to_f), #Convert to amount of cents since Stripe deals with cents. #TODO: We must make sure it's at least 50 cent charge for stripe to work.
currency: 'usd',
source: params[:stripeToken],
description: "Charge from #{@invoice.business.name}. #{@invoice.notes}",
application_fee_amount: StripeHelper.to_cents(params[:amount_to_charge].to_f*(@invoice.business.app_percentage_fee || 0.0)),
}, stripe_account: "#{stripe_user_id}")
puts results
if results[:paid]
@invoice.payments.new(invoice_id: @invoice.id, job_id: @invoice.job_id, paid_on: DateTime.now, payment_type: Payment.payment_types[:Online], amount: params[:amount_to_charge]&.to_f, notes: "Paid through Stripe").save!
flash[:success] = results[:outcome][:seller_message]
redirect_to pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
else
flash[:error] = results[:outcome][:seller_message]
redirect_to pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
end
rescue Stripe::CardError => e
flash[:error] = e.message
redirect_to pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
end
elsif @invoice.business.payroc_ibx_enabled
response = Ibx.post_card(@invoice.business.payroc_user, @invoice.business.payroc_password, params[:ibx_card_token], params[:ibx_payment_amount])
ibx_response = JSON.parse(response.body)
if response.code == "200"
Rails.logger.info("Just sucessfully processed invoice #{@invoice.id} for #{@invoice.customer_name} for #{params[:ibx_payment_amount]}!")
Rails.logger.info("IBX Response Info: #{ibx_response}")
pn_ref = ibx_response["pnRef"] #Save PN reference and cardToken in payment so we can refund this payment if we need to.
payment_save = @invoice.payments.new(invoice_id: @invoice.id, job_id: @invoice.job_id, paid_on: DateTime.now, payment_type: Payment.payment_types[:Online], amount: params[:amount_to_charge]&.to_f, notes: "Paid through #{params[:payment_gateway_name].upcase}", card_token: params[:ibx_card_token], pn_ref: pn_ref).save!
if payment_save
flash[:success] = "Card Payment Successful!"
redirect_to pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
else
flash[:error] = "This credit card was successfully processed, but we couldn't mark the invoice as paid"
pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
Rails.logger.info("The payment wasn't saved to the database! payment: #{payment_save&.errors}")
end
else
Rails.logger.info("IBX response status was: #{response.code} for #{@invoice.customer_name} for #{params[:ibx_payment_amount]}!")
flash[:error] = "This card has failed to process."
pay_invoice_url(@invoice, :secret_key => @invoice.secret_key)
end
end
end
def amount_that_will_be_refunded_ibx
amount_to_refund = 0
@invoice.payments&.each do |payment|
amount_to_refund += payment.amount_to_refund_with_processing
end
render :json => {amount: amount_to_refund}, :status => 200
end
def refund_ibx_payment
refunded_all = true
@invoice.payments.each do |payment|
refunded_all = refunded_all && payment.refund_ibx_payment
end
if refunded_all
render :json => {:response => "Success"}, :status => 200, flash: {success: 'Successfully Refunded the Card Payments!'}
else
render json: @invoice.errors, status: :unprocessable_entity
end
end
def upload_image
respond_to do |format|
if image_uploader(@invoice, image_params, false) # Will return false on successful upload
format.js { render js: "gritterError('Error', 'Somthing went wrong');"}
else
format.js #re-render the partial for newly added images
end
end
end
#DELETE /invoice/1/image
def delete_image
delete_uploaded_image(Invoice.find_by(id: params[:id]), params[:image_id])
end
def generate_session_token
@invoice = Invoice.find_by(id: params[:id])
require 'net/http'
require 'uri'
respond_to do |format|
if @invoice
#calculating the amount with surcharge
amount_with_surcharge = @invoice.amount_due * 1.03 + 0.50
data = {
ssl_merchant_id: ENV['MERCHANT_ID'],
ssl_user_id: ENV['MERCHANT_USER_ID'],
ssl_pin: ENV['MERCHANT_PIN'],
ssl_transaction_type: 'ccsale',
ssl_first_name: @invoice.customer_to_invoice&.first_name,
ssl_last_name: @invoice.customer_to_invoice&.last_name,
ssl_get_token: 'Y',
ssl_add_token: 'Y',
ssl_amount: amount_with_surcharge
}
uri = URI.parse(ENV['ELAVON_SESSION_TOKEN_URL'])
response = Net::HTTP.post_form( uri, data )
if response.code == "200"
format.json { render json: { status: response.code, body: response.body }}
else
format.json { render json: @invoice.errors, status: response.code, body: response.body }
end
else
format.json { render json: @invoice.errors, status: :unprocessable_entity, body: {} }
end
end
end
#This is an index for getting invoices that are overdue
def overdue_invoices
handle_breadcrumb "overdue_invoices", "/invoices"
respond_to do |format|
format.html {
@invoices = current_business.invoices.where(invoice_type: "Invoice").includes([:job, :job_business, :invoice_to, :direct_customer]).where('total_price > amount_paid AND due_on < ?',Time.now.strftime("%Y-%m-%d")).ransack(params[:q]).result().order(created_at: :desc).paginate(page: params[:page], per_page: 50)
overdue_invoices = current_business.invoices.where(invoice_type: "Invoice").includes([:job, :job_business, :invoice_to, :direct_customer]).where('total_price > amount_paid AND due_on < ?',Time.now.strftime("%Y-%m-%d"))
@total_overdue_amount = overdue_invoices.map {|i| i.total_price - i.amount_paid}.sum
}
format.json {
@invoices = current_business.invoices.includes([:job, :job_business, :invoice_to, :direct_customer]).where('total_price > amount_paid AND due_on < ?',Time.now.strftime("%Y-%m-%d")).ransack(params[:q]).result()
render :json => @invoices
}
end
end
# PUT /invoices/1/update_attribute
# params: {attribute: , value: params[:value]})
render json: @invoice, status: 202
else
render json: @invoice.errors, status: :unprocessable_entity
end
else
render json: @invoice.errors, status: :unauthorized
end
end
# GET /invoices/:id/create_seo_post
def create_seo_post
# This will create a post online for the job.
end
# GET /invoices/publishable_jobs.json
def publishable_jobs
@invoices = current_business
.invoices
.includes([:job, :job_business, :invoice_to, :direct_customer, :published_job_page])
.where("invoice_type = ?", "Invoice").order(created_at: :desc)
.paginate(page: params[:page], per_page: 100)
# How do we include published_job status.? # If there is a single published job for this invoice?
#TODO: How do we link this ROI for companies?
#TODO: How do we link views for customers?
#TODO: How do we show pictures/invoice notes..?
# this status gets calculated in the React Component: Not Published, Created, Published!
# Not Published = published_job_page.nil?
# Created = published_job_page not nil.
# Published = published_job_page.slug not nil.
render json: @invoices.as_json(
include: {
published_job_page: {
only: [:id, :slug],
methods: [:number_of_views]
}
}
)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_invoice
# @invoice = Invoice.includes(:line_items).find(params[:id])
@invoice = current_business.invoices.find_by(id: params[:id])
end
def set_public_invoice
invoice = Invoice.find(params[:id])
if !user_is_authenticated
access_denied_redirect
else
@invoice = invoice
end
end
def user_is_authenticated
secret_key = Invoice.find(params[:id]).secret_key
# byebug
return !current_employee.nil? || params[:secret_key] == secret_key
end
def updateable_attributes
["notes"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
# Never trust parameters from the scary internet, only allow the white list through.
def invoice_params
params.require(:invoice).permit(
:job_id,
:was_paid,
:customer_id,
:sent_on,
:issued_on,
:due_on,
:notes,
:was_delivered,
:invoice_type,
:invoice_display_number,
:private_notes,
:invoice_to_id,
:signature,
:invoice_template_id,
line_item_attributes: [ :name, :description, :price, :product_id, :quantity]
)
end
def converted_invoice_params
ActionController::Parameters.new(JSON.parse(params[:invoice])).permit(
:job_id,
:was_paid,
:customer_id,
:sent_on,
:issued_on,
:due_on,
:notes,
:was_delivered,
:invoice_type,
:invoice_display_number,
:private_notes,
:invoice_to_id,
:signature,
:invoice_template_id,
line_item_attributes: [ :name, :description, :price, :product_id, :quantity])
end
def converted_line_items_params
ActionController::Parameters.new(JSON.parse(params[:invoice]))
.permit(
line_items: [:name, :description, :price, :product_id, :quantity, :position]
)[:line_items]
end
def image_params
params.require(:invoice).permit(
images: []
)
end
def email_params
params.permit(
:from_mobile,
:email_recipients,
:email_ccs,
:email_subject,
:email_body,
:id,
:invoice,
email_recipients: [],
email_ccs: [],
attachments: []
)
end
end
class BranchesController < ApplicationController
before_action :set_branch, only: [:show, :edit, :update, :destroy, :daily_summary]
# GET /branches
# GET /branches.json
def index
handle_breadcrumb("Branches", branches_path)
@branches = current_business.branches
end
# GET /branches/1
# GET /branches/1.json
def show
handle_breadcrumb("Branch #{@branch.name}", branch_path(@branch))
end
# GET /branches/new
def new
handle_breadcrumb "New Branch", new_branch_path
@branch = Branch.new
end
# GET /branches/1/edit
def edit
handle_breadcrumb "Edit Branch #{@branch.name}", edit_branch_path
end
# POST /branches
# POST /branches.json
def create
@branch = current_business.branches.new(branch_params)
respond_to do |format|
if @branch.save
format.html { redirect_to @branch, notice: 'Branch was successfully created.' }
format.json { render :show, status: :created, location: @branch }
else
format.html { render :new }
format.json { render json: @branch.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /branches/1
# PATCH/PUT /branches/1.json
def update
respond_to do |format|
if @branch.update(branch_params)
format.html { redirect_to @branch, notice: 'Branch was successfully updated.' }
format.json { render :show, status: :ok, location: @branch }
else
format.html { render :edit }
format.json { render json: @branch.errors, status: :unprocessable_entity }
end
end
end
# DELETE /branches/1
# DELETE /branches/1.json
def destroy
@branch.destroy
respond_to do |format|
format.html { redirect_to branches_url, notice: 'Branch was successfully destroyed.' }
format.json { head :no_content }
end
end
def daily_summary
start_period = DateTime.now.beginning_of_day
end_period = DateTime.now.end_of_day
@employee_jobs = []
@branch.employees.each do |employee|
employee.employee_jobs.where('time_start BETWEEN ? AND ?', start_period, end_period).each do |ej|
@employee_jobs << ej
end
end
render "employees/daily_summary"
# @employee_jobs = @branch.employees.employee_jobs.where('time_start BETWEEN ? AND ?', start_period, end_period)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_branch
@branch = current_business.branches.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def branch_params
params.require(:branch).permit(:name)
end
end
class ReviewsController < ApplicationController
before_action :set_review, only: %i[ show edit update destroy ]
# GET /reviews or /reviews.json
def index
@reviews = current_business.reviews
end
# GET /reviews/1 or /reviews/1.json
def show
end
# GET /reviews/new
def new
@review = current_business.reviews.new
end
# GET /reviews/1/edit
def edit
end
# POST /reviews or /reviews.json
def create
@review = current_business.reviews.new(review_params)
respond_to do |format|
if @review.save
format.html { redirect_to review_url(@review), notice: "Review was successfully created." }
format.json { render :show, status: :created, location: @review }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @review.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /reviews/1 or /reviews/1.json
def update
respond_to do |format|
if @review.update(review_params)
format.html { redirect_to review_url(@review), notice: "Review was successfully updated." }
format.json { render :show, status: :ok, location: @review }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @review.errors, status: :unprocessable_entity }
end
end
end
# DELETE /reviews/1 or /reviews/1.json
def destroy
@review.destroy
respond_to do |format|
format.html { redirect_to reviews_url, notice: "Review was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_review
@review = current_business.reviews.find(params[:id])
end
# Only allow a list of trusted parameters through.
def review_params
params.require(:review).permit(:name, :city, :content, :home_page_id, :business_id)
end
end
class CustomerContactsController < ApplicationController
before_action :set_customer_contact, only: [:update, :destroy, :show, :call_contact_through_twilio]
require 'twilio'
# POST /customer_contacts
# POST /customer_contacts.json
def create
if customer_contact_params[:business_id].empty?
params[:customer_contact][:business_id] = current_business.id
end
@customer_contact = CustomerContact.new(customer_contact_params)
respond_to do |format|
if @customer_contact.save
format.html { redirect_to @customer_contact.customer, flash: {success: "Customer contact successfully created"}}
format.json { render status: 200, json: @customer_contact }
else
format.json { render json: @customer_contact.errors, status: :unprocessable_entity }
end
end
end
# DELETE /customer_contacts/:id
def destroy
customer = @customer_contact.customer
@customer_contact.destroy
respond_to do |format|
format.html { redirect_to customer, flash: {success: 'Customer Contact was successfully deleted'}}
format.json { render status: 200 }
end
end
def show
respond_to do |format|
format.html
format.json { render :json => @customer_contact }
end
end
def update
respond_to do |format|
if @customer_contact.update(customer_contact_params)
format.html
format.json {render json: @customer_contact, status: 202}
else
format.html
format.json {render json: @customer_contact.formatted_error_messages, status: :unprocessable_entity}
end
end
end
def call_contact_through_twilio
current_business.calls.create(
is_active: true,
is_outbound: true,
phone_number_employee_is_calling_from_in_outbound_call: Twilio.internationalize(current_employee.phone),
customer_phone_number_in_outbound_call: Twilio.internationalize(@customer_contact.phone))
Twilio.connect_employee_to_customer_through_call(Twilio.internationalize(current_employee.phone), Twilio.internationalize(@customer_contact.phone), current_business.twilio_number)
end
private
# Use callbacks to share common setup or constraints between actions.
def set_customer_contact
customer_contact = CustomerContact.find(params[:id])
if customer_contact.customer.business_id == current_business.id
@customer_contact = customer_contact
end
end
# Never trust parameters from the scary internet, only allow the white list through.
def customer_contact_params
params.require(:customer_contact).permit(:first_name, :last_name, :phone, :email, :customer_id, :business_id, :notes)
end
end
class EmployeeJobsController < ApplicationController
# load_and_authorize_resource
before_action :set_employee_job, except: :remove_appointment_breadcrumb
def destroy
job = @employee_job.job
@employee_job.destroy
respond_to do |format|
format.html {redirect_to job, flash: {success: 'Successfully deleted appointment'}}
format.json { render :json => {:response => "Success"}, :status => 200 }
end
end
# PUT /employee_jobs/:id
def update
respond_to do |format|
if @employee_job.update(employee_jobs_params)
Event.create(resource: "EmployeeJob", name: "Updated", resource_id: @employee_job.id, notes: @employee_job.previous_changes.except(:updated_at), employee_id: current_employee.id)
format.html {redirect_to @employee_job.job, flash: {success: 'Updated appointment'}}
format.json { render json: @employee_job, :status => 202 }
else
format.html {redirect_to job, flash: {error: "Couldn't delete appointment"}}
format.json { render json: @employee_job.formatted_error_messages, status: :unprocessable_entity }
end
end
end
def details
if params[:breadcrumb]
handle_appointment_breadcrumb "Appointment #{@employee_job.id}", "/schedule?appointment=#{@employee_job.id}", true
end
respond_to do |format|
images_paths = []
@employee_job&.job&.images&.includes(:blob).each do |img|
images_paths << url_for(img)
end
format.json { render :json => @employee_job.job.as_json(except: [:job_request_id]).merge(images: images_paths).merge(@employee_job.as_json)} #Have to use the "except" job_request_id because it's breaking the fullcalendar javascript for some reason. So we have to not allow the job_request id in there.
end
end
# POST /remove_appointment_breadcrumb
def remove_appointment_breadcrumb
pop_breadcrumb
end
def show
respond_to do |format|
format.html
format.json { render :json => @employee_job }
end
end
def update_attribute
if safe_attribute_update? params[:attribute]
if @employee_job.update({params[:attribute] => params[:value]})
render json: @employee_job, status: 202
else
render json: @employee_job.formatted_error_messages, status: :unprocessable_entity
end
end
end
def update_start_time_end_time
if @employee_job.update(time_start: params[:time_start], time_end: params[:time_end])
render json: @employee_job, status: 202
else
render json: @employee_job.formatted_error_messages, status: :unprocessable_entity
end
end
private
def set_employee_job
@employee_job = current_business.employee_jobs.find(params[:id])
end
def updateable_attributes
['employee_job_type_id', 'time_start', 'time_end', 'employee_id']
end
def safe_attribute_update?(attr)
updateable_attributes.include?(attr)
end
def employee_jobs_params
params.require(:employee_job).permit(:id, :employee_job_type_id, :time_start, :time_end, :employee_id, :appointment_color_id)
end
end
class LineItemsController < ApplicationController
before_action :set_line_item, only: [:update]
before_action :set_line_item_for_delete, only: [:destroy]
before_action :set_invoice, only: [:create]
def create
@line_item = @invoice.line_items.new(line_item_params)
respond_to do |format|
if @line_item.save!
new_total_price = @line_item.invoice.total_price
format.json { render json: {line_item: @line_item, new_total_price: new_total_price}, status: :created }
else
@line_item.errors.messages do |attr_name, message|
Rails.logger.error("#{attr_name}: #{message}")
flash[:error] = "#{attr_name}: #{message}"
end
format.json { render json: @line_item.errors, status: :unprocessable_entity }
end
end
end
#update a specific line item
#PUT /line_items.json
def update #This is returning a 302 response for some reason
if @line_item.update(line_item_params)
new_total_price = @line_item.invoice.total_price
render :json => {:response => "Success", :new_total_price => new_total_price}, :status => 200
else
render :json => {:response => "Error"}, :status => 400
end
end
def destroy
if @line_item.destroy
new_total_price = @line_item.invoice.total_price
render :json => {:response => "Success", :new_total_price => new_total_price}, :status => 200
else
render :json => {:response => "Error"}, :status => 400
end
end
private
def set_invoice
@invoice = current_business.invoices.find(line_item_params[:invoice_id])
end
def set_line_item
@line_item = LineItem.find(params[:id])
line_item_invoice_id = line_item_params[:invoice_id]
invoice = current_business.invoices.find_by(id: line_item_invoice_id || @line_item.invoice_id)
if invoice.nil?
unauthorized_forbidden_response
end
end
def set_line_item_for_delete
@line_item = LineItem.find(params[:id])
invoice = @line_item.invoice
if invoice.business_id != current_business.id
unauthorized_forbidden_response
end
end
# def authenticate
# line_item = LineItem.find(line_item_params[:id])
# line_item_invoice_id = line_item.invoice_id
# invoice = current_business.invoices.find(line_item_invoice_id)
# current_business.invoices.find(line_item_params[:invoice_id])line_items.find(line_item_params[:id])
# return !invoice.nil?
# end
def line_item_params
params.require(:line_item).permit(
:description,
:price,
:quantity,
:name,
:product_id,
:invoice_id,
:position,
)
end
end
class PublicController < ApplicationController
skip_before_action :authenticate_employee!
#skip_before_action :verify_authenticity_token
before_action :set_employee, only: [:destroy, :edit, :update]
require 'twilio'
def home
if employee_signed_in?
@business = current_business
# DataViz module and class located in ../queries/dashboard_viz.rb
# Frequented Cities
# Job Status
# Income Totalling
# Customer Count Change
# All Jobs Status
# current/next jobs
end
end
def alert_sign_up
#Send twilio message
body = "Someone Signed Up! Email: '#{params["email"]}' Password: '#{params["password"]}', Phone: '#{params["phone"]}' First Name: '#{params["firstName"]}', Company: '#{params["companyName"]}'"
Twilio.send_text('8013499925', body) #text kody
# Twilio.send_text('8019890014', body) #text mike
render json: {}, status: 200
end
def set_employee
@employee = current_business.employees.find params[:id]
end
end
class JobStatusesController < ApplicationController
before_action :set_job_status, only: [:show, :edit, :update, :destroy]
# GET /job_statuses
# GET /job_statuses.json
def index
@job_statuses = current_business.job_statuses.ransack(params[:q]).result(distinct:true)
respond_to do |format|
format.html
format.json {render json: @job_statuses.to_json}
end
end
# GET /job_statuses/1
# GET /job_statuses/1.json
def show
end
# GET /job_statuses/new
def new
@job_status = current_business.job_statuses.new
end
# GET /job_statuses/1/edit
def edit
end
# POST /job_statuses
# POST /job_statuses.json
def create
@job_status = current_business.job_statuses.new(job_status_params)
respond_to do |format|
if @job_status.save
format.html { redirect_to @job_status, notice: 'Job status was successfully created.' }
format.json { render json: @job_status, status: :created }
else
format.html { render :new }
format.json { render json: @job_status.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /job_statuses/1
# PATCH/PUT /job_statuses/1.json
def update
respond_to do |format|
if @job_status.update(job_status_params)
format.html { redirect_to @job_status, notice: 'Job status was successfully updated.' }
format.json { render :show, status: :ok, location: @job_status }
else
format.html { render :edit }
format.json { render json: @job_status.errors, status: :unprocessable_entity }
end
end
end
# DELETE /job_statuses/1
# DELETE /job_statuses/1.json
def destroy
@job_status.destroy
respond_to do |format|
format.html { redirect_to job_statuses_url, notice: 'Job status was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_job_status
@job_status = JobStatus.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def job_status_params
params.require(:job_status).permit(:status, :color)
end
end
# app/controllers/authentication_controller.rb
class ApiAuthenticationController < ApplicationController
##skip_before_action :verify_authenticity_token
skip_before_action :authenticate_employee!
def authenticate #Route used just to login and get a authentication token for an API user.
command = AuthenticateUser.call(params[:email], params[:password])
if command.success?
business = Employee.where(email: params[:email]).first&.business
twilio_client_name = Employee.where(email: params[:email]).first&.business&.twilio_mobile_client_name
sms_form_template = business.sms_form_template
render json: { auth_token: command.result, twilio_client_name: twilio_client_name, sms_form_template: sms_form_template, twilio_number: business.twilio_number }
else
render json: { error: command.errors }, status: :unauthorized
end
end
end
class MarketingSourcesController < ApplicationController
before_action :set_marketing_source, only: [:show, :edit, :update, :destroy]
skip_before_action :authenticate_employee!, :only => [:index]
# GET /marketing_sources
# GET /marketing_sources.json
def index
handle_breadcrumb "Marketing Sources", marketing_sources_path
@marketing_sources = current_business.marketing_sources.ransack(params[:q]).result
end
# GET /marketing_sources/1
# GET /marketing_sources/1.json
def show
end
# GET /marketing_sources/new
def new
@marketing_source = current_business.marketing_sources.new
end
# GET /marketing_sources/1/edit
def edit
end
# POST /marketing_sources
# POST /marketing_sources.json
def create
@marketing_source = current_business.marketing_sources.build(marketing_source_params)
respond_to do |format|
if @marketing_source.save
format.html { redirect_to @marketing_source, flash: {success: 'Marketing Source was successfully created.' }}
format.json { render :show, status: :created, location: @marketing_source }
else
format.html { render :new, flash: {error: "Couldn't create Marketing Source"}}
format.json { render json: @marketing_source.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /marketing_sources/1
# PATCH/PUT /marketing_sources/1.json
def update
respond_to do |format|
if @marketing_source.update(marketing_source_params)
format.html { redirect_to @marketing_source, flash: {success: 'Marketing source was successfully updated.'}}
format.json { render :show, status: :ok, location: @marketing_source }
else
format.html { render :edit, error: "Couldn't update Marketing Source" }
format.json { render json: @marketing_source.errors, status: :unprocessable_entity }
end
end
end
# DELETE /marketing_sources/1
# DELETE /marketing_sources/1.json
def destroy
@marketing_source.destroy
respond_to do |format|
format.html { redirect_to marketing_sources_url, flash: {success: 'Marketing source was successfully destroyed.'}}
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_marketing_source
@marketing_source = current_business.marketing_sources.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def marketing_source_params
params.require(:marketing_source).permit(:name)
end
end
class GroupsController < ApplicationController
load_and_authorize_resource
before_action :set_group, only: [:destroy, :edit, :show, :update]
def create
@group = current_business.groups.new(group_params)
if @group.save
flash[:success] = 'Group created successfully.'
redirect_to @group
else
flash[:error] = 'Failed to create group.'
render :new
end
end
def destroy
if @group.destroy
flash[:success] = 'Group destroyed successfully.'
else
flash[:error] = 'Failed to destroy group. All employees must be un-assigned from group.'
end
redirect_to groups_path
end
def edit
@permissions = @group.permissions
end
def index
respond_to do |format|
format.html do
@query = Group.employee_groups(current_business).ransack(params[:q]).result(distinct: true)
@groups = @query.paginate(page: params[:page], per_page: 20)
end
end
end
def new
@group = current_business.groups.new
end
def show
@permissions = @group.permissions
end
def update
if @group.update(group_params)
flash[:success] = 'Group updated successfully.'
redirect_to @group
else
flash[:error] = 'Failed to update group.'
render :edit
end
end
private
def group_params
params.require(:group).permit(:name)
end
def set_group
@group = current_business.groups.find(params[:id])
end
end
class TimesheetsController < ApplicationController
before_action :set_employee, only: [ :create , ]
before_action :set_job, only: [ :create, :clock_out_for_specific_job, :clock_out_of_current_and_into_new, :get_base_duration_for_current_job ]
before_action :set_timesheet, only: [ :show, :clock_out, :update, :destroy ]
# All Timesheets.
# get /timesheets
# get /timesheets.json
def index
@start_date = params[:start_date].nil? ? Date.today.beginning_of_week : params[:start_date]
@end_date = params[:end_date].nil? ? Date.today.end_of_week : params[:end_date]
# Get the timesheets nested by each Job (descending from latest timesheet for the job)
@job_timesheets = Timesheet.where(business_id: current_business.id, clocked_in: @start_date..@end_date).order(clocked_in: :desc).group_by(&:job_id)
# Get the timesheets nested by each Employee (employee in alphabetical order, timesheets in descending order of when they were clocked in)
@employee_timesheets = Timesheet.where(business_id: current_business.id, clocked_in: @start_date..@end_date).order(clocked_in: :desc).group_by(&:employee_id)
respond_to do |format|
format.html
format.json {render :json => current_business.timesheets.order(:created_at).to_json }#sort by last}
end
end
#get '/timesheets_for_employee_range'
def timesheets_for_employee_range
@start_date = params[:start_date].nil? ? Date.today.beginning_of_week : params[:start_date]
@end_date = params[:end_date].nil? ? Date.today.end_of_week : params[:end_date]
@employee_timesheets = Timesheet.where(business_id: current_business.id, clocked_in: @start_date..@end_date, employee_id: current_employee.id).order(clocked_in: :desc)
render :json => @employee_timesheets.to_json
end
# GET /timesheets/clocked_in.json
def get_currently_clocked_in_timesheets
@timesheets = current_business.timesheets.where(is_clocked_in: true)
render :json => @timesheets.to_json
end
# GET /get_currently_clocked_in_timesheets_for_current_user.json
def get_currently_clocked_in_timesheets_for_current_user
@timesheets = current_employee.timesheets.where(clocked_out: nil)
render :json => @timesheets.to_json
end
# GET /get_current_users_daily_timesheets.json
def get_current_users_daily_timesheets
@timesheets = current_employee.timesheets.where(clocked_in: DateTime.now.in_time_zone(current_business.time_zone).all_day)
render :json => @timesheets.to_json
end
# POST /clock_out_of_current_and_into_new.json
def clock_out_of_current_and_into_new
timesheets_to_clock_out = Timesheet.where(employee_id: current_employee.id, clocked_out: nil)
clock_out_time = DateTime.now.in_time_zone(current_business.time_zone)
unless timesheets_to_clock_out.empty? #Clock out of any active timesheets before clocking into this one.
timesheets_to_clock_out.update(clocked_out: clock_out_time)
end
@timesheet = current_employee.timesheets.create(job_id: @job&.id, business_id: current_employee.business_id, clocked_in: DateTime.now.in_time_zone(current_business.time_zone))
if @timesheet.errors.count > 0
render json: @timesheet.errors, status: :unprocessable_entity
else
render :json => @timesheet.to_json
end
end
# GET /timesheets/1/edit
def edit
# handle_breadcrumb "Edit timesheet", edit_timesheet_path(@timesheet)
end
def destroy
@timesheet.destroy
respond_to do |format|
format.html { redirect_to '/timesheets', flash: {success: 'Timesheet was successfully destroyed.' }}
format.json { head :no_content }
end
end
# PUT /timesheets/1/edit
def update
if @timesheet.update(timesheet_params)
render json: {timesheet: @timesheet}, status: :ok
else
render json: @timesheet.errors, status: :unprocessable_entity
end
end
# POST /timesheets
def create #same as create.
employee = @employee || current_employee
@timesheet = employee.timesheets.create(job_id: @job&.id, business_id: employee.business_id, clocked_in: params[:start_time], clocked_out: params[:end_time])
user_clocked_into_general_time = @job.nil? #TODO: Return a timesheet with the duration from the total jobs, or general id timesheet if possible
if (user_clocked_into_general_time)
@duration_for_day = get_time_clocked_into_general_time
else
@duration_for_day = @job.get_time_clocked_into_this_specific_job(employee.id)
end
Rails.logger.info("Duration for this previous job was: #{@duration_for_day}")
if @timesheet.clocked_in.nil?
@timesheet.clock_in
end
if @timesheet.save
render json: {timesheet: @timesheet.to_json, base_duration_for_day: @duration_for_day}
else
render json: @timesheet.errors, status: :unprocessable_entity
end
end
# POST /timesheets/1/clock_out
def clock_out
@timesheet.clock_out
if @timesheet.save
render :json => @timesheet.to_json
else
render json: @timesheet.errors, status: :unprocessable_entity
end
end
# POST /timesheets/clock_out_for_job
def clock_out_for_specific_job
timesheets_to_clock_out = Timesheet.where(employee_id: current_employee.id, job_id: @job.id, clocked_out: nil)
clock_out_time = DateTime.now
if timesheets_to_clock_out.update(clocked_out: clock_out_time)
render json: {message: "success"}
else
render json: {message: "error"}
end
end
# POST /timesheets/clock_out_of_general_time
def clock_out_of_general_time
timesheets_to_clock_out = Timesheet.where(employee_id: current_employee.id, job_id: nil, clocked_out: nil)
clock_out_time = DateTime.now
if timesheets_to_clock_out.update(clocked_out: clock_out_time)
render json: {message: "success"}
else
render json: {message: "error"}
end
end
# GET /get_general_timesheet_for_current_user
def get_general_timesheet_for_current_user
@all_general_timesheets_for_today = current_employee.timesheets.where(job_id: nil, created_at: DateTime.now.utc().all_day) #query in utc() because it's saved in utc() format.
@finished_general_timesheets_for_today = @all_general_timesheets_for_today.where.not(clocked_out: nil)
@active_general_timesheet_for_today = @all_general_timesheets_for_today.where(clocked_out: nil)
@duration = Timesheet.sum_duration_of_timesheets(@finished_general_timesheets_for_today)
render json: {duration: @duration, active_general_timesheet: @active_general_timesheet_for_today.first}
end
# GET /clockable_jobs_for_current_user
def get_clockable_jobs_for_current_user
# byebug
# @employee_jobs_clockable_for_day = current_business.employee_jobs.where(time_start: DateTime.now.utc.to_date.all_day) #TODO: A bug is happening here because of timezone issues. Timezone issues are the bane of my existence, lol.
@employee_jobs_clockable_for_day = current_business.employee_jobs.where(time_start: DateTime.now.in_time_zone('America/Denver').to_date.all_day)
Rails.logger.info("Getting the clockable jobs for today, with time_start in the range of: #{DateTime.now.in_time_zone('America/Denver').to_date.all_day}")
unless params[:date].nil?
dt = DateTime.strptime(params[:date], '%m/%d/%Y')
@employee_jobs_clockable_for_day = current_business.employee_jobs.where(time_start: dt.to_date.all_day)
end
@clockable_jobs_for_day = @employee_jobs_clockable_for_day.map{|employee_job| {job_id: employee_job.job_id,
customer_name: employee_job.job.customer&.full_name,
time_start: employee_job.time_start,
employee_actively_tracking_time: employee_job.job.is_employee_tracking_time_against_this_job_currently?(current_employee.id), # True/false value
clocked_in: employee_job.job.get_clock_in_time_for_this_job_if_active(current_employee.id),
duration: employee_job.job.get_time_clocked_into_this_specific_job(current_employee.id)}} #this is where we see the time that was tracked.
@no_duplicates_clockable_jobs = @clockable_jobs_for_day.uniq{|job| job[:job_id]}
render :json => @no_duplicates_clockable_jobs
end
#GET /base_duration_for_current_job
def get_base_duration_for_current_job
#adds all duration values of jobs that have been clocked in to and out of.
@duration_for_current_job = @job.get_time_clocked_into_this_specific_job(current_employee.id)
render :json => @duration_for_current_job.to_json
end
private
# Use callbacks to share common setup or constraints between actions.
def set_employee
@employee = params[:employee_id].nil? ? current_employee : current_business.employees.find(params[:employee_id])
end
def set_job
#this is getting a job record with job_id (so @job is the job record wher the jobs id = job_id)
@job = params[:job_id].nil? ? nil : current_business.jobs.find(params[:job_id])
end
def set_timesheet
@timesheet = current_business.timesheets.find(params[:id])
end
def get_time_clocked_into_general_time
@general_timesheets_for_today = Timesheet.where(clocked_in: DateTime.now.utc().to_date.all_day, job_id: nil)
duration = 0
@general_timesheets_for_today.each do |ts|
duration += ts.duration
end
return duration
end
def timesheet_params
params.require(:timesheet).permit(
:clocked_in,
:clocked_out
)
end
#This functinoality was essentially moved to job.rb
# def get_time_clocked_into_this_specific_job
# @timesheets_for_this_job_today = Timesheet.where(clocked_in: DateTime.now.utc().to_date.all_day, job_id: @job.id)
# duration = 0
# @timesheets_for_this_job_today.each do |ts|
# duration += ts.duration
# end
# return duration
# end
end
class EmailTemplatesController < ApplicationController
before_action :set_email_template, only: [:show, :edit, :update, :destroy]
# GET /email_templates
# GET /email_templates.json
def index
handle_breadcrumb("Email Templates", "/email_templates")
@email_templates = current_business.email_templates.all
end
# GET /email_templates/1
# GET /email_templates/1.json
def show
handle_breadcrumb("Email Template #{@email_template.name}", "/email_templates/#{@email_template.id}")
end
# GET /email_templates/new
def new
handle_breadcrumb("New Email Template", "/email_templates/new")
@email_template = current_business.email_templates.new
end
# GET /email_templates/1/edit
def edit
handle_breadcrumb("Edit Email Template", "/email_templates/#{@email_template.id}/edit")
end
# POST /email_templates
# POST /email_templates.json
def create
@email_template = current_business.email_templates.new(email_template_params)
respond_to do |format|
if @email_template.save
format.html { redirect_to @email_template, flash: {success: 'Email template was successfully created.'}}
format.json { render :show, status: :created, location: @email_template }
else
format.html { render :new }
format.json { render json: @email_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /email_templates/1
# PATCH/PUT /email_templates/1.json
def update
respond_to do |format|
if @email_template.update(email_template_params)
format.html { redirect_to @email_template, flash: {success: 'Email template was successfully updated.'}}
format.json { render :show, status: :ok, location: @email_template }
else
format.html { render :edit }
format.json { render json: @email_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /email_templates/1
# DELETE /email_templates/1.json
def destroy
@email_template.destroy
respond_to do |format|
format.html { redirect_to email_templates_url, flash: {success: 'Email template was successfully destroyed.'}}
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_email_template
@email_template = current_business.email_templates.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def email_template_params
params.require(:email_template).permit(:name, :body, :business_id)
end
end
class PermissionsController < ApplicationController
load_and_authorize_resource
before_action :set_group
before_action :set_permission, only: [:destroy, :edit, :update]
def create
@permission = @group.permissions.new(permission_params)
if @permission.save
flash[:success] = 'Permission created successfully.'
redirect_to edit_group_path(@group)
else
flash[:error] = 'Failed to create permission.'
render :new
end
end
def destroy
if @permission.destroy
flash[:success] = 'Permission deleted!'
redirect_to edit_group_path(@group)
else
flash[:error] = 'Failed to delete permission.'
redirect_to edit_group_path(@group)
end
end
def edit
end
def new
@permission = @group.permissions.new
end
def update
if @permission.update(permission_params)
flash[:success] = 'Permission updated successfully.'
redirect_to edit_group_path(@group)
else
flash[:error] = 'Failed to update permission.'
render :edit
end
end
private
def permission_params
params.require(:permission).permit(:action, :resource)
end
def set_group
@group = current_business.groups.find(params[:group_id])
end
def set_permission
@permission = @group.permissions.find(params[:id])
end
end
class EventsController < ApplicationController
before_action :set_event, only: %i[ show edit update destroy ]
before_action :set_ranges, only: :total_report_data
skip_before_action :authenticate_employee!, only: [:create]
# GET /events or /events.json
def index
@businesses = Business.all
@employees = Employee.all
if params[:business_id]
@business = Business.find(params[:business_id])
@events = Event.where(business_id: params[:business_id])
.includes(:employee)
.paginate(page: params[:page], per_page: 100)
.order(created_at: :desc)
else
@events = Event.includes(:employee)
.paginate(page: params[:page], per_page: 100)
.order(created_at: :desc)
end
# respond_to do |format|
# format.json {render json: @events}
# end
end
def leaderboard
# Yesterday's Daily Active Users
yesterday_start = Time.zone.yesterday.beginning_of_day
yesterday_end = Time.zone.yesterday.end_of_day
@yesterday_active_users = Event.where(created_at: yesterday_start..yesterday_end).pluck(:employee_id).uniq.count
# Today's Daily Active Users So Far
today_start = Time.zone.now.beginning_of_day
@todays_active_users_so_far = Event.where(created_at: today_start..Time.zone.now).pluck(:employee_id).uniq.count
# Last Week's Daily Active Users
last_week_start = Time.zone.now.beginning_of_week - 1.week
last_week_end = Time.zone.now.end_of_week - 1.week
@last_week_daily_active_users = Event.where(created_at: last_week_start..last_week_end).pluck(:employee_id).uniq.count
# Week's Active Users So Far
current_week_start = Time.zone.now.beginning_of_week
@weeks_active_users_so_far = Event.where(created_at: current_week_start..Time.zone.now).pluck(:employee_id).uniq.count
@mrr = Subscription.where(deactivated: false).sum(:mrr)
@processing_fee_mrr = 0
@businesses_paying_application = Business.where("app_percentage_fee > 0")
@businesses_paying_application.each do |business|
stripe_payments_this_month = business.payments.where(notes: "Paid through Stripe", created_at: Time.now.beginning_of_month..Time.now.end_of_month).sum(:amount)
@processing_fee_mrr += stripe_payments_this_month * business.app_percentage_fee
end
#One liner for Rails Console
#@processing_fee_mrr = 0; @businesses_paying_application.each { |business| stripe_payments_last_month = business.payments.where(notes: "Paid through Stripe", created_at: (Time.now.last_month.beginning_of_month)..(Time.now.last_month.end_of_month)).sum(:amount); @processing_fee_mrr += stripe_payments_last_month * business.app_percentage_fee }
#Business.where("subscription_id IS NOT NULL").sum(:subscription_price)
# Today's Daily Active Users
# @todays_active_users = Event.where(created_at: today_start..Time.zone.now).pluck(:employee_id).uniq.count
# Leaderboard by business with the total number of events for the current week, excluding businesses with 0 events
current_week_start = Time.zone.now.beginning_of_week
current_week_end = Time.zone.now.end_of_week
# byebug
@current_month_start = params[:startDate].nil? ? Time.zone.now.beginning_of_month : DateTime.strptime(params[:startDate], "%Y-%m-%d")
@current_month_end = params[:endDate].nil? ? Time.zone.now.end_of_month : DateTime.strptime(params[:endDate], "%Y-%m-%d")
#Gets the businesses to show, and their event count for this current week.
# @leaderboard = Event.where(created_at: current_week_start..current_week_end)
# .group(:business_id)
# .count
# .select { |business_id, count| count > 0 }
# .sort_by { |business_id, count| -count }
#Gets the businesses to show, and their event count for this current week.
@leaderboard = Event.where(created_at: @current_month_start..@current_month_end)
.where.not(business_id: nil) # filter out events without a business_id
.group(:business_id)
.count
.select { |business_id, count| count > 0 }
.sort_by { |business_id, count| -count }
.to_h
#Get all active businesses
@active_businesses_with_subscription = Business.where("subscription_id IS NOT NULL")
#expunge any businesses that have the suppress_analytics tag set.
@suppressed_businesses = Business.where(suppress_analytics: true)
#expunge any businesses that have the suppress_analytics tag set.(We have fake & dummy data we don't want to include in the leaderboard list.)
@suppressed_businesses.each do |business|
@leaderboard.delete(business.id)
end
# get the survey answers for the business.
# Ensure all active businesses are in the leaderboard
@active_businesses_with_subscription.each do |business|
unless @leaderboard.key?(business.id)
@leaderboard[business.id] = -1000
end
end
#remove any businesess from leaderboard that are in suppressed_businesses list
#TODO: How to do this.?
# Sort the leaderboard
@leaderboard = @leaderboard.sort_by { |business_id, count| [-count, business_id] }.to_h
# #How would we get all the businesses that have an active subscription, or have registered in the last 30 days?
# @active_businesses_with_subscription = Business.where("subscription_id IS NOT NULL") #Or do they all have a subscription_id.?
# #How would we get all the businesses that have 'churned', or signed up for an account but haven't done anything with it?
# @registered_in_last_30_days = Business.where("created_at >= ?", 30.days.ago).count
# @registered_after_30_days_but_churned = Business.where("created_at < ?", 30.days.ago).count # and no subscription is set.
#Break it down by:
# Daily Active Companies
# Get an average for a time period
# Weekly Active Companies
# Get an average for a time period
# Monthly Active Companies
# Get an average for a time period
# Daily Active Users
# Get an average for a time period.
# Weekly Active Users
# Get an average for a time period.
# Monthly Active Users
# Get an average for a time period
end
def registrations
@businesses = Business.all.order(created_at: :desc)
end
def marketing
#TODO: This will get all businesses where SEO is enabled @businesses = Business.where(enable_seo_posts: true
# @businesses = Business.where(enable_seo_posts: true)
@businesses = Business.joins(:subscription).where("subscriptions.mrr > ? OR enable_seo_posts = ?", 110, true)
end
def job_link_clicks
@events = Event.where(resource: "Job Link Clicked")
# @business = Business.find(params[:id])
end
# get /events/business?id=1
def business
@business = Business.find(params[:id])
@events = Event.where(business_id: @business.id)
.includes(:employee)
.paginate(page: params[:page], per_page: 250)
.order(created_at: :desc)
@employees = @business.employees
end
# GET /events/business_settings?id=1
def business_settings
@business = Business.find(params[:id])
@employees = @business.employees
end
# POST /events/business_settings?id=1
def update_business_settings
@business = Business.find(params[:id])
if params[:admin_password] != "fieldrocketadmin"
flash[:error] = "Unauthorized access"
redirect_to business_settings_events_path(id: @business.id)
return
end
# Ensure visitor_device_info_auth_token is nil if empty
if business_params[:visitor_device_info_auth_token].blank?
params[:business][:visitor_device_info_auth_token] = nil
end
if @business.update(business_params)
flash[:notice] = "Business settings were successfully updated."
redirect_to business_settings_events_path(id: @business.id)
else
flash[:alert] = "There was an error updating the business settings: #{@business.errors.full_messages.join(', ')}"
redirect_to business_settings_events_path(id: @business.id)
end
end
# # get "/events/get_daily_leaderboard_stats"
def get_daily_leaderboard_stats #get number of daily active users between start_date and end_date.
start_date = params[:start_date] ? Date.parse(params[:start_date]) : Date.today - 30.days
end_date = params[:end_date] ? Date.parse(params[:end_date]) : Date.today
daily_active_users = calculate_daily_active_users(start_date, end_date)
render json: daily_active_users
end
def calculate_daily_active_users(start_date, end_date)
daus = []
(start_date..end_date).each do |date| #TODO: This is calculating daily active businesses!, not users.
# count = Event.where("DATE(created_at) = ?", date).distinct.count(:business_id)
count = Event.where("DATE(created_at) = ?", date).distinct.count(:employee_id)
daus << [date, count]
end
daus
end
# get "/events/get_weekly_leaderboard_stats"
def get_weekly_leaderboard_stats
start_date = params[:start_date] ? Date.parse(params[:start_date]) : Date.today - 30.days
end_date = params[:end_date] ? Date.parse(params[:end_date]) : Date.today
# Calculate weekly active users and render the result as JSON
weekly_active_users = calculate_weekly_active_users(start_date, end_date)
render json: weekly_active_users
end
#TODO: This is calculating weekly active businesses!, not users.
def calculate_weekly_active_users(start_date, end_date)
# Align the start_date to the previous Sunday
start_date = start_date - start_date.wday
# Initialize an empty array to store the weekly active users data
waus = []
# Iterate through each week between start_date and end_date
while start_date <= end_date
# Calculate the end of the week (Saturday)
week_end = start_date + 6.days
# Count the distinct business_ids with events during the week
count = Event.where("DATE(created_at) >= ? AND DATE(created_at) <= ?", start_date, week_end)
.distinct.count(:employee_id)
# .distinct.count(:business_id)
# Add the weekly active users count and the week range to the waus array
waus << {start_date: start_date, end_date: week_end, count: count}
# Move to the next week
start_date += 7.days
end
waus
end
# GET /events/1 or /events/1.json
def show
end
# GET /events/new
def new
@event = Event.new
end
# GET /events/1/edit
def edit
end
# POST /events or /events.json
def create
@event = Event.new(event_params)
if !current_employee.nil?
@event.employee_id = current_employee.id
end
respond_to do |format|
if @event.save
format.html { redirect_to event_url(@event), notice: "Event was successfully created." }
format.json { render :show, status: :created, location: @event }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @event.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /events/1 or /events/1.json
def update
respond_to do |format|
if @event.update(event_params)
format.html { redirect_to event_url(@event), notice: "Event was successfully updated." }
format.json { render :show, status: :ok, location: @event }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @event.errors, status: :unprocessable_entity }
end
end
end
# DELETE /events/1 or /events/1.json
def destroy
@event.destroy
respond_to do |format|
format.html { redirect_to events_url, notice: "Event was successfully destroyed." }
format.json { head :no_content }
end
end
# GET '/event_total_report_data'
def total_report_data
if !params[:business_id].nil? && !params[:business_id].empty?
@business = Business.find(params[:business_id]).id
end
if !params[:employee_id].nil? && !params[:employee_id].empty?
@employee = Business.find(params[:employee_id]).id
end
render json: ReportsHelper.event_total_report_data(@start_range, @end_range, @business, @employee).to_json
end
private
# Use callbacks to share common setup or constraints between actions.
def set_event
@event = Event.find(params[:id])
end
def set_ranges
@start_range = params[:start]&.to_datetime
@end_range = params[:end]&.to_datetime
end
# Only allow a list of trusted parameters through.
def event_params
params.require(:event).permit(:resource, :resource_id, :name)
end
def business_params
params.require(:business).permit(
:name,
:app_percentage_fee,
:suppress_analytics,
:force_signature_on_payment,
:surcharge_customer,
:stripe_enabled,
:parse_call_transcripts_using_ai,
:gps_prompts_user_to_clock_in,
:poll_server_to_refresh_unread_message_count,
:extra_instructions_for_job_request,
:placeholder_for_job_request_notes,
:manually_outbound_call_feature_flag,
:enable_seo_posts,
:target_keywords,
:wordpress_url,
:wordpress_auth_token,
:wordpress_username,
:send_appointment_reminders_to_customers,
:automated_appointment_sms_reminder_template,
:visitor_device_info_auth_token,
:enable_sales_tax_calculation,
:sales_tax,
:quickbooks_tax_code_ref_id,
:enable_total_from_multiplying_by_quantity, # Add this line
:hide_job_request_address_fields
)
end
end
class BusinessesController < ApplicationController
skip_before_action :authenticate_employee!, only: [:create, :new, :privacy_policy, :eula_policy, :inbound_sms, :connect_call, :twilio_access_token_ios, :twilio_access_token_android, :twilio_incoming, :twilio_after_inbound_call, :twilio_recording, :twilio_incoming_gather, :public_marketing_sources, :gmailoauthcallbackmobile, :home, :get_started, :public_website, :request_data_deletion_form, :post_request_data_deletion_form]
#skip_before_action :verify_authenticity_token, only: [:inbound_sms, :connect_call, :twilio_access_token_ios, :twilio_access_token_android, :twilio_incoming, :twilio_after_inbound_call, :twilio_recording, :twilio_incoming_gather]
before_action :set_business, only: [:edit, :update, :show, :gmailoauthcallback, :stripeoauthcallback, :update_attribute]
layout 'layout_public_website', only: [:public_website, :home]
include GmailSender
include Quickbooks
include GoogleCalendarApi
require "google/apis/calendar_v3"
require 'twilio'
include ApplicationHelper
#POST/businesses
def create
created_params = new_business_params #Here is actually where we're going to create.....?
@business = Business.new created_params
if new_business_params.has_key?(:logo)
@business.logo.attach(params[:business][:logo])
end
if @business.save
#Need to manually link the business_id in the employee or else the business_id doesn't get set for the employee. Not completely sure why..
@business.employees.first.update(business_id: @business.id)
@business.employees.first.register_for_job_request_notifications
@business.employees.first.register_for_call_received_notification
#Homemade Event Tracker Analytics Event.
Event.create(resource: "Business", name: "Registration", resource_id: @business.id, employee_id: @business.employees.first.id)
date_time_now = DateTime.now
#Tracking event for business registration
Analytics.track( user_id: @business.id, event: 'New Business Registered', properties: { plan: 'Business', phone: @business.phone})
log_tracked_event({
business_id: @business.id,
name: @business.name.to_s,
employee_name: @business.employees&.first&.full_name,
email: @business.email.to_s,
phone: @business.phone,
survey_answers: @business.survey_answers,
signed_in_at: date_time_now,
event: 'Business Registered'
})
respond_to do |format|
format.html {
flash[:success] = 'Registration successful.'
sign_in @business.employees.first
#Create the notification for job requests for this first employee, to send it to their cell phone.
redirect_to root_path
}
format.json {
render :status => 200, :json => @business
#create an access code + phone number to save in database.. then when they submit the access code to our server, then it will
#activate the account for usage, and assign an auth token to the employee..
}
end
else
respond_to do |format|
format.html {
flash[:error] = 'Registration failed.'
flash[:error] = @business.errors.full_messages.to_sentence
render :new
}
format.json {
formatted_errors = @business.errors.full_messages.concat(@business.employees.first.errors.full_messages).reject{|error| error == "Employees is invalid"}
render :status => 404, :json => {errors: formatted_errors}
}
end
end
end
#GET /businesses/:id
def show
render json: current_business.to_json
end
#GET /businesses/business_info
def business_info
render json: current_business.to_json
end
def edit
handle_breadcrumb "Settings", edit_business_path(current_business)
end
def new
#TODO: Temporarily disabling mandatory subscriptions.
@subscription = Subscription.where(key: params[:key]).first
if @subscription.nil?
redirect_to "/subscriptions/new", flash: {error: 'You must have a subscription before registering.'}
return
end
@business = Business.new
@business.employees.build
@minimum_password_length = 6 # TODO: Owen!!! Set from employee mode.
end
def get_started
@title = "Field Rocket | Sign Up"
#recreate the mobile app experience.
# Name, Business Name, Email
# Phone Number
# Access Code/Verification Code Block
# Survey
# Select Twilio Number
# Take to Dashboard
end
def update
if edit_business_params.has_key?(:logo)
@business.logo.attach(params[:business][:logo])
end
if edit_business_params.has_key?(:phone_tree_greeting_mp3)
@business.phone_tree_greeting_mp3.attach(params[:business][:phone_tree_greeting_mp3])
end
if @business.update(edit_business_params)
redirect_to root_path, notice: 'Business was successfully updated.'
else
render :edit
end
end
def gmailoauthcallback
auth_client = GmailSender.get_auth_client
Rails.logger.info "Gmail oauth just directed user here with params: #{params}"
auth_client.code = params["code"]
access_token = auth_client.fetch_access_token
Rails.logger.info "Just got cashed in our code and got this access token: #{access_token}"
if access_token["refresh_token"].nil?
flash[:error] = "Gmail didn't give us a refresh token. Contact the support team for help resolving this issue"
Rails.logger.error "Gmail didn't respond with a refresh token! We will lose access to this gmail account when the current access token expires."
end
current_employee.update(:gmail_token => {"access"=>access_token["access_token"], "refresh"=>access_token["refresh_token"]}.to_json)
if !(params["state"].nil? || params["state"].empty?)
state = JSON.parse(params["state"])
if state["email_type"] == "invoice_delivery"
invoice_id_to_deliver = state["invoice_id"]
# additional_options = state["additional_options"]
@invoice = @business.invoices.find(invoice_id_to_deliver)
#TODO: This is a re-implementation of logic that's already in the invoice controller. Anti-pattern.
invoice_html_as_str = render_to_string(:template => 'invoices/_invoice.html.erb', :layout => false, :locals => { :editable => false, :show_payment_link => true, :show_download_link => true})
# pdf = nil
pdf = WickedPdf.new.pdf_from_string(invoice_html_as_str)
@invoice.email_to_customer(pdf, {email_recipients: state["email_recipients"]}, current_employee, nil, send_without_refresh = true)
redirect_to @invoice, flash: {success: "#{@invoice.invoice_type} was successfully sent"}
end
end
#If we fall through, we will just render the 'you are now authenticated with gmail' page
end
#This is what's used when authenticating with mobile device. (Doesn't require authentication...)
def gmailoauthcallbackmobile
auth_client = GmailSender.get_auth_client_for_mobile
Rails.logger.info "Gmail oauth just directed user here with params: #{params}"
auth_client.code = params["code"]
access_token = auth_client.fetch_access_token
Rails.logger.info "Just got cashed in our code and got this access token: #{access_token}"
if access_token["refresh_token"].nil?
# flash[:error] = "Gmail didn't give us a refresh token. Contact the support team for help resolving this issue"
Rails.logger.error "Gmail didn't respond with a refresh token! We will lose access to this gmail account when the current access token expires."
end
#If current employee is nil.. then how could we authenticate..??
#We would have to somehow check the params of this token.
#we need to grab the employee id and update it here.
if !(params["state"].nil? || params["state"].empty?)
state = JSON.parse(params["state"])
employee_id = state["employee_id"]
Rails.logger.info("Trying to set this gmail token for employee id: #{employee_id}")
Employee.find(employee_id).update(:gmail_token => {"access"=>access_token["access_token"], "refresh"=>access_token["refresh_token"]}.to_json)
end
render json: "You are now authenticated to send invoices and estimates through your Gmail account using Field Rocket", status: 202
end
def stripeoauthcallback
if current_business.authenticate_stripe_user_account!(params[:code])
flash[:success] = "Your business is now ready to accept payments through stripe!"
else
flash[:error] = "There was a problem setting up payments. Try again"
end
end
def deletedemodata
#remove demo data, view should update accordingly
customers_to_delete = current_business.customers.where(last_name: "Example")
customers_to_delete.each do |customer|
customer.jobs.delete
customer.delete
end
current_business.save
end
def quickbooksoauthcallback
if current_business.authenticate_quickbooks_account(params[:code], params[:realmId], params[:state])
flash[:success] = "Your business is now connected with Quickbooks"
# if current_business.customers.where(:quickbooks_id => nil).count == current_business.customers.count
# @import_customers = true
# end
else
flash[:error] = "There was a problem connecting with Quickbooks."
end
end
def settings
respond_to do |format|
format.html {
handle_breadcrumb "Settings", settings_path
}
format.json {
render :json => current_business.settings
}
end
end
#GET /integrations
def integrations
end
def twilio_settings
@business = current_business
end
def privacy_policy
end
def eula_policy
end
def stripe_setup
end
def support_help
end
def onboarding
@quickbooks_synced = current_business.authenticated_with_quickbooks?
@stripe_enabled = current_business.stripe_access_code.present?
@gmail_synced = current_business.employees.first.gmail_token.present?
@wordpress_connected = current_business.wordpress_url.present? && current_business.wordpress_auth_token.present? && current_business.wordpress_username.present?
@facebook_connected = current_business.facebook_page_connections.any?
@employees_count = current_business.employees.count >= 3
@first_invoice_created = current_business.invoices.any?
@job_requests_count = current_business.job_requests.count >= 5
@logo_uploaded = current_business.logo.attached?
end
def reports
handle_breadcrumb "Reports", "/reports"
end
def open_support_ticket
ApplicationMailer::send_support_ticket(current_employee, params[:email], params[:phone], params[:support_message]).deliver_now
redirect_to root_path, flash: {success: "Your support ticket has been submitted! We'll get back to you as soon as possible."}
end
def quickbooks_setup
@authorization_code_url = Quickbooks.GetAuthorizationCodeUrl()
current_business.update(quickbooks_csrf_token: Quickbooks.get_state_from_auth_uri(@authorization_code_url))
# redirect_to @authorization_code_url
end
def facebook_setup
end
def facebook_oauth_redirect
access_token = params[:access_token]
user_id = params[:user_id]
# page_id = params[:page_id] #ignore page id for now.
if access_token.present? && user_id.present? #&& page_id.present? #ignore page id for now
current_business.update(facebook_access_token: access_token, facebook_user_id: user_id) # ignore page id for now: , facebook_page_id: page_id)
# save_facebook_page_connections(params[:pages], current_business.id)
render json: { success: true, message: "Facebook account successfully linked." }, status: :ok
else
render json: { success: false, message: "Failed to link Facebook account. Missing access token or user ID." }, status: :unprocessable_entity
end
#eventually, we will want to save the page IDs as well
end
def save_facebook_page_connections
pages = params[:pages]
pages.each do |page|
FacebookPageConnection.create(
business_id: current_business.id,
facebook_id: page['id'],
access_token: page['access_token'],
name: page['name'],
page_type: page['category']
)
end
end
def google_calendar_setup
client = Signet::OAuth2::Client.new(GoogleCalendarApi.client_options)
redirect_to client.authorization_uri.to_s
end
#After authenticating with Google for Google Calendar permission.
def gcaloauthcallback
client = Signet::OAuth2::Client.new(GoogleCalendarApi.client_options)
client.code = params[:code]
response = client.fetch_access_token! #Exchange the auth token immediately to get the access token.
current_employee.update(:google_calendar_token => response.to_json)
redirect_to "/schedule", flash: {success: "You are now authenticated with Google Calendar"}
end
def update_attribute
business = Business.find(params[:id])
if safe_attribute_update? params[:attribute]
if business.update({params[:attribute] => params[:value]})
render json: business, status: 202
else
render json: business.errors, status: :unprocessable_entity
end
else
render json: business.errors, status: :unauthorized
end
end
def connect_call
Rails.logger.info("params for connect_call: #{params}")
if !twilio_authenticate
return
end
business, from, to, body = Twilio.parse_info_from_inbound_params(params)
Rails.logger.info("Just got a call from: #{from} to our business: #{business&.name}. Will connect to twilio client name: #{business.twilio_mobile_client_name}")
customer_thats_calling = MessagesHelper.resolve_phone_number(business, from)
twiml_xml = Twilio.connect_call(business.twilio_mobile_client_name, customer_thats_calling)
Rails.logger.info("Replying with TWIML: #{twiml_xml}")
render xml: twiml_xml
end
def twilio_incoming
Rails.logger.info("params for twilio_incoming: #{params}")
if !twilio_authenticate
return
end
business, from, to, body = Twilio.parse_info_from_inbound_params(params)
if params["Digits"]
call_sid = params["CallSid"] #TODO: Here is where we could skip the gather process entirely, and just forward immediately to their phone.
Twilio.begin_recording_call_from_sid(call_sid)
response = Twilio::TwiML::VoiceResponse.new
#TODO: We could just change this phone_to_forward_call_to to be the backup phone number if there currently a call active with the business.
phone_to_forward_call_to = business.get_phone_number_from_branch_input_gather(Integer(params["Digits"] || 1)) || business.get_default_forward_number
#TODO: This is where we could look and see if there is an active call going on right now in the business, and if there is then we can forward to the backup phone number.
if params[:show_customer_number] #If /twilio_incoming?show_customer_number=true, then we don't set the caller_id to be the business number. Otherwise, we do.
response.dial do |dial| #We had an issue with TMobile auto-rejecting certain phone numbers, so if the contractor has TMobile, sometimes we need to NOT have the caller_id be the businesses customer facing twilio phone number.
dial.number(phone_to_forward_call_to)
end
else #The default case is to use the Businesses's Twilio phone number as the caller_id for ALL incoming calls. This way, the user is able to conditionally call forward, and block all unknown callers, and have them redirected through Field Rocket to have them press 1 to connect, and they get the text message.
response.dial(caller_id: Twilio.internationalize(business.twilio_number)) do |dial| #Set the caller id to come from the business number. This is important for business owners who are using their personal number and business number as the same phone number, so that they can still get call filtering if they use the conditional call forwarding and silence all uknown callers, then the call will get redirected through a saved contact on their phone and allow it to ring through.
dial.number(phone_to_forward_call_to)
end
end
business_twilio_call_message = business.twilio_missed_call_message
business_id = business.id #Run this auto respond message separately to reduce latency. It's important to have low latency in this controller actin because a phone call is waiting to connect.
AutoRespondTextFromInboundCallWorker.perform_async(from, to, business_twilio_call_message, business_id) #This kicks off a job that will send an automatic text message to the caller if the twilio_missed_call_message is configured.
render xml: response
else
render xml: Twilio.inbound_call(business)
end
Rails.logger.info(" We made it here even after rendering a response from the controller!!!")
end
def all_businesses
@title = "Field Rocket | All Businesses"
@businesses = Business.where(subscription: [nil, 0])
end
def twilio_after_inbound_call
#TODO: This is where we can conditionally tell if the dialed call was completed or not. If it was, display voicemail and send the customer an SMS message with a link to filling out the online form for them.
Rails.logger.info("params for twilio_after_inbound_call: #{params}")
call_was_unanswered = Twilio.uncompleted_call_statuses.include? params["DialCallStatus"]
if call_was_unanswered
business, from, to, body = Twilio.parse_info_from_inbound_params(params)
# Send text message to the incoming number apologizing for missing a call for this business.
business.messages.create(to: from, sent_from: business.twilio_number, content: business.twilio_missed_call_message.nil? ? "Sorry I couldn't pick up your call, if you need to get on my schedule, will you please reply here and I'll get back to you with some times I can come out!" : business.twilio_missed_call_message)
# Now render the voicemail since it was a missed call.
render xml: Twilio.voicemail_instructions(business)
else
render xml: Twilio.hangup
end
end
# This comes after a user leaves a voicemail message in Field Rocket.. how do we set this up?
def twilio_recording
mp3_recording_url = params["RecordingUrl"] + ".mp3"
sms_notification = "You got a voice recording message from #{params["From"]} - listen here: #{mp3_recording_url}"
business, from, to, body = Twilio.parse_info_from_inbound_params(params)
Rails.logger.info("Business was: #{business&.name}, from: #{from}, to: #{to}")
call = Call.create(to: to, call_from: from, recording_uri: mp3_recording_url, start_time: DateTime.now, end_time: DateTime.now, business_id: business.id)
# Mesage.create
# :to, :call_from, :caller_name, :recording_uri, :price, :start_time, :end_time, :duration, :status, :uri, :notes, :outcome)
# byebug
Twilio.send_text(business.employees.first.phone, sms_notification, business.twilio_number)
head :ok
end
def twilio_incoming_gather
business, from, to, body = Twilio.parse_info_from_inbound_params(params)
business_twilio_call_message = business.twilio_missed_call_message
business_id = business.id #Run this auto respond message separately to reduce latency. It's important to have low latency in this controller actin because a phone call is waiting to connect.
AutoRespondTextFromInboundCallWorker.perform_async(from, to, business_twilio_call_message, business_id) #This kicks off a job that will send an automatic text message to the caller if the twilio_missed_call_message is configured.
# business, from, to, body = Twilio.parse_info_from_inbound_params(params)
message = business.twilio_missed_call_message.blank? ? "This is #{business&.employees.first&.first_name} with #{business&.name}. Sorry I couldn't pick up your call, if you need to get on my schedule, will you please reply here and I'll get back to you with some times I can come out!" : business.twilio_missed_call_message
# byebug
# Send text message to the incoming number apologizing for missing a call for this business.
business.messages.create(to: from, sent_from: business.twilio_number, content: message)
# Now render the voicemail since it was a missed call.
render xml: Twilio.voicemail_instructions(business)
end
def twilio_access_token_ios #TODO: What are the security implications of not testing params or anything else URL?
Rails.logger.info("params for twilio_access_token: #{params}")
render xml: Twilio.access_token(params[:identity], false)
end
def twilio_access_token_android
Rails.logger.info("params for twilio_access_token: #{params}")
render xml: Twilio.access_token(params[:identity], true)
end
def public_marketing_sources
render json: Business.find(params[:id]).marketing_sources.to_json #I don't think this is dangerous because it's a read only operation & I don't think there will be anything critical exposed here.
end
def suppress_analytics
Business.find(params[:id]).toggle_suppress_analytics
render json: {success: true}
end
def home
#check domain
#if domain is not fieldrocket.com, redirect to fieldrocket.com
#from here, how do we tell if it's a different domain or not.?
domain = request.env["HTTP_HOST"].dup
domain.slice! "www."
Rails.logger.info("Domain request for: " + domain)
#if domain ends in fieldrocket.us, or homeservice.cloud, then just proceed as normal:
if domain.ends_with? "fieldrocket.us" or domain.ends_with? "getservicerocket.com" or domain.ends_with? "fieldrocket.site" or domain.ends_with? "homeservice.cloud" or domain.include? "localhost:3000" or domain.include? "127.0.0.1" or domain.include? "ngrok"
Rails.logger.info("Taking user to field rocket application home page")
redirect_to schedule_url
elsif domain.ends_with? "llamapress.ai"
Rails.logger.info("Taking user to llama press application home page")
redirect_to "/llama_bot"
else #we need to see if we have the domain here in our database. If we do, we need to redirect there.
Rails.logger.info("Searching for domain: #{domain} in our home_page database")
found_homepage = HomePage.find_by(domain: domain)
if found_homepage.nil?
Rails.logger.info("Domain: #{domain} not found in our home_page database. Redirecting to fieldrocket.com")
redirect_to "https://www.fieldrocket.us"
else
#TODO: How do I get them to land on public_website controller, and set the params with the business ID without changing their root URL ?
Rails.logger.info("Domain: #{domain} found in our home_page database. Redirecting to: #{found_homepage.domain}")
# Set any variables the action needs
@business = Business.find(found_homepage.business_id)
@home_page = @business.home_page
#TODO: Render a subpage as well if there is an end to this domain.
# Render the template associated with the action
render template: 'businesses/public_website'
return
end
end
end
def public_website
@business = @business || Business.find(params[:id])
@home_page = @business.home_page
@job_request = @business.job_requests.new
@job_request.first_name = params[:first_name]
@job_request.last_name = params[:last_name]
@job_request.street_address = params[:street_address]
@job_request.city = params[:city]
@job_request.email = params[:email]
@job_request.zip = params[:zip]
@job_request.phone = params[:phone]
@job_request.state = params[:state] || @business.state
@title = "#{@business.name} | Submit Request"
@use_business_logo_as_favicon = true
@secret_key = @business.secret_key
#TODO: Parse UTM here that will be useful.? Or will there be a type of redirection anyways?
# https://rocketr.us/l/45?utm_source=fieldrocket.us&utm_content=&utm_campaign=fr_seo
# params["utm_campaign"]: # "fr_seo"
# params["utm_content"]: # "stump_x_drain_system_installation"
# params["utm_source"]: # "fieldrocket.us"
@job_request.utm_source = params["utm_source"]
@job_request.utm_campaign = params["utm_campaign"]
@job_request.utm_content = params["utm_content"]
if params["utm_source"]
utm_json_object = {"utm_source": params["utm_source"], "utm_content": params["utm_content"], "utm_campaign": params["utm_campaign"]}
Event.create(resource: "Job Link Clicked", name: utm_json_object.to_json, business_id: @business&.id, notes: utm_json_object.to_json)
end
end
def pictures
# First, get IDs of Customers and Jobs for the current_business
customer_ids = Customer.where(business_id: current_business.id).pluck(:id)
job_ids = Job.where(business_id: current_business.id).pluck(:id)
# Then, find attachments for these IDs and group them by record type and ID
# @grouped_attachments = ActiveStorage::Attachment
# .where(record_type: 'Customer', record_id: customer_ids)
# .or(ActiveStorage::Attachment.where(record_type: 'Job', record_id: job_ids))
# .group_by { |attachment| [attachment.record_type, attachment.record_id] }
#
# # Convert the grouped attachments to an array and paginate
# @page = params[:page] || 1
# @per_page = 20 # Adjust this value as needed
# @grouped_attachments = Kaminari.paginate_array(@grouped_attachments.to_a).page(@page).per(@per_page)
#
# # Old code being a copycat of CompanyCam UI and is being saved here for future use
@attachments = ActiveStorage::Attachment.where(record_type: 'Customer', record_id: customer_ids)
.or(ActiveStorage::Attachment.where(record_type: 'Job', record_id: job_ids))
@attachments = @attachments.map do |attachment|
if attachment.blob.content_type == 'image/heic'
attachment.variant(convert: 'jpg').processed
else
attachment
end
end
end
# GET /new_images_upload
def new_images_upload
#Show the HTML page for the user to upload new images.
end
# POST /attach_pre_uploaded_s3_blob_to_business.json
def attach_pre_uploaded_s3_blob_to_business
@business = current_business
Rails.logger.info("Receiving a request from the mobile app to attach an uploaded S3 file to business #{current_business.name}! Key was: #{params[:key]}, signed id was: #{params[:signed_id]}")
if @business.images.attach(params[:signed_id]) #params needed: job_id, signed_id, and key.
@blob = ActiveStorage::Blob.find_signed(params[:signed_id])
@attachment = @blob.attachments&.first
render json: {key: params[:key], image: @business.extract_image_data(@attachment)}, status: 200
else
render json: {error: "error"}, status: 500
end
end
def attach_multiple_pre_uploaded_s3_blobs_to_business
# params: images: []
# has_many_attached :images
# imges include blob IDS and attach to this model business.rb
@business = current_business
if params[:image_blob_ids].present?
params[:image_blob_ids].each do |blob_id|
blob = ActiveStorage::Blob.find_signed(blob_id)
@business.images.attach(blob.signed_id) if blob
end
render json: { message: "Images attached to business successfully" }, status: 200
else
render json: { error: "No image blob IDs provided" }, status: 400
end
#TODO: Attach to this business and send back success message. Assume JSON format
end
# GET /view_unassigned_images
def view_images_and_assign_them_to_job
end
def get_business_images
page = params[:page] || 1
per_page = 10
offset = (page.to_i - 1) * per_page
@images = current_business.images.order(created_at: :desc).offset(offset).limit(per_page)
render json: @images.map { |img|
{
attachment_id: img.id,
blob_id: img.blob_id,
url: img.service.send(:object_for, img.key).public_url,
image: current_business.extract_image_data(img)
}
}
end
#businesses_controller.rb
# POST /assign_images_to_new_job
def assign_images_to_new_job
# Retrieve the selected image IDs from the request parameters
selected_image_ids = params[:selectedImages] #NOTE: these are only the indexes. We need to find a way to bring these in.
#locations_attributes: [:id, :street_address, :city, :zip_code, :state, :customer_id, :business_id])
puts params
@customer = current_business.customers.create(
first_name: params[:firstName],
last_name: params[:lastName],
)
@location = @customer.locations.create(
{
street_address: params[:streetAddress],
city: params[:city],
zip_code: params[:zipCode],
state: params[:state]
}
)
# @customer.save!
# Create a new job with the provided details
@job = @customer.jobs.create(
customer_id: @customer.id,
notes: params[:jobNotes],
title: params[:jobTitle],
business_id: current_business.id,
location_id: @location.id
)
unless params[:price].nil?
@invoice = @job.invoices.create(
job_id: @job.id,
notes: params[:jobNotes],
)
@line_item = @invoice.line_items.create(price: params[:price], quantity: 1)
@payment = @invoice.payments.create(amount: params[:price], payment_type: Payment.payment_types["Cash"], paid_on: DateTime.now, notes: "Auto marked as paid due to this invoice being created from picture page")
end
if @customer.save && @job.save && @invoice.save
# Attach the selected images to the newly created job
begin
selected_image_ids.each do |image|
image_id = image["attachment_id"]
image = current_business.images.find_by(id: image_id)
if image.present?
@job.images.attach(image.blob)
# Confirm that the image is attached to the @job
attached_image = @job.images.find_by(blob_id: image.blob_id)
if attached_image.present?
# Unattach the image from the current_business
current_business.images.find_by(blob_id: image.blob_id).purge
else
# Handle the case when the image fails to attach to the @job
# You can log an error, raise an exception, or take appropriate action
logger.error("Failed to attach image #{image.blob_id} to the job #{@job.id}")
end
end
end
rescue => e
# Capture the error message
error_message = "Error occurred while attaching image: #{e.message}"
# Log the error message using the Rails logger
Rails.logger.error(error_message)
# You can also re-raise the exception if needed
raise e
end
render json: { message: 'Images assigned to new job successfully', job: @job, customer: @customer, invoice: @invoice }, status: :created
else
render json: { errors: @job.errors.full_messages }, status: :unprocessable_entity
end
end
def all_businesses
@businesses = Business.where.not(subscription_id: nil).joins(:subscription).where('subscriptions.mrr < ?', 200)
# @mrr = Subscription.where(deactivated: false).sum(:mrr)
# business.subscription.nil? ? 'color:red;' : business.subscription.deactivated ? 'color:#8B8000;' : 'color:green;'%>
end
#POST /businesses/pre_signed_s3_url_for_upload
def get_signed_s3_url_for_uploading_images #TODO:! Duplicate code in job_requests_controller
@blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: params[:filename],
byte_size: params[:byte_size],
checksum: params[:checksum],
content_type: params[:content_type]
)
render json: { url: @blob.service_url_for_direct_upload(expires_in: 30.minutes), headers: @blob.service_headers_for_direct_upload, signed_id: @blob.signed_id, key: @blob.key }, status: 200
end
#Get the form for user to request to delete data
def request_data_deletion_form
end
#POST the form for user to request to delete data
def post_request_data_deletion_form
# Extract user information from the form submission
user_email = params[:email]
user_name = params[:name]
user_message = params[:message]
# # Generate the email content
# email_subject = "Data Deletion Request"
# email_body = <<~EOS
# User Name: #{user_name}
# User Email: #{user_email}
# Message:
# #{user_message}
# EOS
# Send the email to Kody
# KodyMailer.data_deletion_request(email_subject, email_body).deliver_now
# Redirect to a confirmation page or display a success message
redirect_to "/request_data_deletion", notice: "Your data deletion request has been submitted."
end
# Sales:
# 07/31/2024: Prospecting Dashboard Improvements /events/registrations
def toggle_contacted
@business = Business.find(params[:id])
@business.toggle_contacted!
render json: { contacted_at: @business.contacted_at }
end
private
def twilio_authenticate
if !Twilio.verify_account_sid(params[:AccountSid])
access_denied_redirect
return false
end
return true
end
def updateable_attributes
["invoice_terms", "estimate_terms", "survey_answers", "attribution_source"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
def set_business
@business = current_business
end
def edit_business_params
params.require(:business).permit(
:name,
:email,
:street_address,
:city,
:state,
:zip_code,
:phone,
:notes,
:logo,
:phone_tree_greeting_mp3,
:twilio_number,
:sms_form_template,
:twilio_missed_call_message,
:invoice_number,
:estimate_number
)
end
def new_business_params
params.require(:business).permit(
:name,
:email,
:street_address,
:city,
:state,
:zip_code,
:phone,
:notes,
:logo,
:subscription_id,
:timezone,
:survey_answers,
:attribution_source,
employees_attributes: [
:id,
:first_name,
:last_name,
:phone,
:email,
:password,
:password_confirmation,
:business_id,
:user_needs_tutorial
]
)
end
end
class TextTemplatesController < ApplicationController
before_action :set_text_template, only: [:show, :edit, :update, :destroy, :text_templates_variables]
include TextTemplatesHelper
# GET /text_templates
# GET /text_templates.json
def index
@text_templates = current_business.text_templates.all
end
# GET /text_templates/1
# GET /text_templates/1.json
def show
end
# GET /text_templates/new
def new
@text_template = current_business.text_templates.new
end
# GET /text_templates/1/edit
def edit
end
# POST /text_templates
# POST /text_templates.json
def create
@text_template = current_business.text_templates.new(text_template_params)
respond_to do |format|
if @text_template.save
format.html { redirect_to @text_template, notice: 'Text template was successfully created.' }
format.json { render :show, status: :created, location: @text_template }
else
format.html { render :new }
format.json { render json: @text_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /text_templates/1
# PATCH/PUT /text_templates/1.json
def update
respond_to do |format|
if @text_template.update(text_template_params)
format.html { redirect_to @text_template, notice: 'Text template was successfully updated.' }
format.json { render :show, status: :ok, location: @text_template }
else
format.html { render :edit }
format.json { render json: @text_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /text_templates/1
# DELETE /text_templates/1.json
def destroy
@text_template.destroy
respond_to do |format|
format.html { redirect_to text_templates_url, notice: 'Text template was successfully destroyed.' }
format.json { head :no_content }
end
end
# GET /text_templates/:id/text_templates_variables
def text_templates_variables
customer_data = current_business.customers.find_by(id: params[:customer_id])
employee_job = customer_data&.find_latest_employee_job
if !@text_template.nil?# && !customer_data.nil?
# We aren't going to check customer_data anymore because we are going to evaluate the template even if we don't have all the info, so this will make it more useable by a user.
new_temp_msg = @text_template.body.gsub("{{employee}}", current_employee.first_name )
new_temp_msg = new_temp_msg.gsub("{{user}}", current_employee.first_name)
new_temp_msg = new_temp_msg.gsub("{{customer_first_name}}", customer_data&.first_name || "")
new_temp_msg = new_temp_msg.gsub("{{contact_first_name}}", customer_data&.first_name || "")
# Location
new_temp_msg = new_temp_msg.gsub("{{location}}", customer_data&.display_address || "")
# Dates
new_temp_msg = new_temp_msg.gsub("{{start_time}}", evaluate_start_time_for_text_message(employee_job&.time_start || ""))
new_temp_msg = new_temp_msg.gsub("{{readable_start_time}}", evaluate_readable_start_time_for_text_message(employee_job&.time_start || ""))
new_temp_msg = new_temp_msg.gsub("{{end_time}}", evaluate_end_time_for_text_message(employee_job&.time_end || ""))
new_temp_msg = new_temp_msg.gsub("{{day}}", evaluate_day_for_text_message(employee_job&.time_start || ""))
new_temp_msg = new_temp_msg.gsub("{{date}}", evaluate_date_for_text_message(employee_job&.time_start || ""))
msg = {
status: "200",
message: "Success",
data: new_temp_msg
}
else
msg = {
status: "203",
message: "Error! Text Template Id or Customer Id is missing.",
data: ""
}
end
respond_to do |format|
format.html
format.json { render json: msg }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_text_template
@text_template = current_business.text_templates.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def text_template_params
params.require(:text_template).permit(:title, :body)
end
end
class JobsController < ApplicationController
include PopulateEmployeeOptions
include PopulateRosterOptions
include CustomersHelper
include MessagesHelper
load_and_authorize_resource
# skip_before_action :authenticate_employee!, :only => [:image_upload, :upload_image]
before_action :set_job, only: [:show, :edit, :update, :destroy, :relevant_emails, :search_invoices, :update_attribute, :update_job_and_times, :timesheets, :upload_image_from_mobile]
before_action :set_employee_job, only: [:update_job_and_times]
before_action :set_appointment, if: -> {params[:appointment]}
# GET /jobs
# GET /jobs.json
def index
respond_to do |format|
format.html {
@q = current_business.jobs.ransack(params[:q])
@jobs = @q.result(distinct:true).paginate(page: params[:page], per_page: 20)
@p = current_business.customers.ransack(params[:q])
@customers = @p.result(distinct:true)
}
format.json {
@jobs = current_business.jobs.ransack(params[:q]).result().limit(10)
render :json => @jobs
}
end
end
# GET /jobs/1
# GET /jobs/1.json
def show
handle_breadcrumb "Job #{@job.id}", job_path(@job)
respond_to do |format|
format.html do
@employee_jobs = @job.employee_jobs.includes([:employee]).map do |ej|
{employee_name: ej.employee&.full_name, time_start: ej.start, time_end: ej.end}
end
end
format.json { render :json => @job.to_json }
end
end
def generate_invoice
@job_to_invoice = current_business.jobs.find(job_id)
redirect_to new_invoice_path(:job_to_invoice => @job_to_invoice)
end
# GET /jobs/new
def new
@job.id ? handle_breadcrumb("New Invoice/Estimate", generate_invoice_path(@job)) : false
@business_name = current_business.name
@job = current_business.jobs.new
customer = Customer.find(params[:customer_id])
if params[:customer_id] && customer&.business_id == current_business.id
@customer = customer
@job.customer_id = customer.id
@job.location_id = customer.default_location&.id
@job.customer_contact_id = customer.default_contact&.id
@job.owner_id = current_employee.id
end
end
# GET /schedule -- this is the home page
def schedule
reset_breadcrumbs
if @appointment
reset_breadcrumbs()
handle_breadcrumb("Appointment #{@appointment}", "/schedule?appointment=#{@appointment}")
end
@business_name = current_business.name #error here.. current_business didn't get set after registration from the application?????
@job = current_business.jobs.new
@job.employee_jobs.build
@job.build_customer
@job.build_location
@employee_options = populate_employee_options
@employee = current_employee
@text_templates = current_business.text_templates#[{id: 1, title: 'On My Way', evaluatedUrl: "This%20is%20{{employee}}%20from%20#{@business_name},%20just%20wanted%20to%20let%20you%20know%20I%27m%20on%20my%20way%20to%20{{location}}for%20our%20appointment%20that%20is%20on%20{{day}}%20around%20{{start_time}}%20to%20{{end_time}},%20I%20should%20be%20there%20in%20about%20thirty%20minutes!"}]
request_ids_that_have_been_assigned = current_business.jobs.includes([:job_request]).where.not(job_request_id: nil).map {|j| j.job_request_id}
@requests = current_business.job_requests.where.not(id: [request_ids_that_have_been_assigned]).order('created_at DESC')
end
# GET /jobs/1/edit
def edit
end
# GET /job/:id/search_invoices
def search_invoices
@invoices = @job.invoices&.ransack(params[:q]).result(distinct:true)
respond_to do |format|
format.json {render json: @invoices}
end
end
# GET /jobs/date/employeeid.js
def jobs_week_of
end
# POST /jobs
# POST /jobs.json
def create
# Pretty complicated here. What do I do if a user is simply trying to schedule a new appointment for an existing job. Is this the right route? What if they want to edit the job notes.? Or notify the customer of a new appointment/job.?
# The app doesn't support that at this moment. How do I fix that though? I do want it to support those changes, but it feels like I will have to hack all of this stuff together.
#Answer: I added the below code to allow the params[:job][:id] field to be set, which will now just update the current job and add a new employee_job (appointment) to that job. Used on Mobile app to schedule new appointment for an existing job seamlessly.
editing_existing_job_and_adding_new_appointment = !params[:job][:id].blank?
if editing_existing_job_and_adding_new_appointment
@job = current_business.jobs.find(params[:job][:id])
@job.update(job_params) #by calling update here, we are able to update the notes, any customer info, etc. at the same time we are scheduling a new employee_job. This also creates a new employee when employee_job params are nested in the job params, which I believe is always the case.
else
@job = current_business.jobs.new(job_params)
@job.customer.business_id = current_business.id
end
customer_errors = []
#This param only gets set from the fullcalendar appointment, which is why even though each job has multiple appoitments, it will only have had one appointment when we notify the customer, so we can take the job's first appointment and use it to build the notification sms to the customer.
send_customer_sms_of_their_time = params[:job][:notify] ?
ActiveModel::Type::Boolean.new.cast(params[:job][:notify]) && !@job.customer.phone.blank?
: false
@job.location&.customer = @job.customer #Cool way to assign the location to the customer even though neither have been saved yet.
Rails.logger.info("New Job Created by #{current_employee&.id}!!! With Info: #{params}. Notify customer = #{send_customer_sms_of_their_time}")
respond_to do |format|
if @job.save
Event.create(resource: "Job", name: "Created", resource_id: @job.id, employee_id: current_employee.id)
#Tracking schedule create event
Analytics.track( user_id: current_employee.id, event: 'Schedule Created', properties: { plan: 'Schedule' } )
if send_customer_sms_of_their_time
@job.notify_customer_of_appointment_time(current_employee)
end
#before redirecting, I want to check the parameters and see if we need to take them to the page where we create a new invoice for the job
if params[:job][:create_invoice] == 'true'
format.html { redirect_to generate_invoice_path(@job), flash: {success: "Job was successfully created. Now let's create an invoice or estimate" }}
else
format.html { redirect_to @job, flash: {success: 'Job was successfully created.' }}
end
format.json { render :show, status: :created, location: @job }
else
@job.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
job_errors = @job.formatted_error_messages
format.json { render json: (job_errors + customer_errors).to_json, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /jobs/1
# PATCH/PUT /jobs/1.json
def update
respond_to do |format|
if @job.update(job_params)
Event.create(resource: "Job", name: "Updated", resource_id: @job.id, employee_id: current_employee.id)
format.html { redirect_to @job, flash: {success: 'Job was successfully updated.' }}
format.json { render :show, status: :ok, location: @job }
else
@job.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :edit, flash: {error: "Job couldn't be updated." }}
format.json { render json: @job.formatted_error_messages, status: :unprocessable_entity }
end
end
end
# DELETE /jobs/1
# DELETE /jobs/1.json
def destroy
@customer = @job.customer
@job.delete_all_appointments #Destroy all appointments for this job too.
@job.destroy
respond_to do |format|
format.html { redirect_to @customer, notice: 'Job was successfully destroyed.' }
format.json { head :no_content }
end
end
#PUT /update_job_and_times, used for editing appointment details, or dragging & dropping existing events to new time slots on the calendar, or when you resize them on the calendar
def update_job_and_times
Rails.logger.info("Update Job and Times from Employee: #{current_employee.full_name}")
Rails.logger.info("For Customer: #{@job.customer.full_name}, Job: #{@job.id}, Employee Job: #{@employee_job.id}")
Rails.logger.info("Employee Job Params: #{employee_job_params}")
Rails.logger.info("Job Params: #{job_params}")
Rails.logger.info("Job Address Before Changes: #{@job.location&.street_address}")
Rails.logger.info("Employee Job Before Changes: #{@employee_job.inspect}")
submittedWithEmployee = !employee_job_params["employee_id"].blank?
if !submittedWithEmployee
Rails.logger.info("Employee id was not set when trying to update a job")
end
@employee_job.job_id = @job.id #This is used for when we are switching an employee_job to have a new job_id after selecting from the link_to_job menu
if @employee_job.update(employee_job_params) && @job.update(job_params)
Event.create(resource: "Job", name: "Updated", resource_id: @job.id, notes: @job.previous_changes.except(:updated_at), employee_id: current_employee.id)
Event.create(resource: "EmployeeJob", name: "Updated", resource_id: @employee_job.id, notes: @employee_job.previous_changes.except(:updated_at), employee_id: current_employee.id)
update_location_to_be_default = params["job"]["location_attributes"] && ActiveModel::Type::Boolean.new.cast(params["job"]["location_attributes"]["default_location"])
if update_location_to_be_default
Rails.logger.info("We are set to update the location to be default on the appointment modal!")
@job.location&.set_customer_default_location!
end
render :json => {:response => "Success"}, :status => 200
else
render json: (@job.formatted_error_messages + @employee_job.formatted_error_messages).to_json, status: :unprocessable_entity
end
end
def update_appointment
end
#This is to get the history of the employees who have changed a job or appointment, to hold them accountable or to track who has been working on a job
def history
@job = current_business.jobs.find(params[:id])
# Get job events
job_events = current_business.events.where(resource: "Job", resource_id: @job.id)
# Get employee job events
employee_job_ids = EmployeeJob.where(job_id: params[:id]).pluck(:id)
employee_job_events = current_business.events.where(resource: "EmployeeJob", resource_id: employee_job_ids)
# Combine both sets of events and order them by created_at
@events = (job_events + employee_job_events).sort_by(&:created_at).reverse
end
def create_employee_job
@job = current_business.jobs.find(new_employee_job_params[:job_id])
@appointment = @job.employee_jobs.new(new_employee_job_params)
respond_to do |format|
if @appointment.save
format.html { redirect_to @job, flash: {success: 'Job appointment was successfully added.'}}
format.json { render status: 200, json: @appointment}
else
@appointment.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :show, flash: {error: "Couldn't add appointment."}}
format.json {render json: @appointment.formatted_error_messages.to_json, status: :unprocessable_entity}
end
end
end
def get_appointment_details
appointment = current_business.employee_jobs.find(params[:id])
end
#GET /jobs/:id/emails
def relevant_emails
results = []
results << {id: @job.customer.email, text: @job.customer.full_name}
results << {id: @job.customer_contact&.email, text: @job.customer_contact&.full_name} #Should include other contacts?
render :json => {results: results}
end
# GET /jobs/1/image_upload
# def image_upload
# @job = Business.where(secret_key: params[:secret_key]).first.jobs.find(params[:id])
# @secret_key=params[:secret_key]
# end
#POST /jobs/1/image
def upload_image
# @job = current_business.nil? ? Business.where(secret_key: params[:secret_key]).first.jobs.find(params[:id]) : current_business.jobs.find(params[:id])
image_uploader(@job, image_params, true)
end
#POST /jobs/:id/upload_image_from_mobile
def upload_image_from_mobile
@blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: params[:filename],
byte_size: params[:byte_size],
checksum: params[:checksum],
content_type: params[:content_type]
)
render json: { url: @blob.service_url_for_direct_upload(expires_in: 30.minutes), headers: @blob.service_headers_for_direct_upload, signed_id: @blob.signed_id, key: @blob.key }, status: 200
end
def attach_pre_uploaded_s3_blob_to_job
@job = current_business.jobs.find(params[:id])
Rails.logger.info("Receiving a request from the mobile app to attach an uploaded S3 file to job #{params[:id]}! Key was: #{params[:key]}, signed id was: #{params[:signed_id]}")
if @job.images.attach(params[:signed_id])
render json: {key: params[:key]}, status: 200
else
render json: {error: "error"}, status: 500
end
end
#DELETE /jobs/1/image
def delete_image
delete_uploaded_image(@job, params[:image_id])
end
#POST /jobs/1/set_job_status?job_status_id=2
def set_job_status
respond_to do |format|
if @job.update(:job_status_id => params[:job_status_id])
format.html {redirect_to @job, flash: {success: "Successfully set the Job Status"}}
format.json { render json: @job.job_status, status: 200 }
else
format.html { render :show, flash: {error: "Job status couldn't be updated." }}
format.json {render json: @job.formatted_error_messages.to_json, status: :unprocessable_entity}
end
end
end
def timesheets
@job.timesheets
end
def update_attribute
if safe_attribute_update? params[:attribute]
before_value = @job[params[:attribute]]
if @job.update({params[:attribute] => params[:value]})
render json: @job, status: 200
else
render json: @job.errors, status: :unprocessable_entity
end
else
render json: @job.errors, status: :unauthorized
end
end
# POST /convert_to_job_request
def convert_to_job_request
respond_to do |format|
@job = current_business.jobs.find(params[:id])
if !@job.nil?
#If this job has job request, simply delete this job but maintain the job request.
#if this job doesn't have job request, then create new job request and delete this job.
@job_request = @job.convert_to_job_request!
if @job.destroyed? && !@job_request.nil?
format.all { render json: @job_request, status: 200 }
else
format.all { render json: @job.errors, status: :unprocessable_entity }
end
else
format.all { render json: @job.errors, status: :unprocessable_entity }
end
end
end
private
# Use callbacks to share common setup or straints between actions.
def set_job #the @job is mainly getting set from CanCan's load_and_authorize_resources above. But for JSON calls, it's not setting the @job properly, so we need this for JSON calls.
@job = current_business.jobs.find(params[:id])
end
def set_employee_job
#This is referencing the business's employee jobs because we need to be able to reassign this employee job's parent job to a new job
@employee_job = @job.business.employee_jobs.find(employee_job_params[:id])
end
def set_appointment
@appointment = current_business.employee_jobs.find(params[:appointment])&.id
end
def updateable_attributes
["job_status_id","notes","owner_id","location_id"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
# Never trust parameters from the scary internet, only allow the white list through.
def job_params
params.require(:job).permit(
:notes,
:customer_id,
:location_id,
:customer_contact_id,
:job_status,
:images,
:owner_id,
:job_request_id,
:job_status_id,
location_attributes: [:street_address, :city, :zip_code, :state, :customer_id, :id],
employee_jobs_attributes: [:id, :employee_id, :job_id, :time_start, :time_end, :date, :employee_job_type_id, :appointment_color_id],
customer_attributes: [:id, :first_name, :last_name, :phone, :email, :notes, :marketing_source_id, :business_id],
)
end
def new_employee_job_params
params.require(:employee_job).permit(
:employee_id,
:business_id,
:job_id,
:time_start,
:time_end,
:employee_job_type_id
)
end
def image_params
params.require(:job).permit(
images: []
)
end
def employee_job_params
return job_params["employee_jobs_attributes"]["0"]
end
end
class WebhooksController < ApplicationController
require "pipedrive"
require "mixmax"
skip_before_action :authenticate_employee!, only: [:pipedrive]
# POST "/webhooks/pipedrive"
def pipedrive
event_data = JSON.parse(request.body.read)
#use company id to get the info like email
Rails.logger.info("Pipedrive event data: #{event_data}")
if event_data['event'] == 'updated.deal'
deal_id = event_data['current']['id']
stage_order_number = event_data['current']['stage_order_nr']
if stage_order_number == 3 #Lost Contact Drip stage (the first stage order number is 0)
deal_details = Pipedrive.get_deal_details(deal_id)
person_id = deal_details['data']['person_id']['value']
person_details = Pipedrive.get_person_details(person_id)
email = person_details['data']['email'].first['value']
name = person_details['data']['name']
sequence_id = "664b9d43bb47034f671be24d" #Exhausted call follow up efforts sequnce id
Mixmax.add_user_to_mixmax_campaign(email, name, sequence_id)
elsif stage_order_number == 2 # Had Convo + Interested (Drip)
# GOAL: Get them to schedule a demo + onboarding call with us so we can close them.
deal_details = Pipedrive.get_deal_details(deal_id)
person_id = deal_details['data']['person_id']['value']
person_details = Pipedrive.get_person_details(person_id)
email = person_details['data']['email'].first['value']
name = person_details['data']['name']
sequence_id = "67eac21a51ca3422782b0e19" #Test that this works to enroll them.
Mixmax.add_user_to_mixmax_campaign(email, name, sequence_id)
end
end
render json: { status: 'success' }, status: :ok
rescue StandardError => e
render json: { status: 'error', message: e.message }, status: :internal_server_error
end
end
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy, :update_attribute]
# GET /products
# GET /products.json
def index
@products = current_business.products.ransack(params[:q]).result(distinct:true)
end
# GET /products/1
# GET /products/1.json
def show
end
# GET /products/new
def new
@product = current_business.products.new
end
# GET /products/1/edit
def edit
end
# POST /products
# POST /products.json
def create
@product = current_business.products.new(product_params)
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to @product, notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: @product }
else
format.html { render :edit }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
end
end
# DELETE /products/1
# DELETE /products/1.json
def destroy
@product.destroy
respond_to do |format|
format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
# POST /quickbooks_products_services_import
def quickbooksimport
if current_business.import_quickbooks_products_services!
flash[:success] = "Imported products and services from Quickbooks"
render json: 'success', status: 202
else
render json: 'error', status: :unprocessable_entity
flash[:error] = "Failed to import some products and services"
end
end
def update_attribute
if safe_attribute_update? params[:attribute]
before_value = @product[params[:attribute]]
if @product.update({params[:attribute] => params[:value]})
render json: @product, status: 202
else
render json: @product.errors, status: :unprocessable_entity
end
else
render json: @product.errors, status: :unauthorized
end
end
private
def updateable_attributes
["name", "description", "price", "SKU"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = current_business.products.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:name, :description, :price, :SKU)
end
end
class ApplicationController < ActionController::Base
# protect_from_forgery with: :exception if proc { |c| c.request.format != 'application/json' }
# protect_from_forgery with: :null_session if proc { |c| c.request.format == 'application/json' }
skip_forgery_protection
before_action :authenticate_employee!, :set_timezone, :set_context
rescue_from CanCan::AccessDenied, with: :access_denied_redirect
helper_method :current_business, :get_breadcrumbs
before_action :turn_off_304_not_modified
def set_context
@context = request.path
@view_path = resolve_view_path
end
def get_breadcrumbs
if current_employee.nil?
return []
end
if session["breadcrumbs_#{current_employee.id}"].nil?
session["breadcrumbs_#{current_employee.id}"] = [['Dispatch Board', "/schedule"]]
end
session["breadcrumbs_#{current_employee.id}"]
end
def handle_breadcrumb(name, route, force = false)
if request.format.symbol == :html || force
found_duplicate = false
get_breadcrumbs.each do |bc|
if (bc.second == route)
found_duplicate = true
break
end
end
if found_duplicate
bc = get_breadcrumbs.pop
while bc.second != route do
bc = get_breadcrumbs.pop
end
end
get_breadcrumbs << [name, route]
if get_breadcrumbs.length > 5 #Keep the breadcrumbs from getting too out of control
get_breadcrumbs.shift
end
end
end
#this is to try and handle when there's duplicate breadcrumbs
def handle_appointment_breadcrumb(name, route, force = false)
remove_breadcrumb("Appointment") #Remove any appointment breadcrumbs first.
handle_breadcrumb(name, route, force)
end
def remove_breadcrumb(name)
get_breadcrumbs.reject {|bc| bc.first.include?(name)}
end
def pop_breadcrumb()
get_breadcrumbs.pop
end
def reset_breadcrumbs
session["breadcrumbs_#{current_employee.id}"] = [['Dispatch Board', "/schedule"]]
end
def set_dispatchboard_appointment_breadcrumb(appointment_id)
if !session["breadcrumbs_#{current_employee.id}"].nil?
session["breadcrumbs_#{current_employee.id}"][0] = ["Appointment #{appointment_id}", "/schedule?appointment=#{appointment_id}"]
end
end
def reset_dispatchboard_appointment_breadcrumb
if !session["breadcrumbs_#{current_employee.id}"].nil?
session["breadcrumbs_#{current_employee.id}"][0] = ["Dispatch Board", "/schedule"]
end
end
def after_sign_in_path_for(resource)
root_path
end
# Override CanCanCan defaults.
def current_ability
@current_ability ||= Ability.new(current_employee)
end
def current_business
current_employee.try :business
end
def turn_off_304_not_modified
#update last-modified so content is always fresh
headers['Last-Modified'] = Time.now.httpdate
end
def image_uploader(model, image_params, return_response = true)
respond_to do |format|
image_count_before_upload = model.images.count
image_params[:images].each do |img|
if img.present?
if File.size(img.tempfile) <= 25000000 #Don't let people upload files larger than 25 Mb.
model.images.attach(img)
else
flash[:error] = "#{img.original_filename} was too large (20 Megabytes limit)" #We say 20 MB to play it safe. Real limit is 25 MB.
end
end
end
images_uploaded = model.images.count == image_count_before_upload
return images_uploaded unless return_response
if images_uploaded
format.json {render json: model.formatted_error_messages.to_json, status: :unprocessable_entity}
format.html {redirect_to model, flash: {error: "Couldn't upload image to job"}}
end
format.json {render json: model, status: 200 }
format.html{redirect_to model, flash: {success: 'Image uploaded'}}
end
end
def delete_uploaded_image(model, image_id)
image_count_before_upload = model.images.count
model.images.find(image_id).purge
if image_count_before_upload == image_count_before_upload
flash[:success] = "Successfully deleted image"
else
flash[:error] = "Something went wrong"
end
respond_to do |format|
format.json { render json: {id: image_id}, status: :ok}
end
end
private
def access_denied_redirect
flash[:error] = ('You are not authorized to access this page.')
redirect_to root_path
end
def unauthorized_forbidden_response
render plain: 'You are not authorized to access this page.', status: :forbidden
end
def flash_error_messages(record)
record&.errors&.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
end
def set_timezone
user_timezone = current_business&.timezone #WARNING: This will cause a bug if you set this business.timezone to anything other than 'nil' right now, it will offset all employee_job (appointments) that are stored by default in UTC.
# if user_timezone
# Time.zone = user_timezone
# else
# Time.zone = 'America/Denver'
# end
end
def resolve_view_path
route = Rails.application.routes.recognize_path(request.path, method: request.method)
controller = route[:controller]
action = route[:action]
# Check if there's a specific route helper for this path
route_helper = Rails.application.routes.named_routes.helper_names.find do |helper|
path = send("#{helper}_path") rescue nil
path == request.path
end
if route_helper
# If a route helper is found, use it to determine the view
controller, action = route_helper.to_s.sub(/_path$/, '').split('_', 2)
end
"app/views/#{controller}/#{action}.html.erb"
rescue ActionController::RoutingError
nil
end
end
class LocationsController < ApplicationController
before_action :set_location, only: [:show, :update, :destroy, :make_default, :update_attribute]
def index
respond_to do |format|
format.html {
@locations = current_business.locations.ransack(params[:q]).result()
}
format.json {
@locations = current_business.locations.ransack(params[:q]).result()
render :json => @locations
}
end
end
# POST /locations
# POST /locations.json
def create
if location_params[:business_id]&.empty?
params[:location][:business_id] = current_business.id
end
@location = Location.new(location_params)
respond_to do |format|
if @location.save
if params[:set_as_default_location]
@location.customer.update(:location_id => @location.id)
end
format.html { redirect_to @location.customer, flash: {success: "Successfully added location"} }
format.json { render status: 200, json: @location }
else
format.html { redirect_to @location.customer, flash: {error: "Couldn't add location"} }
format.json { render json: @location.errors, status: :unprocessable_entity }
end
end
end
def show
respond_to do |format|
format.html
format.json { render :json => @location }
end
end
def update
respond_to do |format|
if @location.update(location_params)
format.html #TODO:
format.json {render json: @location, status: 202}
else
format.html #TODO:
format.json {render json: @location.formatted_error_messages, status: :unprocessable_entity}
end
end
end
def destroy
customer = @location.customer
if @location.is_default?
customer.update(location_id: nil)
end
@location.destroy
respond_to do |format|
format.html { redirect_to customer, flash: {success: 'Location was successfully destroyed.'}}
format.json { render status: 200 }
end
end
def make_default
@location.set_customer_default_location!
@customer = @location.customer
flash[:success] = "You've successfully set the default location to: #{@location.display_address}"
render "customers/show"
end
def update_attribute
if safe_attribute_update? params[:attribute]
# make_default if params[:attribute].include?('default_location')
before_value = @location[params[:attribute]]
if @location.update({params[:attribute] => params[:value]})
render json: @location, status: 202
else
render json: @location.errors, status: :unprocessable_entity
end
else
render json: @location.errors, status: :unauthorized
end
end
private
def updateable_attributes
["street_address", "city", "zip_code", "state", "default_location"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
# Use callbacks to share common setup or constraints between actions.
def set_location
location = Location.find(params[:id])
if location.customer.business.id == current_business.id #Verifying that the location is only set if it's scoped for the current business..
@location = location
end
end
# Never trust parameters from the scary internet, only allow the white list through.
def location_params
params.require(:location).permit(:id, :street_address, :city, :zip_code, :state, :customer_id)
end
end
class SubscriptionsController < ApplicationController
require "securerandom"
before_action :set_subscription, only: [:show, :edit, :update, :destroy, :show]
skip_before_action :authenticate_employee!, :only => [:new, :create] #For customers who need acess so they can pay
# GET /subscriptions/new
def new
@subscription = Subscription.new
if params[:monthly]
@monthly = true
else
@monthly = false
end
end
# GET /subscriptions/new_manual
def new_manual
@business = Business.find(params[:business_id])
@subscription = Subscription.new(key: SecureRandom.uuid, email: @business.email, activated: nil, bill_annually: true)
if params[:monthly]
@monthly = true
else
@monthly = false
end
end
# POST /subscriptions/create_manual
def create_manual
# byebug
@business = Business.find(params[:business_id])
# :email, :key, :activated, :mrr, :name, :sku, :deactivated
# activated == nil means that this key hasn't been activated yet. activated == true means the key is active and valid. activated == false means it's been activated, but key is not currently valid.
@subscription = Subscription.new(subscription_params)
# @subscription.key = key
@business = Business.find(params[:business_id])
respond_to do |format|
if @subscription.save
if @business
@business.update(subscription_id: @subscription.id)
end
format.html { redirect_to "/events/business?id=#{@business.id}", flash: {success: "You've successfully subscribed! Now it's time to register your business."}, params: {token: @subscription.key}}
# format.json { render :show, status: :created, location: @subscription }
else
format.html { render :new_manual }
# format.json { render json: @subscription.errors, status: :unprocessable_entity }
end
end
end
# POST /subscriptions
# POST /subscriptions.json
def create
Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
customer = Stripe::Customer.create({
email: params[:stripeEmail],
source: params[:stripeToken],
})
# yearly_plan_id = 'plan_HBmzreuwwyZPyW'.freeze # production env
# yearly_plan_id = 'plan_HCbEC6xdXH6dvn'.freeze # test env
# monthly_plan_id = 'plan_HBmxYzamdBbV6I'.freeze # production env
# monthly_plan_id = 'plan_HCbDgpV7twpDPR'.freeze # test env
plan_id = params[:monthly] ? ENV["STRIPE_MONTHLY_KEY"] : ENV["STRIPE_ANNUAL_KEY"]
Rails.logger.info("Subscription with a monthly #{params[:monthly]}")
Rails.logger.info("Subscribing with Plan Id: #{plan_id}}")
subscription = Stripe::Subscription.create({
customer: customer.id,
items: [{plan: plan_id}],
})
# activated == nil means that this key hasn't been activated yet. activated == true means the key is active and valid. activated == false means it's been activated, but key is not currently valid.
@subscription = Subscription.new(key: SecureRandom.uuid, email: params[:stripeEmail], activated: nil, bill_annually: !params[:monthly])
respond_to do |format|
if @subscription.save
format.html { redirect_to "/businesses/register?key=#{@subscription.key}", flash: {success: "You've successfully subscribed! Now it's time to register your business."}, params: {token: @subscription.key}}
format.json { render :show, status: :created, location: @subscription }
else
format.html { render :new }
format.json { render json: @subscription.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /subscriptions/1
# PATCH/PUT /subscriptions/1.json
def update
respond_to do |format|
if @subscription.update(subscription_params)
format.html { redirect_to @subscription, notice: 'Subscription was successfully updated.' }
format.json { render :show, status: :ok, location: @subscription }
else
format.html { render :edit }
format.json { render json: @subscription.errors, status: :unprocessable_entity }
end
end
end
# DELETE /subscriptions/1
# DELETE /subscriptions/1.json
def destroy
@business = Business.find_by(subscription_id: @subscription.id)
Business.where(subscription_id: @subscription.id).update_all(subscription_id: nil)
@subscription.destroy
respond_to do |format|
format.html { redirect_to "/events/business?id=#{@business&.id}", notice: 'Subscription was successfully destroyed.' }
format.json { head :no_content }
end
end
# GET /subscriptions/1
# GET /subscriptions/1.json
def show
# handle_breadcrumb "#{@customer.full_name}", url_for(@customer)
# @query = @customer.jobs.ransack(params[:q])
# @jobs = @query.result(distinct:true)
respond_to do |format|
format.html
format.json {render json: @subscription.as_json}
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_subscription
@subscription = Subscription.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def subscription_params
params.require(:subscription).permit(:email, :key, :activated, :mrr, :name, :sku, :deactivated)
end
end
class CustomersController < ApplicationController
#skip_before_action :authenticate_employee! #comment when you need to check customers table
load_and_authorize_resource
before_action :set_customer, only: [:show, :edit, :update, :destroy, :customer_locations_ajax, :update_attribute, :jobs, :images, :lite_show, :get_last_job, :images]
before_action :turn_off_304_not_modified, only: [:index, :lite_show, :show, :search_customer_ajax, :invoices, :images, :jobs, :get_last_job_lite] #prevents the server from getting fussy about sending all the customers at once.
# GET /customers
# GET /customers.json
# GET /customers.csv
def index
handle_breadcrumb "Customers", "/customers"
@q = current_business.jobs.ransack(params[:q])
@jobs = @q.result(distinct:true)
@p = current_business.customers.includes([:locations]).ransack(params[:q])
@customers = nil #3-27-22, seems like a waste, we should just set customers to this result..?
if request.format.json? ##if it's .json we want to send all the customers so that we can display all of them on the mobile app.
@customers = @p.result(distinct: true).paginate(page: params[:page], per_page: 20)
else
@customers = @p.result(distinct:true).paginate(page: params[:page], per_page: 20)
end
if params[:q]
inputValue = params[:q]&.values&.first
@contacts = current_business.customer_contacts.ransack({first_name_or_last_name_or_full_name_or_phone_or_email_cont: inputValue}).result(distinct: true)
@locations = current_business.locations.ransack({street_address_or_city_or_state_or_full_address_cont: inputValue}).result(distinct: true)
end
respond_to do |format|
format.html
format.csv {send_data current_business.customers.to_csv, filename: "#{current_business.name} customers #{Date.today}.csv"}
format.json {@customers.includes([:customer_contacts]).to_json}
end
end
# GET /customers/1/images.json
def images
respond_to do |format|
format.html
format.json {render json: {images: @customer.get_images}}
end
end
# GET /customers/1
# GET /customers/1.json
def show
handle_breadcrumb "#{@customer.full_name}", url_for(@customer)
@query = @customer.jobs.ransack(params[:q])
@jobs = @query.result(distinct:true)
respond_to do |format|
format.html
format.json {render json: @customer.as_json.merge(@customer.get_customer_info_for_page)}
end
end
# GET /customers/1/lite.json
def lite_show
respond_to do |format|
format.html
format.json {render json: @customer.as_json_lite}
end
end
# GET /customers/new
def new
handle_breadcrumb "New Customer", "/customers/new"
@customer = current_business.customers.new
@customer.locations.build
end
# GET /customers/1/edit
def edit
handle_breadcrumb "Edit #{@customer.full_name}", edit_customer_path(@customer)
end
# POST /customers
# POST /customers.json
def create
@customer = current_business.customers.new(customer_params)
respond_to do |format|
if @customer.save
format.html { redirect_to @customer, flash: {success: 'Customer was successfully created.'} }
format.json { render json: @customer, status: :created, location: @customer}
else
# flash_error_messages(@customer)
@customer.errors.messages do |attr_name, message|
Rails.logger.error("#{attr_name}: #{message}")
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :new }
format.json { render json: @customer.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /customers/1
# PATCH/PUT /customers/1.json
def update
respond_to do |format|
if @customer.update(customer_params)
format.html { redirect_to @customer, flash: {success: 'Customer was successfully updated.' }}
format.json { render :show, status: :ok, location: @customer }
else
@customer.errors.messages do |attr_name, message|
flash[:error] = "#{attr_name}: #{message}"
end
format.html { render :edit}
format.json { render json: @customer.errors, status: :unprocessable_entity }
end
end
end
# DELETE /customers/1
# DELETE /customers/1.json
def destroy
@customer.destroy
respond_to do |format|
format.html { redirect_to customers_url, flash: {success: 'Customer was successfully destroyed.' }}
format.json { head :no_content }
end
end
# GET /customers_merge
def merge_customers_index
@base_id = params[:base_id]
@other_id = params[:other_id]
#default merge.. take the base first name, base last name, base phone number, base email address, update all customer contacts to have base customer_id, update all customer locations to have base customer_id, update all jobs to have base customer_id, updates all invoices to have base customer_id, update all pictures to have base customer id.?
#How to merge...???
# Information we will need to merge:
# first name
# last name
# base phone number --
# base email address --
# customer contacts -- naive merge these. (don't overwrite at all, simply reassign all locations to the customer base.)
# phone number
# email
# first name
# last name
# notes
# customer locations -- naive merge these. have a second ability to merge addresses within a customer record.
# street address
# city
# zip code
# state
# jobs -- all we have to do is simply update the customer ID on all the jobs.
# employee jobs -- all we have to do is update the customer ID on all the jobs
# invoices. Either they have a job ID, or they don't. If they don't, then just update the customer ID.
# pictures. pictures are either uploaded onto the base of the customer record, or on to the job.
# psuedo-code:
# 1: pick a base customer record to merge into.
# 2: figure out which information can be discarded -- simple: if phone numbers do not match, simply add in as new customer contact. If phone numbers match, discard the matching ones.
# For example, which phone number do we keep?
# which email address do we keep? -- simple: if email addresses do not match, simply add in as new customer contact. If phone numbers match, discard the matching ones.
# which customer contacts do we keep? -- simple: all of them.
# which customer locations do we keep? Or do we merge into the job as well..?? -- simple: all of them, duplicate all of them.
# how to merge jobs..? -- simple way: just reassign customer id on all the job types. (What about location id.??) Simple, 80/20 and naive approach: duplicate all locations.
# how to merge invoices..? -- simple way: just keep the invoices assigned to the job.
# how to merge pictures..?
end
# POST /customers_merge
def initiate_customer_merge
@base_id = params[:base_id]
@other_id = params[:other_id]
@merge_instructions = params[:merge_instructions]
if @merge_instructions.nil?
current_business.customers.find(@base_id).merge_with_other_customer(@other_id)
end
render json: {message: "Success"}
#default merge.. take the base first name, base last name, base phone number, base email address, update all customer contacts to have base customer_id, update all customer locations to have base customer_id, update all jobs to have base customer_id, updates all invoices to have base customer_id, update all pictures to have base customer id.?
end
# POST /customers_merge_all
def initiate_all_customer_merge
@base_id = params[:base_id]
@others_to_merge = params[:others_to_merge]
@merge_instructions = params[:merge_instructions]
if @merge_instructions.nil?
current_business.customers.find(@base_id).merge_with_other_customers(@others_to_merge)
end
render json: {message: "Success"}
#default merge.. take the base first name, base last name, base phone number, base email address, update all customer contacts to have base customer_id, update all customer locations to have base customer_id, update all jobs to have base customer_id, updates all invoices to have base customer_id, update all pictures to have base customer id.?
end
def search_customer_ajax
@search_name = params[:name]
@found_customers = current_business.search_customers(@search_name)
render json: @found_customers.to_json
end
def get_last_job_lite
render json: @customer.jobs.last&.as_json_lite
end
def get_customer_data
@search_name = params[:name]
@found_customers = current_business.search_customers(@search_name)
@customers_data = @found_customers.map do |customer| {
value: customer.id,
label: customer.full_name,
customer: customer
}
end
render json: @customers_data.to_json
end
def customer_locations_ajax
render json: @customer.locations
end
def customer_contacts_ajax
render json: @customer.customer_contacts
end
def import_customers_page
end
def import
Customer.import(params[:customer_import][:file], current_business.id)
flash[:success] = "Customer data imported successfully."
redirect_to customers_path
rescue StandardError => e
flash[:error] = "Import error. " + e.message
redirect_to customers_path
end
# POST /quickbooks_customer_import
def quickbooksimport
# TODO: How do I get a visual UI element to tell the customer to wait for a moment...
if current_business.import_quickbooks_customers!
flash[:success] = "Imported data from customers"
render json: 'success', status: 202
else
render json: 'error', status: :unprocessable_entity
flash[:error] = "Failed to import data from customers"
end
end
def update_attribute
if safe_attribute_update? params[:attribute]
before_value = @customer[params[:attribute]]
if @customer.update({params[:attribute] => params[:value]})
render json: @customer, status: 202
else
render json: @customer.errors, status: :unprocessable_entity
end
else
render json: @customer.errors, status: :unauthorized
end
end
def find_customer_from_phone_number
@p = current_business.customers.ransack(params[:q])
@customers = @p.result(distinct:true).paginate(page: params[:page], per_page: 20)
end
def message_thread
end
def jobs
jobs = @customer.jobs.includes([:job_status, :location, :customer_contact, :owner, :business, :employee_jobs])
render json: jobs
end
#TODO: This has an n+1 error.
def invoices #how to format..?
render json: @customer.invoices.order('issued_on DESC') #This will ensure that the invoices with the most recent issued_on dates appear first in the list.
end
# GET /customers/:id/data_to_evaluate_text_template.json
def data_to_evaluate_text_template
#TODO: We need to evaluate the text templates for the latest employee job that they have.
ej = current_business.employee_jobs.where(customer_id: @customer.id).last
#Merge employee job info with @customer data info.
customer_and_appointment_info = @customer.as_json.merge(ej.as_json)
render json: customer_and_appointment_info
end
#POST /customers/call
#POST /call
def call_contact_from_entering_phone_number
# params: :phone
c = current_business.calls.create(
start_time: DateTime.now,
is_active: true,
is_outbound: true,
phone_number_employee_is_calling_from_in_outbound_call: current_employee.phone,
customer_phone_number_in_outbound_call: params[:phone],
employee_id: current_employee.id)
Twilio.connect_employee_to_customer_through_call(Twilio.internationalize(current_employee.phone), Twilio.internationalize(params[:phone]), Twilio.internationalize(current_business.twilio_number))
end
#GET /customers/call
def call
#gets the customers/call page.
end
def upload_image
image_uploader(@customer, image_params, true)
end
#DELETE /customer/1/image
def delete_image
delete_uploaded_image(@customer, params[:image_id])
end
#PUT /customer/:id/attach_image_from_message
def attach_image_from_message_to_customer_record
message_id = params[:message_id]
if @customer.attach_image_from_message_to_customer_record(message_id)
render json: {message: "Success", status: 202}
else
render json: {message: "Error", status: 500}
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_customer
@customer = current_business.customers.find(params[:id])
end
def updateable_attributes
["first_name", "last_name", "notes", "email", "phone", "marketing_source_id", "location_id"]
end
def safe_attribute_update?(attribute)
updateable_attributes.include?(attribute)
end
# Never trust parameters from the scary internet, only allow the white list through.
def customer_params
params.require(:customer).permit(
:first_name,
:last_name,
:city,
:phone,
:company,
:notes,
:email,
:marketing_source_id,
locations_attributes: [:id, :street_address, :city, :zip_code, :state, :customer_id, :business_id])
end
def image_params
params.require(:customer).permit(
images: []
)
end
end
class WebPagesController < ApplicationController
skip_before_action :authenticate_employee!
before_action :set_web_page, only: %i[ show edit update destroy histories restore ]
layout "llama_bot"
# GET /web_pages or /web_pages.json
def index
@web_pages = WebPage.all
end
# GET /web_pages/1 or /web_pages/1.json
def show
content = ""
if @web_page.github_raw_url.present?
content += fetch_github_content(@web_page.github_raw_url)
else
content += @web_page.home_page.navbar_html if @web_page.use_navbar && @web_page.home_page&.navbar_html.present?
content += @web_page.content
content += @web_page.home_page.footer_html if @web_page.use_footer && @web_page.home_page&.footer_html.present?
end
# Inject the chat partial
# content = "hello!!" #inject_chat_partial(content)
if current_employee.present? #don't allow llamabot if the user isn't signed in.
content = inject_chat_partial(content)
end
# Render with a layout that includes necessary JavaScript
render inline: content.html_safe, layout: 'web_page'
end
def histories
@web_page = WebPage.find(params[:id])
@web_page_histories = @web_page.web_page_histories.order(created_at: :desc)
respond_to do |format|
format.html # Renders the default history.html.erb view
format.json do
encoded_histories = @web_page_histories.map do |history|
history_data = history.as_json
history_data['content'] = Base64.strict_encode64(history.content)
history_data
end
encoded_histories_json_object = { web_page_histories: encoded_histories }
render json: encoded_histories_json_object
end
end
end
def restore
@web_page_history = WebPageHistory.find(params[:web_page_history_id])
@web_page.restore(@web_page_history)
respond_to do |format|
format.html { redirect_to web_page_path(@web_page), notice: "Web page was successfully restored." }
format.json { render json: { message: "Web page restored successfully", web_page: @web_page }, status: :ok }
end
end
# GET /web_pages/new
def new
@web_page = WebPage.new
end
# GET /web_pages/1/edit
def edit
end
# POST /web_pages or /web_pages.json
def create
@web_page = WebPage.new(web_page_params)
respond_to do |format|
if @web_page.save
format.html { redirect_to web_page_url(@web_page), notice: "Web page was successfully created." }
format.json { render :show, status: :created, location: @web_page }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @web_page.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /web_pages/1 or /web_pages/1.json
def update
respond_to do |format|
if @web_page.update(web_page_params)
format.html { redirect_to web_page_url(@web_page), notice: "Web page was successfully updated." }
format.json { render :show, status: :ok, location: @web_page }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @web_page.errors, status: :unprocessable_entity }
end
end
end
# DELETE /web_pages/1 or /web_pages/1.json
def destroy
@web_page.destroy
respond_to do |format|
format.html { redirect_to web_pages_url, notice: "Web page was successfully destroyed." }
format.json { head :no_content }
end
end
private
def inject_chat_partial(html_content)
doc = Nokogiri::HTML(html_content)
body = doc.at('body')
chat_partial = render_to_string(partial: 'shared/chatbot/chatbot')
body.add_child(Nokogiri::HTML::DocumentFragment.parse(chat_partial))
doc.to_html
end
# Use callbacks to share common setup or constraints between actions.
def set_web_page
# @web_page = WebPage.find(params[:id])
@web_page = WebPage.friendly.find(params[:id])
end
# Only allow a list of trusted parameters through.
def web_page_params
params.require(:web_page).permit(:title, :slug, :content, :is_home_page, :business_id, :meta_description, :meta_keywords, :position, :use_navbar, :use_footer, :home_page_id, :github_raw_url)
end
#TODO: We could use this to host our websites on github and push from cursor
def fetch_github_content(url)
uri = URI(url)
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
response.body
else
nil
end
rescue StandardError => e
Rails.logger.error("Error fetching GitHub content: #{e.message}")
nil
end
end
class VisitorDeviceInfoSerializer < ActiveModel::Serializer
attributes :id, :user_agent_raw, :referrer, :url, :ip_address, :device_width, :device_height, :device_os, :os_version, :browser_version, :browser, :user_cookie_guid, :auth_token
has_one :business
end
class PublishedJobPageSerializer < ActiveModel::Serializer
attributes :id, :title, :html, :slug
has_one :job
end
class TimesheetSerializer < ActiveModel::Serializer
attributes :id, :notes, :clocked_in, :clocked_out
has_one :employee
has_one :job
end
class ReviewSerializer < ActiveModel::Serializer
attributes :id, :name, :city, :content
has_one :home_page
end
class EventSerializer < ActiveModel::Serializer
attributes :id, :resource, :resource_id, :name
end
class ForwardingNumberSerializer < ActiveModel::Serializer
attributes :id, :phone, :description
has_one :business
end
class LlamaBotSerializer < ActiveModel::Serializer
attributes :id
end
# NOTE: When we added this serializer, it broke our appointment modal being able to load all employees because it relied
# on some of the other attributes in the employee model.
# class EmployeeSerializer < ActiveModel::Serializer
# attributes :id, :email, :first_name, :last_name, :phone #Only the model attributes you specify here will be returned in an API request.
# end
class WebPageSerializer < ActiveModel::Serializer
attributes :id, :title, :slug, :content, :is_home_page, :meta_description, :meta_keywords, :position
has_one :business
has_one :page_category
end
class HomePageSerializer < ActiveModel::Serializer
attributes :id, :primary_color, :secondary_color, :tertiary_color, :facebook_url, :instagram_url
has_one :business
end
class CallSerializer < ActiveModel::Serializer
attributes :id, :to, :call_from, :caller_name, :recording_uri, :price, :start_time, :end_time, :duration, :status, :uri, :notes
end
class WebPageHistorySerializer < ActiveModel::Serializer
attributes :id, :content
has_one :web_page
end
What do you want to build?