- DateTime widget to handle dates with times

- Time & Date+time parser & formatter functions
- Date widget does not return a time
This commit is contained in:
nathan 2021-11-03 09:05:16 -06:00
parent 312bf62adc
commit 672ed0aa0e
7 changed files with 294 additions and 27 deletions

View File

@ -40,34 +40,26 @@ export function parseDate(dateString)
} }
} }
let formatString = <string>(egw.preference("dateformat") || 'Y-m-d'); let formatString = <string>(window.egw.preference("dateformat") || 'Y-m-d');
formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-'); formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-');
let parsedString = ""; let parsedString = "";
switch(formatString) switch(formatString)
{ {
case 'd-m-Y': case 'd-m-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice( parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
3,
5,
)}/${dateString.slice(0, 2)}`;
break; break;
case 'm-d-Y': case 'm-d-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice( parsedString = `${dateString.slice(6, 10)}/${dateString.slice(0, 2,)}/${dateString.slice(3, 5)}`;
0,
2,
)}/${dateString.slice(3, 5)}`;
break; break;
case 'Y-m-d': case 'Y-m-d':
parsedString = `${dateString.slice(0, 4)}/${dateString.slice( parsedString = `${dateString.slice(0, 4)}/${dateString.slice(5, 7,)}/${dateString.slice(8, 10)}`;
5, break;
7, case 'Y-d-m':
)}/${dateString.slice(8, 10)}`; parsedString = `${dateString.slice(0, 4)}/${dateString.slice(8, 10)}/${dateString.slice(5, 7)}`;
break; break;
case 'd-M-Y': case 'd-M-Y':
parsedString = `${dateString.slice(6, 10)}/${dateString.slice( parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
3, break;
5,
)}/${dateString.slice(0, 2)}`;
default: default:
parsedString = '0000/00/00'; parsedString = '0000/00/00';
} }
@ -89,6 +81,97 @@ export function parseDate(dateString)
return undefined; return undefined;
} }
/**
* To parse a time into the right format
* Date will be 1970-01-01
*
* @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;
timeString = timeString.replaceAll(/[^0-9:]/gi, '');
const [hour, minute] = timeString.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
*
* @param {string} dateTimeString
* @returns {Date | undefined}
*/
export function parseDateTime(dateTimeString)
{
// First try the server format
if(typeof dateTimeString === "string" && dateTimeString.substr(-1) === "Z" || !isNaN(dateTimeString))
{
if(!isNaN(dateTimeString) && parseInt(dateTimeString) == dateTimeString)
{
this.egw().debug("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 * Format dates according to user preference
* *
@ -97,9 +180,9 @@ export function parseDate(dateString)
* set 'dateFormat': "Y-m-d" to specify a particular format * set 'dateFormat': "Y-m-d" to specify a particular format
* @returns {string} * @returns {string}
*/ */
export function formatDate(date: Date, options): string export function formatDate(date : Date, options) : string
{ {
if (!date || !(date instanceof Date)) if(!date || !(date instanceof Date))
{ {
return ""; return "";
} }
@ -107,7 +190,7 @@ export function formatDate(date: Date, options): string
// Add timezone offset back in, or formatDate will lose those hours // Add timezone offset back in, or formatDate will lose those hours
let formatDate = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000); let formatDate = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000);
let dateformat = options.dateFormat || <string>egw.preference("dateformat") || 'Y-m-d'; let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d';
var replace_map = { var replace_map = {
d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(), d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(),
@ -115,13 +198,61 @@ export function formatDate(date: Date, options): string
Y: "" + date.getUTCFullYear() Y: "" + date.getUTCFullYear()
} }
var re = new RegExp(Object.keys(replace_map).join("|"), "gi"); var 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];
}); });
return _value; 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) : 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) : string
{
if(!date || !(date instanceof Date))
{
return "";
}
return formatDate(date, options) + " " + formatTime(date, options);
}
export class Et2Date extends Et2InputWidget(LionInputDatepicker) export class Et2Date extends Et2InputWidget(LionInputDatepicker)
{ {
static get styles() static get styles()
@ -196,6 +327,10 @@ export class Et2Date extends Et2InputWidget(LionInputDatepicker)
{ {
return null; return null;
} }
this.modelValue.setUTCHours(0);
this.modelValue.setUTCMinutes(0);
this.modelValue.setSeconds(0, 0);
return this.modelValue.toJSON(); return this.modelValue.toJSON();
} }

View File

@ -0,0 +1,69 @@
/**
* EGroupware eTemplate2 - Date+Time 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 {Et2Date, formatDateTime, parseDateTime} from "./Et2Date";
import {Unparseable} from "@lion/form-core";
export class Et2DateTime extends Et2Date
{
static get styles()
{
return [
...super.styles,
css`
:host([focused]) ::slotted(button), :host(:hover) ::slotted(button) {
display: inline-block;
}
::slotted(.calendar_button) {
border: none;
background: transparent;
margin-left: -20px;
display: none;
}
`,
];
}
static get properties()
{
return {
...super.properties
}
}
constructor()
{
super();
this.parser = parseDateTime;
this.formatter = formatDateTime;
}
getValue()
{
if(this.readOnly)
{
return null;
}
// The supplied value was not understandable, return null
if(this.modelValue instanceof Unparseable || !this.modelValue)
{
return null;
}
return this.modelValue.toJSON();
}
}
// @ts-ignore TypeScript is not recognizing that Et2DateTime is a LitElement
customElements.define("et2-datetime", Et2DateTime);

View File

@ -63,7 +63,7 @@ describe("Date widget", () =>
}); });
const tz_list = [ const tz_list = [
{name: "America/Edmonton", offset: 600}, {name: "America/Edmonton", offset: -600},
{name: "UTC", offset: 0}, {name: "UTC", offset: 0},
{name: "Australia/Adelaide", offset: 630} {name: "Australia/Adelaide", offset: 630}
]; ];
@ -85,8 +85,8 @@ describe("Date widget", () =>
// Use a Promise to wait for asychronous changes to the DOM // Use a Promise to wait for asychronous changes to the DOM
return Promise.resolve().then(() => return Promise.resolve().then(() =>
{ {
// Widget gives time as a string so we can send to server // Widget gives time as a string so we can send to server, but zeros the time
assert.equal(element.getValue(), test_time_string); assert.equal(element.getValue().substr(0, 11), test_time_string.substr(0, 11));
}); });
}); });

View File

@ -0,0 +1,59 @@
/**
* Test file for Etemplate date parsing
*/
import {assert, fixture} from '@open-wc/testing';
import {Et2Date, parseDate} from "../Et2Date";
import {html} from "lit-element";
import * as sinon from 'sinon';
describe("Date parsing", () =>
{
// Function under test
let parser = parseDate;
// Setup run before each test
beforeEach(async() =>
{
// Stub global egw for preference
// @ts-ignore
window.egw = {
preference: () => 'Y-m-d'
};
});
it("Handles server format", () =>
{
let test_string = '2021-09-22T19:22:00Z';
let test_date = new Date(test_string);
let parsed = parser(test_string);
// Can't compare results - different objects
//assert.equal(parsed, test_date);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
it("Handles Y-m-d", () =>
{
let test_string = '2021-09-22';
let test_date = new Date(2021, 8, 22, 0, 0, 0);
let parsed = parser(test_string);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
it("Handles Y.d.m", () =>
{
let test_string = '2021.22.09';
let test_date = new Date(2021, 8, 22, 0, 0, 0);
//@ts-ignore
window.egw = {
preference: () => 'Y.d.m'
};
let parsed = parser(test_string);
assert.equal(parsed.toJSON(), test_date.toJSON());
});
});

View File

@ -25,7 +25,7 @@ import '../jsapi/egw_json.js';
import {egwIsMobile} from "../egw_action/egw_action_common.js"; import {egwIsMobile} from "../egw_action/egw_action_common.js";
import './Et2Box/Et2Box'; import './Et2Box/Et2Box';
import './Et2Button/Et2Button'; import './Et2Button/Et2Button';
import './Et2Date/Et2Date'; import './Et2Date/Et2DateTime';
import './Et2Textarea/Et2Textarea'; import './Et2Textarea/Et2Textarea';
import './Et2Textbox/Et2Textbox'; import './Et2Textbox/Et2Textbox';
import './Et2Colorpicker/Et2Colorpicker'; import './Et2Colorpicker/Et2Colorpicker';

View File

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

View File

@ -165,7 +165,9 @@
</menulist> </menulist>
<description/> <description/>
<description value="Startdate" for="info_startdate"/> <description value="Startdate" for="info_startdate"/>
<et2-date statustext="when should the ToDo or Phonecall be started, it shows up from that date in the filter open or own open (startpage)" id="info_startdate" class="et2_fullWidth"></et2-date> <et2-datetime
statustext="when should the ToDo or Phonecall be started, it shows up from that date in the filter open or own open (startpage)"
id="info_startdate" class="et2_fullWidth"></et2-datetime>
</row> </row>
<row class="dialogHeader3"> <row class="dialogHeader3">
<description value="Contact"/> <description value="Contact"/>