in api/applib/src/main/java/org/apache/causeway/applib/fa/FontAwesomeLayers.java [51:258]
public record FontAwesomeLayers(
@NonNull IconType iconType,
/**
* Position of <i>Font Awesome</i> icon, relative to its accompanied title.
*/
@Nullable CssClassFaPosition position,
@Nullable String containerCssClasses,
@Nullable String containerCssStyle,
@Nullable List<IconEntry> iconEntries,
@Nullable List<SpanEntry> spanEntries
) implements Serializable {
// -- FACTORIES
public static FontAwesomeLayers empty() {
return new FontAwesomeLayers(IconType.SINGLE, null, null, null, null, null);
}
public static FontAwesomeLayers blank() {
return singleIcon("fa-blank");
}
public static FontAwesomeLayers singleIcon(final String faClasses) {
return new FontAwesomeLayers(IconType.SINGLE, null, null, null,
List.of(new IconEntry(normalizeCssClasses(faClasses, "fa"), null)),
null);
}
public static FontAwesomeLayers iconStack(
final @Nullable String containerCssClasses,
final @Nullable String containerCssStyle,
final @NonNull IconEntry baseEntry,
final @NonNull IconEntry overlayEntry,
final IconEntry ...additionalOverlayEntries) {
var iconEntries = Stream.concat(
Stream.of(baseEntry, overlayEntry),
Can.ofArray(additionalOverlayEntries).stream() // does not collect nulls
)
.collect(Collectors.toList());
return new FontAwesomeLayers(
IconType.STACKED,
null,
normalizeCssClasses(containerCssClasses, "fa-stack"),
containerCssStyle,
iconEntries,
null);
}
public static FontAwesomeLayers fromJson(final @Nullable String json) {
return FontAwesomeJsonParser.parse(json);
}
/**
* Example:
* <pre>
* solid person-walking-arrow-right .my-color,
* solid scale-balanced .my-color .bottom-right-overlay</pre>
*/
public static FontAwesomeLayers fromQuickNotation(final @Nullable String quickNotation) {
return FontAwesomeQuickNotationParser.parse(quickNotation);
}
// -- BUILDER
@AllArgsConstructor
public static class StackBuilder {
private IconType iconType;
@Setter @Accessors(fluent=true, chain = true) private CssClassFaPosition postition;
@Setter @Accessors(fluent=true, chain = true) private String containerCssClasses;
@Setter @Accessors(fluent=true, chain = true) private String containerCssStyle;
private List<IconEntry> iconEntries;
public FontAwesomeLayers build() {
switch (iconType) {
case STACKED:{
return new FontAwesomeLayers(
IconType.STACKED,
null,
normalizeCssClasses(containerCssClasses, "fa-stack"),
containerCssStyle,
Can.ofCollection(iconEntries).toList(),
null);
}
default:
throw _Exceptions.unmatchedCase(iconType);
}
}
public StackBuilder addIconEntry(final @NonNull String cssClasses) {
return addIconEntry(cssClasses, null);
}
public StackBuilder addIconEntry(final @NonNull String cssClasses, final @Nullable String cssStyle) {
iconEntries.add(new IconEntry(normalizeCssClasses(cssClasses), cssStyle));
return this;
}
}
public static StackBuilder stackBuilder() {
return new StackBuilder(IconType.STACKED, null,
null, null, new ArrayList<FontAwesomeLayers.IconEntry>());
}
// -- UTILITIES
public static String normalizeCssClasses(final String cssClasses, final String... mandatory) {
var elements = _Strings.splitThenStream(cssClasses, " ")
.map(String::trim)
.filter(_Strings::isNotEmpty)
.collect(Collectors.toCollection(TreeSet::new));
_NullSafe.stream(mandatory)
.forEach(elements::add);
return elements.stream()
//TODO[CAUSEWAY-3646] filter out malformed names (hardening)
.collect(Collectors.joining(" "));
}
// --
public enum IconType {
SINGLE,
STACKED,
LAYERED
}
public record IconEntry(
@Nullable String cssClasses,
@Nullable String cssStyle) implements Serializable {
// canonical constructor
public IconEntry(final String cssClasses, final String cssStyle) {
this.cssClasses = normalizeCssClasses(cssClasses);
this.cssStyle = cssStyle;
}
public String toHtml() {
return faIconHtml(cssClasses, cssStyle);
}
// -- HELPER
private static String faIconHtml(final @Nullable String faClasses) {
if(_Strings.isEmpty(faClasses)) return "";
return "<i class=\"%s\"></i>".formatted(faClasses);
}
private static String faIconHtml(final @Nullable String faClasses, final @Nullable String faStyle) {
if(_Strings.isEmpty(faStyle)) return faIconHtml(faClasses);
return "<i class=\"%s\" style=\"%s\"></i>".formatted(faClasses, faStyle);
}
}
public record SpanEntry(
@Nullable String cssClasses,
@Nullable String cssStyle,
@Nullable String transform,
@Nullable String text) implements Serializable {
}
public String toHtml() {
var iconEntries = Can.ofCollection(iconEntries());
if(iconEntries.isEmpty()) return "";
if(iconEntries.isCardinalityOne()) {
// use simple rendering (not a stack nor layered)
return faSpanHtml(iconEntries.getFirstElseFail().toHtml(), null, null);
}
var sb = new StringBuilder();
iconEntries.forEach(iconEntry->sb.append(iconEntry.toHtml()));
return faSpanHtml(sb.toString(), containerCssClasses, containerCssStyle);
}
public String toJson() {
return JsonUtils.toStringUtf8(this, JsonUtils::indentedOutput);
}
/**
* If this instance was not created from a quick-notation,
* the result may loose style information.
*/
public String toQuickNotation() {
return FontAwesomeQuickNotationGenerator.generate(this);
}
public FontAwesomeLayers withPosition(CssClassFaPosition newPosition) {
return new FontAwesomeLayers(iconType, newPosition, containerCssClasses, containerCssStyle, iconEntries, spanEntries);
}
// -- UTILITY
//TODO[CAUSEWAY-3646] how to determine position when empty
public FontAwesomeLayers emptyToBlank() {
return _NullSafe.size(this.iconEntries())>0
? this
: FontAwesomeLayers.blank();
}
// -- HELPER
private static String faSpanHtml(
final @Nullable String innerHtml,
final @Nullable String cssClasses,
final @Nullable String cssStyle) {
if(_Strings.isEmpty(innerHtml)) return "";
var attrClass = _Strings.nonEmpty(cssClasses)
.map(" class=\"%s\""::formatted)
.orElse("");
var attrStyle = _Strings.nonEmpty(cssStyle)
.map(" style=\"%s\""::formatted)
.orElse("");
return "<span%s%s>%s</span>".formatted(attrClass, attrStyle, innerHtml);
}
}