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
sync enwiki
m 9 revisions imported
 
(3 intermediate revisions by 2 users not shown)
Line 3: Line 3:
--- Generates wikitext for clickable Codex button.
--- Generates wikitext for clickable Codex button.
---  
---  
--- Outputs wikitext to render the button component from the (Codex design
--- Outputs wikitext to render the (button component)[mdoc:codex/latest/components/demos/button.html]
--- system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
--- from the (Codex design system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
---- Options to include an icon or create an icon-only button.
---- Options to include an icon or create an icon-only button.
---- Target a URL or a wikilink
---- Target a URL or a wikilink
Line 10: Line 10:
---- Dummy button creation can be disabled.
---- Dummy button creation can be disabled.
---
---
--- Includes helper functions for URL parsing and cleaning, adding tracking  
--- Includes helper functions for URL parsing and cleaning, and adding tracking  
--- categories. Intended for use in templates and other modules.
--- categories. Intended for use in templates and other modules.
--- Supports legacy parameters. To add icons, see CSS.
--- Supports legacy parameters. To add icons, see CSS link in constants below.
---
---
--- @author [[User:Waddie96]]
--- @author [[User:Waddie96]]
Line 37: Line 37:
--- @field [1]? string Positional argument 1 (alias for link/label).
--- @field [1]? string Positional argument 1 (alias for link/label).
--- @field [2]? string Positional argument 2 (alias for label).
--- @field [2]? string Positional argument 2 (alias for label).
--- @var categories? string Additional categories to add.
--- @var categories? string|boolean Additional categories to add.
--- @var ariaDisabled? boolean Internal flag indicating if the button is functionally disabled for ARIA.
--- @var ariaDisabled? boolean Internal flag indicating if the button is functionally disabled for ARIA.
--- @var oldClassMatched string|boolean Internal flag for outdated classes.
--- @var oldClassMatched string|boolean Internal flag for outdated classes.
--- @var isUrl boolean Whether the link is a URL.
--- @var isUrl boolean Whether the link is a URL.
--- @var errorText string|nil Internal string used as both an indicator of an error, and error message text.
--- @var errorText string|nil  
--- @var tblClasses table Classes for the button span tag.
--- @var tblClasses table Classes for the button span tag.
--- @var pageTitle mw.title Title of the current page.
--- @var pageTitle mw.title Title of the current page.
--- @todo [[Module:Neturl]] [[Module:Check for unknown parameters]]
--- @todo [[Module:Neturl]] [[Module:Check for unknown parameters]]


-- Dependencies.
require('strict')
local yesno = require('Module:Yesno')
local yesno = require('Module:Yesno')
-- [[Module:Yesno]] [[Module:Arguments]] [[Module:Check for unknown parameters]]
local delink = require('Module:Delink')._delink
-- [[Special:Version]] must include @wikimedia/codex. [[Module:If preview]]


local DEFINITIONS = {
--- Tracking category constants.
trackingCategories = {
dummyButton = 'Category:Pages using clickable dummy button',
disabledButton = 'Category:Pages using disabled button',
    externalLinks = 'Category:Pages using clickable button with external links',
outdatedClasses = 'Category:Pages using clickable button with outdated classes',
unknownParams = 'Category:Pages using Module:Clickable button with unknown parameters',
errors = 'Category:Errors reported by Module:Clickable button',
},
--- Parameters whos inputs are converted to lowercase, and are case-insensitive.
lowercaseArgs = {'action', 'color', 'weight', 'size', 'icon'},
--- Valid arguments.
    knownArgs = {
        'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
        'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
        'nocat', 'category', '1', '2'
    },
--- Preview warning text for unknown arguments.
    unknownArgsPreviewText = '<span class="error"><strong>Preview warning:</strong>'
        .. ' Using undocumented parameter(s): "_VALUE_".</span>',
--- No ARIA-label warning text.
noAriaLabelWarningText = '<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>',
labelLengthWarningText = '<span class="error"><strong>Preview warning:'
.. '</strong> A button label should ideally be shorter '
.. 'than 38 characters, see [[en:Template:Clickable button/doc'
.. '#Button label length|documentation]].'
.. '</span>',
baseCSS = 'Template:Clickable button/styles.css',
iconsCSS = 'Template:Clickable button/icons.css',
legacyClassSets = { progressive = { ['blue'] = true, ['green'] = true,
['ui-button-green'] = true, ['ui-button-blue'] = true,
['mw-ui-constructive'] = true, ['mw-ui-progressive'] = true,
['progressive'] = true
},
destructive = { ['red'] = true, ['ui-button-red'] = true,
['mw-ui-destructive'] = true, ['destructive'] = true
}
}
}
local p = {}
local p = {}
local gsub = mw.ustring.gsub
local lower = mw.ustring.lower


--- Creates [URI object](lua://mw.uri).
function p.main(frame)
--- @see https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.uri
local getArgs = require('Module:Arguments').getArgs
---
local args = getArgs(frame)
--- Creates [URI object](lua://mw.uri) from URL.
return p._main(args)
--- Checks the URI is safe for use as a wikilink in MediaWiki.
---@param s string The URL to check.
---@return mw.uri|nil uri The URI of the given URL.
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 obtained.
---@return string|nil url The raw URL.
local function extractUrl(extract)
local url = extract
---@type string Extracted URL.
url = gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
'https://%2')
---@type mw.uri|nil
local uri = safeUri(url);
if uri and uri.host then
return url
end
return nil
end
end


--- Parses the `url`. The `url` parameter is required. `text` label is
function p._main(args)
--- optional and can be generated from the `url`.
-- If first arg or a url is not provided,
---@param url string|nil The URL
-- but we have a second arg, make a button.
---@param text? string|nil The display label of the wikilink
-- Otherwise, return nothing.
---@return string|nil url The URL
args.originalInput = args[1]
---@return string|nil text The display label of the wikilink
args[1] = delink({args[1]})
local function _url(url, text)
if args[1] == "" then
---@type string URL with trailing whitespace removed
args[1] = nil
url = mw.text.trim(url or '')
text = mw.text.trim(text or '')
 
if url == '' then
return '', text
end
end


-- If the URL contains any unencoded spaces, encode them,
if not args[1] and not args.url then
-- because MediaWiki will otherwise interpret a space as the end of the URL.
if args[2] then
url = gsub(url, '%s', function(s)
p.nolink = true
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 = gsub(url, '#$', '')
url = gsub(url, '%?$', '')
-- If it's an http(s) URL without the double slash, fix it.
url = gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
----url = gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
---- 'https://%2')
 
---@type mw.uri|nil
local uri = safeUri(url)
 
-- Handle URL's without a protocol or 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 = lower(uri.host or '') .. port .. (uri.relativePath or '')
-- Add `<wbr>` before `_/.-#` sequences
-- This entry _must_ be the first. `<wbr/>` has a `/` in it, you know.
text = gsub(text, "(/+)", "<wbr/>%1")
text = gsub(text, "(%.+)", "<wbr/>%1")
-- _Disabled_ for now.
---- text = gsub(text,"(%-+)","<wbr/>%1")
text = gsub(text, "(%#+)", "<wbr/>%1")
text = gsub(text, "(_+)", "<wbr/>%1")
else
else
-- URL is badly-formed, so just display whatever was given.
return ''
text = url
end
end
end
end


return url, text
local data = p.makeLinkData(args)
local link = p.renderLink(args.originalInput, data)
local trackingCategories = p.renderTrackingCategories(args)
return link .. trackingCategories
end
end


--- Cleans and normalises a URL string.
function p.makeLinkData(args)
---
local data = {}
---- 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|nil localUrl Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
---[!] @deprecated `mw.uri` class deprecated in MW 1.43 for native browser `URL`.
function p.url(url, text)
local localUrl = url
localUrl = localUrl or extractUrl(localUrl) or extractUrl(text) or ''
-- Strip out HTML tags and [ ] from URL
localUrl = (localUrl or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
-- Truncate anything after a space
localUrl = localUrl:gsub("%%20", " "):gsub(" .*", "")
return _url(localUrl, text)
end


--[[
-- Get the link and display values,
local function netUrl(url, text)
-- and find whether we are outputting
local localUrl = url or ''
-- a wikilink or a URL.
local localText = text or ''
if args.url then
local parsedUrl = nil
data.isUrl = true
local moduleNetUrl = require('Module:Neturl')
data.link = args.url
if not localUrl and not localUrl ~= '' then return nil, nil end
if args[1] then
if text ~= '' then
data.display = args[1]
parsedUrl = moduleNetUrl:parse(localUrl)
elseif args[2] then
localText = tostring(parsedUrl.host) .. tostring(parsedUrl.path)
data.display = args[2]
localUrl = tostring(parsedUrl:normalize())
else
data.display = args.url
p.urlisdisplay = true
end
else
else
localUrl = tostring(moduleNetUrl:parse(localUrl):normalize())
data.isUrl = false
end
p.urlisdisplay = false
return localUrl, localText
data.link = args[1]
end
if args[2] then
]]
data.display = args[2]
 
else
--- Helper function for tracking categories.
data.display = args[1]
---- Checks for unknown parameter use.
---- Validates given arguments.
---- Categorizes accordingly.
---@param data args Arguments table.
---@return args data Arguments table.
---@return string categories Category wikitext.
---@return mw.title pageTitle Current page title.
local function renderTrackingCategories(data, oldClassMatched)
local categories = ''
local category = data.category or ''
local class = type(data.class) == 'string' and lower(data.class) or ''
-- local checkForUnknowns = require("Module:Check for unknown parameters")._check
local pageTitle = mw.title.getCurrentTitle()
 
--- Don't add categories if `nocat=true`, but still add any custom category.
--- Custom category passed in.
if category and category ~= '' then
local s = category
s = s:gsub('%[', ''):gsub('%]', ''):gsub('[Cc]ategory:', '')
categories = string.format(' [[Category:%s]]', s)
end
if yesno(data.nocat) == true then
return data, categories, pageTitle
end
 
--[=[ local unknownText = string.format('[[Category:%s]]',
DEFINITIONS.trackingCategories.unknownParams, pageTitle.fullText)
local unknownParams = checkForUnknowns({
checkpositional = 'y', unknown = unknownText,
preview = DEFINITIONS.unknownArgsPreviewText, ignoreblank = 'y',
'class', 'color', 'weight', 'size', 'icon', 'link', 'action',
'url', 'disabled', 'label', 'aria-label', 'arialabel', 'aria_label',
'nocat', 'category', '1', '2'
}, data
)
 
if unknownParams ~= '' then
categories = string.format('%s %s', categories, unknownParams)
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'
}, data) ]=]
 
--- Add categories for outdated classes, dummy buttons, disabled buttons,
--- and external links.
do
---Dummy button is:
---- Clickable (i.e. not disabled visually)
---- No target link and no URL
---- Gives feedback it'll do something, but does nothing.
---All matches to if-statements below should all have `ariaDisabled == true`,
---and therefore `aria-disabled = true`.
if (not data.link
or yesno(data.link) == false) -- Checks for falsy or `link == 'no'`
and not data.url
and not data.disabled
then
categories = string.format('%s [[%s]]', categories,
DEFINITIONS.trackingCategories.dummyButton)
end
end
---Disabled button is:
if args[1] and args[1]:find('http') then
--- - Greyed out (`data.disabled == true`)
data.isUrl = true
if data.disabled then
categories = string.format('%s [[%s]]', categories,
DEFINITIONS.trackingCategories.disabledButton)
end
end
if class and oldClassMatched then
categories = string.format('%s [[%s]]', categories,
DEFINITIONS.trackingCategories.outdatedClasses)
end
if data.url then
categories = string.format('%s [[%s]]', categories,
DEFINITIONS.trackingCategories.externalLinks)
end
end
return data, categories, pageTitle
end
--- Renders the wikitext span tags for the button.
--- @param data args table Arguments table.
--- @param iconSpan mw.html Icon span element for the button.
--- @param isUrl boolean Whether target is URL
--- @param ariaDisabled boolean Whether button is disabled for ARIA API.
--- @param categories string Categories for the button.
--- @param errorText string Error message for the button, if needed.
--- @return string link Wikitext span tags for the button.
local function renderLink(data, iconSpan, isUrl, ariaDisabled, categories, errorText, tblClasses)
---@class mw.html: table MediaWiki DOM document content model based on HTML and RDFa.
---@type mw.html Span tag that creates the button.
local displaySpan = mw.html.create('span')
---@type string|nil Custom CSS style attributes for parent span node (not including
--- plainlinks span tag if URL used).
local styleAttributes = type(data.style) == string and data.style or nil
---@future Additional ARIA attributes for button. If implement 'fake' button for use in collapsible/accordion component, don't forget to declare:
--- displaySpan:attr('aria-haspopup', 'true') --- displaySpan:attr('aria-expanded', 'false')
for _, aClass in ipairs(tblClasses or {}) do
displaySpan:addClass(aClass)
end
--- ARIA role and label attributes for button.
displaySpan:attr('role', 'button')
if data.aria_label then
displaySpan:attr('aria-label', data.aria_label)
end
end
if styleAttributes then
displaySpan:attr('style', styleAttributes)
if yesno(args.link) == false then
p.nolink = true
end
end


if iconSpan ~= '' then
-- Colours
displaySpan:node(iconSpan)
-- For the merge with {{clickable button}}
end
local colour = args.color and args.color:lower()
if data.label then
displaySpan:wikitext(data.label)
end


---@type string Wikilink that wraps around button wikitext.
-- Classes
local link
local class = args.class and args.class:lower()
if data.disabled then
data.classes = {}
-- ARIA disabled attribute for disabled buttons
if class == 'ui-button-green'
displaySpan:attr('aria-disabled', 'true')
or class == 'ui-button-blue'
link = string.format('%s %s', tostring(displaySpan), categories)
or class == 'ui-button-red'
elseif ariaDisabled then
then
-- ARIA disabled attribute for no-link/dummy buttons
table.insert(
displaySpan:attr('aria-disabled', 'true')
data.classes,
link = string.format('%s %s', tostring(displaySpan), categories)
'submit ui-button ui-widget ui-state-default ui-corner-all'
.. ' ui-button-text-only ui-button-text'
)
else
else
displaySpan:attr('aria-disabled', 'false')
table.insert(data.classes, 'mw-ui-button')
if isUrl then
end
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
data.url, tostring(displaySpan), categories)
--If class is unset,
elseif isUrl == false then
--then let color determine class
link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
if not class then
categories)
if colour == 'blue' then
else-- `isUrl` should be `nil` to get here.
class = 'mw-ui-progressive'
-- Dummy/disabled button
elseif colour == 'red' then
link = string.format('%s %s', tostring(displaySpan), categories)
class = 'mw-ui-destructive'
elseif colour == 'green' then
class = 'mw-ui-constructive'
end
end
end
end
 
if errorText then
if class then
--- Generate error message when viewed in preview mode of an edit.
table.insert(data.classes, class)
--- Categorise into [[Category:Errors reported by Module:Clickable button]]
---@class ifPreview
---@field main function
---@type ifPreview Module checks if previewing an edit.
local ifPreview = require('Module:If preview')
if yesno(data.nocat) == false then -- Don't add category if `nocat=true`
link = string.format('%s [[%s]]', link, DEFINITIONS.trackingCategories.errors)
end -- Add error message to the link if viewing in preview mode.
return ifPreview.main({ errorText .. link, link })
end
end


return link
-- Styles
end
do
 
--[[
--- Parses arguments from old template parameters. For backward compatibility.
-- Check whether we are on the same page as we have specified in
---@param color? string `color` argument.
-- args[1], but not if we are using a URL link, as then args[1] is only
---@param class? string `class` argument.
-- a display value. If we are currently on the page specified in
---@param action? 'progressive'|'destructive'|'default'|string `action` argument.
-- args[1] make the button colour darker so that it stands out from
---@return string class String with class that did not match, likely custom class(es).
-- other buttons on the page.
---@return string action Returns action resolved.
--]]
---@return string|boolean matched Value of matched class if any of the arguments matched.
local success, linkTitle, currentTitle
local function checkColorAndClass(color, class, action)
if not data.isUrl then
    local actionValue = (type(action) == 'string' and action) or ''
currentTitle = mw.title.getCurrentTitle()
    color = (type(color) == 'string' and color) or ''
success, linkTitle = pcall(mw.title.new, args[1])
    class = (type(class) == 'string' and lower(class)) or ''
elseif p.urlisdisplay then
 
currentTitle = mw.title.getCurrentTitle()
    if color == '' and class == '' then
end
        return '', actionValue, false
if success
    end
and linkTitle
 
and mw.title.equals(currentTitle, linkTitle)
    -- Resolve action, check against set constants.
and not p.urlisdisplay
    for actionName, set in pairs(DEFINITIONS.legacyClassSets) do
then
        if set[color] and not DEFINITIONS.legacyClassSets[actionName][actionValue] then
if class == 'ui-button-blue'
            return class, actionName, actionValue  -- Found `color`.
or class == 'mw-ui-progressive'
        end
or class == 'mw-ui-constructive'
        if set[class] and not DEFINITIONS.legacyClassSets[actionName][actionValue] then
then
            return '', actionName, actionValue -- Found `class`.
data.backgroundColor = '#2962CB'
        end
data.color = '#fff'
        if set[actionValue] then
elseif class == 'ui-button-green' then
            return class, actionName, actionValue  -- Found `action`.
data.backgroundColor = '#008B6D'
        end
elseif class == 'ui-button-red' or class == 'mw-ui-destructive' then
    end
data.backgroundColor = '#A6170F'
 
else
    -- No match.
data.backgroundColor = '#CCC'
    return class, '', false
data.color = '#666'
end
end
 
elseif p.urlisdisplay then
--- Parses the module's arguments for backward compatibility.
data.dummyLink = tostring(currentTitle)
--- With deprecated parameters from old templates and modules.
end
---@param rawArgs args table Module arguments.
-- Add user-specified styles.
---@return args parsedArgs Parsed arguments.
data.style = args.style
---@return boolean ariaDisabled Whether button is disabled for ARIA API.
local function parseParameters(rawArgs)
local ariaDisabled = false
--- 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`
rawArgs.label = rawArgs.label or rawArgs[2] or rawArgs[1]
 
---@todo Should `link == 'no'` disable dummy buttons?
rawArgs.disabled = yesno(rawArgs.disabled) or (yesno(rawArgs.link) == false)
--- `link` value priority: `link` > `1`
rawArgs.link = rawArgs.link or rawArgs[1]
if rawArgs.disabled then
-- If `link` was `'no'`, i.e. `true`, then must
-- not generate a link either. Clear positional `1`
-- after assigning.
rawArgs.link = nil
rawArgs.url = nil
end
end
 
return data
-- Remove positional rawArgs after assigning
rawArgs[1] = nil
rawArgs[2] = nil
 
if (rawArgs.link and yesno(rawArgs.link) ~= false)
or rawArgs.url then
ariaDisabled = false
--[=[ -- Remove `[[` and `]]` if present in wikilink and label.
if rawArgs.link
and yesno(rawArgs.link) ~= false then
rawArgs.link = rawArgs.link:gsub('^%[%[', ''):gsub('%]%]$', '')
rawArgs.label = rawArgs.label:gsub('^%[%[', ''):gsub('%]%]', '')
end ]=]
else
--- `aria-disabled = true` if no link whatsoever, always.
--- Make dummy button. But for accessibility,
--- ARIA must know it won't do anything.
--- _OPTION_ to forcefully disable dummy buttons by setting:
--- rawArgs.disabled = true
ariaDisabled = true
end
-- Normalize ARIA label keys
rawArgs.aria_label = rawArgs.aria_label or rawArgs['aria-label'] or rawArgs.arialabel
    rawArgs['aria-label'] = nil
    rawArgs.arialabel = nil
 
return rawArgs, ariaDisabled
end
end


--- Constructs the attributes for the wikitext/HTML elements.
function p.renderLink(originalInput, data)
---@param parsedArgs args Parsed arguments.
-- Render the display span tag.
---@return args data Data, such as attributes, ready to be assembled.
local display
---@return mw.html iconSpan
do
---@return boolean isUrl
local displaySpan = mw.html.create('span')
---@return boolean ariaDisabled
for i, class in ipairs(data.classes or {}) do
---@return boolean hasIcon
displaySpan:addClass(class)
---@return string|boolean oldClassMatched
end
---@return string|nil errorText
---@return table tblClasses
local function makeLinkData(parsedArgs, ariaDisabled)
local data = {}
local iconSpan
local isUrl = false


-- Decide link vs. url vs. none
displaySpan
-- URL has priority over link if both provided.
:css{
-- Make pretty URL and label based on URL if no label.
['background-color'] = data.backgroundColor,
if parsedArgs.url then
color = data.color
isUrl = true
}
local label
if data.style then
data.url, label = p.url(parsedArgs.url, parsedArgs.label) -- netUrl(parsedArgs.url, parsedArgs.label)
displaySpan:cssText(data.style)
data.label = parsedArgs.label or label
end
elseif parsedArgs.link then
displaySpan:wikitext(data.display)
isUrl = false
display = tostring(displaySpan)
data.link = parsedArgs.link
data.label = parsedArgs.label
elseif not parsedArgs.url and not parsedArgs.link then
data.label = parsedArgs.label -- Dummy button as has no link or url
end
end


local errorText = nil
-- Render the link
local hasNoLabel = not data.label and not parsedArgs.aria_label
local link
local isVisuallyActive = not parsedArgs.disabled and not ariaDisabled
if originalInput and originalInput:find('|') then
if hasNoLabel and isVisuallyActive then --- Error if no aria-label and no visible label
link = string.format('[[%s|%s]]', delink({originalInput, wikilinks = 'target'}), display)
errorText = DEFINITIONS.noAriaLabelWarningText
elseif p.nolink then
end
if p.urlisdisplay then
 
link = string.format('[[%s|%s]]', data.dummyLink, display)
local tblClasses = {}
else
tblClasses = { 'cdx-button', 'cdx-button--fake-button' }
link = string.format('%s', display)
local class, action, oldClassMatched
= checkColorAndClass(parsedArgs.color, parsedArgs.class, parsedArgs.action)
local weight = type(parsedArgs.weight) == 'string' and parsedArgs.weight or 'normal'
local size  = type(parsedArgs.size) == 'string' and parsedArgs.size or 'medium'
table.insert(tblClasses, 'cdx-button--action-' .. action)
table.insert(tblClasses, 'cdx-button--weight-' .. weight)
table.insert(tblClasses, 'cdx-button--size-' .. size)
if (class and class ~= '') then
table.insert(tblClasses, class) -- Custom class.
data.class = class
end
 
---@todo Check if current page is the target link, if so, make button darker.
local isSamePage = false
if not isUrl then
if data.link == data.fullText then
isSamePage = true
end
end
end
data.disabled = parsedArgs.disabled
local labelLength = (type(data.label) == 'string' and mw.ustring.len(data.label)) or 0
if data.disabled then
table.insert(tblClasses, 'cdx-button--fake-button--disabled')
else
else
table.insert(tblClasses, 'cdx-button--fake-button--enabled')
if data.isUrl then
end
link = string.format('[%s %s]', data.link, display)
if data.label and labelLength > 38 then
else
table.insert(tblClasses, 'cdx-button--word-wrap')
link = string.format('[[%s |%s]]', data.link, display)
end
---@todo Must still actually use this in the CSS file.
if isSamePage then
        table.insert(tblClasses, 'cdx-button--same-page')
    end
 
local hasIcon = false
if type(parsedArgs.icon) == 'string' and parsedArgs.icon then
---@type string Name of icon for button.
local icon = parsedArgs.icon
hasIcon = true -- Assign to carry to final return for CSS output.
iconSpan = mw.html.create('span')
iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
iconSpan:attr('aria-hidden', 'true')
if not data.label then
-- Icon-only button, add extra class for styling.
table.insert(tblClasses, 'cdx-button--icon-only')
end
end
end
end


-- Label length checks.
return string.format('<span class="plainlinks clickbutton">%s</span>', link)
if data.label then
if labelLength > 38 then
errorText = errorText
and string.format('%s %s', errorText, DEFINITIONS.labelLengthWarningText)
or DEFINITIONS.labelLengthWarningText
elseif labelLength < 3 then
table.insert(tblClasses, 'cdx-button--short-label')
end
end
 
data.aria_label = parsedArgs.aria_label
 
return data, iconSpan, isUrl, ariaDisabled, hasIcon,
oldClassMatched, errorText, tblClasses
end
 
--- Interface for other Lua modules.
--- Function can be called by other Lua modules to generate wikitext.
--- Does not render CSS file or pre-process arguments.
---
---@param rawArgs args Module's arguments.
---@return string data Wikitext that renders button, without CSS file.
---@return boolean hasIcon Whether the button has an icon.
function p._main(rawArgs)
---@type args Parsed arguments.
local parsedArgs, ariaDisabled = parseParameters(rawArgs)
 
---@type args HTML attributes with values, and contents.
local data, iconSpan, isUrl, hasIcon, oldClassMatched, errorText, tblClasses
data, iconSpan, isUrl, ariaDisabled, hasIcon, oldClassMatched, errorText, tblClasses
= makeLinkData(parsedArgs, ariaDisabled)
 
local categories
data, categories = renderTrackingCategories(data, oldClassMatched)
 
return renderLink(data, iconSpan, isUrl, ariaDisabled,
categories, errorText, tblClasses), hasIcon
end
end


--- Interface for templates.
function p.renderTrackingCategories(args)
--- Called by the `{{#invoke: Clickable button | main }}` parser function.
if yesno(args.category) == false then
--- Pre-processes arguments, inserts CSS file, and renders the button.
return ''
---
---@param frame frame Module's arguments from template invocation.
---@return string wikitextOutput Wikitext for insertion on a wiki page.
function p.main(frame)
    ---@type table<string, string> Parsed arguments.
    -- If called from wrapper, don't look for parentFrame().
    local rawArgs = require('Module:Arguments').getArgs(frame, {
            wrappers = {
                'Template:Clickable button',
                'Template:Clickable button/sandbox',
                'Template:Cdx-button', 'Template:Cdx-button/sandbox'
            }
        })
 
    -- Make arguments lowercase where appropriate.
    -- Except `class` as _HTML class names_ are case-sensitive.
    for _, key in ipairs(DEFINITIONS.lowercaseArgs) do
        if rawArgs[key] then
            rawArgs[key] = lower(rawArgs[key])
        end
    end
 
    -- Return empty string if no arguments supplied.
    do
local hasInput = false
for _, v in pairs(rawArgs) do
if v and v ~= "" then
hasInput = true
break
end
end
if not hasInput then
return ''
end
end
end
 
local class = args.class and args.class:lower()
local output, hasIcon = p._main(rawArgs)
if class == 'ui-button-green'
-- Insert CSS file into the output.
or class == 'ui-button-blue'
    -- Note: This is not the most efficient way to include CSS,
or class == 'ui-button-red'
    -- but it's simple and avoids potential issues with caching.
then
local outputCSS = frame:extensionTag(
return '[[Category:Pages using old style ui-button-color]]'
'templatestyles', '',
{ src = DEFINITIONS.baseCSS }
)
if hasIcon then
output = string.format('%s%s%s', outputCSS,
frame:extensionTag(
'templatestyles', '',
{ src = DEFINITIONS.iconsCSS }
),
output
)
else
else
output = string.format('%s%s', outputCSS, output)
return ''
end
end
    return output
end
end


return p
return p

Latest revision as of 17:55, 12 August 2026

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

edit

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

edit

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

edit

Length of visible label

edit

See the Codex button component documentation.


--------------------------------------------------------------------------------
--- @module 'CodexClickableButton'
--- Generates wikitext for clickable Codex button.
--- 
--- Outputs wikitext to render the (button component)[mdoc:codex/latest/components/demos/button.html]
--- from the (Codex design system for Wikimedia)[https://doc.wikimedia.org/codex/latest].
---- 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.
---
--- Includes helper functions for URL parsing and cleaning, and adding tracking 
--- categories. Intended for use in templates and other modules.
--- Supports legacy parameters. To add icons, see CSS link in constants below.
---
--- @author [[User:Waddie96]]
--- @license CC-BY-SA-4.0/GFDL
--- @class CodexClickableButton extends ClickableButton
---  Table containing arguments for the button.
--- @class args table
--- @field label? string The button's visible text label.
--- @field link? string|'no' The target wikilink for the button.
--- @field url? string The target external URL for the button.
--- @field icon? string The name of the icon to display found in CSS file.
--- @field color? 'blue'|'green'|'red'|string Legacy color parameter.
--- @field class? string Custom CSS classes for the button.
--- @field weight? 'quiet'|'normal'|'primary' The visual weight of the button.
--- @field size? 'small'|'medium'|'large' The size of the button.
--- @field action? 'progressive'|'destructive'|'default'|string The action type of the button.
--- @field disabled? boolean|'1'|string Whether the button is disabled/greyed out. `disabled` is `true` if: `link` = `'no'` or `false` or `disabled` = `'1'` or `true`.
--- @field style? string Custom inline CSS styles.
--- @field nocat? boolean|string If `true`, suppresses tracking categories.
--- @field category? string An additional category to add.
--- @field aria-label? string The ARIA label for accessibility.
--- @field arialabel? string (alias for aria-label)
--- @field aria_label? string (alias for aria-label)
--- @field [1]? string Positional argument 1 (alias for link/label).
--- @field [2]? string Positional argument 2 (alias for label).
--- @var categories? string|boolean Additional categories to add.
--- @var ariaDisabled? boolean Internal flag indicating if the button is functionally disabled for ARIA.
--- @var oldClassMatched string|boolean Internal flag for outdated classes.
--- @var isUrl boolean Whether the link is a URL.
--- @var errorText string|nil 
--- @var tblClasses table Classes for the button span tag.
--- @var pageTitle mw.title Title of the current page.
--- @todo [[Module:Neturl]] [[Module:Check for unknown parameters]]

local yesno = require('Module:Yesno')
local delink = require('Module:Delink')._delink

local p = {}

function p.main(frame)
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame)
	return p._main(args)
end

function p._main(args)
	-- If first arg or a url is not provided,
	-- but we have a second arg, make a button.
	-- Otherwise, return nothing.
	args.originalInput = args[1]
	args[1] = delink({args[1]})
	if args[1] == "" then
		args[1] = nil
	end

	if not args[1] and not args.url then
		if args[2] then
			p.nolink = true
		else
			return ''
		end
	end

	local data = p.makeLinkData(args)
	local link = p.renderLink(args.originalInput, data)
	local trackingCategories = p.renderTrackingCategories(args)
	return link .. trackingCategories
end

function p.makeLinkData(args)
	local data = {}

	-- Get the link and display values,
	-- and find whether we are outputting
	-- a wikilink or a URL.
	if args.url then
		data.isUrl = true
		data.link = args.url
		if args[1] then
			data.display = args[1]
		elseif args[2] then
			data.display = args[2]
		else
			data.display = args.url
			p.urlisdisplay = true
		end
	else
		data.isUrl = false
		p.urlisdisplay = false
		data.link = args[1]
		if args[2] then
			data.display = args[2]
		else
			data.display = args[1]
		end
		if args[1] and args[1]:find('http') then
			data.isUrl = true
		end
	end
	
	if yesno(args.link) == false then
		p.nolink = true
	end

	-- Colours
	-- For the merge with {{clickable button}}
	local colour = args.color and args.color:lower()

	-- Classes
	local class = args.class and args.class:lower()
	data.classes = {}
	if class == 'ui-button-green'
		or class == 'ui-button-blue'
		or class == 'ui-button-red'
	then
		table.insert(
			data.classes,
			'submit ui-button ui-widget ui-state-default ui-corner-all'
				.. ' ui-button-text-only ui-button-text'
		)
	else
		table.insert(data.classes, 'mw-ui-button')
	end
	
	--If class is unset,
	--then let color determine class
	if not class then
		if colour == 'blue' then
			class = 'mw-ui-progressive'
		elseif colour == 'red' then
			class = 'mw-ui-destructive'
		elseif colour == 'green' then
			class = 'mw-ui-constructive'
		end
	end
	
	if class then
		table.insert(data.classes, class)
	end

	-- Styles
	do
		--[[
		-- Check whether we are on the same page as we have specified in
		-- args[1], but not if we are using a URL link, as then args[1] is only
		-- a display value. If we are currently on the page specified in
		-- args[1] make the button colour darker so that it stands out from
		-- other buttons on the page.
		--]]
		local success, linkTitle, currentTitle
		if not data.isUrl then
			currentTitle = mw.title.getCurrentTitle()
			success, linkTitle = pcall(mw.title.new, args[1])
		elseif p.urlisdisplay then
			currentTitle = mw.title.getCurrentTitle()
		end
		if success
			and linkTitle
			and mw.title.equals(currentTitle, linkTitle)
			and not p.urlisdisplay
		then
			if class == 'ui-button-blue'
				or class == 'mw-ui-progressive'
				or class == 'mw-ui-constructive'
			then
				data.backgroundColor = '#2962CB'
				data.color = '#fff'
			elseif class == 'ui-button-green' then
				data.backgroundColor = '#008B6D'
			elseif class == 'ui-button-red' or class == 'mw-ui-destructive' then
				data.backgroundColor = '#A6170F'
			else
				data.backgroundColor = '#CCC'
				data.color = '#666'
			end
		elseif p.urlisdisplay then
			data.dummyLink = tostring(currentTitle)
		end
		-- Add user-specified styles.
		data.style = args.style
	end
	return data
end

function p.renderLink(originalInput, data)
	-- Render the display span tag.
	local display
	do
		local displaySpan = mw.html.create('span')
		for i, class in ipairs(data.classes or {}) do
			displaySpan:addClass(class)
		end

		displaySpan
			:css{
				['background-color'] = data.backgroundColor,
				color = data.color
			}
		if data.style then
			displaySpan:cssText(data.style)
		end
		displaySpan:wikitext(data.display)
		display = tostring(displaySpan)
	end

	-- Render the link
	local link
	if originalInput and originalInput:find('|') then
		link = string.format('[[%s|%s]]', delink({originalInput, wikilinks = 'target'}), display)
	elseif p.nolink then
		if p.urlisdisplay then
			link = string.format('[[%s|%s]]', data.dummyLink, display)
		else
			link = string.format('%s', display)
		end
	else
		if data.isUrl then
			link = string.format('[%s %s]', data.link, display)
		else
			link = string.format('[[%s |%s]]', data.link, display)
		end
	end

	return string.format('<span class="plainlinks clickbutton">%s</span>', link)
end

function p.renderTrackingCategories(args)
	if yesno(args.category) == false then
		return ''
	end
	local class = args.class and args.class:lower()
	if class == 'ui-button-green'
		or class == 'ui-button-blue'
		or class == 'ui-button-red'
	then
		return '[[Category:Pages using old style ui-button-color]]'
	else
		return ''
	end
end

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