-
-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This fixes #320 by adding a SwiftUI-compatible `DatePicker`. However, `DatePickerStyle` is not supported. This uses the HTML inputs `date`, `time`, or `datetime-local`, depending on the given `displayedComponents`. This means that not all browsers show the picker, as Mac Safari currently does not support them. Safari on Mac will just show an ISO-format text field. If the date is in an invalid format, the binding will not receive updates until it becomes parseable by JSDate. On supported browsers, the binding gets updated in real time, as you would expect, with a Foundation.Date, just like SwiftUI. * Add DatePicker to TokamakCore and TokamakDOM * Fix crash on invalid date * Update progress.md and add credit * Fix time zone related issues with the DatePicker * Add DatePickerDemo to the TokamakDemo * Fix overview for DatePicker * Fix NativeDemo build
- Loading branch information
Showing
7 changed files
with
351 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// Copyright 2020 Tokamak contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
// Created by Emil Pedersen on 2021-03-26. | ||
// | ||
|
||
import struct Foundation.Date | ||
|
||
/// A control for selecting an absolute date. | ||
/// | ||
/// Available when `Label` conform to `View`. | ||
public struct DatePicker<Label>: PrimitiveView where Label: View { | ||
let label: Label | ||
let valueBinding: Binding<Date> | ||
let displayedComponents: DatePickerComponents | ||
let min: Date? | ||
let max: Date? | ||
|
||
public typealias Components = DatePickerComponents | ||
} | ||
|
||
public extension DatePicker { | ||
init( | ||
selection: Binding<Date>, | ||
in range: ClosedRange<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date], | ||
@ViewBuilder label: () -> Label | ||
) { | ||
self.init( | ||
label: label(), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: range.lowerBound, | ||
max: range.upperBound | ||
) | ||
} | ||
|
||
init( | ||
selection: Binding<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date], | ||
@ViewBuilder label: () -> Label | ||
) { | ||
self.init( | ||
label: label(), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: nil, | ||
max: nil | ||
) | ||
} | ||
|
||
init( | ||
selection: Binding<Date>, | ||
in range: PartialRangeFrom<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date], | ||
@ViewBuilder label: () -> Label | ||
) { | ||
self.init( | ||
label: label(), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: range.lowerBound, | ||
max: nil | ||
) | ||
} | ||
|
||
init( | ||
selection: Binding<Date>, | ||
in range: PartialRangeThrough<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date], | ||
@ViewBuilder label: () -> Label | ||
) { | ||
self.init( | ||
label: label(), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: nil, | ||
max: range.upperBound | ||
) | ||
} | ||
} | ||
|
||
public extension DatePicker where Label == Text { | ||
init<S>( | ||
_ title: S, | ||
selection: Binding<Date>, | ||
in range: ClosedRange<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date] | ||
) where S: StringProtocol { | ||
self.init( | ||
label: Text(title), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: range.lowerBound, | ||
max: range.upperBound | ||
) | ||
} | ||
|
||
init<S>( | ||
_ title: S, | ||
selection: Binding<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date] | ||
) where S: StringProtocol { | ||
self.init( | ||
label: Text(title), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: nil, | ||
max: nil | ||
) | ||
} | ||
|
||
init<S>( | ||
_ title: S, | ||
selection: Binding<Date>, | ||
in range: PartialRangeFrom<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date] | ||
) where S: StringProtocol { | ||
self.init( | ||
label: Text(title), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: range.lowerBound, | ||
max: nil | ||
) | ||
} | ||
|
||
init<S>( | ||
_ title: S, | ||
selection: Binding<Date>, | ||
in range: PartialRangeThrough<Date>, | ||
displayedComponents: DatePickerComponents = [.hourAndMinute, .date] | ||
) where S: StringProtocol { | ||
self.init( | ||
label: Text(title), | ||
valueBinding: selection, | ||
displayedComponents: displayedComponents, | ||
min: nil, | ||
max: range.upperBound | ||
) | ||
} | ||
} | ||
|
||
public struct DatePickerComponents: OptionSet { | ||
public static let hourAndMinute = DatePickerComponents(rawValue: 1 << 0) | ||
public static let date = DatePickerComponents(rawValue: 1 << 1) | ||
|
||
public let rawValue: UInt | ||
|
||
public init(rawValue: UInt) { | ||
self.rawValue = rawValue | ||
} | ||
} | ||
|
||
/// This is a helper type that works around absence of "package private" access control in Swift | ||
public struct _DatePickerProxy<Label> where Label: View { | ||
public let subject: DatePicker<Label> | ||
|
||
public init(_ subject: DatePicker<Label>) { self.subject = subject } | ||
|
||
public var label: Label { subject.label } | ||
public var valueBinding: Binding<Date> { subject.valueBinding } | ||
public var displayedComponents: DatePickerComponents { subject.displayedComponents } | ||
public var min: Date? { subject.min } | ||
public var max: Date? { subject.max } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright 2020 Tokamak contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
// Created by Emil Pedersen on 2021-03-26. | ||
// | ||
|
||
import struct Foundation.Date | ||
import JavaScriptKit | ||
import TokamakCore | ||
import TokamakStaticHTML | ||
|
||
extension DatePicker: ViewDeferredToRenderer { | ||
@_spi(TokamakCore) | ||
public var deferredBody: AnyView { | ||
let proxy = _DatePickerProxy(self) | ||
|
||
let type = proxy.displayedComponents | ||
|
||
let attributes: [HTMLAttribute: String] = [ | ||
"type": type.inputType, | ||
"min": proxy.min.map { type.format(date: $0) } ?? "", | ||
"max": proxy.max.map { type.format(date: $0) } ?? "", | ||
.value: type.format(date: proxy.valueBinding.wrappedValue), | ||
] | ||
|
||
return AnyView( | ||
HStack { | ||
proxy.label | ||
Text(" ") | ||
DynamicHTML( | ||
"input", | ||
attributes, | ||
listeners: [ | ||
"input": { event in | ||
let current = JSDate( | ||
millisecondsSinceEpoch: proxy.valueBinding.wrappedValue | ||
.timeIntervalSince1970 * 1000 | ||
) | ||
let str = event.target.object!.value.string! | ||
let decomposed = type.parse(date: str) | ||
if let date = decomposed.date { | ||
let components = date.components(separatedBy: "-") | ||
if components.count == 3 { | ||
current.fullYear = Int(components[0]) ?? 0 | ||
current.month = (Int(components[1]) ?? 0) - 1 | ||
current.date = Int(components[2]) ?? 0 | ||
} | ||
} | ||
if let time = decomposed.time { | ||
let components = time.components(separatedBy: ":") | ||
if components.count == 2 { | ||
current.hours = Int(components[0]) ?? 0 | ||
current.minutes = Int(components[1]) ?? 0 | ||
} | ||
} | ||
let ms = current.valueOf() | ||
if ms.isFinite { | ||
proxy.valueBinding.wrappedValue = Date(timeIntervalSince1970: ms / 1000) | ||
} | ||
}, | ||
] | ||
) | ||
} | ||
) | ||
} | ||
} | ||
|
||
extension DatePickerComponents { | ||
var inputType: String { | ||
switch (contains(.hourAndMinute), contains(.date)) { | ||
case (true, true): return "datetime-local" | ||
case (true, false): return "time" | ||
case (false, true): return "date" | ||
case (false, false): | ||
fatalError("invalid date components: must select at least date or hourAndMinute") | ||
} | ||
} | ||
|
||
func format(date: Date) -> String { | ||
let date = JSDate(millisecondsSinceEpoch: date.timeIntervalSince1970 * 1000) | ||
var partial: [String] = [] | ||
if contains(.date) { | ||
let y = date.fullYear | ||
let year: String | ||
if y < 0 { | ||
year = "-" + "00000\(-y)".suffix(6) | ||
} else if y > 9999 { | ||
year = "+" + "00000\(y)".suffix(6) | ||
} else { | ||
year = String("000\(y)".suffix(4)) | ||
} | ||
partial.append("\(year)-\("0\(date.month + 1)".suffix(2))-\("0\(date.date)".suffix(2))") | ||
} | ||
if contains(.hourAndMinute) { | ||
partial.append("\("0\(date.hours)".suffix(2)):\("0\(date.minutes)".suffix(2))") | ||
} | ||
return partial.joined(separator: "T") | ||
} | ||
|
||
/// Decomposes a formatted string into a date and a time component | ||
func parse(date: String) -> (date: String?, time: String?) { | ||
switch (contains(.hourAndMinute), contains(.date)) { | ||
case (true, true): | ||
let components = date.components(separatedBy: "T") | ||
if components.count == 2 { | ||
return (components[0], components[1]) | ||
} | ||
return (nil, nil) | ||
case (true, false): | ||
return (nil, date) | ||
case (false, true): | ||
return (date, nil) | ||
case (false, false): | ||
return (nil, nil) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright 2020 Tokamak contributors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
// Created by Emil Pedersen on 2021-03-27. | ||
// | ||
|
||
import struct Foundation.Date | ||
import TokamakShim | ||
|
||
struct DatePickerDemo: View { | ||
@State private var date = Date() | ||
|
||
var body: some View { | ||
VStack { | ||
DatePicker(selection: $date, displayedComponents: .date) { | ||
Text("Appointment date:") | ||
} | ||
DatePicker(selection: $date, displayedComponents: .hourAndMinute) { | ||
Text("Appointment time:") | ||
} | ||
DatePicker(selection: $date) { | ||
Text("Confirm:") | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.