forked from extern/egroupware
662ea62790
Now using regular calendar header. "Go" button and custom header styles removed. Changing the date in sidebox calendar immediately updates state.
672 lines
16 KiB
TypeScript
672 lines
16 KiB
TypeScript
/**
|
|
* EGroupware eTemplate2 - Date widget (WebComponent)
|
|
*
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package etemplate
|
|
* @subpackage api
|
|
* @link https://www.egroupware.org
|
|
* @author Nathan Gray
|
|
*/
|
|
|
|
|
|
import {css, html} from "@lion/core";
|
|
import {FormControlMixin, ValidateMixin} from "@lion/form-core";
|
|
import 'lit-flatpickr';
|
|
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
|
import {dateStyles} from "./DateStyles";
|
|
import {LitFlatpickr} from "lit-flatpickr";
|
|
import "flatpickr/dist/plugins/scrollPlugin.js";
|
|
import "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr";
|
|
import {holidays} from "./Holidays";
|
|
import flatpickr from "flatpickr";
|
|
import {egw} from "../../jsapi/egw_global";
|
|
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
|
|
|
|
// Request this year's holidays now
|
|
holidays(new Date().getFullYear());
|
|
|
|
// list of existing localizations from node_modules/flatpicker/dist/l10n directory:
|
|
const l10n = [
|
|
'ar', 'at', 'az', 'be', 'bg', 'bn', 'bs', 'cat', 'cs', 'cy', 'da', 'de', 'eo', 'es', 'et', 'fa', 'fi', 'fo',
|
|
'fr', 'ga', 'gr', 'he', 'hi', 'hr', 'hu', 'id', 'index', 'is', 'it', 'ja', 'ka', 'km', 'ko', 'kz', 'lt', 'lv', 'mk',
|
|
'mn', 'ms', 'my', 'nl', 'no', 'pa', 'pl', 'pt', 'ro', 'ru', 'si', 'sk', 'sl', 'sq', 'sr-cyr', 'sr', 'sv', 'th', 'tr',
|
|
'uk', 'uz', 'uz_latn', 'vn', 'zh-tw', 'zh',
|
|
];
|
|
const lang = egw ? <string>egw.preference('lang') || "" : "";
|
|
// only load localization, if we have one
|
|
if (l10n.indexOf(lang) >= 0)
|
|
{
|
|
import(egw.webserverUrl + "/node_modules/flatpickr/dist/l10n/" + lang + ".js").then(() =>
|
|
{
|
|
// @ts-ignore
|
|
flatpickr.localize(flatpickr.l10ns[lang]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse a date string into a Date object
|
|
* Time will be 00:00:00 UTC
|
|
*
|
|
* @param {string} dateString
|
|
* @returns {Date | undefined}
|
|
*/
|
|
export function parseDate(dateString, formatString?)
|
|
{
|
|
// First try the server format
|
|
if(dateString.substr(-1) === "Z")
|
|
{
|
|
try
|
|
{
|
|
let date = new Date(dateString);
|
|
if(date instanceof Date)
|
|
{
|
|
return date;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// Nope, that didn't parse directly
|
|
}
|
|
}
|
|
|
|
formatString = formatString || <string>(window.egw.preference("dateformat") || 'Y-m-d');
|
|
//@ts-ignore replaceAll() does not exist
|
|
formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-');
|
|
let parsedString = "";
|
|
switch(formatString)
|
|
{
|
|
case 'd-m-Y':
|
|
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
|
|
break;
|
|
case 'm-d-Y':
|
|
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(0, 2,)}/${dateString.slice(3, 5)}`;
|
|
break;
|
|
case 'Y-m-d':
|
|
parsedString = `${dateString.slice(0, 4)}/${dateString.slice(5, 7,)}/${dateString.slice(8, 10)}`;
|
|
break;
|
|
case 'Y-d-m':
|
|
parsedString = `${dateString.slice(0, 4)}/${dateString.slice(8, 10)}/${dateString.slice(5, 7)}`;
|
|
break;
|
|
case 'd-M-Y':
|
|
parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
|
|
break;
|
|
case 'Ymd':
|
|
// Not a preference option, but used by some dates
|
|
parsedString = `${dateString.slice(0, 4)}/${dateString.slice(4, 6,)}/${dateString.slice(6, 8)}`;
|
|
break;
|
|
default:
|
|
parsedString = '0000/00/00';
|
|
}
|
|
|
|
const [year, month, day] = parsedString.split('/').map(Number);
|
|
const parsedDate = new Date(`${year}-${month < 10 ? "0" + month : month}-${day < 10 ? "0" + day : day}T00:00:00Z`);
|
|
|
|
// Check if parsedDate is not `Invalid Date` or that the date has changed (e.g. the not existing 31.02.2020)
|
|
if(
|
|
year > 0 &&
|
|
month > 0 &&
|
|
day > 0 &&
|
|
parsedDate.getUTCDate() === day &&
|
|
parsedDate.getUTCMonth() === month - 1
|
|
)
|
|
{
|
|
return parsedDate;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* To parse a time into a Date object
|
|
* Date will be 1970-01-01, time is in UTC to avoid browser issues
|
|
*
|
|
* @param {string} timeString
|
|
* @returns {Date | undefined}
|
|
*/
|
|
export function parseTime(timeString)
|
|
{
|
|
// First try the server format
|
|
if(timeString.substr(-1) === "Z")
|
|
{
|
|
try
|
|
{
|
|
let date = new Date(timeString);
|
|
if(date instanceof Date)
|
|
{
|
|
return date;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// Nope, that didn't parse directly
|
|
}
|
|
}
|
|
|
|
let am_pm = timeString.endsWith("pm") || timeString.endsWith("PM") ? 12 : 0;
|
|
|
|
let strippedString = timeString.replaceAll(/[^0-9:]/gi, '');
|
|
|
|
if(timeString.startsWith("12") && strippedString != timeString)
|
|
{
|
|
// 12:xx am -> 0:xx, 12:xx pm -> 12:xx
|
|
am_pm -= 12;
|
|
}
|
|
|
|
const [hour, minute] = strippedString.split(':').map(Number);
|
|
|
|
const parsedDate = new Date("1970-01-01T00:00:00Z");
|
|
parsedDate.setUTCHours(hour + am_pm);
|
|
parsedDate.setUTCMinutes(minute);
|
|
|
|
// Check if parsedDate is not `Invalid Date` or that the time has changed
|
|
if(
|
|
parsedDate.getUTCHours() === hour + am_pm &&
|
|
parsedDate.getUTCMinutes() === minute
|
|
)
|
|
{
|
|
return parsedDate;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* To parse a date+time into an object
|
|
* Time is in UTC to avoid browser issues
|
|
*
|
|
* @param {string} dateTimeString
|
|
* @returns {Date | undefined}
|
|
*/
|
|
export function parseDateTime(dateTimeString)
|
|
{
|
|
// First try some common invalid values
|
|
if(dateTimeString === "" || dateTimeString === "0" || dateTimeString === 0)
|
|
{
|
|
return undefined;
|
|
}
|
|
|
|
// Next try server format
|
|
if(typeof dateTimeString === "string" && dateTimeString.substr(-1) === "Z" || !isNaN(dateTimeString))
|
|
{
|
|
if(!isNaN(dateTimeString) && parseInt(dateTimeString) == dateTimeString)
|
|
{
|
|
console.warn("Invalid date/time string: " + dateTimeString);
|
|
dateTimeString *= 1000;
|
|
}
|
|
try
|
|
{
|
|
let date = new Date(dateTimeString);
|
|
if(date instanceof Date)
|
|
{
|
|
return date;
|
|
}
|
|
}
|
|
catch(e)
|
|
{
|
|
// Nope, that didn't parse directly
|
|
}
|
|
}
|
|
|
|
const date = parseDate(dateTimeString);
|
|
|
|
let explody = dateTimeString.split(" ");
|
|
explody.shift();
|
|
const time = parseTime(explody.join(" "));
|
|
|
|
if(typeof date === "undefined" || typeof time === "undefined")
|
|
{
|
|
return undefined;
|
|
}
|
|
date.setUTCHours(time.getUTCHours());
|
|
date.setUTCMinutes(time.getUTCMinutes());
|
|
date.setUTCSeconds(time.getUTCSeconds());
|
|
return date;
|
|
}
|
|
|
|
/**
|
|
* Format dates according to user preference
|
|
*
|
|
* @param {Date} date
|
|
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
|
|
* set 'dateFormat': "Y-m-d" to specify a particular format
|
|
* @returns {string}
|
|
*/
|
|
export function formatDate(date : Date, options = {dateFormat: ""}) : string
|
|
{
|
|
if(!date || !(date instanceof Date))
|
|
{
|
|
return "";
|
|
}
|
|
let _value = '';
|
|
let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d';
|
|
|
|
let replace_map = {
|
|
d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(),
|
|
m: (date.getUTCMonth() < 9 ? "0" : "") + (date.getUTCMonth() + 1),
|
|
Y: "" + date.getUTCFullYear()
|
|
}
|
|
if(dateformat.indexOf("M") != -1)
|
|
{
|
|
replace_map["M"] = flatpickr.formatDate(date, "M");
|
|
}
|
|
|
|
let re = new RegExp(Object.keys(replace_map).join("|"), "g");
|
|
_value = dateformat.replace(re, function(matched)
|
|
{
|
|
return replace_map[matched];
|
|
});
|
|
return _value;
|
|
}
|
|
|
|
/**
|
|
* Format dates according to user preference
|
|
*
|
|
* @param {Date} date
|
|
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
|
|
* set 'timeFormat': "12" to specify a particular format
|
|
* @returns {string}
|
|
*/
|
|
export function formatTime(date : Date, options = {timeFormat: ""}) : string
|
|
{
|
|
if(!date || !(date instanceof Date))
|
|
{
|
|
return "";
|
|
}
|
|
let _value = '';
|
|
|
|
let timeformat = options.timeFormat || <string>window.egw.preference("timeformat") || "24";
|
|
let hours = (timeformat == "12" && date.getUTCHours() > 12) ? (date.getUTCHours() - 12) : date.getUTCHours();
|
|
if(timeformat == "12" && hours == 0)
|
|
{
|
|
// 00:00 is 12:00 am
|
|
hours = 12;
|
|
}
|
|
|
|
_value = (timeformat == "24" && hours < 10 ? "0" : "") + hours + ":" +
|
|
(date.getUTCMinutes() < 10 ? "0" : "") + (date.getUTCMinutes()) +
|
|
(timeformat == "24" ? "" : (date.getUTCHours() < 12 ? " am" : " pm"));
|
|
|
|
return _value;
|
|
}
|
|
|
|
/**
|
|
* Format date+time according to user preference
|
|
*
|
|
* @param {Date} date
|
|
* @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
|
|
* set 'dateFormat': "Y-m-d", 'timeFormat': "12" to specify a particular format
|
|
* @returns {string}
|
|
*/
|
|
export function formatDateTime(date : Date, options = {dateFormat: "", timeFormat: ""}) : string
|
|
{
|
|
if(!date || !(date instanceof Date))
|
|
{
|
|
return "";
|
|
}
|
|
return formatDate(date, options) + " " + formatTime(date, options);
|
|
}
|
|
|
|
export class Et2Date extends Et2InputWidget(FormControlMixin(ValidateMixin(LitFlatpickr)))
|
|
{
|
|
static get styles()
|
|
{
|
|
return [
|
|
...super.styles,
|
|
dateStyles,
|
|
css`
|
|
:host {
|
|
width: auto;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
static get properties()
|
|
{
|
|
return {
|
|
...super.properties,
|
|
/**
|
|
* Display the calendar inline instead of revealed as needed
|
|
*/
|
|
inline: {type: Boolean}
|
|
}
|
|
}
|
|
|
|
get slots()
|
|
{
|
|
return {
|
|
...super.slots,
|
|
input: () =>
|
|
{
|
|
const text = <Et2Textbox>document.createElement('et2-textbox');
|
|
text.type = "text";
|
|
return text;
|
|
}
|
|
}
|
|
}
|
|
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._onDayCreate = this._onDayCreate.bind(this);
|
|
}
|
|
|
|
|
|
connectedCallback()
|
|
{
|
|
super.connectedCallback();
|
|
this._updateValueOnChange = this._updateValueOnChange.bind(this);
|
|
this._handleShortcutButtonClick = this._handleShortcutButtonClick.bind(this);
|
|
}
|
|
|
|
disconnectedCallback()
|
|
{
|
|
super.disconnectedCallback();
|
|
this._inputNode.removeEventListener('change', this._onChange);
|
|
this.destroy();
|
|
}
|
|
|
|
/**
|
|
* Override parent to skip call to CDN
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async init()
|
|
{
|
|
if(this.locale)
|
|
{
|
|
// await loadLocale(this.locale);
|
|
}
|
|
if(typeof this._instance === "undefined")
|
|
{
|
|
this.initializeComponent();
|
|
|
|
// This has to go in init() rather than connectedCallback() because flatpickr creates its nodes in
|
|
// initializeComponent() so this._inputNode is not available before this
|
|
this._inputNode.setAttribute("slot", "input");
|
|
this._inputNode.addEventListener('change', this._updateValueOnChange);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override some flatpickr defaults to get things how we like it
|
|
*
|
|
* @see https://flatpickr.js.org/options/
|
|
* @returns {any}
|
|
*/
|
|
protected getOptions()
|
|
{
|
|
let options = super.getOptions();
|
|
|
|
options.altFormat = <string>this.egw()?.preference("dateformat") || "Y-m-d";
|
|
options.altInput = true;
|
|
options.allowInput = true;
|
|
options.dateFormat = "Y-m-dT00:00:00\\Z";
|
|
options.weekNumbers = true;
|
|
|
|
options.onDayCreate = this._onDayCreate;
|
|
|
|
this._localize(options);
|
|
|
|
if(this.inline)
|
|
{
|
|
options.inline = this.inline;
|
|
}
|
|
|
|
options.plugins = [
|
|
// Turn on scroll wheel support
|
|
// @ts-ignore TypeScript can't find scrollPlugin, but rollup does
|
|
new scrollPlugin(),
|
|
|
|
// Add "today" button
|
|
// @ts-ignore TypeScript can't find ShortcutButtonsPlugin, but rollup does
|
|
ShortcutButtonsPlugin({
|
|
button: [{label: this.egw().lang("Today")}],
|
|
onClick: this._handleShortcutButtonClick
|
|
})
|
|
];
|
|
|
|
// Listen for flatpickr change so we can update internal value, needed for validation
|
|
options.onChange = options.onReady = this._updateValueOnChange;
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Handle click on shortcut button(s) like "Today"
|
|
*
|
|
* @param button_index
|
|
* @param fp Flatpickr instance
|
|
*/
|
|
public _handleShortcutButtonClick(button_index, fp)
|
|
{
|
|
fp.setDate(new Date());
|
|
}
|
|
|
|
/**
|
|
* Set localize options & translations
|
|
* @param options
|
|
* @protected
|
|
*/
|
|
protected _localize(options)
|
|
{
|
|
let first_dow = <string>this.egw()?.preference('weekdaystarts', 'calendar') || 'Monday';
|
|
const DOW_MAP = {Monday: 1, Sunday: 0, Saturday: 6};
|
|
options.locale = {
|
|
firstDayOfWeek: DOW_MAP[first_dow]
|
|
};
|
|
}
|
|
|
|
set_value(value)
|
|
{
|
|
if(!value || value == 0 || value == "0")
|
|
{
|
|
value = "";
|
|
this.modelValue = "";
|
|
this.clear();
|
|
return;
|
|
}
|
|
let date;
|
|
// handle relative time (eg. "+3600" or "-3600") used in calendar
|
|
if (typeof value === 'string' && (value[0] === '+' || value[0] === '-'))
|
|
{
|
|
date = new Date(this.getValue());
|
|
date.set_value(date.getSeconds() + parseInt(value));
|
|
}
|
|
else
|
|
{
|
|
date = new Date(value);
|
|
}
|
|
// Handle timezone offset, flatpickr uses local time
|
|
let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
|
|
if(!this._instance)
|
|
{
|
|
this.defaultDate = formatDate;
|
|
}
|
|
else
|
|
{
|
|
this.setDate(formatDate);
|
|
}
|
|
}
|
|
|
|
getValue()
|
|
{
|
|
if(this.readonly || this.disabled)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Copied from flatpickr, since Et2InputWidget overwrote flatpickr.getValue()
|
|
if(!this._inputElement)
|
|
{
|
|
return '';
|
|
}
|
|
let value = this._inputElement.value;
|
|
|
|
// Empty field, return ''
|
|
if(!value)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Inline calendars need a slot
|
|
*
|
|
* @return {TemplateResult}
|
|
* @protected
|
|
*/
|
|
// eslint-disable-next-line class-methods-use-this
|
|
_inputGroupAfterTemplate()
|
|
{
|
|
return html`
|
|
<div class="input-group__after">
|
|
<slot name="after"></slot>
|
|
<slot/>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Change handler setting modelValue for validation
|
|
*
|
|
* @param _ev
|
|
* @returns
|
|
*/
|
|
_updateValueOnChange(_ev : Event)
|
|
{
|
|
this.modelValue = this.getValue();
|
|
}
|
|
|
|
/**
|
|
* Customise date rendering
|
|
*
|
|
* @see https://flatpickr.js.org/events/
|
|
*
|
|
* @param {Date} dates Currently selected date(s)
|
|
* @param dStr a string representation of the latest selected Date object by the user. The string is formatted as per the dateFormat option.
|
|
* @param inst flatpickr instance
|
|
* @param dayElement
|
|
* @protected
|
|
*/
|
|
protected _onDayCreate(dates : Date[], dStr : string, inst, dayElement : HTMLElement)
|
|
{
|
|
//@ts-ignore flatpickr adds dateObj to days
|
|
let date = new Date(dayElement.dateObj);
|
|
let f_date = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000);
|
|
if(!f_date)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let set_holiday = function(holidays, element)
|
|
{
|
|
let day_holidays = holidays[formatDate(f_date, {dateFormat: "Ymd"})]
|
|
let tooltip = '';
|
|
if(typeof day_holidays !== 'undefined' && day_holidays.length)
|
|
{
|
|
for(var i = 0; i < day_holidays.length; i++)
|
|
{
|
|
if(typeof day_holidays[i]['birthyear'] !== 'undefined')
|
|
{
|
|
element.classList.add('calBirthday');
|
|
}
|
|
else
|
|
{
|
|
element.classList.add('calHoliday');
|
|
}
|
|
tooltip += day_holidays[i]['name'] + "\n";
|
|
}
|
|
}
|
|
if(tooltip)
|
|
{
|
|
this.egw().tooltipBind(element, tooltip);
|
|
}
|
|
}.bind(this);
|
|
|
|
let holiday_list = holidays(f_date.getFullYear());
|
|
if(holiday_list instanceof Promise)
|
|
{
|
|
holiday_list.then((h) => {set_holiday(h, dayElement);});
|
|
}
|
|
else
|
|
{
|
|
set_holiday(holiday_list, dayElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the minimum allowed date
|
|
* @param {string | Date} min
|
|
*/
|
|
set_min(min : string | Date)
|
|
{
|
|
this.minDate = min;
|
|
}
|
|
|
|
set minDate(min : string | Date)
|
|
{
|
|
if(this._instance)
|
|
{
|
|
if(min)
|
|
{
|
|
// Handle timezone offset, flatpickr uses local time
|
|
let date = new Date(min);
|
|
let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
|
|
this._instance.set("minDate", formatDate)
|
|
}
|
|
else
|
|
{
|
|
this._instance.set("minDate", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the minimum allowed date
|
|
* @param {string | Date} max
|
|
*/
|
|
set_max(max : string | Date)
|
|
{
|
|
this.maxDate = max;
|
|
}
|
|
|
|
set maxDate(max : string | Date)
|
|
{
|
|
if(this._instance)
|
|
{
|
|
if(max)
|
|
{
|
|
// Handle timezone offset, flatpickr uses local time
|
|
let date = new Date(max);
|
|
let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
|
|
this._instance.set("maxDate", formatDate)
|
|
}
|
|
else
|
|
{
|
|
this._instance.set("maxDate", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override from flatpickr
|
|
* @returns {any}
|
|
*/
|
|
findInputField()
|
|
{
|
|
return this._inputNode;
|
|
}
|
|
|
|
/**
|
|
* The interactive (form) element.
|
|
* @protected
|
|
*/
|
|
get _inputNode() : HTMLElement
|
|
{
|
|
return this.querySelector('et2-textbox');
|
|
}
|
|
}
|
|
|
|
// @ts-ignore TypeScript is not recognizing that Et2Date is a LitElement
|
|
customElements.define("et2-date", Et2Date); |