datastar/sdk/go/execute-script-sugar.go

235 lines
8.3 KiB
Go

package datastar
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// ConsoleLog is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(msg)`.
func (sse *ServerSentEventGenerator) ConsoleLog(msg string, opts ...ExecuteScriptOption) error {
call := fmt.Sprintf("console.log(%q)", msg)
return sse.ExecuteScript(call, opts...)
}
// ConsoleLogf is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(fmt.Sprintf(format, args...))`.
func (sse *ServerSentEventGenerator) ConsoleLogf(format string, args ...any) error {
return sse.ConsoleLog(fmt.Sprintf(format, args...))
}
// ConsoleError is a convenience method for [see.ExecuteScript].
// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.error(msg)`.
func (sse *ServerSentEventGenerator) ConsoleError(err error, opts ...ExecuteScriptOption) error {
call := fmt.Sprintf("console.error(%q)", err.Error())
return sse.ExecuteScript(call, opts...)
}
// Redirectf is a convenience method for [see.ExecuteScript].
// It sends a redirect event to the client formatted using [fmt.Sprintf].
func (sse *ServerSentEventGenerator) Redirectf(format string, args ...any) error {
url := fmt.Sprintf(format, args...)
return sse.Redirect(url)
}
// Redirect is a convenience method for [see.ExecuteScript].
// It sends a redirect event to the client .
func (sse *ServerSentEventGenerator) Redirect(url string, opts ...ExecuteScriptOption) error {
js := fmt.Sprintf("setTimeout(() => window.location.href = %q)", url)
return sse.ExecuteScript(js, opts...)
}
// dispatchCustomEventOptions holds the configuration data
// modified by [DispatchCustomEventOption]s
// for dispatching custom events to the client.
type dispatchCustomEventOptions struct {
EventID string
RetryDuration time.Duration
Selector string
Bubbles bool
Cancelable bool
Composed bool
}
// DispatchCustomEventOption configures one custom
// server-sent event.
type DispatchCustomEventOption func(*dispatchCustomEventOptions)
// WithDispatchCustomEventEventID configures an optional event ID for the custom event.
// The client message field [lastEventId] will be set to this value.
// If the next event does not have an event ID, the last used event ID will remain.
//
// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
func WithDispatchCustomEventEventID(id string) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.EventID = id
}
}
// WithDispatchCustomEventRetryDuration overrides the [DefaultSseRetryDuration] for one custom event.
func WithDispatchCustomEventRetryDuration(retryDuration time.Duration) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.RetryDuration = retryDuration
}
}
// WithDispatchCustomEventSelector replaces the default custom event target `document` with a
// [CSS selector]. If the selector matches multiple HTML elements, the event will be dispatched
// from each one. For example, if the selector is `#my-element`, the event will be dispatched
// from the element with the ID `my-element`. If the selector is `main > section`, the event will be dispatched
// from each `<section>` element which is a direct child of the `<main>` element.
//
// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors
func WithDispatchCustomEventSelector(selector string) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.Selector = selector
}
}
// WithDispatchCustomEventBubbles overrides the default custom [event bubbling] `true` value.
// Setting bubbling to `false` is equivalent to calling `event.stopPropagation()` Javascript
// command on the client side for the dispatched event. This prevents the event from triggering
// event handlers of its parent elements.
//
// [event bubbling]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
func WithDispatchCustomEventBubbles(bubbles bool) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.Bubbles = bubbles
}
}
// WithDispatchCustomEventCancelable overrides the default custom [event cancelability] `true` value.
// Setting cancelability to `false` is blocks `event.preventDefault()` Javascript
// command on the client side for the dispatched event.
//
// [event cancelability]: https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable
func WithDispatchCustomEventCancelable(cancelable bool) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.Cancelable = cancelable
}
}
// WithDispatchCustomEventComposed overrides the default custom [event composed] `true` value.
// It indicates whether or not the event will propagate across the shadow HTML DOM boundary into
// the document DOM tree. When `false`, the shadow root will be the last node to be offered the event.
//
// [event composed]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
func WithDispatchCustomEventComposed(composed bool) DispatchCustomEventOption {
return func(o *dispatchCustomEventOptions) {
o.Composed = composed
}
}
// DispatchCustomEvent is a convenience method for dispatching a custom event by executing
// a client side script via [sse.ExecuteScript] call. The detail struct is marshaled to JSON and
// passed as a parameter to the event.
func (sse *ServerSentEventGenerator) DispatchCustomEvent(eventName string, detail any, opts ...DispatchCustomEventOption) error {
if eventName == "" {
return fmt.Errorf("eventName is required")
}
detailsJSON, err := json.Marshal(detail)
if err != nil {
return fmt.Errorf("failed to marshal detail: %w", err)
}
const defaultSelector = "document"
options := dispatchCustomEventOptions{
EventID: "",
RetryDuration: DefaultSseRetryDuration,
Selector: defaultSelector,
Bubbles: true,
Cancelable: true,
Composed: true,
}
for _, opt := range opts {
opt(&options)
}
elementsJS := `[document]`
if options.Selector != "" && options.Selector != defaultSelector {
elementsJS = fmt.Sprintf(`document.querySelectorAll(%q)`, options.Selector)
}
js := fmt.Sprintf(`
const elements = %s
const event = new CustomEvent(%q, {
bubbles: %t,
cancelable: %t,
composed: %t,
detail: %s,
});
elements.forEach((element) => {
element.dispatchEvent(event);
});
`,
elementsJS,
eventName,
options.Bubbles,
options.Cancelable,
options.Composed,
string(detailsJSON),
)
executeOptions := make([]ExecuteScriptOption, 0)
if options.EventID != "" {
executeOptions = append(executeOptions, WithExecuteScriptEventID(options.EventID))
}
if options.RetryDuration != 0 {
executeOptions = append(executeOptions, WithExecuteScriptRetryDuration(options.RetryDuration))
}
return sse.ExecuteScript(js, executeOptions...)
}
// ReplaceURL replaces the current URL in the browser's history.
func (sse *ServerSentEventGenerator) ReplaceURL(u url.URL, opts ...ExecuteScriptOption) error {
js := fmt.Sprintf(`window.history.replaceState({}, "", %q)`, u.String())
return sse.ExecuteScript(js, opts...)
}
// ReplaceURLQuerystring is a convenience wrapper for [sse.ReplaceURL] that replaces the query
// string of the current URL request with new a new query built from the provided values.
func (sse *ServerSentEventGenerator) ReplaceURLQuerystring(r *http.Request, values url.Values, opts ...ExecuteScriptOption) error {
// TODO: rename this function to ReplaceURLQuery
u := *r.URL
u.RawQuery = values.Encode()
return sse.ReplaceURL(u, opts...)
}
// Prefetch is a convenience wrapper for [sse.ExecuteScript] that prefetches the provided links.
// It follows the Javascript [speculation rules API] prefetch specification.
//
// [speculation rules API]: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
func (sse *ServerSentEventGenerator) Prefetch(urls ...string) error {
wrappedURLs := make([]string, len(urls))
for i, url := range urls {
wrappedURLs[i] = fmt.Sprintf(`"%s"`, url)
}
script := fmt.Sprintf(`
{
"prefetch": [
{
"source": "list",
"urls": [
%s
]
}
]
}
`, strings.Join(wrappedURLs, ",\n\t\t\t\t"))
return sse.ExecuteScript(
script,
WithExecuteScriptAutoRemove(false),
WithExecuteScriptAttributes("type speculationrules"),
)
}