Jump to content

Module:ElementPortraitCarousel

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

--[[ 
 * Module:ElementPortraitCarousel
 * Renders a portrait carousel for person templates with 3 display modes:
 * 1. Single image - Simple centered display
 * 2. Dual images - Orbital display with animation
 * 3. Multi-image - Full carousel with navigation arrows
 * 
 * Integrates with Blueprint template system.
 ]]

local p = {}

p.elementName = "portraitCarousel"

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

-- 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)
        -- 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