Module:Tabs: Difference between revisions
Jump to navigation
Jump to search
Want an adless experience? Log in or Create an account.
(for tabcontent, read default from the id arg since it doesn't make sense to have an id in that case) |
(I knew the logic was bad but not that bad. breaking it into two ifs for clarity) |
||
(34 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local Args = require( 'Module:Args' ) | |||
function getRequiredArg( args, argName, fnName ) | function getRequiredArg( args, argName, fnName ) | ||
return assert( args[argName], 'missing required arg for ' .. fnName .. ': ' .. argName ) | return assert( args[argName], 'missing required arg for ' .. fnName .. ': ' .. argName ) | ||
Line 5: | Line 7: | ||
local TabContainer = {} | local TabContainer = {} | ||
TabContainer.__index = TabContainer | TabContainer.__index = TabContainer | ||
local TabSet = {} | |||
TabSet.__index = TabSet | |||
local Tab = {} | |||
Tab.__index = Tab | |||
local TabContent = {} | |||
TabContent.__index = TabContent | |||
function TabContainer.new( args ) | function TabContainer.new( args ) | ||
args = args or {} | |||
return setmetatable( { | return setmetatable( { | ||
contents = {}, | |||
args = args | args = args | ||
}, TabContainer ) | }, TabContainer ) | ||
end | |||
function TabContainer:leftTabs( args ) | |||
if not self.tabsLeft then | |||
args.target = args.target or self.args.id | |||
args.activation = args.activation or self.args.activation | |||
self.tabsLeft = TabSet.new( args ) | |||
end | |||
return self.tabsLeft | |||
end | |||
function TabContainer:topTabs( args ) | |||
if not self.tabsTop then | |||
args.target = args.target or self.args.id | |||
args.activation = args.activation or self.args.activation | |||
self.tabsTop = TabSet.new( args ) | |||
end | |||
return self.tabsTop | |||
end | |||
function TabContainer:addTabLeftWithContent( args ) | |||
-- normalize args so they can just be passed to everything. definitely a bad idea but... | |||
args.selection = getRequiredArg( args, 'contentId', 'TabContainer:addTabLeftWithContent' ) | |||
self:leftTabs( args ):addTab( args ) | |||
self:addContent( args ) | |||
end | |||
function TabContainer:addTabTopWithContent( args ) | |||
-- normalize args so they can just be passed to everything. definitely a bad idea but... | |||
args.selection = getRequiredArg( args, 'contentId', 'TabContainer:addTabTopWithContent' ) | |||
self:topTabs( args ):addTab( args ) | |||
self:addContent( args ) | |||
end | |||
function TabContainer:addContent( args ) | |||
self.contents[#self.contents + 1] = TabContent.new( args ) | |||
end | end | ||
function TabContainer:render() | function TabContainer:render() | ||
local container = mw.html.create( 'div' ) | local container = mw.html.create( 'div' ) | ||
:addClass( 'zdw-tabcontainer zdw-box' ) | :addClass( 'zdw-tabcontainer zdw-box' ) | ||
if self.args.id then container:attr( 'id', self.args.id ) end | |||
if self.args.width then container:css( 'width', self.args.width .. 'px' ) end | if self.args.width then container:css( 'width', self.args.width .. 'px' ) end | ||
if self.args.height then container:css( 'height', self.args.height .. 'px' ) end | if self.args.height then container:css( 'height', self.args.height .. 'px' ) end | ||
if self.tabsLeft then | if self.tabsLeft then | ||
local width = tonumber( self.args. | local width = tonumber( self.args.left and self.args.left.width ) or 60 | ||
container:addClass( 'zdw-tabcontainer--hastabsleft' ) | container:addClass( 'zdw-tabcontainer--hastabsleft' ) | ||
container:css( 'margin-left', tostring( width ) .. 'px' ) | container:css( 'margin-left', tostring( width ) .. 'px' ) | ||
container:tag( 'div' ) | local left = container:tag( 'div' ) | ||
:addClass( 'zdw-tabcontainer__tabset--left' ) | :addClass( 'zdw-tabcontainer__tabset--left' ) | ||
:css( 'width', tostring( width ) .. 'px' ) | :css( 'width', tostring( width ) .. 'px' ) | ||
:css( 'margin-left', '-' .. tostring( width + 10 ) .. 'px' ) | :css( 'margin-left', '-' .. tostring( width + 10 ) .. 'px' ) | ||
:wikitext( self.tabsLeft ) | :wikitext( self.tabsLeft:render() ) | ||
if self.args.height then left:css( 'height', self.args.height .. 'px' ) end | |||
end | end | ||
if self.tabsTop then | if self.tabsTop then | ||
container:addClass( 'zdw-tabcontainer--hastabstop' ) | container:addClass( 'zdw-tabcontainer--hastabstop' ) | ||
container:tag( 'div' ) | local top = container:tag( 'div' ) | ||
:addClass( 'zdw-tabcontainer__tabset--top' ) | :addClass( 'zdw-tabcontainer__tabset--top' ) | ||
:wikitext( self.tabsTop ) | :wikitext( self.tabsTop:render() ) | ||
if self.args.width then top:css( 'width', self.args.width .. 'px' ) end | |||
end | end | ||
container:wikitext( | |||
for _, c in ipairs( self.contents ) do | |||
if type( c.selection ) == 'string' | |||
and (self.tabsLeft and self.tabsLeft.defaultSelection == c.selection:gsub( "%s", "" ) | |||
or self.tabsTop and self.tabsTop.defaultSelection == c.selection:gsub( "%s", "" )) then | |||
c.default = true | |||
elseif type( c.selection ) == 'table' | |||
and self.tabsLeft and self.tabsLeft.defaultSelection == c.selection.left:gsub( "%s", "" ) | |||
and self.tabsTop and self.tabsTop.defaultSelection == c.selection.top:gsub( "%s", "" ) then | |||
c.default = true | |||
end | |||
container:wikitext( c:render() ) | |||
end | |||
container:tag( 'div' ) | container:tag( 'div' ) | ||
:css( 'clear', 'both' ) | :css( 'clear', 'both' ) | ||
Line 44: | Line 108: | ||
return tostring( container ) | return tostring( container ) | ||
end | end | ||
function TabSet.new( args ) | function TabSet.new( args ) | ||
args = args or {} | |||
return setmetatable( { | return setmetatable( { | ||
target = | target = args.target, | ||
selector = args.selector | selector = args.selector, | ||
activation = args.activation or 'click', | activation = args.activation or 'click', | ||
defaultTab = args.default and (tonumber(args.default) or error('invalid arg: default must be a number')) or 1, | |||
tabs = {} | |||
}, TabSet ) | }, TabSet ) | ||
end | |||
function TabSet:addTab( args ) | |||
local index = #self.tabs + 1 | |||
if index == self.defaultTab then | |||
self.defaultSelection = args.selection:gsub( "%s", "" ) | |||
args.default = true | |||
end | |||
self.tabs[index] = Tab.new( args ) | |||
end | end | ||
Line 60: | Line 132: | ||
local tabSet = mw.html.create( 'ul' ) | local tabSet = mw.html.create( 'ul' ) | ||
:addClass( 'zdw-tabset' ) | :addClass( 'zdw-tabset' ) | ||
:attr( 'data-tab-type', self.activation ) | :attr( 'data-tab-type', self.activation ) | ||
if self.target then tabSet:attr( 'data-tab-target', self.target ) end | |||
if self.selector then tabSet:attr( 'data-tab-selector', self.selector ) end | |||
for _, tab in ipairs( self.tabs ) do | |||
tabSet:wikitext( tab:render() ) | |||
end | |||
return tostring( tabSet ) | return tostring( tabSet ) | ||
end | end | ||
function Tab.new( args ) | function Tab.new( args ) | ||
args = args or {} | |||
return setmetatable( { | return setmetatable( { | ||
selection = getRequiredArg( args, | selection = getRequiredArg( args, 'selection', 'Tab.new' ):gsub( "%s", "" ), | ||
label = args | label = args.label or args.selection, | ||
args = args | args = args | ||
}, Tab ) | }, Tab ) | ||
Line 82: | Line 155: | ||
local tab = mw.html.create( 'li' ) | local tab = mw.html.create( 'li' ) | ||
:addClass( 'zdw-tab' ) | :addClass( 'zdw-tab' ) | ||
:attr( 'data-tab-selection', self.selection | :attr( 'data-tab-selection', self.selection ) | ||
:wikitext( self.label ) | :wikitext( self.label ) | ||
if self.args.default then | if self.args.default then | ||
tab:addClass( 'active' ) | |||
tab:attr( 'data-tab-default', 'true' ) | tab:attr( 'data-tab-default', 'true' ) | ||
end | end | ||
Line 91: | Line 165: | ||
return tostring( tab ) | return tostring( tab ) | ||
end | end | ||
function TabContent.new( args ) | function TabContent.new( args ) | ||
args = args or {} | |||
return setmetatable( { | return setmetatable( { | ||
selection = getRequiredArg( args, 'selection', 'TabContent.new' ), | |||
content = args | content = args.content or args.selection, | ||
args = args | args = args | ||
}, TabContent ) | }, TabContent ) | ||
Line 106: | Line 178: | ||
local content = mw.html.create( 'div' ) | local content = mw.html.create( 'div' ) | ||
:addClass( 'zdw-tabcontent' ) | :addClass( 'zdw-tabcontent' ) | ||
: | :wikitext( '\n' .. self.content ) -- newline is needed for tables, lists, etc. | ||
if self. | if type( self.selection ) == 'table' then | ||
for k, v in pairs( self.selection ) do | |||
content:attr( 'data-tab-content-' .. k, v:gsub( "%s", "" ) ) | |||
end | |||
else | |||
content:attr( 'data-tab-content', self.selection:gsub( "%s", "" ) ) | |||
end | |||
if self.default then | |||
content:addClass( 'default' ) | content:addClass( 'default' ) | ||
end | end | ||
Line 122: | Line 201: | ||
local p = {} | local p = {} | ||
p. | p.Tabs = TabContainer | ||
function p.tabs( frame ) | function p.tabs( frame ) | ||
local tabs = TabContainer.new( | local args = Args.fromFrame( frame ) | ||
local tabs = TabContainer.new( args ) | |||
end | |||
-- set selectors | |||
if args.left then args.left.selector = args.top and not args.combine and 'left' or nil end | |||
if args.top then args.top.selector = args.left and not args.combine and 'top' or nil end | |||
-- fix default tabs if combined | |||
-- l\t nil 0 # | |||
-- nil nil\0 nil\0 0\# | |||
-- 0 0\nil 0\0 0\# | |||
-- # #\0 #\0 #\0 | |||
if args.left and args.top and args.combine then | |||
if args.left.default and args.left.default ~= '0' then args.top.default = '0' end | |||
if not args.left.default then | |||
if args.top.default and args.top.default ~= '0' then | |||
args.left.default = 0 | |||
else | |||
args.top.default = 0 | |||
end | |||
end | |||
end | |||
if args.left then | |||
local left = tabs:leftTabs( args.left ) | |||
for _, tabArgs in ipairs( args.left ) do | |||
tabArgs = Args.getTable( tabArgs ) | |||
tabArgs.selection = Args.getValue( tabArgs ) | |||
left:addTab( tabArgs ) | |||
end | |||
end | |||
if args.top then | |||
local top = tabs:topTabs( args.top ) | |||
for _, tabArgs in ipairs( args.top ) do | |||
tabArgs = Args.getTable( tabArgs ) | |||
tabArgs.selection = Args.getValue( tabArgs ) | |||
top:addTab( tabArgs ) | |||
end | |||
end | |||
-- Special case where content is transcluded from subpages rather than provided directly | |||
-- For now, assumes 2D mode (both left and top tabs) | |||
if Args.getValue( args.content or {} ) == 'transclude' then | |||
for _, leftArgs in ipairs( args.left ) do | |||
local leftSelection = Args.getValue( leftArgs ) | |||
for _, topArgs in ipairs( args.top ) do | |||
local topSelection = Args.getValue( topArgs ) | |||
local template = mw.title.getCurrentTitle().text .. '/' .. leftSelection .. '/' .. topSelection | |||
-- add edit link, aligned to transcluded table's header | |||
local editlink = mw.html.create( 'span' ) | |||
:addClass( 'edit plainlinks' ) | |||
:css( 'position', 'absolute' ) -- css to align with table header | |||
:css( 'top', '12px' ) | |||
:css( 'right', '15px' ) | |||
:wikitext( '[' .. tostring( mw.uri.fullUrl( template, { action = 'edit' } ) ) .. ' [edit]]' ) | |||
-- add the transcluded content | |||
local | tabs:addContent{ | ||
selection = { left = leftSelection, top = topSelection }, | |||
end | content = mw.getCurrentFrame():expandTemplate{ title = ':' .. template } .. tostring( editlink ) | ||
} | |||
end | |||
end | |||
else -- content is provided by the caller | |||
for contentSelection, contentArgs in pairs( args.content or {} ) do -- order doesn't matter here since only one is displayed at a time | |||
local leftSelection, topSelection = string.match( contentSelection, '(.+) (.+)' ) | |||
contentArgs = Args.getTable( contentArgs ) | |||
contentArgs.selection = leftSelection and { left = leftSelection, top = topSelection } or contentSelection | |||
contentArgs.content = Args.getValue( contentArgs ) | |||
tabs:addContent( contentArgs ) | |||
end | |||
end | |||
return tabs:render() | |||
return | |||
end | end | ||
return p | return p |
Latest revision as of 02:55, July 26, 2020
Documentation for this module may be created at Module:Tabs/doc
local Args = require( 'Module:Args' ) function getRequiredArg( args, argName, fnName ) return assert( args[argName], 'missing required arg for ' .. fnName .. ': ' .. argName ) end local TabContainer = {} TabContainer.__index = TabContainer local TabSet = {} TabSet.__index = TabSet local Tab = {} Tab.__index = Tab local TabContent = {} TabContent.__index = TabContent function TabContainer.new( args ) args = args or {} return setmetatable( { contents = {}, args = args }, TabContainer ) end function TabContainer:leftTabs( args ) if not self.tabsLeft then args.target = args.target or self.args.id args.activation = args.activation or self.args.activation self.tabsLeft = TabSet.new( args ) end return self.tabsLeft end function TabContainer:topTabs( args ) if not self.tabsTop then args.target = args.target or self.args.id args.activation = args.activation or self.args.activation self.tabsTop = TabSet.new( args ) end return self.tabsTop end function TabContainer:addTabLeftWithContent( args ) -- normalize args so they can just be passed to everything. definitely a bad idea but... args.selection = getRequiredArg( args, 'contentId', 'TabContainer:addTabLeftWithContent' ) self:leftTabs( args ):addTab( args ) self:addContent( args ) end function TabContainer:addTabTopWithContent( args ) -- normalize args so they can just be passed to everything. definitely a bad idea but... args.selection = getRequiredArg( args, 'contentId', 'TabContainer:addTabTopWithContent' ) self:topTabs( args ):addTab( args ) self:addContent( args ) end function TabContainer:addContent( args ) self.contents[#self.contents + 1] = TabContent.new( args ) end function TabContainer:render() local container = mw.html.create( 'div' ) :addClass( 'zdw-tabcontainer zdw-box' ) if self.args.id then container:attr( 'id', self.args.id ) end if self.args.width then container:css( 'width', self.args.width .. 'px' ) end if self.args.height then container:css( 'height', self.args.height .. 'px' ) end if self.tabsLeft then local width = tonumber( self.args.left and self.args.left.width ) or 60 container:addClass( 'zdw-tabcontainer--hastabsleft' ) container:css( 'margin-left', tostring( width ) .. 'px' ) local left = container:tag( 'div' ) :addClass( 'zdw-tabcontainer__tabset--left' ) :css( 'width', tostring( width ) .. 'px' ) :css( 'margin-left', '-' .. tostring( width + 10 ) .. 'px' ) :wikitext( self.tabsLeft:render() ) if self.args.height then left:css( 'height', self.args.height .. 'px' ) end end if self.tabsTop then container:addClass( 'zdw-tabcontainer--hastabstop' ) local top = container:tag( 'div' ) :addClass( 'zdw-tabcontainer__tabset--top' ) :wikitext( self.tabsTop:render() ) if self.args.width then top:css( 'width', self.args.width .. 'px' ) end end for _, c in ipairs( self.contents ) do if type( c.selection ) == 'string' and (self.tabsLeft and self.tabsLeft.defaultSelection == c.selection:gsub( "%s", "" ) or self.tabsTop and self.tabsTop.defaultSelection == c.selection:gsub( "%s", "" )) then c.default = true elseif type( c.selection ) == 'table' and self.tabsLeft and self.tabsLeft.defaultSelection == c.selection.left:gsub( "%s", "" ) and self.tabsTop and self.tabsTop.defaultSelection == c.selection.top:gsub( "%s", "" ) then c.default = true end container:wikitext( c:render() ) end container:tag( 'div' ) :css( 'clear', 'both' ) return tostring( container ) end function TabSet.new( args ) args = args or {} return setmetatable( { target = args.target, selector = args.selector, activation = args.activation or 'click', defaultTab = args.default and (tonumber(args.default) or error('invalid arg: default must be a number')) or 1, tabs = {} }, TabSet ) end function TabSet:addTab( args ) local index = #self.tabs + 1 if index == self.defaultTab then self.defaultSelection = args.selection:gsub( "%s", "" ) args.default = true end self.tabs[index] = Tab.new( args ) end function TabSet:render() local tabSet = mw.html.create( 'ul' ) :addClass( 'zdw-tabset' ) :attr( 'data-tab-type', self.activation ) if self.target then tabSet:attr( 'data-tab-target', self.target ) end if self.selector then tabSet:attr( 'data-tab-selector', self.selector ) end for _, tab in ipairs( self.tabs ) do tabSet:wikitext( tab:render() ) end return tostring( tabSet ) end function Tab.new( args ) args = args or {} return setmetatable( { selection = getRequiredArg( args, 'selection', 'Tab.new' ):gsub( "%s", "" ), label = args.label or args.selection, args = args }, Tab ) end function Tab:render() local tab = mw.html.create( 'li' ) :addClass( 'zdw-tab' ) :attr( 'data-tab-selection', self.selection ) :wikitext( self.label ) if self.args.default then tab:addClass( 'active' ) tab:attr( 'data-tab-default', 'true' ) end return tostring( tab ) end function TabContent.new( args ) args = args or {} return setmetatable( { selection = getRequiredArg( args, 'selection', 'TabContent.new' ), content = args.content or args.selection, args = args }, TabContent ) end function TabContent:render() local content = mw.html.create( 'div' ) :addClass( 'zdw-tabcontent' ) :wikitext( '\n' .. self.content ) -- newline is needed for tables, lists, etc. if type( self.selection ) == 'table' then for k, v in pairs( self.selection ) do content:attr( 'data-tab-content-' .. k, v:gsub( "%s", "" ) ) end else content:attr( 'data-tab-content', self.selection:gsub( "%s", "" ) ) end if self.default then content:addClass( 'default' ) end if self.args.width then content:css( 'width', self.args.width .. 'px' ) end return tostring( content ) end local p = {} p.Tabs = TabContainer function p.tabs( frame ) local args = Args.fromFrame( frame ) local tabs = TabContainer.new( args ) -- set selectors if args.left then args.left.selector = args.top and not args.combine and 'left' or nil end if args.top then args.top.selector = args.left and not args.combine and 'top' or nil end -- fix default tabs if combined -- l\t nil 0 # -- nil nil\0 nil\0 0\# -- 0 0\nil 0\0 0\# -- # #\0 #\0 #\0 if args.left and args.top and args.combine then if args.left.default and args.left.default ~= '0' then args.top.default = '0' end if not args.left.default then if args.top.default and args.top.default ~= '0' then args.left.default = 0 else args.top.default = 0 end end end if args.left then local left = tabs:leftTabs( args.left ) for _, tabArgs in ipairs( args.left ) do tabArgs = Args.getTable( tabArgs ) tabArgs.selection = Args.getValue( tabArgs ) left:addTab( tabArgs ) end end if args.top then local top = tabs:topTabs( args.top ) for _, tabArgs in ipairs( args.top ) do tabArgs = Args.getTable( tabArgs ) tabArgs.selection = Args.getValue( tabArgs ) top:addTab( tabArgs ) end end -- Special case where content is transcluded from subpages rather than provided directly -- For now, assumes 2D mode (both left and top tabs) if Args.getValue( args.content or {} ) == 'transclude' then for _, leftArgs in ipairs( args.left ) do local leftSelection = Args.getValue( leftArgs ) for _, topArgs in ipairs( args.top ) do local topSelection = Args.getValue( topArgs ) local template = mw.title.getCurrentTitle().text .. '/' .. leftSelection .. '/' .. topSelection -- add edit link, aligned to transcluded table's header local editlink = mw.html.create( 'span' ) :addClass( 'edit plainlinks' ) :css( 'position', 'absolute' ) -- css to align with table header :css( 'top', '12px' ) :css( 'right', '15px' ) :wikitext( '[' .. tostring( mw.uri.fullUrl( template, { action = 'edit' } ) ) .. ' [edit]]' ) -- add the transcluded content tabs:addContent{ selection = { left = leftSelection, top = topSelection }, content = mw.getCurrentFrame():expandTemplate{ title = ':' .. template } .. tostring( editlink ) } end end else -- content is provided by the caller for contentSelection, contentArgs in pairs( args.content or {} ) do -- order doesn't matter here since only one is displayed at a time local leftSelection, topSelection = string.match( contentSelection, '(.+) (.+)' ) contentArgs = Args.getTable( contentArgs ) contentArgs.selection = leftSelection and { left = leftSelection, top = topSelection } or contentSelection contentArgs.content = Args.getValue( contentArgs ) tabs:addContent( contentArgs ) end end return tabs:render() end return p