Bugzilla – Attachment 193933 Details for
Bug 33886
Flatpickr calendar - year controls for date of birth on memberentry.pl are not intuitive
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 33886: Add DateInputStyle system preference for split date inputs
05ac2bb.patch (text/plain), 36.95 KB, created by
Martin Renvoize (ashimema)
on 2026-02-25 18:13:08 UTC
(
hide
)
Description:
Bug 33886: Add DateInputStyle system preference for split date inputs
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2026-02-25 18:13:08 UTC
Size:
36.95 KB
patch
obsolete
>From 05ac2bb5654511d43069f6bab48fed3bfacecd51 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Date: Wed, 25 Feb 2026 17:09:27 +0000 >Subject: [PATCH] Bug 33886: Add DateInputStyle system preference for split > date inputs >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >Adds a new 'DateInputStyle' system preference (single|split) in the >I18N/L10N section that controls how date input fields are presented >across the entire interface. > >In 'split' mode, date fields are replaced by three auto-advancing >text inputs (Day, Month, Year â ordered by the dateformat preference) >plus an optional calendar button that opens the flatpickr picker on >demand. This gives users the best of all worlds: accessible separate >fields for those who prefer them, plus the familiar calendar picker >for those who want it. > >The implementation lives entirely in calendar.inc (both staff and >OPAC), so no per-template HTML changes are needed. The apply_flatpickr() >function routes eligible fields to apply_split_date_widget() when >split mode is active; time, datetime, and booking period fields are >excluded. A validateSplitDateInputs() utility function handles form >validation for any split widget on the page. >--- > .../data/mysql/atomicupdate/bug_33886.pl | 26 +++ > .../prog/css/src/_flatpickr.scss | 4 +- > .../prog/css/src/staff-global.scss | 65 ++++++ > .../prog/en/includes/calendar.inc | 185 ++++++++++++++++- > .../modules/admin/preferences/i18n_l10n.pref | 8 + > .../prog/en/modules/members/memberentrygen.tt | 2 +- > koha-tmpl/intranet-tmpl/prog/js/members.js | 14 +- > .../bootstrap/css/src/_flatpickr.scss | 4 +- > .../opac-tmpl/bootstrap/css/src/opac.scss | 65 ++++++ > .../bootstrap/en/includes/calendar.inc | 191 +++++++++++++++++- > .../bootstrap/en/modules/opac-memberentry.tt | 5 +- > 11 files changed, 560 insertions(+), 9 deletions(-) > create mode 100644 installer/data/mysql/atomicupdate/bug_33886.pl > >diff --git a/installer/data/mysql/atomicupdate/bug_33886.pl b/installer/data/mysql/atomicupdate/bug_33886.pl >new file mode 100644 >index 00000000000..4a0474580c2 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_33886.pl >@@ -0,0 +1,26 @@ >+use Modern::Perl; >+use Koha::Installer::Output qw(say_warning say_success say_info); >+ >+return { >+ bug_number => "33886", >+ description => "Add DateInputStyle system preference", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ $dbh->do( >+ q{ >+ INSERT IGNORE INTO systempreferences ( variable, value, options, explanation, type ) >+ VALUES ( >+ 'DateInputStyle', >+ 'single', >+ 'single|split', >+ 'Controls how date input fields are displayed. "single" uses the standard flatpickr calendar widget with maskito input masking. "split" shows three separate inputs for day, month, and year (order follows the dateformat preference) with an optional calendar button; does not apply to time, datetime, or booking date range fields.', >+ 'Choice' >+ ) >+ } >+ ); >+ >+ say_success( $out, "Added new system preference 'DateInputStyle'" ); >+ }, >+}; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/_flatpickr.scss b/koha-tmpl/intranet-tmpl/prog/css/src/_flatpickr.scss >index 58162e62cba..ff0bbacba80 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/_flatpickr.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/_flatpickr.scss >@@ -1,5 +1,7 @@ > @import "flatpickr_vars"; > >+$flatpickr-calendar-icon: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23369' class='bi bi-calendar3' viewBox='0 0 32 32'%3E%3Cg stroke-width='1.333'%3E%3Cpath d='M25 4H7a3 3 0 0 0-3 3v18a3 3 0 0 0 3 3h18a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3ZM5.5 9.786c0-.71.672-1.286 1.5-1.286h18c.828 0 1.5.576 1.5 1.286v15.428c0 .71-.672 1.286-1.5 1.286H7c-.828 0-1.5-.576-1.5-1.286z'/%3E%3Cpath d='M13.75 14.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.25 19a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm-13.5 4.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z'/%3E%3C/g%3E%3C/svg%3E" ); >+ > @-webkit-keyframes fpFadeInDown { > from { > opacity: 0; >@@ -957,7 +959,7 @@ span.flatpickr-weekday { > } > > .flatpickr-input { >- background-image: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23369' class='bi bi-calendar3' viewBox='0 0 32 32'%3E%3Cg stroke-width='1.333'%3E%3Cpath d='M25 4H7a3 3 0 0 0-3 3v18a3 3 0 0 0 3 3h18a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3ZM5.5 9.786c0-.71.672-1.286 1.5-1.286h18c.828 0 1.5.576 1.5 1.286v15.428c0 .71-.672 1.286-1.5 1.286H7c-.828 0-1.5-.576-1.5-1.286z'/%3E%3Cpath d='M13.75 14.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM9.25 19a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm-13.5 4.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm4.5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z'/%3E%3C/g%3E%3C/svg%3E" ); >+ background-image: $flatpickr-calendar-icon; > background-position: 0; > background-repeat: no-repeat; > border-style: inset; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index 23c42d00425..6f1522a1253 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -1261,6 +1261,71 @@ ol { > font-weight: bold; > } > >+// Split date input widget (generated by calendar.inc in DateInputStyle=split mode) >+.date-split-wrapper { >+ align-items: flex-end; >+ display: inline-flex; >+ flex-wrap: wrap; >+ gap: .4rem; >+ >+ .split-part-group { >+ display: flex; >+ flex-direction: column; >+ >+ label { >+ float: none; >+ font-size: .9em; >+ font-weight: normal; >+ margin-bottom: .2rem; >+ margin-right: 0; >+ text-align: left; >+ width: auto; >+ } >+ } >+ >+ .split-day, >+ .split-month { >+ text-align: center; >+ width: 3.8rem; >+ } >+ >+ .split-year { >+ text-align: center; >+ width: 5.8rem; >+ } >+ >+ .split-sep { >+ align-self: flex-end; >+ font-size: 1.2em; >+ font-weight: bold; >+ padding-bottom: .35rem; >+ } >+ >+ .split-calendar-btn, >+ .split-clear-btn { >+ align-self: flex-end; >+ font-size: 130%; >+ margin-left: .25rem; >+ vertical-align: bottom; >+ } >+ >+ .split-calendar-btn { >+ background-image: $flatpickr-calendar-icon; >+ background-position: center; >+ background-repeat: no-repeat; >+ background-size: contain; >+ display: inline-block; >+ height: 1em; >+ opacity: .75; >+ width: 1em; >+ >+ &:hover { >+ opacity: 1; >+ text-decoration: none; >+ } >+ } >+} >+ > fieldset { > + fieldset { > &.action { >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc >index 607d72d929b..d785927a4fc 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/calendar.inc >@@ -31,6 +31,7 @@ > delimiter = "-"; > } > var sentmsg = 0; >+ var dateInputStyle = "[% Koha.Preference('DateInputStyle') | html %]"; > var bidi = [% IF(bidi) %] true[% ELSE %] false[% END %]; > var calendarFirstDayOfWeek = '[% Koha.Preference('CalendarFirstDayOfWeek') | html %]'; > var flatpickr_timeformat_string = [% IF Koha.Preference('TimeFormat') == '12hr' %]"G:i K"[% ELSE %]"H:i"[% END %]; >@@ -212,6 +213,9 @@ > let options = {}; > let refresh_max_date = 0; > let disable_buttons = []; >+ let is_time = $(input).data('flatpickr-enable-time') === true; >+ let is_time_only = $(input).data('flatpickr-time-only') === true; >+ let is_period = $(input).data('flatpickr-period'); > > if( $(input).data("flatpickr-futureinclusive") === true > || $(input).data("flatpickr-futuredate") === true ) { >@@ -272,7 +276,12 @@ > options['plugins'] = []; > } > >- let fp = $(input).flatpickr(options); >+ let fp; >+ if ( dateInputStyle === 'split' && !is_time && !is_time_only && !is_period ) { >+ fp = apply_split_date_widget(input, options); >+ } else { >+ fp = $(input).flatpickr(options); >+ } > > $(disable_buttons).each(function(index, value){ > $(fp.calendarContainer).find(".shortcut-buttons-flatpickr-button[data-index='"+value+"']").prop("disabled", "disabled"); >@@ -292,6 +301,180 @@ > } > } > >+ function apply_split_date_widget(input, fpOptions) { >+ let parts_order, sep; >+ switch (dateformat_pref) { >+ case 'us': parts_order = ['month', 'day', 'year']; sep = '/'; break; >+ case 'iso': >+ case 'sql': parts_order = ['year', 'month', 'day']; sep = '-'; break; >+ case 'dmydot': parts_order = ['day', 'month', 'year']; sep = '.'; break; >+ default: parts_order = ['day', 'month', 'year']; sep = '/'; /* metric */ >+ } >+ let labels = { day: __("Day"), month: __("Month"), year: __("Year") }; >+ let placeholders = { day: 'DD', month: 'MM', year: 'YYYY' }; >+ let autocompletes = { day: 'bday-day', month: 'bday-month', year: 'bday-year' }; >+ let inputId = input.id || ('fp_split_' + Math.random().toString(36).substr(2, 8)); >+ >+ let options = Object.assign({}, fpOptions, { >+ clickOpens: false, >+ onReady: function(selectedDates, dateStr, instance) { >+ /* Hide the altInput; our split inputs replace it visually */ >+ $(instance.altInput) >+ .addClass('visually-hidden') >+ .attr('tabindex', '-1') >+ .attr('aria-hidden', 'true'); >+ >+ let $wrapper = $('<div class="date-split-wrapper"></div>') >+ .attr('data-split-for', inputId); >+ >+ for (let i = 0; i < parts_order.length; i++) { >+ let part = parts_order[i]; >+ let partId = inputId + '_' + part; >+ let $group = $('<span class="split-part-group"></span>'); >+ $group.append( >+ $('<label></label>').attr('for', partId).text(labels[part]) >+ ); >+ $group.append( >+ $('<input type="text" inputmode="numeric" pattern="[0-9]*">') >+ .attr('id', partId) >+ .attr('maxlength', part === 'year' ? 4 : 2) >+ .attr('placeholder', placeholders[part]) >+ .attr('autocomplete', autocompletes[part]) >+ .addClass('split-part split-' + part) >+ ); >+ $wrapper.append($group); >+ if (i < parts_order.length - 1) { >+ $wrapper.append( >+ $('<span class="split-sep" aria-hidden="true"></span>').text(sep) >+ ); >+ } >+ } >+ >+ let $calBtn = $('<a href="#" class="split-calendar-btn"></a>') >+ .attr('title', __("Open calendar")) >+ .attr('aria-label', __("Open calendar")) >+ .attr('aria-hidden', 'true'); >+ let $clearBtn = $('<a href="#" class="clear_date split-clear-btn fa fa-fw fa-times"></a>') >+ .attr('aria-label', __("Clear date")) >+ .attr('aria-hidden', 'true'); >+ $wrapper.append($calBtn).append($clearBtn); >+ >+ $(instance.altInput).after($wrapper); >+ >+ /* Populate split inputs from the current ISO value (edit mode) */ >+ let val = $(input).val(); >+ if (val && /^\d{4}-\d{2}-\d{2}$/.test(val)) { >+ let p = val.split('-'); >+ $wrapper.find('.split-year').val(p[0]); >+ $wrapper.find('.split-month').val(p[1]); >+ $wrapper.find('.split-day').val(p[2]); >+ } >+ >+ let syncing = false; >+ >+ /* Split inputs -> flatpickr ISO */ >+ $wrapper.on('input', '.split-part', function() { >+ let d = $wrapper.find('.split-day').val().trim(); >+ let m = $wrapper.find('.split-month').val().trim(); >+ let y = $wrapper.find('.split-year').val().trim(); >+ syncing = true; >+ if (d && m && y && y.length === 4) { >+ instance.setDate(y + '-' + m.padStart(2, '0') + '-' + d.padStart(2, '0'), false); >+ } else { >+ instance.clear(false); >+ $(input).val(''); >+ } >+ $(input).trigger('change'); >+ syncing = false; >+ }); >+ >+ /* Auto-advance from day/month after 2 digits (skip on delete/backspace) */ >+ $wrapper.on('input', '.split-day, .split-month', function(e) { >+ let inputType = e.originalEvent && e.originalEvent.inputType; >+ if ($(this).val().length >= 2 && inputType && !inputType.startsWith('delete')) { >+ let $parts = $wrapper.find('.split-part'); >+ let idx = $parts.index(this); >+ if (idx < $parts.length - 1) { >+ $parts.eq(idx + 1).focus().select(); >+ } >+ } >+ }); >+ >+ /* Calendar date selected -> populate split inputs */ >+ $(input).on('change', function() { >+ if (syncing) return; >+ let v = $(input).val(); >+ if (v && /^\d{4}-\d{2}-\d{2}$/.test(v)) { >+ let p = v.split('-'); >+ $wrapper.find('.split-year').val(p[0]); >+ $wrapper.find('.split-month').val(p[1]); >+ $wrapper.find('.split-day').val(p[2]); >+ } else { >+ $wrapper.find('.split-year, .split-month, .split-day').val(''); >+ } >+ }); >+ >+ /* Calendar button opens the flatpickr calendar */ >+ $calBtn.on('click', function(e) { >+ e.preventDefault(); >+ instance.open(); >+ }); >+ >+ /* Clear button clears all split inputs and the flatpickr value */ >+ $clearBtn.on('click', function(e) { >+ e.preventDefault(); >+ syncing = true; >+ instance.clear(); >+ $(input).val(''); >+ $wrapper.find('.split-year, .split-month, .split-day').val(''); >+ syncing = false; >+ }); >+ } >+ }); >+ >+ return $(input).flatpickr(options); >+ } >+ >+ function validateSplitDateInputs() { >+ let valid = true; >+ $('.date-split-wrapper').each(function() { >+ let $wrapper = $(this); >+ let origId = $wrapper.data('split-for'); >+ let $orig = origId ? $('#' + origId) : null; >+ let isRequired = $orig && $orig.is('[required]'); >+ let d = $wrapper.find('.split-day').val().trim(); >+ let m = $wrapper.find('.split-month').val().trim(); >+ let y = $wrapper.find('.split-year').val().trim(); >+ let anyFilled = d || m || y; >+ let allFilled = d && m && y && y.length === 4; >+ >+ if (isRequired && !allFilled) { >+ alert(__("Please enter a complete date.")); >+ $wrapper.find('.split-part').first().focus(); >+ valid = false; >+ return false; >+ } >+ if (anyFilled && !allFilled) { >+ alert(__("Please complete all parts of the date field.")); >+ $wrapper.find('.split-part').first().focus(); >+ valid = false; >+ return false; >+ } >+ if (allFilled) { >+ let dt = new Date(parseInt(y, 10), parseInt(m, 10) - 1, parseInt(d, 10)); >+ if ( dt.getFullYear() !== parseInt(y, 10) >+ || dt.getMonth() !== parseInt(m, 10) - 1 >+ || dt.getDate() !== parseInt(d, 10) ) { >+ alert(__("Please enter a valid date.")); >+ $wrapper.find('.split-part').first().focus(); >+ valid = false; >+ return false; >+ } >+ } >+ }); >+ return valid; >+ } >+ > $(document).ready(function(){ > $(".flatpickr").each(function(){ > apply_flatpickr(this); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >index be78a5088a8..3e625e732e1 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >@@ -10,6 +10,14 @@ I18N/L10N: > dmydot: dd.mm.yyyy > iso: yyyy-mm-dd > - . >+ - >+ - Use >+ - pref: DateInputStyle >+ default: single >+ choices: >+ single: single input field with calendar picker >+ split: split inputs (day, month, year) with optional calendar button >+ - "style for date entry fields. The split option follows the dateformat preference for field order and is recommended for accessibility; it does not apply to time, datetime, or booking date range fields." > - > - Format times in > - pref: TimeFormat >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >index 561b0d9aa0a..d6dbd0954fc 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >@@ -1804,7 +1804,7 @@ > }); > > $("#dateofbirth").on("change", function(){ >- write_age(); >+ write_age(); > }); > > $("#debarred_comment, #debarred_expiration").on("change", function(){ >diff --git a/koha-tmpl/intranet-tmpl/prog/js/members.js b/koha-tmpl/intranet-tmpl/prog/js/members.js >index a5566e9e751..a3b3e49ab4a 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/members.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/members.js >@@ -224,7 +224,11 @@ function write_age() { > var hint = $("#dateofbirth_hint"); > hint.html(dateformat); > >- var age = CalculateAge(document.form.dateofbirth.value); >+ var dobVal = >+ document.form && document.form.dateofbirth >+ ? document.form.dateofbirth.value >+ : $("#dateofbirth").val(); >+ var age = CalculateAge(dobVal); > > if (!age.year && !age.month) { > return; >@@ -319,7 +323,7 @@ $(document).ready(function () { > ).val(matches[4]); > }); > >- dateformat = $("#dateofbirth").siblings(".hint").first().html(); >+ dateformat = $("#dateofbirth_hint").html() || ""; > > if ($("#dateofbirth").length) { > write_age(); >@@ -362,6 +366,12 @@ $(document).ready(function () { > }, > }, > submitHandler: function (form) { >+ if ( >+ typeof validateSplitDateInputs === "function" && >+ !validateSplitDateInputs() >+ ) { >+ return false; >+ } > $( > "body, form input[type='submit'], form button[type='submit'], form a" > ).addClass("waiting"); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/_flatpickr.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/_flatpickr.scss >index 042e0fc0621..869e7848596 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/_flatpickr.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/_flatpickr.scss >@@ -1,5 +1,7 @@ > @import "flatpickr_vars"; > >+$flatpickr-calendar-icon: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23369' class='bi bi-calendar3'%3E%3Cg stroke-width='1.333'%3E%3Cpath d='M10.5 0h-9A1.5 1.5 0 0 0 0 1.5v9A1.5 1.5 0 0 0 1.5 12h9a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 10.5 0zM.75 2.893c0-.355.336-.643.75-.643h9c.414 0 .75.288.75.643v7.714c0 .355-.336.643-.75.643h-9c-.414 0-.75-.288-.75-.643z'/%3E%3Cpath d='M4.875 5.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zM2.625 7.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm-6.75 2.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z'/%3E%3C/g%3E%3C/svg%3E" ); >+ > @keyframes fpFadeInDown { > from { > opacity: 0; >@@ -836,7 +838,7 @@ span.flatpickr-weekday { > } > > .flatpickr-input { >- background-image: url( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23369' class='bi bi-calendar3'%3E%3Cg stroke-width='1.333'%3E%3Cpath d='M10.5 0h-9A1.5 1.5 0 0 0 0 1.5v9A1.5 1.5 0 0 0 1.5 12h9a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 10.5 0zM.75 2.893c0-.355.336-.643.75-.643h9c.414 0 .75.288.75.643v7.714c0 .355-.336.643-.75.643h-9c-.414 0-.75-.288-.75-.643z'/%3E%3Cpath d='M4.875 5.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zM2.625 7.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm-6.75 2.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5zm2.25 0a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z'/%3E%3C/g%3E%3C/svg%3E" ); >+ background-image: $flatpickr-calendar-icon; > background-position: 3px; > background-repeat: no-repeat; > border-style: inset; >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >index 42268b4b126..8304cfda929 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >@@ -926,6 +926,71 @@ div { > } > } > >+// Split date input widget (generated by calendar.inc in DateInputStyle=split mode) >+.date-split-wrapper { >+ align-items: flex-end; >+ display: inline-flex; >+ flex-wrap: wrap; >+ gap: .4rem; >+ >+ .split-part-group { >+ display: flex; >+ flex-direction: column; >+ >+ label { >+ float: none; >+ font-size: .9em; >+ font-weight: normal; >+ margin-bottom: .2rem; >+ margin-right: 0; >+ text-align: left; >+ width: auto; >+ } >+ } >+ >+ .split-day, >+ .split-month { >+ text-align: center; >+ width: 3.8rem; >+ } >+ >+ .split-year { >+ text-align: center; >+ width: 5.8rem; >+ } >+ >+ .split-sep { >+ align-self: flex-end; >+ font-size: 1.2em; >+ font-weight: bold; >+ padding-bottom: .35rem; >+ } >+ >+ .split-calendar-btn, >+ .split-clear-btn { >+ align-self: flex-end; >+ font-size: 130%; >+ margin-left: .25rem; >+ vertical-align: bottom; >+ } >+ >+ .split-calendar-btn { >+ background-image: $flatpickr-calendar-icon; >+ background-position: center; >+ background-repeat: no-repeat; >+ background-size: contain; >+ display: inline-block; >+ height: 1em; >+ opacity: .75; >+ width: 1em; >+ >+ &:hover { >+ opacity: 1; >+ text-decoration: none; >+ } >+ } >+} >+ > fieldset { > &.rows { > clear: left; >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >index 240fa9953c3..437067dd25c 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >@@ -13,6 +13,7 @@ > }; > var debug = "[% debug | html %]"; > var dateformat_pref = "[% Koha.Preference('dateformat') | html %]"; >+ var dateInputStyle = "[% Koha.Preference('DateInputStyle') | html %]"; > var sentmsg = 0; > if (debug > 1) {alert("dateformat: " + dateformat_pref + "\ndebug is on (level " + debug + ")");} > var calendarFirstDayOfWeek = '[% Koha.Preference('CalendarFirstDayOfWeek') | html %]'; >@@ -161,11 +162,194 @@ > } > } > >+ function apply_split_date_widget(input, fpOptions) { >+ let parts_order, sep; >+ switch (dateformat_pref) { >+ case "us": >+ parts_order = ["month", "day", "year"]; >+ sep = "/"; >+ break; >+ case "iso": >+ case "sql": >+ parts_order = ["year", "month", "day"]; >+ sep = "-"; >+ break; >+ case "dmydot": >+ parts_order = ["day", "month", "year"]; >+ sep = "."; >+ break; >+ default: >+ parts_order = ["day", "month", "year"]; >+ sep = "/"; // metric >+ } >+ let labels = { day: _("Day"), month: _("Month"), year: _("Year") }; >+ let placeholders = { day: "DD", month: "MM", year: "YYYY" }; >+ let autocompletes = { day: "bday-day", month: "bday-month", year: "bday-year" }; >+ let inputId = input.id || "fp_split_" + Math.random().toString(36).substr(2, 8); >+ >+ let options = Object.assign({}, fpOptions, { >+ clickOpens: false, >+ onReady: function (selectedDates, dateStr, instance) { >+ // Handle pastdate / futuredate CSS classes (normally in default onReady) >+ if ($(instance.input).hasClass("pastdate")) { >+ instance.set("maxDate", new Date().fp_incr(-1)); >+ } >+ if ($(instance.input).hasClass("futuredate")) { >+ instance.set("minDate", new Date().fp_incr(1)); >+ } >+ >+ // Hide the altInput; our split inputs replace it visually >+ $(instance.altInput).addClass("visually-hidden").attr("tabindex", "-1").attr("aria-hidden", "true"); >+ >+ let $wrapper = $('<div class="date-split-wrapper"></div>').attr("data-split-for", inputId); >+ >+ for (let i = 0; i < parts_order.length; i++) { >+ let part = parts_order[i]; >+ let partId = inputId + "_" + part; >+ let $group = $('<span class="split-part-group"></span>'); >+ $group.append($("<label></label>").attr("for", partId).text(labels[part])); >+ $group.append( >+ $('<input type="text" inputmode="numeric" pattern="[0-9]*">') >+ .attr("id", partId) >+ .attr("maxlength", part === "year" ? 4 : 2) >+ .attr("placeholder", placeholders[part]) >+ .attr("autocomplete", autocompletes[part]) >+ .addClass("split-part split-" + part) >+ ); >+ $wrapper.append($group); >+ if (i < parts_order.length - 1) { >+ $wrapper.append($('<span class="split-sep" aria-hidden="true"></span>').text(sep)); >+ } >+ } >+ >+ let $calBtn = $('<a href="#" class="split-calendar-btn"></a>').attr("title", _("Open calendar")).attr("aria-label", _("Open calendar")).attr("aria-hidden", "true"); >+ let $clearBtn = $('<a href="#" class="clear_date split-clear-btn fa fa-fw fa-times"></a>').attr("aria-label", _("Clear date")).attr("aria-hidden", "true"); >+ $wrapper.append($calBtn).append($clearBtn); >+ >+ $(instance.altInput).after($wrapper); >+ >+ // Populate split inputs from the current ISO value (edit mode) >+ let val = $(input).val(); >+ if (val && /^\d{4}-\d{2}-\d{2}$/.test(val)) { >+ let p = val.split("-"); >+ $wrapper.find(".split-year").val(p[0]); >+ $wrapper.find(".split-month").val(p[1]); >+ $wrapper.find(".split-day").val(p[2]); >+ } >+ >+ let syncing = false; >+ >+ // Split inputs â flatpickr ISO >+ $wrapper.on("input", ".split-part", function () { >+ let d = $wrapper.find(".split-day").val().trim(); >+ let m = $wrapper.find(".split-month").val().trim(); >+ let y = $wrapper.find(".split-year").val().trim(); >+ syncing = true; >+ if (d && m && y && y.length === 4) { >+ instance.setDate(y + "-" + m.padStart(2, "0") + "-" + d.padStart(2, "0"), false); >+ } else { >+ instance.clear(false); >+ $(input).val(""); >+ } >+ $(input).trigger("change"); >+ syncing = false; >+ }); >+ >+ // Auto-advance from day/month after 2 digits (skip on delete/backspace) >+ $wrapper.on("input", ".split-day, .split-month", function (e) { >+ let inputType = e.originalEvent && e.originalEvent.inputType; >+ if ($(this).val().length >= 2 && inputType && !inputType.startsWith("delete")) { >+ let $parts = $wrapper.find(".split-part"); >+ let idx = $parts.index(this); >+ if (idx < $parts.length - 1) { >+ $parts >+ .eq(idx + 1) >+ .focus() >+ .select(); >+ } >+ } >+ }); >+ >+ // Calendar date selected â populate split inputs >+ $(input).on("change", function () { >+ if (syncing) return; >+ let v = $(input).val(); >+ if (v && /^\d{4}-\d{2}-\d{2}$/.test(v)) { >+ let p = v.split("-"); >+ $wrapper.find(".split-year").val(p[0]); >+ $wrapper.find(".split-month").val(p[1]); >+ $wrapper.find(".split-day").val(p[2]); >+ } else { >+ $wrapper.find(".split-year, .split-month, .split-day").val(""); >+ } >+ }); >+ >+ // Calendar button opens the flatpickr calendar >+ $calBtn.on("click", function (e) { >+ e.preventDefault(); >+ instance.open(); >+ }); >+ >+ // Clear button clears all split inputs and the flatpickr value >+ $clearBtn.on("click", function (e) { >+ e.preventDefault(); >+ syncing = true; >+ instance.clear(); >+ $(input).val(""); >+ $wrapper.find(".split-year, .split-month, .split-day").val(""); >+ syncing = false; >+ }); >+ }, >+ }); >+ >+ return $(input).flatpickr(options); >+ } >+ >+ function validateSplitDateInputs() { >+ let valid = true; >+ $(".date-split-wrapper").each(function () { >+ let $wrapper = $(this); >+ let origId = $wrapper.data("split-for"); >+ let $orig = origId ? $("#" + origId) : null; >+ let isRequired = $orig && $orig.is("[required]"); >+ let d = $wrapper.find(".split-day").val().trim(); >+ let m = $wrapper.find(".split-month").val().trim(); >+ let y = $wrapper.find(".split-year").val().trim(); >+ let anyFilled = d || m || y; >+ let allFilled = d && m && y && y.length === 4; >+ >+ if (isRequired && !allFilled) { >+ alert(_("Please enter a complete date.")); >+ $wrapper.find(".split-part").first().focus(); >+ valid = false; >+ return false; >+ } >+ if (anyFilled && !allFilled) { >+ alert(_("Please complete all parts of the date field.")); >+ $wrapper.find(".split-part").first().focus(); >+ valid = false; >+ return false; >+ } >+ if (allFilled) { >+ let dt = new Date(parseInt(y, 10), parseInt(m, 10) - 1, parseInt(d, 10)); >+ if (dt.getFullYear() !== parseInt(y, 10) || dt.getMonth() !== parseInt(m, 10) - 1 || dt.getDate() !== parseInt(d, 10)) { >+ alert(_("Please enter a valid date.")); >+ $wrapper.find(".split-part").first().focus(); >+ valid = false; >+ return false; >+ } >+ } >+ }); >+ return valid; >+ } >+ > $(document).ready(function () { > $(".flatpickr").each(function () { > let options = {}; > let refresh_max_date = 0; > let disable_buttons = []; >+ let is_time = $(this).data("flatpickr-enable-time") === true; >+ let is_period = $(this).data("flatpickr-period"); > > if ($(this).data("flatpickr-futureinclusive") === true || $(this).data("flatpickr-futuredate") === true) { > let original_date = $(this).val(); >@@ -209,7 +393,12 @@ > options["dateFormat"] = flatpickr_dateformat_string + " " + flatpickr_timeformat_string; > } > >- let fp = $(this).flatpickr(options); >+ let fp; >+ if (typeof dateInputStyle !== "undefined" && dateInputStyle === "split" && !is_time && !is_period) { >+ fp = apply_split_date_widget(this, options); >+ } else { >+ fp = $(this).flatpickr(options); >+ } > > $(disable_buttons).each(function (index, value) { > $(fp.calendarContainer) >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt >index 8a7563c7756..1e6f167b59d 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt >@@ -483,7 +483,6 @@ > [% UNLESS hidden.defined('dateofbirth') %] > <li> > <label for="borrower_dateofbirth" class="[% required.dateofbirth | html %]">Date of birth:</label> >- > <input > type="text" > id="borrower_dateofbirth" >@@ -493,7 +492,6 @@ > class="[% required.dateofbirth | html %] flatpickr pastdate" > [% IF required.dateofbirth %]required[% END %] > /> >- > <div class="required_label [% required.dateofbirth | html %]">Required</div> > </li> > [% END %] >@@ -1426,6 +1424,9 @@ > } > }, > submitHandler: function(form) { >+ if (typeof validateSplitDateInputs === "function" && !validateSplitDateInputs()) { >+ return false; >+ } > if (form.beenSubmitted) { > return false; > } >-- >2.53.0 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 33886
: 193933 |
193970
|
193971