Jump to content

Module:ElementPortraitCarousel: Difference between revisions

// via Wikitext Extension for VSCode
 
// via Wikitext Extension for VSCode
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
--[[  
--[[
* Module:ElementPortraitCarousel
* Name: ElementPortraitCarousel
* Renders a portrait carousel for person templates with 3 display modes:
* Author: Mark W. Datysgeld
* 1. Single image - Simple centered display
* Description: Element module that renders portrait carousels for Person templates with three display modes based on image count
* 2. Dual images - Orbital display with animation
* Notes: Single image (centered display); dual images (orbital display with animation); multi-image (full carousel with navigation arrows); integrates with Blueprint template system; image sanitization; protected execution with error handling
* 3. Multi-image - Full carousel with navigation arrows
]]
*
* Integrates with Blueprint template system.
]]


local p = {}
local p = {}
Line 15: Line 12:
-- Load required modules
-- Load required modules
local ErrorHandling = require('Module:ErrorHandling')
local ErrorHandling = require('Module:ErrorHandling')
local TemplateHelpers = require('Module:TemplateHelpers')


-- Default configuration
-- Default configuration
Line 30: Line 28:
     local images = {}
     local images = {}
     for image in portrait:gmatch("[^;]+") do
     for image in portrait:gmatch("[^;]+") do
         table.insert(images, mw.text.trim(image))
         local imageName = mw.text.trim(image)
        -- Sanitize logo path - extract filename and remove prefixes
        imageName = TemplateHelpers.sanitizeUserInput(imageName, "IMAGE_FILES")
        -- Store just the image name, we'll use MediaWiki's renderer
        if imageName and imageName ~= "" then
            table.insert(images, imageName)
        end
     end
     end
     return images
     return images
Line 36: Line 40:


-- Create single image HTML
-- Create single image HTML
local function createSingleImage(image, maxWidth)
local function createSingleImage(imageName, maxWidth)
    local frame = mw.getCurrentFrame()
    -- Use wiki syntax for images via preprocess
    local img = frame:preprocess(string.format('[[File:%s|%s]]', imageName, maxWidth))
   
     return string.format(
     return string.format(
         '<div class="person-portrait-carousel">' ..
         '<div class="person-portrait-carousel">%s</div>',
        '<img src="%s" style="max-width:%s;" alt="Portrait" />' ..
         img
        '</div>',
         image, maxWidth
     )
     )
end
end


-- Create dual-image orbital display HTML
-- Create dual-image orbital display HTML with navigation arrows
local function createDualImages(images, maxWidth)
local function createDualImages(images, maxWidth)
     local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
     local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
    local frame = mw.getCurrentFrame()
      
      
    -- Use wiki syntax for images through preprocess
    local img1 = frame:preprocess(string.format('[[File:%s|%s]]', images[1], maxWidth))
    local img2 = frame:preprocess(string.format('[[File:%s|%s]]', images[2], maxWidth))
   
    -- Include navigation arrows similar to the carousel, but keep the orbital animation styles
     return string.format(
     return string.format(
         '<div class="person-portrait-carousel">' ..
         '<div class="person-portrait-carousel">' ..
         '<div class="carousel-container">' ..
         '<div class="carousel-container">' ..
        '<div class="carousel-nav carousel-prev">&#9664;</div>' ..
         '<div class="carousel-images" id="carousel-%s">' ..
         '<div class="carousel-images" id="carousel-%s">' ..
         '<div class="carousel-item carousel-orbital-1"><img src="%s" alt="Portrait 1" /></div>' ..
         '<div class="carousel-item carousel-orbital-1" data-index="1">%s</div>' ..
         '<div class="carousel-item carousel-orbital-2"><img src="%s" alt="Portrait 2" /></div>' ..
         '<div class="carousel-item carousel-orbital-2" data-index="2">%s</div>' ..
         '</div></div></div>',
        '</div>' ..
         '<div class="carousel-nav carousel-next">&#9654;</div>' ..
        '</div></div>',
         navId,
         navId,
         images[1], images[2]
         img1, img2
     )
     )
end
end
Line 65: Line 80:
     local itemsHtml = {}
     local itemsHtml = {}
     local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
     local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
    local frame = mw.getCurrentFrame()
      
      
     -- Generate carousel items
     -- Generate carousel items
     for i, image in ipairs(images) do
     for i, imageName in ipairs(images) do
         local class = i == 1 and "carousel-visible" or "carousel-hidden"
         local class = i == 1 and "carousel-visible" or "carousel-hidden"
        -- Add positioning classes for 3+ images
        if i == 2 then
            class = class .. " carousel-right"
        elseif i == #images then
            class = class .. " carousel-left"
        end
       
        -- Use wiki syntax for images
        local img = frame:preprocess(string.format('[[File:%s|%s]]', imageName, maxWidth))
       
         table.insert(itemsHtml, string.format(
         table.insert(itemsHtml, string.format(
             '<div class="carousel-item %s"><img src="%s" alt="Portrait %d" /></div>',
             '<div class="carousel-item %s" data-index="%d">%s</div>',
             class, image, i
             class, i, img
         ))
         ))
     end
     end
Line 79: Line 105:
         '<div class="person-portrait-carousel">' ..
         '<div class="person-portrait-carousel">' ..
         '<div class="carousel-container">' ..
         '<div class="carousel-container">' ..
         '<div class="carousel-nav carousel-prev" onclick="rotateCarousel(\'%s\', \'prev\')">&#9664;</div>' ..
         '<div class="carousel-nav carousel-prev">&#9664;</div>' ..
         '<div class="carousel-images" id="carousel-%s">%s</div>' ..
         '<div class="carousel-images" id="carousel-%s">%s</div>' ..
         '<div class="carousel-nav carousel-next" onclick="rotateCarousel(\'%s\', \'next\')">&#9654;</div>' ..
         '<div class="carousel-nav carousel-next">&#9654;</div>' ..
         '</div></div>',
         '</div></div>',
         navId, navId, table.concat(itemsHtml), navId
         navId, table.concat(itemsHtml)
     )
     )
end
end
Line 99: Line 125:
              
              
             -- Determine display type based on number of images
             -- Determine display type based on number of images
            local carouselHtml = ""
             if #images == 0 then
             if #images == 0 then
                 return "" -- No images
                 return "" -- No images, don't create a row
             elseif #images == 1 then
             elseif #images == 1 then
                 return createSingleImage(images[1], maxWidth)
                 carouselHtml = createSingleImage(images[1], maxWidth)
             elseif #images == 2 then
             elseif #images == 2 then
                 return createDualImages(images, maxWidth)
                 carouselHtml = createDualImages(images, maxWidth)
             else
             else
                 return createCarousel(images, maxWidth)
                 carouselHtml = createCarousel(images, maxWidth)
             end
             end
           
            -- Wrap in a table row to ensure proper separation from the title
            return string.format('|-\n| colspan="2" class="person-portrait-row" | %s', carouselHtml)
         end
         end
          
          

Latest revision as of 03:01, 25 August 2025

Documentation for this module may be created at Module:ElementPortraitCarousel/doc

--[[
* Name: ElementPortraitCarousel
* Author: Mark W. Datysgeld
* Description: Element module that renders portrait carousels for Person templates with three display modes based on image count
* Notes: Single image (centered display); dual images (orbital display with animation); multi-image (full carousel with navigation arrows); integrates with Blueprint template system; image sanitization; protected execution with error handling
]]

local p = {}

p.elementName = "portraitCarousel"

-- Load required modules
local ErrorHandling = require('Module:ErrorHandling')
local TemplateHelpers = require('Module:TemplateHelpers')

-- Default configuration
p.defaultConfig = {
    maxWidth = "220px",
    carouselClass = "person-portrait-carousel"
}

-- Split portrait field by semicolons
local function splitPortraitImages(portrait)
    if not portrait or portrait == "" then
        return {}
    end
    
    local images = {}
    for image in portrait:gmatch("[^;]+") do
        local imageName = mw.text.trim(image)
        -- Sanitize logo path - extract filename and remove prefixes
        imageName = TemplateHelpers.sanitizeUserInput(imageName, "IMAGE_FILES")
        -- Store just the image name, we'll use MediaWiki's renderer
        if imageName and imageName ~= "" then
            table.insert(images, imageName)
        end
    end
    return images
end

-- Create single image HTML
local function createSingleImage(imageName, maxWidth)
    local frame = mw.getCurrentFrame()
    -- Use wiki syntax for images via preprocess
    local img = frame:preprocess(string.format('[[File:%s|%s]]', imageName, maxWidth))
    
    return string.format(
        '<div class="person-portrait-carousel">%s</div>',
        img
    )
end

-- Create dual-image orbital display HTML with navigation arrows
local function createDualImages(images, maxWidth)
    local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
    local frame = mw.getCurrentFrame()
    
    -- Use wiki syntax for images through preprocess
    local img1 = frame:preprocess(string.format('[[File:%s|%s]]', images[1], maxWidth))
    local img2 = frame:preprocess(string.format('[[File:%s|%s]]', images[2], maxWidth))
    
    -- Include navigation arrows similar to the carousel, but keep the orbital animation styles
    return string.format(
        '<div class="person-portrait-carousel">' ..
        '<div class="carousel-container">' ..
        '<div class="carousel-nav carousel-prev">&#9664;</div>' ..
        '<div class="carousel-images" id="carousel-%s">' ..
        '<div class="carousel-item carousel-orbital-1" data-index="1">%s</div>' ..
        '<div class="carousel-item carousel-orbital-2" data-index="2">%s</div>' ..
        '</div>' ..
        '<div class="carousel-nav carousel-next">&#9654;</div>' ..
        '</div></div>',
        navId,
        img1, img2
    )
end

-- Create multi-image carousel HTML
local function createCarousel(images, maxWidth)
    local itemsHtml = {}
    local navId = mw.uri.anchorEncode(mw.title.getCurrentTitle().text or "person")
    local frame = mw.getCurrentFrame()
    
    -- Generate carousel items
    for i, imageName in ipairs(images) do
        local class = i == 1 and "carousel-visible" or "carousel-hidden"
        -- Add positioning classes for 3+ images
        if i == 2 then
            class = class .. " carousel-right"
        elseif i == #images then
            class = class .. " carousel-left"
        end
        
        -- Use wiki syntax for images
        local img = frame:preprocess(string.format('[[File:%s|%s]]', imageName, maxWidth))
        
        table.insert(itemsHtml, string.format(
            '<div class="carousel-item %s" data-index="%d">%s</div>',
            class, i, img
        ))
    end
    
    -- Build complete carousel HTML
    return string.format(
        '<div class="person-portrait-carousel">' ..
        '<div class="carousel-container">' ..
        '<div class="carousel-nav carousel-prev">&#9664;</div>' ..
        '<div class="carousel-images" id="carousel-%s">%s</div>' ..
        '<div class="carousel-nav carousel-next">&#9654;</div>' ..
        '</div></div>',
        navId, table.concat(itemsHtml)
    )
end

-- Create block function
function p.createBlock()
    return function(template, args)
        -- Protected execution wrapper
        local function execute()
            local portrait = args.portrait or ""
            local images = splitPortraitImages(portrait)
            local maxWidth = template.config.portraitCarousel 
                and template.config.portraitCarousel.maxWidth
                or p.defaultConfig.maxWidth
            
            -- Determine display type based on number of images
            local carouselHtml = ""
            if #images == 0 then
                return "" -- No images, don't create a row
            elseif #images == 1 then
                carouselHtml = createSingleImage(images[1], maxWidth)
            elseif #images == 2 then
                carouselHtml = createDualImages(images, maxWidth)
            else
                carouselHtml = createCarousel(images, maxWidth)
            end
            
            -- Wrap in a table row to ensure proper separation from the title
            return string.format('|-\n| colspan="2" class="person-portrait-row" | %s', carouselHtml)
        end
        
        -- Wrap with error handling
        if template._errorContext then
            return ErrorHandling.protect(
                template._errorContext,
                "ElementBlock_portraitCarousel",
                execute,
                ""
            )
        else
            local ok, result = pcall(execute)
            return ok and result or ""
        end
    end
end

return p