Replace jQuery with VanillaJS

This commit is contained in:
Michael Weimann 2022-11-29 21:47:26 +01:00
parent 7bbdb95885
commit 282f4b45ac
No known key found for this signature in database
GPG Key ID: 34F0524D4DA694A1
11 changed files with 223 additions and 129 deletions

View File

@ -3,6 +3,7 @@
"extends": [ "plugin:editorconfig/all" ], "extends": [ "plugin:editorconfig/all" ],
"plugins": [ "editorconfig" ], "plugins": [ "editorconfig" ],
"rules": { "rules": {
"prefer-arrow-callback": "error",
"no-var": "error", "no-var": "error",
"quotes": [ "quotes": [
"error", "error",

View File

@ -38,13 +38,13 @@ function form_spinner($name, $label, $value)
</button> </button>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
$(\'#spinner-' . $name . '-down\').click(function () { document.getElementById("spinner-' . $name . '-down")?.addEventListener("click", () => {
const spinner = $(\'#spinner-' . $name . '\'); const spinner = document.getElementById("spinner-' . $name . '");
spinner.val(parseInt(spinner.val()) - 1); spinner.value = parseInt(spinner.value, 10) - 1;
}); });
$(\'#spinner-' . $name . '-up\').click(function () { document.getElementById("spinner-' . $name . '-up")?.addEventListener("click", () => {
const spinner = $(\'#spinner-' . $name . '\'); const spinner = document.getElementById("spinner-' . $name . '");
spinner.val(parseInt(spinner.val()) + 1); spinner.value = parseInt(spinner.value, 10) + 1;
}); });
</script> </script>
', 'spinner-' . $name); ', 'spinner-' . $name);

View File

@ -389,15 +389,18 @@ function render_table($columns, $rows, $data = true)
* @param string $href * @param string $href
* @param string $label * @param string $label
* @param string $class * @param string $class
* @param string $id
* @return string * @return string
*/ */
function button($href, $label, $class = '') function button($href, $label, $class = '', $id = '')
{ {
if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) { if (!Str::contains(str_replace(['btn-sm', 'btn-xl'], '', $class), 'btn-')) {
$class = 'btn-secondary' . ($class ? ' ' . $class : ''); $class = 'btn-secondary' . ($class ? ' ' . $class : '');
} }
return '<a href="' . $href . '" class="btn ' . $class . '">' . $label . '</a>'; $idAttribute = $id ? 'id="' . $id . '"' : '';
return '<a ' . $idAttribute . ' href="' . $href . '" class="btn ' . $class . '">' . $label . '</a>';
} }
/** /**

View File

@ -41,24 +41,16 @@ function public_dashboard_view($stats, $free_shifts)
stats(__('Angels needed for nightshifts'), $stats['needed-night']), stats(__('Angels needed for nightshifts'), $stats['needed-night']),
stats(__('Angels currently working'), $stats['angels-working'], 'default'), stats(__('Angels currently working'), $stats['angels-working'], 'default'),
stats(__('Hours to be worked'), $stats['hours-to-work'], 'default'), stats(__('Hours to be worked'), $stats['hours-to-work'], 'default'),
'<script>
$(function () {
setInterval(function () {
$(\'#content .wrapper\').load(window.location.href + \' #public-dashboard\');
}, 60000);
})
</script>'
], 'statistics'), ], 'statistics'),
$needed_angels $needed_angels
], 'public-dashboard'), ], 'public-dashboard'),
]), ]),
div('first col-md-12 text-center', [buttons([ div('first col-md-12 text-center', [buttons([
button_js( button(
' '#',
$(\'#navbar-collapse-1,.navbar-nav,.navbar-toggler,#footer,#fullscreen-button\').remove(); icon('fullscreen') . __('Fullscreen'),
$(\'.navbar-brand\').append(\' ' . __('Public Dashboard') . '\'); '',
', 'dashboard-fullscreen'
icon('fullscreen') . __('Fullscreen')
), ),
auth()->user() ? button( auth()->user() ? button(
public_dashboard_link($isFiltered ? [] : ['filtered' => 1] + $filter), public_dashboard_link($isFiltered ? [] : ['filtered' => 1] + $filter),

View File

@ -53,20 +53,20 @@ function UserDriverLicense_edit_view($user_source, $user_driver_license)
]), ]),
' '
<script type="text/javascript"> <script type="text/javascript">
$(function () { const drivingLicenseElement = document.getElementById("driving_license");
const checkbox = $(\'#wants_to_drive\');
if (checkbox.is(\':checked\'))
$(\'#driving_license\').show();
else
$(\'#driving_license\').hide();
checkbox.click(function () { if (drivingLicenseElement) {
if ($(\'#wants_to_drive\').is(\':checked\')) const checkbox = document.getElementById("wants_to_drive");
$(\'#driving_license\').show(); drivingLicenseElement.style.display = checkbox?.checked
else ? ""
$(\'#driving_license\').hide(); : "none";
});
checkbox.addEventListener("click", () => {
drivingLicenseElement.style.display = document.getElementById("wants_to_drive")?.checked
? ""
: "none";
}); });
}
</script> </script>
' '
], true); ], true);

View File

@ -69,14 +69,13 @@ function formatFromNow(timestamp) {
/** /**
* Initialises all countdown fields on the page. * Initialises all countdown fields on the page.
*/ */
ready(function () { ready(() => {
$.each($('[data-countdown-ts]'), function (i, e) { document.querySelectorAll('[data-countdown-ts]').forEach((element) => {
const span = $(e); const timestamp = element.dataset.countdownTs;
const timestamp = span.data('countdown-ts'); const template = element.innerHTML;
const text = span.html(); element.innerHTML = template.replace('%c', formatFromNow(timestamp));
span.html(text.replace('%c', formatFromNow(timestamp))); setInterval(() => {
setInterval(function () { element.innerHTML = template.replace('%c', formatFromNow(timestamp));
span.html(text.replace('%c', formatFromNow(timestamp)));
}, 1000); }, 1000);
}); });
}); });

View File

@ -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'));
});
});

View File

@ -1,7 +1,15 @@
require('select2'); import 'select2';
import { formatDay, formatTime } from './date'; import { formatDay, formatTime } from './date';
import { ready } from './ready'; 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 * 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 * @param {boolean} checked True if the checkboxes should be checked
*/ */
global.checkAll = (id, checked) => { global.checkAll = (id, checked) => {
$('#' + id + ' input[type="checkbox"]').each(function () { document.querySelectorAll('#' + id + ' input[type="checkbox"]').forEach((element) => {
this.checked = checked; element.checked = checked;
}); });
}; };
@ -18,11 +26,12 @@ global.checkAll = (id, checked) => {
* Sets the checkboxes according to the given type * Sets the checkboxes according to the given type
* *
* @param {string} id The elements ID * @param {string} id The elements ID
* @param {list} shiftsList A list of numbers * @param {int[]} shiftsList A list of numbers
*/ */
global.checkOwnTypes = (id, shiftsList) => { global.checkOwnTypes = (id, shiftsList) => {
$('#' + id + ' input[type="checkbox"]').each(function () { document.querySelectorAll('#' + id + ' input[type="checkbox"]').forEach((element) => {
this.checked = $.inArray(parseInt(this.value), shiftsList) != -1; const value = parseInt(element.value, 10);
element.checked = shiftsList.includes(value);
}); });
}; };
@ -37,21 +46,23 @@ global.checkOwnTypes = (id, shiftsList) => {
* @param {Date} to * @param {Date} to
*/ */
global.setInput = (from, to) => { global.setInput = (from, to) => {
const fromDay = $('#start_day'); const fromDay = document.getElementById('start_day');
const fromTime = $('#start_time'); const fromTime = document.getElementById('start_time');
const toDay = $('#end_day'); const toDay = document.getElementById('end_day');
const toTime = $('#end_time'); const toTime = document.getElementById('end_time');
if (!fromDay || !fromTime || !toDay || !toTime) { if (!fromDay || !fromTime || !toDay || !toTime) {
console.warn('cannot set input date because of missing field'); console.warn('cannot set input date because of missing field');
return; return;
} }
fromDay.val(formatDay(from)).trigger('change'); fromDay.value = formatDay(from);
fromTime.val(formatTime(from)); triggerChange(fromDay);
fromTime.value = formatTime(from);
toDay.val(formatDay(to)).trigger('change'); toDay.value = formatDay(to);
toTime.val(formatTime(to)); triggerChange(toDay);
toTime.value = formatTime(to);
}; };
global.setDay = (days) => { global.setDay = (days) => {
@ -86,33 +97,42 @@ global.setHours = (hours) => {
setInput(from, to); setInput(from, to);
}; };
ready(function () { ready(() => {
/** /**
* Disable every submit button after clicking (to prevent double-clicking) * Disable every submit button after clicking (to prevent double-clicking)
*/ */
$('form').submit(function (ev) { document.querySelectorAll('form').forEach((formElement) => {
$('input[type="submit"]').prop('readonly', true).addClass('disabled'); formElement.addEventListener('submit', () => {
return true; 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. * Button to set current time in time input fields.
*/ */
ready(function () { ready(() => {
$('.input-group.time').each(function () { document.querySelectorAll('.input-group.time').forEach((element) => {
const elem = $(this); const button = element.querySelector('button');
elem.find('button').on('click', function () { if (!button) return;
button.addEventListener('click', () => {
const now = new Date(); const now = new Date();
const input = elem.children('input').first(); const input = element.querySelector('input');
input.val(formatTime(now)); if (!input) return;
const daySelector = $('#' + input.attr('id').replace('time', 'day'));
const days = daySelector.children('option'); 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); const yyyyMMDD = formatDay(now);
days.each(function (i) { dayElements.forEach((dayElement) => {
if ($(days[i]).val() === yyyyMMDD) { if (dayElement.value === yyyyMMDD) {
daySelector.val($(days[i]).val()); daySelector.value = dayElement.value;
return false; return false;
} }
}); });
@ -120,7 +140,7 @@ ready(function () {
}); });
}); });
ready(function () { ready(() => {
$('select').select2({ $('select').select2({
theme: 'bootstrap-5', theme: 'bootstrap-5',
width: '100%', width: '100%',
@ -130,15 +150,17 @@ ready(function () {
/** /**
* Show oauth buttons on welcome title click * Show oauth buttons on welcome title click
*/ */
ready(function () { ready(() => {
$('#welcome-title').on('click', function () { [
$('.btn-group.btn-group .btn.d-none').removeClass('d-none'); ['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');
}); });
$('#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');
}); });
}); });
@ -149,7 +171,7 @@ ready(function () {
*/ */
ready(() => { ready(() => {
const filter = document.getElementById('collapseShiftsFilterSelect'); const filter = document.getElementById('collapseShiftsFilterSelect');
if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden') { if (!filter || localStorage.getItem('collapseShiftsFilterSelect') !== 'hidden.bs.collapse') {
return; return;
} }
@ -165,7 +187,9 @@ ready(() => {
localStorage.setItem('collapseShiftsFilterSelect', e.type); localStorage.setItem('collapseShiftsFilterSelect', e.type);
}; };
$('#collapseShiftsFilterSelect') document.getElementById('collapseShiftsFilterSelect')
.on('hidden.bs.collapse', onChange) ?.addEventListener('hidden.bs.collapse', onChange);
.on('shown.bs.collapse', onChange);
document.getElementById('collapseShiftsFilterSelect')
?.addEventListener('shown.bs.collapse', onChange);
}); });

View File

@ -1,34 +1,44 @@
import { ready } from './ready'; 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 * Enables the fixed headers and time lane for the shift-calendar and datatables
*/ */
ready(function () { ready(() => {
if ($('.shift-calendar').length) { if (!document.querySelector('.shift-calendar')) return;
const timeLanes = $('.shift-calendar .time');
const headers = $('.shift-calendar .header'); const headers = document.querySelectorAll('.shift-calendar .header');
const topReference = $('.container-fluid .row'); const timeLane = document.querySelector('.shift-calendar .time');
timeLanes.css({ const topReference = document.querySelector('.container-fluid .row');
'position': 'relative',
'z-index': 999 if (!headers.length || !timeLane || !topReference) return;
});
headers.css({ timeLane.style.position = 'relative';
'position': 'relative', timeLane.style.zIndex = 999;
'z-index': 900
}); applyStyle(headers, 'position', 'relative');
$(window).scroll( applyStyle(headers, 'zIndex', 900);
function () {
const top = headers.parent().offset().top; window.addEventListener('scroll', () => {
const top = headers.item(0).parentNode.getBoundingClientRect().top;
const left = 15; const left = 15;
timeLanes.css({
'left': Math.max(0, $(window).scrollLeft() - left) + 'px' 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);
}); });
headers.css({
'top': Math.max(0, $(window).scrollTop() - top
- 13
+ topReference.offset().top)
+ 'px'
});
});
}
}); });

View File

@ -1,11 +1,7 @@
require('core-js/stable'); import 'core-js/stable';
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
window.bootstrap = require('bootstrap'); window.bootstrap = require('bootstrap');
require('./forms'); import './forms';
require('./sticky-headers'); import './sticky-headers';
require('./countdown'); import './countdown';
import './dashboard';
$.ajaxSetup({
headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')}
});

View File

@ -249,13 +249,27 @@
</div> </div>
</div> </div>
<div class="col-md-3 col-lg-2"> <div class="col-md-3 col-lg-2">
Submit button<br> Form submit
<form id="form">
{{ f.submit() }} {{ f.submit() }}
</form>
</div> </div>
<div class="col-md-3 col-lg-2"> <div id="checkboxes" class="col-md-3 col-lg-2">
Checkbox<br> Checkbox<br>
{{ f.checkbox('form-input-checkbox', 'Checkbox', true) }} {{ f.checkbox('form-input-checkbox', 'Checkbox 1', true, '1') }}
{{ f.checkbox('form-input-checkbox-2', 'Checkbox 2') }} {{ f.checkbox('form-input-checkbox-2', 'Checkbox 2', false, '2') }}
{{ f.checkbox('form-input-checkbox-3', 'Checkbox 3', false, '3') }}
<div class="d-grid gap-2">
<div id="select-all-checkboxes" class="btn btn-sm btn-block btn-primary">
Select all
</div>
<div id="select-23-checkboxes" class="btn btn-sm btn-block btn-primary">
Select 2, 3
</div>
<div id="unselect-all-checkboxes" class="btn btn-sm btn-block btn-danger">
Unselect all
</div>
</div>
</div> </div>
<div class="col-md-3 col-lg-2 checkbox-inline"> <div class="col-md-3 col-lg-2 checkbox-inline">
Radio<br> Radio<br>
@ -406,4 +420,22 @@
{{ bar_chart | raw }} {{ bar_chart | raw }}
</div> </div>
</div> </div>
<script>
document.getElementById('select-all-checkboxes').addEventListener('click', () => {
checkAll('checkboxes', true);
});
document.getElementById('unselect-all-checkboxes').addEventListener('click', () => {
checkAll('checkboxes', false);
});
document.getElementById('select-23-checkboxes').addEventListener('click', () => {
checkOwnTypes('checkboxes', [2, 3]);
});
document.getElementById('form').addEventListener('submit', (e) => {
e.preventDefault();
return false;
});
</script>
{% endblock %} {% endblock %}