Module:Collapsible
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Collapsible/doc
-- Module:Collapsible (safe args version)
-- Renders a simple collapsible box using MediaWiki's mw-collapsible JS/CSS.
-- Exposed functions:
-- * p.collapsible(frame)
-- * p.main(frame) -- alias
--
-- FIX: never write to frame.args or parent.args. Always copy to a plain table.
local p = {}
-- ---------- helpers ----------
local function shallow_copy(t)
if type(t) ~= 'table' then return {} end
local out = {}
for k, v in pairs(t) do out[k] = v end
return out
end
local function trim(s)
if type(s) ~= 'string' then return s end
if mw and mw.text and mw.text.trim then
return mw.text.trim(s)
end
return (s:gsub('^%s+', ''):gsub('%s+$', ''))
end
-- Get args as a **plain, writable** table
local function getArgsPlain(frame)
-- Try Module:Arguments first
local ok, Arguments = pcall(require, 'Module:Arguments')
if ok and type(Arguments) == 'table' and type(Arguments.getArgs) == 'function' then
-- Some wikis return a proxy; copy again to be 100% writable
local a = Arguments.getArgs(frame, { parentOnly = true })
return shallow_copy(a)
end
-- Manual fallback: copy parent/child args
local src = (frame.getParent and frame:getParent() and frame:getParent().args) or frame.args or {}
-- src may be read-only; copy to a new table:
return shallow_copy(src)
end
-- Normalize without mutating the original
local function normalizeArgs(inArgs)
local a = {}
for k, v in pairs(inArgs) do
if type(v) == 'string' then
local t = trim(v)
if t ~= '' then a[k] = t end
else
a[k] = v
end
end
return a
end
-- ---------- renderer ----------
local function renderBox(a)
-- Params (read only)
local title = a.title or a.heading or 'Details'
local state = a.state or a.collapse or '' -- 'collapsed' to start closed
local content = a.content or a[1] or '' -- |content= or |1=
local width = a.width or ''
local class = a.class or ''
local style = a.style or ''
-- sanitize width
if width ~= '' then
if width:match('^%d+$') then
width = width .. 'px'
elseif not width:match('^%d+%s*[%a%%]+$') and not width:match('^[%d%.]+%s*ch$') then
width = '' -- ignore suspicious values
end
end
-- classes for collapsible behavior
local collClass = 'mw-collapsible'
if state == 'collapsed' or state == 'close' or state == 'closed' then
collClass = collClass .. ' mw-collapsed'
end
if class ~= '' then
collClass = collClass .. ' ' .. class
end
-- wrapper styles (Wikipedia-ish)
local baseStyle = 'border:1px solid #a2a9b1;background:#f8f9fa;border-radius:2px;overflow:hidden;margin:0.4em 0;'
if width ~= '' then baseStyle = baseStyle .. 'width:' .. width .. ';' end
if style ~= '' then baseStyle = baseStyle .. style end
local out = {}
out[#out+1] = string.format('<div class="%s" style="%s">', collClass, baseStyle)
-- Header with toggle
out[#out+1] = '<div style="background:#eaecf0;border-bottom:1px solid #a2a9b1;padding:6px 8px;font-weight:600;display:flex;align-items:center;gap:8px;">'
out[#out+1] = '<span class="mw-collapsible-toggle"></span>'
out[#out+1] = title
out[#out+1] = '</div>'
-- Content
out[#out+1] = '<div class="mw-collapsible-content" style="padding:8px 10px;">'
out[#out+1] = content or ''
out[#out+1] = '</div>'
out[#out+1] = '</div>'
return table.concat(out)
end
-- ---------- entry points ----------
function p.collapsible(frame)
-- ALWAYS work on a writable copy
local raw = getArgsPlain(frame)
local args = normalizeArgs(raw)
return renderBox(args)
end
p.main = p.collapsible
return p