diff --git a/.eslintrc.json b/.eslintrc.json index 2164bf67..ee854f44 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,6 +3,7 @@ "extends": [ "plugin:editorconfig/all" ], "plugins": [ "editorconfig" ], "rules": { + "prefer-arrow-callback": "error", "no-var": "error", "quotes": [ "error", diff --git a/includes/sys_form.php b/includes/sys_form.php index 2c43ed58..ce6258e6 100644 --- a/includes/sys_form.php +++ b/includes/sys_form.php @@ -38,13 +38,13 @@ function form_spinner($name, $label, $value) ', 'spinner-' . $name); diff --git a/includes/sys_template.php b/includes/sys_template.php index f2960d1b..27f68115 100644 --- a/includes/sys_template.php +++ b/includes/sys_template.php @@ -389,15 +389,18 @@ function render_table($columns, $rows, $data = true) * @param string $href * @param string $label * @param string $class + * @param string $id * @return string */ -function button($href, $label, $class = '') +function button($href, $label, $class = '', $id = '') { if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) { $class = 'btn-secondary' . ($class ? ' ' . $class : ''); } - return '' . $label . ''; + $idAttribute = $id ? 'id="' . $id . '"' : ''; + + return '' . $label . ''; } /** diff --git a/includes/view/PublicDashboard_view.php b/includes/view/PublicDashboard_view.php index c5822f0a..7a3104cb 100644 --- a/includes/view/PublicDashboard_view.php +++ b/includes/view/PublicDashboard_view.php @@ -41,24 +41,16 @@ function public_dashboard_view($stats, $free_shifts) stats(__('Angels needed for nightshifts'), $stats['needed-night']), stats(__('Angels currently working'), $stats['angels-working'], 'default'), stats(__('Hours to be worked'), $stats['hours-to-work'], 'default'), - '' ], 'statistics'), $needed_angels ], 'public-dashboard'), ]), div('first col-md-12 text-center', [buttons([ - button_js( - ' - $(\'#navbar-collapse-1,.navbar-nav,.navbar-toggler,#footer,#fullscreen-button\').remove(); - $(\'.navbar-brand\').append(\' ' . __('Public Dashboard') . '\'); - ', - icon('fullscreen') . __('Fullscreen') + button( + '#', + icon('fullscreen') . __('Fullscreen'), + '', + 'dashboard-fullscreen' ), auth()->user() ? button( public_dashboard_link($isFiltered ? [] : ['filtered' => 1] + $filter), diff --git a/includes/view/UserDriverLicenses_view.php b/includes/view/UserDriverLicenses_view.php index 0f7fe150..f32f4f2f 100644 --- a/includes/view/UserDriverLicenses_view.php +++ b/includes/view/UserDriverLicenses_view.php @@ -53,20 +53,20 @@ function UserDriverLicense_edit_view($user_source, $user_driver_license) ]), ' ' ], true); diff --git a/resources/assets/js/countdown.js b/resources/assets/js/countdown.js index cd849677..a31a2d36 100644 --- a/resources/assets/js/countdown.js +++ b/resources/assets/js/countdown.js @@ -69,14 +69,13 @@ function formatFromNow(timestamp) { /** * Initialises all countdown fields on the page. */ -ready(function () { - $.each($('[data-countdown-ts]'), function (i, e) { - const span = $(e); - const timestamp = span.data('countdown-ts'); - const text = span.html(); - span.html(text.replace('%c', formatFromNow(timestamp))); - setInterval(function () { - span.html(text.replace('%c', formatFromNow(timestamp))); +ready(() => { + document.querySelectorAll('[data-countdown-ts]').forEach((element) => { + const timestamp = element.dataset.countdownTs; + const template = element.innerHTML; + element.innerHTML = template.replace('%c', formatFromNow(timestamp)); + setInterval(() => { + element.innerHTML = template.replace('%c', formatFromNow(timestamp)); }, 1000); }); }); diff --git a/resources/assets/js/dashboard.js b/resources/assets/js/dashboard.js new file mode 100644 index 00000000..e5fd4d11 --- /dev/null +++ b/resources/assets/js/dashboard.js @@ -0,0 +1,37 @@ +import { ready } from './ready'; + +ready(() => { + if (!document.getElementById('public-dashboard')) return; + + // reload page every minute + setInterval(async () => { + const response = await fetch(window.location.href); + + if (!response.ok) { + console.warn('error loading dashboard'); + return; + } + + const responseData = await response.text(); + const parser = new DOMParser(); + const dummyDocument = parser.parseFromString(responseData, 'text/html'); + const dashboardContent = dummyDocument.getElementById('public-dashboard'); + document.querySelector('#content .wrapper').innerHTML = dashboardContent.outerHTML; + }, 60000); + + // Handle fullscreen button + // - Remove some elements from UI + // - Add "Public Dashboard" to title + document.getElementById('dashboard-fullscreen') + ?.addEventListener('click', (event) => { + event.preventDefault(); + document.querySelectorAll( + '#navbar-collapse-1,.navbar-nav,.navbar-toggler,#footer,#fullscreen-button' + ).forEach((element) => { + element.parentNode.removeChild(element); + }); + + document.querySelector('.navbar-brand') + ?.appendChild(document.createTextNode('Dashboard')); + }); +}); diff --git a/resources/assets/js/forms.js b/resources/assets/js/forms.js index 8fb28228..2b0cec96 100644 --- a/resources/assets/js/forms.js +++ b/resources/assets/js/forms.js @@ -1,7 +1,15 @@ -require('select2'); +import 'select2'; import { formatDay, formatTime } from './date'; import { ready } from './ready'; +/** + * @param {HTMLElement} element + */ +const triggerChange = (element) => { + const changeEvent = new Event('change'); + element.dispatchEvent(changeEvent); +} + /** * Sets all checkboxes to the wanted state * @@ -9,8 +17,8 @@ import { ready } from './ready'; * @param {boolean} checked True if the checkboxes should be checked */ global.checkAll = (id, checked) => { - $('#' + id + ' input[type="checkbox"]').each(function () { - this.checked = checked; + document.querySelectorAll('#' + id + ' input[type="checkbox"]').forEach((element) => { + element.checked = checked; }); }; @@ -18,11 +26,12 @@ global.checkAll = (id, checked) => { * Sets the checkboxes according to the given type * * @param {string} id The elements ID - * @param {list} shiftsList A list of numbers + * @param {int[]} shiftsList A list of numbers */ global.checkOwnTypes = (id, shiftsList) => { - $('#' + id + ' input[type="checkbox"]').each(function () { - this.checked = $.inArray(parseInt(this.value), shiftsList) != -1; + document.querySelectorAll('#' + id + ' input[type="checkbox"]').forEach((element) => { + const value = parseInt(element.value, 10); + element.checked = shiftsList.includes(value); }); }; @@ -37,21 +46,23 @@ global.checkOwnTypes = (id, shiftsList) => { * @param {Date} to */ global.setInput = (from, to) => { - const fromDay = $('#start_day'); - const fromTime = $('#start_time'); - const toDay = $('#end_day'); - const toTime = $('#end_time'); + const fromDay = document.getElementById('start_day'); + const fromTime = document.getElementById('start_time'); + const toDay = document.getElementById('end_day'); + const toTime = document.getElementById('end_time'); if (!fromDay || !fromTime || !toDay || !toTime) { console.warn('cannot set input date because of missing field'); return; } - fromDay.val(formatDay(from)).trigger('change'); - fromTime.val(formatTime(from)); + fromDay.value = formatDay(from); + triggerChange(fromDay); + fromTime.value = formatTime(from); - toDay.val(formatDay(to)).trigger('change'); - toTime.val(formatTime(to)); + toDay.value = formatDay(to); + triggerChange(toDay); + toTime.value = formatTime(to); }; global.setDay = (days) => { @@ -86,33 +97,42 @@ global.setHours = (hours) => { setInput(from, to); }; -ready(function () { +ready(() => { /** - * Disable every submit button after clicking (to prevent double-clicking) - */ - $('form').submit(function (ev) { - $('input[type="submit"]').prop('readonly', true).addClass('disabled'); - return true; + * Disable every submit button after clicking (to prevent double-clicking) + */ + document.querySelectorAll('form').forEach((formElement) => { + formElement.addEventListener('submit', () => { + document.querySelectorAll('input[type="submit"],button[type="submit"]').forEach((element) => { + element.readOnly = true; + element.classList.add('disabled'); + }); + }); }); - }); /* * Button to set current time in time input fields. */ -ready(function () { - $('.input-group.time').each(function () { - const elem = $(this); - elem.find('button').on('click', function () { +ready(() => { + document.querySelectorAll('.input-group.time').forEach((element) => { + const button = element.querySelector('button'); + if (!button) return; + + button.addEventListener('click', () => { const now = new Date(); - const input = elem.children('input').first(); - input.val(formatTime(now)); - const daySelector = $('#' + input.attr('id').replace('time', 'day')); - const days = daySelector.children('option'); + const input = element.querySelector('input'); + if (!input) return; + + input.value = formatTime(now); + const daySelector = document.getElementById(input.id.replace('time', 'day')); + if (!daySelector) return; + + const dayElements = daySelector.querySelectorAll('option'); const yyyyMMDD = formatDay(now); - days.each(function (i) { - if ($(days[i]).val() === yyyyMMDD) { - daySelector.val($(days[i]).val()); + dayElements.forEach((dayElement) => { + if (dayElement.value === yyyyMMDD) { + daySelector.value = dayElement.value; return false; } }); @@ -120,7 +140,7 @@ ready(function () { }); }); -ready(function () { +ready(() => { $('select').select2({ theme: 'bootstrap-5', width: '100%', @@ -130,15 +150,17 @@ ready(function () { /** * Show oauth buttons on welcome title click */ -ready(function () { - $('#welcome-title').on('click', function () { - $('.btn-group.btn-group .btn.d-none').removeClass('d-none'); - }); - $('#settings-title').on('click', function () { - $('.user-settings .nav-item').removeClass('d-none'); - }); - $('#oauth-settings-title').on('click', function () { - $('table tr.d-none').removeClass('d-none'); +ready(() => { + [ + ['welcome-title', '.btn-group.btn-group .btn.d-none'], + ['settings-title', '.user-settings .nav-item'], + ['oauth-settings-title', 'table tr.d-none'], + ].forEach(([id, selector]) => { + document.getElementById(id)?.addEventListener('click', () => { + document.querySelectorAll(selector).forEach((element) => { + element.classList.remove('d-none'); + }); + }); }); }); @@ -149,7 +171,7 @@ ready(function () { */ ready(() => { const filter = document.getElementById('collapseShiftsFilterSelect'); - if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden') { + if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden.bs.collapse') { return; } @@ -165,7 +187,9 @@ ready(() => { localStorage.setItem('collapseShiftsFilterSelect', e.type); }; - $('#collapseShiftsFilterSelect') - .on('hidden.bs.collapse', onChange) - .on('shown.bs.collapse', onChange); + document.getElementById('collapseShiftsFilterSelect') + ?.addEventListener('hidden.bs.collapse', onChange); + + document.getElementById('collapseShiftsFilterSelect') + ?.addEventListener('shown.bs.collapse', onChange); }); diff --git a/resources/assets/js/sticky-headers.js b/resources/assets/js/sticky-headers.js index fb243f12..9f7abc44 100644 --- a/resources/assets/js/sticky-headers.js +++ b/resources/assets/js/sticky-headers.js @@ -1,34 +1,44 @@ import { ready } from './ready'; +/** + * @param {NodeList} elements + * @param {string} styleProp + * @param {*} value + */ +const applyStyle = (elements, prop, value) => { + elements.forEach((element) => { + element.style[prop] = value; + }); +} + /** * Enables the fixed headers and time lane for the shift-calendar and datatables */ -ready(function () { - if ($('.shift-calendar').length) { - const timeLanes = $('.shift-calendar .time'); - const headers = $('.shift-calendar .header'); - const topReference = $('.container-fluid .row'); - timeLanes.css({ - 'position': 'relative', - 'z-index': 999 - }); - headers.css({ - 'position': 'relative', - 'z-index': 900 - }); - $(window).scroll( - function () { - const top = headers.parent().offset().top; - const left = 15; - timeLanes.css({ - 'left': Math.max(0, $(window).scrollLeft() - left) + 'px' - }); - headers.css({ - 'top': Math.max(0, $(window).scrollTop() - top - - 13 - + topReference.offset().top) - + 'px' - }); - }); - } +ready(() => { + if (!document.querySelector('.shift-calendar')) return; + + const headers = document.querySelectorAll('.shift-calendar .header'); + const timeLane = document.querySelector('.shift-calendar .time'); + const topReference = document.querySelector('.container-fluid .row'); + + if (!headers.length || !timeLane || !topReference) return; + + timeLane.style.position = 'relative'; + timeLane.style.zIndex = 999; + + applyStyle(headers, 'position', 'relative'); + applyStyle(headers, 'zIndex', 900); + + window.addEventListener('scroll', () => { + const top = headers.item(0).parentNode.getBoundingClientRect().top; + const left = 15; + + timeLane.style.left = Math.max(0, window.scrollX - left) + 'px'; + + const headersTop = Math.max( + 0, + window.scrollY - top - 13 + topReference.getBoundingClientRect().top + ) + 'px'; + applyStyle(headers, 'top', headersTop); + }); }); diff --git a/resources/assets/js/vendor.js b/resources/assets/js/vendor.js index 13f93e70..d056fcd5 100644 --- a/resources/assets/js/vendor.js +++ b/resources/assets/js/vendor.js @@ -1,11 +1,7 @@ -require('core-js/stable'); +import 'core-js/stable'; window.$ = window.jQuery = require('jquery'); window.bootstrap = require('bootstrap'); -require('./forms'); -require('./sticky-headers'); -require('./countdown'); - - -$.ajaxSetup({ - headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')} -}); +import './forms'; +import './sticky-headers'; +import './countdown'; +import './dashboard'; diff --git a/resources/views/pages/design.twig b/resources/views/pages/design.twig index 651e329b..6c43c65c 100644 --- a/resources/views/pages/design.twig +++ b/resources/views/pages/design.twig @@ -249,13 +249,27 @@