diff --git a/js/rollup.conf.js b/js/rollup.conf.js index 477e391..b0a1fa8 100644 --- a/js/rollup.conf.js +++ b/js/rollup.conf.js @@ -2,6 +2,7 @@ import cleanup from 'rollup-plugin-cleanup'; import {terser} from 'rollup-plugin-terser'; const out_dir = 'src/yafowil/widget/datetime/resources'; +const out_dir_bs5 = 'src/yafowil/widget/datetime/resources/bootstrap5'; const outro = ` window.yafowil = window.yafowil || {}; @@ -43,5 +44,43 @@ export default args => { interop: 'default' }); } - return conf; + + // Bootstrap 5 + let conf_2 = { + input: 'js/src/bootstrap5/bundle.js', + plugins: [ + cleanup() + ], + output: [{ + name: 'yafowil_datetime', + file: `${out_dir_bs5}/widget.js`, + format: 'iife', + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }], + external: [ + 'jquery', + 'bootstrap' + ] + }; + if (args.configDebug !== true) { + conf_2.output.push({ + name: 'yafowil_datetime', + file: `${out_dir_bs5}/widget.min.js`, + format: 'iife', + plugins: [ + terser() + ], + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }); + } + + return [conf, conf_2]; }; diff --git a/js/src/bootstrap5/bundle.js b/js/src/bootstrap5/bundle.js new file mode 100644 index 0000000..c63879a --- /dev/null +++ b/js/src/bootstrap5/bundle.js @@ -0,0 +1,24 @@ +import $ from 'jquery'; + +import {DatepickerWidget} from './datepicker.js'; +import {TimepickerWidget} from './timepicker.js'; +import {register_datepicker_array_subscribers} from './datepicker.js'; +import {register_timepicker_array_subscribers} from './timepicker.js'; + +export * from './datepicker.js'; +export * from './timepicker.js'; + +$(function() { + if (window.ts !== undefined) { + ts.ajax.register(DatepickerWidget.initialize, true); + ts.ajax.register(TimepickerWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(DatepickerWidget.initialize, true); + bdajax.register(TimepickerWidget.initialize, true); + } else { + DatepickerWidget.initialize(); + TimepickerWidget.initialize(); + } + register_datepicker_array_subscribers(); + register_timepicker_array_subscribers(); +}); diff --git a/js/src/bootstrap5/datepicker.js b/js/src/bootstrap5/datepicker.js new file mode 100644 index 0000000..b9ba386 --- /dev/null +++ b/js/src/bootstrap5/datepicker.js @@ -0,0 +1,109 @@ +import $ from 'jquery'; + +// Datepicker base class is global. +export class DatepickerWidget extends Datepicker { + + static initialize(context) { + $('input.datepicker', context).each(function() { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new DatepickerWidget(elem, elem.data('date-locale')); + }); + } + + constructor(elem, locale, opts={}) { + let opts_ = { + language: locale, + orientation: 'bottom', + buttonClass: 'btn', + todayHighlight: true, + autohide: true + }; + + let lower_edge = elem.offset().top + elem.outerHeight() + 250; + if (lower_edge > $(document).height()) { + opts_.orientation = "top"; + } + + let locale_options = DatepickerWidget.locale_options; + Object.assign(opts_, locale_options[locale] || locale_options.en); + Object.assign(opts_, opts); + super(elem[0], opts_); + + elem.data('yafowil-datepicker', this); + this.elem = elem; + + this.adapt(); + + this.trigger = $(``) + .addClass('datepicker-trigger btn btn-outline-secondary') + .text('...') + .insertAfter(elem); + + this.toggle_picker = this.toggle_picker.bind(this); + this.trigger + .off('mousedown touchstart', this.toggle_picker) + .on('mousedown touchstart', this.toggle_picker); + this.trigger.on('click', (e) => {e.preventDefault()}); + this.elem.on('changeDate', () => { + this.elem.trigger('change'); + }); + + let created_event = $.Event('datepicker_created', {widget: this}); + this.elem.trigger(created_event); + } + + adapt() { + const p_el = $(this.picker.element); + $('.datepicker-picker', p_el).addClass('card'); + const header = $('.datepicker-header', p_el).addClass('card-header bg-primary'); + $('.btn', header).addClass('btn-primary'); + $('.datepicker-main', p_el).addClass('card-body'); + } + + unload() { + this.trigger.off('mousedown touchstart', this.toggle_picker); + this.elem.off('focus', this.prevent_hide); + } + + toggle_picker(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (this.picker.active || this.active) { + this.hide(); + this.elem.blur(); + } else { + this.show(); + } + } +} + +DatepickerWidget.locale_options = { + en: { + weekStart: 1, + format: 'mm.dd.yyyy' + }, + de: { + weekStart: 1, + format: 'dd.mm.yyyy' + } +}; + +////////////////////////////////////////////////////////////////////////////// +// yafowil.widget.array integration +////////////////////////////////////////////////////////////////////////////// + +function datepicker_on_array_add(inst, context) { + DatepickerWidget.initialize(context); +} + +export function register_datepicker_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', datepicker_on_array_add); +} diff --git a/js/src/bootstrap5/timepicker.js b/js/src/bootstrap5/timepicker.js new file mode 100644 index 0000000..451a39e --- /dev/null +++ b/js/src/bootstrap5/timepicker.js @@ -0,0 +1,424 @@ +import $ from 'jquery'; + +export class TimepickerButton { + + constructor(elem) { + this.elem = elem; + } + + get selected() { + return this.elem.hasClass('selected'); + } + + set selected(value) { + if (value) { + this.elem.addClass('selected'); + } else { + this.elem.removeClass('selected'); + } + } +} + +export class TimepickerButtonContainer { + + constructor(picker, elem) { + this.picker = picker; + this.elem = elem; + this.children = []; + } + + unload_all() { + for (let child of this.children) { + child.elem.off('click', child.on_click); + } + } + + unselect_all() { + for (let child of this.children) { + child.selected = false; + } + } +} + +export class TimepickerHour extends TimepickerButton { + + constructor(hours, container, value, period) { + super($('
') + .addClass('cell') + .text(new String(value).padStart(2, '0')) + .appendTo(container) + ); + this.hours = hours; + this.picker = hours.picker; + this.period = period; + this.on_click = this.on_click.bind(this); + this.elem.on('click', this.on_click); + } + + on_click(e) { + let hour = this.elem.text(); + this.hours.unselect_all(); + this.selected = true; + this.picker.period = this.period; + this.picker.hour = hour; + } +} + +export class TimepickerMinute extends TimepickerButton { + + constructor(minutes, container, value) { + super($('') + .addClass('cell') + .text(new String(value).padStart(2, '0')) + .appendTo(container) + ); + this.minutes = minutes; + this.picker = minutes.picker; + this.on_click = this.on_click.bind(this); + this.elem.on('click', this.on_click); + } + + on_click(e) { + this.minutes.unselect_all(); + this.selected = true; + this.picker.minute = this.elem.text(); + } +} + +export class TimepickerHours extends TimepickerButtonContainer { + + constructor(picker, container) { + super(picker, $('').addClass('card-body hours-content')); + if (picker.clock === 24) { + this.create_clock_24(); + } else if (picker.clock === 12) { + this.create_clock_12(); + } + let header = $('') + .addClass('header card-header bg-primary text-white') + .text(picker.translate('hour')); + $('') + .addClass('card timepicker-hours') + .append(header) + .append(this.elem) + .appendTo(container); + } + + create_clock_24() { + for (let i = 0; i < 24; i++) { + this.children.push(new TimepickerHour(this, this.elem, i)); + } + } + + create_clock_12() { + $('').addClass('am').text('A.M.').appendTo(this.elem); + let hours_am = $('').addClass('am').appendTo(this.elem); + $('').addClass('pm').text('P.M.').appendTo(this.elem); + let hours_pm = $('').addClass('pm').appendTo(this.elem); + for (let i = 0; i < 12; i++) { + this.children.push(new TimepickerHour(this, hours_am, i, 'AM')); + } + for (let i = 0; i < 12; i++) { + this.children.push(new TimepickerHour(this, hours_pm, i, 'PM')); + } + this.elem.css('grid-template-rows', '1fr 1fr'); + this.elem.css('grid-template-columns', '1fr 1fr'); + } +} + +export class TimepickerMinutes extends TimepickerButtonContainer { + + constructor(picker, container, step) { + super(picker, $('').addClass('card-body minutes-content')); + this.step = step; + let count = 60 / step; + if (count <= 32) { + // calculate grid columns based on number of cells + let cols = '1fr '.repeat(Math.ceil(count / 4)); + this.elem.css( + 'grid-template-columns', + cols + ); + } else { + this.elem.css( + 'grid-template-columns', + '1fr '.repeat(10) + ); + if (picker.clock === 24) { + $('div.hours-content', picker.dd_elem).css({ + 'grid-template-columns': '1fr 1fr 1fr 1fr', + 'width': '110px' + }); + } else { + $('div.am, div.pm', picker.dd_elem).css({ + 'grid-template-columns': '1fr 1fr 1fr 1fr' + }); + } + } + + for (let i = 0; i < count; i++) { + this.children.push(new TimepickerMinute(this, this.elem, i * step)); + } + let header = $('') + .addClass('header card-header bg-primary text-white') + .text(picker.translate('minute')); + $('') + .addClass('card timepicker-minutes') + .append(header) + .append(this.elem) + .appendTo(container); + } +} + +export class TimepickerWidget { + + static initialize(context) { + $('input.timepicker', context).each(function() { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + elem.attr('spellcheck', false); + new TimepickerWidget(elem, { + language: elem.data('time-locale'), + clock: elem.data('time-clock'), + step: elem.data('time-minutes_step') + }); + }); + } + + constructor(elem, opts) { + elem.data('yafowil-timepicker', this); + this.elem = elem; + this.language = opts.language || 'en'; + this.clock = opts.clock || 24; + + this.period = null; + this.hour = ''; + this.minute = ''; + if (opts.step <= 0 || opts.step > 60 || typeof opts.step !== 'number') { + this.step = 5; + } else { + this.step = opts.step; + } + + this.trigger_elem = $('') + .addClass('timepicker-trigger btn btn-outline-secondary') + .text('...') + .insertAfter(elem); + + let dd_elem = this.dd_elem = $('') + .addClass('timepicker-dropdown') + .insertAfter(elem.closest('.input-group')); + + let dd_container = $('') + .addClass('timepicker-container card-group shadow') + .appendTo(dd_elem); + + this.hours = new TimepickerHours(this, dd_container); + this.minutes = new TimepickerMinutes(this, dd_container, this.step); + + this.validate(); + this.place = this.place.bind(this); + // this.place(); + + this.show_dropdown = this.show_dropdown.bind(this); + this.elem.on('focus', this.show_dropdown); + + this.toggle_dropdown = this.toggle_dropdown.bind(this); + this.trigger_elem.on('click', this.toggle_dropdown); + + this.hide_dropdown = this.hide_dropdown.bind(this); + $(document).on('click', this.hide_dropdown); + + this.on_keypress = this.on_keypress.bind(this); + this.elem.on('keypress', this.on_keypress); + this.validate = this.validate.bind(this); + this.elem.on('keyup', this.validate); + + // $(window).on('resize', this.place); + + let created_event = $.Event('timepicker_created', {widget: this}); + this.elem.trigger(created_event); + } + + unload() { + $(document).off('click', this.hide_dropdown); + + this.hours.unload_all(); + this.minutes.unload_all(); + } + + get hour() { + return this._hour; + } + + set hour(hour) { + this._hour = hour; + this.set_time(); + } + + get minute() { + return this._minute; + } + + set minute(minute) { + this._minute = minute; + this.set_time(); + } + + place() { + let offset = this.elem.offset().left - this.elem.parent().offset().left; + this.dd_elem.css('left', `${offset}px`); + + let offset_left = this.elem.offset().left, + offset_top = this.elem.offset().top, + dd_width = this.dd_elem.outerWidth(), + elem_width = this.elem.outerWidth(); + + let lower_edge = offset_top + this.elem.outerHeight() + 250; + let right_edge = offset_left + dd_width; + this.dd_elem.css('transform', 'translateX(0px)'); + + if (lower_edge > $(document).height()) { + let height = this.dd_elem.outerHeight() + 9; + this.dd_elem.css('top', `-${height}px`); + } + if (offset_left + elem_width - dd_width < 0) { + let leftx = right_edge - $(window).width(); + this.dd_elem.css('transform', `translateX(-${leftx}px)`); + } else if (right_edge > $(window).width()) { + this.dd_elem + .css('transform', + `translateX(calc(-100% + ${elem_width}px))`); + } + } + + set_time() { + if (this.hour === '' || this.minute === '') { + return; + } + if (this.clock === 24) { + this.elem.val(`${this.hour}:${this.minute}`); + } else if (this.clock === 12) { + this.elem.val(`${this.hour}:${this.minute}${this.period}`); + } + this.elem.trigger('change'); + this.hour = ''; + this.minute = ''; + this.dd_elem.hide(); + } + + hide_dropdown(e) { + if (e.target !== this.elem[0] && e.target !== this.trigger_elem[0]) { + if ($(e.target).closest(this.dd_elem).length === 0) { + this.dd_elem.hide(); + } + } + } + + show_dropdown(e) { + this.dd_elem.show(); + } + + toggle_dropdown(e) { + e.preventDefault(); + this.dd_elem.toggle(); + } + + on_keypress(e) { + e.preventDefault(); + let val = this.elem.val(); + let cursor_pos = e.target.selectionStart; + if (e.key === 'Enter') { + this.dd_elem.hide(); + this.elem.blur(); + } + + if (cursor_pos <= 4) { + if (e.key.match(new RegExp('[0-9]'))) { + this.elem.val(val + e.key); + if (cursor_pos === 2) { + this.elem.val(`${val}:${e.key}`); + } + } + } else if (cursor_pos === 5 && this.clock === 12) { + let correct = ['a', 'A', 'p', 'P']; + if (correct.includes(e.key)) { + this.elem.val(val + e.key); + } + } else if (cursor_pos === 6 && this.clock === 12) { + if (e.key === 'm' || e.key === 'M') { + this.elem.val(val + e.key); + } + }; + } + + validate() { + if (!this.elem.val()) return; + + let time = this.elem.val(), + match_24 = new RegExp(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/), + match_12 = new RegExp(/^(?:0?\d|1[012]):[0-5]\d[apAP][mM]$/), + hour = time.substr(0, 2), + hour_index = parseInt(hour), + minute = time.substr(3, 2), + minute_index = parseInt(minute) / this.step; + + if (this.clock === 24) { + if (!time.match(match_24) || time.length < 5) { + return; + } + } else if (this.clock === 12) { + if (!time.match(match_12) || time.length < 7) { + return; + } + let period = time.substr(5).toUpperCase(); + this.period = (period === 'PM') ? 'PM' : 'AM'; + if (period === 'PM') hour_index += 12; + } + + let hour_elem = this.hours.children[hour_index]; + hour_elem.on_click(); + + let minute_elem = this.minutes.children[minute_index]; + if (minute_elem) { + minute_elem.on_click(); + } else { + this.minutes.unselect_all(); + } + } + + translate(msgid) { + let locales = this.constructor.locales, + locale = locales[this.language] || locales.en; + return locale[msgid]; + } +} + +TimepickerWidget.locales = { + en: { + hour: 'Hour', + minute: 'Minute' + }, + de: { + hour: 'Stunde', + minute: 'Minute' + } +}; + +////////////////////////////////////////////////////////////////////////////// +// yafowil.widget.array integration +////////////////////////////////////////////////////////////////////////////// + +function timepicker_on_array_add(inst, context) { + TimepickerWidget.initialize(context); +} + +export function register_timepicker_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', timepicker_on_array_add); +} diff --git a/scripts/styles.sh b/scripts/styles.sh index 2628280..efd2d6f 100755 --- a/scripts/styles.sh +++ b/scripts/styles.sh @@ -4,4 +4,6 @@ SASS_BIN="./node_modules/sass/sass.js" SASS_DIR="./scss" TARGET_DIR="./src/yafowil/widget/datetime/resources" -$SASS_BIN $SASS_DIR/timepicker.scss --no-source-map $TARGET_DIR/timepicker.css \ No newline at end of file +$SASS_BIN $SASS_DIR/timepicker.scss --no-source-map $TARGET_DIR/timepicker.css +$SASS_BIN $SASS_DIR/bootstrap5/timepicker.scss --no-source-map $TARGET_DIR/bootstrap5/timepicker.css +$SASS_BIN $SASS_DIR/bootstrap5/datepicker.scss --no-source-map $TARGET_DIR/bootstrap5/datepicker.css \ No newline at end of file diff --git a/scss/bootstrap5/datepicker.scss b/scss/bootstrap5/datepicker.scss new file mode 100644 index 0000000..aa070ad --- /dev/null +++ b/scss/bootstrap5/datepicker.scss @@ -0,0 +1,278 @@ +div.datepicker { + display: none; +} + +.datepicker.active { + display: block; +} + +.datepicker-dropdown { + position: absolute; + top: 0; + left: 0; + z-index: 2000; + padding-top: 4px; +} + +.datepicker-bs-dropdown { + border: 0; + background: none; + padding: 0; +} + +.datepicker-header { + .datepicker-controls { + padding: 2px 2px 0; + } + .datepicker-title { + box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.1); + background-color: #f8f9fa; + padding: 0.375rem 0.75rem; + text-align: center; + font-weight: 700; + } + .datepicker-controls { + display: flex; + width: 300px; + + .view-switch { + flex: auto; + } + + prev-btn, + .next-btn { + padding-right: 0.375rem; + padding-left: 0.375rem; + width: 2.25rem; + + &.disabled { + visibility: hidden; + } + } + } +} + +.dateinput { + width: 10em; +} + +.datepicker-dropdown .datepicker-picker { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175); +} + +.datepicker-picker span { + display: block; + flex: 1; + margin-bottom: 5px; + border: 0; + border-radius: 0.25rem; + cursor: default; + text-align: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.datepicker-footer { + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + background-color: #f8f9fa; +} + +.datepicker-controls, .datepicker-view, .datepicker-view .days-of-week, .datepicker-grid { + display: flex; + width: 300px; +} + +.datepicker-grid { + flex-wrap: wrap; +} + +.datepicker-view .dow, .datepicker-view .days .datepicker-cell { + flex-basis: 14.28571%; +} + +.datepicker-view.datepicker-grid .datepicker-cell { + flex-basis: 25%; +} + +.datepicker-view .week, .datepicker-cell { + height: 2.25rem; + line-height: 2.25rem; +} + + +.datepicker-footer .datepicker-controls .btn { + margin: calc(0.375rem - 1px) 0.375rem; + border-radius: 0.2rem; + width: 100%; + font-size: 0.875rem; +} + +.datepicker-view .dow { + color: var(--bs-primary); +} + +.datepicker-view .week { + width: 2.25rem; + color: #dee2e6; + font-size: 0.875rem; +} + +@media (max-width: 22.5rem) { + .datepicker-view .week { + width: 1.96875rem; + } +} + +@media (max-width: 22.5rem) { + .calendar-weeks + .days .datepicker-grid { + width: 13.78125rem; + } +} + +.datepicker-cell:not(.disabled):hover { + background-color: #f9f9f9; + cursor: pointer; +} +.datepicker-cell.focused:not(.selected) { + background-color: #f1f3f5; +} +.datepicker-cell.selected, .datepicker-cell.selected:hover { + background-color: var(--bs-primary); + color: #fff; + font-weight: 600; +} +.datepicker-cell.disabled { + color: #cdd0d4; +} +.datepicker-cell.prev:not(.disabled), .datepicker-cell.next:not(.disabled) { + color: #cdd0d4; +} +.datepicker-cell.prev.selected, .datepicker-cell.next.selected { + color: #e6e6e6; +} +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today) { + border-radius: 0; + background-color: #f8f9fa; +} +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover { + background-color: #f1f3f5; +} +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused { + background-color: #f1f3f5; +} +.datepicker-cell.today:not(.selected) { + background-color: #eee; +} +.datepicker-cell.today:not(.selected):not(.disabled) { + color: #000; +} +.datepicker-cell.today.focused:not(.selected) { + background-color: #eee; +} +.datepicker-cell.range-start:not(.selected), .datepicker-cell.range-end:not(.selected) { + background-color: #6c757d; + color: #fff; +} +.datepicker-cell.range-start.focused:not(.selected), .datepicker-cell.range-end.focused:not(.selected) { + background-color: #666f76; +} +.datepicker-cell.range { + border-radius: 0; + background-color: #e9ecef; +} +.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover { + background-color: #e2e6ea; +} +.datepicker-cell.range.disabled { + color: #cbd3da; +} +.datepicker-cell.range.focused { + background-color: #dadfe4; +} + +[data-bs-theme="dark"] { + .datepicker-cell:not(.disabled):hover { + background-color: #343a40; + cursor: pointer; + } + .datepicker-cell.focused:not(.selected) { + background-color: #495057; + } + .datepicker-cell.selected, .datepicker-cell.selected:hover { + background-color: var(--bs-primary); + color: #fff; + font-weight: 600; + } + .datepicker-cell.disabled { + color: #868e96; + } + .datepicker-cell.prev:not(.disabled), .datepicker-cell.next:not(.disabled) { + color: #868e96; + } + .datepicker-cell.prev.selected, .datepicker-cell.next.selected { + color: #adb5bd; + } + .datepicker-cell.highlighted:not(.selected):not(.range):not(.today) { + border-radius: 0; + background-color: #3f454a; + } + .datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover { + background-color: #495057; + } + .datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused { + background-color: #495057; + } + .datepicker-cell.today:not(.selected) { + background-color: #4e555b; + } + .datepicker-cell.today:not(.selected):not(.disabled) { + color: #ffffff; + } + .datepicker-cell.today.focused:not(.selected) { + background-color: #4e555b; + } + .datepicker-cell.range-start:not(.selected), .datepicker-cell.range-end:not(.selected) { + background-color: #343a40; + color: #fff; + } + .datepicker-cell.range-start.focused:not(.selected), .datepicker-cell.range-end.focused:not(.selected) { + background-color: #495057; + } + .datepicker-cell.range { + border-radius: 0; + background-color: #495057; + } + .datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover { + background-color: #6c757d; + } + .datepicker-cell.range.disabled { + color: #6c757d; + } + .datepicker-cell.range.focused { + background-color: #495057; + } +} + +.datepicker-cell.range-start { + border-radius: 0.25rem 0 0 0.25rem; +} + +.datepicker-cell.range-end { + border-radius: 0 0.25rem 0.25rem 0; +} + +.datepicker-view.datepicker-grid .datepicker-cell { + height: 4.5rem; + line-height: 4.5rem; +} + +.datepicker-input.in-edit { + border-color: var(--yafowil-accent-color, #0d6efd); +} + +.datepicker-input.in-edit:focus, .datepicker-input.in-edit:active { + box-shadow: 0 0 0.25em 0.25em rgba(102, 176, 255, 0.2); +} diff --git a/scss/bootstrap5/timepicker.scss b/scss/bootstrap5/timepicker.scss new file mode 100644 index 0000000..d447d15 --- /dev/null +++ b/scss/bootstrap5/timepicker.scss @@ -0,0 +1,106 @@ +// input.timeinput { +// width: 6em; +// margin-right: 45px; +// display: inline-block; +// } + +// button.timepicker-trigger { +// position: absolute; +// width: 34px; +// height: 34px; +// margin: 0; +// padding: 0; +// transform: translateX(calc(-100% - 5px)); +// } + +// XXX: flex-basis! + +.timepicker-dropdown { + position: absolute; + margin-top: 4px; + display: none; + z-index: 1000; +} + +.timepicker-container { +// display: flex; +// height: 100%; + +// .header { +// height: 30px; +// background: none; +// padding: 5px; +// font-weight: bold; +// border-radius: 5px; +// &:hover { +// background: #dae0e5; +// } +// } + + .cell { + cursor: pointer; + // display: flex; + // align-items: center; + // justify-content: center; + padding: 5px; + border-radius: 3px; + user-select: none; + &:hover { + background:#f9f9f9; + } + &.selected { + background-color: var(--yafowil-accent-color, #0d6efd); + color: var(--yafowil-accent-font-color, #fff); + font-weight: 600; + } + } + + .timepicker-hours { + // background: white; + // margin-right: 6px; + + .hours-content { + // width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + } + div.am, div.pm { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + } + div.am { + border-bottom: 1px solid #e7e7e7; + } + span.am, span.pm { + font-size: 10px; + float: left; + position: relative; + top: 25px; + padding: 0 6px; + font-weight: 600; + } + } + + .timepicker-minutes { + // background: white; + // flex: auto; + + .minutes-content { + // width: 100%; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + + // &::before { + // content: ""; + // position: absolute; + // height: 100%; + // bottom: 0; + // border-left: 1px solid #e7e7e7; + // transform: translateX(-4px); + // } + } + } +} diff --git a/src/yafowil/widget/datetime/__init__.py b/src/yafowil/widget/datetime/__init__.py index 5295e14..2ae27a4 100644 --- a/src/yafowil/widget/datetime/__init__.py +++ b/src/yafowil/widget/datetime/__init__.py @@ -71,6 +71,65 @@ }] +############################################################################## +# Bootstrap 5 +############################################################################## + +# webresource ################################################################ + +bootstrap5_resources = wr.ResourceGroup( + name='yafowil.widget.datetime', + directory=resources_dir, + path='yafowil-datetime' +) +bootstrap5_resources.add(wr.ScriptResource( + name='datepicker-js', + resource='datepicker.js', + # compressed='datepicker.min.js' +)) +bootstrap5_resources.add(wr.ScriptResource( + name='datepicker-de-js', + depends='datepicker-js', + directory=os.path.join(resources_dir, 'locales'), + path='yafowil-datetime/locales', + resource='de.js' +)) +bootstrap5_resources.add(wr.ScriptResource( + name='yafowil-datetime-js', + depends=['jquery-js', 'datepicker-js'], + resource='bootstrap5/widget.js', + compressed='bootstrap5/widget.min.js' +)) +bootstrap5_resources.add(wr.StyleResource( + name='yafowil-datepicker-css', + resource='bootstrap5/datepicker.css' +)) +bootstrap5_resources.add(wr.StyleResource( + name='yafowil-timepicker-css', + resource='bootstrap5/timepicker.css' +)) +# B/C resources ############################################################## + +bootstrap5_js = [{ + 'group': 'yafowil.widget.tiptap.dependencies', + 'resource': 'tiptap/tiptap.js', + 'order': 20, +}, { + 'group': 'yafowil.widget.tiptap.common', + 'resource': 'bootstrap5/widget.js', + 'order': 21, +}] +bootstrap5_css = [{ + 'group': 'yafowil.widget.tiptap.dependencies', + 'resource': 'tiptap/tiptap.css', + 'order': 20, +}, { + 'group': 'yafowil.widget.tiptap.common', + 'resource': 'bootstrap5/widget.css', + 'order': 21, +}] + + ############################################################################## # Registration ############################################################################## @@ -90,3 +149,18 @@ def register(): css=css ) factory.register_resources('default', widget_name, resources) + + # Bootstrap 5 + factory.register_theme( + ['bootstrap5'], + widget_name, + resources_dir, + js=bootstrap5_js, + css=bootstrap5_css + ) + + factory.register_resources( + ['bootstrap5'], + widget_name, + bootstrap5_resources + ) diff --git a/src/yafowil/widget/datetime/resources/bootstrap5/datepicker.css b/src/yafowil/widget/datetime/resources/bootstrap5/datepicker.css new file mode 100644 index 0000000..bc836da --- /dev/null +++ b/src/yafowil/widget/datetime/resources/bootstrap5/datepicker.css @@ -0,0 +1,286 @@ +div.datepicker { + display: none; +} + +.datepicker.active { + display: block; +} + +.datepicker-dropdown { + position: absolute; + top: 0; + left: 0; + z-index: 2000; + padding-top: 4px; +} + +.datepicker-bs-dropdown { + border: 0; + background: none; + padding: 0; +} + +.datepicker-header .datepicker-controls { + padding: 2px 2px 0; +} +.datepicker-header .datepicker-title { + box-shadow: inset 0 -1px 1px rgba(0, 0, 0, 0.1); + background-color: #f8f9fa; + padding: 0.375rem 0.75rem; + text-align: center; + font-weight: 700; +} +.datepicker-header .datepicker-controls { + display: flex; + width: 300px; +} +.datepicker-header .datepicker-controls .view-switch { + flex: auto; +} +.datepicker-header .datepicker-controls prev-btn, +.datepicker-header .datepicker-controls .next-btn { + padding-right: 0.375rem; + padding-left: 0.375rem; + width: 2.25rem; +} +.datepicker-header .datepicker-controls prev-btn.disabled, +.datepicker-header .datepicker-controls .next-btn.disabled { + visibility: hidden; +} + +.dateinput { + width: 10em; +} + +.datepicker-dropdown .datepicker-picker { + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175); +} + +.datepicker-picker span { + display: block; + flex: 1; + margin-bottom: 5px; + border: 0; + border-radius: 0.25rem; + cursor: default; + text-align: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.datepicker-footer { + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + background-color: #f8f9fa; +} + +.datepicker-controls, .datepicker-view, .datepicker-view .days-of-week, .datepicker-grid { + display: flex; + width: 300px; +} + +.datepicker-grid { + flex-wrap: wrap; +} + +.datepicker-view .dow, .datepicker-view .days .datepicker-cell { + flex-basis: 14.28571%; +} + +.datepicker-view.datepicker-grid .datepicker-cell { + flex-basis: 25%; +} + +.datepicker-view .week, .datepicker-cell { + height: 2.25rem; + line-height: 2.25rem; +} + +.datepicker-footer .datepicker-controls .btn { + margin: calc(0.375rem - 1px) 0.375rem; + border-radius: 0.2rem; + width: 100%; + font-size: 0.875rem; +} + +.datepicker-view .dow { + color: var(--bs-primary); +} + +.datepicker-view .week { + width: 2.25rem; + color: #dee2e6; + font-size: 0.875rem; +} + +@media (max-width: 22.5rem) { + .datepicker-view .week { + width: 1.96875rem; + } +} +@media (max-width: 22.5rem) { + .calendar-weeks + .days .datepicker-grid { + width: 13.78125rem; + } +} +.datepicker-cell:not(.disabled):hover { + background-color: #f9f9f9; + cursor: pointer; +} + +.datepicker-cell.focused:not(.selected) { + background-color: #f1f3f5; +} + +.datepicker-cell.selected, .datepicker-cell.selected:hover { + background-color: var(--bs-primary); + color: #fff; + font-weight: 600; +} + +.datepicker-cell.disabled { + color: #cdd0d4; +} + +.datepicker-cell.prev:not(.disabled), .datepicker-cell.next:not(.disabled) { + color: #cdd0d4; +} + +.datepicker-cell.prev.selected, .datepicker-cell.next.selected { + color: #e6e6e6; +} + +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today) { + border-radius: 0; + background-color: #f8f9fa; +} + +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover { + background-color: #f1f3f5; +} + +.datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused { + background-color: #f1f3f5; +} + +.datepicker-cell.today:not(.selected) { + background-color: #eee; +} + +.datepicker-cell.today:not(.selected):not(.disabled) { + color: #000; +} + +.datepicker-cell.today.focused:not(.selected) { + background-color: #eee; +} + +.datepicker-cell.range-start:not(.selected), .datepicker-cell.range-end:not(.selected) { + background-color: #6c757d; + color: #fff; +} + +.datepicker-cell.range-start.focused:not(.selected), .datepicker-cell.range-end.focused:not(.selected) { + background-color: #666f76; +} + +.datepicker-cell.range { + border-radius: 0; + background-color: #e9ecef; +} + +.datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover { + background-color: #e2e6ea; +} + +.datepicker-cell.range.disabled { + color: #cbd3da; +} + +.datepicker-cell.range.focused { + background-color: #dadfe4; +} + +[data-bs-theme=dark] .datepicker-cell:not(.disabled):hover { + background-color: #343a40; + cursor: pointer; +} +[data-bs-theme=dark] .datepicker-cell.focused:not(.selected) { + background-color: #495057; +} +[data-bs-theme=dark] .datepicker-cell.selected, [data-bs-theme=dark] .datepicker-cell.selected:hover { + background-color: var(--bs-primary); + color: #fff; + font-weight: 600; +} +[data-bs-theme=dark] .datepicker-cell.disabled { + color: #868e96; +} +[data-bs-theme=dark] .datepicker-cell.prev:not(.disabled), [data-bs-theme=dark] .datepicker-cell.next:not(.disabled) { + color: #868e96; +} +[data-bs-theme=dark] .datepicker-cell.prev.selected, [data-bs-theme=dark] .datepicker-cell.next.selected { + color: #adb5bd; +} +[data-bs-theme=dark] .datepicker-cell.highlighted:not(.selected):not(.range):not(.today) { + border-radius: 0; + background-color: #3f454a; +} +[data-bs-theme=dark] .datepicker-cell.highlighted:not(.selected):not(.range):not(.today):not(.disabled):hover { + background-color: #495057; +} +[data-bs-theme=dark] .datepicker-cell.highlighted:not(.selected):not(.range):not(.today).focused { + background-color: #495057; +} +[data-bs-theme=dark] .datepicker-cell.today:not(.selected) { + background-color: #4e555b; +} +[data-bs-theme=dark] .datepicker-cell.today:not(.selected):not(.disabled) { + color: #ffffff; +} +[data-bs-theme=dark] .datepicker-cell.today.focused:not(.selected) { + background-color: #4e555b; +} +[data-bs-theme=dark] .datepicker-cell.range-start:not(.selected), [data-bs-theme=dark] .datepicker-cell.range-end:not(.selected) { + background-color: #343a40; + color: #fff; +} +[data-bs-theme=dark] .datepicker-cell.range-start.focused:not(.selected), [data-bs-theme=dark] .datepicker-cell.range-end.focused:not(.selected) { + background-color: #495057; +} +[data-bs-theme=dark] .datepicker-cell.range { + border-radius: 0; + background-color: #495057; +} +[data-bs-theme=dark] .datepicker-cell.range:not(.disabled):not(.focused):not(.today):hover { + background-color: #6c757d; +} +[data-bs-theme=dark] .datepicker-cell.range.disabled { + color: #6c757d; +} +[data-bs-theme=dark] .datepicker-cell.range.focused { + background-color: #495057; +} + +.datepicker-cell.range-start { + border-radius: 0.25rem 0 0 0.25rem; +} + +.datepicker-cell.range-end { + border-radius: 0 0.25rem 0.25rem 0; +} + +.datepicker-view.datepicker-grid .datepicker-cell { + height: 4.5rem; + line-height: 4.5rem; +} + +.datepicker-input.in-edit { + border-color: var(--yafowil-accent-color, #0d6efd); +} + +.datepicker-input.in-edit:focus, .datepicker-input.in-edit:active { + box-shadow: 0 0 0.25em 0.25em rgba(102, 176, 255, 0.2); +} diff --git a/src/yafowil/widget/datetime/resources/bootstrap5/timepicker.css b/src/yafowil/widget/datetime/resources/bootstrap5/timepicker.css new file mode 100644 index 0000000..c70f3db --- /dev/null +++ b/src/yafowil/widget/datetime/resources/bootstrap5/timepicker.css @@ -0,0 +1,47 @@ +.timepicker-dropdown { + position: absolute; + margin-top: 4px; + display: none; + z-index: 1000; +} + +.timepicker-container .cell { + cursor: pointer; + padding: 5px; + border-radius: 3px; + user-select: none; +} +.timepicker-container .cell:hover { + background: #f9f9f9; +} +.timepicker-container .cell.selected { + background-color: var(--yafowil-accent-color, #0d6efd); + color: var(--yafowil-accent-font-color, #fff); + font-weight: 600; +} +.timepicker-container .timepicker-hours .hours-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; +} +.timepicker-container .timepicker-hours div.am, .timepicker-container .timepicker-hours div.pm { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; +} +.timepicker-container .timepicker-hours div.am { + border-bottom: 1px solid #e7e7e7; +} +.timepicker-container .timepicker-hours span.am, .timepicker-container .timepicker-hours span.pm { + font-size: 10px; + float: left; + position: relative; + top: 25px; + padding: 0 6px; + font-weight: 600; +} +.timepicker-container .timepicker-minutes .minutes-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; +} diff --git a/src/yafowil/widget/datetime/resources/bootstrap5/widget.js b/src/yafowil/widget/datetime/resources/bootstrap5/widget.js new file mode 100644 index 0000000..6854e39 --- /dev/null +++ b/src/yafowil/widget/datetime/resources/bootstrap5/widget.js @@ -0,0 +1,481 @@ +var yafowil_datetime = (function (exports, $) { + 'use strict'; + + class DatepickerWidget extends Datepicker { + static initialize(context) { + $('input.datepicker', context).each(function() { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new DatepickerWidget(elem, elem.data('date-locale')); + }); + } + constructor(elem, locale, opts={}) { + let opts_ = { + language: locale, + orientation: 'bottom', + buttonClass: 'btn', + todayHighlight: true, + autohide: true + }; + let lower_edge = elem.offset().top + elem.outerHeight() + 250; + if (lower_edge > $(document).height()) { + opts_.orientation = "top"; + } + let locale_options = DatepickerWidget.locale_options; + Object.assign(opts_, locale_options[locale] || locale_options.en); + Object.assign(opts_, opts); + super(elem[0], opts_); + elem.data('yafowil-datepicker', this); + this.elem = elem; + this.adapt(); + this.trigger = $(``) + .addClass('datepicker-trigger btn btn-outline-secondary') + .text('...') + .insertAfter(elem); + this.toggle_picker = this.toggle_picker.bind(this); + this.trigger + .off('mousedown touchstart', this.toggle_picker) + .on('mousedown touchstart', this.toggle_picker); + this.trigger.on('click', (e) => {e.preventDefault();}); + this.elem.on('changeDate', () => { + this.elem.trigger('change'); + }); + let created_event = $.Event('datepicker_created', {widget: this}); + this.elem.trigger(created_event); + } + adapt() { + const p_el = $(this.picker.element); + $('.datepicker-picker', p_el).addClass('card'); + const header = $('.datepicker-header', p_el).addClass('card-header bg-primary'); + $('.btn', header).addClass('btn-primary'); + $('.datepicker-main', p_el).addClass('card-body'); + } + unload() { + this.trigger.off('mousedown touchstart', this.toggle_picker); + this.elem.off('focus', this.prevent_hide); + } + toggle_picker(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (this.picker.active || this.active) { + this.hide(); + this.elem.blur(); + } else { + this.show(); + } + } + } + DatepickerWidget.locale_options = { + en: { + weekStart: 1, + format: 'mm.dd.yyyy' + }, + de: { + weekStart: 1, + format: 'dd.mm.yyyy' + } + }; + function datepicker_on_array_add(inst, context) { + DatepickerWidget.initialize(context); + } + function register_datepicker_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', datepicker_on_array_add); + } + + class TimepickerButton { + constructor(elem) { + this.elem = elem; + } + get selected() { + return this.elem.hasClass('selected'); + } + set selected(value) { + if (value) { + this.elem.addClass('selected'); + } else { + this.elem.removeClass('selected'); + } + } + } + class TimepickerButtonContainer { + constructor(picker, elem) { + this.picker = picker; + this.elem = elem; + this.children = []; + } + unload_all() { + for (let child of this.children) { + child.elem.off('click', child.on_click); + } + } + unselect_all() { + for (let child of this.children) { + child.selected = false; + } + } + } + class TimepickerHour extends TimepickerButton { + constructor(hours, container, value, period) { + super($('') + .addClass('cell') + .text(new String(value).padStart(2, '0')) + .appendTo(container) + ); + this.hours = hours; + this.picker = hours.picker; + this.period = period; + this.on_click = this.on_click.bind(this); + this.elem.on('click', this.on_click); + } + on_click(e) { + let hour = this.elem.text(); + this.hours.unselect_all(); + this.selected = true; + this.picker.period = this.period; + this.picker.hour = hour; + } + } + class TimepickerMinute extends TimepickerButton { + constructor(minutes, container, value) { + super($('') + .addClass('cell') + .text(new String(value).padStart(2, '0')) + .appendTo(container) + ); + this.minutes = minutes; + this.picker = minutes.picker; + this.on_click = this.on_click.bind(this); + this.elem.on('click', this.on_click); + } + on_click(e) { + this.minutes.unselect_all(); + this.selected = true; + this.picker.minute = this.elem.text(); + } + } + class TimepickerHours extends TimepickerButtonContainer { + constructor(picker, container) { + super(picker, $('').addClass('card-body hours-content')); + if (picker.clock === 24) { + this.create_clock_24(); + } else if (picker.clock === 12) { + this.create_clock_12(); + } + let header = $('') + .addClass('header card-header bg-primary text-white') + .text(picker.translate('hour')); + $('') + .addClass('card timepicker-hours') + .append(header) + .append(this.elem) + .appendTo(container); + } + create_clock_24() { + for (let i = 0; i < 24; i++) { + this.children.push(new TimepickerHour(this, this.elem, i)); + } + } + create_clock_12() { + $('').addClass('am').text('A.M.').appendTo(this.elem); + let hours_am = $('').addClass('am').appendTo(this.elem); + $('').addClass('pm').text('P.M.').appendTo(this.elem); + let hours_pm = $('').addClass('pm').appendTo(this.elem); + for (let i = 0; i < 12; i++) { + this.children.push(new TimepickerHour(this, hours_am, i, 'AM')); + } + for (let i = 0; i < 12; i++) { + this.children.push(new TimepickerHour(this, hours_pm, i, 'PM')); + } + this.elem.css('grid-template-rows', '1fr 1fr'); + this.elem.css('grid-template-columns', '1fr 1fr'); + } + } + class TimepickerMinutes extends TimepickerButtonContainer { + constructor(picker, container, step) { + super(picker, $('').addClass('card-body minutes-content')); + this.step = step; + let count = 60 / step; + if (count <= 32) { + let cols = '1fr '.repeat(Math.ceil(count / 4)); + this.elem.css( + 'grid-template-columns', + cols + ); + } else { + this.elem.css( + 'grid-template-columns', + '1fr '.repeat(10) + ); + if (picker.clock === 24) { + $('div.hours-content', picker.dd_elem).css({ + 'grid-template-columns': '1fr 1fr 1fr 1fr', + 'width': '110px' + }); + } else { + $('div.am, div.pm', picker.dd_elem).css({ + 'grid-template-columns': '1fr 1fr 1fr 1fr' + }); + } + } + for (let i = 0; i < count; i++) { + this.children.push(new TimepickerMinute(this, this.elem, i * step)); + } + let header = $('') + .addClass('header card-header bg-primary text-white') + .text(picker.translate('minute')); + $('') + .addClass('card timepicker-minutes') + .append(header) + .append(this.elem) + .appendTo(container); + } + } + class TimepickerWidget { + static initialize(context) { + $('input.timepicker', context).each(function() { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + elem.attr('spellcheck', false); + new TimepickerWidget(elem, { + language: elem.data('time-locale'), + clock: elem.data('time-clock'), + step: elem.data('time-minutes_step') + }); + }); + } + constructor(elem, opts) { + elem.data('yafowil-timepicker', this); + this.elem = elem; + this.language = opts.language || 'en'; + this.clock = opts.clock || 24; + this.period = null; + this.hour = ''; + this.minute = ''; + if (opts.step <= 0 || opts.step > 60 || typeof opts.step !== 'number') { + this.step = 5; + } else { + this.step = opts.step; + } + this.trigger_elem = $('') + .addClass('timepicker-trigger btn btn-outline-secondary') + .text('...') + .insertAfter(elem); + let dd_elem = this.dd_elem = $('') + .addClass('timepicker-dropdown') + .insertAfter(elem.closest('.input-group')); + let dd_container = $('') + .addClass('timepicker-container card-group shadow') + .appendTo(dd_elem); + this.hours = new TimepickerHours(this, dd_container); + this.minutes = new TimepickerMinutes(this, dd_container, this.step); + this.validate(); + this.place = this.place.bind(this); + this.show_dropdown = this.show_dropdown.bind(this); + this.elem.on('focus', this.show_dropdown); + this.toggle_dropdown = this.toggle_dropdown.bind(this); + this.trigger_elem.on('click', this.toggle_dropdown); + this.hide_dropdown = this.hide_dropdown.bind(this); + $(document).on('click', this.hide_dropdown); + this.on_keypress = this.on_keypress.bind(this); + this.elem.on('keypress', this.on_keypress); + this.validate = this.validate.bind(this); + this.elem.on('keyup', this.validate); + let created_event = $.Event('timepicker_created', {widget: this}); + this.elem.trigger(created_event); + } + unload() { + $(document).off('click', this.hide_dropdown); + this.hours.unload_all(); + this.minutes.unload_all(); + } + get hour() { + return this._hour; + } + set hour(hour) { + this._hour = hour; + this.set_time(); + } + get minute() { + return this._minute; + } + set minute(minute) { + this._minute = minute; + this.set_time(); + } + place() { + let offset = this.elem.offset().left - this.elem.parent().offset().left; + this.dd_elem.css('left', `${offset}px`); + let offset_left = this.elem.offset().left, + offset_top = this.elem.offset().top, + dd_width = this.dd_elem.outerWidth(), + elem_width = this.elem.outerWidth(); + let lower_edge = offset_top + this.elem.outerHeight() + 250; + let right_edge = offset_left + dd_width; + this.dd_elem.css('transform', 'translateX(0px)'); + if (lower_edge > $(document).height()) { + let height = this.dd_elem.outerHeight() + 9; + this.dd_elem.css('top', `-${height}px`); + } + if (offset_left + elem_width - dd_width < 0) { + let leftx = right_edge - $(window).width(); + this.dd_elem.css('transform', `translateX(-${leftx}px)`); + } else if (right_edge > $(window).width()) { + this.dd_elem + .css('transform', + `translateX(calc(-100% + ${elem_width}px))`); + } + } + set_time() { + if (this.hour === '' || this.minute === '') { + return; + } + if (this.clock === 24) { + this.elem.val(`${this.hour}:${this.minute}`); + } else if (this.clock === 12) { + this.elem.val(`${this.hour}:${this.minute}${this.period}`); + } + this.elem.trigger('change'); + this.hour = ''; + this.minute = ''; + this.dd_elem.hide(); + } + hide_dropdown(e) { + if (e.target !== this.elem[0] && e.target !== this.trigger_elem[0]) { + if ($(e.target).closest(this.dd_elem).length === 0) { + this.dd_elem.hide(); + } + } + } + show_dropdown(e) { + this.dd_elem.show(); + } + toggle_dropdown(e) { + e.preventDefault(); + this.dd_elem.toggle(); + } + on_keypress(e) { + e.preventDefault(); + let val = this.elem.val(); + let cursor_pos = e.target.selectionStart; + if (e.key === 'Enter') { + this.dd_elem.hide(); + this.elem.blur(); + } + if (cursor_pos <= 4) { + if (e.key.match(new RegExp('[0-9]'))) { + this.elem.val(val + e.key); + if (cursor_pos === 2) { + this.elem.val(`${val}:${e.key}`); + } + } + } else if (cursor_pos === 5 && this.clock === 12) { + let correct = ['a', 'A', 'p', 'P']; + if (correct.includes(e.key)) { + this.elem.val(val + e.key); + } + } else if (cursor_pos === 6 && this.clock === 12) { + if (e.key === 'm' || e.key === 'M') { + this.elem.val(val + e.key); + } + } } + validate() { + if (!this.elem.val()) return; + let time = this.elem.val(), + match_24 = new RegExp(/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/), + match_12 = new RegExp(/^(?:0?\d|1[012]):[0-5]\d[apAP][mM]$/), + hour = time.substr(0, 2), + hour_index = parseInt(hour), + minute = time.substr(3, 2), + minute_index = parseInt(minute) / this.step; + if (this.clock === 24) { + if (!time.match(match_24) || time.length < 5) { + return; + } + } else if (this.clock === 12) { + if (!time.match(match_12) || time.length < 7) { + return; + } + let period = time.substr(5).toUpperCase(); + this.period = (period === 'PM') ? 'PM' : 'AM'; + if (period === 'PM') hour_index += 12; + } + let hour_elem = this.hours.children[hour_index]; + hour_elem.on_click(); + let minute_elem = this.minutes.children[minute_index]; + if (minute_elem) { + minute_elem.on_click(); + } else { + this.minutes.unselect_all(); + } + } + translate(msgid) { + let locales = this.constructor.locales, + locale = locales[this.language] || locales.en; + return locale[msgid]; + } + } + TimepickerWidget.locales = { + en: { + hour: 'Hour', + minute: 'Minute' + }, + de: { + hour: 'Stunde', + minute: 'Minute' + } + }; + function timepicker_on_array_add(inst, context) { + TimepickerWidget.initialize(context); + } + function register_timepicker_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', timepicker_on_array_add); + } + + $(function() { + if (window.ts !== undefined) { + ts.ajax.register(DatepickerWidget.initialize, true); + ts.ajax.register(TimepickerWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(DatepickerWidget.initialize, true); + bdajax.register(TimepickerWidget.initialize, true); + } else { + DatepickerWidget.initialize(); + TimepickerWidget.initialize(); + } + register_datepicker_array_subscribers(); + register_timepicker_array_subscribers(); + }); + + exports.DatepickerWidget = DatepickerWidget; + exports.TimepickerButton = TimepickerButton; + exports.TimepickerButtonContainer = TimepickerButtonContainer; + exports.TimepickerHour = TimepickerHour; + exports.TimepickerHours = TimepickerHours; + exports.TimepickerMinute = TimepickerMinute; + exports.TimepickerMinutes = TimepickerMinutes; + exports.TimepickerWidget = TimepickerWidget; + exports.register_datepicker_array_subscribers = register_datepicker_array_subscribers; + exports.register_timepicker_array_subscribers = register_timepicker_array_subscribers; + + Object.defineProperty(exports, '__esModule', { value: true }); + + + window.yafowil = window.yafowil || {}; + window.yafowil.datetime = exports; + + + return exports; + +})({}, jQuery); diff --git a/src/yafowil/widget/datetime/resources/bootstrap5/widget.min.js b/src/yafowil/widget/datetime/resources/bootstrap5/widget.min.js new file mode 100644 index 0000000..06c8955 --- /dev/null +++ b/src/yafowil/widget/datetime/resources/bootstrap5/widget.min.js @@ -0,0 +1 @@ +var yafowil_datetime=function(e,t){"use strict";class i extends Datepicker{static initialize(e){t("input.datepicker",e).each((function(){let e=t(this);void 0!==window.yafowil_array&&window.yafowil_array.inside_template(e)||new i(e,e.data("date-locale"))}))}constructor(e,s,r={}){let l={language:s,orientation:"bottom",buttonClass:"btn",todayHighlight:!0,autohide:!0};e.offset().top+e.outerHeight()+250>t(document).height()&&(l.orientation="top");let a=i.locale_options;Object.assign(l,a[s]||a.en),Object.assign(l,r),super(e[0],l),e.data("yafowil-datepicker",this),this.elem=e,this.adapt(),this.trigger=t("").addClass("datepicker-trigger btn btn-outline-secondary").text("...").insertAfter(e),this.toggle_picker=this.toggle_picker.bind(this),this.trigger.off("mousedown touchstart",this.toggle_picker).on("mousedown touchstart",this.toggle_picker),this.trigger.on("click",(e=>{e.preventDefault()})),this.elem.on("changeDate",(()=>{this.elem.trigger("change")}));let o=t.Event("datepicker_created",{widget:this});this.elem.trigger(o)}adapt(){const e=t(this.picker.element);t(".datepicker-picker",e).addClass("card");const i=t(".datepicker-header",e).addClass("card-header bg-primary");t(".btn",i).addClass("btn-primary"),t(".datepicker-main",e).addClass("card-body")}unload(){this.trigger.off("mousedown touchstart",this.toggle_picker),this.elem.off("focus",this.prevent_hide)}toggle_picker(e){e.preventDefault(),e.stopPropagation(),this.picker.active||this.active?(this.hide(),this.elem.blur()):this.show()}}function s(e,t){i.initialize(t)}function r(){void 0!==window.yafowil_array&&window.yafowil_array.on_array_event("on_add",s)}i.locale_options={en:{weekStart:1,format:"mm.dd.yyyy"},de:{weekStart:1,format:"dd.mm.yyyy"}};class l{constructor(e){this.elem=e}get selected(){return this.elem.hasClass("selected")}set selected(e){e?this.elem.addClass("selected"):this.elem.removeClass("selected")}}class a{constructor(e,t){this.picker=e,this.elem=t,this.children=[]}unload_all(){for(let e of this.children)e.elem.off("click",e.on_click)}unselect_all(){for(let e of this.children)e.selected=!1}}class o extends l{constructor(e,i,s,r){super(t("").addClass("cell").text(new String(s).padStart(2,"0")).appendTo(i)),this.hours=e,this.picker=e.picker,this.period=r,this.on_click=this.on_click.bind(this),this.elem.on("click",this.on_click)}on_click(e){let t=this.elem.text();this.hours.unselect_all(),this.selected=!0,this.picker.period=this.period,this.picker.hour=t}}class d extends l{constructor(e,i,s){super(t("").addClass("cell").text(new String(s).padStart(2,"0")).appendTo(i)),this.minutes=e,this.picker=e.picker,this.on_click=this.on_click.bind(this),this.elem.on("click",this.on_click)}on_click(e){this.minutes.unselect_all(),this.selected=!0,this.picker.minute=this.elem.text()}}class n extends a{constructor(e,i){super(e,t("").addClass("card-body hours-content")),24===e.clock?this.create_clock_24():12===e.clock&&this.create_clock_12();let s=t("").addClass("header card-header bg-primary text-white").text(e.translate("hour"));t("").addClass("card timepicker-hours").append(s).append(this.elem).appendTo(i)}create_clock_24(){for(let e=0;e<24;e++)this.children.push(new o(this,this.elem,e))}create_clock_12(){t("").addClass("am").text("A.M.").appendTo(this.elem);let e=t("").addClass("am").appendTo(this.elem);t("").addClass("pm").text("P.M.").appendTo(this.elem);let i=t("").addClass("pm").appendTo(this.elem);for(let t=0;t<12;t++)this.children.push(new o(this,e,t,"AM"));for(let e=0;e<12;e++)this.children.push(new o(this,i,e,"PM"));this.elem.css("grid-template-rows","1fr 1fr"),this.elem.css("grid-template-columns","1fr 1fr")}}class h extends a{constructor(e,i,s){super(e,t("").addClass("card-body minutes-content")),this.step=s;let r=60/s;if(r<=32){let e="1fr ".repeat(Math.ceil(r/4));this.elem.css("grid-template-columns",e)}else this.elem.css("grid-template-columns","1fr ".repeat(10)),24===e.clock?t("div.hours-content",e.dd_elem).css({"grid-template-columns":"1fr 1fr 1fr 1fr",width:"110px"}):t("div.am, div.pm",e.dd_elem).css({"grid-template-columns":"1fr 1fr 1fr 1fr"});for(let e=0;e