Use flatpickr for date widget calendar

This commit is contained in:
nathan 2022-02-15 11:47:42 -07:00
parent 5ff64259ee
commit 8203eb3efd
5 changed files with 87 additions and 156 deletions

View File

@ -3,8 +3,12 @@
*/ */
import {css} from "@lion/core"; import {css} from "@lion/core";
import {colorsDefStyles} from "../Styles/colorsDefStyles";
import {cssImage} from "../Et2Widget/Et2Widget";
export const dateStyles = css` export const dateStyles = [
colorsDefStyles,
css`
:host { :host {
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
@ -13,4 +17,27 @@ export const dateStyles = css`
.overdue { .overdue {
color: red; // var(--whatever the theme color) color: red; // var(--whatever the theme color)
} }
`; input.flatpickr-input {
border: 1px solid;
border-color: var(--input-border-color);
color: var(--input-text-color);
padding-top: 4px;
padding-bottom: 4px;
}
input.flatpickr-input:hover {
background-image: ${cssImage("datepopup")};
background-repeat: no-repeat;
background-position-x: right;
background-position-y: 1px;
background-size: 18px;
}
`];
// The lit-flatpickr package uses a CDN, in violation of best practices
// I've downloaded it
const themeUrl = "api/js/etemplate/Et2Date/flatpickr_material_blue.css";
const styleElem = document.createElement('link');
styleElem.rel = 'stylesheet';
styleElem.type = 'text/css';
styleElem.href = themeUrl;
document.head.append(styleElem);

View File

@ -9,11 +9,11 @@
*/ */
import {css, html} from "@lion/core"; import {css} from "@lion/core";
import {LionInputDatepicker} from "@lion/input-datepicker"; import 'lit-flatpickr';
import {Unparseable} from "@lion/form-core";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import {dateStyles} from "./DateStyles"; import {dateStyles} from "./DateStyles";
import {LitFlatpickr} from "lit-flatpickr";
/** /**
@ -43,6 +43,7 @@ export function parseDate(dateString)
} }
let formatString = <string>(window.egw.preference("dateformat") || 'Y-m-d'); let formatString = <string>(window.egw.preference("dateformat") || 'Y-m-d');
//@ts-ignore replaceAll() does not exist
formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-'); formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-');
let parsedString = ""; let parsedString = "";
switch(formatString) switch(formatString)
@ -204,17 +205,14 @@ export function formatDate(date : Date, options = {dateFormat: ""}) : string
return ""; return "";
} }
let _value = ''; let _value = '';
// Add timezone offset back in, or formatDate will lose those hours
let formatDate = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000);
let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d'; let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d';
var replace_map = { let replace_map = {
d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(), d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(),
m: (date.getUTCMonth() < 9 ? "0" : "") + (date.getUTCMonth() + 1), m: (date.getUTCMonth() < 9 ? "0" : "") + (date.getUTCMonth() + 1),
Y: "" + date.getUTCFullYear() Y: "" + date.getUTCFullYear()
} }
var re = new RegExp(Object.keys(replace_map).join("|"), "gi"); let re = new RegExp(Object.keys(replace_map).join("|"), "gi");
_value = dateformat.replace(re, function(matched) _value = dateformat.replace(re, function(matched)
{ {
return replace_map[matched]; return replace_map[matched];
@ -270,22 +268,16 @@ export function formatDateTime(date : Date, options = {dateFormat: "", timeForma
return formatDate(date, options) + " " + formatTime(date, options); return formatDate(date, options) + " " + formatTime(date, options);
} }
export class Et2Date extends Et2InputWidget(LionInputDatepicker) export class Et2Date extends Et2InputWidget(LitFlatpickr)
{ {
static get styles() static get styles()
{ {
return [ return [
...super.styles, ...super.styles,
dateStyles, ...dateStyles,
css` css`
:host([focused]) ::slotted(button), :host(:hover) ::slotted(button) { :host {
display: inline-block; width: auto;
}
::slotted(.calendar_button) {
border: none;
background: transparent;
margin-left: -20px;
display: none;
} }
`, `,
]; ];
@ -301,36 +293,45 @@ export class Et2Date extends Et2InputWidget(LionInputDatepicker)
constructor() constructor()
{ {
super(); super();
this.parser = parseDate;
this.formatter = formatDate;
}
connectedCallback() // Override some flatpickr defaults how we like it
{ this.altFormat = this.egw().preference("dateformat") || "Y-m-d";
super.connectedCallback(); this.altInput = true;
this.allowInput = true;
this.dateFormat = "Y-m-d\T00:00:00\Z";
this.weekNumbers = true;
} }
/** /**
* @param {Date} modelValue * Override parent to skip call to CDN
* @returns {Promise<void>}
*/ */
// eslint-disable-next-line class-methods-use-this async init()
serializer(modelValue : Date)
{ {
// isValidDate() is hidden inside LionInputDate, and not exported if(this.locale)
// @ts-ignore Can't call isNan(Date), but we're just checking
if(!(modelValue instanceof Date) || isNaN(modelValue))
{ {
return ''; // await loadLocale(this.locale);
} }
// modelValue is localized, so we take the timezone offset in milliseconds and subtract it this.initializeComponent();
// before converting it to ISO string.
const offset = modelValue.getTimezoneOffset() * 60000;
return new Date(modelValue.getTime() - offset).toJSON().replace(/\.\d{3}Z$/, 'Z');
} }
set_value(value) set_value(value)
{ {
this.modelValue = this.parser(value); if(!value || value == 0 || value == "0")
{
value = '';
}
// Handle timezone offset, flatpickr uses local time
let date = new Date(value);
let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
if(!this._instance)
{
this.defaultDate = formatDate;
}
else
{
this.setDate(formatDate);
}
} }
getValue() getValue()
@ -340,108 +341,20 @@ export class Et2Date extends Et2InputWidget(LionInputDatepicker)
return null; return null;
} }
// Copied from flatpickr, since Et2InputWidget overwrote flatpickr.getValue()
if(!this._inputElement)
{
return '';
}
let value = this._inputElement.value;
// Empty field, return '' // Empty field, return ''
if(!this.modelValue) if(!value)
{ {
return ''; return '';
} }
// The supplied value was not understandable, return null return value;
if(this.modelValue instanceof Unparseable || !this.modelValue)
{
return null;
}
this.modelValue.setUTCHours(0);
this.modelValue.setUTCMinutes(0);
this.modelValue.setSeconds(0, 0);
return this.modelValue.toJSON();
}
get _overlayReferenceNode()
{
return this.getInputNode();
}
/**
* @override Configures OverlayMixin
* @desc overrides default configuration options for this component
* @returns {Object}
*/
_defineOverlayConfig()
{
this.hasArrow = false;
if(window.innerWidth >= 600)
{
return {
hidesOnOutsideClick: true,
placementMode: 'local',
popperConfig: {
placement: 'bottom-end',
},
};
}
return super.withBottomSheetConfig();
}
/**
* The LionCalendar shouldn't know anything about the modelValue;
* it can't handle Unparseable dates, but does handle 'undefined'
* @param {?} modelValue
* @returns {Date|undefined} a 'guarded' modelValue
*/
static __getSyncDownValue(modelValue)
{
if(!(modelValue instanceof Date))
{
return undefined;
}
const offset = modelValue.getTimezoneOffset() * 60000;
return new Date(modelValue.getTime() + offset);
}
/**
* Overriding from parent for read-only
*
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate()
{
if(this.readOnly)
{
return this.formattedValue;
}
else
{
return super._inputGroupInputTemplate();
}
}
/**
* Overriding parent to add class to button, and use an image instead of unicode emoji
*/
// eslint-disable-next-line class-methods-use-this
_invokerTemplate()
{
if(this.readOnly)
{
return '';
}
let img = this.egw() ? this.egw().image("calendar") || '' : '';
return html`
<button
type="button"
class="calendar_button"
@click="${this.__openCalendarOverlay}"
id="${this.__invokerId}"
aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
>
<img src="${img}" style="width:16px"/>
</button>
`;
} }
} }

View File

@ -9,9 +9,8 @@
*/ */
import {css, html} from "@lion/core"; import {css} from "@lion/core";
import {Et2Date, formatDateTime, parseDateTime} from "./Et2Date"; import {Et2Date} from "./Et2Date";
import {Unparseable} from "@lion/form-core";
export class Et2DateTime extends Et2Date export class Et2DateTime extends Et2Date
@ -44,24 +43,15 @@ export class Et2DateTime extends Et2Date
constructor() constructor()
{ {
super(); super();
this.parser = parseDateTime;
this.formatter = formatDateTime;
}
getValue() // Configure flatpickr
{ let dateFormat = (this.egw().preference("dateformat") || "Y-m-d");
if(this.readOnly) let timeFormat = ((<string>window.egw.preference("timeformat") || "24") == "24" ? "H:i" : "h:i K");
{ this.altFormat = dateFormat + " " + timeFormat;
return null; this.enableTime = true;
} this.time_24hr = this.egw().preference("timeformat", "common") == "24";
this.dateFormat = "Y-m-dTH:i:00\\Z";
// The supplied value was not understandable, return null this.defaultHour = new Date().getHours();
if(this.modelValue instanceof Unparseable || !this.modelValue)
{
return null;
}
return this.modelValue.toJSON();
} }
} }

View File

@ -283,5 +283,5 @@ class Date extends Transformer
} }
\EGroupware\Api\Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Date', \EGroupware\Api\Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\Date',
array('et2-date', 'et2-datetime', 'time_or_date') array('et2-date', 'et2-date-time', 'time_or_date')
); );

View File

@ -63,6 +63,7 @@
"@lion/select": "^0.14.7", "@lion/select": "^0.14.7",
"@lion/textarea": "^0.13.4", "@lion/textarea": "^0.13.4",
"jquery-ui-timepicker-addon": "^1.6.3", "jquery-ui-timepicker-addon": "^1.6.3",
"lit-flatpickr": "^0.3.0",
"sortablejs": "^1.14.0" "sortablejs": "^1.14.0"
}, },
"engines": { "engines": {