Skip to main content

A Day of Coding - Files / Presentation

Prepare for Class

This event will be ‘fluid’, the times may not be exact depending on questions.

  • 8:30am - In-person guests can arrive
  • 9:00am - Begin session 1
  • 10:30am - Break
  • 12:00pm - Lunch Break
  • 1:30pm - Complete Session 1
  • 2:00pm - Session 2 (GSAP)
  • 3:00pm - Break
  • 4:00pm - End

Class Files / Presentations

Presentation Part 1 - Create Articles Module Override

 

Code for Class

grid_items.php: Add line 13:

use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;

grid_items.php: Add line 26:

$jcfields = FieldsHelper::getFields('com_content.article', $item, true);

        // Create an associative array for easier access by field name
        foreach($jcfields as $jcfield) {
            $jcfields[$jcfield->name] = $jcfield;
        }

grid_items.php: Before afterDisplayTitle:

>  <?php if (!empty($jcfields['sub-title']->value)) : ?>
    <p class="lead"><?php echo $jcfields['sub-title']->value; ?></p>
  <?php endif; ?>

grid_items.php: Replace introText:

 <?php if (!empty($jcfields['teaser-text']->value)) : ?>
  <?php echo $jcfields['teaser-text']->value; ?>
<?php elseif ($params->get('show_introtext', 1)) : ?>
  <?php echo $item->displayIntrotext; ?>
<?php endif; ?>  

grid_items.php: Add images

<?php if(!empty($jcfields['teaser-image']->value)) : ?>
    <figure class="mod-articles-image item-image">
        <?php echo $jcfields['teaser-image']->value; ?>
    </figure>
<?php elseif (in_array($params->get('img_intro_full'), ['intro', 'full']) && !empty($item->imageSrc)) : ?>
    <?php echo LayoutHelper::render('joomla.content.' . $params->get('img_intro_full') . '_image', $item); ?>
<?php else : ?>
    <?php
        $layoutAttr = [
            'src'      => $mediaPath . 'images/placeholder.png',
            'alt'      => '',
        ];
    ?>
    <figure class="mod-articles-image item-image">
        <?php echo LayoutHelper::render('joomla.html.image', $layoutAttr); ?>
    </figure>
<?php endif; ?>

grid.php: Near top of file

/** Day of Code: Get current template */
$template = $app->getTemplate();
$mediaPath = 'media/templates/site/' . $template . '/';  
  /** Day of Code: Use own CSS file */
$wa->registerAndUseStyle('mod_articles_rtg', $mediaPath . 'css/mod_articles/mod-articles-rtg.css');

mod-articles-rtg.css

.mod-articles-rtg-item {
    display: grid;
    grid-template-columns: 1fr;
}
.mod-articles-rtg .mod-articles-item-content {
    display: flex;
    flex-direction: column;
    justify-content: center;
}
.mod-articles-rtg .mod-articles-image {
    margin-bottom: 0;
}
.mod-articles-rtg .mod-articles-image img {
    width: 100%;
    object-fit: cover;
}
@media (width >= 768px) {
    .container-bottom-a {
        grid-column: full-start/full-end;
    }
    .mod-articles-rtg-item {
        grid-template-columns: 1fr 1fr;
    }
    .mod-articles-rtg .mod-articles-item-content {
        padding-inline: 6rem 2rem;
    }
    .alternate .mod-articles-rtg-item:nth-child(odd) .mod-articles-item-content {
        order: 1;
        padding-left: 2rem;
        padding-right: 6rem;
    }
    .alternate .mod-articles-rtg-item:nth-child(even) .mod-articles-item-content {
        padding-right: 2rem;
        padding-left: 6rem;
    }
    .alternate .mod-articles-rtg-item:nth-child(odd) .mod-articles-image {
        order: 0;
    }
}

mod-articles-gsap-vslide.css

/* Rutgers-style 50/50 layout with full page scrolling */
.mod-articles-scroll-wrapper {
    position: relative;
    width: 100%;
    min-height: 100vh;
    display: flex;
}

/* Left side - scrolling text container */
.mod-articles-text-container {
    width: 50%;
    min-height: 100vh;
    position: relative;
    /* Remove overflow-y: auto - let the page scroll naturally */
}

/* Right side - image container stays within module */
.mod-articles-image-container {
    width: 50%;
    height: 100vh;
    position: sticky;
    top: 0;
    overflow: hidden;
    z-index: 10;
}

/* Stack all articles vertically in text container */
.mod-articles-rtg {
    display: block;
}

.mod-articles-rtg-item {
    display: block;
    min-height: 100vh; /* Each article takes full height */
    padding: 2rem;
    position: relative;
}

/* Content styling */
.mod-articles-rtg .mod-articles-item-content {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 90vh;
    padding: 2rem 0;
}

.mod-articles-title {
    font-size: 2.5rem;
    font-weight: bold;
    margin-bottom: 1.5rem;
    line-height: 1.2;
    color: #333;
}

.mod-articles-rtg .mod-articles-item-content .lead {
    font-size: 1.3rem;
    color: #666;
    margin-bottom: 2rem;
    font-weight: 500;
}

.mod-articles-rtg .mod-articles-item-content p,
.mod-articles-rtg .mod-articles-item-content div {
    font-size: 1.1rem;
    line-height: 1.7;
    color: #444;
    margin-bottom: 1.5rem;
}

/* Images - positioned absolutely in image container */
.mod-articles-image-slide {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.8s ease-in-out;
}

.mod-articles-image-slide.active {
    opacity: 1;
    visibility: visible;
}

/* Ensure images take full container size */
.mod-articles-rtg .mod-articles-image {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    display: block;
}

.mod-articles-rtg .mod-articles-image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    display: block;
}

/* Ensure figure takes full size */
.mod-articles-rtg .mod-articles-image.item-image {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    display: block;
}

/* Force all images to fill container */
.mod-articles-image-container img {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
    object-position: center !important;
    display: block !important;
}

/* Target the specific structure from the selector */
#scroll-112 > div.mod-articles-image-container > div.mod-articles-image-slide.active > figure > img,
.mod-articles-image-container .mod-articles-image-slide.active figure img,
.mod-articles-image-container .mod-articles-image-slide figure img {
    width: 100% !important;
    height: 100% !important;
    object-fit: cover !important;
    object-position: center !important;
    display: block !important;
    margin: 0 !important;
    padding: 0 !important;
}

/* Ensure figure also takes full size */
.mod-articles-image-container .mod-articles-image-slide figure {
    margin: 0 !important;
    padding: 0 !important;
    width: 100% !important;
    height: 100% !important;
    display: block !important;
}

/* Hide original images in text area */
.mod-articles-rtg-item .mod-articles-image {
    display: none;
}

/* Remove scrollbar hiding since we're not using internal scrolling */
/* .mod-articles-text-container::-webkit-scrollbar {
    width: 0;
    display: none;
}

.mod-articles-text-container {
    scrollbar-width: none;
    -ms-overflow-style: none;
} */

.container-bottom-a {
    grid-area: bot-a;
    grid-column: 1 / -1;
}

/* Mobile responsive */
@media (max-width: 768px) {
    .mod-articles-scroll-wrapper {
        flex-direction: column;
    }
    
    .mod-articles-text-container {
        width: 100%;
        min-height: auto;
        order: 2;
    }
    
    .mod-articles-image-container {
        width: 100%;
        height: 40vh;
        position: relative;
        order: 1;
    }
    
    .mod-articles-rtg-item {
        min-height: 60vh;
        padding: 1rem;
    }
}  

mod-articles-gsap-vslide.js

/**
 * Rutgers-style scroll effect - text scrolls with page, images transition within module
 */
document.addEventListener('DOMContentLoaded', function() {
    
    const scrollWrappers = document.querySelectorAll('.mod-articles-scroll-wrapper');
    
    scrollWrappers.forEach((wrapper, wrapperIndex) => {
        const textContainer = wrapper.querySelector('.mod-articles-text-container');
        const imageContainer = wrapper.querySelector('.mod-articles-image-container');
        const articles = wrapper.querySelector('.mod-articles-rtg');
        const textItems = articles.querySelectorAll('.mod-articles-rtg-item');
        const imageSlides = imageContainer.querySelectorAll('.mod-articles-image-slide');
        const totalItems = textItems.length;
        
        
        if (totalItems <= 1) {
            return;
        }
        
        let currentActiveImage = 0;
        let isTransitioning = false;
        
        // Set first image as active
        imageSlides[0].classList.add('active');
        
        function transitionToImage(newIndex) {
            if (isTransitioning || newIndex === currentActiveImage || newIndex < 0 || newIndex >= totalItems) {
                return;
            }
            
            isTransitioning = true;
            
            const currentImage = imageSlides[currentActiveImage];
            const newImage = imageSlides[newIndex];
            
            // GSAP timeline for smooth crossfade transition
            const tl = gsap.timeline({
                onComplete: () => {
                    currentActiveImage = newIndex;
                    isTransitioning = false;
                }
            });
            
            // Set new image initial state (invisible but ready)
            tl.set(newImage, { 
                className: 'mod-articles-image-slide',
                opacity: 0,
                scale: 1
            })
            
            // Fade out current image and fade in new image simultaneously
            .to(currentImage, {
                duration: 0.8,
                opacity: 0,
                ease: "power2.inOut"
            }, 0)
            .to(newImage, {
                duration: 0.8,
                opacity: 1,
                ease: "power2.inOut"
            }, 0)
            
            // Update classes after transition
            .set(currentImage, { className: 'mod-articles-image-slide' })
            .set(newImage, { className: 'mod-articles-image-slide active' });
        }
        
        // Track which article is currently in view based on module position in viewport
        function checkVisibleArticle() {
            const wrapperRect = wrapper.getBoundingClientRect();
            const windowHeight = window.innerHeight;
            
            // Check if the module is visible in the viewport
            if (wrapperRect.bottom < 0 || wrapperRect.top > windowHeight) {
                return; // Module is not visible
            }
            
            // Calculate which article should be active based on module position
            let activeArticle = 0;
            
            textItems.forEach((item, index) => {
                const itemRect = item.getBoundingClientRect();
                const itemTop = itemRect.top;
                const itemBottom = itemRect.bottom;
                const viewportCenter = windowHeight / 2;
                
                // Check if this article is in the center of the viewport
                if (itemTop <= viewportCenter && itemBottom >= viewportCenter) {
                    activeArticle = index;
                }
                
                // Add visual feedback for debugging
                const isInView = itemTop <= viewportCenter && itemBottom >= viewportCenter;
                item.style.backgroundColor = isInView ? '#f0f8ff' : 'transparent';
                
            });
            
            // Transition image if needed
            if (activeArticle !== currentActiveImage) {
                transitionToImage(activeArticle);
            }
        }
        
        // Intersection Observer for better performance - observing module position
        const observerOptions = {
            root: null, // Use viewport as root
            threshold: [0.1, 0.3, 0.5, 0.7, 0.9],
            rootMargin: '-10% 0px -10% 0px'
        };
        
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.target === wrapper) {
                    // Module is entering/exiting viewport
                    if (entry.isIntersecting) {
                        checkVisibleArticle();
                    }
                } else {
                    // Individual article is visible
                    const index = Array.from(textItems).indexOf(entry.target);
                    
                    if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
                        
                        if (index !== currentActiveImage && !isTransitioning) {
                            transitionToImage(index);
                        }
                    }
                }
            });
        }, observerOptions);
        
        // Observe the wrapper and all text items
        observer.observe(wrapper);
        textItems.forEach(item => observer.observe(item));
        
        // Page scroll listener with throttling
        let scrollTimeout;
        window.addEventListener('scroll', () => {
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(() => {
                checkVisibleArticle();
            }, 100);
        });
        
        // Touch support for mobile
        let startY = 0;
        
        window.addEventListener('touchstart', (e) => {
            startY = e.touches[0].clientY;
        }, { passive: true });
        
        window.addEventListener('touchend', (e) => {
            const endY = e.changedTouches[0].clientY;
            const swipeDistance = Math.abs(endY - startY);
            
            if (swipeDistance > 100) {
                // Force check after significant touch movement
                setTimeout(checkVisibleArticle, 200);
            }
        }, { passive: true });
        
        // Initial check
        setTimeout(() => {
            checkVisibleArticle();
        }, 500);
        
        // Initial entrance animations - SMOOTH crossfade style
        gsap.from(imageSlides[0], {
            duration: 1.2,
            opacity: 0,
            ease: "power2.out"
        });
        
        gsap.from(textItems[0].querySelector('.mod-articles-item-content'), {
            duration: 1.0,
            opacity: 0,
            y: 30,
            ease: "power2.out",
            delay: 0.4
        });
        
    });
});

grid-gsap-vslide.php

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Language\Text;

/** Day of Code: Get current template */
$template = $app->getTemplate();
$mediaPath = 'media/templates/site/' . $template . '/';

/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();

/** Day of Code: Add GSAP library */
$wa->registerAndUseScript('gsap', 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js', [], [], []);

/** Day of Code: Use own CSS file */
$wa->registerAndUseStyle('mod_articles_gsap_vslide', $mediaPath . '/css/mod_articles/mod-articles-gsap-vslide.css');

/** Day of Code: Javascript for animations */
$wa->registerAndUseScript('mod_articles_gsap_vslide', $mediaPath . '/js/mod_articles/mod-articles-gsap-vslide.js', ['gsap'], ['defer' => true], []);

if (!$list) {
    return;
}

$groupHeading = 'h4';

if ((bool) $module->showtitle) {
    $modTitle = $params->get('header_tag');

    if ($modTitle == 'h1') {
        $groupHeading = 'h2';
    } elseif ($modTitle == 'h2') {
        $groupHeading = 'h3';
    }
}

$layoutSuffix = $params->get('title_only', 0) ? '_titles' : '_items';

?>

<div class="mod-articles-scroll-wrapper" id="scroll-<?php echo $module->id; ?>">
    
    <?php if ($grouped) : ?>
        <?php foreach ($list as $groupName => $items) : ?>
            <div class="mod-articles-group">
                <<?php echo $groupHeading; ?>><?php echo Text::_($groupName); ?></<?php echo $groupHeading; ?>>
                <?php require ModuleHelper::getLayoutPath('mod_articles', $params->get('layout', 'default') . $layoutSuffix); ?>
            
        <?php endforeach; ?>
    <?php else : ?>
        <?php $items = $list; ?>
        <?php require ModuleHelper::getLayoutPath('mod_articles', $params->get('layout', 'default') . $layoutSuffix); ?>
    <?php endif; ?>
    
</div>

grid-gsap-vslide-items.php

<?php
defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;

if ($params->get('articles_layout') == 1) {
    $gridCols = 'grid-cols-' . $params->get('layout_columns');
}
?>

<div class="mod-articles-text-container">
    <div class="mod-articles-rtg" data-total-items="<?php echo count($items); ?>">
        <?php foreach ($items as $index => $item) : ?>
            <?php
            $jcfields = FieldsHelper::getFields('com_content.article', $item, true);

            // Create an associative array for easier access by field name
            foreach($jcfields as $jcfield) {
                $jcfields[$jcfield->name] = $jcfield;
            }

            $displayInfo = $item->displayHits || $item->displayAuthorName || $item->displayCategoryTitle || $item->displayDate;
            ?>
            <div class="mod-articles-rtg-item" data-slide="<?php echo $index; ?>">

                <?php if ($params->get('item_title') || $displayInfo || $params->get('show_tags') || $params->get('show_introtext') || $params->get('show_readmore')) : ?>
                    <div class="mod-articles-item-content">

                        <?php if ($params->get('item_title')) : ?>
                            <?php $item_heading = $params->get('item_heading', 'h4'); ?>
                            <<?php echo $item_heading; ?> class="mod-articles-title" itemprop="name">
                                <?php if ($params->get('link_titles') == 1) : ?>
                                    <?php $attributes = ['class' => 'mod-articles-link ' . $item->active, 'itemprop' => 'url']; ?>
                                    <?php $link = htmlspecialchars($item->link, ENT_COMPAT, 'UTF-8', false); ?>
                                    <?php $title = htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8', false); ?>
                                    <?php echo HTMLHelper::_('link', $link, $title, $attributes); ?>
                                <?php else : ?>
                                    <?php echo $item->title; ?>
                                <?php endif; ?>
                            </<?php echo $item_heading; ?>>
                        <?php endif; ?>

                        <?php if (!empty($jcfields['sub-title']->value)) : ?>
                            <p class="lead"><?php echo $jcfields['sub-title']->value; ?><</p>
                        <?php endif; ?>

                        <?php echo $item->event->afterDisplayTitle; ?>

                        <?php if ($displayInfo) : ?>
                            <?php $listClass = ($params->get('info_layout') == 1) ? 'list-inline' : 'list-unstyled'; ?>
                            <dl class="<?php echo $listClass; ?>">
                                <dt class="article-info-term">
                                    <span class="visually-hidden">
                                        <?php echo Text::_('MOD_ARTICLES_INFO'); ?>
                                    </span>
                                </dt>

                                <?php if ($item->displayAuthorName) : ?>
                                    <dd class="mod-articles-writtenby <?php echo ($params->get('info_layout') == 1 ? 'list-inline-item' : ''); ?>">
                                        <?php echo LayoutHelper::render('joomla.icon.iconclass', ['icon' => 'icon-user icon-fw']); ?>
                                        <?php echo $item->displayAuthorName; ?>
                                    </dd>
                                <?php endif; ?>

                                <?php if ($item->displayCategoryTitle) : ?>
                                    <dd class="mod-articles-category get('info_layout') == 1 ? 'list-inline-item' : ''); ?>">
                                        <?php echo LayoutHelper::render('joomla.icon.iconclass', ['icon' => 'icon-folder-open icon-fw']); ?>
                                        <?php if ($item->displayCategoryLink) : ?>
                                            <a href="/<?php echo $item->displayCategoryLink; ?>">
                                                <?php echo $item->displayCategoryTitle; ?>
                                            </a>
                                        <?php else : ?>
                                            <?php echo $item->displayCategoryTitle; ?>
                                        <?php endif; ?>
                                    </dd>
                                <?php endif; ?>

                                <?php if ($item->displayDate) : ?>
                                    <dd class="mod-articles-date <?php echo ($params->get('info_layout') == 1 ? 'list-inline-item' : ''); ?>">
                                        <?php echo LayoutHelper::render('joomla.icon.iconclass', ['icon' => 'icon-calendar icon-fw']); ?>
                                        <?php echo $item->displayDate; ?>
                                    <</dd>
                                <?php endif; ?>

                                <?php if ($item->displayHits) : ?>
                                    <dd class="mod-articles-hits get('info_layout') == 1 ? 'list-inline-item' : ''); ?>">
                                        <?php echo LayoutHelper::render('joomla.icon.iconclass', ['icon' => 'icon-eye icon-fw']); ?>
                                        <?php echo $item->displayHits; ?>
                                    </dd>
                                <?php endif; ?>
                            </dl>
                        <?php endif; ?>

                        <?php if ($params->get('show_tags', 0) && $item->tags->itemTags) : ?>
                            <div class="mod-articles-tags">
                                <?php echo LayoutHelper::render('joomla.content.tags', $item->tags->itemTags); ?>
                            </div>
                        <?php endif; ?>

                        <?php echo $item->event->beforeDisplayContent; ?>

                        <?php if (!empty($jcfields['teaser-text']->value)) : ?>
                            <?php echo $jcfields['teaser-text']->value; ?>
                        <?php elseif ($params->get('show_introtext', 1)) : ?>
                            <?php echo $item->displayIntrotext; ?>
                        <?php endif; ?>

                        <?php echo $item->event->afterDisplayContent; ?>

                        <?php if ($params->get('show_readmore')) : ?>
                            <?php if ($params->get('show_readmore_title', '') !== '') : ?>
                                <?php $item->params->set('show_readmore_title', $params->get('show_readmore_title')); ?>
                                <?php $item->params->set('readmore_limit', $params->get('readmore_limit')); ?>
                            <?php endif; ?>
                            <?php echo LayoutHelper::render('joomla.content.readmore', ['item' => $item, 'params' => $item->params, 'link' => $item->link]); ?>
                        <?php endif; ?>
                    </div>
                <?php endif; ?>
            </div>
        <?php endforeach; ?>
    </div>
</div>

<div class="mod-articles-image-container">
    <?php foreach ($items as $index => $item) : ?>
        <?php
        $jcfields = FieldsHelper::getFields('com_content.article', $item, true);
        foreach($jcfields as $jcfield) {
            $jcfields[$jcfield->name] = $jcfield;
        }
        ?>
        <div class="mod-articles-image-slide <?php echo $index === 0 ? 'active' : ''; ?>" data-slide="<?php echo $index; ?>">
            <?php if(!empty($jcfields['teaser-image']->value)) : ?>
                <<figure class="mod-articles-image item-image">
                    <?php echo $jcfields['teaser-image']->value; ?>
                </figure>
            <?php elseif (in_array($params->get('img_intro_full'), ['intro', 'full']) && !empty($item->imageSrc)) : ?>
                <?php echo LayoutHelper::render('joomla.content.' . $params->get('img_intro_full') . '_image', $item); ?>
            <?php else : ?>
                <?php
                    $layoutAttr = [
                        'src'      => $mediaPath . 'images/placeholder.png',
                        'alt'      => '',
                    ];
                ?>
                <figure class="mod-articles-image item-image">
                    <?php echo LayoutHelper::render('joomla.html.image', $layoutAttr); ?>
                </figure>
            <?php endif; ?>
        </div>
    <?php endforeach; ?>
</div>

Thank you to:

  • Our Presenters: Daniel Dubois, Shirat Goldstein, Viviana Menzel & Olivier Buisard
  • Our Host: Laura Gordon, Joomla Users Group of NJ
  • Rutgers for the space
  • Rochen for hosting all of our sites

rochen

JoomlaDay™ events are independently managed local events that are officially recognized by The Joomla Project™. Use of the Joomla!® name, symbol, logo, JoomlaDay,™ JDay™ and related trademarks is licensed by Open Source Matters, Inc.

joomlausersnj.com is not affiliated with or endorsed by The Joomla! Project™ or Open Source Matters.
The Joomla!® name and logo is used under a limited license granted by Open Source Matters the trademark holder in the United States and other countries.