Compare commits

...

8 Commits

Author SHA1 Message Date
gitea-actions[bot] b0d1e42bab chore(release): build 01.08.00-rc [skip ci] 2026-06-04 13:02:18 +00:00
Jonathan Miller c3a4a1f28d fix: final review — logging, null guards, XSS filter, stale descriptions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Successful in 17s
Add Joomla Log::add for article query, DirectoryIterator, and JSON
decode failures. Filter article content through HTMLHelper content.prepare
to prevent XSS. Add null guards on scroll indicator and mute toggle
hero/icon elements. Add console.warn on slide content JSON parse failure.
Remove stale license key references from package and plugin descriptions.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:58:34 -05:00
Jonathan Miller 4d67a32cb0 feat: article content source, per-slide content, remove license check (#56, #57)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add content source selector — manual editor or Joomla article with
optional article title override. Add per-slide unique content via
subform repeatable field with heading, body, link, and link text per
slide, swapped in sync with background transitions using safe DOM
methods. Remove license key check from system plugin and plugin
dependency from module — extension is now free. Language strings in
all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:45:23 -05:00
Jonathan Miller 6f9aa71573 feat: content entrance animations and parallax scroll effect (#48, #52)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add configurable content entrance animations (fade-in, slide-up,
slide-left, slide-right) triggered by IntersectionObserver on scroll
into view, with adjustable delay. Add parallax scroll effect with
configurable speed (0.1–0.9) using passive scroll listener and GPU-
accelerated transforms. Both features respect prefers-reduced-motion.
Language strings in all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:37:59 -05:00
Jonathan Miller 4a18f46c68 feat: reduced motion, scroll indicator, and video poster (#49, #50, #51)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add prefers-reduced-motion support (WCAG 2.1 AA) — disables slideshow
cycling, CSS transitions/animations, and Ken Burns zoom when OS setting
is enabled. Add optional scroll-down chevron indicator with bounce
animation and smooth-scroll click handler. Add video poster image
fallback displayed while video loads. Language strings in all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:30:12 -05:00
Jonathan Miller f5fdf6742f fix: input validation, JS error handling, and package description
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add hex color validation for all color params, allowlist validation
for textAlign/fadeType/overlayType, range clamp for gradientAngle,
and try-catch around DirectoryIterator. Fix video.play() promise
rejection and iframe.contentWindow null guards in JS. Hardcode
package description in manifest. Normalize tabs throughout.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:23:31 -05:00
Jonathan Miller fe3abf6ddb feat: vertical alignment, mobile height, and gradient overlay (#53, #54, #55)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add vertical text alignment (top/center/bottom) for overlay content,
mobile-specific hero height via CSS custom property, and directional
gradient overlay (dark at bottom/top/left/right) reusing existing
overlay colour controls. Language strings added to all locale files.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:13:04 -05:00
Jonathan Miller 601cf77170 fix: address PR review issues and add configurable slide transitions
Fix CSS injection on heroHeight with regex validation, add missing
language keys to .sys.ini files, fix plugin manifest languages folder
attribute and display name, narrow catch to \Exception with logging,
add error handling to pkg_script auto-enable, fix mobile CSS for
color/gradient modes, add SPDX headers, remove dead code, and update
stale file path headers.

Add configurable transition type for image slideshow: crossfade, slide,
fade-to-black, and zoom (Ken Burns).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 06:46:47 -05:00
24 changed files with 1198 additions and 191 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<display-name>Package - MokoJoomHero</display-name>
<org>MokoConsulting</org>
<description>A Joomla Module designed to provide a random image from a folder with content on top as a Hero.</description>
<version>01.06.00</version>
<version>01.08.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 01.04.01
# VERSION: 01.08.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+4 -1
View File
@@ -1,11 +1,14 @@
# Changelog
## [Unreleased]
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Version format: `XX.YY.ZZ` (zero-padded semver).
## [Unreleased]
## [01.08.00] --- 2026-06-04
## [01.04.00] - 2026-05-30
+4 -4
View File
@@ -36,12 +36,12 @@ This is a Joomla **package** extension (`pkg_mokojoomhero`) containing two sub-e
### mod_mokojoomhero (Site Module)
- Random hero image slideshow or background video with content overlay
- Supports image folders, YouTube, Vimeo, and local video
- Configurable overlay, text alignment, card animation
- **Requires** `plg_system_mokojoomhero` to be enabled — module silently skips rendering if the system plugin is disabled
- Supports image folders, YouTube, Vimeo, local video, solid colour, gradient
- Configurable overlay, text alignment, card animation, parallax, content animations
- Works independently — no plugin dependency required
### plg_system_mokojoomhero (System Plugin)
- License key validation — warns admin once per session if no download key configured
- Placeholder for future system-level features
- Auto-enabled on package install via `pkg_script.php`
- Namespace: `Joomla\Plugin\System\MokoJoomHero`
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP:
INGROUP: Project.Documentation
REPO:
VERSION: 01.04.01
VERSION: 01.08.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -7,7 +7,7 @@
# FILE INFORMATION
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
FILE: ./README.md
VERSION: 01.04.01
VERSION: 01.08.00
BRIEF: MokoJoomHero - Joomla Module
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 01.04.01
VERSION: 01.08.00
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -6,15 +6,24 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
; VERSION: 01.04.01
; VERSION: 01.08.00
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
@@ -27,6 +36,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
@@ -35,6 +52,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
@@ -43,10 +69,35 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
@@ -68,18 +119,35 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Alignment options
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
; VERSION: 01.04.01
; VERSION: 01.08.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
@@ -14,17 +14,36 @@ MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or backgrou
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
@@ -34,6 +53,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
@@ -42,31 +70,85 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Solid colour background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Alignment options
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -6,15 +6,24 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.ini
; VERSION: 01.04.01
; VERSION: 01.08.00
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
@@ -27,6 +36,14 @@ MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
@@ -35,6 +52,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
@@ -64,22 +90,64 @@ MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Alignment options
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -6,7 +6,7 @@
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
; VERSION: 01.04.01
; VERSION: 01.08.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
@@ -14,17 +14,36 @@ MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or backgrou
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
@@ -34,6 +53,15 @@ MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slid
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
@@ -42,31 +70,85 @@ MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Solid color background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Alignment options
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -6,9 +6,9 @@
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/css/template.css
* VERSION: 01.04.01
* BRIEF: Hero module stylesheet — slideshow, video background, overlay
* PATH: /src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css
* VERSION: 01.08.00
* BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive
*/
/* ============================================================
@@ -31,7 +31,7 @@
}
/* ============================================================
Image slides
Image slides — base
============================================================ */
.mokojoomhero__slide {
position: absolute;
@@ -40,13 +40,51 @@
background-position: center;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 1s ease;
}
.mokojoomhero__slide--active {
opacity: 1;
}
/* ── Crossfade (default) ── */
.mokojoomhero[data-transition="crossfade"] .mokojoomhero__slide {
transition: opacity 1s ease;
}
/* ── Slide ── */
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide {
opacity: 1;
transform: translateX(100%);
transition: transform 0.8s ease;
}
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--active {
transform: translateX(0);
}
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--exit {
transform: translateX(-100%);
}
/* ── Fade to black ── */
.mokojoomhero[data-transition="fade-black"] .mokojoomhero__slide {
transition: opacity 0.6s ease;
}
/* ── Zoom (Ken Burns) ── */
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide {
transition: opacity 1s ease;
}
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active {
animation: mokojoomhero-zoom 8s ease forwards;
}
@keyframes mokojoomhero-zoom {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
/* ============================================================
Video background
============================================================ */
@@ -83,7 +121,6 @@ iframe.mokojoomhero__video {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
@@ -173,12 +210,147 @@ iframe.mokojoomhero__video {
background: rgba(0, 0, 0, 0.7);
}
/* ============================================================
Video poster image
============================================================ */
.mokojoomhero__poster {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* ============================================================
Scroll-down indicator
============================================================ */
.mokojoomhero__scroll-indicator {
position: absolute;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 2;
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 0;
opacity: 0.8;
transition: opacity 0.3s;
animation: mokojoomhero-bounce 2s infinite;
}
.mokojoomhero__scroll-indicator:hover {
opacity: 1;
}
.mokojoomhero__scroll-indicator--hidden {
display: none;
}
@keyframes mokojoomhero-bounce {
0%, 20%, 50%, 80%, 100% { transform: translateX(-50%) translateY(0); }
40% { transform: translateX(-50%) translateY(-8px); }
60% { transform: translateX(-50%) translateY(-4px); }
}
/* ============================================================
Content entrance animations
============================================================ */
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
opacity: 0;
}
.mokojoomhero__content--anim-fade-in.mokojoomhero__content--visible {
animation: mokojoomhero-anim-fade-in 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-up.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-up 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-left.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-left 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-right.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-right 0.8s ease forwards;
}
@keyframes mokojoomhero-anim-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mokojoomhero-anim-slide-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes mokojoomhero-anim-slide-left {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes mokojoomhero-anim-slide-right {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
/* ============================================================
Parallax
============================================================ */
.mokojoomhero[data-parallax] .mokojoomhero__slide,
.mokojoomhero[data-parallax] .mokojoomhero__color,
.mokojoomhero[data-parallax] .mokojoomhero__poster,
.mokojoomhero[data-parallax] video.mokojoomhero__video,
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
will-change: transform;
}
/* ============================================================
Reduced motion — WCAG 2.1 AA (SC 2.3.3)
============================================================ */
@media (prefers-reduced-motion: reduce) {
.mokojoomhero__slide {
transition: none !important;
animation: none !important;
}
.mokojoomhero__card[data-card-delay] {
opacity: 1;
animation: none !important;
}
.mokojoomhero__scroll-indicator {
animation: none;
}
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active {
animation: none !important;
}
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
opacity: 1;
animation: none !important;
}
.mokojoomhero[data-parallax] .mokojoomhero__slide,
.mokojoomhero[data-parallax] .mokojoomhero__color,
.mokojoomhero[data-parallax] .mokojoomhero__poster,
.mokojoomhero[data-parallax] video.mokojoomhero__video,
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
will-change: auto;
transform: none !important;
}
}
/* ============================================================
Responsive
============================================================ */
@media (max-width: 768px) {
.mokojoomhero {
height: auto !important;
height: var(--mokojoomhero-mobile-height, auto) !important;
}
.mokojoomhero__video,
@@ -186,8 +358,17 @@ iframe.mokojoomhero__video {
display: none;
}
/* Keep colour/gradient backgrounds visible on mobile */
.mokojoomhero__color {
position: relative;
}
.mokojoomhero__overlay {
padding: 1rem;
}
/* Only clear overlay background when media is hidden (image/video modes) */
.mokojoomhero:not(:has(.mokojoomhero__color)) .mokojoomhero__overlay {
background-color: transparent !important;
}
@@ -7,9 +7,9 @@
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/js/template.js
* VERSION: 01.04.01
* BRIEF: Hero module JavaScript — image slideshow crossfade
* PATH: /src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js
* VERSION: 01.08.00
* BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle
*/
'use strict';
@@ -20,25 +20,114 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// ── Image slideshow ──
document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) {
var slides = hero.querySelectorAll('.mokojoomhero__slide');
var interval = parseInt(hero.dataset.interval, 10) || 5000;
var current = 0;
var slides = hero.querySelectorAll('.mokojoomhero__slide');
var interval = parseInt(hero.dataset.interval, 10) || 5000;
var transition = hero.dataset.transition || 'crossfade';
var current = 0;
if (slides.length < 2) {
// Per-slide content data
var slideContentData = null;
var contentEl = hero.querySelector('.mokojoomhero__content');
if (hero.dataset.slideContent) {
try {
slideContentData = JSON.parse(hero.dataset.slideContent);
} catch (e) {
console.warn('MokoJoomHero: Failed to parse slide content data:', e.message);
slideContentData = null;
}
}
if (slides.length < 2 || prefersReducedMotion) {
return;
}
setInterval(function () {
slides[current].classList.remove('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'true');
function updateSlideContent(index) {
if (!slideContentData || !slideContentData[index] || !contentEl) {
return;
}
var data = slideContentData[index];
var card = contentEl.querySelector('.mokojoomhero__card');
var target = card || contentEl;
// Clear existing content safely
while (target.firstChild) {
target.removeChild(target.firstChild);
}
if (data.heading) {
var h2 = document.createElement('h2');
h2.className = 'mokojoomhero__title';
h2.textContent = data.heading;
target.appendChild(h2);
}
if (data.body) {
var p = document.createElement('p');
p.textContent = data.body;
target.appendChild(p);
}
if (data.link && data.linkText) {
var linkP = document.createElement('p');
var a = document.createElement('a');
a.href = data.link;
a.className = 'btn btn-primary';
a.textContent = data.linkText;
linkP.appendChild(a);
target.appendChild(linkP);
}
}
function advanceSlide() {
var prev = current;
current = (current + 1) % slides.length;
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}, interval);
if (transition === 'slide') {
slides[prev].classList.add('mokojoomhero__slide--exit');
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
// Reset exiting slide after transition completes
setTimeout(function () {
slides[prev].classList.remove('mokojoomhero__slide--exit');
}, 800);
} else if (transition === 'fade-black') {
// Phase 1: fade out current
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
// Phase 2: fade in next after a brief black gap
setTimeout(function () {
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}, 600);
} else {
// Crossfade and zoom use the same JS logic
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}
updateSlideContent(current);
}
// Set initial slide content
updateSlideContent(0);
setInterval(advanceSlide, interval);
});
// ── Pause/resume videos when out of viewport ──
@@ -55,9 +144,14 @@ document.addEventListener('DOMContentLoaded', function () {
if (entry.isIntersecting) {
// Resume
if (video) {
video.play();
var playPromise = video.play();
if (playPromise !== undefined) {
playPromise.catch(function () {
// Autoplay blocked by browser policy — not actionable
});
}
}
if (iframe) {
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
@@ -70,7 +164,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (video) {
video.pause();
}
if (iframe) {
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
@@ -86,9 +180,92 @@ document.addEventListener('DOMContentLoaded', function () {
observer.observe(hero);
});
// ── Content entrance animations ──
if (!prefersReducedMotion) {
var animObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('mokojoomhero__content--visible');
animObserver.unobserve(entry.target);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('.mokojoomhero__content[class*="mokojoomhero__content--anim-"]').forEach(function (el) {
animObserver.observe(el);
});
}
// ── Parallax scroll ──
if (!prefersReducedMotion) {
var parallaxHeroes = document.querySelectorAll('.mokojoomhero[data-parallax]');
if (parallaxHeroes.length) {
var onScroll = function () {
parallaxHeroes.forEach(function (hero) {
var rect = hero.getBoundingClientRect();
var speed = parseFloat(hero.dataset.parallax) || 0.5;
if (rect.bottom > 0 && rect.top < window.innerHeight) {
var offset = Math.round(rect.top * speed * -1);
var bg = hero.querySelector('.mokojoomhero__slide, .mokojoomhero__color, .mokojoomhero__poster, video.mokojoomhero__video');
if (bg) {
bg.style.transform = 'translateY(' + offset + 'px)';
}
var iframe = hero.querySelector('iframe.mokojoomhero__video');
if (iframe) {
iframe.style.transform = 'translate(-50%, calc(-50% + ' + offset + 'px))';
}
}
});
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
}
// ── Scroll-down indicator ──
document.querySelectorAll('.mokojoomhero__scroll-indicator').forEach(function (btn) {
var hero = btn.closest('.mokojoomhero');
if (!hero) {
return;
}
btn.addEventListener('click', function () {
var nextEl = hero.nextElementSibling || hero.parentElement.nextElementSibling;
if (nextEl) {
nextEl.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
}
});
// Hide indicator once hero scrolls out of view
var scrollObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) {
btn.classList.add('mokojoomhero__scroll-indicator--hidden');
} else {
btn.classList.remove('mokojoomhero__scroll-indicator--hidden');
}
});
}, { threshold: 0.1 });
scrollObserver.observe(hero);
});
// ── Mute/unmute toggle ──
document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) {
var hero = btn.closest('.mokojoomhero');
var hero = btn.closest('.mokojoomhero');
if (!hero) {
return;
}
var video = hero.querySelector('video.mokojoomhero__video');
var iframe = hero.querySelector('iframe.mokojoomhero__video');
var icon = btn.querySelector('.mokojoomhero__mute-icon');
@@ -99,7 +276,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (video) {
video.muted = !muted;
}
if (iframe) {
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
var func = muted ? 'unMute' : 'mute';
@@ -112,7 +289,10 @@ document.addEventListener('DOMContentLoaded', function () {
btn.setAttribute('data-muted', muted ? 'false' : 'true');
btn.setAttribute('aria-label', muted ? 'Mute video' : 'Unmute video');
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
if (icon) {
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
}
});
});
});
@@ -6,19 +6,14 @@
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
// Require the system plugin to be installed and enabled
if (!PluginHelper::isEnabled('system', 'mokojoomhero')) {
return;
}
/** @var \Joomla\CMS\Application\SiteApplication $app */
/** @var \stdClass $module */
/** @var \Joomla\Registry\Registry $params */
@@ -33,47 +28,129 @@ $heroMode = $params->get('heroMode', 'images');
$imageFolder = $params->get('imageFolder', 'images/heroes');
$imageCount = (int) $params->get('imageCount', 5);
$slideInterval = (int) $params->get('slideInterval', 5000);
$fadeType = $params->get('fadeType', 'crossfade');
$videoFile = $params->get('videoFile', '');
$heroHeight = $params->get('heroHeight', '60vh');
$heroHeightMobile = $params->get('heroHeightMobile', '');
$overlayColor = $params->get('overlayColor', '#000000');
$overlayType = $params->get('overlayType', 'solid');
$overlayOpacity = (float) $params->get('overlayOpacity', 0.5);
$textAlign = $params->get('textAlign', 'center');
$verticalAlign = $params->get('verticalAlign', 'center');
$textColor = $params->get('textColor', '#ffffff');
$heroContent = $params->get('heroContent', '');
$contentSource = $params->get('contentSource', 'manual');
$articleId = (int) $params->get('articleId', 0);
$useArticleTitle = (bool) $params->get('useArticleTitle', 0);
$heroContent = $params->get('heroContent', '');
$slideContent = $params->get('slideContent', '');
$showCard = (bool) $params->get('showCard', 1);
$cardDelay = (int) $params->get('cardDelay', 0);
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
$localVideoFile = $params->get('localVideoFile', '');
$contentAnimation = $params->get('contentAnimation', 'none');
$contentAnimationDelay = (int) $params->get('contentAnimationDelay', 0);
$parallaxEnabled = (bool) $params->get('parallaxEnabled', 0);
$parallaxSpeed = (float) $params->get('parallaxSpeed', 0.5);
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
$videoPoster = $params->get('videoPoster', '');
$showScrollIndicator = (bool) $params->get('showScrollIndicator', 0);
$localVideoFile = $params->get('localVideoFile', '');
$bgColor = $params->get('bgColor', '#003366');
$gradientStart = $params->get('gradientStart', '#003366');
$gradientEnd = $params->get('gradientEnd', '#006699');
$gradientAngle = (int) $params->get('gradientAngle', 135);
// Validate CSS height values to prevent injection
if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) {
$heroHeight = '60vh';
}
if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) {
$heroHeightMobile = '';
}
// Validate hex colour values
$hexColorPattern = '/^#[0-9a-fA-F]{6}$/';
if (!preg_match($hexColorPattern, $overlayColor)) {
$overlayColor = '#000000';
}
if (!preg_match($hexColorPattern, $textColor)) {
$textColor = '#ffffff';
}
if (!preg_match($hexColorPattern, $bgColor)) {
$bgColor = '#003366';
}
if (!preg_match($hexColorPattern, $gradientStart)) {
$gradientStart = '#003366';
}
if (!preg_match($hexColorPattern, $gradientEnd)) {
$gradientEnd = '#006699';
}
// Validate allowlist values
$allowedTextAlign = ['left', 'center', 'right'];
if (!in_array($textAlign, $allowedTextAlign, true)) {
$textAlign = 'center';
}
$allowedFadeTypes = ['crossfade', 'slide', 'fade-black', 'zoom'];
if (!in_array($fadeType, $allowedFadeTypes, true)) {
$fadeType = 'crossfade';
}
$allowedOverlayTypes = ['solid', 'gradient-bottom', 'gradient-top', 'gradient-left', 'gradient-right'];
if (!in_array($overlayType, $allowedOverlayTypes, true)) {
$overlayType = 'solid';
}
$allowedContentAnimations = ['none', 'fade-in', 'slide-up', 'slide-left', 'slide-right'];
if (!in_array($contentAnimation, $allowedContentAnimations, true)) {
$contentAnimation = 'none';
}
$parallaxSpeed = max(0.1, min(0.9, $parallaxSpeed));
$gradientAngle = max(0, min(360, $gradientAngle));
// Collect hero images
$heroImages = [];
if ($heroMode === 'images') {
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
if (is_dir($folderPath)) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
$all = [];
if (is_dir($folderPath)) {
try {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
$all = [];
foreach (new DirectoryIterator($folderPath) as $file) {
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
$all[] = $file->getFilename();
}
}
foreach (new DirectoryIterator($folderPath) as $file) {
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
$all[] = $file->getFilename();
}
}
if ($all) {
shuffle($all);
$picked = array_slice($all, 0, min($imageCount, 5));
if ($all) {
shuffle($all);
$picked = array_slice($all, 0, min($imageCount, 5));
foreach ($picked as $filename) {
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
}
}
}
foreach ($picked as $filename) {
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
}
}
} catch (\UnexpectedValueException $e) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Cannot read image folder "' . $folderPath . '": ' . $e->getMessage(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
}
}
// Build video URL — smartly detect YouTube, Vimeo, or local/direct file
@@ -82,23 +159,84 @@ $youtubeId = '';
$vimeoId = '';
if ($heroMode === 'localvideo' && $localVideoFile) {
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
} elseif ($heroMode === 'video' && $videoFile) {
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
$youtubeId = $m[1];
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
$vimeoId = $m[1];
} else {
// Direct URL or local file path
$videoUrl = (strpos($videoFile, '://') !== false)
? $videoFile
: Uri::root() . ltrim($videoFile, '/');
}
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
$youtubeId = $m[1];
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
$vimeoId = $m[1];
} else {
// Direct URL or local file path
$videoUrl = (strpos($videoFile, '://') !== false)
? $videoFile
: Uri::root() . ltrim($videoFile, '/');
}
}
// Module content from the editor (overlay text)
$content = $module->content ?? '';
// Load content from article if configured
$articleTitle = '';
if ($contentSource === 'article' && $articleId > 0) {
try {
$db = \Joomla\CMS\Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['title', 'introtext', 'fulltext']))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId)
->where($db->quoteName('state') . ' = 1');
$db->setQuery($query);
$article = $db->loadObject();
if ($article) {
$rawContent = $article->introtext ?: $article->fulltext;
$heroContent = \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $rawContent);
$articleTitle = $article->title;
}
} catch (\RuntimeException $e) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Failed to load article ID ' . $articleId . ': ' . $e->getMessage(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
}
// Process per-slide content — overrides folder-based images when populated
$slides = [];
if ($heroMode === 'images' && !empty($slideContent)) {
$slideData = is_string($slideContent) ? json_decode($slideContent, true) : (array) $slideContent;
if ($slideData === null && json_last_error() !== JSON_ERROR_NONE) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Failed to decode slideContent JSON: ' . json_last_error_msg(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
if (is_array($slideData)) {
foreach ($slideData as $item) {
$item = (array) $item;
if (!empty($item['image'])) {
$slides[] = [
'image' => Uri::root() . ltrim($item['image'], '/'),
'heading' => $item['heading'] ?? '',
'body' => $item['body'] ?? '',
'link' => $item['link'] ?? '',
'linkText' => $item['linkText'] ?? 'Learn More',
];
}
}
}
// Per-slide content overrides folder-based random images
if ($slides) {
$heroImages = array_column($slides, 'image');
}
}
require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default'));
@@ -10,7 +10,7 @@
DEFGROUP: MokoJoomHero.Module
INGROUP: MokoJoomHero
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
PATH: /src/mod_mokojoomhero.xml
PATH: /src/packages/mod_mokojoomhero/mod_mokojoomhero.xml
VERSION: 01.00.20
BRIEF: Joomla module manifest — random hero image with content overlay
-->
@@ -22,7 +22,7 @@
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<version>01.04.01-dev</version>
<version>01.08.00-rc</version>
<description>Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting.</description>
<scriptfile>script.php</scriptfile>
@@ -99,6 +99,19 @@
step="15"
showon="heroMode:gradient"
/>
<field
name="fadeType"
type="list"
label="MOD_MOKOJOOMHERO_FADE_TYPE_LABEL"
description="MOD_MOKOJOOMHERO_FADE_TYPE_DESC"
default="crossfade"
showon="heroMode:images"
>
<option value="crossfade">MOD_MOKOJOOMHERO_FADE_CROSSFADE</option>
<option value="slide">MOD_MOKOJOOMHERO_FADE_SLIDE</option>
<option value="fade-black">MOD_MOKOJOOMHERO_FADE_BLACK</option>
<option value="zoom">MOD_MOKOJOOMHERO_FADE_ZOOM</option>
</field>
<field
name="imageFolder"
type="text"
@@ -128,6 +141,51 @@
step="500"
showon="heroMode:images"
/>
<field
name="slideContent"
type="subform"
label="MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL"
description="MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
showon="heroMode:images"
max="5"
>
<form>
<field
name="image"
type="media"
label="MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL"
types="images"
/>
<field
name="heading"
type="text"
label="MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL"
filter="string"
/>
<field
name="body"
type="textarea"
label="MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL"
filter="safehtml"
rows="3"
/>
<field
name="link"
type="url"
label="MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL"
filter="url"
/>
<field
name="linkText"
type="text"
label="MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL"
filter="string"
default="Learn More"
/>
</form>
</field>
<field
name="videoFile"
type="text"
@@ -153,6 +211,56 @@
default="60vh"
filter="string"
/>
<field
name="heroHeightMobile"
type="text"
label="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL"
description="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC"
hint="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT"
default=""
filter="string"
/>
<field
name="videoPoster"
type="media"
label="MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL"
description="MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC"
types="images"
showon="heroMode:video,localvideo"
/>
<field
name="showScrollIndicator"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL"
description="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="parallaxEnabled"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_PARALLAX_LABEL"
description="MOD_MOKOJOOMHERO_PARALLAX_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="parallaxSpeed"
type="range"
label="MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL"
description="MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC"
default="0.5"
min="0.1"
max="0.9"
step="0.1"
showon="parallaxEnabled:1"
/>
<field
name="showMuteToggle"
type="radio"
@@ -169,6 +277,16 @@
<fieldset name="content"
label="MOD_MOKOJOOMHERO_FIELDSET_CONTENT"
>
<field
name="contentSource"
type="list"
label="MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC"
default="manual"
>
<option value="manual">MOD_MOKOJOOMHERO_SOURCE_MANUAL</option>
<option value="article">MOD_MOKOJOOMHERO_SOURCE_ARTICLE</option>
</field>
<field
name="heroContent"
type="editor"
@@ -177,7 +295,31 @@
filter="safehtml"
buttons="true"
hide="readmore,pagebreak"
showon="contentSource:manual"
/>
<field
name="articleId"
type="sql"
label="MOD_MOKOJOOMHERO_ARTICLE_LABEL"
description="MOD_MOKOJOOMHERO_ARTICLE_DESC"
query="SELECT id, title FROM #__content WHERE state = 1 ORDER BY title ASC"
key_field="id"
value_field="title"
header="MOD_MOKOJOOMHERO_ARTICLE_SELECT"
showon="contentSource:article"
/>
<field
name="useArticleTitle"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL"
description="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC"
default="0"
showon="contentSource:article"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="showCard"
type="radio"
@@ -189,6 +331,30 @@
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="contentAnimation"
type="list"
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC"
default="none"
>
<option value="none">MOD_MOKOJOOMHERO_ANIM_NONE</option>
<option value="fade-in">MOD_MOKOJOOMHERO_ANIM_FADE_IN</option>
<option value="slide-up">MOD_MOKOJOOMHERO_ANIM_SLIDE_UP</option>
<option value="slide-left">MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT</option>
<option value="slide-right">MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT</option>
</field>
<field
name="contentAnimationDelay"
type="number"
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC"
default="0"
min="0"
max="3000"
step="100"
showon="contentAnimation!:none"
/>
<field
name="cardDelay"
type="number"
@@ -211,6 +377,19 @@
description="MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC"
default="#000000"
/>
<field
name="overlayType"
type="list"
label="MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC"
default="solid"
>
<option value="solid">MOD_MOKOJOOMHERO_OVERLAY_SOLID</option>
<option value="gradient-bottom">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM</option>
<option value="gradient-top">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP</option>
<option value="gradient-left">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT</option>
<option value="gradient-right">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT</option>
</field>
<field
name="overlayOpacity"
type="range"
@@ -232,6 +411,17 @@
<option value="center">MOD_MOKOJOOMHERO_ALIGN_CENTER</option>
<option value="right">MOD_MOKOJOOMHERO_ALIGN_RIGHT</option>
</field>
<field
name="verticalAlign"
type="list"
label="MOD_MOKOJOOMHERO_VALIGN_LABEL"
description="MOD_MOKOJOOMHERO_VALIGN_DESC"
default="center"
>
<option value="top">MOD_MOKOJOOMHERO_VALIGN_TOP</option>
<option value="center">MOD_MOKOJOOMHERO_VALIGN_CENTER</option>
<option value="bottom">MOD_MOKOJOOMHERO_VALIGN_BOTTOM</option>
</field>
<field
name="textColor"
type="color"
+1
View File
@@ -6,6 +6,7 @@
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
+76 -13
View File
@@ -6,47 +6,89 @@
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/** @var string $heroMode */
/** @var array $heroImages */
/** @var int $slideInterval */
/** @var string $fadeType */
/** @var string $videoUrl */
/** @var string $youtubeId */
/** @var string $vimeoId */
/** @var string $heroHeight */
/** @var string $heroHeightMobile */
/** @var string $overlayColor */
/** @var string $overlayType */
/** @var float $overlayOpacity */
/** @var string $textAlign */
/** @var string $verticalAlign */
/** @var string $textColor */
/** @var string $contentSource */
/** @var int $articleId */
/** @var bool $useArticleTitle */
/** @var string $heroContent */
/** @var string $articleTitle */
/** @var array $slides */
/** @var bool $showCard */
/** @var int $cardDelay */
/** @var string $contentAnimation */
/** @var int $contentAnimationDelay */
/** @var bool $parallaxEnabled */
/** @var float $parallaxSpeed */
/** @var bool $showMuteToggle */
/** @var string $videoPoster */
/** @var bool $showScrollIndicator */
/** @var string $bgColor */
/** @var string $gradientStart */
/** @var string $gradientEnd */
/** @var int $gradientAngle */
/** @var string $content */
$moduleId = 'mod-mokojoomhero-' . $module->id;
// Convert hex overlay colour to rgba
$r = hexdec(substr($overlayColor, 1, 2));
$g = hexdec(substr($overlayColor, 3, 2));
$b = hexdec(substr($overlayColor, 5, 2));
$rgba = "rgba($r, $g, $b, $overlayOpacity)";
$rgbaOpaque = "rgba($r, $g, $b, $overlayOpacity)";
$rgbaTransparent = "rgba($r, $g, $b, 0)";
// Build overlay background based on type
$overlayDirections = [
'gradient-bottom' => 'to bottom',
'gradient-top' => 'to top',
'gradient-left' => 'to left',
'gradient-right' => 'to right',
];
if ($overlayType !== 'solid' && isset($overlayDirections[$overlayType])) {
$dir = $overlayDirections[$overlayType];
$overlayBg = "background: linear-gradient($dir, $rgbaTransparent, $rgbaOpaque);";
} else {
$overlayBg = "background-color: $rgbaOpaque;";
}
// Map vertical alignment to CSS align-items
$valignMap = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end'];
$valignCss = $valignMap[$verticalAlign] ?? 'center';
$heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
?>
<?php if ($heroHeightMobile) : ?>
<style>#<?php echo $moduleId; ?> { --mokojoomhero-mobile-height: <?php echo htmlspecialchars($heroHeightMobile, ENT_QUOTES, 'UTF-8'); ?>; }</style>
<?php endif; ?>
<div id="<?php echo $moduleId; ?>" class="mokojoomhero" style="height: <?php echo $heightAttr; ?>;"
<?php if ($parallaxEnabled) : ?>
data-parallax="<?php echo $parallaxSpeed; ?>"
<?php endif; ?>
<?php if ($heroMode === 'images' && count($heroImages) > 1) : ?>
data-slides="<?php echo htmlspecialchars(json_encode($heroImages), ENT_QUOTES, 'UTF-8'); ?>"
data-interval="<?php echo $slideInterval; ?>"
data-transition="<?php echo htmlspecialchars($fadeType, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($slides) : ?>
data-slide-content="<?php echo htmlspecialchars(json_encode($slides), ENT_QUOTES, 'UTF-8'); ?>"
<?php endif; ?>
<?php endif; ?>
>
<?php // Background layer — solid colour, single image, slideshow, or video ?>
@@ -55,11 +97,20 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
<?php elseif ($heroMode === 'gradient') : ?>
<div class="mokojoomhero__color" style="background: linear-gradient(<?php echo $gradientAngle; ?>deg, <?php echo htmlspecialchars($gradientStart, ENT_QUOTES, 'UTF-8'); ?>, <?php echo htmlspecialchars($gradientEnd, ENT_QUOTES, 'UTF-8'); ?>);"></div>
<?php elseif ($heroMode === 'video' && $youtubeId) : ?>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<iframe class="mokojoomhero__video" src="https://www.youtube-nocookie.com/embed/<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&mute=1&loop=1&playlist=<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&origin=<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root(), ENT_QUOTES, 'UTF-8'); ?>" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<?php elseif ($heroMode === 'video' && $vimeoId) : ?>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<iframe class="mokojoomhero__video" src="https://player.vimeo.com/video/<?php echo htmlspecialchars($vimeoId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&muted=1&loop=1&background=1" allow="autoplay" allowfullscreen></iframe>
<?php elseif (($heroMode === 'video' || $heroMode === 'localvideo') && $videoUrl) : ?>
<video class="mokojoomhero__video" autoplay muted loop playsinline>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<video class="mokojoomhero__video" autoplay muted loop playsinline<?php if ($videoPoster) : ?> poster="<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>"<?php endif; ?>>
<source src="<?php echo htmlspecialchars($videoUrl, ENT_QUOTES, 'UTF-8'); ?>">
</video>
<?php elseif ($heroImages) : ?>
@@ -77,20 +128,32 @@ $heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
</button>
<?php endif; ?>
<?php if ($showScrollIndicator) : ?>
<button class="mokojoomhero__scroll-indicator" type="button" aria-label="Scroll down">
<svg class="mokojoomhero__scroll-chevron" viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<?php endif; ?>
<?php // Overlay + content ?>
<div class="mokojoomhero__overlay" style="background-color: <?php echo $rgba; ?>;">
<div class="mokojoomhero__content" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;">
<?php if ($heroContent || $module->showtitle) : ?>
<div class="mokojoomhero__overlay" style="<?php echo $overlayBg; ?> align-items: <?php echo $valignCss; ?>;">
<div class="mokojoomhero__content<?php if ($contentAnimation !== 'none') : ?> mokojoomhero__content--anim-<?php echo htmlspecialchars($contentAnimation, ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;<?php if ($contentAnimationDelay) : ?> animation-delay: <?php echo $contentAnimationDelay; ?>ms;<?php endif; ?>">
<?php
$displayTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle)
? $articleTitle
: $module->title;
$showTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle) || $module->showtitle;
?>
<?php if ($heroContent || $showTitle) : ?>
<?php if ($showCard) : ?>
<div class="mokojoomhero__card"<?php if ($cardDelay) : ?> style="animation-delay: <?php echo $cardDelay; ?>ms;" data-card-delay="<?php echo $cardDelay; ?>"<?php endif; ?>>
<?php if ($module->showtitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php if ($showTitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
</div>
<?php else : ?>
<?php if ($module->showtitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php if ($showTitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
<?php endif; ?>
@@ -2,4 +2,4 @@
; SPDX-License-Identifier: GPL-3.0-or-later
PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license key validation"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero"
@@ -2,4 +2,4 @@
; SPDX-License-Identifier: GPL-3.0-or-later
PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license key validation"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero"
@@ -7,8 +7,8 @@
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_mokojoomhero</name>
<version>01.04.01-dev</version>
<name>PLG_SYSTEM_MOKOJOOMHERO</name>
<version>01.08.00-rc</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -25,10 +25,10 @@
<folder>src</folder>
</files>
<languages>
<language tag="en-GB">language/en-GB/plg_system_mokojoomhero.ini</language>
<language tag="en-GB">language/en-GB/plg_system_mokojoomhero.sys.ini</language>
<language tag="en-US">language/en-US/plg_system_mokojoomhero.ini</language>
<language tag="en-US">language/en-US/plg_system_mokojoomhero.sys.ini</language>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokojoomhero.ini</language>
<language tag="en-GB">en-GB/plg_system_mokojoomhero.sys.ini</language>
<language tag="en-US">en-US/plg_system_mokojoomhero.ini</language>
<language tag="en-US">en-US/plg_system_mokojoomhero.sys.ini</language>
</languages>
</extension>
@@ -13,7 +13,6 @@ namespace Joomla\Plugin\System\MokoJoomHero\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
@@ -21,70 +20,6 @@ class MokoJoomHero extends CMSPlugin implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
public function onAfterRoute(): void
{
$app = $this->getApplication();
if ($app->isClient('administrator')) {
$this->warnMissingLicenseKey();
}
}
/**
* Warn administrators once per session when no license key is configured.
*
* @return void
*/
private function warnMissingLicenseKey(): void
{
$session = Factory::getSession();
if ($session->get('mokojoomhero.license_warned', false)) {
return;
}
$user = Factory::getUser();
if ($user->guest || !$user->authorise('core.manage')) {
return;
}
$session->set('mokojoomhero.license_warned', true);
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('extra_query'))
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomHero Updates'))
->setLimit(1);
$db->setQuery($query);
$extraQuery = (string) $db->loadResult();
if (!empty($extraQuery)) {
parse_str($extraQuery, $parsed);
if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) {
return;
}
}
$this->getApplication()->enqueueMessage(
'<strong>Moko Consulting License Key Required</strong> — '
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System → Update Sites</a> '
. 'and enter your license key (<code>MOKO-XXXX-XXXX-XXXX-XXXX</code>) in the Download Key field '
. 'for the MokoJoomHero update site.',
'warning'
);
} catch (\Throwable $e) {
// Don't break admin over a license check
}
return [];
}
}
+2 -2
View File
@@ -8,14 +8,14 @@
<extension type="package" method="upgrade">
<name>Package - MokoJoomHero</name>
<packagename>mokojoomhero</packagename>
<version>01.04.01-dev</version>
<version>01.08.00-rc</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PKG_MOKOJOOMHERO_DESCRIPTION</description>
<description>Random hero image slideshow or background video with content overlay. Includes the hero module and system plugin. By Moko Consulting.</description>
<scriptfile>pkg_script.php</scriptfile>
+27 -11
View File
@@ -16,7 +16,7 @@ use Joomla\CMS\Installer\InstallerAdapter;
class Pkg_MokoJoomHeroInstallerScript
{
/**
* Called after install/update.
* Called after install/update — only enables the system plugin on fresh install.
*
* @param string $type Action type
* @param InstallerAdapter $parent Installer adapter
@@ -26,18 +26,34 @@ class Pkg_MokoJoomHeroInstallerScript
public function postflight(string $type, InstallerAdapter $parent): void
{
if ($type === 'install') {
$db = Factory::getDbo();
try {
$db = Factory::getDbo();
// Enable the system plugin automatically on fresh install
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero'));
// Enable the system plugin automatically on fresh install
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero'));
$db->setQuery($query);
$db->execute();
$db->setQuery($query);
$db->execute();
if ($db->getAffectedRows() === 0) {
Factory::getApplication()->enqueueMessage(
'MokoJoomHero: The system plugin could not be auto-enabled. '
. 'Please enable it manually in Extensions &rarr; Plugins.',
'warning'
);
}
} catch (\Exception $e) {
Factory::getApplication()->enqueueMessage(
'MokoJoomHero: Failed to auto-enable system plugin: ' . $e->getMessage()
. ' — Please enable it manually in Extensions &rarr; Plugins.',
'warning'
);
}
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 01.07.00
VERSION: 01.08.00
-->
<updates>