Skip to content

Commit 491cf93

Browse files
jverkoeyclaude
andauthored
Implement dialog, meter, object, output, samp elements (#250)
This commit adds four new HTML elements to Slipstream: - Dialog: Interactive dialog/modal element with `open` attribute - Meter: Scalar measurement gauge with value, min, max, low, high, and optimum attributes - Object: External resource embedding with data URL, type, name, width, and height - Output: Form calculation result with `for` and `name` attributes Also includes: - Comprehensive tests for all four elements - Updated SlipstreamForWebDevelopers.md documentation Part of #25 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 855ce35 commit 491cf93

9 files changed

Lines changed: 509 additions & 4 deletions

File tree

Sources/Slipstream/Documentation.docc/Guides/SlipstreamForWebDevelopers.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
151151
[`<img>`](https://html.spec.whatwg.org/multipage/sections.html#the-img-element) | ``Image``
152152
[`<iframe>`](https://html.spec.whatwg.org/multipage/sections.html#the-iframe-element) | ``IFrame``
153153
[`<embed>`](https://html.spec.whatwg.org/multipage/sections.html#the-embed-element) | ``Embed``
154-
[`<object>`](https://html.spec.whatwg.org/multipage/sections.html#the-object-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
154+
[`<object>`](https://html.spec.whatwg.org/multipage/sections.html#the-object-element) | ``Object``
155155
[`<video>`](https://html.spec.whatwg.org/multipage/sections.html#the-video-element) | ``Video``
156156
[`<audio>`](https://html.spec.whatwg.org/multipage/sections.html#the-audio-element) | ``Audio``
157157
[`<track>`](https://html.spec.whatwg.org/multipage/sections.html#the-track-element) | ``Track``
@@ -186,9 +186,9 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
186186
[`<optgroup>`](https://html.spec.whatwg.org/multipage/sections.html#the-optgroup-element) | ``OptGroup``
187187
[`<option>`](https://html.spec.whatwg.org/multipage/sections.html#the-option-element) | ``Option``
188188
[`<textarea>`](https://html.spec.whatwg.org/multipage/sections.html#the-textarea-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
189-
[`<output>`](https://html.spec.whatwg.org/multipage/sections.html#the-output-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
189+
[`<output>`](https://html.spec.whatwg.org/multipage/sections.html#the-output-element) | ``Output``
190190
[`<progress>`](https://html.spec.whatwg.org/multipage/sections.html#the-progress-element) | ``ProgressView``
191-
[`<meter>`](https://html.spec.whatwg.org/multipage/sections.html#the-meter-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
191+
[`<meter>`](https://html.spec.whatwg.org/multipage/sections.html#the-meter-element) | ``Meter``
192192
[`<fieldset>`](https://html.spec.whatwg.org/multipage/sections.html#the-fieldset-element) | ``Fieldset``
193193
[`<legend>`](https://html.spec.whatwg.org/multipage/sections.html#the-legend-element) | ``Legend``
194194

@@ -198,7 +198,7 @@ provided below is an organized table of W3C HTML tags and their equivalent Slips
198198
:--------|:----------------
199199
[`<details>`](https://html.spec.whatwg.org/multipage/sections.html#the-details-element) | ``Details``
200200
[`<summary>`](https://html.spec.whatwg.org/multipage/sections.html#the-summary-element) | ``Summary``
201-
[`<dialog>`](https://html.spec.whatwg.org/multipage/sections.html#the-dialog-element) | [Not implemented yet](https://github.com/jverkoey/slipstream/issues/25)
201+
[`<dialog>`](https://html.spec.whatwg.org/multipage/sections.html#the-dialog-element) | ``Dialog``
202202

203203
### Scripting
204204

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Foundation
2+
import SwiftSoup
3+
4+
/// A view that represents an external resource, which can be treated as an image, a nested browsing context, or a plugin.
5+
///
6+
/// The Object element represents an external resource such as an image, PDF document,
7+
/// video, or plugin. It provides a fallback mechanism through its content for browsers
8+
/// that don't support the specified resource.
9+
///
10+
/// ```swift
11+
/// struct MySiteContent: View {
12+
/// var body: some View {
13+
/// Body {
14+
/// Object(
15+
/// data: URL(string: "/media/document.pdf"),
16+
/// type: "application/pdf",
17+
/// width: 600,
18+
/// height: 400
19+
/// ) {
20+
/// Paragraph("Your browser doesn't support embedded PDFs.")
21+
/// }
22+
/// }
23+
/// }
24+
/// }
25+
/// ```
26+
///
27+
/// - SeeAlso: W3C [`object`](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element) specification.
28+
@available(iOS 17.0, macOS 14.0, *)
29+
public struct Object<Content>: View where Content: View {
30+
/// Creates an Object view with the specified resource and attributes.
31+
///
32+
/// - Parameters:
33+
/// - data: The URL of the resource.
34+
/// - type: The MIME type of the resource.
35+
/// - name: The name of the nested browsing context.
36+
/// - width: The width of the object in pixels.
37+
/// - height: The height of the object in pixels.
38+
/// - content: Fallback content for browsers that don't support the object.
39+
public init(
40+
data: URL? = nil,
41+
type: String? = nil,
42+
name: String? = nil,
43+
width: Int? = nil,
44+
height: Int? = nil,
45+
@ViewBuilder content: @escaping @Sendable () -> Content = { EmptyView() }
46+
) {
47+
self.data = data
48+
self.type = type
49+
self.name = name
50+
self.width = width
51+
self.height = height
52+
self.content = content
53+
}
54+
55+
@_documentation(visibility: private)
56+
public func render(_ container: Element, environment: EnvironmentValues) throws {
57+
let element = try container.appendElement("object")
58+
59+
if let data {
60+
try element.attr("data", data.absoluteString)
61+
}
62+
63+
if let type {
64+
try element.attr("type", type)
65+
}
66+
67+
if let name {
68+
try element.attr("name", name)
69+
}
70+
71+
if let width {
72+
try element.attr("width", String(width))
73+
}
74+
75+
if let height {
76+
try element.attr("height", String(height))
77+
}
78+
79+
try self.content().render(element, environment: environment)
80+
}
81+
82+
private let data: URL?
83+
private let type: String?
84+
private let name: String?
85+
private let width: Int?
86+
private let height: Int?
87+
@ViewBuilder private let content: @Sendable () -> Content
88+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import SwiftSoup
2+
3+
/// A view that represents a scalar measurement within a known range, or a fractional value.
4+
///
5+
/// The Meter element represents a gauge showing the current level of a measurement
6+
/// within a known range. This is appropriate for things like disk usage, vote counts,
7+
/// or progress that has a defined maximum. For progress bars that don't have a known
8+
/// maximum, use a progress bar instead.
9+
///
10+
/// ```swift
11+
/// struct MySiteContent: View {
12+
/// var body: some View {
13+
/// Body {
14+
/// Paragraph {
15+
/// DOMString("Disk usage: ")
16+
/// Meter(value: 0.75, min: 0, max: 1, low: 0.3, high: 0.9, optimum: 0.2)
17+
/// }
18+
/// }
19+
/// }
20+
/// }
21+
/// ```
22+
///
23+
/// - SeeAlso: W3C [`meter`](https://html.spec.whatwg.org/multipage/form-elements.html#the-meter-element) specification.
24+
@available(iOS 17.0, macOS 14.0, *)
25+
public struct Meter<Content>: View where Content: View {
26+
/// Creates a Meter view with a value and optional range boundaries.
27+
///
28+
/// - Parameters:
29+
/// - value: The current value of the meter.
30+
/// - min: The lower numeric bound of the measured range. Default is `0`.
31+
/// - max: The upper numeric bound of the measured range. Default is `1`.
32+
/// - low: The upper numeric bound of the low end of the measured range.
33+
/// - high: The lower numeric bound of the high end of the measured range.
34+
/// - optimum: The optimal numeric value.
35+
/// - content: Optional fallback content for browsers that don't support the meter element.
36+
public init(
37+
value: Double,
38+
min: Double = 0,
39+
max: Double = 1,
40+
low: Double? = nil,
41+
high: Double? = nil,
42+
optimum: Double? = nil,
43+
@ViewBuilder content: @escaping @Sendable () -> Content = { EmptyView() }
44+
) {
45+
self.value = value
46+
self.min = min
47+
self.max = max
48+
self.low = low
49+
self.high = high
50+
self.optimum = optimum
51+
self.content = content
52+
}
53+
54+
@_documentation(visibility: private)
55+
public func render(_ container: Element, environment: EnvironmentValues) throws {
56+
let element = try container.appendElement("meter")
57+
58+
try element.attr("value", String(value))
59+
try element.attr("min", String(min))
60+
try element.attr("max", String(max))
61+
62+
if let low {
63+
try element.attr("low", String(low))
64+
}
65+
66+
if let high {
67+
try element.attr("high", String(high))
68+
}
69+
70+
if let optimum {
71+
try element.attr("optimum", String(optimum))
72+
}
73+
74+
try self.content().render(element, environment: environment)
75+
}
76+
77+
private let value: Double
78+
private let min: Double
79+
private let max: Double
80+
private let low: Double?
81+
private let high: Double?
82+
private let optimum: Double?
83+
@ViewBuilder private let content: @Sendable () -> Content
84+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import SwiftSoup
2+
3+
/// A view that represents the result of a calculation or user action.
4+
///
5+
/// The Output element represents the result of a calculation performed by the application,
6+
/// or the result of a user action. It's typically used in forms to display computed values.
7+
///
8+
/// ```swift
9+
/// struct MySiteContent: View {
10+
/// var body: some View {
11+
/// Body {
12+
/// Form {
13+
/// Input(type: .number, id: "a", name: "a")
14+
/// DOMString(" + ")
15+
/// Input(type: .number, id: "b", name: "b")
16+
/// DOMString(" = ")
17+
/// Output(for: ["a", "b"], name: "result") {
18+
/// DOMString("0")
19+
/// }
20+
/// }
21+
/// }
22+
/// }
23+
/// }
24+
/// ```
25+
///
26+
/// - SeeAlso: W3C [`output`](https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element) specification.
27+
@available(iOS 17.0, macOS 14.0, *)
28+
public struct Output<Content>: View where Content: View {
29+
/// Creates an Output view with optional for and name attributes.
30+
///
31+
/// - Parameters:
32+
/// - for: An array of element IDs that contributed to the output value.
33+
/// - name: The name of the output element for form submission.
34+
/// - content: A view builder that creates the output's content.
35+
public init(
36+
for elementIds: [String]? = nil,
37+
name: String? = nil,
38+
@ViewBuilder content: @escaping @Sendable () -> Content
39+
) {
40+
self.elementIds = elementIds
41+
self.name = name
42+
self.content = content
43+
}
44+
45+
/// Creates an Output view with static text.
46+
///
47+
/// - Parameters:
48+
/// - text: The output text to display.
49+
/// - for: An array of element IDs that contributed to the output value.
50+
/// - name: The name of the output element for form submission.
51+
public init(
52+
_ text: String,
53+
for elementIds: [String]? = nil,
54+
name: String? = nil
55+
) where Content == DOMString {
56+
self.elementIds = elementIds
57+
self.name = name
58+
self.content = {
59+
DOMString(text)
60+
}
61+
}
62+
63+
@_documentation(visibility: private)
64+
public func render(_ container: Element, environment: EnvironmentValues) throws {
65+
let element = try container.appendElement("output")
66+
67+
if let elementIds, !elementIds.isEmpty {
68+
try element.attr("for", elementIds.joined(separator: " "))
69+
}
70+
71+
if let name {
72+
try element.attr("name", name)
73+
}
74+
75+
try self.content().render(element, environment: environment)
76+
}
77+
78+
private let elementIds: [String]?
79+
private let name: String?
80+
@ViewBuilder private let content: @Sendable () -> Content
81+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import SwiftSoup
2+
3+
/// A view that represents a dialog box or other interactive component.
4+
///
5+
/// The Dialog element represents a part of an application that a user interacts with
6+
/// to perform a task, such as a modal dialog, alert, or confirmation. By default,
7+
/// dialogs are hidden unless the `open` attribute is set.
8+
///
9+
/// ```swift
10+
/// struct MySiteContent: View {
11+
/// var body: some View {
12+
/// Body {
13+
/// Dialog(open: true) {
14+
/// Heading(level: 2) {
15+
/// "Confirm Action"
16+
/// }
17+
/// Paragraph("Are you sure you want to proceed?")
18+
/// Button("Confirm") {
19+
/// DOMString("Confirm")
20+
/// }
21+
/// }
22+
/// }
23+
/// }
24+
/// }
25+
/// ```
26+
///
27+
/// - SeeAlso: W3C [`dialog`](https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element) specification.
28+
@available(iOS 17.0, macOS 14.0, *)
29+
public struct Dialog<Content>: View where Content: View {
30+
/// Creates a Dialog view.
31+
///
32+
/// - Parameters:
33+
/// - open: Whether the dialog is visible and active. Default is `false`.
34+
/// - content: A view builder that creates the dialog's content.
35+
public init(open: Bool = false, @ViewBuilder content: @escaping @Sendable () -> Content) {
36+
self.open = open
37+
self.content = content
38+
}
39+
40+
@_documentation(visibility: private)
41+
public func render(_ container: Element, environment: EnvironmentValues) throws {
42+
let element = try container.appendElement("dialog")
43+
44+
if open {
45+
try element.attr("open", "")
46+
}
47+
48+
try self.content().render(element, environment: environment)
49+
}
50+
51+
private let open: Bool
52+
@ViewBuilder private let content: @Sendable () -> Content
53+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
import Testing
3+
4+
import Slipstream
5+
6+
struct DialogTests {
7+
@Test func empty() throws {
8+
try #expect(renderHTML(Dialog {}) == "<dialog></dialog>")
9+
}
10+
11+
@Test func withContent() throws {
12+
try #expect(renderHTML(Dialog {
13+
DOMString("Dialog content")
14+
}) == """
15+
<dialog>
16+
Dialog content
17+
</dialog>
18+
""")
19+
}
20+
21+
@Test func openFalse() throws {
22+
try #expect(renderHTML(Dialog(open: false) {
23+
DOMString("Dialog content")
24+
}) == """
25+
<dialog>
26+
Dialog content
27+
</dialog>
28+
""")
29+
}
30+
31+
@Test func openTrue() throws {
32+
try #expect(renderHTML(Dialog(open: true) {
33+
DOMString("Dialog content")
34+
}) == """
35+
<dialog open>
36+
Dialog content
37+
</dialog>
38+
""")
39+
}
40+
41+
@Test func globalAttribute() throws {
42+
try #expect(renderHTML(Dialog(open: true) {
43+
DOMString("Dialog content")
44+
}.language("en")) == """
45+
<dialog open lang="en">
46+
Dialog content
47+
</dialog>
48+
""")
49+
}
50+
}

0 commit comments

Comments
 (0)