Module:Tabs: Difference between revisions
Jump to navigation
Jump to search
Want an adless experience? Log in or Create an account.
(implement the other classes) |
(I knew the logic was bad but not that bad. breaking it into two ifs for clarity) |
||
(41 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
local Args = require( 'Module:Args' ) | |||
function getRequiredArg( args, argName, fnName ) | |||
return assert( args[argName], 'missing required arg for ' .. fnName .. ': ' .. argName ) | |||
end | |||
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' ) | ||
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:tag( 'div' ) | container:addClass( 'zdw-tabcontainer--hastabstop' ) | ||
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 | |||
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 | end | ||
container:tag( 'div' ) | container:tag( 'div' ) | ||
:css( 'clear', 'both' ) | :css( 'clear', 'both' ) | ||
Line 39: | 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 55: | 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( { | |||
selection = getRequiredArg( args, 'selection', 'Tab.new' ):gsub( "%s", "" ), | |||
label = args.label or args.selection, | |||
args = args | args = args | ||
}, Tab ) | }, Tab ) | ||
end | end | ||
Line 80: | 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 | |||
tab:addClass( 'active' ) | |||
tab:attr( 'data-tab-default', 'true' ) | |||
end | |||
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 or args. | content = args.content or args.selection, | ||
args = args | args = args | ||
}, TabContent ) | }, TabContent ) | ||
Line 100: | Line 178: | ||
local content = mw.html.create( 'div' ) | local content = mw.html.create( 'div' ) | ||
:addClass( 'zdw-tabcontent' ) | :addClass( 'zdw-tabcontent' ) | ||
:attr( 'data-tab-content', | :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 | if self.args.width then | ||
Line 112: | 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