diff --git a/DomDefsGenerator.mill b/DomDefsGenerator.mill index 8ac180d..a4c1719 100644 --- a/DomDefsGenerator.mill +++ b/DomDefsGenerator.mill @@ -78,6 +78,70 @@ class DomDefsGenerator(baseOutputDirectoryPath: String): format = format ).printTrait().getOutput() end generateTagsTrait + + override def generateAttrsTrait( + defGroups: List[(String, List[AttrDef])], + printDefGroupComments: Boolean, + traitCommentLines: List[String], + traitModifiers: List[String], + traitName: String, + keyKind: String, + implNameSuffix: String, + baseImplDefComments: List[String], + baseImplName: String, + namespaceImports: List[String], + namespaceImpl: String => String, + transformAttrDomName: String => String, + defType: DefType + ): String = + val (defs, defGroupComments) = defsAndGroupComments(defGroups, printDefGroupComments) + + val tagTypes = defs.foldLeft(List[TagType]())((acc, k) => (acc :+ k.tagType).distinct) + if tagTypes.size > 1 then + throw new Exception( + "Sorry, generateAttrsTrait does not support mixing attrs of different types in one call. You can contribute a PR (please contact us first), or bypass this limitation by calling AttrsTraitGenerator manually." + ) + val tagType = tagTypes.head + + val baseImplDef = + if tagType == SvgTagType then + List( + s"def $baseImplName[V]($keyImplNameArgName: String, encoder: Encoder[V, String], namespace: Option[String]): $keyKind[V] = ${keyKindConstructor(keyKind)}($keyImplNameArgName, encoder, namespace)" + ) + else + List( + s"def $baseImplName[V]($keyImplNameArgName: String, encoder: Encoder[V, String]): $keyKind[V] = ${keyKindConstructor(keyKind)}($keyImplNameArgName, encoder)" + ) + + val headerLines = List( + s"package $attrDefsPackagePath", + "", + keyTypeImport(keyKind), + codecsImport + ) ++ namespaceImports ++ List("") ++ standardTraitCommentLines.map("// " + _) + + new AttrsTraitGenerator( + defs = defs.map(d => d.copy(domName = transformAttrDomName(d.domName))), + defGroupComments = defGroupComments, + headerLines = headerLines, + traitCommentLines = traitCommentLines, + traitModifiers = traitModifiers, + traitName = traitName, + traitExtends = Nil, + traitThisType = None, + defType = _ => defType, + keyKind = keyKind, + keyImplName = attr => attrImplName(attr.codec, implNameSuffix), + keyImplNameArgName = keyImplNameArgName, + baseImplDefComments = baseImplDefComments, + baseImplName = baseImplName, + baseImplDef = baseImplDef, + transformCodecName = _ + "Encoder", + namespaceImpl = namespaceImpl, + outputImplDefs = true, + format = format + ).printTrait().getOutput() + end generateAttrsTrait end generator def generate(): Unit = @@ -134,7 +198,7 @@ class DomDefsGenerator(baseOutputDirectoryPath: String): "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", + "@param codec - used to encode V into String, e.g. StringAsIsEncoder", "", "@tparam V - value type for this attr in Scala" ), diff --git a/core/src/scalive/HtmlElement.scala b/core/src/scalive/HtmlElement.scala index ea0d485..9ac4a0a 100644 --- a/core/src/scalive/HtmlElement.scala +++ b/core/src/scalive/HtmlElement.scala @@ -2,8 +2,8 @@ package scalive import scalive.Mod.Attr import scalive.Mod.Content -import scalive.codecs.BooleanAsAttrPresenceCodec -import scalive.codecs.Codec +import scalive.codecs.BooleanAsAttrPresenceEncoder +import scalive.codecs.Encoder import zio.json.* class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]): @@ -43,8 +43,8 @@ class HtmlTag(val name: String, val void: Boolean = false): } ) -class HtmlAttr[V](val name: String, val codec: Codec[V, String]): - private inline def isBooleanAsAttrPresence = codec == BooleanAsAttrPresenceCodec +class HtmlAttr[V](val name: String, val codec: Encoder[V, String]): + private inline def isBooleanAsAttrPresence = codec == BooleanAsAttrPresenceEncoder def :=(value: V): Mod.Attr = if isBooleanAsAttrPresence then diff --git a/core/src/scalive/Scalive.scala b/core/src/scalive/Scalive.scala index 0566d9d..ae7e09f 100644 --- a/core/src/scalive/Scalive.scala +++ b/core/src/scalive/Scalive.scala @@ -1,12 +1,15 @@ -import scalive.codecs.BooleanAsAttrPresenceCodec -import scalive.codecs.StringAsIsCodec +import scalive.codecs.BooleanAsAttrPresenceEncoder +import scalive.codecs.BooleanAsTrueFalseStringEncoder +import scalive.codecs.Encoder +import scalive.codecs.IntAsStringEncoder +import scalive.codecs.StringAsIsEncoder import scalive.defs.attrs.HtmlAttrs import scalive.defs.complex.ComplexHtmlKeys import scalive.defs.tags.HtmlTags package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys: - lazy val defer = htmlAttr("defer", codecs.BooleanAsAttrPresenceCodec) + lazy val defer = htmlAttr("defer", codecs.BooleanAsAttrPresenceEncoder) object link: def navigate(path: String, mods: Mod*): HtmlElement = @@ -14,20 +17,76 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys: object phx: private def phxAttr(suffix: String): HtmlAttr[String] = - new HtmlAttr(s"phx-$suffix", StringAsIsCodec) + new HtmlAttr(s"phx-$suffix", StringAsIsEncoder) + private def phxAttrBool(suffix: String): HtmlAttr[Boolean] = + new HtmlAttr(s"phx-$suffix", BooleanAsTrueFalseStringEncoder) + private def phxAttrInt(suffix: String): HtmlAttr[Int] = + new HtmlAttr(s"phx-$suffix", IntAsStringEncoder) private def phxAttrJson(suffix: String): HtmlAttrJsonValue = new HtmlAttrJsonValue(s"phx-$suffix") private def dataPhxAttr(suffix: String): HtmlAttr[String] = dataAttr(s"phx-$suffix") private[scalive] lazy val session = dataPhxAttr("session") - private[scalive] lazy val main = htmlAttr("data-phx-main", BooleanAsAttrPresenceCodec) + private[scalive] lazy val main = htmlAttr("data-phx-main", BooleanAsAttrPresenceEncoder) private[scalive] lazy val link = dataPhxAttr("link") private[scalive] lazy val linkState = dataPhxAttr("link-state") - lazy val click = phxAttrJson("click") - def value(key: String) = phxAttr(s"value-$key") - lazy val trackStatic = htmlAttr("phx-track-static", BooleanAsAttrPresenceCodec) + + // Click + lazy val click = phxAttrJson("click") + lazy val clickAway = phxAttrJson("click-away") + // Focus + lazy val blur = phxAttrJson("blur") + lazy val focus = phxAttrJson("focus") + lazy val windowBlur = phxAttrJson("window-blur") + + // Keyboard + lazy val keydown = phxAttrJson("keydown") + lazy val keyup = phxAttrJson("keyup") + lazy val windowKeydown = phxAttrJson("window-keydown") + lazy val windowKeyup = phxAttrJson("window-keyup") + lazy val key = phxAttr("key") + + // Scroll + lazy val viewportTop = phxAttrJson("viewport-top") + lazy val viewportBottom = phxAttrJson("viewport-bottom") + + // Form + lazy val change = phxAttrJson("change") + lazy val submit = phxAttrJson("submit") + lazy val autoRecover = phxAttrJson("auto-recover") + lazy val triggerAction = phxAttrBool("trigger-action") + + // Button + lazy val disableWith = phxAttr("disable-with") + + // Socket connection lifecycle + lazy val connected = phxAttrJson("connected") + lazy val disconnected = phxAttrJson("disconnected") + + // DOM element lifecycle + lazy val mounted = phxAttrJson("mounted") + lazy val remove = phxAttrJson("remove") + lazy val update = new HtmlAttr["update" | "stream" | "ignore"](s"phx-update", Encoder(identity)) + + // Client hooks + lazy val hook = phxAttr("hook") + + // Rate limiting + lazy val debounce = new HtmlAttr["blur" | Int]( + s"phx-debounce", + Encoder { + case _: "blur" => "blur" + case value: Int => value.toString + } + ) + lazy val throttle = phxAttrInt("throttle") + + def value(key: String) = phxAttr(s"value-$key") + lazy val trackStatic = htmlAttr("phx-track-static", BooleanAsAttrPresenceEncoder) + end phx implicit def stringToMod(v: String): Mod = Mod.Content.Text(v) implicit def htmlElementToMod(el: HtmlElement): Mod = Mod.Content.Tag(el) implicit def dynStringToMod(d: Dyn[String]): Mod = Mod.Content.DynText(d) +end scalive diff --git a/core/src/scalive/codecs/Codec.scala b/core/src/scalive/codecs/Codec.scala deleted file mode 100644 index 95df7e1..0000000 --- a/core/src/scalive/codecs/Codec.scala +++ /dev/null @@ -1,29 +0,0 @@ -package scalive.codecs - -class Codec[ScalaType, DomType](val encode: ScalaType => DomType, val decode: DomType => ScalaType) - -def AsIsCodec[V](): Codec[V, V] = Codec(identity, identity) - -val StringAsIsCodec: Codec[String, String] = AsIsCodec() - -val IntAsIsCodec: Codec[Int, Int] = AsIsCodec() - -lazy val IntAsStringCodec: Codec[Int, String] = Codec[Int, String](_.toString, _.toInt) - -lazy val DoubleAsIsCodec: Codec[Double, Double] = AsIsCodec() - -lazy val DoubleAsStringCodec: Codec[Double, String] = Codec[Double, String](_.toString, _.toDouble) - -val BooleanAsIsCodec: Codec[Boolean, Boolean] = AsIsCodec() - -lazy val BooleanAsAttrPresenceCodec: Codec[Boolean, String] = - Codec[Boolean, String](if _ then "" else null, _ != null) - -lazy val BooleanAsTrueFalseStringCodec: Codec[Boolean, String] = - Codec[Boolean, String](if _ then "true" else "false", _ == "true") - -lazy val BooleanAsYesNoStringCodec: Codec[Boolean, String] = - Codec[Boolean, String](if _ then "yes" else "no", _ == "yes") - -lazy val BooleanAsOnOffStringCodec: Codec[Boolean, String] = - Codec[Boolean, String](if _ then "on" else "off", _ == "on") diff --git a/core/src/scalive/codecs/Encoder.scala b/core/src/scalive/codecs/Encoder.scala new file mode 100644 index 0000000..da7ced1 --- /dev/null +++ b/core/src/scalive/codecs/Encoder.scala @@ -0,0 +1,30 @@ +package scalive.codecs + +class Encoder[ScalaType, DomType](val encode: ScalaType => DomType) + +def AsIsEncoder[V](): Encoder[V, V] = Encoder(identity) + +val StringAsIsEncoder: Encoder[String, String] = AsIsEncoder() + +val IntAsIsEncoder: Encoder[Int, Int] = AsIsEncoder() + +lazy val IntAsStringEncoder: Encoder[Int, String] = Encoder[Int, String](_.toString) + +lazy val DoubleAsIsEncoder: Encoder[Double, Double] = AsIsEncoder() + +lazy val DoubleAsStringEncoder: Encoder[Double, String] = + Encoder[Double, String](_.toString) + +val BooleanAsIsEncoder: Encoder[Boolean, Boolean] = AsIsEncoder() + +lazy val BooleanAsAttrPresenceEncoder: Encoder[Boolean, String] = + Encoder[Boolean, String](if _ then "" else null) + +lazy val BooleanAsTrueFalseStringEncoder: Encoder[Boolean, String] = + Encoder[Boolean, String](if _ then "true" else "false") + +lazy val BooleanAsYesNoStringEncoder: Encoder[Boolean, String] = + Encoder[Boolean, String](if _ then "yes" else "no") + +lazy val BooleanAsOnOffStringEncoder: Encoder[Boolean, String] = + Encoder[Boolean, String](if _ then "on" else "off") diff --git a/core/src/scalive/defs/complex/ComplexHtmlKeys.scala b/core/src/scalive/defs/complex/ComplexHtmlKeys.scala index a39caa7..8c3cb74 100644 --- a/core/src/scalive/defs/complex/ComplexHtmlKeys.scala +++ b/core/src/scalive/defs/complex/ComplexHtmlKeys.scala @@ -12,7 +12,7 @@ trait ComplexHtmlKeys: * select and access specific elements via the class selectors or functions like the DOM method * document.getElementsByClassName */ - val className: HtmlAttr[String] = new HtmlAttr("class", StringAsIsCodec) + val className: HtmlAttr[String] = new HtmlAttr("class", StringAsIsEncoder) val cls: HtmlAttr[String] = className @@ -22,7 +22,7 @@ trait ComplexHtmlKeys: * stylesheet, and the href attribute is set to the URL of an external style sheet to format the * page. */ - lazy val rel: HtmlAttr[String] = new HtmlAttr("rel", StringAsIsCodec) + lazy val rel: HtmlAttr[String] = new HtmlAttr("rel", StringAsIsEncoder) /** The attribute describes the role(s) the current element plays in the context of the document. * This can be used, for example, by applications and assistive technologies to determine the @@ -35,7 +35,7 @@ trait ComplexHtmlKeys: * * See: [[http://www.w3.org/TR/role-attribute/#s_role_module_attributes]] */ - lazy val role: HtmlAttr[String] = new HtmlAttr("role", StringAsIsCodec) + lazy val role: HtmlAttr[String] = new HtmlAttr("role", StringAsIsEncoder) /** This class of attributes, called custom data attributes, allows proprietary information to be * exchanged between the HTML and its DOM representation that may be used by scripts. All such @@ -51,13 +51,13 @@ trait ComplexHtmlKeys: * attribute data-test-value will be accessible via HTMLElement.dataset.testValue as any dash * (U+002D) is replaced by the capitalization of the next letter (camelcase). */ - def dataAttr(suffix: String): HtmlAttr[String] = new HtmlAttr(s"data-$suffix", StringAsIsCodec) + def dataAttr(suffix: String): HtmlAttr[String] = new HtmlAttr(s"data-$suffix", StringAsIsEncoder) /** This attribute contains CSS styling declarations to be applied to the element. Note that it is * recommended for styles to be defined in a separate file or files. This attribute and the style * element have mainly the purpose of allowing for quick styling, for example for testing * purposes. */ - lazy val styleAttr: HtmlAttr[String] = new HtmlAttr("style", StringAsIsCodec) + lazy val styleAttr: HtmlAttr[String] = new HtmlAttr("style", StringAsIsEncoder) end ComplexHtmlKeys