Module:Clickable button/sandbox2
Appearance
-- [[w:en:Module:Clickable button]]
-- Builds Codex's button component. See [[wmdoc:codex/latest]].
-- Implements [[Template:Clickable button]] and others.
-- To add icons: [[Template:Clickable button/styles.css]].
-- TRACKING CATEGORIES: [[Category:Pages using clickable dummy button]]
-- [[Category:Pages using clickable button with external links]]
-- [[Category:Pages using clickable button with outdated classes]]
-- unless nocat= is true value. Adds category= any custom category regardless of nocat=.
-- DEPENDENCIES:
local yesno = require('Module:Yesno')
-- [[Special:Version]] must include @wikimedia/codex.
-- [[Template:Clickable button/styles.css]]
-- [[Module:Yesno]] [[Module:Arguments]]
-- [[Module:Check for unknown parameters]]
local p = {}
-- Cleans up and validates a URL.
-- Copied from [[en:Module:URL]] with minor modifications.
local function safeUri(s)
local success, uri = pcall(function()
return mw.uri.new(s)
end)
if success then
return uri
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
local function _url(url, text)
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')
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 passed in
text = url
end
end
return url, text
end
local function url(URL, TEXT)
local s = URL or ''
local text = TEXT or ''
s = s or extractUrl(s) or extractUrl(text) or ''
-- Strip out HTML tags and [ ] from URL
s = (s or ''):gsub("<[^>]*>", ""):gsub("[%[%]]", "")
-- Truncate anything after a space
s = s:gsub("%%20", " "):gsub(" .*", "")
return _url(s, text)
end
-- Renders tracking categories based on parameters.
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
mw.logObject(args, categories) -- DEBUG
--[=[
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) ]=]
-- @TODO: whether all dummy buttons should get the dummy-category,
-- or only those without disabled/ariaDisabled?
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 == no link, no url and 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
mw.logObject(categories)
return categories
end
local function renderLink(data)
-- Build span tag
local span = mw.html.create('span')
for _, class in ipairs(data.classes or {}) do
span:addClass(class)
end
span:attr('role', 'button')
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)
--[[ span:node(mw.html.create('span')
:addClass('cdx-button--text')
:wikitext(data.label)) ]]
end
local display = tostring(span)
if data.disabled then
return string.format('%s %s', display, data.categories)
end
if data.isUrl then
---- mw.logObject(data)
return string.format('<span class="plainlinks">[%s %s]</span> %s',
data.url, display, data.categories)
elseif data.isUrl == false then
return string.format('[[%s|%s]] %s', data.link, display, data.categories)
else
-- No url/link provided
return string.format('%s %s', display, data.categories)
end
end
-- Backward compatibility for deprecated parameters
local function parseParameters(args)
-- It's weird that we may make a link a label above, but if we truly
-- only got 1=, then that would mean it's intentional to make both the link
-- and label the same.
-- Ensure label is set from priority: label= > 2= > 1=
args.label = args.label or args[2] or args[1]
-- Disable if link=no or disabled=1
args.disabled = (yesno(args.link) == false)
or yesno(args.disabled)
args.link = args.link or args[1]
-- Remove positional args after assigning
args[1] = nil
args[2] = nil
-- Make aria-disabled=true later on if no link
if (args.link and yesno(args.link) ~= false) or args.url then
args.ariaDisabled = false
else
-- If no link whatsoever, make dummy button.
-- But for accessibility, ARIA must know it won't do anything.
args.ariaDisabled = true
-- OPTION to forcefully disable dummy buttons.
---- args.disabled = 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 attributes for the HTML elements.
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
error('A button without a visible label needs an [[WAI-ARIA|ARIA]] '
.. 'label, please define it using "aria-label"', 2)
end
-- Classes
data.classes = {'cdx-button', 'cdx-button--fake-button'}
local class = type(args.class) == 'string' and args.class:lower(args.class)
local action = type(args.action) == 'string' and args.action:lower()
or '' -- or 'default' by default
local weight = type(args.weight) == 'string' and args.weight:lower()
or '' -- or 'normal' by default
local size = type(args.size) == 'string' and args.size:lower()
or '' -- 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)
table.insert(data.classes, class) -- Custom class
-- 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')
:addClass('cdx-icon cdx-button__icon cdx-demo-css-icon--' .. icon)
: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 string.len(data.label) > 40 then
error('Buttons labels should ideally be fewer than 38 characters' ..
', see [[Template:Clickable button#Button label length|' ..
'documentation]]', 2) -- @TODO: Preview warning only.
end
-- Short label min-width custom CSS adjustment per Codex documentation.
if string.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
---- data.nocat = args.nocat
---- data.category = args.category
return data
end
local function _main(args)
---- local backupArgs = args
local parsedArgs = parseParameters(args)
local data = makeLinkData(parsedArgs)
data.categories = renderTrackingCategories(parsedArgs)
return renderLink(data)
end
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 were 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' }
) .. _main(args)
end
return p