A Day of Coding - Files / Presentation
Prepare for Class
- Install on your laptop
- Code: visual studio code - https://code.visualstudio.com/
- Install FTP tool, FileZilla - https://filezilla-project.org/
- Create an account:
- AI Assistance: create an account with claude.ai
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
- Original JPA of site (without any code)
- Files Part 1: an override for the Articles module.
- Files 'Get Ready' for Part 2.
- Files Part 2: Add an override with animations.
- Intro presentation file
- Image placeholder
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
