Localize datepicker and respect user's defaults

This commit is contained in:
Francis Lachapelle
2016-01-11 16:32:12 -05:00
parent 09b3b4e13f
commit 77baffb85c
5 changed files with 238 additions and 46 deletions
@@ -104,3 +104,5 @@
"Loading" = "Loading";
"No such user." = "No such user.";
"You cannot (un)subscribe to a folder that you own!" = "You cannot (un)subscribe to a folder that you own!";
/* Aria label for datepicker button */
"Open Calendar" = "Open Calendar";
+10 -14
View File
@@ -44,18 +44,6 @@ static SoProduct *preferencesProduct = nil;
@implementation UIxJSONPreferences
- (WOResponse *) _makeResponse: (NSDictionary *) values
{
WOResponse *response;
response = [context response];
[response setHeader: @"text/plain; charset=utf-8"
forKey: @"content-type"];
[response appendContentString: [values jsonRepresentation]];
return response;
}
- (WOResponse *) jsonDefaultsAction
{
NSMutableDictionary *values, *account;
@@ -134,6 +122,8 @@ static SoProduct *preferencesProduct = nil;
sortedArrayUsingSelector: @selector (localizedCaseInsensitiveCompare:)];
[defaults setCalendarCategories: categoryLabels];
// TODO: build categories colors dictionary with localized keys
}
if (![defaults calendarCategoriesColors])
{
@@ -222,6 +212,12 @@ static SoProduct *preferencesProduct = nil;
// Add locale code (used by CK Editor)
locale = [[preferencesProduct resourceManager] localeForLanguageNamed: [defaults language]];
[values setObject: [locale objectForKey: @"NSLocaleCode"] forKey: @"LocaleCode"];
[values setObject: [NSDictionary dictionaryWithObjectsAndKeys:
[locale objectForKey: @"NSMonthNameArray"], @"months",
[locale objectForKey: @"NSShortMonthNameArray"], @"shortMonths",
[locale objectForKey: @"NSWeekDayNameArray"], @"days",
[locale objectForKey: @"NSShortWeekDayNameArray"], @"shortDays",
nil] forKey: @"locale"];
accounts = [NSMutableArray arrayWithArray: [values objectForKey: @"AuxiliaryMailAccounts"]];
account = [[[context activeUser] mailAccounts] objectAtIndex: 0];
@@ -238,7 +234,7 @@ static SoProduct *preferencesProduct = nil;
[values setObject: accounts forKey: @"AuxiliaryMailAccounts"];
return [self _makeResponse: values];
return [self responseWithStatus: 200 andJSONRepresentation: values];
}
- (WOResponse *) jsonSettingsAction
@@ -269,7 +265,7 @@ static SoProduct *preferencesProduct = nil;
if (![settings objectForKey: @"Mail"])
[settings setObject: [NSMutableDictionary dictionary] forKey: @"Mail"];
return [self _makeResponse: [[settings source] values]];
return [self responseWithStatus: 200 andJSONRepresentation: [[settings source] values]];
}
@end
@@ -436,14 +436,19 @@
*
* ngInject @constructor
*/
function TimePickerCtrl($scope, $element, $attrs, $compile, $timeout, $mdConstant, $mdMedia, $mdTheming,
$mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) {
TimePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window",
"$mdConstant", "$mdMedia", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"];
function TimePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window,
$mdConstant, $mdMedia, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) {
/** @final */
this.$compile = $compile;
/** @final */
this.$timeout = $timeout;
/** @final */
this.$window = $window;
/** @final */
this.dateLocale = $mdDateLocale;
@@ -542,32 +547,29 @@
});
}
TimePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$mdConstant", "$mdMedia", "$mdTheming",
"$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"];
/**
* Sets up the controller's reference to ngModelController.
* @param {!angular.NgModelController} ngModelCtrl
*/
TimePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) {
this.ngModelCtrl = ngModelCtrl;
var self = this;
ngModelCtrl.$render = function() {
self.time = self.ngModelCtrl.$viewValue;
self.inputElement.value = self.formatTime(self.time);
var value = self.ngModelCtrl.$viewValue;
if (value && !(value instanceof Date)) {
throw Error('The ng-model for sg-timepicker must be a Date instance. ' +
'Currently the model is a: ' + (typeof value));
}
self.time = value;
self.inputElement.value = self.dateLocale.formatTime(value);
self.resizeInputElement();
self.updateErrorState();
};
};
TimePickerCtrl.prototype.formatTime = function(time) {
var t = new Date(time);
if (t) {
var h = t.getHours();
var m = t.getMinutes();
return (h < 10? ('0' + h) : h) + ':' + (m < 10? ('0' + m) : m);
}
else return '';
};
/**
* Attach event listeners for both the text input and the md-time.
* Events are used instead of ng-model so that updates don't infinitely update the other
@@ -580,7 +582,7 @@
var time = new Date(data.date);
self.ngModelCtrl.$setViewValue(time);
self.time = time;
self.inputElement.value = self.formatTime(self.time);
self.inputElement.value = self.dateLocale.formatTime(time);
if (data.changed == 'minutes') {
self.closeTimePane();
}
@@ -646,6 +648,45 @@
this.timeButton.disabled = isDisabled;
};
/**
* Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
* - mindate: whether the selected date is before the minimum date.
* - maxdate: whether the selected flag is after the maximum date.
* - filtered: whether the selected date is allowed by the custom filtering function.
* - valid: whether the entered text input is a valid date
*
* The 'required' flag is handled automatically by ngModel.
*
* @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value.
*/
TimePickerCtrl.prototype.updateErrorState = function(opt_date) {
var date = opt_date || this.date;
// Clear any existing errors to get rid of anything that's no longer relevant.
this.clearErrorState();
if (!this.dateUtil.isValidDate(date)) {
// The date is seen as "not a valid date" if there is *something* set
// (i.e.., not null or undefined), but that something isn't a valid date.
this.ngModelCtrl.$setValidity('valid', date === null);
}
// TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests
// because it doesn't conform to the DOMTokenList spec.
// See https://github.com/ariya/phantomjs/issues/12782.
if (!this.ngModelCtrl.$valid) {
this.inputContainer.classList.add(INVALID_CLASS);
}
};
/** Clears any error flags set by `updateErrorState`. */
TimePickerCtrl.prototype.clearErrorState = function() {
this.inputContainer.classList.remove(INVALID_CLASS);
['valid'].forEach(function(field) {
this.ngModelCtrl.$setValidity(field, true);
}, this);
};
/**
* Resizes the input element based on the size of its content.
*/
@@ -659,7 +700,7 @@
*/
TimePickerCtrl.prototype.handleInputEvent = function(self) {
var inputString = this.inputElement.value;
var arr = inputString.split(':');
var arr = inputString.split(/[\.:]/);
if (inputString === '') {
this.ngModelCtrl.$setViewValue(null);
@@ -721,7 +762,7 @@
}
timePane.style.top = paneTop + 'px';
document.body.appendChild(this.timePane);
document.body.appendChild(timePane);
// The top of the calendar pane is a transparent box that shows the text input underneath.
// Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
@@ -780,14 +821,16 @@
/** Close the floating time pane. */
TimePickerCtrl.prototype.closeTimePane = function() {
this.isTimeOpen = false;
this.detachTimePane();
this.timePaneOpenedFrom.focus();
this.timePaneOpenedFrom = null;
this.$mdUtil.enableScrolling();
if (this.isTimeOpen) {
this.isTimeOpen = false;
this.detachTimePane();
this.timePaneOpenedFrom.focus();
this.timePaneOpenedFrom = null;
this.$mdUtil.enableScrolling();
document.body.removeEventListener('click', this.bodyClickHandler);
window.removeEventListener('resize', this.windowResizeHandler);
document.body.removeEventListener('click', this.bodyClickHandler);
window.removeEventListener('resize', this.windowResizeHandler);
}
};
/** Gets the controller instance for the time in the floating pane. */
+137 -4
View File
@@ -173,6 +173,75 @@ String.prototype.timeInterval = function () {
return interval;
};
String.prototype.parseDate = function(localeProvider, format) {
var string, formattingTokens, tokens, token, now, date, regexes, i, parsedInput, matchesCount;
string = '' + this;
formattingTokens = /%[dembByY]/g;
now = new Date();
date = {
year: -1,
month: -1,
day: -1
};
regexes = {
'%d': [/\d\d/, function(input) {
date.day = parseInt(input);
return (date.day < 32);
}],
'%e': [/ ?\d?\d/, function(input) {
date.day = parseInt(input);
return (date.day < 32);
}],
'%m': [/\d\d/, function(input) {
date.month = parseInt(input) - 1;
return (date.month < 12);
}],
'%b': [/[^\d\s\.\/\-]{2,}/, function(input) {
var i = _.indexOf(localeProvider.shortMonths, input);
if (i >= 0)
date.month = i;
return (i >= 0);
}],
'%B': [/[^\d\s\.\/\-]{2,}/, function(input) {
var i = _.indexOf(localeProvider.months, input);
if (i >= 0)
date.month = i;
return (i >= 0);
}],
'%y': [/\d\d/, function(input) {
var nearFuture = parseInt(now.getFullYear().toString().substring(2)) + 5;
date.year = parseInt(input);
if (date.year < nearFuture) date.year += 2000;
else date.year += 1900;
return true;
}],
'%Y': [/[12]\d\d\d/, function(input) {
date.year = parseInt(input);
return true;
}]
};
tokens = format.match(formattingTokens) || [];
matchesCount = 0;
for (i = 0; i < tokens.length; i++) {
token = tokens[i];
parsedInput = (string.match(regexes[token][0]) || [])[0];
if (parsedInput) {
string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
if (regexes[token][1](parsedInput))
matchesCount++;
}
}
if (tokens.length === matchesCount) {
// console.debug(this + ' + ' + format + ' = ' + JSON.stringify(date));
return new Date(date.year, date.month, date.day);
}
else
return new Date(NaN);
};
Date.prototype.daysUpTo = function(otherDate) {
var days = [];
@@ -308,19 +377,83 @@ Date.prototype.getHourString = function() {
return newString;
};
Date.prototype.format = function(localeProvider, format) {
var separators, parts, i, max,
date = [],
validParts = /%[daAmbByYHIM]/g,
val = {
'%d': this.getUTCDate(), // day of month (e.g., 01)
'%e': this.getUTCDate(), // day of month, space padded
'%a': localeProvider.shortDays[this.getUTCDay()], // locale's abbreviated weekday name (e.g., Sun)
'%A': localeProvider.days[this.getUTCDay()], // locale's full weekday name (e.g., Sunday)
'%m': this.getUTCMonth() + 1, // month (01..12)
'%b': localeProvider.shortMonths[this.getUTCMonth()], // locale's abbreviated month name (e.g., Jan)
'%B': localeProvider.months[this.getUTCMonth()], // locale's full month name (e.g., January)
'%y': this.getUTCFullYear().toString().substring(2), // last two digits of year (00..99)
'%Y': this.getUTCFullYear(), // year
'%H': this.getHours(), // hour (00..23)
'%M': this.getMinutes() }; // minute (00..59)
val['%I'] = val['%H'] > 12 ? val['%H'] % 12 : val['%H']; // hour (01..12)
val['%d'] = (val['%d'] < 10 ? '0' : '') + val['%d'];
val['%e'] = (val['%e'] < 10 ? ' ' : '') + val['%e'];
val['%m'] = (val['%m'] < 10 ? '0' : '') + val['%m'];
val['%H'] = (val['%H'] < 10 ? '0' : '') + val['%H'];
val['%I'] = (val['%I'] < 10 ? '0' : '') + val['%I'];
val['%M'] = (val['%M'] < 10 ? '0' : '') + val['%M'];
separators = format.replace(validParts, '\0').split('\0');
parts = format.match(validParts);
for (i = 0, max = parts.length; i <= max; i++){
if (separators.length)
date.push(separators.shift());
date.push(val[parts[i]]);
}
return date.join('');
};
/* Functions */
function l() {
var key = arguments[0];
var value = key;
var key = arguments[0], value = key, args = arguments, i, j;
// Retrieve translation
if (labels[key]) {
value = labels[key];
}
else if (clabels[key]) {
value = clabels[key];
}
for (var i = 1, j = 0; i < arguments.length; i++, j++) {
value = value.replace('%{' + j + '}', arguments[i]);
// Format placeholders %{0}, %{1], %{2}, ...
for (i = 1, j = 0; i < args.length; i++, j++) {
value = value.replace('%{' + j + '}', args[i]);
}
// Format placeholders %d and %s
i = 1;
if (args.length > 1) {
value = value.replace(/%((%)|s|d)/g, function(m) {
// m is the matched format, e.g. %s, %d
var val = null;
if (m[2]) {
val = m[2];
}
else {
val = args[i];
// A switch statement so that the formatter can be extended. Default is %s
switch (m) {
case '%d':
val = parseFloat(val);
if (isNaN(val))
val = 0;
break;
}
i++;
}
return val;
});
}
return value;
@@ -65,11 +65,28 @@
angular.extend(_this.defaults, data);
angular.extend(Preferences.$mdDateLocaleProvider, data.locale);
Preferences.$mdDateLocaleProvider.firstDayOfWeek = parseInt(data.SOGoFirstDayOfWeek);
Preferences.$mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
return l('Week %d', weekNumber);
};
Preferences.$mdDateLocaleProvider.msgCalendar = l('Calender');
Preferences.$mdDateLocaleProvider.msgOpenCalendar = l('Open Calendar');
Preferences.$mdDateLocaleProvider.parseDate = function(dateString) {
return dateString? dateString.parseDate(Preferences.$mdDateLocaleProvider, data.SOGoShortDateFormat) : new Date(NaN);
};
Preferences.$mdDateLocaleProvider.formatDate = function(date) {
return date? date.format(Preferences.$mdDateLocaleProvider, data.SOGoShortDateFormat) : '';
};
Preferences.$mdDateLocaleProvider.formatTime = function(date) {
return date? date.format(Preferences.$mdDateLocaleProvider, data.SOGoTimeFormat) : '';
};
return _this.defaults;
});
this.settingsPromise = Preferences.$$resource.fetch("jsonSettings").then(function(data) {
// We convert our PreventInvitationsWhitelist hash into a array of user
// We convert our PreventInvitationsWhitelist hash into a array of user
if (data.Calendar) {
if (data.Calendar.PreventInvitationsWhitelist)
data.Calendar.PreventInvitationsWhitelist = _.map(data.Calendar.PreventInvitationsWhitelist, function(value, key) {
@@ -91,11 +108,12 @@
* @desc The factory we'll use to register with Angular
* @returns the Preferences constructor
*/
Preferences.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Resource', 'User', function($q, $timeout, $log, Settings, Resource, User) {
Preferences.$factory = ['$q', '$timeout', '$log', '$mdDateLocale', 'sgSettings', 'Resource', 'User', function($q, $timeout, $log, $mdDateLocaleProvider, Settings, Resource, User) {
angular.extend(Preferences, {
$q: $q,
$timeout: $timeout,
$log: $log,
$mdDateLocaleProvider: $mdDateLocaleProvider,
$$resource: new Resource(Settings.activeUser('folderURL'), Settings.activeUser()),
activeUser: Settings.activeUser(),
$User: User