Module:Mbox: Difference between revisions
From Paracosm
More actions
No edit summary |
m update from wiki-tools |
||
| (One intermediate revision by the same user not shown) | |||
| Line 1: | Line 1: | ||
local libraryUtil = require( 'libraryUtil' ) | require('strict') | ||
--- @module Mbox | |||
--- The box behind page notices, hatnotes and Entity empty states: a title, an | |||
--- optional body that opens in place as a <details> disclosure, a type colour | |||
--- and an optional icon. Module:Hatnote, Module:Entity/EmptyState and | |||
--- Module:Dependencies draw through render(); the notice templates call main() | |||
--- via #invoke. | |||
local libraryUtil = require('libraryUtil') | |||
local checkType = libraryUtil.checkType | local checkType = libraryUtil.checkType | ||
local | local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg | ||
local | local details = require('Module:Details') | ||
local icon = require('Module:Icon') | |||
local p = {} | local p = {} | ||
--- | local TYPES = { notice = true, warning = true, error = true } | ||
-- | |||
-- @ | --- @class MboxProps | ||
-- | --- @field title string Headline wikitext. | ||
-- @ | --- @field text? string Body wikitext; given, the box becomes a collapsible <details>. | ||
local function | --- @field type? string 'notice' (default), 'warning' or 'error'; anything else is 'notice'. | ||
--- @field icon? string File name, without the File: prefix. | |||
--- @field iconMask? boolean CSS mask (default) so the icon takes the title colour; false renders a [[File:]] thumbnail. | |||
--- @field placeholder? boolean Dashed slot standing in for an empty section; carries no role. | |||
--- @field open? boolean Body starts open. Defaults to false. | |||
--- @field class? string Extra classes on the root (metadata, plainlinks, selfref, …). | |||
--- @param value any | |||
--- @return boolean | |||
local function isNonEmptyString(value) | |||
return type(value) == 'string' and value ~= '' | |||
end | end | ||
function | --- @param props MboxProps | ||
--- @return string | |||
local function rootClass(props) | |||
local | -- TextExtracts drops div elements and the classes in $wgExtractsRemoveClasses but keeps | ||
-- <details>, so only noexcerpt (one of those classes) keeps a disclosure box's text out of | |||
if | -- the meta description and Page Previews; a static box's div root is dropped either way. | ||
local classes = { 't-mbox', 't-mbox--' .. props.type, 'navigation-not-searchable', 'noexcerpt' } | |||
if props.placeholder then | |||
classes[#classes + 1] = 't-mbox--placeholder' | |||
end | end | ||
if isNonEmptyString(props.class) then | |||
classes[#classes + 1] = props.class | |||
end | |||
return table.concat(classes, ' ') | |||
end | end | ||
function | --- Icon, title and, for a disclosure, the chevron. | ||
--- @param props MboxProps | |||
--- @param withIndicator boolean | |||
--- @return string | |||
local function headerContent(props, withIndicator) | |||
local parts = {} | |||
if isNonEmptyString(props.icon) then | |||
local mask = props.iconMask ~= false | |||
parts[#parts + 1] = icon.render({ | |||
icon = props.icon, | |||
mask = mask, | |||
-- A [[File:]] size must be in px; the mask scales with the title. | |||
size = mask and '1em' or '14px', | |||
class = 't-mbox__icon', | |||
}) | |||
end | |||
parts[#parts + 1] = tostring(mw.html.create('span'):addClass('t-mbox__title'):wikitext(props.title)) | |||
if withIndicator then | |||
-- The Citizen collapse icon Module:CollapsibleCard uses; styles.css rotates it. | |||
parts[#parts + 1] = | |||
tostring(mw.html.create('span'):addClass('citizen-ui-icon mw-ui-icon-wikimedia-collapse t-mbox__indicator')) | |||
end | |||
return table.concat(parts) | |||
end | |||
--- @param props MboxProps | |||
local | --- @return string | ||
local function renderStatic(props) | |||
if | local root = mw.html.create('div'):addClass(rootClass(props)) | ||
if not props.placeholder then | |||
root:attr('role', 'note') | |||
end | end | ||
root:tag('div'):addClass('t-mbox__header'):wikitext(headerContent(props, false)) | |||
return tostring(root) | |||
end | |||
--- @param props MboxProps | |||
--- @return string | |||
local function renderDisclosure(props) | |||
-- Newlines let a body that starts with a list or table parse as block wikitext. | |||
local body = mw.html.create('div'):addClass('t-mbox__text'):newline():wikitext(props.text):newline() | |||
return details.getWikitext({ | |||
details = { content = tostring(body), class = rootClass(props), open = props.open }, | |||
summary = { content = headerContent(props, true), class = 't-mbox__header' }, | |||
}) | |||
end | |||
--- @param props MboxProps | |||
--- @return string | |||
function p.render(props) | |||
checkType('Module:Mbox.render', 1, props, 'table') | |||
checkTypeForNamedArg('Module:Mbox.render', 'title', props.title, 'string') | |||
local | local resolved = { | ||
title = props.title, | |||
text = isNonEmptyString(props.text) and props.text or nil, | |||
type = TYPES[props.type] and props.type or 'notice', | |||
icon = props.icon, | |||
iconMask = props.iconMask, | |||
placeholder = props.placeholder == true, | |||
open = props.open == true, | |||
class = props.class, | |||
} | |||
local frame = mw.getCurrentFrame() | |||
local styles = frame:extensionTag({ name = 'templatestyles', args = { src = 'Module:Mbox/styles.css' } }) | |||
if isNonEmptyString(resolved.icon) then | |||
styles = styles .. frame:extensionTag({ name = 'templatestyles', args = { src = 'Module:Icon/styles.css' } }) | |||
end | end | ||
local markup = resolved.text and renderDisclosure(resolved) or renderStatic(resolved) | |||
return styles .. markup | |||
end | |||
--- #invoke entry. Reads only the invocation's own arguments, so the calling | |||
: | --- template's parameters (CommLink's `type`, TranscriptNotice's | ||
--- `type`) never reach the box. | |||
--- @param frame mw.frame | |||
--- @return string | |||
function p.main(frame) | |||
local args = require('Module:Arguments').getArgs(frame, { frameOnly = true }) | |||
local yesno = require('Module:Yesno') | |||
local title = args.title or args[1] | |||
if not title then | |||
return require('Module:Error').error({ message = 'Error: no title specified.' }) | |||
end | |||
return p.render({ | |||
title = title, | |||
text = args.text or args[2], | |||
type = args.type, | |||
icon = args.icon, | |||
iconMask = yesno(args.iconmask, true), | |||
placeholder = yesno(args.placeholder, false), | |||
open = yesno(args.open, false), | |||
class = args.class, | |||
}) | |||
end | |||
--- Alias for `{{#invoke:Mbox|mbox}}` invocations. | |||
p.mbox = p.main | |||
return p | return p | ||
Latest revision as of 00:27, 12 November 2026
Documentation for this module may be created at Module:Mbox/doc
require('strict')
--- @module Mbox
--- The box behind page notices, hatnotes and Entity empty states: a title, an
--- optional body that opens in place as a <details> disclosure, a type colour
--- and an optional icon. Module:Hatnote, Module:Entity/EmptyState and
--- Module:Dependencies draw through render(); the notice templates call main()
--- via #invoke.
local libraryUtil = require('libraryUtil')
local checkType = libraryUtil.checkType
local checkTypeForNamedArg = libraryUtil.checkTypeForNamedArg
local details = require('Module:Details')
local icon = require('Module:Icon')
local p = {}
local TYPES = { notice = true, warning = true, error = true }
--- @class MboxProps
--- @field title string Headline wikitext.
--- @field text? string Body wikitext; given, the box becomes a collapsible <details>.
--- @field type? string 'notice' (default), 'warning' or 'error'; anything else is 'notice'.
--- @field icon? string File name, without the File: prefix.
--- @field iconMask? boolean CSS mask (default) so the icon takes the title colour; false renders a [[File:]] thumbnail.
--- @field placeholder? boolean Dashed slot standing in for an empty section; carries no role.
--- @field open? boolean Body starts open. Defaults to false.
--- @field class? string Extra classes on the root (metadata, plainlinks, selfref, …).
--- @param value any
--- @return boolean
local function isNonEmptyString(value)
return type(value) == 'string' and value ~= ''
end
--- @param props MboxProps
--- @return string
local function rootClass(props)
-- TextExtracts drops div elements and the classes in $wgExtractsRemoveClasses but keeps
-- <details>, so only noexcerpt (one of those classes) keeps a disclosure box's text out of
-- the meta description and Page Previews; a static box's div root is dropped either way.
local classes = { 't-mbox', 't-mbox--' .. props.type, 'navigation-not-searchable', 'noexcerpt' }
if props.placeholder then
classes[#classes + 1] = 't-mbox--placeholder'
end
if isNonEmptyString(props.class) then
classes[#classes + 1] = props.class
end
return table.concat(classes, ' ')
end
--- Icon, title and, for a disclosure, the chevron.
--- @param props MboxProps
--- @param withIndicator boolean
--- @return string
local function headerContent(props, withIndicator)
local parts = {}
if isNonEmptyString(props.icon) then
local mask = props.iconMask ~= false
parts[#parts + 1] = icon.render({
icon = props.icon,
mask = mask,
-- A [[File:]] size must be in px; the mask scales with the title.
size = mask and '1em' or '14px',
class = 't-mbox__icon',
})
end
parts[#parts + 1] = tostring(mw.html.create('span'):addClass('t-mbox__title'):wikitext(props.title))
if withIndicator then
-- The Citizen collapse icon Module:CollapsibleCard uses; styles.css rotates it.
parts[#parts + 1] =
tostring(mw.html.create('span'):addClass('citizen-ui-icon mw-ui-icon-wikimedia-collapse t-mbox__indicator'))
end
return table.concat(parts)
end
--- @param props MboxProps
--- @return string
local function renderStatic(props)
local root = mw.html.create('div'):addClass(rootClass(props))
if not props.placeholder then
root:attr('role', 'note')
end
root:tag('div'):addClass('t-mbox__header'):wikitext(headerContent(props, false))
return tostring(root)
end
--- @param props MboxProps
--- @return string
local function renderDisclosure(props)
-- Newlines let a body that starts with a list or table parse as block wikitext.
local body = mw.html.create('div'):addClass('t-mbox__text'):newline():wikitext(props.text):newline()
return details.getWikitext({
details = { content = tostring(body), class = rootClass(props), open = props.open },
summary = { content = headerContent(props, true), class = 't-mbox__header' },
})
end
--- @param props MboxProps
--- @return string
function p.render(props)
checkType('Module:Mbox.render', 1, props, 'table')
checkTypeForNamedArg('Module:Mbox.render', 'title', props.title, 'string')
local resolved = {
title = props.title,
text = isNonEmptyString(props.text) and props.text or nil,
type = TYPES[props.type] and props.type or 'notice',
icon = props.icon,
iconMask = props.iconMask,
placeholder = props.placeholder == true,
open = props.open == true,
class = props.class,
}
local frame = mw.getCurrentFrame()
local styles = frame:extensionTag({ name = 'templatestyles', args = { src = 'Module:Mbox/styles.css' } })
if isNonEmptyString(resolved.icon) then
styles = styles .. frame:extensionTag({ name = 'templatestyles', args = { src = 'Module:Icon/styles.css' } })
end
local markup = resolved.text and renderDisclosure(resolved) or renderStatic(resolved)
return styles .. markup
end
--- #invoke entry. Reads only the invocation's own arguments, so the calling
--- template's parameters (CommLink's `type`, TranscriptNotice's
--- `type`) never reach the box.
--- @param frame mw.frame
--- @return string
function p.main(frame)
local args = require('Module:Arguments').getArgs(frame, { frameOnly = true })
local yesno = require('Module:Yesno')
local title = args.title or args[1]
if not title then
return require('Module:Error').error({ message = 'Error: no title specified.' })
end
return p.render({
title = title,
text = args.text or args[2],
type = args.type,
icon = args.icon,
iconMask = yesno(args.iconmask, true),
placeholder = yesno(args.placeholder, false),
open = yesno(args.open, false),
class = args.class,
})
end
--- Alias for `{{#invoke:Mbox|mbox}}` invocations.
p.mbox = p.main
return p