Introduce a new localized date control

Added a helped Backend::DateTime() for rendering the appropriate HTML output as a <time /> tag
The meta now specifies the locale and timezone preference used here
Lists now take advantage of this to display dates relative to the timezone and language (awesome sauce)
@todo Still need to get apply this logic to the datepicker form widget
This commit is contained in:
Samuel Georges 2016-04-23 13:17:04 +10:00
parent 4df7c6704e
commit 02165a8a4a
5 changed files with 312 additions and 24 deletions

View File

@ -0,0 +1,166 @@
/*
* Date time converter.
* See moment.js for format options.
* http://momentjs.com/docs/#/displaying/format/
*
* Usage:
*
* <time
* data-datetime-control
* datetime="2014-11-19 01:21:57"
* data-format="dddd Do [o]f MMMM YYYY hh:mm:ss A"
* data-timezone="Australia/Sydney"
* data-locale="en-au">This text will be replaced</time>
*
* Alias options:
*
* time -> 6:28 AM
* timeLong -> 6:28:01 AM
* date -> 04/23/2016
* dateMin -> 4/23/2016
* dateLong -> April 23, 2016
* dateLongMin -> Apr 23, 2016
* dateTime -> April 23, 2016 6:28 AM
* dateTimeMin -> Apr 23, 2016 6:28 AM
* dateTimeLong -> Saturday, April 23, 2016 6:28 AM
* dateTimeLongMin -> Sat, Apr 23, 2016 6:29 AM
*
*/
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var DateTimeConverter = function (element, options) {
this.$el = $(element)
this.options = options || {}
$.oc.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
DateTimeConverter.prototype = Object.create(BaseProto)
DateTimeConverter.prototype.constructor = DateTimeConverter
DateTimeConverter.prototype.init = function() {
this.initDefaults()
this.$el.text(this.getDateTimeValue())
this.$el.one('dispose-control', this.proxy(this.dispose))
}
DateTimeConverter.prototype.initDefaults = function() {
if (!this.options.timezone) {
this.options.timezone = $('meta[name="backend-timezone"]').attr('content')
}
if (!this.options.locale) {
this.options.locale = $('meta[name="backend-locale"]').attr('content')
}
if (!this.options.format) {
this.options.format = 'llll'
}
if (this.options.formatAlias) {
this.options.format = this.getFormatFromAlias(this.options.formatAlias)
}
}
DateTimeConverter.prototype.getDateTimeValue = function() {
this.datetime = this.$el.attr('datetime')
var momentObj = moment(this.datetime),
result
if (this.options.locale) {
moment.locale(this.options.locale)
}
if (this.options.timezone) {
momentObj = momentObj.tz(this.options.timezone)
}
if (this.options.timeSince) {
result = momentObj.fromNow()
}
else if (this.options.timeTense) {
result = momentObj.calendar()
}
else {
result = momentObj.format(this.options.format)
}
return result
}
DateTimeConverter.prototype.getFormatFromAlias = function(alias) {
var map = {
time: 'LT',
timeLong: 'LTS',
date: 'L',
dateMin: 'l',
dateLong: 'LL',
dateLongMin: 'll',
dateTime: 'LLL',
dateTimeMin: 'lll',
dateTimeLong: 'LLLL',
dateTimeLongMin: 'llll'
}
return map[alias] ? map[alias] : 'llll'
}
DateTimeConverter.prototype.dispose = function() {
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.dateTimeConverter')
this.$el = null
this.options = null
BaseProto.dispose.call(this)
}
DateTimeConverter.DEFAULTS = {
format: null,
formatAlias: null,
timezone: null,
locale: null,
timeTense: false,
timeSince: false
}
// PLUGIN DEFINITION
// ============================
var old = $.fn.dateTimeConverter
$.fn.dateTimeConverter = function (option) {
var args = Array.prototype.slice.call(arguments, 1), items, result
items = this.each(function () {
var $this = $(this)
var data = $this.data('oc.dateTimeConverter')
var options = $.extend({}, DateTimeConverter.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.dateTimeConverter', (data = new DateTimeConverter(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : items
}
$.fn.dateTimeConverter.Constructor = DateTimeConverter
$.fn.dateTimeConverter.noConflict = function () {
$.fn.dateTimeConverter = old
return this
}
// Add this only if required
$(document).render(function (){
$('time[data-datetime-control]').dateTimeConverter()
})
}(window.jQuery);

View File

@ -1,10 +1,12 @@
<?php namespace Backend\Helpers;
use Url;
use Html;
use Config;
use Request;
use Redirect;
use October\Rain\Router\Helper as RouterHelper;
use System\Helpers\DateTime as DateTimeHelper;
use Backend\Classes\Skin;
/**
@ -79,4 +81,48 @@ class Backend
{
return Redirect::intended($this->uri() . '/' . $path, $status, $headers, $secure);
}
/**
* Returns the HTML for a date formatted in the backend.
*/
public function dateTime($dateTime, $value = '', $options = [])
{
extract(array_merge([
'format' => null,
'formatAlias' => null,
'jsFormat' => null,
'timeTense' => false,
'timeSince' => false,
], $options));
$carbon = DateTimeHelper::makeCarbon($dateTime);
if ($jsFormat !== null) {
$format = $jsFormat;
}
else {
$format = DateTimeHelper::momentFormat($format);
}
$attributes = [
'datetime' => $carbon,
'data-datetime-control' => 1,
];
if ($timeTense) {
$attributes['data-time-tense'] = 1;
}
elseif ($timeSince) {
$attributes['data-time-since'] = 1;
}
elseif ($format) {
$attributes['data-format'] = $format;
}
elseif ($formatAlias) {
$attributes['data-format-alias'] = $formatAlias;
}
return '<time'.Html::attributes($attributes).'>'.e($value).'</time>'.PHP_EOL;
}
}

View File

@ -2,6 +2,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="backend-base-path" content="<?= Backend::baseUrl() ?>">
<meta name="backend-timezone" content="<?= e(Backend\Models\Preferences::get('timezone')) ?>">
<meta name="backend-locale" content="<?= e(Backend\Models\Preferences::get('locale')) ?>">
<meta name="csrf-token" content="<?= csrf_token() ?>">
<link rel="icon" type="image/png" href="<?= Backend::skinAsset('assets/images/favicon.png') ?>">
<title data-title-template="<?= empty($this->pageTitleTemplate) ? '%s' : e($this->pageTitleTemplate) ?> | <?= e(Backend\Models\BrandSettings::get('app_name')) ?>">

View File

@ -913,13 +913,19 @@ class Lists extends WidgetBase
return null;
}
$value = $this->validateDateTimeValue($value, $column);
$dateTime = $this->validateDateTimeValue($value, $column);
if ($column->format !== null) {
return $value->format($column->format);
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toDayDateTimeString();
}
return $value->toDayDateTimeString();
return Backend::dateTime($dateTime, $value, [
'format' => $column->format,
'formatAlias' => 'dateTimeLongMin'
]);
}
/**
@ -931,13 +937,16 @@ class Lists extends WidgetBase
return null;
}
$value = $this->validateDateTimeValue($value, $column);
$dateTime = $this->validateDateTimeValue($value, $column);
if ($column->format === null) {
$column->format = 'g:i A';
}
$format = $column->format !== null ? $column->format : 'g:i A';
return $value->format($column->format);
$value = $dateTime->format($format);
return Backend::dateTime($dateTime, $value, [
'format' => $column->format,
'formatAlias' => 'time'
]);
}
/**
@ -949,13 +958,19 @@ class Lists extends WidgetBase
return null;
}
$value = $this->validateDateTimeValue($value, $column);
$dateTime = $this->validateDateTimeValue($value, $column);
if ($column->format !== null) {
return $value->format($column->format);
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toFormattedDateString();
}
return $value->toFormattedDateString();
return Backend::dateTime($dateTime, $value, [
'format' => $column->format,
'formatAlias' => 'dateLongMin'
]);
}
/**
@ -967,9 +982,13 @@ class Lists extends WidgetBase
return null;
}
$value = $this->validateDateTimeValue($value, $column);
$dateTime = $this->validateDateTimeValue($value, $column);
return DateTimeHelper::timeSince($value);
$value = DateTimeHelper::timeSince($dateTime);
return Backend::dateTime($dateTime, $value, [
'timeSince' => true
]);
}
/**
@ -981,9 +1000,13 @@ class Lists extends WidgetBase
return null;
}
$value = $this->validateDateTimeValue($value, $column);
$dateTime = $this->validateDateTimeValue($value, $column);
return DateTimeHelper::timeTense($value);
$value = DateTimeHelper::timeTense($dateTime);
return Backend::dateTime($dateTime, $value, [
'timeTense' => true
]);
}
/**
@ -991,7 +1014,7 @@ class Lists extends WidgetBase
*/
protected function validateDateTimeValue($value, $column)
{
$value = DateTimeHelper::instance()->makeCarbon($value, false);
$value = DateTimeHelper::makeCarbon($value, false);
if (!$value instanceof Carbon) {
throw new ApplicationException(Lang::get(

View File

@ -8,8 +8,6 @@ use InvalidArgumentException;
class DateTime
{
use \October\Rain\Support\Traits\Singleton;
/**
* Returns a human readable time difference from the value to the
* current time. Eg: **10 minutes ago**
@ -18,10 +16,7 @@ class DateTime
*/
public static function timeSince($datetime)
{
return self::instance()
->makeCarbon($datetime)
->diffForHumans()
;
return self::makeCarbon($datetime)->diffForHumans();
}
/**
@ -33,7 +28,7 @@ class DateTime
*/
public static function timeTense($datetime)
{
$datetime = self::instance()->makeCarbon($datetime);
$datetime = self::makeCarbon($datetime);
$yesterday = $datetime->subDays(1);
$tomorrow = $datetime->addDays(1);
$time = $datetime->format('H:i');
@ -57,7 +52,7 @@ class DateTime
*
* @return Carbon\Carbon
*/
public function makeCarbon($value, $throwException = true)
public static function makeCarbon($value, $throwException = true)
{
if ($value instanceof Carbon) {
// Do nothing
@ -78,4 +73,60 @@ class DateTime
return $value;
}
/**
* Converts a PHP date format to "Moment.js" format.
* @param string $format
* @return string
*/
public static function momentFormat($format)
{
$replacements = [
'd' => 'DD',
'D' => 'ddd',
'j' => 'D',
'l' => 'dddd',
'N' => 'E',
'S' => 'o',
'w' => 'e',
'z' => 'DDD',
'W' => 'W',
'F' => 'MMMM',
'm' => 'MM',
'M' => 'MMM',
'n' => 'M',
't' => '', // no equivalent
'L' => '', // no equivalent
'o' => 'YYYY',
'Y' => 'YYYY',
'y' => 'YY',
'a' => 'a',
'A' => 'A',
'B' => '', // no equivalent
'g' => 'h',
'G' => 'H',
'h' => 'hh',
'H' => 'HH',
'i' => 'mm',
's' => 'ss',
'u' => 'SSS',
'e' => 'zz', // deprecated since version 1.6.0 of moment.js
'I' => '', // no equivalent
'O' => '', // no equivalent
'P' => '', // no equivalent
'T' => '', // no equivalent
'Z' => '', // no equivalent
'c' => '', // no equivalent
'r' => '', // no equivalent
'U' => 'X',
];
foreach ($replacements as $from => $to) {
$replacements['\\'.$from] = '['.$from.']';
}
$momentFormat = strtr($format, $replacements);
return $momentFormat;
}
}