diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-19 23:40:03 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-19 23:40:03 +0100 |
commit | 92e8596b78982885803994b50c6b35f73f7a403e (patch) | |
tree | 13428243987317da540495215ed4d9e3938fb5cb /src | |
parent | 985a9b0aa469cbe94fb95c433c97e2b321014341 (diff) | |
download | lustre-92e8596b78982885803994b50c6b35f73f7a403e.tar.gz lustre-92e8596b78982885803994b50c6b35f73f7a403e.zip |
:recycle: So long monorepo.
Diffstat (limited to 'src')
-rw-r--r-- | src/lustre.ffi.mjs | 231 | ||||
-rw-r--r-- | src/lustre.gleam | 97 | ||||
-rw-r--r-- | src/lustre/attribute.gleam | 326 | ||||
-rw-r--r-- | src/lustre/effect.gleam | 47 | ||||
-rw-r--r-- | src/lustre/element.gleam | 139 | ||||
-rw-r--r-- | src/lustre/element/html.gleam | 889 | ||||
-rw-r--r-- | src/lustre/element/svg.gleam | 416 | ||||
-rw-r--r-- | src/lustre/event.gleam | 178 | ||||
-rw-r--r-- | src/runtime.ffi.mjs | 239 |
9 files changed, 2562 insertions, 0 deletions
diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs new file mode 100644 index 0000000..f29e7ea --- /dev/null +++ b/src/lustre.ffi.mjs @@ -0,0 +1,231 @@ +import { + AppAlreadyStarted, + AppNotYetStarted, + BadComponentName, + ComponentAlreadyRegistered, + ElementNotFound, + NotABrowser, +} from "./lustre.mjs"; +import { from } from "./lustre/effect.mjs"; +import { morph } from "./runtime.ffi.mjs"; +import { Ok, Error, isEqual } from "./gleam.mjs"; + +// RUNTIME --------------------------------------------------------------------- + +/// +/// +export class App { + #root = null; + #state = null; + #queue = []; + #effects = []; + #didUpdate = false; + + #init = null; + #update = null; + #view = null; + + constructor(init, update, render) { + this.#init = init; + this.#update = update; + this.#view = render; + } + + start(selector, flags) { + if (!is_browser()) return new Error(new NotABrowser()); + if (this.#root) return new Error(new AppAlreadyStarted()); + + this.#root = + selector instanceof HTMLElement + ? selector + : document.querySelector(selector); + + if (!this.#root) return new Error(new ElementNotFound()); + + const [next, effects] = this.#init(flags); + + this.#state = next; + this.#effects = effects[0].toArray(); + this.#didUpdate = true; + + window.requestAnimationFrame(() => this.#tick()); + + return new Ok((msg) => this.dispatch(msg)); + } + + dispatch(msg) { + this.#queue.push(msg); + this.#tick(); + } + + emit(name, event = null) { + this.#root.dispatchEvent( + new CustomEvent(name, { + bubbles: true, + detail: event, + composed: true, + }) + ); + } + + destroy() { + if (!this.#root) return new Error(new AppNotYetStarted()); + + this.#root.remove(); + this.#root = null; + this.#state = null; + this.#queue = []; + this.#effects = []; + this.#didUpdate = false; + this.#update = () => {}; + this.#view = () => {}; + } + + #tick() { + this.#flush(); + + if (this.#didUpdate) { + const vdom = this.#view(this.#state); + + this.#root = morph(this.#root, vdom, (msg) => this.dispatch(msg)); + this.#didUpdate = false; + } + } + + #flush(times = 0) { + if (!this.#root) return; + if (this.#queue.length) { + while (this.#queue.length) { + const [next, effects] = this.#update(this.#state, this.#queue.shift()); + // If the user returned their model unchanged and not reconstructed then + // we don't need to trigger a re-render. + this.#didUpdate ||= this.#state !== next; + this.#state = next; + this.#effects = this.#effects.concat(effects[0].toArray()); + } + } + + // Each update can produce effects which must now be executed. + while (this.#effects.length) + this.#effects.shift()( + (msg) => this.dispatch(msg), + (name, data) => this.emit(name, data) + ); + + // Synchronous effects will immediately queue a message to be processed. If + // it is reasonable, we can process those updates too before proceeding to + // the next render. + if (this.#queue.length) { + times >= 5 ? console.warn(tooManyUpdates) : this.#flush(++times); + } + } +} + +export const setup = (init, update, render) => new App(init, update, render); +export const start = (app, selector, flags) => app.start(selector, flags); +export const destroy = (app) => app.destroy(); + +export const emit = (name, data) => + // Normal `Effect`s constructed in Gleam from `effect.from` don't get told + // about the second argument, but it's there 👀. + from((_, emit) => { + emit(name, data); + }); + +// HTML EVENTS ----------------------------------------------------------------- + +export const prevent_default = (e) => e.preventDefault?.(); +export const stop_propagation = (e) => e.stopPropagation?.(); + +// CUSTOM ELEMENTS ------------------------------------------------------------- + +export const setup_component = ( + name, + init, + update, + render, + on_attribute_change +) => { + if (!name.includes("-")) return new Error(new BadComponentName()); + if (!is_browser()) return new Error(new NotABrowser()); + if (customElements.get(name)) { + return new Error(new ComponentAlreadyRegistered()); + } + + customElements.define( + name, + class extends HTMLElement { + static get observedAttributes() { + return on_attribute_change.entries().map(([name, _]) => name); + } + + #container = document.createElement("div"); + #app = null; + #dispatch = null; + + constructor() { + super(); + + this.#app = new App(init, update, render); + // This is necessary for ✨ reasons ✨. Clearly there's a bug in the + // implementation of either the `App` or the runtime but I con't work it + // out. + // + // If we pass the container to the app directly then the component fails + // to render anything to the ODM. + this.#container.appendChild(document.createElement("div")); + + const dispatch = this.#app.start(this.#container.firstChild); + this.#dispatch = dispatch[0]; + + on_attribute_change.forEach((decoder, name) => { + Object.defineProperty(this, name, { + get: () => { + return this[`_${name}`] || this.getAttribute(name); + }, + + set: (value) => { + const prev = this[name]; + const decoded = decoder(value); + + // We need this equality check to prevent constantly dispatching + // messages when the value is an object or array: it might not have + // changed but its reference might have and we don't want to trigger + // useless updates. + if (decoded.isOk() && !isEqual(prev, decoded[0])) { + this.#dispatch(decoded[0]); + } + + if (typeof value === "string") { + this.setAttribute(name, value); + } else { + this[`_${name}`] = value; + } + }, + }); + }); + } + + connectedCallback() { + this.appendChild(this.#container.firstChild); + } + + attributeChangedCallback(name, prev, next) { + if (prev !== next) { + this[name] = next; + } + } + + disconnectedCallback() { + this.#app.destroy(); + } + } + ); + + return new Ok(null); +}; + +// UTLS ------------------------------------------------------------------------ + +export const is_browser = () => window && window.document; +export const is_registered = (name) => !!customElements.get(name); diff --git a/src/lustre.gleam b/src/lustre.gleam new file mode 100644 index 0000000..4f8249a --- /dev/null +++ b/src/lustre.gleam @@ -0,0 +1,97 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre](https://pkg.hayleigh.dev/lustre/api/lustre) + +// IMPORTS --------------------------------------------------------------------- + +import gleam/dynamic.{Decoder} +import gleam/map.{Map} +import lustre/effect.{Effect} +import lustre/element.{Element} + +// TYPES ----------------------------------------------------------------------- + +/// +pub type App(flags, model, msg) + +pub type Error { + AppAlreadyStarted + AppNotYetStarted + BadComponentName + ComponentAlreadyRegistered + ElementNotFound + NotABrowser +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +@target(javascript) +/// +pub fn element(element: Element(msg)) -> App(Nil, Nil, msg) { + let init = fn(_) { #(Nil, effect.none()) } + let update = fn(_, _) { #(Nil, effect.none()) } + let view = fn(_) { element } + + application(init, update, view) +} + +@target(javascript) +/// +pub fn simple( + init: fn(flags) -> model, + update: fn(model, msg) -> model, + view: fn(model) -> Element(msg), +) -> App(flags, model, msg) { + let init = fn(flags) { #(init(flags), effect.none()) } + let update = fn(model, msg) { #(update(model, msg), effect.none()) } + + application(init, update, view) +} + +@target(javascript) +/// +@external(javascript, "./lustre.ffi.mjs", "setup") +pub fn application( + init: fn(flags) -> #(model, Effect(msg)), + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), +) -> App(flags, model, msg) + +@target(javascript) +@external(javascript, "./lustre.ffi.mjs", "setup_component") +pub fn component( + name: String, + init: fn() -> #(model, Effect(msg)), + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), + on_attribute_change: Map(String, Decoder(msg)), +) -> Result(Nil, Error) + +// EFFECTS --------------------------------------------------------------------- + +@target(javascript) +/// +@external(javascript, "./lustre.ffi.mjs", "start") +pub fn start( + app: App(flags, model, msg), + selector: String, + flags: flags, +) -> Result(fn(msg) -> Nil, Error) + +@target(javascript) +/// +@external(javascript, "./lustre.ffi.mjs", "destroy") +pub fn destroy(app: App(flags, model, msg)) -> Result(Nil, Error) + +// UTILS ----------------------------------------------------------------------- + +/// +@external(javascript, "./lustre.ffi.mjs", "is_browser") +pub fn is_browser() -> Bool { + False +} + +/// +@external(javascript, "./lustre.ffi.mjs", "is_registered") +pub fn is_registered(_name: String) -> Bool { + False +} diff --git a/src/lustre/attribute.gleam b/src/lustre/attribute.gleam new file mode 100644 index 0000000..0c79692 --- /dev/null +++ b/src/lustre/attribute.gleam @@ -0,0 +1,326 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/attribute](https://pkg.hayleigh.dev/lustre/api/lustre/attribute) + +// IMPORTS --------------------------------------------------------------------- + +import gleam/dynamic.{Dynamic} +import gleam/function +import gleam/int +import gleam/list +import gleam/result +import gleam/string +import gleam/string_builder.{StringBuilder} + +// TYPES ----------------------------------------------------------------------- + +/// +pub opaque type Attribute(msg) { + Attribute(String, Dynamic, as_property: Bool) + Event(String, fn(Dynamic) -> Result(msg, Nil)) +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +pub fn attribute(name: String, value: String) -> Attribute(msg) { + Attribute(name, dynamic.from(value), as_property: False) +} + +/// +pub fn property(name: String, value: any) -> Attribute(msg) { + Attribute(name, dynamic.from(value), as_property: True) +} + +/// +pub fn on( + name: String, + handler: fn(Dynamic) -> Result(msg, error), +) -> Attribute(msg) { + Event("on" <> name, function.compose(handler, result.replace_error(_, Nil))) +} + +// MANIPULATIONS --------------------------------------------------------------- + +/// +pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b) { + case attr { + Attribute(name, value, as_property) -> Attribute(name, value, as_property) + Event(on, handler) -> Event(on, fn(e) { result.map(handler(e), f) }) + } +} + +// CONVERSIONS ----------------------------------------------------------------- + +/// +/// +pub fn to_string(attr: Attribute(msg)) -> String { + case attr { + Attribute(name, value, as_property: False) -> { + case dynamic.classify(value) { + "String" -> name <> "=\"" <> dynamic.unsafe_coerce(value) <> "\"" + + // Boolean attributes are determined based on their presence, eg we don't + // want to render `disabled="false"` if the value is `false` we simply + // want to omit the attribute altogether. + "Boolean" -> + case dynamic.unsafe_coerce(value) { + True -> name + False -> "" + } + + // For everything else we'll just make a best-effort serialisation. + _ -> name <> "=\"" <> string.inspect(value) <> "\"" + } + } + Attribute(_, _, as_property: True) -> "" + Event(on, _) -> "data-lustre-on:" <> on + } +} + +/// +pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder { + case attr { + Attribute(name, value, as_property: False) -> { + case dynamic.classify(value) { + "String" -> + [name, "=\"", dynamic.unsafe_coerce(value), "\""] + |> string_builder.from_strings + + // Boolean attributes are determined based on their presence, eg we don't + // want to render `disabled="false"` if the value is `false` we simply + // want to omit the attribute altogether. + "Boolean" -> + case dynamic.unsafe_coerce(value) { + True -> string_builder.from_string(name) + False -> string_builder.new() + } + + // For everything else we'll just make a best-effort serialisation. + _ -> + [name, "=\"", string.inspect(value), "\""] + |> string_builder.from_strings + } + } + Attribute(_, _, as_property: True) -> string_builder.new() + Event(on, _) -> + ["data-lustre-on:", on] + |> string_builder.from_strings + } +} + +// COMMON ATTRIBUTES ----------------------------------------------------------- + +/// +pub fn style(properties: List(#(String, String))) -> Attribute(msg) { + attribute( + "style", + { + use styles, #(name, value) <- list.fold(properties, "") + styles <> name <> ":" <> value <> ";" + }, + ) +} + +/// +pub fn class(name: String) -> Attribute(msg) { + attribute("class", name) +} + +/// +pub fn classes(names: List(#(String, Bool))) -> Attribute(msg) { + attribute( + "class", + names + |> list.filter_map(fn(class) { + case class.1 { + True -> Ok(class.0) + False -> Error(Nil) + } + }) + |> string.join(" "), + ) +} + +/// +pub fn id(name: String) -> Attribute(msg) { + attribute("id", name) +} + +// INPUTS ---------------------------------------------------------------------- + +/// +pub fn type_(name: String) -> Attribute(msg) { + attribute("type", name) +} + +/// +pub fn value(val: Dynamic) -> Attribute(msg) { + property("value", val) +} + +/// +pub fn checked(is_checked: Bool) -> Attribute(msg) { + property("checked", is_checked) +} + +/// +pub fn placeholder(text: String) -> Attribute(msg) { + attribute("placeholder", text) +} + +/// +pub fn selected(is_selected: Bool) -> Attribute(msg) { + property("selected", is_selected) +} + +// INPUT HELPERS --------------------------------------------------------------- + +/// +pub fn accept(types: List(String)) -> Attribute(msg) { + attribute("accept", string.join(types, " ")) +} + +/// +pub fn accept_charset(types: List(String)) -> Attribute(msg) { + attribute("acceptCharset", string.join(types, " ")) +} + +/// +pub fn msg(uri: String) -> Attribute(msg) { + attribute("msg", uri) +} + +/// +pub fn autocomplete(name: String) -> Attribute(msg) { + attribute("autocomplete", name) +} + +/// +pub fn autofocus(should_autofocus: Bool) -> Attribute(msg) { + property("autoFocus", should_autofocus) +} + +/// +pub fn disabled(is_disabled: Bool) -> Attribute(msg) { + property("disabled", is_disabled) +} + +/// +pub fn name(name: String) -> Attribute(msg) { + attribute("name", name) +} + +/// +pub fn pattern(regex: String) -> Attribute(msg) { + attribute("pattern", regex) +} + +/// +pub fn readonly(is_readonly: Bool) -> Attribute(msg) { + property("readonly", is_readonly) +} + +/// +pub fn required(is_required: Bool) -> Attribute(msg) { + property("required", is_required) +} + +/// +pub fn for(id: String) -> Attribute(msg) { + attribute("for", id) +} + +// INPUT RANGES ---------------------------------------------------------------- + +/// +pub fn max(val: String) -> Attribute(msg) { + attribute("max", val) +} + +/// +pub fn min(val: String) -> Attribute(msg) { + attribute("min", val) +} + +/// +pub fn step(val: String) -> Attribute(msg) { + attribute("step", val) +} + +// INPUT TEXT AREAS ------------------------------------------------------------ + +/// +pub fn cols(val: Int) -> Attribute(msg) { + attribute("cols", int.to_string(val)) +} + +/// +pub fn rows(val: Int) -> Attribute(msg) { + attribute("rows", int.to_string(val)) +} + +/// +pub fn wrap(mode: String) -> Attribute(msg) { + attribute("wrap", mode) +} + +// LINKS AND AREAS ------------------------------------------------------------- + +/// +pub fn href(uri: String) -> Attribute(msg) { + attribute("href", uri) +} + +/// +pub fn target(target: String) -> Attribute(msg) { + attribute("target", target) +} + +/// +pub fn download(filename: String) -> Attribute(msg) { + attribute("download", filename) +} + +/// +pub fn rel(relationship: String) -> Attribute(msg) { + attribute("rel", relationship) +} + +// EMBEDDED CONTENT ------------------------------------------------------------ + +/// +pub fn src(uri: String) -> Attribute(msg) { + attribute("src", uri) +} + +/// +pub fn height(val: Int) -> Attribute(msg) { + property("height", int.to_string(val)) +} + +/// +pub fn width(val: Int) -> Attribute(msg) { + property("width", int.to_string(val)) +} + +/// +pub fn alt(text: String) -> Attribute(msg) { + attribute("alt", text) +} + +// AUDIO AND VIDEO ------------------------------------------------------------- + +/// +pub fn autoplay(should_autoplay: Bool) -> Attribute(msg) { + property("autoplay", should_autoplay) +} + +/// +pub fn controls(visible: Bool) -> Attribute(msg) { + property("controls", visible) +} + +/// +pub fn loop(should_loop: Bool) -> Attribute(msg) { + property("loop", should_loop) +} diff --git a/src/lustre/effect.gleam b/src/lustre/effect.gleam new file mode 100644 index 0000000..964ddd2 --- /dev/null +++ b/src/lustre/effect.gleam @@ -0,0 +1,47 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/effect](https://pkg.hayleigh.dev/lustre/api/lustre/effect) + +// IMPORTS --------------------------------------------------------------------- + +import gleam/list + +// TYPES ----------------------------------------------------------------------- + +/// +pub opaque type Effect(msg) { + Effect(List(fn(fn(msg) -> Nil) -> Nil)) +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +pub fn from(effect: fn(fn(msg) -> Nil) -> Nil) -> Effect(msg) { + Effect([effect]) +} + +/// Typically our app's `update` function needs to return a tuple of +/// `#(model, Effect(msg))`. When we don't need to perform any side effects we +/// can just return `none()`! +/// +pub fn none() -> Effect(msg) { + Effect([]) +} + +// MANIPULATIONS --------------------------------------------------------------- + +/// +pub fn batch(effects: List(Effect(msg))) -> Effect(msg) { + Effect({ + use b, Effect(a) <- list.fold(effects, []) + list.append(b, a) + }) +} + +/// +pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b) { + let Effect(l) = effect + Effect(list.map( + l, + fn(effect) { fn(dispatch) { effect(fn(a) { dispatch(f(a)) }) } }, + )) +} diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam new file mode 100644 index 0000000..4608db7 --- /dev/null +++ b/src/lustre/element.gleam @@ -0,0 +1,139 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/element](https://pkg.hayleigh.dev/lustre/api/lustre/element) + +// IMPORTS --------------------------------------------------------------------- + +import gleam/list +import gleam/string +import gleam/string_builder.{StringBuilder} +import lustre/attribute.{Attribute} + +// TYPES ----------------------------------------------------------------------- + +/// +pub opaque type Element(msg) { + Text(String) + Element(String, List(Attribute(msg)), List(Element(msg))) + ElementNs(String, List(Attribute(msg)), List(Element(msg)), String) +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +pub fn element( + tag: String, + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + Element(tag, attrs, children) +} + +/// +pub fn namespaced( + namespace: String, + tag: String, + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + ElementNs(tag, attrs, children, namespace) +} + +/// +pub fn text(content: String) -> Element(msg) { + Text(content) +} + +fn escape(escaped: String, content: String) -> String { + case string.pop_grapheme(content) { + Ok(#("<", xs)) -> escape(escaped <> "<", xs) + Ok(#(">", xs)) -> escape(escaped <> ">", xs) + Ok(#("&", xs)) -> escape(escaped <> "&", xs) + Ok(#("\"", xs)) -> escape(escaped <> """, xs) + Ok(#("'", xs)) -> escape(escaped <> "'", xs) + Ok(#(x, xs)) -> escape(escaped <> x, xs) + Error(_) -> escaped <> content + } +} + +// MANIPULATIONS --------------------------------------------------------------- + +/// +pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b) { + case element { + Text(content) -> Text(content) + Element(tag, attrs, children) -> + Element( + tag, + list.map(attrs, attribute.map(_, f)), + list.map(children, map(_, f)), + ) + ElementNs(tag, attrs, children, namespace) -> + ElementNs( + tag, + list.map(attrs, attribute.map(_, f)), + list.map(children, map(_, f)), + namespace, + ) + } +} + +// CONVERSIONS ----------------------------------------------------------------- + +/// +pub fn to_string(element: Element(msg)) -> String { + to_string_builder(element) + |> string_builder.to_string +} + +/// +pub fn to_string_builder(element: Element(msg)) -> StringBuilder { + case element { + Text(content) -> string_builder.from_string(escape("", content)) + Element("area" as tag, attrs, _) + | Element("base" as tag, attrs, _) + | Element("br" as tag, attrs, _) + | Element("col" as tag, attrs, _) + | Element("embed" as tag, attrs, _) + | Element("hr" as tag, attrs, _) + | Element("img" as tag, attrs, _) + | Element("input" as tag, attrs, _) + | Element("link" as tag, attrs, _) + | Element("meta" as tag, attrs, _) + | Element("param" as tag, attrs, _) + | Element("source" as tag, attrs, _) + | Element("track" as tag, attrs, _) + | Element("wbr" as tag, attrs, _) -> + string_builder.from_string("<" <> tag) + |> attrs_to_string_builder(attrs) + |> string_builder.append(">") + Element(tag, attrs, children) -> + string_builder.from_string("<" <> tag) + |> attrs_to_string_builder(attrs) + |> string_builder.append(">") + |> children_to_string_builder(children) + |> string_builder.append("</" <> tag <> ">") + ElementNs(tag, attrs, children, namespace) -> + string_builder.from_string("<" <> tag) + |> attrs_to_string_builder(attrs) + |> string_builder.append(" xmlns=\"" <> namespace <> "\"") + |> string_builder.append(">") + |> children_to_string_builder(children) + |> string_builder.append("</" <> tag <> ">") + } +} + +fn attrs_to_string_builder( + html: StringBuilder, + attrs: List(Attribute(msg)), +) -> StringBuilder { + use html, attr <- list.fold(attrs, html) + string_builder.append_builder(html, attribute.to_string_builder(attr)) +} + +fn children_to_string_builder( + html: StringBuilder, + children: List(Element(msg)), +) -> StringBuilder { + use html, child <- list.fold(children, html) + string_builder.append_builder(html, to_string_builder(child)) +} diff --git a/src/lustre/element/html.gleam b/src/lustre/element/html.gleam new file mode 100644 index 0000000..a69a5fd --- /dev/null +++ b/src/lustre/element/html.gleam @@ -0,0 +1,889 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/html](https://pkg.hayleigh.dev/lustre/api/lustre/element/html) + +// IMPORTS --------------------------------------------------------------------- + +import lustre/element.{Element, element, namespaced, text} +import lustre/attribute.{Attribute} + +// HTML ELEMENTS: MAIN ROOT ---------------------------------------------------- + +/// +pub fn html( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("html", attrs, children) +} + +// HTML ELEMENTS: DOCUMENT METADATA -------------------------------------------- + +/// +pub fn base(attrs: List(Attribute(msg))) -> Element(msg) { + element("base", attrs, []) +} + +/// +pub fn head( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("head", attrs, children) +} + +/// +pub fn link(attrs: List(Attribute(msg))) -> Element(msg) { + element("link", attrs, []) +} + +/// +pub fn meta(attrs: List(Attribute(msg))) -> Element(msg) { + element("meta", attrs, []) +} + +/// +pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) { + element("style", attrs, [text(css)]) +} + +/// +pub fn title(attrs: List(Attribute(msg)), content: String) -> Element(msg) { + element("title", attrs, [text(content)]) +} + +// HTML ELEMENTS: SECTIONING ROOT ----------------------------------------------- + +/// +pub fn body( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("body", attrs, children) +} + +// HTML ELEMENTS: CONTENT SECTIONING ------------------------------------------- + +/// +pub fn address( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("address", attrs, children) +} + +/// +pub fn article( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("article", attrs, children) +} + +/// +pub fn aside( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("aside", attrs, children) +} + +/// +pub fn footer( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("footer", attrs, children) +} + +/// +pub fn header( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("header", attrs, children) +} + +/// +pub fn h1( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h1", attrs, children) +} + +/// +pub fn h2( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h2", attrs, children) +} + +/// +pub fn h3( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h3", attrs, children) +} + +/// +pub fn h4( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h4", attrs, children) +} + +/// +pub fn h5( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h5", attrs, children) +} + +/// +pub fn h6( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("h6", attrs, children) +} + +/// +pub fn hgroup( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("hgroup", attrs, children) +} + +/// +pub fn main( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("main", attrs, children) +} + +/// +pub fn nav( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("nav", attrs, children) +} + +/// +pub fn section( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("section", attrs, children) +} + +/// +pub fn search( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("search", attrs, children) +} + +// HTML ELEMENTS: TEXT CONTENT ------------------------------------------------- + +/// +pub fn blockquote( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("blockquote", attrs, children) +} + +/// +pub fn dd( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("dd", attrs, children) +} + +/// +pub fn div( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("div", attrs, children) +} + +/// +pub fn dl( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("dl", attrs, children) +} + +/// +pub fn dt( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("dt", attrs, children) +} + +/// +pub fn figcaption( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("figcaption", attrs, children) +} + +/// +pub fn figure( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("figure", attrs, children) +} + +/// +pub fn hr(attrs: List(Attribute(msg))) -> Element(msg) { + element("hr", attrs, []) +} + +/// +pub fn li( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("li", attrs, children) +} + +/// +pub fn menu( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("menu", attrs, children) +} + +/// +pub fn ol( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("ol", attrs, children) +} + +/// +pub fn p( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("p", attrs, children) +} + +/// +pub fn pre( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("pre", attrs, children) +} + +/// +pub fn ul( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("ul", attrs, children) +} + +// HTML ELEMENTS: INLINE TEXT SEMANTICS ---------------------------------------- + +/// +pub fn a( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("a", attrs, children) +} + +/// +pub fn abbr( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("abbr", attrs, children) +} + +/// +pub fn b( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("b", attrs, children) +} + +/// +pub fn bdi( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("bdi", attrs, children) +} + +/// +pub fn bdo( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("bdo", attrs, children) +} + +/// +pub fn br(attrs: List(Attribute(msg))) -> Element(msg) { + element("br", attrs, []) +} + +/// +pub fn cite( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("cite", attrs, children) +} + +/// +pub fn code( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("code", attrs, children) +} + +/// +pub fn data( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("data", attrs, children) +} + +/// +pub fn dfn( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("dfn", attrs, children) +} + +/// +pub fn em( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("em", attrs, children) +} + +/// +pub fn i( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("i", attrs, children) +} + +/// +pub fn kbd( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("kbd", attrs, children) +} + +/// +pub fn mark( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("mark", attrs, children) +} + +/// +pub fn q( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("q", attrs, children) +} + +/// +pub fn rp( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("rp", attrs, children) +} + +/// +pub fn rt( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("rt", attrs, children) +} + +/// +pub fn ruby( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("ruby", attrs, children) +} + +/// +pub fn s( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("s", attrs, children) +} + +/// +pub fn samp( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("samp", attrs, children) +} + +/// +pub fn small( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("small", attrs, children) +} + +/// +pub fn span( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("span", attrs, children) +} + +/// +pub fn strong( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("strong", attrs, children) +} + +/// +pub fn sub( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("sub", attrs, children) +} + +/// +pub fn sup( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("sup", attrs, children) +} + +/// +pub fn time( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("time", attrs, children) +} + +/// +pub fn u( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("u", attrs, children) +} + +/// +pub fn var( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("var", attrs, children) +} + +/// +pub fn wbr(attrs: List(Attribute(msg))) -> Element(msg) { + element("wbr", attrs, []) +} + +// HTML ELEMENTS: IMAGE AND MULTIMEDIA ----------------------------------------- + +/// +pub fn area(attrs: List(Attribute(msg))) -> Element(msg) { + element("area", attrs, []) +} + +/// +pub fn audio( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("audio", attrs, children) +} + +/// +pub fn img(attrs: List(Attribute(msg))) -> Element(msg) { + element("img", attrs, []) +} + +/// Used with <area> elements to define an image map (a clickable link area). +/// +pub fn map( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("map", attrs, children) +} + +/// +pub fn track(attrs: List(Attribute(msg))) -> Element(msg) { + element("track", attrs, []) +} + +/// +pub fn video( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("video", attrs, children) +} + +// HTML ELEMENTS: EMBEDDED CONTENT --------------------------------------------- + +/// +pub fn embed(attrs: List(Attribute(msg))) -> Element(msg) { + element("embed", attrs, []) +} + +/// +pub fn iframe(attrs: List(Attribute(msg))) -> Element(msg) { + element("iframe", attrs, []) +} + +/// +pub fn object(attrs: List(Attribute(msg))) -> Element(msg) { + element("object", attrs, []) +} + +/// +pub fn picture( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("picture", attrs, children) +} + +/// +pub fn portal(attrs: List(Attribute(msg))) -> Element(msg) { + element("portal", attrs, []) +} + +/// +pub fn source(attrs: List(Attribute(msg))) -> Element(msg) { + element("source", attrs, []) +} + +// HTML ELEMENTS: SVG AND MATHML ----------------------------------------------- + +/// +pub fn svg( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced("http://www.w3.org/2000/svg", "svg", attrs, children) +} + +/// +pub fn math( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("math", attrs, children) +} + +// HTML ELEMENTS: SCRIPTING ---------------------------------------------------- + +/// +pub fn canvas(attrs: List(Attribute(msg))) -> Element(msg) { + element("canvas", attrs, []) +} + +/// +pub fn noscript( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element("noscript", attrs, children) +} + +/// +pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) { + element("script", attrs, [text(js)]) +} + +// HTML ELEMENTS: DEMARCATING EDITS --------------------------------------------- + +/// +pub fn del( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("del", attrs, children) +} + +/// +pub fn ins( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("ins", attrs, children) +} + +// HTML ELEMENTS: TABLE CONTENT ------------------------------------------------ + +/// +pub fn caption( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("caption", attrs, children) +} + +/// +pub fn col(attrs: List(Attribute(msg))) -> Element(msg) { + element.element("col", attrs, []) +} + +/// +pub fn colgroup( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("colgroup", attrs, children) +} + +/// +pub fn table( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("table", attrs, children) +} + +/// +pub fn tbody( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("tbody", attrs, children) +} + +/// +pub fn td( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("td", attrs, children) +} + +/// +pub fn tfoot( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("tfoot", attrs, children) +} + +/// +pub fn th( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("th", attrs, children) +} + +/// +pub fn thead( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("thead", attrs, children) +} + +/// +pub fn tr( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("tr", attrs, children) +} + +// HTML ELEMENTS: FORMS -------------------------------------------------------- + +/// +pub fn button( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("button", attrs, children) +} + +/// +pub fn datalist( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("datalist", attrs, children) +} + +/// +pub fn fieldset( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("fieldset", attrs, children) +} + +/// +pub fn form( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("form", attrs, children) +} + +/// +pub fn input(attrs: List(Attribute(msg))) -> Element(msg) { + element.element("input", attrs, []) +} + +/// +pub fn label( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("label", attrs, children) +} + +/// +pub fn legend( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("legend", attrs, children) +} + +/// +pub fn meter( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("meter", attrs, children) +} + +/// +pub fn optgroup( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("optgroup", attrs, children) +} + +/// +pub fn option(attrs: List(Attribute(msg))) -> Element(msg) { + element.element("option", attrs, []) +} + +/// +pub fn output( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("output", attrs, children) +} + +/// +pub fn progress( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("progress", attrs, children) +} + +/// +pub fn select( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("select", attrs, children) +} + +/// +pub fn textarea(attrs: List(Attribute(msg))) -> Element(msg) { + element.element("textarea", attrs, []) +} + +// HTML ELEMENTS: INTERACTIVE ELEMENTS ----------------------------------------- + +/// +pub fn details( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("details", attrs, children) +} + +/// +pub fn dialog( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("dialog", attrs, children) +} + +/// +pub fn summary( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("summary", attrs, children) +} + +// HTML ELEMENTS: WEB COMPONENTS ----------------------------------------------- + +/// +pub fn slot(attrs: List(Attribute(msg))) -> Element(msg) { + element.element("slot", attrs, []) +} + +/// +pub fn template( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + element.element("template", attrs, children) +} diff --git a/src/lustre/element/svg.gleam b/src/lustre/element/svg.gleam new file mode 100644 index 0000000..a1fc0ce --- /dev/null +++ b/src/lustre/element/svg.gleam @@ -0,0 +1,416 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/svg](https://pkg.hayleigh.dev/lustre/api/lustre/element/svg) + +// IMPORTS --------------------------------------------------------------------- + +import lustre/element.{Element, namespaced, text as inline_text} +import lustre/attribute.{Attribute} + +// CONSTANTS ------------------------------------------------------------------- + +const namespace = "http://www.w3.org/2000/svg" + +// The doc comments (and order) for functions in this module are taken from the +// MDN Element reference: +// +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element +// + +// SVG ELEMENTS: ANIMATION ELEMENTS -------------------------------------------- + +/// +pub fn animate(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "animate", attrs, []) +} + +/// +pub fn animate_motion(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "animateMotion", attrs, []) +} + +/// +pub fn animate_transform(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "animateTransform", attrs, []) +} + +/// +pub fn mpath(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "mpath", attrs, []) +} + +/// +pub fn set(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "set", attrs, []) +} + +// SVG ELEMENTS: BASIC SHAPES -------------------------------------------------- + +/// +pub fn circle(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "circle", attrs, []) +} + +/// +pub fn ellipse(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "ellipse", attrs, []) +} + +/// +pub fn line(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "line", attrs, []) +} + +/// +pub fn polygon(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "polygon", attrs, []) +} + +/// +pub fn polyline(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "polyline", attrs, []) +} + +/// +pub fn rect(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "rect", attrs, []) +} + +// SVG ELEMENTS: CONTAINER ELEMENTS -------------------------------------------- + +/// +pub fn a( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "a", attrs, children) +} + +/// +pub fn defs( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "defs", attrs, children) +} + +/// +pub fn g( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "g", attrs, children) +} + +/// +pub fn marker( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "marker", attrs, children) +} + +/// +pub fn mask( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "mask", attrs, children) +} + +/// +pub fn missing_glyph( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "missing-glyph", attrs, children) +} + +/// +pub fn pattern( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "pattern", attrs, children) +} + +/// +pub fn svg( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "svg", attrs, children) +} + +/// +pub fn switch( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "switch", attrs, children) +} + +/// +pub fn symbol( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "symbol", attrs, children) +} + +// SVG ELEMENTS: DESCRIPTIVE ELEMENTS ------------------------------------------ + +/// +pub fn desc( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "desc", attrs, children) +} + +/// +pub fn metadata( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "metadata", attrs, children) +} + +/// +pub fn title( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "title", attrs, children) +} + +// SVG ELEMENTS: FILTER EFFECTS ------------------------------------------------ + +/// +pub fn fe_blend(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feBlend", attrs, []) +} + +/// +pub fn fe_color_matrix(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feColorMatrix", attrs, []) +} + +/// +pub fn fe_component_transfer(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feComponentTransfer", attrs, []) +} + +/// +pub fn fe_composite(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feComposite", attrs, []) +} + +/// +pub fn fe_convolve_matrix(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feConvolveMatrix", attrs, []) +} + +/// +pub fn fe_diffuse_lighting( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "feDiffuseLighting", attrs, children) +} + +/// +pub fn fe_displacement_map(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feDisplacementMap", attrs, []) +} + +/// +pub fn fe_drop_shadow(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feDropShadow", attrs, []) +} + +/// +pub fn fe_flood(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feFlood", attrs, []) +} + +/// +pub fn fe_func_a(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feFuncA", attrs, []) +} + +/// +pub fn fe_func_b(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feFuncB", attrs, []) +} + +/// +pub fn fe_func_g(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feFuncG", attrs, []) +} + +/// +pub fn fe_func_r(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feFuncR", attrs, []) +} + +/// +pub fn fe_gaussian_blur(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feGaussianBlur", attrs, []) +} + +/// +pub fn fe_image(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feImage", attrs, []) +} + +/// +pub fn fe_merge( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "feMerge", attrs, children) +} + +/// +pub fn fe_merge_node(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feMergeNode", attrs, []) +} + +/// +pub fn fe_morphology(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feMorphology", attrs, []) +} + +/// +pub fn fe_offset(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feOffset", attrs, []) +} + +/// +pub fn fe_specular_lighting( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "feSpecularLighting", attrs, children) +} + +/// +pub fn fe_tile( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "feTile", attrs, children) +} + +/// +pub fn fe_turbulence(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feTurbulence", attrs, []) +} + +// SVG ELEMENTS: GRADIENT ELEMENTS --------------------------------------------- + +/// +pub fn linear_gradient( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "linearGradient", attrs, children) +} + +/// +pub fn radial_gradient( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "radialGradient", attrs, children) +} + +/// +pub fn stop(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "stop", attrs, []) +} + +// SVG ELEMENTS: GRAPHICAL ELEMENTS -------------------------------------------- + +/// +pub fn image(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "image", attrs, []) +} + +/// +pub fn path(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "path", attrs, []) +} + +/// +pub fn text(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "text", attrs, []) +} + +/// +pub fn use_(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "use", attrs, []) +} + +// SVG ELEMENTS: LIGHTING ELEMENTS --------------------------------------------- + +/// +pub fn fe_distant_light(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feDistantLight", attrs, []) +} + +/// +pub fn fe_point_light(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "fePointLight", attrs, []) +} + +/// +pub fn fe_spot_light(attrs: List(Attribute(msg))) -> Element(msg) { + namespaced(namespace, "feSpotLight", attrs, []) +} + +// SVG ELEMENTS: NEVER-RENDERED ELEMENTS --------------------------------------- + +/// +pub fn clip_path( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "clipPath", attrs, children) +} + +/// +pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) { + namespaced(namespace, "script", attrs, [inline_text(js)]) +} + +/// +pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) { + namespaced(namespace, "style", attrs, [inline_text(css)]) +} + +// SVG ELEMENTS: RENDERABLE ELEMENTS ------------------------------------------- + +/// +pub fn foreign_object( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "foreignObject", attrs, children) +} + +/// +pub fn text_path( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "textPath", attrs, children) +} + +/// +pub fn tspan( + attrs: List(Attribute(msg)), + children: List(Element(msg)), +) -> Element(msg) { + namespaced(namespace, "tspan", attrs, children) +} diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam new file mode 100644 index 0000000..7913ef5 --- /dev/null +++ b/src/lustre/event.gleam @@ -0,0 +1,178 @@ +//// To read the full documentation for this module, please visit +//// [https://pkg.hayleigh.dev/lustre/api/lustre/event](https://pkg.hayleigh.dev/lustre/api/lustre/event) + +// IMPORTS --------------------------------------------------------------------- + +import gleam/dynamic.{DecodeError, Dynamic} +import gleam/result +import lustre/attribute.{Attribute} +import lustre/effect.{Effect} + +// TYPES ----------------------------------------------------------------------- + +type Decoded(a) = + Result(a, List(DecodeError)) + +// EFFECTS --------------------------------------------------------------------- + +@target(javascript) +/// +@external(javascript, "../lustre.ffi.mjs", "emit") +pub fn emit(event: String, data: any) -> Effect(msg) + +// CUSTOM EVENTS --------------------------------------------------------------- + +/// +pub fn on( + name: String, + handler: fn(Dynamic) -> Result(msg, error), +) -> Attribute(msg) { + attribute.on(name, handler) +} + +// MOUSE EVENTS ---------------------------------------------------------------- + +/// +pub fn on_click(msg: msg) -> Attribute(msg) { + use _ <- on("click") + Ok(msg) +} + +/// +pub fn on_mouse_down(msg: msg) -> Attribute(msg) { + use _ <- on("mousedown") + Ok(msg) +} + +/// +pub fn on_mouse_up(msg: msg) -> Attribute(msg) { + use _ <- on("mouseup") + Ok(msg) +} + +/// +pub fn on_mouse_enter(msg: msg) -> Attribute(msg) { + use _ <- on("mouseenter") + Ok(msg) +} + +/// +pub fn on_mouse_leave(msg: msg) -> Attribute(msg) { + use _ <- on("mouseleave") + Ok(msg) +} + +/// +pub fn on_mouse_over(msg: msg) -> Attribute(msg) { + use _ <- on("mouseover") + Ok(msg) +} + +/// +pub fn on_mouse_out(msg: msg) -> Attribute(msg) { + use _ <- on("mouseout") + Ok(msg) +} + +// KEYBOARD EVENTS ------------------------------------------------------------- + +/// Listens for key presses on an element, and dispatches a message with the +/// current key being pressed. +/// +pub fn on_keypress(msg: fn(String) -> msg) -> Attribute(msg) { + use event <- on("keypress") + + event + |> dynamic.field("key", dynamic.string) + |> result.map(msg) +} + +/// Listens for key dow events on an element, and dispatches a message with the +/// current key being pressed. +/// +pub fn on_keydown(msg: fn(String) -> msg) -> Attribute(msg) { + use event <- on("keydown") + + event + |> dynamic.field("key", dynamic.string) + |> result.map(msg) +} + +/// Listens for key up events on an element, and dispatches a message with the +/// current key being released. +/// +pub fn on_keyup(msg: fn(String) -> msg) -> Attribute(msg) { + use event <- on("keyup") + + event + |> dynamic.field("key", dynamic.string) + |> result.map(msg) +} + +// FORM EVENTS ----------------------------------------------------------------- + +/// +pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) { + use event <- on("input") + + value(event) + |> result.map(msg) +} + +pub fn on_check(msg: fn(Bool) -> msg) -> Attribute(msg) { + use event <- on("change") + + checked(event) + |> result.map(msg) +} + +pub fn on_submit(msg: msg) -> Attribute(msg) { + use event <- on("submit") + let _ = prevent_default(event) + + Ok(msg) +} + +// FOCUS EVENTS ---------------------------------------------------------------- + +pub fn on_focus(msg: msg) -> Attribute(msg) { + use _ <- on("focus") + Ok(msg) +} + +pub fn on_blur(msg: msg) -> Attribute(msg) { + use _ <- on("blur") + Ok(msg) +} + +// DECODERS -------------------------------------------------------------------- + +/// +pub fn value(event: Dynamic) -> Decoded(String) { + event + |> dynamic.field("target", dynamic.field("value", dynamic.string)) +} + +/// +pub fn checked(event: Dynamic) -> Decoded(Bool) { + event + |> dynamic.field("target", dynamic.field("checked", dynamic.bool)) +} + +/// +pub fn mouse_position(event: Dynamic) -> Decoded(#(Float, Float)) { + use x <- result.then(dynamic.field("clientX", dynamic.float)(event)) + use y <- result.then(dynamic.field("clientY", dynamic.float)(event)) + + Ok(#(x, y)) +} + +// UTILS ----------------------------------------------------------------------- + +@target(javascript) +@external(javascript, "../lustre.ffi.mjs", "prevent_default") +pub fn prevent_default(event: Dynamic) -> Nil + +@target(javascript) +@external(javascript, "../lustre.ffi.mjs", "stop_propagation") +pub fn stop_propagation(event: Dynamic) -> Nil diff --git a/src/runtime.ffi.mjs b/src/runtime.ffi.mjs new file mode 100644 index 0000000..dd02f8b --- /dev/null +++ b/src/runtime.ffi.mjs @@ -0,0 +1,239 @@ +import { Empty } from "./gleam.mjs"; +import { map as result_map } from "../gleam_stdlib/gleam/result.mjs"; + +export function morph(prev, curr, dispatch, parent) { + if (curr[3]) { + return prev?.nodeType === 1 && + prev.nodeName === curr[0].toUpperCase() && + prev.namespaceURI === curr[3] + ? morphElement(prev, curr, curr[3], dispatch, parent) + : createElement(prev, curr, curr[3], dispatch, parent); + } + + if (curr[2]) { + return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase() + ? morphElement(prev, curr, null, dispatch, parent) + : createElement(prev, curr, null, dispatch, parent); + } + + if (curr[0] && typeof curr[0] === "string") { + return prev?.nodeType === 3 + ? morphText(prev, curr) + : createText(prev, curr); + } + + return document.createComment( + [ + "[internal lustre error] I couldn't work out how to render this element. This", + "function should only be called internally by lustre's runtime: if you think", + "this is an error, please open an issue at", + "https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new", + ].join(" ") + ); +} + +// ELEMENTS -------------------------------------------------------------------- + +function createElement(prev, curr, ns, dispatch, parent = null) { + const el = ns + ? document.createElementNS(ns, curr[0]) + : document.createElement(curr[0]); + + el.$lustre = {}; + + let attr = curr[1]; + while (attr.head) { + morphAttr( + el, + attr.head[0], + attr.head[0] === "class" && el.className + ? `${el.className} ${attr.head[1]}` + : attr.head[1], + dispatch + ); + + attr = attr.tail; + } + + if (customElements.get(curr[0])) { + el._slot = curr[2]; + } else if (curr[0] === "slot") { + let child = new Empty(); + let parentWithSlot = parent; + + while (parentWithSlot) { + if (parentWithSlot._slot) { + child = parentWithSlot._slot; + break; + } else { + parentWithSlot = parentWithSlot.parentNode; + } + } + + while (child.head) { + el.appendChild(morph(null, child.head, dispatch, el)); + child = child.tail; + } + } else { + let child = curr[2]; + while (child.head) { + el.appendChild(morph(null, child.head, dispatch, el)); + child = child.tail; + } + + if (prev) prev.replaceWith(el); + } + + return el; +} + +function morphElement(prev, curr, ns, dispatch, parent) { + const prevAttrs = prev.attributes; + const currAttrs = new Map(); + + let currAttr = curr[1]; + while (currAttr.head) { + currAttrs.set( + currAttr.head[0], + currAttr.head[0] === "class" && currAttrs.has("class") + ? `${currAttrs.get("class")} ${currAttr.head[1]}` + : currAttr.head[1] + ); + + currAttr = currAttr.tail; + } + + for (const { name, value: prevValue } of prevAttrs) { + if (!currAttrs.has(name)) { + prev.removeAttribute(name); + } else { + const value = currAttrs.get(name); + + if (value !== prevValue) { + morphAttr(prev, name, value, dispatch); + currAttrs.delete(name); + } + } + } + + for (const [name, value] of currAttrs) { + morphAttr(prev, name, value, dispatch); + } + + if (customElements.get(curr[0])) { + prev._slot = curr[2]; + } else if (curr[0] === "slot") { + let prevChild = prev.firstChild; + let currChild = new Empty(); + let parentWithSlot = parent; + + while (parentWithSlot) { + if (parentWithSlot._slot) { + currChild = parentWithSlot._slot; + break; + } else { + parentWithSlot = parentWithSlot.parentNode; + } + } + + while (prevChild) { + if (currChild.head) { + morph(prevChild, currChild.head, dispatch, prev); + currChild = currChild.tail; + } + + prevChild = prevChild.nextSibling; + } + + while (currChild.head) { + prev.appendChild(morph(null, currChild.head, dispatch, prev)); + currChild = currChild.tail; + } + } else { + let prevChild = prev.firstChild; + let currChild = curr[2]; + + while (prevChild) { + if (currChild.head) { + const next = prevChild.nextSibling; + morph(prevChild, currChild.head, dispatch, prev); + currChild = currChild.tail; + prevChild = next; + } else { + const next = prevChild.nextSibling; + prevChild.remove(); + prevChild = next; + } + } + + while (currChild.head) { + prev.appendChild(morph(null, currChild.head, dispatch, prev)); + currChild = currChild.tail; + } + } + + return prev; +} + +// ATTRIBUTES ------------------------------------------------------------------ + +function morphAttr(el, name, value, dispatch) { + switch (typeof value) { + case "string": + if (el.getAttribute(name) !== value) el.setAttribute(name, value); + if (value === "") el.removeAttribute(name); + if (name === "value" && el.value !== value) el.value = value; + break; + + // Event listeners need to be handled slightly differently because we need + // to be able to support custom events. We + case name.startsWith("on") && "function": { + if (el.$lustre[name] === value) break; + + const event = name.slice(2).toLowerCase(); + const handler = (e) => result_map(value(e), dispatch); + + if (el.$lustre[`${name}Handler`]) { + el.removeEventListener(event, el.$lustre[`${name}Handler`]); + } + + el.addEventListener(event, handler); + + el.$lustre[name] = value; + el.$lustre[`${name}Handler`] = handler; + + break; + } + + default: + el[name] = value; + } +} + +// TEXT ------------------------------------------------------------------------ + +function createText(prev, curr) { + if (!curr[0]) { + prev?.remove(); + return null; + } + + const el = document.createTextNode(curr[0]); + + if (prev) prev.replaceWith(el); + return el; +} + +function morphText(prev, curr) { + const prevValue = prev.nodeValue; + const currValue = curr[0]; + + if (!currValue) { + prev?.remove(); + return null; + } + + if (prevValue !== currValue) prev.nodeValue = currValue; + + return prev; +} |