commit a679aeb9785df313c0e37f14a903c1177a9ed382 Author: davidontop Date: Wed Sep 18 21:14:02 2024 +0200 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93aef27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +shared/src/main/scala/top/davidon/sfs/dom/defs +target/ \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..bc46528 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,6 @@ +version = 3.8.3 +runner.dialect = scala3 +rewrite.scala3.newSyntax.control = true +rewrite.scala3.newSyntax.deprecated = true +rewrite.scala3.removeOptionalBraces.enabled = false +rewrite.scala3.convertToNewSyntax = true \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6db395b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 David Kozak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..9b108f8 --- /dev/null +++ b/build.sbt @@ -0,0 +1,26 @@ +ThisBuild / version := "0.1.0" +ThisBuild / organization := "top.davidon" + +ThisBuild / scalaVersion := "3.5.0" + +lazy val precompile = taskKey[Unit]("pre compilation tasks") + +precompile := DomGenerator.generate() + +(Compile / compile) := ((Compile / compile) dependsOn precompile).value + +lazy val root = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file(".")) + .settings( + name := "sfs", + libraryDependencies ++= Seq( + "de.tu-darmstadt.stg" %%% "reactives" % "0.36.0", + "org.scala-js" % "scalajs-dom" % "2.8.0" % "sjs1_3" + ) + ) + .jvmSettings( + libraryDependencies ++= Seq( + "org.scala-js" % "scalajs-dom_sjs1_3" % "2.8.0" + ) + ) diff --git a/project/DomGenerator.scala b/project/DomGenerator.scala new file mode 100644 index 0000000..d1135be --- /dev/null +++ b/project/DomGenerator.scala @@ -0,0 +1,367 @@ +import com.raquo.domtypes.codegen.* +import com.raquo.domtypes.codegen.DefType.LazyVal +import com.raquo.domtypes.common.{HtmlTagType, SvgTagType} + +object DomGenerator { + + def generate(): Unit = { + val defGroups = new CanonicalDefGroups() + + { + val traitName = "HtmlTags" + + val fileContent = generator.generateTagsTrait( + tagType = HtmlTagType, + defGroups = defGroups.htmlTagsDefGroups, + printDefGroupComments = true, + traitCommentLines = Nil, + traitModifiers = Nil, + traitName = traitName, + keyKind = "HtmlTag", + baseImplDefComments = List( + "Create HTML tag", + "", + "Note: this simply creates an instance of HtmlTag.", + " - This does not create the element (to do that, call .apply() on the returned tag instance)", + " - This does not register this tag name as a custom element", + " - See https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements", + "", + "@param name - e.g. \"div\" or \"mwc-input\"", + "", + "@tparam Ref - type of elements with this tag, e.g. dom.html.Input for \"input\" tag" + ), + keyImplName = "htmlTag", + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.tagDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val traitName = "SvgTags" + + val fileContent = generator.generateTagsTrait( + tagType = SvgTagType, + defGroups = defGroups.svgTagsDefGroups, + printDefGroupComments = false, + traitCommentLines = Nil, + traitModifiers = Nil, + traitName = traitName, + keyKind = "SvgTag", + baseImplDefComments = List( + "Create SVG tag", + "", + "Note: this simply creates an instance of HtmlTag.", + " - This does not create the element (to do that, call .apply() on the returned tag instance)", + "", + "@param name - e.g. \"circle\"", + "", + "@tparam Ref - type of elements with this tag, e.g. dom.svg.Circle for \"circle\" tag" + ), + keyImplName = "svgTag", + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.tagDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val traitName = "HtmlAttrs" + + val fileContent = generator.generateAttrsTrait( + defGroups = defGroups.htmlAttrDefGroups, + printDefGroupComments = false, + traitCommentLines = Nil, + traitModifiers = Nil, + traitName = traitName, + keyKind = "HtmlAttr", + implNameSuffix = "HtmlAttr", + baseImplDefComments = List( + "Create HTML attribute (Note: for SVG attrs, use L.svg.svgAttr)", + "", + "@param name - name of the attribute, e.g. \"value\"", + "@param codec - used to encode V into String, e.g. StringAsIsCodec", + "", + "@tparam V - value type for this attr in Scala" + ), + baseImplName = "htmlAttr", + namespaceImports = Nil, + namespaceImpl = _ => ???, + transformAttrDomName = identity, + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.attrDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val traitName = "SvgAttrs" + + val fileContent = generator.generateAttrsTrait( + defGroups = defGroups.svgAttrDefGroups, + printDefGroupComments = false, + traitModifiers = Nil, + traitName = traitName, + traitCommentLines = Nil, + keyKind = "SvgAttr", + baseImplDefComments = List( + "Create SVG attribute (Note: for HTML attrs, use L.htmlAttr)", + "", + "@param name - name of the attribute, e.g. \"value\"", + "@param codec - used to encode V into String, e.g. StringAsIsCodec", + "", + "@tparam V - value type for this attr in Scala" + ), + implNameSuffix = "SvgAttr", + baseImplName = "svgAttr", + namespaceImports = Nil, + namespaceImpl = SourceRepr(_), + transformAttrDomName = identity, + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.attrDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val traitName = "AriaAttrs" + + def transformAttrDomName(ariaAttrName: String): String = { + // format: off + if (ariaAttrName.startsWith("aria-")) { + ariaAttrName.substring(5) + } else { + throw new Exception( + s"Aria attribute does not start with `aria-`: $ariaAttrName" + ) + } + // format: on + } + + val fileContent = generator.generateAttrsTrait( + defGroups = defGroups.ariaAttrDefGroups, + printDefGroupComments = false, + traitModifiers = Nil, + traitName = traitName, + traitCommentLines = Nil, + keyKind = "AriaAttr", + implNameSuffix = "AriaAttr", + baseImplDefComments = List( + "Create ARIA attribute (Note: for HTML attrs, use L.htmlAttr)", + "", + "@param name - suffix of the attribute, without \"aria-\" prefix, e.g. \"labelledby\"", + "@param codec - used to encode V into String, e.g. StringAsIsCodec", + "", + "@tparam V - value type for this attr in Scala" + ), + baseImplName = "ariaAttr", + namespaceImports = Nil, + namespaceImpl = _ => ???, + transformAttrDomName = transformAttrDomName, + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.attrDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val traitName = "HtmlProps" + + val fileContent = generator.generatePropsTrait( + defGroups = defGroups.propDefGroups, + printDefGroupComments = true, + traitCommentLines = Nil, + traitModifiers = Nil, + traitName = traitName, + keyKind = "HtmlProp", + implNameSuffix = "Prop", + baseImplDefComments = List( + "Create custom HTML element property", + "", + "@param name - name of the prop in JS, e.g. \"value\"", + "@param codec - used to encode V into DomV, e.g. StringAsIsCodec,", + "", + "@tparam V - value type for this prop in Scala", + "@tparam DomV - value type for this prop in the underlying JS DOM." + ), + baseImplName = "htmlProp", + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.propDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + + { + val baseTraitName = "GlobalEventProps" + + val subTraits = List( + "WindowEventProps" -> defGroups.windowEventPropDefGroups, + "DocumentEventProps" -> defGroups.documentEventPropDefGroups + ) + + { + val fileContent = generator.generateEventPropsTrait( + defSources = defGroups.globalEventPropDefGroups, + printDefGroupComments = true, + traitCommentLines = Nil, + traitModifiers = Nil, + traitName = baseTraitName, + traitExtends = Nil, + traitThisType = None, + baseImplDefComments = List( + "Create custom event property", + "", + "@param name - event type in JS, e.g. \"click\"", + "", + "@tparam Ev - event type in JS, e.g. dom.MouseEvent" + ), + outputBaseImpl = true, + keyKind = "EventProp", + keyImplName = "eventProp", + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.eventPropDefsPackagePath, + fileName = baseTraitName, + fileContent = fileContent + ) + } + + subTraits.foreach { case (traitName, eventPropsDefGroups) => + val fileContent = generator.generateEventPropsTrait( + defSources = eventPropsDefGroups, + printDefGroupComments = true, + traitCommentLines = List(eventPropsDefGroups.head._1), + traitModifiers = Nil, + traitName = traitName, + traitExtends = Nil, + traitThisType = Some(baseTraitName), + baseImplDefComments = Nil, + outputBaseImpl = false, + keyKind = "EventProp", + keyImplName = "eventProp", + defType = LazyVal + ) + + generator.writeToFile( + packagePath = generator.eventPropDefsPackagePath, + fileName = traitName, + fileContent = fileContent + ) + } + } + + // -- Style props -- + +// { +// val traitName = "StyleProps" +// +// val fileContent = generator.generateStylePropsTrait( +// defSources = defGroups.stylePropDefGroups, +// printDefGroupComments = true, +// traitCommentLines = Nil, +// traitModifiers = Nil, +// traitName = traitName, +// keyKind = "StyleProp", +// keyKindAlias = "StyleProp", +// setterType = "StyleSetter", +// setterTypeAlias = "SS", +// derivedKeyKind = "DerivedStyleProp", +// derivedKeyKindAlias = "DSP", +// baseImplDefComments = List( +// "Create custom CSS property", +// "", +// "@param name - name of CSS property, e.g. \"font-weight\"", +// "", +// "@tparam V - type of values recognized by JS for this property, e.g. Int", +// " Note: String is always allowed regardless of the type you put here.", +// " If unsure, use String type as V." +// ), +// baseImplName = "styleProp", +// defType = LazyVal, +// lengthUnitsNumType = "Int", +// outputUnitTraits = true +// ) +// +// generator.writeToFile( +// packagePath = generator.stylePropDefsPackagePath, +// fileName = traitName, +// fileContent = fileContent +// ) +// } + + // -- Style keyword traits + +// { +// StyleTraitDefs.defs.foreach { styleTrait => +// val fileContent = generator.generateStyleKeywordsTrait( +// defSources = styleTrait.keywordDefGroups, +// printDefGroupComments = styleTrait.keywordDefGroups.length > 1, +// traitCommentLines = Nil, +// traitModifiers = Nil, +// traitName = styleTrait.scalaName.replace("[_]", ""), +// extendsTraits = styleTrait.extendsTraits.map(_.replace("[_]", "")), +// extendsUnitTraits = styleTrait.extendsUnits, +// propKind = "StyleProp", +// keywordType = "StyleSetter", +// derivedKeyKind = "DerivedStyleProp", +// lengthUnitsNumType = "Int", +// defType = LazyVal, +// outputUnitTypes = true, +// allowSuperCallInOverride = false // can't access lazy val from `super` +// ) +// +// generator.writeToFile( +// packagePath = generator.styleTraitsPackagePath(), +// fileName = styleTrait.scalaName.replace("[_]", ""), +// fileContent = fileContent +// ) +// } +// } + } + + private object generator + extends CanonicalGenerator( + baseOutputDirectoryPath = "shared/src/main/scala/top/davidon/sfs/dom", + basePackagePath = "top.davidon.sfs.dom", + standardTraitCommentLines = List( + "#NOTE: GENERATED CODE", + s" - This file is generated at compile time from the data in Scala DOM Types", + " - See `project/DomDefsGenerator.scala` for code generation params", + " - Contribute to https://github.com/raquo/scala-dom-types to add missing tags / attrs / props / etc." + ), + format = CodeFormatting() + ) { + + override def settersPackagePath: String = + basePackagePath + ".modifiers.KeySetter" + + override def scalaJsElementTypeParam: String = "Ref" + } + +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..136f452 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.10.1 diff --git a/project/build.sbt b/project/build.sbt new file mode 100644 index 0000000..b150703 --- /dev/null +++ b/project/build.sbt @@ -0,0 +1,6 @@ +lazy val root = (project in file(".")) + .settings( + libraryDependencies ++= Seq( + "com.raquo" %% "domtypes" % "18.1.0" + ) + ) diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..f524850 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") diff --git a/shared/src/main/scala/ComplexHtmlKeys.scala b/shared/src/main/scala/ComplexHtmlKeys.scala new file mode 100644 index 0000000..da2c4a2 --- /dev/null +++ b/shared/src/main/scala/ComplexHtmlKeys.scala @@ -0,0 +1,18 @@ +package top.davidon.sfs.dom.defs.complex + +import top.davidon.sfs.dom.codecs +import top.davidon.sfs.dom.keys.HtmlAttr + +trait ComplexHtmlKeys { + lazy val rel = HtmlAttr("rel", codecs.StringAsIsCodec) + lazy val role = HtmlAttr("role", codecs.StringAsIsCodec) + lazy val styleAttr = HtmlAttr("style", codecs.StringAsIsCodec) + val className = HtmlAttr("class", codecs.StringAsIsCodec) + val cls: HtmlAttr[String] = className + val `class`: HtmlAttr[String] = className + val classList = HtmlAttr("class", codecs.IterableAsSpaceSeparatedStringCodec) + val cl: HtmlAttr[Iterable[String]] = classList + + def dataAttr(suffix: String): HtmlAttr[String] = + HtmlAttr[String]("data-" + suffix, codecs.StringAsIsCodec) +} diff --git a/shared/src/main/scala/ComplexSvgKeys.scala b/shared/src/main/scala/ComplexSvgKeys.scala new file mode 100644 index 0000000..1b23759 --- /dev/null +++ b/shared/src/main/scala/ComplexSvgKeys.scala @@ -0,0 +1,13 @@ +package top.davidon.sfs.dom.defs.complex + +import top.davidon.sfs.dom.codecs +import top.davidon.sfs.dom.keys.HtmlAttr + +trait ComplexSvgKeys { + lazy val role = HtmlAttr("role", codecs.StringAsIsCodec) + val className = HtmlAttr("class", codecs.StringAsIsCodec) + val cls: HtmlAttr[String] = className + val `class`: HtmlAttr[String] = className + val classList = HtmlAttr("class", codecs.IterableAsSpaceSeparatedStringCodec) + val cl: HtmlAttr[Iterable[String]] = classList +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/Element.scala b/shared/src/main/scala/top/davidon/sfs/dom/Element.scala new file mode 100644 index 0000000..2155b2c --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/Element.scala @@ -0,0 +1,10 @@ +package top.davidon.sfs.dom + +import top.davidon.sfs.dom.tags.Tag + +/** tag + modifiers + value */ +class Element[+Ref <: org.scalajs.dom.Element]( + val tag: Tag[Ref], + val mods: Iterable[Modifier[?, ?]], + val value: Value[?, String] +) {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/Modifier.scala b/shared/src/main/scala/top/davidon/sfs/dom/Modifier.scala new file mode 100644 index 0000000..8c9ece2 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/Modifier.scala @@ -0,0 +1,6 @@ +package top.davidon.sfs.dom + +import top.davidon.sfs.dom.keys.Key +import top.davidon.sfs.dom.Value + +class Modifier[F, T](val key: Key, val value: Value[F, T]) {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/Renderer.scala b/shared/src/main/scala/top/davidon/sfs/dom/Renderer.scala new file mode 100644 index 0000000..318ee1c --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/Renderer.scala @@ -0,0 +1,7 @@ +package top.davidon.sfs.dom + +import org.scalajs.dom + +trait Renderer[T] { + def render(elements: Element[dom.Element]*): T +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/SFS.scala b/shared/src/main/scala/top/davidon/sfs/dom/SFS.scala new file mode 100644 index 0000000..9e7d752 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/SFS.scala @@ -0,0 +1,3 @@ +package top.davidon.sfs.dom + +object SFS extends ScalaFullStack {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala b/shared/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala new file mode 100644 index 0000000..553d451 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/ScalaFullStack.scala @@ -0,0 +1,58 @@ +package top.davidon.sfs.dom + +import top.davidon.sfs.dom.codecs.* +import top.davidon.sfs.dom.defs.attrs.{AriaAttrs, HtmlAttrs, SvgAttrs} +import top.davidon.sfs.dom.defs.complex.{ComplexHtmlKeys, ComplexSvgKeys} +import top.davidon.sfs.dom.defs.eventProps.GlobalEventProps +import top.davidon.sfs.dom.defs.props.HtmlProps +import top.davidon.sfs.dom.defs.tags.{HtmlTags, SvgTags} + +trait ScalaFullStack + extends HtmlTags + with HtmlAttrs + with HtmlProps + with GlobalEventProps + with ComplexHtmlKeys { + + type Component = Element[?] + + object aria extends AriaAttrs + + object svg extends SvgTags with SvgAttrs with ComplexSvgKeys + + given AsValue[String, String] with { + extension (from: String) { + def asStringValue(): Value[String, String] = { + Value(from, StringAsIsCodec) + } + } + } + given AsValue[Int, String] with { + extension (from: Int) { + def asStringValue(): Value[Int, String] = { + Value(from, IntAsStringCodec) + } + } + } + given AsValue[Double, String] with { + extension (from: Double) { + def asStringValue(): Value[Double, String] = { + Value(from, DoubleAsStringCodec) + } + } + } + given AsValue[Boolean, String] with { + extension (from: Boolean) { + def asStringValue(): Value[Boolean, String] = { + Value(from, BooleanAsTrueFalseStringCodec) + } + } + } + given AsValue[Iterable[String], String] with { + extension (from: Iterable[String]) { + def asStringValue(): Value[Iterable[String], String] = { + Value(from, IterableAsSpaceSeparatedStringCodec) + } + } + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/Value.scala b/shared/src/main/scala/top/davidon/sfs/dom/Value.scala new file mode 100644 index 0000000..9f73f32 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/Value.scala @@ -0,0 +1,37 @@ +package top.davidon.sfs.dom + +import top.davidon.sfs.dom.codecs.{Codec, StringAsIsCodec} + +class Value[F, T]( + val value: F, + val codec: Codec[F, T], + var isReactive: Boolean = false +) { + def apply(): T = { + codec.encode(value) + } + + def reactive(value: Boolean = true): Value[F, T] = { + isReactive = value + this + } +} + +trait AsValue[F, T] { + extension (from: F) { + def asStringValue(): Value[F, String] + } +} + +object Value { + def join( + iterator: Iterable[Value[?, String]] + ): Value[String, String] = { + Value( + iterator.map(v => v.codec.encode(v.value)).mkString(""), + StringAsIsCodec, + iterator.exists(_.isReactive) + ) + } + +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/codecs/Codecs.scala b/shared/src/main/scala/top/davidon/sfs/dom/codecs/Codecs.scala new file mode 100644 index 0000000..937aba2 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/codecs/Codecs.scala @@ -0,0 +1,25 @@ +package top.davidon.sfs.dom.codecs + +trait Codec[ScalaType, DomType] { + + /** Convert the result of a `dom.Node.getAttribute` call to appropriate Scala + * type. + * + * Note: HTML Attributes are generally optional, and `dom.Node.getAttribute` + * will return `null` if an attribute is not defined on a given DOM node. + * However, this decoder is only intended for cases when the attribute is + * defined. + */ + def decode(domValue: DomType): ScalaType + + /** Convert desired attribute value to appropriate DOM type. The resulting + * value should be passed to `dom.Node.setAttribute` call, EXCEPT when + * resulting value is a `null`. In that case you should call + * `dom.Node.removeAttribute` instead. + * + * We use `null` instead of [[Option]] here to reduce overhead in JS land. + * This method should not be called by end users anyway, it's the consuming + * library's job to call this method under the hood. + */ + def encode(scalaValue: ScalaType): DomType +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/codecs/package.scala b/shared/src/main/scala/top/davidon/sfs/dom/codecs/package.scala new file mode 100644 index 0000000..265b570 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/codecs/package.scala @@ -0,0 +1,87 @@ +package top.davidon.sfs.dom + +package object codecs { + + lazy val IntAsStringCodec: Codec[Int, String] = new Codec[Int, String] { + + override def decode(domValue: String): Int = + domValue.toInt // @TODO this can throw exception. How do we handle this? + + override def encode(scalaValue: Int): String = scalaValue.toString + } + + lazy val DoubleAsIsCodec: Codec[Double, Double] = AsIsCodec() + + lazy val DoubleAsStringCodec: Codec[Double, String] = + new Codec[Double, String] { + + override def decode(domValue: String): Double = + domValue.toDouble // @TODO this can throw exception. How do we handle this? + + override def encode(scalaValue: Double): String = scalaValue.toString + } + lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] = + new Codec[Boolean, String] { + + override def decode(domValue: String): Boolean = domValue == "true" + + override def encode(scalaValue: Boolean): String = + if scalaValue then "true" else "false" + } + + lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] = + new Codec[Boolean, String] { + + override def decode(domValue: String): Boolean = domValue == "yes" + + override def encode(scalaValue: Boolean): String = + if scalaValue then "yes" else "no" + } + lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] = + new Codec[Boolean, String] { + + override def decode(domValue: String): Boolean = domValue == "on" + + override def encode(scalaValue: Boolean): String = + if scalaValue then "on" else "off" + } + + lazy val IterableAsSpaceSeparatedStringCodec + : Codec[Iterable[String], String] = + new Codec[Iterable[String], String] { // could use for e.g. className + + override def decode(domValue: String): Iterable[String] = + if domValue == "" then Nil else domValue.split(' ') + + override def encode(scalaValue: Iterable[String]): String = + scalaValue.mkString(" ") + } + lazy val IterableAsCommaSeparatedStringCodec + : Codec[Iterable[String], String] = + new Codec[Iterable[String], String] { // could use for lists of IDs + + override def decode(domValue: String): Iterable[String] = + if domValue == "" then Nil else domValue.split(',') + + override def encode(scalaValue: Iterable[String]): String = + scalaValue.mkString(",") + } + val StringAsIsCodec: Codec[String, String] = AsIsCodec() + val IntAsIsCodec: Codec[Int, Int] = AsIsCodec() + val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec() + + val BooleanAsAttrPresenceCodec: Codec[Boolean, String] = + new Codec[Boolean, String] { + + override def decode(domValue: String): Boolean = domValue != null + + override def encode(scalaValue: Boolean): String = + if scalaValue then "" else null + } + + def AsIsCodec[V](): Codec[V, V] = new Codec[V, V] { + override def encode(scalaValue: V): V = scalaValue + + override def decode(domValue: V): V = domValue + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala new file mode 100644 index 0000000..256c033 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/AriaAttr.scala @@ -0,0 +1,18 @@ +package top.davidon.sfs.dom.keys +import top.davidon.sfs.dom.codecs.Codec +import top.davidon.sfs.dom.{Modifier, Value} + +class AriaAttr[V]( + suffix: String, + val codec: Codec[V, String] +) extends Key { + override val name: String = "aria-" + suffix + + @inline def apply(value: V): Modifier[V, String] = { + this := value + } + + def :=(value: V): Modifier[V, String] = { + Modifier(this, Value(value, codec)) + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/EventProp.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/EventProp.scala new file mode 100644 index 0000000..568a6fa --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/EventProp.scala @@ -0,0 +1,5 @@ +package top.davidon.sfs.dom.keys + +import org.scalajs.dom + +class EventProp[Event <: dom.Event](override val name: String) extends Key {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala new file mode 100644 index 0000000..c89956f --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlAttr.scala @@ -0,0 +1,17 @@ +package top.davidon.sfs.dom.keys + +import top.davidon.sfs.dom.codecs.Codec +import top.davidon.sfs.dom.{Modifier, Value} + +class HtmlAttr[V]( + override val name: String, + val codec: Codec[V, String] +) extends Key { + @inline def apply(value: V): Modifier[V, String] = { + this := value + } + + def :=(value: V): Modifier[V, String] = { + Modifier(this, Value(value, codec)) + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlProp.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlProp.scala new file mode 100644 index 0000000..dd27129 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/HtmlProp.scala @@ -0,0 +1,16 @@ +package top.davidon.sfs.dom.keys +import top.davidon.sfs.dom.codecs.Codec +import top.davidon.sfs.dom.{Modifier, Value} + +class HtmlProp[V, DomV]( + override val name: String, + val codec: Codec[V, DomV] +) extends Key { + @inline def apply(value: V): Modifier[V, DomV] = { + this := value + } + + def :=(value: V): Modifier[V, DomV] = { + Modifier(this, Value(value, codec)) + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/Key.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/Key.scala new file mode 100644 index 0000000..0a99146 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/Key.scala @@ -0,0 +1,5 @@ +package top.davidon.sfs.dom.keys + +abstract class Key { + val name: String +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala b/shared/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala new file mode 100644 index 0000000..d0d1e58 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/keys/SvgAttr.scala @@ -0,0 +1,38 @@ +package top.davidon.sfs.dom.keys +import top.davidon.sfs.dom.codecs.Codec +import top.davidon.sfs.dom.{Modifier, Value} + +class SvgAttr[V]( + val localName: String, + val codec: Codec[V, String], + val namespacePrefix: Option[String] +) extends Key { + override val name: String = + namespacePrefix.map(_ + ":" + localName).getOrElse(localName) + + val namespaceUri: Option[String] = namespacePrefix.map(SvgAttr.namespaceUri) + + @inline def apply(value: V): Modifier[V, String] = { + this := value + } + + def :=(value: V): Modifier[V, String] = { + Modifier(this, Value(value, codec)) + } +} + +object SvgAttr { + final val svgNamespaceUri: String = "http://www.w3.org/2000/svg" + final val xlinkNamespaceUri: String = "http://www.w3.org/1999/xlink" + final val xmlNamespaceUri: String = "http://www.w3.org/XML/1998/namespace" + final val xmlnsNamespaceUri: String = "http://www.w3.org/2000/xmlns/" + + final def namespaceUri(namespace: String): String = { + namespace match { + case "svg" => svgNamespaceUri + case "xlink" => xlinkNamespaceUri + case "xml" => xmlNamespaceUri + case "xmlns" => xmlnsNamespaceUri + } + } +} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/tags/HtmlTag.scala b/shared/src/main/scala/top/davidon/sfs/dom/tags/HtmlTag.scala new file mode 100644 index 0000000..1f416d9 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/tags/HtmlTag.scala @@ -0,0 +1,6 @@ +package top.davidon.sfs.dom.tags + +class HtmlTag[+Ref <: org.scalajs.dom.html.Element]( + override val name: String, + override val void: Boolean = false +) extends Tag[Ref] {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/tags/SvgTag.scala b/shared/src/main/scala/top/davidon/sfs/dom/tags/SvgTag.scala new file mode 100644 index 0000000..bb10066 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/tags/SvgTag.scala @@ -0,0 +1,6 @@ +package top.davidon.sfs.dom.tags + +class SvgTag[+Ref <: org.scalajs.dom.svg.Element]( + override val name: String, + override val void: Boolean = false +) extends Tag[Ref] {} diff --git a/shared/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala b/shared/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala new file mode 100644 index 0000000..a836184 --- /dev/null +++ b/shared/src/main/scala/top/davidon/sfs/dom/tags/Tag.scala @@ -0,0 +1,17 @@ +package top.davidon.sfs.dom.tags + +import org.scalajs.dom +import top.davidon.sfs.dom.{Element, Modifier, Value} + +trait Tag[+Ref <: dom.Element] { + val name: String + val void: Boolean + + def apply( + modifiers: Modifier[?, ?]* + )( + values: Value[?, String]* + ): Element[Ref] = { + Element[Ref](this, modifiers, Value.join(values)) + } +}