Module:Swatches
Module:Swatches
Lists cosmetic swatches from Module:Swatches/data as infoboxes.
This module outputs its content inside a collapsible container, which is collapsed by default (mw-collapsible mw-collapsed).
When expanded, the swatch infobox cards are laid out using an inner flex container configured for responsive wrapping and horizontal overflow (display:flex; flex-wrap:wrap; overflow-x:auto; align-items:flex-start; gap:1em;).
Usage
List all swatches
If no scope is provided, all swatches in the dataset are shown:
{{#invoke:Swatches|list}}
List swatches for a scope page
Scope can be specified to show all swatches that are compatible with at least one class in that scope:
{{#invoke:Swatches|list|<SCOPE>}}
List swatches for a class page
Class can additionally be specified to show only swatches that are compatible with that class:
{{#invoke:Swatches|list|<SCOPE>|<CLASS>}}
Render swatches expanded
By default, the swatch list is collapsed. To render it expanded, pass a truthy value to the expanded parameter.
{{#invoke:Swatches|list|expanded=yes}}
Parameters
- 1 (optional)
- string
- Compatibility scope to filter swatches.
- If omitted, all swatches are shown.
- If an invalid value is specified, no swatches are shown.
- Valid values (case-insensitive, singular or plural):
- •
Vehicle - •
Weapon - •
Garment - •
Placeable
- •
- 2 (optional)
- string
- Name of a class within the specified scope, to further filter swatch compatibility.
{{PAGENAME}}can be used if the page title matches the dataset naming.- Examples (case-insensitive):
- •
Sandbike - •
Flamethrowers - •
Heavy Armor
- •
- expanded (optional)
- boolean
- If specified with a truthy value, output will be expanded by default.
- Valid values:
- •
true - •
yes - •
1
- •
Notes
- Swatch compatibility rules are defined in Module:Swatches/data using the optional
scopesandonlyClassesfields. classis matched case-insensitively against class names withinonlyClassestokens in Module:Swatches/data.
local p = {}
local data = require("Module:Swatches/data")
-- ---------- Helpers ----------
local function trim(s)
return mw.text.trim(tostring(s or ""))
end
local function lowerTrim(s)
return mw.ustring.lower(trim(s))
end
local function isTruthy(s)
s = lowerTrim(s)
return s == "true" or s == "yes" or s == "1"
end
local function listHasToken(list, token)
for _, v in ipairs(list) do
if lowerTrim(v) == token then return true end
end
return false
end
-- ---------- Data ----------
local SORTED_DATA = {}
for name, swatch in pairs(data) do
swatch.name = name
SORTED_DATA[#SORTED_DATA + 1] = swatch
end
table.sort(SORTED_DATA, function(a, b)
return lowerTrim(a.name) < lowerTrim(b.name)
end)
local SCOPE_MAP = {
vehicle = "Vehicles",
weapon = "Weapons",
garment = "Garments",
placeable = "Placeables",
}
-- ---------- Matching ----------
local function normalizeInputScope(s)
s = lowerTrim(s)
if s == "" then return "" end
-- match singular
if SCOPE_MAP[s] then return s end
-- match plural
if s:sub(-1) == "s" then
local singular = s:sub(1, -2)
if SCOPE_MAP[singular] then return singular end
end
-- invalid input scope
return nil
end
local function deriveCompatibilityDisplay(swatch)
local scopes = swatch.scopes or { "all" }
if listHasToken(scopes, "all") then return nil end
local classByScope = {}
if swatch.onlyClasses then
for _, token in ipairs(swatch.onlyClasses) do
local scope, class = token:match("^([^:]+):(.+)$")
if scope and class then
classByScope[scope] = classByScope[scope] or {}
table.insert(classByScope[scope], class)
end
end
end
local labels = {}
for _, scope in ipairs(scopes) do
local classes = classByScope[scope]
if classes and #classes > 0 then
for _, class in ipairs(classes) do
labels[#labels + 1] = class
end
else
labels[#labels + 1] = "All " .. (SCOPE_MAP[scope] or scope)
end
end
table.sort(labels)
return labels
end
local function swatchAppliesTo(swatch, scope, class)
-- If no scope specified, show all swatches
if scope == "" then return true end
-- If 'scopes' not specified in dataset, default to "all"
local swatchScopes = swatch.scopes or { "all" }
if listHasToken(swatchScopes, "all") then return true end
-- Otherwise must match the requested scope
if not listHasToken(swatchScopes, scope) then return false end
-- If no class specified, scope match is enough
if class == "" then return true end
-- If 'onlyClasses' is not present, applies to all classes in this scope
if not swatch.onlyClasses or #swatch.onlyClasses == 0 then return true end
local hasRestrictionForScope = false
local token = scope .. ":" .. class
for _, v in ipairs(swatch.onlyClasses) do
if lowerTrim(v):match("^" .. scope .. ":") then
hasRestrictionForScope = true
if lowerTrim(v) == token then return true end
end
end
-- If onlyClass was specified for this scope, and none matched → false
-- If there were no restrictions for this scope → true
return not hasRestrictionForScope
end
-- ---------- Rendering ----------
local function renderColorBox(color)
if not color or color == "" then
return mw.html.create("span"):wikitext("NULL")
end
local box = mw.html.create("div")
:addClass("copyable")
:css({
width = "38px",
height = "38px",
["background-color"] = color,
display = "inline-block",
border = "1px solid #000"
})
-- copyable color code
box:tag("span")
:attr("data-value", color)
:addClass("copy-text")
-- tooltip anchor
box:tag("span")
:attr("aria-hidden", "true")
:addClass("tooltiptext")
:wikitext("Copy to clipboard")
return box
end
local function renderCompatibilityHTML(swatch)
local labels = deriveCompatibilityDisplay(swatch)
if not labels then
return mw.html.create("span"):wikitext("All")
end
if #labels == 1
then return mw.html.create("span"):wikitext(labels[1])
end
local ul = mw.html.create("ul")
for _, v in ipairs(labels) do
ul:tag("li"):wikitext(v)
end
return ul
end
local function renderInfoboxHTML(swatch)
local infobox = mw.html.create("table")
:addClass("infobox")
-- title
local trAbove = infobox:tag("tr")
local thAbove = trAbove:tag("th")
:attr("colspan", "2")
:addClass("infobox-above")
if swatch.link and swatch.link ~= "" then
thAbove:wikitext(string.format("[[%s|%s]]", swatch.link, swatch.name))
else
thAbove:wikitext(swatch.name)
end
-- image
local trImage = infobox:tag("tr")
local tdImage = trImage:tag("td")
:attr("colspan", "2")
:addClass("infobox-image")
local image = string.format(
"[[File:%s|200px|center|link=",
swatch.image or "Placeholder.jpg"
)
if swatch.link and swatch.link ~= "" then
image = image .. swatch.link
end
tdImage:wikitext(image .. "]]")
-- color boxes
local trColors = infobox:tag("tr")
local tdColors = trColors:tag("td")
:attr("colspan", "2")
:addClass("infobox-full-data")
local div = tdColors:tag("div")
:css("white-space", "nowrap")
for i = 1, 4 do
local c = swatch.colors and swatch.colors[i]
div:node(renderColorBox(c))
end
-- cosmetic type
local trType = infobox:tag("tr")
trType:tag("th")
:attr("scope", "row")
:addClass("infobox-label")
:wikitext("[[Cosmetics|Cosmetic]] Type")
trType:tag("td")
:addClass("infobox-data")
:wikitext("Swatch")
-- compatibility
local trCompat = infobox:tag("tr")
trCompat:tag("th")
:attr("scope", "row")
:addClass("infobox-label")
:wikitext("Compatibility")
local tdCompat = trCompat:tag("td")
:addClass("infobox-data")
tdCompat:node(renderCompatibilityHTML(swatch))
return infobox
end
function p.list(frame)
local args = frame.args
local scope = normalizeInputScope(args[1] or "")
if args[1] and args[1] ~= "" and not scope then
return string.format(
'<span class="error">Invalid scope "%s"</span>',
mw.text.encode(args[1])
)
end
local class = lowerTrim(args[2] or "")
local outer = mw.html.create("div")
:addClass("mw-collapsible")
local expanded = isTruthy(args.expanded)
if not expanded then outer:addClass("mw-collapsed") end
local content = outer:tag("div")
:addClass("mw-collapsible-content")
:css({
display = "flex",
["flex-wrap"] = "wrap",
["overflow-x"] = "auto",
["align-items"] = "flex-start",
gap = "1em"
})
local rendered = false
for _, swatch in ipairs(SORTED_DATA) do
if swatchAppliesTo(swatch, scope, class) then
content:node(renderInfoboxHTML(swatch))
rendered = true
end
end
if not rendered then return "No matching swatches." end
return tostring(outer)
end
return p