sourceafSlim::SlimTag.fan


** A utility class for use by custom Slim components.
** Builds and writes a HTML tag with custom attrs.
class SlimTag {

    ** The standard sizes.
    static const Str[]  sizes   := "xs sm lg xl".split

    Str         tagName
    Str?        id
    Str:Str?    attrs
    Str[]       classes
    Str         style
    
    ** Creates a tag builder initialised from the given 'SlimComponentCtx'.
    ** 
    ** Tag Name defaults to ctx, falls back to the given name, or defaults to 'div'.
    new fromCtx(SlimComponentCtx ctx, Str? tagName := null) {
        this.tagName    = (ctx.tagName ?: tagName) ?: "div"
        this.id         = null
        this.attrs      = Str:Str?[:] { it.ordered = true }
        this.classes    = Str[,]
        this.style      = ""
        
        this.id         = ctx.id
        this.attrs  .addAll(ctx.attrs)
        this.classes.addAll(ctx.classes)
        this.style      = this.attrs.remove("style")?.trimToNull ?: ""
    }
    
    ** Creates a tag builder.
    ** 
    ** Tag Name defaults to 'div'.
    new make(Str? tagName := null) {
        this.tagName    = tagName ?: "div"
        this.id         = null
        this.attrs      = Str:Str?[:] { it.ordered = true }
        this.classes    = Str[,]
        this.style      = ""
    }
    
    ** Is the value one of the standard sizes? 
    Bool isSize(Str val) {
        sizes.contains(val)
    }
    
    ** Returns the attr value as an Int.
    Int? attrInt(Str name) {
        attrs.remove(name)?.toInt
    }
    
    ** Returns 'true' if the attr name exists.
    Bool attrBool(Str name) {
        bool := attrs.containsKey(name)
        attrs.remove(name)
        return bool
    }   

    ** Returns the attr value as a standard size, an empty string if the base value,
    ** or 'null' if not found.
    Str? attrSize(Str name) {
        if (attrs.containsKey(name) && attrs[name] == null) {
            attrs.remove(name)
            return ""
        }
        return sizes.find { attrBool("${name}-${it}") }
    }
    
    ** Returns the attr value.
    Str? attrStr(Str name) {
        attrs.remove(name)
    }

    ** Returns the first enum value in the given SSV string,
    ** or 'null' if not found.
    Str? attrEnum(Str enum) {
        enum.split.find { attrBool(it) }
    }
    
    ** Adds a CSS class (if it's not 'null' or empty).
    This addClass(Str? klass) {
        if (klass != null && klass.size > 0)
            classes.add(klass)
        return this
    }
    
    ** Adds a single CSS property to the 'style' attribute.
    ** 'size' is appended to the value (if given).
    ** 'null' values are skipped.
    ** 'value' is wrapped in 'var(...)' if a CSS var name (starts with '--').
    ** 
    ** pre>
    ** syntax: fantom
    ** addStyle("padding", "2rem")
    ** addStyle("margin", "padding", "sm")
    ** addStyle("margin", "--padding-sm")
    ** <pre
    This addStyle(Str name, Str? value, Str size := "") {
        if (value == null) return this

        if (this.style.size > 0) {
            if (this.style.endsWith(";") == false)
                this.style += ";"
            this.style += " "
        }
        val := size == "" ? value.trim : "${value.trim}-${size}"
        if (val.startsWith("--"))
            val = "var(${val})"
        this.style += "${name.trim}: ${val}"
        return this
    }
    
    ** Adds a attr name value pair. 'value' may be 'null' for boolean attributes.
    This addAttr(Str name, Str? value := null) {
        this.attrs.remove(name)
        this.attrs[name] = value
        return this
    }
    
    ** Writes the opening start tag to the given out. 
    This write(StrBuf out) {
        atts := Str:Str?[:]
        atts.ordered = true
        
        if (this.id != null)
            atts["id"] = this.id
    
        if (classes.size > 0)
            atts["class"] = classes.join(" ")
        
        if (style.size > 0)
            atts["style"] = style
        
        atts.addAll(attrs)
        writeTag(out, tagName, atts)
        return this
    }
    
    ** Writes the end tag.
    This writeEnd(StrBuf out) {
        out.addChar('<').addChar('/').add(tagName).addChar('>')
        return this
    }
    
    private static Void writeTag(StrBuf out, Str tagName, [Str:Str?]? attrs) {
        out.addChar('<').add(tagName)
        
        // we purposely do NOT escape XML so we can write efan code values
        if (attrs != null && attrs.size > 0)
            attrs.each |val, nom| {
                out.join(nom, " ")
                if (val != null)
                    out.addChar('=').addChar('"').add(val).addChar('"')
            }
        
        out.addChar('>')
    }
}