Jump to content
Toggle menu
  • 1 articles
  • 7 files
  • 3 users
  • 102 edits
Shroudforge
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

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
Last modified
Copyright