Jump to content
Toggle menu
  • 8 articles
  • 75 files
  • 7 users
  • 37.8K edits
Neodyland Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Clickable button: Difference between revisions

From Neodyland Wiki
import from module:clickable button/sandbox that'll likely be merged into module:clickable button
sync enwiki
Line 1: Line 1:
-- [[w:en:Module:Clickable button]]
--------------------------------------------------------------------------------
-- Builds Codex's button component. See [[wmdoc:codex/latest]].
--- @module 'Codex clickable button' [[en:Module:Clickable button]]
-- Implements [[Template:Clickable button]] and others.
--- Generates wikitext for clickable Codex button.
-- Wiki's [[Special:Version]] must have @wikimedia/codex and its dependences.
--- Generates wikitext to render the button component from the (Codex design
-- To add icons: [[Template:Clickable button/styles.css]].
--- system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
--- - Options to include an icon
--- - Create an icon-only or a dummy button
--- - Target a URL or a wikilink
--- - Set the weight, size and state of the button (enabled or disabled).
--- Includes helper functions for URL parsing and cleaning, adding tracking
--- categories. Intended for use in templates and other modules.
--- Implements [[Template:Clickable button]] and others.
--- Supports legacy parameters.
--- To add icons: [[Template:Clickable button/styles.css]].
 
-- TRACKING CATEGORIES:
-- [[Category:Pages using clickable dummy button]]
-- [[Category:Pages using disabled dummy button]]
-- [[Category:Pages using clickable button with external links]]
-- [[Category:Pages using clickable button with outdated classes]]
-- [[Category:Errors reported by Module:Clickable button]]
-- unless nocat=true. Adds category= any custom category regardless of nocat=.
 
-- DEPENDENCIES:
require('strict')
local yesno = require('Module:Yesno')
-- [[Template:Clickable button/styles.css]]
-- [[Module:Yesno]] [[Module:Arguments]]
-- [[Module:Check for unknown parameters]]
-- [[Special:Version]] must include @wikimedia/codex.
-- [[Module:If preview]]


local p = {}
local p = {}


function p.main(frame)
--- Checks the URI is safe to use as a wikilink in MediaWiki.
local args = require('Module:Arguments').getArgs(frame, {
---@param s string The URL to check
wrappers = 'Template:Clickable button', 'Template:Clickable button/sandbox',
---@return mw.uri|nil uri The URI of the given URL
'Template:Cdx-button'})
---@class mw.uri: string
local function safeUri(s)
local success, uri = pcall(function()
return mw.uri.new(s)
end)
if success then
return uri
else
return nil
end
end
 
--- Extracts a URL from a string.
---@param extract string The full string from which the URL must be extracted from
---@return string url The URL
local function extractUrl(extract)
local url = extract
--- @type string
url = mw.ustring.gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
'https://%2')
local uri = safeUri(url);
if uri and uri.host then
return url
end
return ''
end
 
--- Parses the `url`. The `url` parameter is required. `text` label is
--- optional and can be generated from the `url`.
--- @param url string The Url
--- @param text string The display label of the wikilink
--- @return string url The Url
--- @return string text The display label of the wikilink
local function _url(url, text)
--- @type string Given Url but with any trailing whitespace removed
url = mw.text.trim(url or '')
text = mw.text.trim(text or '')
 
if url == '' then
return '', text
end
 
-- If the URL contains any unencoded spaces, encode them,
-- because MediaWiki will otherwise interpret a space as the end of the URL.
url = mw.ustring.gsub(url, '%s', function(s)
return mw.uri.encode(s, 'PATH')
end)
 
-- If there is an empty query string or fragment ID,
-- remove it as it will cause mw.uri.new to throw an error
url = mw.ustring.gsub(url, '#$', '')
url = mw.ustring.gsub(url, '%?$', '')
 
-- If it's an http(s) URL without the double slash, fix it.
---- url = mw.ustring.gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
url = mw.ustring.gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
'https://%2')
 
local uri = safeUri(url)
 
-- Handle URL's without a protocol and URL's that are protocol-relative,
-- e.g. www.example.com/foo or www.example.com:8080/foo, and //www.example.com/foo
if uri and (not uri.protocol or (uri.protocol and not uri.host)) and url:sub(1, 2) ~= '//' then
url = 'http://' .. url
uri = safeUri(url)
end
 
if text == '' then
if uri then
if uri.path == '/' then uri.path = '' end
 
local port = ''
if uri.port then port = ':' .. uri.port end
 
text = mw.ustring.lower(uri.host or '') .. port .. (uri.relativePath or '')


-- Return empty string if no arguments were supplied
-- Add <wbr> before _/.-sequences
local hasInput = false
text = mw.ustring.gsub(text, "(/+)", "<wbr/>%1") -- This entry MUST be the first. "<wbr/>" has a "/" in it, you know.
for k, v in pairs(args) do
text = mw.ustring.gsub(text, "(%.+)", "<wbr/>%1")
if v and v ~= "" then
-- text = mw.ustring.gsub(text,"(%-+)","<wbr/>%1") -- DISABLED for now
hasInput = true
text = mw.ustring.gsub(text, "(%#+)", "<wbr/>%1")
break
text = mw.ustring.gsub(text, "(_+)", "<wbr/>%1")
else -- URL is badly-formed, so just display whatever was given
text = url
end
end
end
end
if not hasInput then
 
return url, text
end
 
--- Cleans and normalises a URL string.
--- Copied from [[en:Module:URL]] with minor modifications.
--- - Encodes `url`.
--- - Removes empty query strings and fragement IDs.
--- - Fixes the protocol and the double slash that follows, i.e. `https://`
--- - Handles URLs that have no protocol or are protocol-relative.
--- - Generates label from the URL if one is not given.
---@param url string The raw URL to clean.
---@param text string Optional link display text.
---@return string url Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
local function url(url, text)
url = url or extractUrl(url) or extractUrl(text) or ''
-- Strip out HTML tags and [ ] from URL
url = (url or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
-- Truncate anything after a space
url = url:gsub("%%20", " "):gsub(" .*", "")
return _url(url, text)
end
 
--- Renders tracking categories based on given parameters.
---@param args table
local function renderTrackingCategories(args)
local categories = ''
local class = args.class and args.class:lower() or ''
---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
---- local title = mw.title.getCurrentTitle()
 
-- Don't add categories if nocat=yes, but still add any custom category.
-- Custom category passed in
if args.category and yesno(args.nocat) == false then
local q = args.category
q = q:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
categories = categories .. '[[' .. 'Category:' .. q .. ']]'
end
if yesno(args.nocat) == true then
return ''
return ''
end
end


return frame:extensionTag(
--[=[
'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
categories = categories .. check_for_unknown_parameters({
) .. p.luaMain(args)
checkpositional = "y",
ignoreblank = "y",
regexp1 = "header[%d]+",
regexp2 = "label[%d]+",
regexp3 = "data[%d]+[abc]?",
regexp4 = "class[%d]+[abc]?",
regexp5 = "rowclass[%d]+",
regexp6 = "rowstyle[%d]+",
regexp7 = "rowcellstyle[%d]+",
unknown = "[[Category:Pages using infobox3cols with undocumented parameters|_VALUE_" .. title.text .. "]]",
'class', 'color', 'weight', 'size', 'icon', 'link', 'url', 'disabled',
'label', 'aria-label', 'arialabel', 'aria_label', 'action', 'nocat',
'category', '1', '2'
}, args) ]=]
 
if ((not args.link and not args.url
and not args.disabled and not args.ariaDisabled)
or ((args.link or args.url) and not args.label)) then
-- Dummy button has no link, no URL and is not disabled
-- OR link/URL but no visible label
categories = categories .. '[[Category:Pages using clickable dummy button]]'
elseif (not args.link and not args.url
and (args.disabled or args.ariaDisabled)) then
-- Disabled button
categories = categories .. '[[Category:Pages using disabled dummy button]]'
end
if class == 'ui-button-green'
or class == 'ui-button-blue'
or class == 'ui-button-red'
or class == 'mw-ui-progressive'
or class == 'mw-ui-destructive' then
categories = categories .. '[[Category:Pages using clickable button with outdated classes]]'
end
if args.url then
categories = categories .. '[[Category:Pages using clickable button with external links]]'
end
if class == 'mw-ui-constructive' then
categories = categories .. '[[Category:Pages using clickable button with deprecated parameters]]'
end
 
return categories
end
end


function p.luaMain(args)
--- Renders the wikitext span tags for the button
--- @param data table
--- @return string link
local function renderLink(data)
-- Build button span
local displaySpan = mw.html.create('span')
for _, class in ipairs(data.classes or {}) do
displaySpan:addClass(class)
end
displaySpan:attr('role', 'button')
if data.aria_label then
displaySpan:attr('aria-label', data.aria_label)
end
-- ARIA disabled attribute for disabled/no-link/dummy buttons
if data.disabled or data.ariaDisabled then
displaySpan:attr('aria-disabled', 'true')
elseif data.disabled == false then
displaySpan:attr('aria-disabled', 'false')
end
if data.iconSpan then
displaySpan:node(data.iconSpan)
end
if data.label then
displaySpan:wikitext(data.label)
--[[ span:node(mw.html.create('span')
:addClass('cdx-button--text')
:wikitext(data.label)) ]]
end
 
local display = tostring(displaySpan)
 
-- Build link
local link
if data.disabled then
return string.format('%s %s', display, data.categories)
end
if data.isUrl then
---- mw.logObject(data)
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
  data.url, display, data.categories)
elseif data.isUrl == false then
link = string.format('[[%s|%s]] %s', data.link, display,
  data.categories)
else
-- Dummy/disabled button
link = string.format('%s %s', display, data.categories)
end
 
if data.error then
local ifPreview = require('Module:If preview')
return ifPreview.main( {data.error .. link .. ' ' ..
[[Category:Errors reported by Module:Clickable button]], link} )
end


-- Backward compatibility:
return link
end


-- @TODO: In future, can format any wikilinks removing underscores,
--- Parses the module's arguments for backward compatibility with deprecated
--       URL-coding using [[Module:Format link]] and [[Module:Urldecode]]
--- parameters from old templates and modules.
--       like in [[Template:Format linkr]]
---@param args table Module's arguments.
-- It's weird that we may make a link a label above, but if we truly only got 1=,
---@return table args Parsed arguments.
-- then that would mean it's intentional to make both the link and label the same.
local function parseParameters(args)
-- Ensure label is set from priority: label= > 2= > 1=
--- It's weird that we may make a link a label, but if we truly
--- only got positional argument `1`, then that would mean it's
--- intentional to make both the link and label the same.
--- `label` value priority: `label` > `2` > `1`
---@type string
args.label = args.label or args[2] or args[1]
args.label = args.label or args[2] or args[1]


--Disable if link=no or disabled=1
--- `disabled` is `true` if:
args.disabled = (args.link == "no") or (args.disabled == "1")
--- - `link` = `'no'` or `false`
 
--- - `disabled` = `'1'` or `true`
--- @type boolean
args.disabled = (yesno(args.link) == false)
or yesno(args.disabled)
--- `link` value priority: `link` > `1`
args.link = args.link or args[1]
args.link = args.link or args[1]


Line 50: Line 292:
args[2] = nil
args[2] = nil


--[[ Remove links if disabled
if (args.link and yesno(args.link) ~= false) or args.url then
if args.disabled then
args.link = nil
args.url = nil
end ]]
 
-- Make aria-disabled=true later on if no link
if args.link and args.link ~= "no" then
args.ariaDisabled = false
args.ariaDisabled = false
else
else
-- If no link whatsoever, make dummy button.
--- `aria-disabled = true` if no link whatsoever.
-- But for accessibility, ARIA must know it won't do anything.
--- Make dummy button. But for accessibility,
--- ARIA must know it won't do anything.
--- _OPTION_ to forcefully disable dummy buttons
--- by setting `args.disabled = true`
args.ariaDisabled = true
args.ariaDisabled = true
-- Option to forcefully disable dummy buttons.
-- args.disabled = true
end
end
 
-- Normalize ARIA label keys
-- Normalize ARIA label keys
    args.aria_label = args.aria_label or args['aria-label'] or args.arialabel
args.aria_label = args.aria_label or args['aria-label'] or args.arialabel


-- Determine action from old parameters color/class
-- Determine action from old parameters color/class
Line 74: Line 310:
local class = type(args.class) == 'string' and args.class:lower()
local class = type(args.class) == 'string' and args.class:lower()
if (color == "blue"
if (color == "blue"
or color == "green"
or color == "green"
or class == 'ui-button-green'
or class == 'ui-button-green'
or class == 'ui-button-blue'
or class == 'ui-button-blue'
or class == 'mw-ui-progressive'
or class == 'mw-ui-constructive'
or class == 'progressive') then
or class == 'mw-ui-progressive'
or class == 'progressive') then
args.action = "progressive"
args.action = "progressive"
args.class = nil
elseif (color == "red"
elseif (color == "red"
    or class == 'ui-button-red'
or class == 'ui-button-red'
or class == 'mw-ui-destructive'
or class == 'mw-ui-destructive'
or class == 'destructive') then
or class == 'destructive') then
args.action = "destructive"
args.action = "destructive"
args.class = nil
end
end


local data = p.makeLinkData(args)
return args
 
-- Error if ARIA label required and not provided
if data == 'error-no-aria' then
return '<strong class="error">Clickable button error: A button without' ..
' a visible label needs an [[WAI-ARIA|ARIA]] label, please define it using "' ..
'aria-label".</strong>'
end
 
return p.renderLink(data)
end
end


-- Constructs attributes for the HTML elements.
--- Constructs the attributes for the wikitext/HTML elements.
function p.makeLinkData(args)
--- @param args table
--- @return table data
local function makeLinkData(args)
local data = {}
local data = {}


Line 107: Line 339:
if args.url then
if args.url then
data.isUrl = true
data.isUrl = true
-- Clean up URL
-- Make pretty URL and label based on URL if no label.
--[[ local URI = require "URI"
local uri = URI:new(args.url)
data.url = uri
local label = uri:host() .. uri:path()
if label:len() > 20 then
label = uri:host()
end ]]
local label
local label
data.url, label = p.url(args.url, args.label)
data.url, label = url(args.url, args.label)
-- Make pretty link label using the URL if no label
data.label = args.label or label
data.label = args.label or label
elseif args.link then
elseif args.link then
Line 117: Line 355:
data.label = args.label
data.label = args.label
elseif not args.url and not args.link then
elseif not args.url and not args.link then
-- Dummy button, no link at all
-- Dummy button, no link or url
data.label = args.label
data.label = args.label
end
end
-- Make sure if icon-only button or no label at all button (i.e. dummy button) it has aria-label else error.
 
if (not data.label
-- @TODO: Error tracking category
and not args.aria_label
-- Error if no aria-label and no visible label
and not args.disabled
if (not data.label and not args.aria_label
and not args.ariaDisabled) then
and not args.disabled and not args.ariaDisabled) then
return 'error-no-aria'
data.error = '<span class="error"><strong>Preview warning:</strong>'
.. ' A button without a visible label '
.. 'needs an [[WAI-ARIA|ARIA]] label, please define it using '
.. '"aria-label".</span>'
end
end


-- Classes
-- Classes
local class = type(args.class) == 'string' and args.class:lower()
local class = type(args.class) == 'string' and args.class:lower()
data.classes = {'cdx-button', 'cdx-button--fake-button'}
or ''
local action = type(args.action) == 'string' and args.action:lower() or 'default'
local action = type(args.action) == 'string' and args.action:lower()
local weight = type(args.weight) == 'string' and args.weight:lower() or 'normal'
or 'default' -- or 'default' by default
local size  = type(args.size) == 'string' and args.size:lower() or 'medium'
local weight = type(args.weight) == 'string' and args.weight:lower()
or 'normal' -- or 'normal' by default
local size  = type(args.size) == 'string' and args.size:lower()
or 'medium' -- or 'medium' by default


data.classes = { 'cdx-button', 'cdx-button--fake-button' }
table.insert(data.classes, 'cdx-button--action-' .. action)
table.insert(data.classes, 'cdx-button--action-' .. action)
table.insert(data.classes, 'cdx-button--weight-' .. weight)
table.insert(data.classes, 'cdx-button--weight-' .. weight)
table.insert(data.classes, 'cdx-button--size-' .. size)
table.insert(data.classes, 'cdx-button--size-' .. size)
if class then
table.insert(data.classes, class) -- Custom class
end


-- Disabled state
-- Disabled state
Line 151: Line 399:
if icon then
if icon then
data.iconSpan = mw.html.create('span')
data.iconSpan = mw.html.create('span')
:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
data.iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
:attr('aria-hidden', 'true')
data.iconSpan:attr('aria-hidden', 'true')
if not data.label then
-- Icon-only button, add extra class for styling
table.insert(data.classes, 'cdx-button--icon-only')
end
end
 
-- Label length checks
if data.label then
if mw.ustring.len(data.label) > 40 then
data.error = ('<span class="error"><strong>Preview warning:</strong> A button label'
.. ' should ideally be shorter than 38 characters, see [[Template:Clickable button'
.. '#Button label length|documentation]].</span>')
end
-- Short label min-width custom CSS adjustment per Codex documentation.
if mw.ustring.len(data.label) < 3 then
table.insert(data.classes, 'cdx-button--short-label')
end
end
end


Line 161: Line 426:
end
end


function p.renderLink(data)
--- Function that can be called by other Lua modules.
-- Build span tag
--- @param args table
local span = mw.html.create('span')
--- @return table|string data
for _, class in ipairs(data.classes or {}) do
function p._main(args)
span:addClass(class)
local parsedArgs = parseParameters(args)
end
local data = makeLinkData(parsedArgs)
span:attr('role', 'button')
data.categories = renderTrackingCategories(parsedArgs)
if data.aria_label then
span:attr('aria-label', data.aria_label)
end
-- ARIA disabled attribute for disabled/no-link/dummy buttons
if data.disabled or data.ariaDisabled then
span:attr('aria-disabled', 'true')
elseif data.disabled == false then
span:attr('aria-disabled', 'false')
end
if data.iconSpan then
span:node(data.iconSpan)
end
if data.label then
span:wikitext(data.label)
end
 
local display = tostring(span)


if data.disabled then
return renderLink(data)
return display
end
if data.isUrl then
-- mw.logObject(data)
return string.format('<span class="plainlinks">[%s %s]</span>', data.url, display)
elseif data.isUrl == false then
return string.format('[[%s|%s]]', data.link, display)
else
-- No url/link provided
return display
end
end
end


-- Cleans up and validates a URL.
--- Main function called by templates to use this module.
-- Copied from [[en:Module:URL]] with minor modifications.
--- Using the `{{#invoke:Clickable button|main|arguments}}` parser function.
--- @param frame frame
--- @return string Returns wikitext for insertion in a wiki page.
function p.main(frame)
local args = require('Module:Arguments').getArgs(frame, {
wrappers = 'Template:Clickable button',
'Template:Clickable button/sandbox',
'Template:Cdx-button'
})


local function safeUri(s)
-- Return empty string if no arguments supplied
local success, uri = pcall(function()
local hasInput = false
return mw.uri.new(s)
for _, v in pairs(args) do
end)
if v and v ~= "" then
if success then
hasInput = true
return uri
break
end
end
 
local function extractUrl(extract)
local url = extract
url = mw.ustring.gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])',
  'http%1://%3')
local uri = safeUri(url);
if uri and uri.host then
return url
end
end
end
function p._url(url, text)
url = mw.text.trim(url or '')
text = mw.text.trim(text or '')
if url == '' then
return text
end
end
 
if not hasInput then
-- If the URL contains any unencoded spaces, encode them, because MediaWiki will otherwise interpret a space as the end of the URL.
return ''
url = mw.ustring.gsub(url, '%s', function(s) return mw.uri.encode(s,
  'PATH')
  end)
 
-- If there is an empty query string or fragment id, remove it as it will cause mw.uri.new to throw an error
url = mw.ustring.gsub(url, '#$', '')
url = mw.ustring.gsub(url, '%?$', '')
 
-- If it's an HTTP[S] URL without the double slash, fix it.
url = mw.ustring.gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
 
local uri = safeUri(url)
 
-- Handle URL's without a protocol and URL's that are protocol-relative,
-- e.g. www.example.com/foo or www.example.com:8080/foo, and //www.example.com/foo
if uri and (not uri.protocol or (uri.protocol and not uri.host)) and url:sub(1, 2) ~= '//' then
url = 'http://' .. url
uri = safeUri(url)
end
end


if text == '' then
return frame:extensionTag(
if uri then
'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
if uri.path == '/' then uri.path = '' end
) .. p._main(args)
 
local port = ''
if uri.port then port = ':' .. uri.port end
 
text = mw.ustring.lower(uri.host or '') .. port .. (uri.relativePath or '')
 
-- Add <wbr> before _/.-# sequences
text = mw.ustring.gsub(text, "(/+)", "<wbr/>%1") -- This entry MUST be the first. "<wbr/>" has a "/" in it, you know.
text = mw.ustring.gsub(text, "(%.+)", "<wbr/>%1")
-- text = mw.ustring.gsub(text,"(%-+)","<wbr/>%1") -- DISABLED for now
text = mw.ustring.gsub(text, "(%#+)", "<wbr/>%1")
text = mw.ustring.gsub(text, "(_+)", "<wbr/>%1")
else -- URL is badly-formed, so just display whatever was passed in
text = url
end
end
 
return url, text
end
 
function p.url(URL, TEXT)
local url = URL or ''
local text = TEXT or ''
url = url or extractUrl(url) or extractUrl(text) or ''
-- Strip out HTML tags and [ ] from URL
url = (url or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
-- Truncate anything after a space
url = url:gsub("%%20", " "):gsub(" .*", "")
return p._url(url, text)
end
end


return p
return p

Revision as of 19:29, 22 September 2025

Module documentation[ view · edit · history · purge ]
This documentation is transcluded from Module:Clickable button/doc.

Template:Template rating

Template:Uses templatestyles

Generates wikitext for clickable Codex button. Renders the button component from the Codex design system for Wikimedia. Includes helper functions for URL parsing and cleaning, adding tracking categories. Intended for use in templates and other modules. Implements Template:Clickable button and others. Supports legacy parameters.

  • Options to include an icon or create an icon-only button.
  • Target a URL or a wikilink
  • Set the weight, size and state of the button (enabled or disabled).
  • Dummy button creation can be disabled.

For more information on appropriate usage of UI buttons, see the Codex documentation.

  • Inserts two CSS files. Module:Clickable button/styles.css is required and makes minor tweaks for word-wrapping if the visible label is too long, centering or aligning button left or right, and minimum widths as is needed for icon-only buttons and labels containing two characters or less.
  • The second CSS file, Module:Clickable button/icons.css, is prepended to the button's HTML only if an icon is used.
  • Supports legacy parameters from previous templates.

Usage in wikitext

Some arguments are case-insensitive.

{{#invoke:Clickable button|main
| 1        = <!-- Alias for wikilink -->
| 2        = <!-- Alias for label -->
| label    = <!-- Button visible text label -->
| link     = <!-- Target wikilink -->
| url      = <!-- Target external URL -->
<!-- Inputs action, weight, size, and icon are case-insensitive -->
| action   = <!-- progressive | destructive | default: default. -->
| weight   = <!-- primary | quiet | default: normal. -->
| size     = <!-- small | large | default: medium. Automatically chooses size based on line-height and device. -->
| icon     = <!-- Name of icon, stored in [[Module:Clickable button/icons.css]] e.g., search  -->
| disabled = <!-- `true` or any other true value like `1` or `yes`. -->
| aria-label = <!-- [[w:ARIA]] label for accessibility DOM tree. -->
| nocat      = <!-- `true` to not auto-categorize. -->
<!-- Others -->
| category = <!-- Category name e.g., Category:Name or Name or [[Category:Name]] -->
| class    = <!-- Custom CSS class without quotation marks -->
| style    = <!-- Custom CSS styling without quotation marks -->
<!-- Legacy arguments -->
| color    = <!-- blue | red --> 
}}

Usage in other modules

Ensure you know what to expect from the function you call from another module.

  • function p.main(frame) emits TemplateStyles for the CSS files with the wikitext, and pre-processes the arguments in a frame using Module:Arguments, e.g. ignore blank values'', and trim trailing whitespace.
  • function p._main(arguments) Parses the arguments such as lowercase appropriate arguments, account for use of legacy parameters and decides whether aria-disabled should be true.
  • function p.url(url, [label]) is available, not for button creation, but as an adaption of Module:URL to clean and normalise a URL string and optionally generate a label.
  • The module's other functions, such as makeLinkData() and renderLink(), are localised/local to the module and would need to be made global first to be accessible to other modules.

To call p.main() for example, use:

local createButton = require( 'Module:Clickable button/sandbox' )
buttonWikitext = createButton.main( {
    link = 'South Africa',
    label = 'Go to South Africa',
    action = 'progressive'
    weight = 'default',
    size = 'medium',
    icon = 'link-external',
} ) 
return buttonWikitext

and the value of buttonWikitext would be:

<<templatestyles src="Module:Cdx-button/styles.css" /><templatestyles src="Module:Cdx-button/icons.css" /><span class="cdx-button cdx-button--fake-button cdx-button--action-progressive cdx-button--weight-quiet cdx-button--size-medium" role="button" aria-disabled="false"><span class="cdx-button__icon cdx-demo-css-icon--link-external" aria-hidden="true"></span>Go to South Africa</span>

Function _main would output:

<span class="cdx-button cdx-button--fake-button cdx-button--action-progressive cdx-button--weight-quiet cdx-button--size-medium" role="button" aria-disabled="false"><span class="cdx-button__icon cdx-demo-css-icon--link-external" aria-hidden="true"></span>Go to South Africa</span>

As a result, unless a CSS file is added to give the appropriate class an icon, the icon will not render.

Implementation

Length of visible label

See the Codex button component documentation.


--------------------------------------------------------------------------------
--- @module 'Codex clickable button' [[en:Module:Clickable button]]
--- Generates wikitext for clickable Codex button.
--- Generates wikitext to render the button component from the (Codex design
--- system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
--- - Options to include an icon
--- - Create an icon-only or a dummy button
--- - Target a URL or a wikilink
--- - Set the weight, size and state of the button (enabled or disabled).
--- Includes helper functions for URL parsing and cleaning, adding tracking 
--- categories. Intended for use in templates and other modules.
--- Implements [[Template:Clickable button]] and others.
--- Supports legacy parameters.
--- To add icons: [[Template:Clickable button/styles.css]].

-- TRACKING CATEGORIES:
-- [[Category:Pages using clickable dummy button]]
-- [[Category:Pages using disabled dummy button]]
-- [[Category:Pages using clickable button with external links]]
-- [[Category:Pages using clickable button with outdated classes]]
-- [[Category:Errors reported by Module:Clickable button]]
-- unless nocat=true. Adds category= any custom category regardless of nocat=.

-- DEPENDENCIES:
require('strict')
local yesno = require('Module:Yesno')
-- [[Template:Clickable button/styles.css]]
-- [[Module:Yesno]] [[Module:Arguments]]
-- [[Module:Check for unknown parameters]]
-- [[Special:Version]] must include @wikimedia/codex.
-- [[Module:If preview]]

local p = {}

--- Checks the URI is safe to use as a wikilink in MediaWiki.
---@param s string The URL to check
---@return mw.uri|nil uri The URI of the given URL
---@class mw.uri: string
local function safeUri(s)
	local success, uri = pcall(function()
		return mw.uri.new(s)
	end)
	if success then
		return uri
	else
		return nil
	end
end

--- Extracts a URL from a string.
---@param extract string The full string from which the URL must be extracted from
---@return string url The URL
local function extractUrl(extract)
	local url = extract
	--- @type string
	url = mw.ustring.gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
		'https://%2')
	local uri = safeUri(url);
	if uri and uri.host then
		return url
	end
	return ''
end

--- Parses the `url`. The `url` parameter is required. `text` label is
--- optional and can be generated from the `url`.
--- @param url string The Url
--- @param text string The display label of the wikilink
--- @return string url The Url
--- @return string text The display label of the wikilink
local function _url(url, text)
	--- @type string Given Url but with any trailing whitespace removed
	url = mw.text.trim(url or '')
	text = mw.text.trim(text or '')

	if url == '' then
		return '', text
	end

	-- If the URL contains any unencoded spaces, encode them,
	-- because MediaWiki will otherwise interpret a space as the end of the URL.
	url = mw.ustring.gsub(url, '%s', function(s)
		return mw.uri.encode(s, 'PATH')
	end)

	-- If there is an empty query string or fragment ID,
	-- remove it as it will cause mw.uri.new to throw an error
	url = mw.ustring.gsub(url, '#$', '')
	url = mw.ustring.gsub(url, '%?$', '')

	-- If it's an http(s) URL without the double slash, fix it.
	---- url = mw.ustring.gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
	url = mw.ustring.gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
		'https://%2')

	local uri = safeUri(url)

	-- Handle URL's without a protocol and URL's that are protocol-relative,
	-- e.g. www.example.com/foo or www.example.com:8080/foo, and //www.example.com/foo
	if uri and (not uri.protocol or (uri.protocol and not uri.host)) and url:sub(1, 2) ~= '//' then
		url = 'http://' .. url
		uri = safeUri(url)
	end

	if text == '' then
		if uri then
			if uri.path == '/' then uri.path = '' end

			local port = ''
			if uri.port then port = ':' .. uri.port end

			text = mw.ustring.lower(uri.host or '') .. port .. (uri.relativePath or '')

			-- Add <wbr> before _/.-sequences
			text = mw.ustring.gsub(text, "(/+)", "<wbr/>%1") -- This entry MUST be the first. "<wbr/>" has a "/" in it, you know.
			text = mw.ustring.gsub(text, "(%.+)", "<wbr/>%1")
			-- text = mw.ustring.gsub(text,"(%-+)","<wbr/>%1") 	-- DISABLED for now
			text = mw.ustring.gsub(text, "(%#+)", "<wbr/>%1")
			text = mw.ustring.gsub(text, "(_+)", "<wbr/>%1")
		else -- URL is badly-formed, so just display whatever was given
			text = url
		end
	end

	return url, text
end

--- Cleans and normalises a URL string.
--- Copied from [[en:Module:URL]] with minor modifications.
--- - Encodes `url`.
--- - Removes empty query strings and fragement IDs.
--- - Fixes the protocol and the double slash that follows, i.e. `https://`
--- - Handles URLs that have no protocol or are protocol-relative.
--- - Generates label from the URL if one is not given.
---@param url string The raw URL to clean.
---@param text string Optional link display text.
---@return string url Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
local function url(url, text)
	url = url or extractUrl(url) or extractUrl(text) or ''
	-- Strip out HTML tags and [ ] from URL
	url = (url or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
	-- Truncate anything after a space
	url = url:gsub("%%20", " "):gsub(" .*", "")
	return _url(url, text)
end

--- Renders tracking categories based on given parameters.
---@param args table
local function renderTrackingCategories(args)
	local categories = ''
	local class = args.class and args.class:lower() or ''
	---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
	---- local title = mw.title.getCurrentTitle()

	-- Don't add categories if nocat=yes, but still add any custom category.
	-- Custom category passed in
	if args.category and yesno(args.nocat) == false then
		local q = args.category
		q = q:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
		categories = categories .. '[[' .. 'Category:' .. q .. ']]'
	end
	if yesno(args.nocat) == true then
		return ''
	end

	--[=[
	categories = categories .. check_for_unknown_parameters({
		checkpositional = "y",
		ignoreblank = "y",
		regexp1 = "header[%d]+",
		regexp2 = "label[%d]+",
		regexp3 = "data[%d]+[abc]?",
		regexp4 = "class[%d]+[abc]?",
		regexp5 = "rowclass[%d]+",
		regexp6 = "rowstyle[%d]+",
		regexp7 = "rowcellstyle[%d]+",
		unknown = "[[Category:Pages using infobox3cols with undocumented parameters|_VALUE_" .. title.text .. "]]",
		'class', 'color', 'weight', 'size', 'icon', 'link', 'url', 'disabled',
		'label', 'aria-label', 'arialabel', 'aria_label', 'action', 'nocat',
		'category', '1', '2'
		}, args) ]=]

	if ((not args.link and not args.url
		and not args.disabled and not args.ariaDisabled)
		or ((args.link or args.url) and not args.label)) then
		-- Dummy button has no link, no URL and is not disabled
		-- OR link/URL but no visible label
		categories = categories .. '[[Category:Pages using clickable dummy button]]'
	elseif (not args.link and not args.url
			and (args.disabled or args.ariaDisabled)) then
		-- Disabled button
		categories = categories .. '[[Category:Pages using disabled dummy button]]'
	end
	if class == 'ui-button-green'
		or class == 'ui-button-blue'
		or class == 'ui-button-red'
		or class == 'mw-ui-progressive'
		or class == 'mw-ui-destructive' then
		categories = categories .. '[[Category:Pages using clickable button with outdated classes]]'
	end
	if args.url then
		categories = categories .. '[[Category:Pages using clickable button with external links]]'
	end
	if class == 'mw-ui-constructive' then
		categories = categories .. '[[Category:Pages using clickable button with deprecated parameters]]'
	end

	return categories
end

--- Renders the wikitext span tags for the button
--- @param data table
--- @return string link
local function renderLink(data)
	-- Build button span
	local displaySpan = mw.html.create('span')
	for _, class in ipairs(data.classes or {}) do
		displaySpan:addClass(class)
	end
	displaySpan:attr('role', 'button')
	if data.aria_label then
		displaySpan:attr('aria-label', data.aria_label)
	end
	-- ARIA disabled attribute for disabled/no-link/dummy buttons
	if data.disabled or data.ariaDisabled then
		displaySpan:attr('aria-disabled', 'true')
	elseif data.disabled == false then
		displaySpan:attr('aria-disabled', 'false')
	end
	if data.iconSpan then
		displaySpan:node(data.iconSpan)
	end
	if data.label then
		displaySpan:wikitext(data.label)
		--[[ span:node(mw.html.create('span')
			:addClass('cdx-button--text')
			:wikitext(data.label)) ]]
	end

	local display = tostring(displaySpan)

	-- Build link
	local link
	if data.disabled then
		return string.format('%s %s', display, data.categories)
	end
	if data.isUrl then
		---- mw.logObject(data)
		link = string.format('<span class="plainlinks">[%s %s]</span> %s',
			   data.url, display, data.categories)
	elseif data.isUrl == false then
		link = string.format('[[%s|%s]] %s', data.link, display,
			   data.categories)
	else
		-- Dummy/disabled button
		link = string.format('%s %s', display, data.categories)
	end

	if data.error then
		local ifPreview = require('Module:If preview')
		return ifPreview.main( {data.error .. link .. ' ' ..
			[[Category:Errors reported by Module:Clickable button]], link} )
	end

	return link
end

--- Parses the module's arguments for backward compatibility with deprecated
--- parameters from old templates and modules.
---@param args table Module's arguments.
---@return table args Parsed arguments.
local function parseParameters(args)
	--- It's weird that we may make a link a label, but if we truly
	--- only got positional argument `1`, then that would mean it's
	--- intentional to make both the link and label the same.
	--- `label` value priority: `label` > `2` > `1`
	---@type string
	args.label = args.label or args[2] or args[1]

	--- `disabled` is `true` if:
	--- - `link` = `'no'` or `false`
	--- - `disabled` = `'1'` or `true`
	--- @type boolean
	args.disabled = (yesno(args.link) == false)
		or yesno(args.disabled)
	--- `link` value priority: `link` > `1`
	args.link = args.link or args[1]

	-- Remove positional args after assigning
	args[1] = nil
	args[2] = nil

	if (args.link and yesno(args.link) ~= false) or args.url then
		args.ariaDisabled = false
	else
		--- `aria-disabled = true` if no link whatsoever.
		--- Make dummy button. But for accessibility,
		--- ARIA must know it won't do anything.
		--- _OPTION_ to forcefully disable dummy buttons
		--- by setting `args.disabled = true`
		args.ariaDisabled = true
	end

	-- Normalize ARIA label keys
	args.aria_label = args.aria_label or args['aria-label'] or args.arialabel

	-- Determine action from old parameters color/class
	local color = type(args.color) == 'string' and args.color:lower()
	local class = type(args.class) == 'string' and args.class:lower()
	if (color == "blue"
			or color == "green"
			or class == 'ui-button-green'
			or class == 'ui-button-blue'
			or class == 'mw-ui-constructive'
			or class == 'mw-ui-progressive'
			or class == 'progressive') then
		args.action = "progressive"
		args.class = nil
	elseif (color == "red"
			or class == 'ui-button-red'
			or class == 'mw-ui-destructive'
			or class == 'destructive') then
		args.action = "destructive"
		args.class = nil
	end

	return args
end

--- Constructs the attributes for the wikitext/HTML elements.
--- @param args table
--- @return table data
local function makeLinkData(args)
	local data = {}

	-- Decide link vs. url vs. none
	-- URL has priority over link if both provided. Also, clean URL
	if args.url then
		data.isUrl = true
		-- Make pretty URL and label based on URL if no label.
		--[[ local URI = require "URI"
		local uri = URI:new(args.url)
		data.url = uri
		local label = uri:host() .. uri:path()
		if label:len() > 20 then
			label = uri:host()
		end ]]
		local label
		data.url, label = url(args.url, args.label)
		data.label = args.label or label
	elseif args.link then
		data.isUrl = false
		data.link = args.link
		data.label = args.label
	elseif not args.url and not args.link then
		-- Dummy button, no link or url
		data.label = args.label
	end

	-- @TODO: Error tracking category
	-- Error if no aria-label and no visible label
	if (not data.label and not args.aria_label
		and not args.disabled and not args.ariaDisabled) then
		data.error = '<span class="error"><strong>Preview warning:</strong>'
		.. ' A button without a visible label '
		.. 'needs an [[WAI-ARIA|ARIA]] label, please define it using '
		.. '"aria-label".</span>'
	end

	-- Classes
	local class  = type(args.class) == 'string' and args.class:lower()
		or ''
	local action = type(args.action) == 'string' and args.action:lower()
		or 'default' -- or 'default' by default
	local weight = type(args.weight) == 'string' and args.weight:lower()
		or 'normal' -- or 'normal' by default
	local size   = type(args.size) == 'string' and args.size:lower()
		or 'medium' -- or 'medium' by default

	data.classes = { 'cdx-button', 'cdx-button--fake-button' }
	table.insert(data.classes, 'cdx-button--action-' .. action)
	table.insert(data.classes, 'cdx-button--weight-' .. weight)
	table.insert(data.classes, 'cdx-button--size-' .. size)
	if class then
		table.insert(data.classes, class) -- Custom class
	end

	-- Disabled state
	data.disabled = args.disabled
	if data.disabled then
		table.insert(data.classes, 'cdx-button--fake-button--disabled')
	else
		table.insert(data.classes, 'cdx-button--fake-button--enabled')
	end

	-- Icon
	local icon = type(args.icon) == 'string' and args.icon:lower()
	if icon then
		data.iconSpan = mw.html.create('span')
		data.iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
		data.iconSpan:attr('aria-hidden', 'true')
		if not data.label then
			-- Icon-only button, add extra class for styling
			table.insert(data.classes, 'cdx-button--icon-only')
		end
	end

	-- Label length checks
	if data.label then
		if mw.ustring.len(data.label) > 40 then
			data.error = ('<span class="error"><strong>Preview warning:</strong> A button label'
			.. ' should ideally be shorter than 38 characters, see [[Template:Clickable button'
			.. '#Button label length|documentation]].</span>')
		end
		-- Short label min-width custom CSS adjustment per Codex documentation.
		if mw.ustring.len(data.label) < 3 then
			table.insert(data.classes, 'cdx-button--short-label')
		end
	end

	data.aria_label = args.aria_label
	data.ariaDisabled = args.ariaDisabled

	return data
end

--- Function that can be called by other Lua modules.
--- @param args table
--- @return table|string data
function p._main(args)
	local parsedArgs = parseParameters(args)
	local data = makeLinkData(parsedArgs)
	data.categories = renderTrackingCategories(parsedArgs)

	return renderLink(data)
end

--- Main function called by templates to use this module.
--- Using the `{{#invoke:Clickable button|main|arguments}}` parser function.
--- @param frame frame
--- @return string Returns wikitext for insertion in a wiki page.
function p.main(frame)
	local args = require('Module:Arguments').getArgs(frame, {
		wrappers = 'Template:Clickable button',
		'Template:Clickable button/sandbox',
		'Template:Cdx-button'
	})

	-- Return empty string if no arguments supplied
	local hasInput = false
	for _, v in pairs(args) do
		if v and v ~= "" then
			hasInput = true
			break
		end
	end
	if not hasInput then
		return ''
	end

	return frame:extensionTag(
		'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
	) .. p._main(args)
end

return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.