Closed reubenfirmin closed 1 year ago
Here's an example class that I use to both define and turn on and off css classes. It'd be awesome if this could be made cleaner (without the strings).
package com.example.model
import com.example.*
import com.example.App.Companion.debug
import com.example.view.canvas.CardView
import io.kvision.core.*
import io.kvision.core.Color.Companion.hex
import io.kvision.core.UNIT.*
import io.kvision.html.H1
import io.kvision.html.H2
import io.kvision.utils.em
import io.kvision.utils.px
const val CLASS_CARD = "card"
const val CLASS_ROUNDED = "rounded"
const val CLASS_OUTLINE_BLUE = "outline_blue" // TODO semantic name
const val CLASS_OUTLINE_GREEN = "outline_green"
const val CLASS_DECORATED = "decorated"
const val CLASS_HOVER = "hover"
const val CLASS_SELECTED = "selected"
const val CLASS_HEADING = "heading"
const val CLASS_INFO = "info"
const val CLASS_ALLDONE = "alldone"
const val CLASS_STRIKETHROUGH = "strikethrough"
const val CLASS_TASK = "task"
class CardStyle(val theme: Theme) {
private val foundationCardStyle = Style() {
color = hex(theme.cardFontColor)
position = Position.RELATIVE
whiteSpace = WhiteSpace.NORMAL
}
private val baseCardStyle = inheritingStyle(parentStyle = foundationCardStyle) {
margin = 10.px
fontSize = 16.px
lineHeight = 1.5.em
opacity = 100.0
padding = 10.px
background = Background(hex(theme.cardBackground))
}
private val imageCardStyle = inheritingStyle(parentStyle = foundationCardStyle) {
paddingRight = 15 to px
paddingLeft = 15 to px
paddingTop = 10 to px
paddingBottom = 0 to px
margin = 0 to px
}
private val roundedCardStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_ROUNDED", parentStyle = baseCardStyle) {
borderRadius = 10.px
boxShadow = BoxShadow(0.px, 8.px, 14.px, 0.px, Color.rgba(0,0,0,45))
}
private val linkCardStyle = inheritingStyle(parentStyle = roundedCardStyle) {
textAlign = TextAlign.CENTER
fontSize = 14 to px
cursor = Cursor.POINTER
}
private val outlineCardStyleBlue = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_OUTLINE_BLUE", parentStyle = linkCardStyle) {
border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
}
private val outlineCardStyleGreen = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_OUTLINE_GREEN", parentStyle = linkCardStyle) {
border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
}
private val decoratedCardStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED", parentStyle = roundedCardStyle) {
borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripe))
}
private val decoratedCardStyleHover = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED.$CLASS_HOVER", parentStyle = decoratedCardStyle, pClass = PClass.HOVER) {
background = Background(hex(theme.cardFocusedBackground))
borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
}
private val headingCard = Style(".$CLASS_CARD.$CLASS_HEADING") {
margin = 10 to px
borderLeft = Border(0 to px)
background = Background(hex(theme.appBackground))
}
private val decoratedCardAllDoneStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_ALLDONE", parentStyle = decoratedCardStyle) {
borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
}
private val selectedStyle = Style(".$CLASS_CARD.selected") {
background = Background(hex(theme.cardFocusedBackground))
}
private val decoratedSelectedStyle = inheritingStyle(selector = ".$CLASS_CARD.$CLASS_DECORATED.$CLASS_SELECTED", parentStyle = selectedStyle) {
borderLeft = Border(10 to UNIT.px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
}
private val strikeThrough = Style(".$CLASS_STRIKETHROUGH") {
textDecoration = TextDecoration(TextDecorationLine.LINETHROUGH)
color = hex(theme.cardHeadingDone)
}
fun styleCard(card: Card, view: CardView, model: Model) {
view.addCssClass(CLASS_CARD)
if (view.selected) {
view.addCssClass(CLASS_SELECTED)
} else {
view.removeCssClass(CLASS_SELECTED)
}
when {
card.headingOnly() -> {
view.addCssClass(CLASS_HEADING)
}
card.infoOnly() -> {
view.addCssClass(CLASS_ROUNDED)
view.addCssClass(CLASS_INFO)
}
else -> {
if (card.done()) {
view.addCssClass(CLASS_ALLDONE)
// TODO this would be better as a ".class > h1,h2"
view.getChildren().filter { it is H1 || it is H2 }.forEach { child ->
child.addCssClass(CLASS_STRIKETHROUGH)
}
} else {
view.addCssClass(CLASS_DECORATED)
view.addCssClass(CLASS_HOVER)
}
view.addCssClass(CLASS_TASK)
}
}
if (card.isCanvasLink()) {
applyCanvasLinkStyle(model, view)
} else if (card.isImageLink()) {
view.addCssStyle(imageCardStyle)
}
}
fun applyCanvasLinkStyle(model: Model, view: CardView) {
if (model.exists(view.card.linkedCanvas()!!)) {
view.removeCssClass(CLASS_OUTLINE_GREEN)
view.addCssClass(CLASS_OUTLINE_BLUE)
} else {
view.removeCssClass(CLASS_OUTLINE_BLUE)
view.addCssClass(CLASS_OUTLINE_GREEN)
}
}
}
But you don't have to rely on class selectors, because they are generally auto-generated. So instead of:
val cardStyle = Style(".card") {
fontSize = 2.em
}
div {
addCssClass("card")
+"Some large text"
}
you should do:
val cardStyle = Style {
fontSize = 2.em
}
div {
addCssStyle(cardStyle)
+"Some large text"
}
or even:
div {
style {
fontSize = 2.em
}
+"Some large text"
}
Yes I think that's right, though these styles need to be declared outside of the class, otherwise they get a new classname per instance of the class.
Check this out:
Left arrow is me taking a hit by loading some global styles.
Middle three arrows is the app on initialization creating three help dialogs (which are initially hidden). They each contain the same boilerplate UI; however, as you can see below, each of them gets their own version of the Style for the X button, because it's created on class initialization.
I also had this problem on initialization of some buttons, shown at the right of first screenshot in this comment. Each of the buttons has their own style class too.
The above is following the pattern:
abstract class HelpTemplate(...) {
val someWidgetStyle = Style {
}
/// ...
widget.addCssStyle(someWidgetStyle)
}
If I instead change to:
class HelpTemplateStyles() {
val someWidgetStyle = Style {
}
}
abstract class HelpTemplate(val styles: HelpTemplateStyles) {
/// ...
widget.addCssStyle(styles.someWidgetStyle)
}
... then initialization instead looks like this:
The three arrows on the right are the three different help templates, now using all of the common style objects. The arrow at the left is the initialization of the global styles for HelpTemplates. So, doing just this cut down rendering time by around 40ms. Incidentally I can't put these into a companion object, because I need to initialize them with the selected theme; and tangentially when the user changes theme, I force a full reload of the app, to reinitialize all of the global styles.
One way to avoid recalculating styles on the fly (and yet keep styling in code) is to have a global styles class (or series of classes/objects) which is initialized.
For example:
This gets "imported" as follows:
...and now styles don't need to be regenerated each time components are rerendered. However, it's a bit messy, since it relies on the css classname to "link" the style to the component; i.e. to use it, of course, I have to have a widget that does:
I can't even break out "card" fully to a constant, because the css rule wants a dot in front of it, so I'd need to prepend with a dot when initializing the style (which is...ok, but still smelly.)
Can you come up with a cleaner way of "linking" the component to the global style? (This may be the same solution as #450, but I thought I would file separately in case there are two separate ways of tackling this.)
One possibility, though I don't know if inline/reifying/etc will work with the js compiler:
Of course, people would eventually want to replicate CSS selectors in code, so you could imagine craziness like:
...but maybe this isn't so bad, as it's more readable than css selectors for somebody who isn't a browser developer.
Another simpler possibility: