I have a project I am working on that takes data from a database of a card game I am creating and displays it to a user. I am struggling with displaying a list of cards to the user, because some of the div items are not aligned, despite them having the same size and only differ by content.
I have been able to deduce so far that the issue is originating with an embedded SVG in a div alongside text, as every card that is misaligned has an SVG in the card’s title. But I cannot figure out why. It seems that if I make the SVG smaller, the problem goes away, but if it is too small, the card instead moves down. I have tried numerous display types to no avail. The SVG is much smaller than the text (I’d honestly want it bigger as well), and yet it seems to control where the card is positioned vertically.
I think that most likely it is due to the mess of CSS rules I have to display an individual card, and something is conflicting. Or maybe there’s something I’m missing? I realize ther
Apologies for the messy code. I’m not sure what’s to be expected or needed as far as code here, so I’ve pretty much just pasted the whole WIP file here. Not included is the card data that is actually passed, but that is simple JSON data containing strings and integers that are interpreted by the javascript.
image of what the page looks like
cardSearch.html: Displayed to the user, takes cardResults (array containing cards) and repeatedly adds card.html to the page with each card’s data.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>CE War - Card Search</title>
<link rel="stylesheet" type="text/css" th:href="@{style.css}">
<link rel="icon" type="image/png" th:href="@{images/logo.png}">
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
</head>
<body>
<div id="card-results">
<div th:each="aCard : ${cardResults}" class="card-display" style="--card-height: 400px">
<div th:replace="~{fragments/card :: card(${aCard})}"></div>
</div>
</div>
</body>
</html>
card.html (fragment that is repeated in cardSearch.html)
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<title>CE War - Card Display</title>
<link rel="stylesheet" type="text/css" th:href="@{style.css}">
<link rel="icon" type="image/png" th:href="@{images/logo.png}">
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
</head>
<body>
<!-- Insert the container located at fragments/header with the tag "th:fragment="header"" -->
<div th:insert="~{fragments/header :: header}"/>
<main>
<div class="card-table blank-card" th:fragment="card(cardData)">
<div class="header dark-color">
<div class="name"></div>
<img class="rarity">
</div>
<img class="art">
<p class="desc light-color"/>
<div class="footer dark-color">
<div class="attack"></div>
<div class="center">
CE 2nd
</div>
<div class="health"></div>
</div>
<script th:inline="javascript">
// Needed for Thymeleaf to be able to run this code
/*<![CDATA[*/
createCard(/*[[${cardData}]]*/[0]);
function createCard(card) {
// SECTION Script Functions
/** Constructs a card symbol in the form of an html span element.
* This element contains a svg with size 1em of the given symbolType, and the same color as the card's type.
* This element has the "card-symbol" class.
*
* @param symbolType - which symbol to create. Valid symbols are c1, c2, c3, c4, stage, equip, god, back-slot, []
*/
function formatSymbol(symbolType) {
// Determine which symbol to use
switch (symbolType.toLowerCase()) {
case "c1": case "c2": case "c3": case "c4":
case "stage": case "equip": case "god":
// symbol type is valid
var symbolID = symbolType.toLowerCase();
break;
case "back-slot": case "[]":
// symbol type is valid
var symbolID = "back-slot";
break;
default:
var symbolID = "";
// Log error to website console. Symbol should end up not appearing.
console.error("Invalid symbol type of type "" + symbolType + """);
}
// Valid types: CREATURE, ACTION, BUILDING, MATERIAL, GOD, REPLICA, OTHER
switch (card.type) {
case "CREATURE": case "ACTION": case "BUILDING": case "MATERIAL": case "GOD": case "REPLICA":
var symbolColor = `var(--${card.type.toLowerCase()}-color)`;
break;
case "OTHER":
if (card.id == "gap") {
var symbolColor = "var(--gap-color)";
break;
}
// else: Continue to default case
default:
var symbolColor = "#000000";
// Log error to website console. Symbol color should be black.
console.error("Invalid card type of type "" + card.type + """);
}
// Combine strings into final html element
return `<svg class="card-symbol">
<use href="images/card_symbol.svg#${symbolID}" style="--icon-color: ${symbolColor};"/>
</svg>`;
}
// Formats card effects to include line breaks, italics, and lists.
function formatEffect(string) {
const listRegex = new RegExp("(^[0-9]+: )|(^- )");
// Split effect, based on line breaks for further processing
let cardEffectArr = string.split("n");
for (let i = 0; i < cardEffectArr.length; i++) {
if (listRegex.test(cardEffectArr[i])) {
// If this is a list item
if (i == 0 | cardEffectArr[i-1].charAt(0) != "<") {
// If previous item was not a list item
if (cardEffectArr[i].charAt(0) == "-") {
var isOrdered = false;
cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
cardEffectArr[i] = "<ul>" + cardEffectArr[i];
} else {
var isOrdered = true;
cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
cardEffectArr[i] = "<ol>" + cardEffectArr[i];
}
} else {
// Ensure this is a list item
// This allows the regex to work properly and avoid issues
cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
}
if (cardEffectArr[i].charAt(0) == "<" && i == cardEffectArr.length - 1) {
// If this is the last item in the list
if (isOrdered) {
cardEffectArr[i] = cardEffectArr[i] + "</ol>";
} else {
cardEffectArr[i] = cardEffectArr[i] + "</ul>";
}
}
} else {
// Not a list item
if (i > 0 && listRegex.test(cardEffectArr[i - 1])) {
// If previous index was a list item
if (isOrdered) {
cardEffectArr[i - 1] = cardEffectArr[i - 1] + "</ol>";
} else {
cardEffectArr[i - 1] = cardEffectArr[i - 1] + "</ul>";
}
}
cardEffectArr[i] = cardEffectArr[i] + "<br>";
}
}
let cardEffect = cardEffectArr.join("");
const symbolRegex = new RegExp("\(\S+\)", "g");
cardEffect = cardEffect.replaceAll(symbolRegex, function(match) {
return formatSymbol(match.slice(1,-1)); // Remove outer parentheses
});
return cardEffect;
}
// Constructs card description field from multiple data sources.
function constructCardDesc() {
let cardArchetypes = "";
if (card.archetypes.length > 0) {
// Format archetypes to be lowercase, since the enums are declared as uppercase
for (let i = 0; i < card.archetypes.length; i++) {
card.archetypes[i] = card.archetypes[i].charAt(0) + card.archetypes[i].slice(1).toLowerCase();
}
cardArchetypes = "<em>" + card.archetypes.join(", ") + "</em><br>";
}
let cardMaterials = "";
if (card.materials.length > 0) {
cardMaterials = "<em>" + card.materials + "</em><br>";
}
let cardFlavorText = "";
if (card.flavorText.length > 0) {
if (card.effect.length > 0) {
cardFlavorText = "<br>";
}
cardFlavorText = cardFlavorText + "<em>" + card.flavorText + "</em>";
}
let cardEffect = formatEffect(card.effect);
return cardArchetypes + cardMaterials + cardEffect + cardFlavorText;
}
// !SECTION
console.log(card);
// Pick the first blank card
var cardDiv = $(".card-table.blank-card").first();
// Prevent this card from being selected again
cardDiv.removeClass("blank-card");
// Construct card name field, adding any needed attribute symbols
let cardAttributes = [];
for (let i = 0; i < card.attributes.length; i++) {
cardAttributes[i] = formatSymbol(card.attributes[i]);
}
cardDiv.find(".name").html(cardAttributes.join("") + " " + card.name);
// Determine card rarity
switch (card.rarity) {
case "COMMON":
cardDiv.find(".rarity").attr("src", "images/rarity_common.png");
break;
case "UNCOMMON":
cardDiv.find(".rarity").attr("src", "images/rarity_uncommon.png");
break;
case "RARE":
cardDiv.find(".rarity").attr("src", "images/rarity_rare.png");
break;
case "ULTRA_RARE":
cardDiv.find(".rarity").attr("src", "images/rarity_ultra_rare.png");
break;
case "NONE":
if (card.type == "GOD") {
// Stretch card text div to take up entire width of card.
cardDiv.find(".name").css("width", "calc(125% - 10px)"); // Accomodate border space
cardDiv.find(".rarity").style = "display: none;" // Hide rarity symbol
}
break;
default:
}
// Add Card Art
cardDiv.find(".art").attr("src", card.artSource);
// Construct card description
cardDiv.find(".desc").html(constructCardDesc());
// Determine left footer text
if (card.attack == -1) {
if (card.size.length == 2) {
cardDiv.find(".attack").text(card.size[0] + "x" + card.size[1]);
} else {
cardDiv.find(".attack").text("");
}
} else if (card.attack == -2) {
cardDiv.find(".attack").text("???");
} else {
cardDiv.find(".attack").text(card.attack);
}
// Determine right footer text
if (card.health == -2) {
cardDiv.find(".health").text("???");
} else {
if (card.health == -1) {
cardDiv.find(".health").text("");
} else {
cardDiv.find(".health").text(card.health);
}
if (card.god) {
cardDiv.find(".health").append(formatSymbol("back-slot")); // Could also do "[]" instead
}
}
// Determine card color
switch(card.type) {
case "CREATURE":
var darkColor = "#7fdc39";
var lightColor = "#b6d7a8";
break;
case "ACTION":
var darkColor = "#4c4cff";
var lightColor = "#9fc5e8";
break;
case "BUILDING":
var darkColor = "#ff0000";
var lightColor = "#ea9999";
break;
case "MATERIAL":
var darkColor = "#ffff00";
var lightColor = "#ffe599";
break;
case "GOD":
var darkColor = "#ff9900";
var lightColor = "#f9cb9c";
break;
case "REPLICA":
var darkColor = "#b7b7b7";
var lightColor = "#d9d9d9";
break;
default:
var darkColor = "#ffffff";
var lightColor = "#ffffff";
}
cardDiv.find(".dark-color").css("background-color", darkColor);
cardDiv.find(".light-color").css("background-color", lightColor);
}
// Adjust text scaling (at least trying to)
// TODO
/*]]>*/
</script>
</div>
</main>
</body>
</html>
style.css (properties relating to other pages have been omitted)
:root {
/* Colors used for cards. Maybe should be moved elsewhere. */
--creature-color: #7fdc39;
--action-color: #4c4cff;
--building-color: #ff0000;
--material-color: #ffff00;
--god-color: #ff9900;
--replica-color: #b7b7b7;
--gap-color: #c934e8;
}
/* used for cards */
@property --icon-color {
syntax: "<color>";
inherit: false;
initial-value: var(--action-color);
}
/* variable way to modify card height */
@property --card-height {
syntax: "<length>";
inherit: true;
initial-value: 600px;
}
main {
padding: 10px;
font-family: Arial;
}
body {
padding: 0px;
margin: 0px;
}
header {
text-align: center;
background-color: #3C0;
padding: 50px;
box-sizing: border-box;
}
/* Navigation bar on all pages */
nav {
text-align: center;
background-color: #aaa;
padding: 10px;
box-sizing: border-box;
}
/* Anchor tags inside navigation bar */
nav a {
display: inline-block;
border: 1px solid black;
padding: 5px;
box-sizing: border-box;
}
.card-table {
display: grid;
grid-template-areas:
"header"
"art"
"desc"
"footer";
grid-template-rows: 12% 34% 42% 12%;
grid-template-columns: 100%;
width: calc(var(--card-height) * 0.725);
height: var(--card-height);
border: 5px solid black;
border-radius: 5px;
div {
display: grid;
margin: 0px 0px 0px 0px;
padding: 5px;
border-radius: 0px;
overflow: hidden;
border: 0px;
}
p {
margin: 0;
line-height: 1.2em;
}
/* No margin, uses area's padding instead. Between paragraphs, insert a new margin.*/
p + p {
margin-block-start: 0.5em;
}
> .header {
grid-area: header;
grid-template-areas:
"name rarity";
grid-template-columns: 80% 20%;
grid-template-rows: 100%;
border-bottom: 5px solid black;
gap: 0px;
padding: 0px;
> .name {
grid-area: name;
font-size: 1.5em;
border-right: 5px solid black;
display: flex;
align-items: center;
padding: 5px;
}
> .rarity {
grid-area: rarity;
justify-self: center;
height: 100%;
width: auto;
}
}
> .art {
grid-area: art;
padding: 0px;
width: 100%;
height: 100%; /* will contract just barely */
}
> .desc {
grid-area: desc;
padding: 5px;
border-top: 5px solid black;
border-bottom: 5px solid black;
ul, ol { /* remove spacing before list */
margin-block: 0;
}
svg.card-symbol {
transform: translateY(0.2em); /* FIXME temporary fix to re-align symbols inside card descriptions */
}
}
> .footer {
grid-template-areas:
"attack center health";
grid-template-columns: 33% 34% 33%;
grid-template-rows: 100%;
align-items: center;
justify-items: center;
> .attack {
grid-area: attack;
font-size: 2em;
display: flex;
align-items: center;
}
> .center {
grid-area: center;
display: flex;
align-items: center;
}
> .health {
grid-area: health;
font-size: 2em;
display: flex;
align-items:center;
}
}
svg.card-symbol {
display: inline-flex;
align-self: center;
width: 1.25em;
height: 1.25em;
}
}
.symbol-color {
fill: var(--icon-color);
}
.card-display {
padding: 5px;
display: inline-block;
}



