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
merge
Line 1: Line 1:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--- @module 'Codex clickable button' [[en:Module:Clickable button]]
--- @module 'Codex clickable button'
--- @author [[User:Waddie96]]
--- -@see [[en:Module:Clickable_button]]
--- @license CC-BY-SA-4.0/GFDL
--- @class args table
--- @field label string
--- Generates wikitext for clickable Codex button.
--- Generates wikitext for clickable Codex button.
--- Generates wikitext to render the button component from the (Codex design
--- Generates wikitext to render the button component from the (Codex design
Line 16: Line 21:
-- TRACKING CATEGORIES:
-- TRACKING CATEGORIES:
-- [[Category:Pages using clickable dummy button]]
-- [[Category:Pages using clickable dummy button]]
-- [[Category:Pages using disabled dummy button]]
-- [[Category:Pages using disabled button]]
-- [[Category:Pages using clickable button with external links]]
-- [[Category:Pages using clickable button with external links]]
-- [[Category:Pages using clickable button with outdated classes]]
-- [[Category:Pages using clickable button with outdated classes]]
Line 23: Line 28:


-- DEPENDENCIES:
-- DEPENDENCIES:
require('strict')
----require('strict')
local yesno = require('Module:Yesno')
local yesno = require('Module:Yesno')
-- [[Template:Clickable button/styles.css]]
-- [[Template:Clickable button/styles.css]]
Line 32: Line 37:


local p = {}
local p = {}
local gsub = mw.ustring.gsub
local len = mw.ustring.len
local lower = mw.ustring.lower
local trim = mw.text.trim
local html = mw.html


--- Checks the URI is safe to use as a wikilink in MediaWiki.
--- Creates [URI object](lua://mw.uri) from URL.
---@param s string The URL to check
--- Checks the URI is safe for use as a wikilink in MediaWiki.
---@return mw.uri|nil uri The URI of the given URL
---@class mw.uri: string
---@class mw.uri: string
---@param s string The URL to check.
---@return mw.uri|nil uri The URI of the given URL.
local function safeUri(s)
local function safeUri(s)
local success, uri = pcall(function()
local success, uri = pcall(function()
Line 49: Line 60:


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


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


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


-- If there is an empty query string or fragment ID,
-- If there is an empty query string or fragment ID,
-- remove it as it will cause mw.uri.new to throw an error
-- remove it as it will cause mw.uri.new to throw an error
url = mw.ustring.gsub(url, '#$', '')
url = gsub(url, '#$', '')
url = mw.ustring.gsub(url, '%?$', '')
url = gsub(url, '%?$', '')
 
-- If it's an http(s) URL without the double slash, fix it.
-- 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 = gsub(url, '^[Hh][Tt][Tt][Pp]([Ss]?):(/?)([^/])', 'http%1://%3')
url = mw.ustring.gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
----url = gsub(url, '^([Hh]?[Tt]?[Tt]?[Pp]?[Ss]?:/*)(.+)',
'https://%2')
---- 'https://%2')


---@type mw.uri|nil
local uri = safeUri(url)
local uri = safeUri(url)


-- Handle URL's without a protocol and URL's that are protocol-relative,
-- 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
-- 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
if uri and (not uri.protocol or (uri.protocol and not uri.host)) and url:sub(1, 2) ~= '//' then
url = 'http://' .. url
url = 'http://' .. url
Line 105: Line 117:
if text == '' then
if text == '' then
if uri then
if uri then
if uri.path == '/' then uri.path = '' end
if uri.path == '/' then
 
uri.path = ''
end
local port = ''
local port = ''
if uri.port then port = ':' .. uri.port end
if uri.port then
 
port = ':' .. uri.port
text = mw.ustring.lower(uri.host or '') .. port .. (uri.relativePath or '')
end
 
text = lower(uri.host or '') .. port .. (uri.relativePath or '')
-- Add <wbr> before _/.-sequences
-- Add `<wbr>` before `_/.-#` sequences
text = mw.ustring.gsub(text, "(/+)", "<wbr/>%1") -- This entry MUST be the first. "<wbr/>" has a "/" in it, you know.
-- This entry _must_ be the first. `<wbr/>` has a `/` in it, you know.
text = mw.ustring.gsub(text, "(%.+)", "<wbr/>%1")
text = gsub(text, "(/+)", "<wbr/>%1")  
-- text = mw.ustring.gsub(text,"(%-+)","<wbr/>%1") -- DISABLED for now
text = gsub(text, "(%.+)", "<wbr/>%1")
text = mw.ustring.gsub(text, "(%#+)", "<wbr/>%1")
-- _Disabled_ for now.
text = mw.ustring.gsub(text, "(_+)", "<wbr/>%1")
---- text = gsub(text,"(%-+)","<wbr/>%1")
else -- URL is badly-formed, so just display whatever was given
text = gsub(text, "(%#+)", "<wbr/>%1")
text = gsub(text, "(_+)", "<wbr/>%1")
else
-- URL is badly-formed, so just display whatever was given.
text = url
text = url
end
end
Line 135: Line 151:
---@param url string The raw URL to clean.
---@param url string The raw URL to clean.
---@param text string Optional link display text.
---@param text string Optional link display text.
---@return string url Cleaned URL for wikilink.
---@return string|nil localUrl Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
---@return string|nil text Display label for wikilink.
----@deprecated `mw.uri` was deprecated in MW 1.43 in favour of the native browser `URL`.
local function url(url, text)
local function url(url, text)
url = url or extractUrl(url) or extractUrl(text) or ''
local localUrl = url
localUrl = localUrl or extractUrl(localUrl) or extractUrl(text) or ''
-- Strip out HTML tags and [ ] from URL
-- Strip out HTML tags and [ ] from URL
url = (url or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
localUrl = (localUrl or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
-- Truncate anything after a space
-- Truncate anything after a space
url = url:gsub("%%20", " "):gsub(" .*", "")
localUrl = localUrl:gsub("%%20", " "):gsub(" .*", "")
return _url(url, text)
return _url(localUrl, text)
end
end


--- Renders tracking categories based on given parameters.
--- Renders tracking categories based on given parameters. Also checks for unknown parameter use,
---@param args table
--- validates given arguments, and categorizes accordingly.
---@param args table Original arguments given to module.
local function renderTrackingCategories(args)
local function renderTrackingCategories(args)
local categories = ''
local categories = ''
local class = args.class and args.class:lower() or ''
local class = args.class or ''
---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
---- local title = mw.title.getCurrentTitle()
---- local title = mw.title.getCurrentTitle()
 
--- Don't add categories if `nocat=true`, but still add any custom category.
-- Don't add categories if nocat=yes, but still add any custom category.
--- Custom category passed in
-- Custom category passed in
if args.category --[[and yesno(args.nocat) == false]] then
if args.category and yesno(args.nocat) == false then
local s = args.category
local q = args.category
s = s:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
q = q:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
categories = categories .. '[[' .. 'Category:' .. s .. ']]'
categories = categories .. '[[' .. 'Category:' .. q .. ']]'
end
end
if yesno(args.nocat) == true then
if yesno(args.nocat) == true then
return ''
return categories or ''
end
end


Line 182: Line 200:
}, args) ]=]
}, args) ]=]


if ((not args.link and not args.url
--- Dummy button is:
and not args.disabled and not args.ariaDisabled)
--- - Clickable (i.e. not disabled visually)
or ((args.link or args.url) and not args.label)) then
--- - No target link/URL (i.e., gives feedback it'll do something,
-- Dummy button has no link, no URL and is not disabled
---  but does nothing).
-- OR link/URL but no visible label
--- They should all have `ariaDisabled == true`, therefore `aria-disabled = true`
categories = categories .. '[[Category:Pages using clickable dummy button]]'
if (not args.link or yesno(args.link) == false)
elseif (not args.link and not args.url
and not args.url
and (args.disabled or args.ariaDisabled)) then
and not args.disabled then
-- Disabled button
categories = categories .. '[[Category:Pages using clickable dummy button]]'
categories = categories .. '[[Category:Pages using disabled dummy button]]'
end
--- Disabled button is:
--- - Greyed out (`args.disabled == true`)
--- - Will likely have no link/URL
if args.disabled then
categories = categories .. '[[Category:Pages using disabled button]]'
end
end
if class == 'ui-button-green'
 
if class == 'ui-button-green'
or class == 'ui-button-blue'
or class == 'ui-button-blue'
or class == 'ui-button-red'
or class == 'ui-button-red'
or class == 'mw-ui-progressive'
or class == 'mw-ui-progressive'
or class == 'mw-ui-destructive' then
or class == 'mw-ui-destructive' then
categories = categories .. '[[Category:Pages using clickable button with outdated classes]]'
categories = categories .. '[[Category:Pages using clickable ' ..
'button with outdated classes]]'
end
end
if args.url then
if args.url then
categories = categories .. '[[Category:Pages using clickable button with external links]]'
categories = categories .. '[[Category:Pages using clickable ' ..
'button with external links]]'
end
end
if class == 'mw-ui-constructive' then
if class == 'mw-ui-constructive' then
categories = categories .. '[[Category:Pages using clickable button with deprecated parameters]]'
categories = categories .. '[[Category:Pages using clickable ' ..
'button with deprecated parameters]]'
end
end
return categories
return categories
end
end
Line 214: Line 240:
--- @return string link
--- @return string link
local function renderLink(data)
local function renderLink(data)
-- Build button span
---@class mw.html: string MediaWiki DOM document content model based on HTML and RDFa
local displaySpan = mw.html.create('span')
---@type mw.html Span tag that creates the button
for _, class in ipairs(data.classes or {}) do
local displaySpan = html.create('span')
displaySpan:addClass(class)
for _, aClass in ipairs(data.classes or {}) do
displaySpan:addClass(aClass)
end
end
displaySpan:attr('role', 'button')
displaySpan:attr('role', 'button')
Line 223: Line 250:
displaySpan:attr('aria-label', data.aria_label)
displaySpan:attr('aria-label', data.aria_label)
end
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
if data.iconSpan then
displaySpan:node(data.iconSpan)
displaySpan:node(data.iconSpan)
Line 234: Line 256:
if data.label then
if data.label then
displaySpan:wikitext(data.label)
displaySpan:wikitext(data.label)
--[[ span:node(mw.html.create('span')
:addClass('cdx-button--text')
:wikitext(data.label)) ]]
end
end


local display = tostring(displaySpan)
--- Wikilink that wraps around button wikitext
 
---@type string
-- Build link
local link
local link
if data.disabled then
if data.disabled then
return string.format('%s %s', display, data.categories)
-- ARIA disabled attribute for disabled buttons
end
displaySpan:attr('aria-disabled', 'true')
if data.isUrl then
link = string.format('%s %s', tostring(displaySpan), data.categories)
---- mw.logObject(data)
elseif data.ariaDisabled then
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
-- ARIA disabled attribute for no-link/dummy buttons
  data.url, display, data.categories)
displaySpan:attr('aria-disabled', 'true')
elseif data.isUrl == false then
link = string.format('%s %s', tostring(displaySpan), data.categories)
link = string.format('[[%s|%s]] %s', data.link, display,
  data.categories)
else
else
-- Dummy/disabled button
displaySpan:attr('aria-disabled', 'false')
link = string.format('%s %s', display, data.categories)
if data.isUrl then
link = string.format('<span class="plainlinks">[%s %s]</span> %s',
data.url, tostring(displaySpan), data.categories)
elseif data.isUrl == false then
link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
data.categories)
else-- `data.isUrl` should be `nil` to get here.
-- Dummy/disabled button
link = string.format('%s %s', tostring(displaySpan), data.categories)
end
end
end


if data.error then
if data.error then
--- Generate error message when viewed in preview.
--- Categorise into [[Category:Errors reported by Module:Clickable button]]
---@class ifPreview
---@field main function
---@type ifPreview
local ifPreview = require('Module:If preview')
local ifPreview = require('Module:If preview')
return ifPreview.main( {data.error .. link .. ' ' ..
if yesno(data.nocat) ~= true then
[[Category:Errors reported by Module:Clickable button]], link} )
link = link .. ' ' .. '[[Category:Errors reported by Module:Clickable button]]'
end
return ifPreview.main( { data.error .. link, link } )
end
end


Line 269: Line 301:
--- Parses the module's arguments for backward compatibility with deprecated
--- Parses the module's arguments for backward compatibility with deprecated
--- parameters from old templates and modules.
--- parameters from old templates and modules.
---@param args table Module's arguments.
---@param args table Module arguments.
---@return table args Parsed arguments.
---@return table args Parsed arguments.
local function parseParameters(args)
local function parseParameters(args)
Line 282: Line 314:
--- - `link` = `'no'` or `false`
--- - `link` = `'no'` or `false`
--- - `disabled` = `'1'` or `true`
--- - `disabled` = `'1'` or `true`
--- @type boolean
---@TODO Should `link == 'no'` disable dummy
args.disabled = (yesno(args.link) == false)
---      buttons?
or yesno(args.disabled)
---@type boolean
args.disabled = yesno(args.disabled) or (yesno(args.link) == false)
--- `link` value priority: `link` > `1`
--- `link` value priority: `link` > `1`
---@type string
args.link = args.link or args[1]
args.link = args.link or args[1]
if args.disabled then
-- If `link` was `'no'`, i.e. `true`, then must
-- not generate a link either. Clearing after assigning
-- positional arg[1].
args.link = nil
args.url = nil
end


-- Remove positional args after assigning
-- Remove positional args after assigning
Line 295: Line 336:
args.ariaDisabled = false
args.ariaDisabled = false
else
else
--- `aria-disabled = true` if no link whatsoever.
--- `aria-disabled = true` if no link whatsoever, always.
--- Make dummy button. But for accessibility,
--- Make dummy button. But for accessibility,
--- ARIA must know it won't do anything.
--- ARIA must know it won't do anything.
--- _OPTION_ to forcefully disable dummy buttons
--- _OPTION_ to forcefully disable dummy buttons
--- by setting `args.disabled = true`
--- by setting: `args.disabled = true`
args.ariaDisabled = true
args.ariaDisabled = true
end
end
Line 307: Line 348:


-- Determine action from old parameters color/class
-- Determine action from old parameters color/class
local color = type(args.color) == 'string' and args.color:lower()
local color = type(args.color) == 'string' and args.color:lower() or ''
local class = type(args.class) == 'string' and args.class:lower()
local class = type(args.class) == 'string' and lower(args.class) or ''
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'
Line 330: Line 371:


--- Constructs the attributes for the wikitext/HTML elements.
--- Constructs the attributes for the wikitext/HTML elements.
--- @param args table
---@param args table Parsed arguments.
--- @return table data
---@return table data Data, such as attributes, ready to be assembled.
local function makeLinkData(args)
local function makeLinkData(args)
local data = {}
local data = {}


-- Decide link vs. url vs. none
-- Decide link vs. url vs. none
-- URL has priority over link if both provided. Also, clean URL
-- URL has priority over link if both provided.
-- Make pretty URL and label based on URL if no label.
if args.url then
if args.url then
data.isUrl = true
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
local label
data.url, label = url(args.url, args.label)
data.url, label = url(args.url, args.label)
Line 354: Line 388:
data.link = args.link
data.link = args.link
data.label = args.label
data.label = args.label
-- Dummy button as has no link or url
elseif not args.url and not args.link then
elseif not args.url and not args.link then
-- Dummy button, no link or url
data.label = args.label
data.label = args.label
end
end


-- @TODO: Error tracking category
--- If error has occured contains error string, or `nil`.
-- Error if no aria-label and no visible label
---@type string|nil
data.error = nil
--- Error if no aria-label and no visible label
if (not data.label and not args.aria_label
if (not data.label and not args.aria_label
and not args.disabled and not args.ariaDisabled) then
and not args.disabled and not args.ariaDisabled) then
Line 369: Line 405:
end
end


-- Classes
--- Classes for button span tag
local class  = type(args.class) == 'string' and args.class:lower()
---@type table
data.classes = { 'cdx-button', 'cdx-button--fake-button' }
local class  = type(args.class) == 'string' and args.class
or ''
or ''
local action = type(args.action) == 'string' and args.action:lower()
local action = type(args.action) == 'string' and args.action:lower()
Line 378: Line 416:
local size  = type(args.size) == 'string' and args.size:lower()
local size  = type(args.size) == 'string' and args.size:lower()
or 'medium' -- or 'medium' by default
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)
Line 387: Line 423:
end
end


-- Disabled state
--- Disabled, greyed-out state of button
---@type boolean
data.disabled = args.disabled
data.disabled = args.disabled
if data.disabled then
if data.disabled then
Line 395: Line 432:
end
end


-- Icon
if type(args.icon) == 'string' then
local icon = type(args.icon) == 'string' and args.icon:lower()
--- Icon for button
if icon then
---@type string
data.iconSpan = mw.html.create('span')
local icon = args.icon:lower()
data.iconSpan = html.create('span')
data.iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
data.iconSpan:addClass('cdx-button__icon cdx-demo-css-icon--' .. icon)
data.iconSpan:attr('aria-hidden', 'true')
data.iconSpan:attr('aria-hidden', 'true')
Line 409: Line 447:
-- Label length checks
-- Label length checks
if data.label then
if data.label then
if mw.ustring.len(data.label) > 40 then
if len(data.label) > 38 then
data.error = ('<span class="error"><strong>Preview warning:</strong> A button label'
local errorMsg ='<span class="error"><strong>Preview warning:'
.. ' should ideally be shorter than 38 characters, see [[Template:Clickable button'
.. '</strong> A button label '
.. '#Button label length|documentation]].</span>')
.. 'should ideally be shorter than 38 characters, see '
end
.. '[[en:Template:Clickable button/doc#Button label length|documentation]].'
-- Short label min-width custom CSS adjustment per Codex documentation.
.. '</span>'
if mw.ustring.len(data.label) < 3 then
data.error = data.error and (data.error .. ' ' .. errorMsg) or errorMsg
elseif len(data.label) < 3 then
table.insert(data.classes, 'cdx-button--short-label')
table.insert(data.classes, 'cdx-button--short-label')
end
end
Line 426: Line 465:
end
end


--- Function that can be called by other Lua modules.
--- **Interface for other Lua modules.**
--- @param args table
--- Function can be called by other Lua modules to generate wikitext
--- @return table|string data
--- without pre-processing arguments with [[Module:Arguments]], adding
--- TemplateStyles, and returning blank string if no arguments were given.
---@param args table Module's arguments.
---@return string data Wikitext that renders button, without CSS file.
function p._main(args)
function p._main(args)
---@type table Parsed arguments.
local parsedArgs = parseParameters(args)
local parsedArgs = parseParameters(args)
---@type table Raw data such as element attributes, values, and contents.
local data = makeLinkData(parsedArgs)
local data = makeLinkData(parsedArgs)
parsedArgs.label = data.label
parsedArgs.ariaDisabled = data.ariaDisabled or false
data.categories = renderTrackingCategories(parsedArgs)
data.categories = renderTrackingCategories(parsedArgs)


Line 437: Line 483:
end
end


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


Line 459: Line 510:
return ''
return ''
end
end
--[[ TESTING
local returnValue = p._main(args)
local templateStyle = frame:extensionTag(
'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
)
do
local returnS1 = html.create('div')
local lexer = require('Module:Lua lexer')
returnS1:addClass('cdx-message')
returnS1:addClass('cdx-message--block')
local returnS2 = html.create('span')
returnS2:addClass('cdx-message__icon')
local returnS3 = html.create('div')
returnS3:addClass('cdx-message__content')
returnS3:attr('font-size', 'small')
returnValue = tostring(returnValue)
returnValue = tostring(lexer(returnValue))
local returnS4 = frame:extensionTag('syntaxhighlight',
returnValue , { lang = 'lua' } )
returnValue = templateStyle ..
tostring(returnS1:node(returnS2):done():node(returnS3):node(returnS4):allDone())
end
return returnValue
-- ]]


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


return p
return p

Revision as of 04:51, 23 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'
--- @author [[User:Waddie96]]
--- -@see [[en:Module:Clickable_button]]
--- @license CC-BY-SA-4.0/GFDL
--- @class args table
--- @field label string
--- 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 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 gsub = mw.ustring.gsub
local len = mw.ustring.len
local lower = mw.ustring.lower
local trim = mw.text.trim
local html = mw.html

--- Creates [URI object](lua://mw.uri) from URL.
--- Checks the URI is safe for use as a wikilink in MediaWiki.
---@class mw.uri: string
---@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

--- Parses the `url`. The `url` parameter is required. `text` label is
--- optional and can be generated from the `url`.
---@param url string|nil The URL
---@param text? string|nil The display label of the wikilink
---@return string|nil url The URL
---@return string|nil text The display label of the wikilink
local function _url(url, text)
	---@type string URL with trailing whitespace removed
	url = trim(url or '')
	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 = 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 = 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
			-- 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|nil localUrl Cleaned URL for wikilink.
---@return string|nil text Display label for wikilink.
----@deprecated `mw.uri` was deprecated in MW 1.43 in favour of the native browser `URL`.
local function 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

--- Renders tracking categories based on given parameters. Also checks for unknown parameter use,
--- validates given arguments, and categorizes accordingly.
---@param args table Original arguments given to module.
local function renderTrackingCategories(args)
	local categories = ''
	local class = args.class or ''
	---- local check_for_unknown_parameters = require("Module:Check for unknown parameters")._check
	---- local title = mw.title.getCurrentTitle()
	--- Don't add categories if `nocat=true`, but still add any custom category.
	--- Custom category passed in
	if args.category --[[and yesno(args.nocat) == false]] then
		local s = args.category
		s = s:gsub('%[%[', ''):gsub('%]%]', ''):gsub('[Cc]ategory:', '')
		categories = categories .. '[[' .. 'Category:' .. s .. ']]'
	end
	if yesno(args.nocat) == true then
		return categories or ''
	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) ]=]

	--- Dummy button is:
	--- - Clickable (i.e. not disabled visually)
	--- - No target link/URL (i.e., gives feedback it'll do something,
	---   but does nothing).
	--- They should all have `ariaDisabled == true`, therefore `aria-disabled = true`
	if 	(not args.link or yesno(args.link) == false)
		and not args.url
		and not args.disabled then
			categories = categories .. '[[Category:Pages using clickable dummy button]]'
	end
	--- Disabled button is:
	--- - Greyed out (`args.disabled == true`)
	--- - Will likely have no link/URL
	if args.disabled then
		categories = categories .. '[[Category:Pages using disabled 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)
	---@class mw.html: string MediaWiki DOM document content model based on HTML and RDFa
	---@type mw.html Span tag that creates the button
	local displaySpan = html.create('span')
	for _, aClass in ipairs(data.classes or {}) do
			displaySpan:addClass(aClass)
	end
	displaySpan:attr('role', 'button')
	if data.aria_label then
		displaySpan:attr('aria-label', data.aria_label)
	end

	if data.iconSpan then
		displaySpan:node(data.iconSpan)
	end
	if data.label then
		displaySpan:wikitext(data.label)
	end

	--- Wikilink that wraps around button wikitext
	---@type string
	local link
	if data.disabled then
		-- ARIA disabled attribute for disabled buttons
		displaySpan:attr('aria-disabled', 'true')
		link = string.format('%s %s', tostring(displaySpan), data.categories)
	elseif data.ariaDisabled then
		-- ARIA disabled attribute for no-link/dummy buttons
		displaySpan:attr('aria-disabled', 'true')
		link = string.format('%s %s', tostring(displaySpan), data.categories)
	else
		displaySpan:attr('aria-disabled', 'false')
		if data.isUrl then
			link = string.format('<span class="plainlinks">[%s %s]</span> %s',
				data.url, tostring(displaySpan), data.categories)
		elseif data.isUrl == false then
			link = string.format('[[%s|%s]] %s', data.link, tostring(displaySpan),
				data.categories)
		else-- `data.isUrl` should be `nil` to get here.
			-- Dummy/disabled button
			link = string.format('%s %s', tostring(displaySpan), data.categories)
		end
	end

	if data.error then
		--- Generate error message when viewed in preview.
		--- Categorise into [[Category:Errors reported by Module:Clickable button]]
		---@class ifPreview
		---@field main function
		---@type ifPreview
		local ifPreview = require('Module:If preview')
		if yesno(data.nocat) ~= true then
			link = link .. ' ' .. '[[Category:Errors reported by Module:Clickable button]]'
		end
		return ifPreview.main( { data.error .. link, 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 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`
	---@TODO Should `link == 'no'` disable dummy
	---      buttons?
	---@type boolean
	args.disabled = yesno(args.disabled) or (yesno(args.link) == false)
	--- `link` value priority: `link` > `1`
	---@type string
	args.link = args.link or args[1]
	if args.disabled then
		-- If `link` was `'no'`, i.e. `true`, then must
		-- not generate a link either. Clearing after assigning
		-- positional arg[1].
		args.link = nil
		args.url = nil
	end

	-- 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, always.
		--- 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() or ''
	local class = type(args.class) == 'string' and lower(args.class) or ''
	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 Parsed arguments.
---@return table data Data, such as attributes, ready to be assembled.
local function makeLinkData(args)
	local data = {}

	-- Decide link vs. url vs. none
	-- URL has priority over link if both provided.
	-- Make pretty URL and label based on URL if no label.
	if args.url then
		data.isUrl = true
		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
	-- Dummy button as has no link or url
	elseif not args.url and not args.link then
		data.label = args.label
	end

	--- If error has occured contains error string, or `nil`.
	---@type string|nil
	data.error = nil
	--- 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 for button span tag
	---@type table
	data.classes = { 'cdx-button', 'cdx-button--fake-button' }
	local class  = type(args.class) == 'string' and args.class
		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
	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, greyed-out state of button
	---@type boolean
	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

	if type(args.icon) == 'string' then
		--- Icon for button
		---@type string
		local icon = args.icon:lower()
		data.iconSpan = 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 len(data.label) > 38 then
			local errorMsg ='<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>'
			data.error = data.error and (data.error .. ' ' .. errorMsg) or errorMsg
		elseif 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

--- **Interface for other Lua modules.**
--- Function can be called by other Lua modules to generate wikitext
--- without pre-processing arguments with [[Module:Arguments]], adding
--- TemplateStyles, and returning blank string if no arguments were given.
---@param args table Module's arguments.
---@return string data Wikitext that renders button, without CSS file.
function p._main(args)
	---@type table Parsed arguments.
	local parsedArgs = parseParameters(args)
	---@type table Raw data such as element attributes, values, and contents.
	local data = makeLinkData(parsedArgs)
	parsedArgs.label = data.label
	parsedArgs.ariaDisabled = data.ariaDisabled or false
	data.categories = renderTrackingCategories(parsedArgs)

	return renderLink(data)
end

--- **Interface for templates.**
--- Main function called by templates to use this module.
--- Using the `{{#invoke:Clickable button|main|arguments}}` parser function.
---@deprecated
---@class frame: string
---@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

--[[ TESTING
	
	local returnValue = p._main(args)
	local templateStyle = frame:extensionTag(
		'templatestyles', '', { src = 'Template:Clickable button/styles.css' }
		)
	do
		local returnS1 = html.create('div')
		local lexer = require('Module:Lua lexer')
		returnS1:addClass('cdx-message')
		returnS1:addClass('cdx-message--block')
		local returnS2 = html.create('span')
		returnS2:addClass('cdx-message__icon')
		local returnS3 = html.create('div')
		returnS3:addClass('cdx-message__content')
		returnS3:attr('font-size', 'small')
		returnValue = tostring(returnValue)
		returnValue = tostring(lexer(returnValue))
		local returnS4 = frame:extensionTag('syntaxhighlight',
			returnValue , { lang = 'lua' } )
		returnValue = templateStyle .. 
			tostring(returnS1:node(returnS2):done():node(returnS3):node(returnS4):allDone())
	end

	return returnValue
	
	-- ]]

	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.