Module:Tabs: Difference between revisions

From Zelda Dungeon Wiki
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( {
     id = getRequiredArg( args, 'id', 'TabContainer.new' ),
     contents = {},
    content = args[1],
    tabsTop = args.top,
    tabsLeft = args.left,
     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' )
    :attr( 'id', self.id )
     :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.vtabwidth ) or 60
     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( self.content )
 
   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
local TabSet = {}
TabSet.__index = TabSet


function TabSet.new( args )
function TabSet.new( args )
  args = args or {}
   return setmetatable( {
   return setmetatable( {
     target = getRequiredArg( args, 'target', 'TabSet.new' ),
     target = args.target,
     selector = args.selector and (tonumber(args.selector) or error('invalid arg: selector must be a number')) or 0,
     selector = args.selector,
     activation = args.activation or 'click',
     activation = args.activation or 'click',
     tabs = getRequiredArg( args, 1, 'TabSet.new' )
     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-target', self.target )
    :attr( 'data-tab-selector', self.selector )
     :attr( 'data-tab-type', self.activation )
     :attr( 'data-tab-type', self.activation )
    :wikitext( self.tabs )
  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
local Tab = {}
Tab.__index = Tab


function Tab.new( args )
function Tab.new( args )
  args = args or {}
   return setmetatable( {
   return setmetatable( {
     selection = getRequiredArg( args, 1, 'Tab.new' ),
     selection = getRequiredArg( args, 'selection', 'Tab.new' ):gsub( "%s", "" ),
     label = args[2] or args[1],
     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:gsub( "%s", "" ) )
     :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
local TabContent = {}
TabContent.__index = TabContent


function TabContent.new( args )
function TabContent.new( args )
  args = args or {}
   return setmetatable( {
   return setmetatable( {
     contentId = getRequiredArg( args, 1, 'TabContent.new' ),
     selection = getRequiredArg( args, 'selection', 'TabContent.new' ),
     content = args[2] or args[1],
     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' )
     :attr( 'data-tab-content', self.contentId )
     :wikitext( '\n' .. self.content ) -- newline is needed for tables, lists, etc.
    :wikitext( self.content )


   if self.contentId == 'default' then
   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.TabContainer = TabContainer
p.Tabs = TabContainer
p.TabSet = TabSet
p.Tab = Tab
p.TabContent = TabContent


function p.tabs( frame )
function p.tabs( frame )
   local tabs = TabContainer.new( frame.args )
  local args = Args.fromFrame( frame )
   return tabs:render()
   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


function p.tabset( frame )
        -- add edit link, aligned to transcluded table's header
  local tabset = TabSet.new( frame.args )
        local editlink = mw.html.create( 'span' )
  return tabset:render()
          :addClass( 'edit plainlinks' )
end
          :css( 'position', 'absolute' ) -- css to align with table header
          :css( 'top', '12px' )
          :css( 'right', '15px' )
          :wikitext( '[' .. tostring( mw.uri.fullUrl( template, { action = 'edit' } ) ) .. ' [edit]]' )


function p.tab( frame )
        -- add the transcluded content
   local tab = Tab.new( frame.args )
        tabs:addContent{
  return tab:render()
          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


function p.content( frame )
   return tabs:render()
  local content = TabContent.new( frame.args )
   return content:render()
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