Skip to content

Commit

Permalink
Replace JQuery Datetimepicker with Tempus Dominus (#526)
Browse files Browse the repository at this point in the history
* Uninstall old Datetimepicker

* Remove old import hack for Datetimepicker

* Try out Tempus Dominus datetimepicker on assignments

* Show datetimepicker when clicking in text field

Note that the datetimepicker is not really accessible right now.
Hopefully, there will be some improvement related to this soon.
See here: Eonasdan/tempus-dominus#2390

* Shrink "action" and enlargen "Deadline" column

* Do not use not working jQuery version of datetimepicker

* Remove autocomplete from input field

* Improve accessibility of Datetimepicker

(keyboard events & focus)

* Add error message for invalid format & decouple from assignment view

* Clean up datetimepicker.js

* Change invalid date format string

* Fix "is-invalid" CSS classes via JS

* Get rid of old datetimepicker occurrences

* Get rid of "moment" as dependency for formatting dates

* Wait for document to be ready to init datetimepicker

Also close the datetimepicker when a date was selected

* Replace old datetimepicker in publish media modal

* Fix datetimepicker in assignments view

* Do not close datetimepicker when only time is changed

* Improve comment

* Check if date is undefined

* Prevent turbolink issue with datetimepicker

* Fix assignment title CSS id

* Empty error message upon update of assignment

* Restore old code for "assignment_title"

* Use 'before-cache' instead of 'load' with Turbolinks
  • Loading branch information
Splines authored Jul 13, 2023
1 parent cd3016c commit 22f91d7
Show file tree
Hide file tree
Showing 17 changed files with 217 additions and 106 deletions.
107 changes: 107 additions & 0 deletions app/assets/javascripts/datetimepicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Initialize on page load (when js file is dynamically loaded)
$(document).ready(startInitialization);

// On page change (e.g. go back and forth in browser)
$(document).on('turbolinks:before-cache', () => {
// Remove stale datetimepickers
$('.tempus-dominus-widget').remove();
});

function startInitialization() {
const pickerElements = $('.td-picker');
if (pickerElements.length == 0) {
console.error('No datetimepicker element found on page, although requested.');
return;
}

pickerElements.each((i, element) => {
element = $(element);
const datetimePicker = initDatetimePicker(element);
registerErrorHandlers(datetimePicker, element);
registerFocusHandlers(datetimePicker, element);
});
}

function initDatetimePicker(element) {
// see https://getdatepicker.com
return new tempusDominus.TempusDominus(
element.get(0),
{
display: {
sideBySide: true, // clock to the right of the calendar
},
localization: {
startOfTheWeek: 1,
// choose format to be compliant with backend time format
format: 'yyyy-MM-dd HH:mm',
hourCycle: 'h23',
}
}
);
}

function registerErrorHandlers(datetimePicker, element) {
// Catch Tempus Dominus error when user types in invalid date
// this is rather hacky at the moment, see this discussion:
// https://github.com/Eonasdan/tempus-dominus/discussions/2656
datetimePicker.dates.oldParseInput = datetimePicker.dates.parseInput;
datetimePicker.dates.parseInput = (input) => {
try {
return datetimePicker.dates.oldParseInput(input);
} catch (err) {
const errorMsg = element.find('.td-error').data('td-invalid-date');
element.find('.td-error').text(errorMsg).show();
datetimePicker.dates.clear();
}
};

datetimePicker.subscribe(tempusDominus.Namespace.events.change, (e) => {
// see https://getdatepicker.com/6/namespace/events.html#change

// Clear error message
if (e.isValid && !e.isClear) {
element.find('.td-error').empty();
}

// If date was selected, close datetimepicker.
// However: leave the datetimepicker open if user only changed time
if (e.oldDate && e.date && !hasUserChangedDate(e.oldDate, e.date)) {
datetimePicker.hide();
}
});
}

function hasUserChangedDate(oldDate, newDate) {
return oldDate.getHours() != newDate.getHours()
|| oldDate.getMinutes() != newDate.getMinutes();
}

function registerFocusHandlers(datetimePicker, element) {
// Show datetimepicker when user clicks in text field next to button
// or when input field receives focus
var isButtonInvokingFocus = false;

element.find('.td-input').on('click focusin', (e) => {
try {
if (!isButtonInvokingFocus) {
datetimePicker.show();
}
}
finally {
isButtonInvokingFocus = false;
}
});

element.find('.td-picker-button').on('click', () => {
isButtonInvokingFocus = true;
element.find('.td-input').focus();
});

// Hide datetimepicker when input field loses focus
element.find('.td-input').blur((e) => {
if (!e.relatedTarget) {
return;
}
datetimePicker.hide();
});
}
15 changes: 1 addition & 14 deletions app/assets/javascripts/media.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ fancyTimeFormat = (time) ->
output

$(document).on 'turbolinks:load', ->

# init datetimepicker
$('#release_date').datetimepicker
format: 'd.m.Y H:i'
inline: false
lang: 'en'

# disable/enable search field on the media search page, depending on
# whether 'all tags'/'all editors'/... are selected
$('[id^="search_all_"]').on 'change', ->
Expand Down Expand Up @@ -395,14 +388,8 @@ $(document).on 'turbolinks:load', ->
return

$('#release_date').on 'focus', ->
# Select other option if user clicks on release date input field
$('#medium_release_now_0').prop('checked', true)
$('#release_date').datetimepicker('toggle')
return

$('#medium_assignment_deadline').on 'focus', ->
$(this).datetimepicker
format: 'd.m.Y H:i'
inline: false
return

$('#medium_create_assignment').on 'click', ->
Expand Down
13 changes: 0 additions & 13 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,10 @@
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)

// JQuery Datetimepicker fix
// import '...' does not work, random names for the import don't work either
// we use "css" and "myLib" imports although these strings do not show up anywhere
// in our codebase. This is just a hack to make the imports-loader work. Maybe it has
// to do with how webpacker resolves the imports or how jquery-datetimepicker exports
// its module.
import css from 'jquery-datetimepicker/build/jquery.datetimepicker.min.css'
import myLib from 'imports-loader?imports=default%20jquery%20$!./../../../node_modules/jquery-datetimepicker/build/jquery.datetimepicker.full.min.js'

import moment from "moment"; // require
window.moment = moment;
import {
WidgetInstance
} from "friendly-challenge";
var friendlyChallengeWidgetInstance = WidgetInstance
$.datetimepicker.setLocale('de');


document.addEventListener("turbolinks:load", function () {
var doneCallback, element, options, widget;
Expand Down
1 change: 0 additions & 1 deletion app/javascript/packs/application.sass
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
@import "jquery-datetimepicker/build/jquery.datetimepicker.min.css"
37 changes: 29 additions & 8 deletions app/views/assignments/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<%= javascript_include_tag 'datetimepicker' %>

<div class="list-group-item assignmentRow"
data-id="<%= assignment.id.to_i %>">
<%= form_with model: assignment do |f| %>
Expand All @@ -10,15 +12,33 @@
id="assignment-title-error">
</div>
</div>
<div class="form-group col-2">
<%= f.text_field :deadline,
class: 'form-control',
autocomplete: 'off',
id: "assignment_deadline_#{assignment.id}" %>
<div class="invalid-feedback"
id="assignment-deadline-error">

<%# Datetimepicker for assignment deadline %>
<div class="col-3 form-group">
<div
class="input-group log-event td-picker"
id="assignment-picker"
data-td-target-input="nearest"
data-td-target-toggle="nearest">
<%= f.text_field :deadline,
class: 'form-control td-input',
autocomplete: 'off',
'data-td-target': '#assignment-picker' %>
<span
class="input-group-text td-picker-button"
role="button"
data-td-target="#assignment-picker"
data-td-toggle="datetimepicker">
<i class="bi bi-calendar-fill"></i>
</span>
<div class="invalid-feedback td-error"
id="assignment-deadline-error"
data-td-invalid-date="<%= t('datetimepicker.invalid_date') %>">
</div>
</div>
</div>


<div class="form-group col-2">
<%= f.select :medium_id,
options_for_select(Medium.where(teachable: assignment.lecture,
Expand Down Expand Up @@ -48,12 +68,13 @@
id="assignment-deletion-date-error">
</div>
</div>
<div class="form-group col-2">
<div class="form-group col-1">
<%= f.submit t('buttons.save'),
class: 'btn btn-sm btn-primary ms-2' %>
<%= link_to t('buttons.cancel'),
cancel_editing_assignment_path(assignment),
class: 'btn btn-sm btn-secondary',
role: 'button',
remote: true %>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/views/assignments/_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div class="col-2">
<%= assignment.title %>
</div>
<div class="col-2">
<div class="col-3">
<% if assignment.deadline %>
<%= I18n.l(assignment.deadline, format: :long) %>
<% end %>
Expand All @@ -26,7 +26,7 @@
<%= I18n.l(assignment.deletion_date, format: :long) %>
<% end %>
</div>
<div class="col-2">
<div class="col-1">
<% if assignment.persisted? %>
<%= link_to edit_assignment_path(assignment),
class: 'text-dark',
Expand Down
8 changes: 4 additions & 4 deletions app/views/assignments/create.coffee
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# clean up from previous error messages
$('#assignment_title').removeClass('is-invalid')
$('#assignment_title_').removeClass('is-invalid')
$('#assignment-title-error').empty()
$('#assignment_deadline').removeClass('is-invalid')
$('#assignment-deadline-error').empty()
$('#assignment_deletion_date').removeClass('is-invalid')
$('#assignment_deletion_date_').removeClass('is-invalid')
$('#assignment-deletion-date-error').empty()

# display error message
<% if @errors.present? %>
<% if @errors[:title].present? %>
$('#assignment-title-error')
.append('<%= @errors[:title].join(" ") %>').show()
$('#assignment_title').addClass('is-invalid')
$('#assignment_title_').addClass('is-invalid')
<% end %>
<% if @errors[:deadline].present? %>
$('#assignment-deadline-error')
Expand All @@ -21,7 +21,7 @@ $('#assignment_deadline').addClass('is-invalid')
<% if @errors[:deletion_date].present? %>
$('#assignment-deletion-date-error')
.append('<%= @errors[:deletion_date].join(" ") %>').show()
$('#assignment_deletion_date').addClass('is-invalid')
$('#assignment_deletion_date_').addClass('is-invalid')
<% end %>
<% else %>
$('.assignmentRow[data-id="0"')
Expand Down
7 changes: 0 additions & 7 deletions app/views/assignments/edit.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,3 @@ new TomSelect('#assignment_medium_id_<%= @assignment.id %>',
# make sure that no medium is preselected
$('#assignment_medium_id_<%= @assignment.id %>').val(null).trigger('change')
<% end %>

# using moment to format the date right
d = moment($("#assignment_deadline_<%= @assignment.id %>").val(),"YYYY-MM-DD hh:mm:ss z")
$("#assignment_deadline_<%= @assignment.id %>").val (d.format("DD.MM.Y H:mm"))
$("#assignment_deadline_<%= @assignment.id %>").datetimepicker
format:'d.m.Y H:i'
inline:false
5 changes: 0 additions & 5 deletions app/views/assignments/new.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,3 @@ new TomSelect('#assignment_medium_id_', {
});

$('#assignment_medium_id_').val(null).trigger('change');

$("#assignment_deadline_").datetimepicker({
format: 'd.m.Y H:i',
inline: false
});
2 changes: 2 additions & 0 deletions app/views/assignments/update.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# clean up from previous error messages
$('#assignment_title').removeClass('is-invalid')
$('#assignment-title-error').empty()
$('#assignment_deadline').removeClass('is-invalid')
$('#assignment-deadline-error').empty()

# display error message
<% if @errors[:title].present? %>
Expand Down
14 changes: 14 additions & 0 deletions app/views/layouts/_head.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@
integrity="sha256-rADF2WO6GbkNb5O9gqHdjQ/XhrfnXDtPdHdRe5KQmek="
crossorigin="anonymous">
</script>

<!-- Tempus Dominus Datetimepicker -->
<!-- Font awesome required for icons in Datetimepicker -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/js/solid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/js/fontawesome.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/@eonasdan/[email protected]/dist/js/tempus-dominus.js">
</script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@eonasdan/[email protected]/dist/css/tempus-dominus.css">

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/sanitize-html.min.js" integrity="sha256-+Tha6wUu+wzAOUzilJ7AmF4OXc3CsY1aMsWopnCVwb4=" crossorigin="anonymous"></script>
<script defer
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js"
Expand All @@ -59,6 +71,8 @@
</script>
<![endif]-->



<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">

Expand Down
4 changes: 2 additions & 2 deletions app/views/lectures/edit/_assignments.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<%= t('basics.title') %>
</h6>
</div>
<div class="col-2">
<div class="col-3">
<h6>
<%= t('basics.deadline') %>
</h6>
Expand All @@ -61,7 +61,7 @@
<%= t('assignment.deletion_date') %>
</h6>
</div>
<div class="col-2">
<div class="col-1">
<h6>
<%= t('basics.action') %>
<%= helpdesk(t('assignment.destruction_info'), true) %>
Expand Down
Loading

0 comments on commit 22f91d7

Please sign in to comment.