Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial TimeSpan support in TextBox #1777

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -308,6 +308,24 @@ private void AddDefaultToStringTranslations()
.Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1])
.WithAnnotation(ResultIsObservableAnnotation.Instance)
));
AddMethodTranslator(() => default(TimeSpan).ToString(), new GenericMethodCompiler(
args => new JsIdentifierExpression("dotvvm").Member("globalize").Member("bindingTimeOnlyToString")
.WithAnnotation(new GlobalizeResourceBindingProperty())
.Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance))
.WithAnnotation(ResultIsObservableAnnotation.Instance)
));
AddMethodTranslator(() => default(Nullable<TimeSpan>).ToString(), new GenericMethodCompiler(
args => new JsIdentifierExpression("dotvvm").Member("globalize").Member("bindingTimeOnlyToString")
.WithAnnotation(new GlobalizeResourceBindingProperty())
.Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance))
.WithAnnotation(ResultIsObservableAnnotation.Instance)
));
AddMethodTranslator(() => default(TimeSpan).ToString("fmt"), new GenericMethodCompiler(
args => new JsIdentifierExpression("dotvvm").Member("globalize").Member("bindingTimeOnlyToString")
.WithAnnotation(new GlobalizeResourceBindingProperty())
.Invoke(args[0].WithAnnotation(ShouldBeObservableAnnotation.Instance), args[1])
.WithAnnotation(ResultIsObservableAnnotation.Instance)
));

foreach (var num in ReflectionUtils.GetNumericTypes().Except(new[] { typeof(char) }))
{
Expand Down
2 changes: 1 addition & 1 deletion src/Framework/Framework/Controls/TextBox.cs
Expand Up @@ -131,7 +131,7 @@ public static FormatValueType ResolveValueType(IValueBinding? binding)
{
return FormatValueType.DateOnly;
}
else if (binding != null && (binding.ResultType == typeof(TimeOnly) || binding.ResultType == typeof(TimeOnly?)))
else if (binding != null && (binding.ResultType == typeof(TimeOnly) || binding.ResultType == typeof(TimeOnly?) || binding.ResultType == typeof(TimeSpan) || binding.ResultType == typeof(TimeSpan?)))
{
return FormatValueType.TimeOnly;
}
Expand Down
13 changes: 11 additions & 2 deletions src/Framework/Framework/Resources/Scripts/Globalize/globalize.js
Expand Up @@ -240,7 +240,8 @@ Globalize.cultures[ "default" ] = {
g: "M/d/yyyy h:mm tt",
// G is a combination of the short date ("d") and short time ("T") patterns, separated by a space
G: "M/d/yyyy h:mm:ss tt"
}
},
additionalDefaultPatterns: ["H:mm", "H:mm:ss"]
// optional fields for each calendar:
/*
monthsGenitive:
Expand Down Expand Up @@ -1536,7 +1537,7 @@ Globalize.localize = function( key, cultureSelector ) {
Globalize.parseDate = function (value, formats, culture, previousValue) {
culture = this.findClosestCulture( culture );

var date, prop, patterns;
var date, prop, patterns, pattern;
if ( formats ) {
if ( typeof formats === "string" ) {
formats = [ formats ];
Expand All @@ -1560,6 +1561,14 @@ Globalize.parseDate = function (value, formats, culture, previousValue) {
break;
}
}
if (!date) {
for ( pattern of culture.calendar.additionalDefaultPatterns) {
date = parseExact(value, pattern, culture, previousValue);
if ( date ) {
break;
}
}
}
}

return date || null;
Expand Down

Large diffs are not rendered by default.

Expand Up @@ -230,6 +230,10 @@ function validateTimeSpan(value: any) {
if (typeof value === "number") {
return { value: serializeTimeSpan(value), wasCoerced: true };
}

if (value instanceof Date) {
return { value: serializeTimeOnly(value, false), wasCoerced: true };
}
}

function validateDateTimeOffset(value: any) {
Expand Down
Expand Up @@ -70,8 +70,13 @@ export function serializeDateOnly(date: Date): string {
return padNumber(date.getFullYear(), 4) + "-" + padNumber(date.getMonth() + 1, 2) + "-" + padNumber(date.getDate(), 2)
}

export function serializeTimeOnly(date: Date): string {
return padNumber(date.getHours(), 2) + ':' + padNumber(date.getMinutes(), 2) + ':' + padNumber(date.getSeconds(), 2) + '.' + padNumber(date.getMilliseconds(), 3) + '0000';
export function serializeTimeOnly(date: Date, useMilliseconds: boolean = true): string {
const result = padNumber(date.getHours(), 2) + ':' + padNumber(date.getMinutes(), 2) + ':' + padNumber(date.getSeconds(), 2);
if (useMilliseconds) {
return result + '.' + padNumber(date.getMilliseconds(), 3) + '0000';
} else{
return result;
}
}

export function serializeTimeSpan(ticks: number): string {
Expand Down
1 change: 1 addition & 0 deletions src/Framework/Framework/package.json
Expand Up @@ -21,6 +21,7 @@
"build-development": "rollup -c && npm run tsc-types",
"build-rollup": "npm run build-production && npm run build-development",
"build-production": "rollup -c --environment BUILD:production",
"build-globalize": "esbuild --minify --outfile=./Resources/Scripts/Globalize/globalize.min.js ./Resources/Scripts/Globalize/globalize.js",
"test": "jest --silent",
"tsc-check": "tsc -p . --noEmit",
"tsc-types": "tsc -d -p . --outFile ./obj/typescript-types/dotvvm.d.ts --emitDeclarationOnly --skipLibCheck"
Expand Down
Expand Up @@ -13,6 +13,7 @@ public TextBoxFormatViewModel()
public DateTime DateValue { get; set; } = DateTime.Parse("2015-12-27T00:00:00.0000000");
public DateTime? NullableDateValue { get; set; } = DateTime.Parse("2015-12-27T00:00:00.0000000");
public double NumberValue { get; set; } = 123.123456789;
public TimeSpan TimeSpanValue { get; set; } = new TimeSpan(11, 48, 25);
public double? NullableNumberValue { get; set; } = 123.123456789;
public double BigNumberValue { get; set; } = 12356789.987654;
public string CurrentCulture => Context.GetCurrentUICulture().Name;
Expand Down
Expand Up @@ -14,6 +14,8 @@ public class TextBoxTypesViewModel : DotvvmViewModelBase
public double? NullableNumber { get; set; } = 42.42;
public int Int { get; set; }
public int? NullableInt { get; set; }
public TimeSpan TimeSpan { get; set; } = new TimeSpan(11, 48, 25);
public TimeSpan? NullableTimeSpan { get; } = new TimeSpan(11, 48, 25);
}
}

Expand Up @@ -58,6 +58,22 @@
Text="{value: NullableNumberValue}">
</dot:Literal>
</p>
<p ID="timespanFormat">
TimeSpan Format: <br>
<dot:textbox ID="timespanTextbox"
ClientIDMode="Static"
Text="{value: TimeSpanValue }"
style="height: 20px; width: 200px;"
FormatString="HH:mm" />
<dot:Literal ID="timespanValueText"
ClientIDMode="Static"
Text="{value: TimeSpanValue }"
ValueType="Time"/>
<dot:Literal ID="timespanFormatText"
ClientIDMode="Static"
Text="(Format: HH:mm)"
ValueType="Text" />
</p>
<p ID="customNumberformat">
Custom Numeric Format: <br>
<dot:textbox ID="customFormatTextbox"
Expand Down
Expand Up @@ -45,6 +45,21 @@
Text="{value: NumberValue}">
</dot:Literal>
</p>
<p ID="timespanFormat">
TimeSpan Format: <br>
<dot:textbox ID="timespanTextbox"
ClientIDMode="Static"
Text="{value: TimeSpanValue.ToString('HH:mm') }"
style="height: 20px; width: 200px;"/>
<dot:Literal ID="timespanValueText"
ClientIDMode="Static"
Text="{value: TimeSpanValue }"
ValueType="Time" />
<dot:Literal ID="timespanFormatText"
ClientIDMode="Static"
Text="(Format: HH:mm)"
ValueType="Text" />
</p>
<p ID="nullableNumberformat">
Nullable Number: <br>
<dot:TextBox ID="nullableNumberTextbox"
Expand Down
Expand Up @@ -19,6 +19,10 @@
<br />
<dot:TextBox Text="{value: NullableDate}" Type="Time" data-ui="nullable-time-textbox" />
<br />
<dot:TextBox Text="{value: TimeSpan}" Type="Time" data-ui="timespan-textbox" />
<br />
<dot:TextBox Text="{value: NullableTimeSpan}" Type="Time" data-ui="nullable-timespan-textbox" />
<br />
<dot:TextBox Text="{value: Date}" Type="DateTimeLocal" data-ui="datetime-textbox" />
<br />
<dot:TextBox Text="{value: NullableDate}" Type="DateTimeLocal" data-ui="nullable-datetime-textbox" />
Expand Down
18 changes: 18 additions & 0 deletions src/Samples/Tests/Tests/Control/TextBoxTests.cs
Expand Up @@ -165,6 +165,12 @@ public void Control_TextBox_StringFormat(string cultureName, string url, string
var nullableNumberValueText = browser.First("#nullableNumberValueText");
AssertUI.InnerTextEquals(nullableNumberValueText, 123.123456789.ToString(culture));

var timespanTextbox = browser.First("#timespanTextbox");
AssertUI.Attribute(timespanTextbox, "value", new TimeSpan(11, 48, 25).ToString("hh\\:mm"));

var timespanValueText = browser.First("#timespanValueText");
AssertUI.InnerTextEquals(timespanValueText, new TimeSpan(11, 48, 25).ToString());

//write new valid values
dateTextBox.Clear().SendKeys(dateResult2);
numberTextbox.Clear().SendKeys(2000.ToString("n0", culture));
Expand All @@ -180,23 +186,28 @@ public void Control_TextBox_StringFormat(string cultureName, string url, string
//write invalid values
dateTextBox.Clear().SendKeys("dsasdasd");
numberTextbox.Clear().SendKeys("000//a");
timespanTextbox.Clear().SendKeys("08:23");
dateTextBox.Click();

//check displayed values (behavior change in 3.0 - previous values should stay there)
AssertUI.InnerTextEquals(dateText, new DateTime(2018, 12, 27).ToString("G", culture));
AssertUI.InnerTextEquals(numberValueText, 2000.ToString(culture));
AssertUI.InnerTextEquals(timespanTextbox, new TimeSpan(8, 23, 0).ToString("hh\\:mm"));
AssertUI.InnerTextEquals(timespanValueText, new TimeSpan(8, 23, 0).ToString());

AssertUI.Attribute(numberTextbox, "value", "000//a");
AssertUI.Attribute(dateTextBox, "value", "dsasdasd");

//write new valid values
dateTextBox.Clear().SendKeys(new DateTime(2018, 1, 1).ToString("d", culture));
numberTextbox.Clear().SendKeys(1000.550277.ToString(culture));
timespanTextbox.Clear().SendKeys(new TimeSpan(11, 48, 25).ToString("hh\\:mm"));
dateTextBox.Click();

//check new values
AssertUI.InnerTextEquals(dateText, new DateTime(2018, 1, 1).ToString("G", culture));
AssertUI.InnerTextEquals(numberValueText, 1000.550277.ToString(culture));
AssertUI.InnerTextEquals(timespanValueText, new TimeSpan(11, 48, 0).ToString());

AssertUI.Attribute(numberTextbox, "value", 1000.550277.ToString("n4", culture));
AssertUI.Attribute(dateTextBox, "value", dateResult3);
Expand All @@ -209,6 +220,10 @@ public void Control_TextBox_StringFormat(string cultureName, string url, string
nullableDateTextBox.Clear().SendKeys(new DateTime(2020, 4, 2).ToString("d", culture)).SendKeys(Keys.Tab);
AssertUI.Attribute(nullableDateTextBox, "value", new DateTime(2020, 4, 2).ToString("G", culture));
AssertUI.InnerTextEquals(nullableDateText, new DateTime(2020, 4, 2).ToString("G", culture));

timespanTextbox.Clear().SendKeys(new TimeSpan(22, 23, 45).ToString("t", culture)).SendKeys(Keys.Tab);
AssertUI.InnerTextEquals(timespanTextbox, new TimeSpan(22, 23, 45).ToString("hh\\:mm"));
AssertUI.InnerTextEquals(timespanValueText, new TimeSpan(22, 23, 45).ToString());
});
}

Expand Down Expand Up @@ -288,6 +303,9 @@ public void Control_TextBox_TextBox_Types(string localizationId)
AssertUI.Value(browser.Single("input[data-ui='datetime-textbox']"), "2017-01-01T08:08");
AssertUI.Value(browser.Single("input[data-ui='nullable-datetime-textbox']"), "2017-01-01T20:10");

AssertUI.Value(browser.Single("input[data-ui='timespan-textbox']"), "11:48:25");
AssertUI.Value(browser.Single("input[data-ui='nullable-timespan-textbox']"), "11:48:25");

var intTextBox = browser.Single("input[data-ui='int-textbox']");
AssertUI.Value(intTextBox, "0");
intTextBox.SetFocus();
Expand Down