Merge pull request #62 from getformwork/feature/improved-date-formats

Improve date formats handling
This commit is contained in:
Giuseppe Criscione 2020-12-06 16:45:36 +01:00 committed by GitHub
commit c208789b43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 246 additions and 40 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,19 @@ import Utils from './utils';
export default function DatePicker(input, options) {
var defaults = {
dayLabels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
monthLabels: ['January', 'February', 'March', 'April', 'May', 'June', 'July' ,'August', 'September', 'October', 'November', 'December'],
weekStarts: 0,
todayLabel: 'Today',
format: 'YYYY-MM-DD'
format: 'YYYY-MM-DD',
labels: {
today: 'Today',
weekdays: {
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
},
months: {
long: ['January', 'February', 'March', 'April', 'May', 'June', 'July' ,'August', 'September', 'October', 'November', 'December'],
short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}
}
};
var today = new Date();
@ -86,9 +94,6 @@ export default function DatePicker(input, options) {
// Return x mod y (always rounded downwards, differs from x % y which is the remainder)
return x - y * Math.floor(x / y);
},
pad: function (num) {
return num.toString().length === 1 ? '0' + num : num;
},
isValidDate: function (date) {
return date && !isNaN(Date.parse(date));
},
@ -98,26 +103,115 @@ export default function DatePicker(input, options) {
daysInMonth: function (month, year) {
return month === 1 && this.isLeapYear(year) ? 29 : this._daysInMonth[month];
},
formatDateTime: function (date) {
var format = options.format;
var year = date.getFullYear();
var month = date.getMonth() + 1;
weekStart: function (date, firstDay) {
var day = date.getDate();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
var am = hours < 12;
if (format.indexOf('a') > -1) {
hours = dateHelpers.mod(hours, 12) > 0 ? dateHelpers.mod(hours, 12) : 12;
if (typeof firstDay === 'undefined') {
firstDay = options.weekStarts;
}
return format.replace('YYYY', year)
.replace('YY', year.toString().substr(-2))
.replace('MM', dateHelpers.pad(month))
.replace('DD', dateHelpers.pad(day))
.replace('hh', dateHelpers.pad(hours))
.replace('mm', dateHelpers.pad(minutes))
.replace('ss', dateHelpers.pad(seconds))
.replace('a', am ? 'AM' : 'PM');
day -= this.mod(date.getDay() - firstDay, 7);
return new Date(date.getFullYear(), date.getMonth(), day);
},
weekNumberingYear: function (date) {
var year = date.getFullYear();
var thisYearFirstWeekStart = this.weekStart(new Date(year, 0, 4), 1);
var nextYearFirstWeekStart = this.weekStart(new Date(year + 1, 0, 4), 1);
if (date.getTime() >= nextYearFirstWeekStart.getTime()) {
return year + 1;
} else if (date.getTime() >= thisYearFirstWeekStart.getTime()) {
return year;
}
return year - 1;
},
weekOfYear: function (date) {
var weekNumberingYear = this.weekNumberingYear(date);
var firstWeekStart = this.weekStart(new Date(weekNumberingYear, 0, 4), 1);
var weekStart = this.weekStart(date, 1);
return Math.round((weekStart.getTime() - firstWeekStart.getTime()) / 604800000) + 1;
},
formatDateTime: function (date, format) {
var regex = /\[([^\]]*)\]|[YR]{4}|uuu|[YR]{2}|[MD]{1,4}|[WHhms]{1,2}|[AaZz]/g;
if (typeof format === 'undefined') {
format = options.format;
}
function pad(num, length) {
var result = num.toString();
while (result.length < length) {
result = '0' + result;
}
return result;
}
function splitTimezoneOffset(offset) {
// Note that the offset returned by Date.getTimezoneOffset()
// is positive if behind UTC and negative if ahead UTC
var sign = offset > 0 ? '-' : '+';
var hours = Math.floor(Math.abs(offset) / 60);
var minutes = Math.abs(offset) % 60;
return [sign + pad(hours, 2), pad(minutes, 2)];
}
return format.replace(regex, function (match, $1) {
switch (match) {
case 'YY':
return date.getFullYear().toString().substr(-2);
case 'YYYY':
return date.getFullYear();
case 'M':
return date.getMonth() + 1;
case 'MM':
return pad(date.getMonth() + 1, 2);
case 'MMM':
return options.labels.months.short[date.getMonth()];
case 'MMMM':
return options.labels.months.long[date.getMonth()];
case 'D':
return date.getDate();
case 'DD':
return pad(date.getDate(), 2);
case 'DDD':
return options.labels.weekdays.short[dateHelpers.mod(date.getDay() + options.weekStarts, 7)];
case 'DDDD':
return options.labels.weekdays.long[dateHelpers.mod(date.getDay() + options.weekStarts, 7)];
case 'W':
return dateHelpers.weekOfYear(date);
case 'WW':
return pad(dateHelpers.weekOfYear(date), 2);
case 'RR':
return dateHelpers.weekNumberingYear(date).toString().substr(-2);
case 'RRRR':
return dateHelpers.weekNumberingYear(date);
case 'H':
return dateHelpers.mod(date.getHours(), 12) || 12;
case 'HH':
return pad(dateHelpers.mod(date.getHours(), 12) || 12, 2);
case 'h':
return date.getHours();
case 'hh':
return pad(date.getHours(), 2);
case 'm':
return date.getMinutes();
case 'mm':
return pad(date.getMinutes(), 2);
case 's':
return date.getSeconds();
case 'ss':
return pad(date.getSeconds(), 2);
case 'uuu':
return pad(date.getMilliseconds(), 3);
case 'A':
return date.getHours() < 12 ? 'AM' : 'PM';
case 'a':
return date.getHours() < 12 ? 'am' : 'pm';
case 'Z':
return splitTimezoneOffset(date.getTimezoneOffset()).join(':');
case 'z':
return splitTimezoneOffset(date.getTimezoneOffset()).join('');
default:
return $1 || match;
}
});
}
};
@ -230,7 +324,7 @@ export default function DatePicker(input, options) {
function generateCalendar() {
calendar = document.createElement('div');
calendar.className = 'calendar';
calendar.innerHTML = '<div class="calendar-buttons"><button type="button" class="prevMonth"><i class="i-chevron-left"></i></button><button class="currentMonth">' + options.todayLabel + '</button><button type="button" class="nextMonth"><i class="i-chevron-right"></i></button></div><div class="calendar-separator"></div><table class="calendar-table"></table>';
calendar.innerHTML = '<div class="calendar-buttons"><button type="button" class="prevMonth"><i class="i-chevron-left"></i></button><button class="currentMonth">' + options.labels.today + '</button><button type="button" class="nextMonth"><i class="i-chevron-right"></i></button></div><div class="calendar-separator"></div><table class="calendar-table"></table>';
document.body.appendChild(calendar);
$('.currentMonth', calendar).addEventListener('mousedown', function (event) {
@ -272,7 +366,7 @@ export default function DatePicker(input, options) {
var num = 1;
var firstDay = new Date(year, month, 1).getDay();
var monthLength = dateHelpers.daysInMonth(month, year);
var monthName = options.monthLabels[month];
var monthName = options.labels.months.long[month];
var start = dateHelpers.mod(firstDay - options.weekStarts, 7);
var html = '';
html += '<tr><th class="calendar-header" colspan="7">';
@ -281,7 +375,7 @@ export default function DatePicker(input, options) {
html += '<tr>';
for (i = 0; i < 7; i++ ){
html += '<td class="calendar-header-day">';
html += options.dayLabels[dateHelpers.mod(i + options.weekStarts, 7)];
html += options.labels.weekdays.short[dateHelpers.mod(i + options.weekStarts, 7)];
html += '</td>';
}
html += '</tr><tr>';

View File

@ -6,6 +6,7 @@ use Formwork\Admin\Admin;
use Formwork\Admin\AdminTrait;
use Formwork\Admin\Security\CSRFToken;
use Formwork\Admin\Users\User;
use Formwork\Admin\Utils\DateFormats;
use Formwork\Admin\View\View;
use Formwork\Core\Formwork;
use Formwork\Core\Site;
@ -68,14 +69,13 @@ abstract class AbstractController
'appConfig' => JSON::encode([
'baseUri' => $this->panelUri(),
'DatePicker' => [
'dayLabels' => $this->label('date.weekdays.short'),
'monthLabels' => $this->label('date.months.long'),
'weekStarts' => $this->option('date.week_starts'),
'todayLabel' => $this->label('date.today'),
'format' => strtr(
$this->option('date.format'),
['Y' => 'YYYY', 'm' => 'MM', 'd' => 'DD', 'H' => 'hh', 'i' => 'mm', 's' => 'ss', 'A' => 'a']
)
'weekStarts' => $this->option('date.week_starts'),
'format' => DateFormats::formatToPattern(Formwork::instance()->option('date.format')),
'labels' => [
'today' => $this->label('date.today'),
'weekdays' => ['long' => $this->label('date.weekdays.long'), 'short' => $this->label('date.weekdays.short')],
'months' => ['long' => $this->label('date.months.long'), 'short' => $this->label('date.months.short')]
]
]
])
];

View File

@ -2,8 +2,45 @@
namespace Formwork\Admin\Utils;
use Formwork\Admin\Admin;
use DateTime;
class DateFormats
{
/**
* Characters used in formats accepted by date()
*
* @var string
*/
protected const DATE_FORMAT_CHARACTERS = 'AaBcDdeFgGHhIijlLMmnNoOpPrsSTtUuvWwyYzZ';
/**
* Regex used to parse formats accepted by date()
*
* @var string
*/
protected const DATE_FORMAT_REGEX = '/((?:\\\\[A-Za-z])+)|[' . self::DATE_FORMAT_CHARACTERS . ']/';
/**
* Regex used to parse date patterns like 'DD/MM/YYYY hh:mm:ss'
*
* @var string
*/
protected const PATTERN_REGEX = '/(?:\[([^\]]+)\])|[YR]{4}|uuu|[YR]{2}|[MD]{1,4}|[WHhms]{1,2}|[AaZz]/';
/**
* Array used to translate pattern tokens to their date() format counterparts
*
* @var array
*/
protected const PATTERN_TO_DATE_FORMAT = [
'YY' => 'y', 'YYYY' => 'Y', 'M' => 'n', 'MM' => 'm', 'MMM' => 'M', 'MMMM' => 'F',
'D' => 'j', 'DD' => 'd', 'DDD' => 'D', 'DDDD' => 'l', 'W' => 'W', 'WW' => 'W',
'RR' => 'o', 'RRRR' => 'o', 'H' => 'g', 'HH' => 'h', 'h' => 'G', 'hh' => 'H',
'm' => 'i', 'mm' => 'i', 's' => 's', 'ss' => 's', 'uuu' => 'v', 'A' => 'A',
'a' => 'a', 'Z' => 'P', 'z' => 'O'
];
/**
* Return common date formats
*/
@ -11,7 +48,7 @@ class DateFormats
{
$formats = [];
foreach (['d/m/Y', 'm/d/Y', 'Y-m-d', 'd-m-Y'] as $format) {
$formats[$format] = date($format) . ' (' . $format . ')';
$formats[$format] = date($format) . ' (' . static::formatToPattern($format) . ')';
}
return $formats;
}
@ -23,7 +60,7 @@ class DateFormats
{
$formats = [];
foreach (['H:i', 'h:i A'] as $format) {
$formats[$format] = date($format) . ' (' . $format . ')';
$formats[$format] = date($format) . ' (' . static::formatToPattern($format) . ')';
}
return $formats;
}
@ -39,4 +76,75 @@ class DateFormats
}
return $timezones;
}
/**
* Convert a format accepted by date() to its corresponding pattern, e.g. the format 'd/m/Y \a\t h:i:s'
* is converted to 'DD/MM/YYYY [at] hh:mm:ss'
*/
public static function formatToPattern(string $format): string
{
$map = array_flip(self::PATTERN_TO_DATE_FORMAT);
return preg_replace_callback(
self::DATE_FORMAT_REGEX,
static function (array $matches) use ($map): string {
return isset($matches[1])
? '[' . str_replace('\\', '', $matches[1]) . ']'
: ($map[$matches[0]] ?? $matches[0]);
},
$format
);
}
/**
* Convert a pattern to its corresponding format accepted by date(), e.g. the format
* 'DDDD DD MMMM YYYY [at] HH:mm:ss A [o\' clock]' is converted to 'l d F Y \a\t h:i:s A \o\' \c\l\o\c\k',
* where brackets are used to escape literal string portions
*/
public static function patternToFormat(string $pattern): string
{
return preg_replace_callback(
self::PATTERN_REGEX,
static function (array $matches): string {
return isset($matches[1])
? addcslashes($matches[1], 'A..Za..z')
: (self::PATTERN_TO_DATE_FORMAT[$matches[0]] ?? $matches[0]);
},
$pattern
);
}
/**
* Formats a DateTime object using the current translation for weekdays and months
*/
public static function formatDateTime(DateTime $dateTime, string $format): string
{
return preg_replace_callback(
self::DATE_FORMAT_REGEX,
static function (array $matches) use ($dateTime): string {
switch ($matches[0]) {
case 'M':
return Admin::instance()->label('date.months.short')[$dateTime->format('n') - 1];
case 'F':
return Admin::instance()->label('date.months.long')[$dateTime->format('n') - 1];
case 'D':
return Admin::instance()->label('date.weekdays.short')[$dateTime->format('w')];
case 'l':
return Admin::instance()->label('date.weekdays.long')[$dateTime->format('w')];
case 'r':
return self::formatDateTime($dateTime, DateTime::RFC2822);
default:
return $dateTime->format($matches[1] ?? $matches[0]);
}
},
$format
);
}
/**
* The same as self::formatDateTime() but takes a timestamp instead of a DateTime object
*/
public static function formatTimestamp(int $timestamp, string $format): string
{
return static::formatDateTime(new DateTime('@' . $timestamp), $format);
}
}

View File

@ -18,6 +18,7 @@ dashboard.welcome: Welcome
date.months.long: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
date.months.short: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
date.today: Today
date.weekdays.long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
date.weekdays.short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
errors.action.report-to-github: Report an issue to GitHub
errors.action.return-to-dashboard: Return to Dashboard

View File

@ -18,6 +18,7 @@ dashboard.welcome: Bienvenue
date.months.long: ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
date.months.short: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Juil', 'Aou', 'Sep', 'Oct', 'Nov', 'Déc']
date.today: Aujourdhui
date.weekdays.long: ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi']
date.weekdays.short: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']
errors.action.report-to-github: Signaler le problème sur GitHub
errors.action.return-to-dashboard: Retour au tableau de bord

View File

@ -18,6 +18,7 @@ dashboard.welcome: Benvenuto/a
date.months.long: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre']
date.months.short: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']
date.today: Oggi
date.weekdays.long: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato']
date.weekdays.short: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
errors.action.report-to-github: Segnala un problema su GitHub
errors.action.return-to-dashboard: Torna al Riepilogo

View File

@ -18,6 +18,7 @@ dashboard.welcome: Добро пожаловать
date.months.long: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октября', 'Ноябрь', 'Декабрь']
date.months.short: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек']
date.today: Сегодня
date.weekdays.long: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']
date.weekdays.short: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
errors.action.report-to-github: Сообщить о проблеме на GitHub
errors.action.return-to-dashboard: Вернуться к панели управления
@ -91,7 +92,7 @@ options.system.images.png-compression-level: PNG Уровень сжатия
options.system.languages: Языки
options.system.languages.available-languages: Доступные Языки
options.system.languages.available-languages.no-languages: Нет Языков
options.system.languages.preferred-language: Использовать предпочитаемый язык браузера
options.system.languages.preferred-language: Использовать предпочитаемый язык браузера
options.system.languages.preferred-language.disabled: Выключить
options.system.languages.preferred-language.enabled: Включить
options.updated: Опции обновляются