feat: collapsible floating social bar, tooltips, and URL validation #141

Merged
jmiller merged 1 commits from feature/social-floating-enhancements into dev 2026-06-21 01:25:25 +00:00
7 changed files with 172 additions and 41 deletions
+5
View File
@@ -16,6 +16,11 @@
## [Unreleased]
### Added
- Collapsible floating social bar with toggle button and localStorage persistence
- Bootstrap tooltips on all social icon links showing platform name on hover
- URL validation on social platform fields (browser, Joomla server-side, and PHP layout logging)
### Fixed
- Moved footer social icons below footer-menu and footer module positions for correct display order
+33 -7
View File
@@ -52,13 +52,22 @@ $platforms = [
$active = [];
foreach ($platforms as $key => [$iconClass, $langKey]) {
$url = trim((string) $params->get('social_' . $key . '_url', ''));
if ($url !== '' && preg_match('#^(https?://|mailto:|/)#i', $url)) {
$active[] = [
'url' => $url,
'iconClass' => $iconClass,
'label' => Text::_($langKey),
];
if ($url === '') {
continue;
}
if (!preg_match('#^(https?://[^\s<>"]+|mailto:[^\s<>"]+|/[^\s<>"]*)$#i', $url)) {
\Joomla\CMS\Log\Log::add(
'MokoOnyx social: skipped invalid URL for "' . $key . '": ' . $url,
\Joomla\CMS\Log\Log::WARNING,
'template'
);
continue;
}
$active[] = [
'url' => $url,
'iconClass' => $iconClass,
'label' => Text::_($langKey),
];
}
if (empty($active)) {
@@ -84,6 +93,10 @@ if ($position === 'floating') {
$listClass .= ' moko-social-icons--floating-' . $floatingPos;
}
?>
<?php if ($position === 'floating') : ?>
<div class="moko-social-floating-wrap moko-social-floating-wrap--<?php echo htmlspecialchars($floatingPos, ENT_QUOTES, 'UTF-8'); ?>"
id="mokoSocialFloating">
<?php endif; ?>
<nav class="<?php echo $listClass; ?>" aria-label="<?php echo Text::_('TPL_MOKOONYX_SOCIAL_NAV_LABEL'); ?>">
<ul>
<?php foreach ($active as $item) : ?>
@@ -91,10 +104,23 @@ if ($position === 'floating') {
<a href="<?php echo htmlspecialchars($item['url'], ENT_QUOTES, 'UTF-8'); ?>"
target="_blank"
rel="noopener noreferrer"
aria-label="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>">
aria-label="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>"
data-bs-toggle="tooltip"
data-bs-placement="<?php echo $position === 'floating' ? ($floatingPos === 'left' ? 'right' : 'left') : 'top'; ?>"
title="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>">
<span class="<?php echo htmlspecialchars($item['iconClass'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true"></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
<?php if ($position === 'floating') : ?>
<button type="button"
class="moko-social-floating-toggle"
id="mokoSocialFloatingToggle"
aria-label="<?php echo Text::_('TPL_MOKOONYX_SOCIAL_FLOATING_TOGGLE'); ?>"
aria-expanded="true">
<span class="fa-solid fa-chevron-<?php echo $floatingPos === 'left' ? 'left' : 'right'; ?>" aria-hidden="true"></span>
</button>
</div>
<?php endif; ?>
+2 -1
View File
@@ -286,6 +286,7 @@ TPL_MOKOONYX_SOCIAL_FLOATING_POS_LABEL="Floating Position"
TPL_MOKOONYX_SOCIAL_FLOATING_POS_DESC="Which side of the screen the floating social bar appears on."
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LEFT="Left"
TPL_MOKOONYX_SOCIAL_FLOATING_POS_RIGHT="Right"
TPL_MOKOONYX_SOCIAL_FLOATING_TOGGLE="Toggle social icons sidebar"
TPL_MOKOONYX_SOCIAL_COLOR_LABEL="Icon Colour"
TPL_MOKOONYX_SOCIAL_COLOR_DESC="Choose the colour scheme for social icons."
TPL_MOKOONYX_SOCIAL_COLOR_THEME="Theme (CSS variables)"
@@ -321,7 +322,7 @@ TPL_MOKOONYX_SOCIAL_MAIL="Email us"
; ===== CSS Variables tab (social) =====
TPL_MOKOONYX_CSS_VARS_SOCIAL_LABEL="Social Icons"
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon colour (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover colour (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)"
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon colour (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover colour (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)<br><br><strong>Floating bar</strong><br>The collapse toggle button inherits <code>--social-icon-bg</code>, <code>--social-icon-color</code>, and <code>--social-icon-hover-bg</code>. Tooltips appear automatically on hover showing the platform name."
; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
+2 -1
View File
@@ -286,6 +286,7 @@ TPL_MOKOONYX_SOCIAL_FLOATING_POS_LABEL="Floating Position"
TPL_MOKOONYX_SOCIAL_FLOATING_POS_DESC="Which side of the screen the floating social bar appears on."
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LEFT="Left"
TPL_MOKOONYX_SOCIAL_FLOATING_POS_RIGHT="Right"
TPL_MOKOONYX_SOCIAL_FLOATING_TOGGLE="Toggle social icons sidebar"
TPL_MOKOONYX_SOCIAL_COLOR_LABEL="Icon Color"
TPL_MOKOONYX_SOCIAL_COLOR_DESC="Choose the color scheme for social icons."
TPL_MOKOONYX_SOCIAL_COLOR_THEME="Theme (CSS variables)"
@@ -321,7 +322,7 @@ TPL_MOKOONYX_SOCIAL_MAIL="Email us"
; ===== CSS Variables tab (social) =====
TPL_MOKOONYX_CSS_VARS_SOCIAL_LABEL="Social Icons"
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon color (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover color (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)"
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon color (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover color (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)<br><br><strong>Floating bar</strong><br>The collapse toggle button inherits <code>--social-icon-bg</code>, <code>--social-icon-color</code>, and <code>--social-icon-hover-bg</code>. Tooltips appear automatically on hover showing the platform name."
; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
+66 -10
View File
@@ -23679,26 +23679,42 @@ font-size: 0.8125rem;
}
/* Position: floating — fixed sidebar, hidden on mobile */
.moko-social-icons--floating {
.moko-social-floating-wrap {
position: fixed;
top: 50%;
transform: translateY(-50%);
z-index: 1030;
display: flex;
align-items: center;
transition: transform 0.3s ease;
}
.moko-social-floating-wrap--left {
left: 0;
flex-direction: row;
}
.moko-social-floating-wrap--right {
right: 0;
flex-direction: row-reverse;
}
.moko-social-floating-wrap--left.is-collapsed {
transform: translateY(-50%) translateX(calc(-100% + 1.5rem));
}
.moko-social-floating-wrap--right.is-collapsed {
transform: translateY(-50%) translateX(calc(100% - 1.5rem));
}
.moko-social-icons--floating {
display: flex;
}
.moko-social-icons--floating ul {
flex-direction: column;
}
.moko-social-icons--floating-left {
left: 0;
}
.moko-social-icons--floating-right {
right: 0;
}
.moko-social-icons--floating-left a {
border-radius: 0 var(--social-icon-radius, 0.375rem) var(--social-icon-radius, 0.375rem) 0;
}
@@ -23712,8 +23728,48 @@ font-size: 0.8125rem;
border-radius: inherit;
}
/* Floating toggle button */
.moko-social-floating-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 2.5rem;
padding: 0;
border: 1px solid var(--social-icon-bg, var(--border-color, hsl(0, 0%, 80%)));
background: var(--social-icon-bg, var(--body-bg, hsl(0, 0%, 100%)));
color: var(--social-icon-color, currentColor);
cursor: pointer;
font-size: 0.65rem;
transition: background 0.2s, color 0.2s;
flex-shrink: 0;
}
.moko-social-floating-wrap--left .moko-social-floating-toggle {
border-radius: 0 var(--social-icon-radius, 0.375rem) var(--social-icon-radius, 0.375rem) 0;
border-left: 0;
}
.moko-social-floating-wrap--right .moko-social-floating-toggle {
border-radius: var(--social-icon-radius, 0.375rem) 0 0 var(--social-icon-radius, 0.375rem);
border-right: 0;
}
.moko-social-floating-toggle:hover {
background: var(--social-icon-hover-bg, var(--accent-color-primary, hsl(220, 70%, 50%)));
color: #fff;
}
.moko-social-floating-toggle .fa-solid {
transition: transform 0.3s ease;
}
.is-collapsed .moko-social-floating-toggle .fa-solid {
transform: rotate(180deg);
}
@media (max-width: 991.98px) {
.moko-social-icons--floating {
.moko-social-floating-wrap {
display: none;
}
}
+42
View File
@@ -789,6 +789,46 @@
});
}
// ========================================================================
// FLOATING SOCIAL BAR (collapsible)
// ========================================================================
var socialStorageKey = "moko-social-collapsed";
function initSocialFloating() {
var wrap = doc.getElementById("mokoSocialFloating");
var toggle = doc.getElementById("mokoSocialFloatingToggle");
if (!wrap || !toggle) return;
// Restore saved state
try {
if (localStorage.getItem(socialStorageKey) === "1") {
wrap.classList.add("is-collapsed");
toggle.setAttribute("aria-expanded", "false");
}
} catch (e) {}
toggle.addEventListener("click", function () {
var collapsed = wrap.classList.toggle("is-collapsed");
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
try { localStorage.setItem(socialStorageKey, collapsed ? "1" : "0"); } catch (e) {}
});
}
// ========================================================================
// SOCIAL ICON TOOLTIPS
// ========================================================================
function initSocialTooltips() {
var triggers = doc.querySelectorAll('.moko-social-icons [data-bs-toggle="tooltip"]');
if (!triggers.length) return;
// Bootstrap 5 tooltip init
if (typeof bootstrap !== "undefined" && bootstrap.Tooltip) {
triggers.forEach(function (el) {
new bootstrap.Tooltip(el, { trigger: "hover focus" });
});
}
}
/**
* Run all template JS initializations
*/
@@ -815,6 +855,8 @@
initBackTop();
initSearchToggle();
initSidebarAccordion();
initSocialFloating();
initSocialTooltips();
initVarCopy();
}
+22 -22
View File
@@ -387,28 +387,28 @@
showon="social_topbar:1[OR]social_footer:1[OR]social_floating:1" />
<field name="social_urls_note" type="note" description="TPL_MOKOONYX_SOCIAL_URLS_NOTE"
showon="social_topbar:1[OR]social_footer:1[OR]social_floating:1" />
<field name="social_facebook_url" type="url" default="" label="Facebook" filter="url" />
<field name="social_twitter_url" type="url" default="" label="X / Twitter" filter="url" />
<field name="social_instagram_url" type="url" default="" label="Instagram" filter="url" />
<field name="social_linkedin_url" type="url" default="" label="LinkedIn" filter="url" />
<field name="social_youtube_url" type="url" default="" label="YouTube" filter="url" />
<field name="social_github_url" type="url" default="" label="GitHub" filter="url" />
<field name="social_bluesky_url" type="url" default="" label="Bluesky" filter="url" />
<field name="social_threads_url" type="url" default="" label="Threads" filter="url" />
<field name="social_discord_url" type="url" default="" label="Discord" filter="url" />
<field name="social_tiktok_url" type="url" default="" label="TikTok" filter="url" />
<field name="social_reddit_url" type="url" default="" label="Reddit" filter="url" />
<field name="social_pinterest_url" type="url" default="" label="Pinterest" filter="url" />
<field name="social_snapchat_url" type="url" default="" label="Snapchat" filter="url" />
<field name="social_telegram_url" type="url" default="" label="Telegram" filter="url" />
<field name="social_whatsapp_url" type="url" default="" label="WhatsApp" filter="url" />
<field name="social_tumblr_url" type="url" default="" label="Tumblr" filter="url" />
<field name="social_twitch_url" type="url" default="" label="Twitch" filter="url" />
<field name="social_spotify_url" type="url" default="" label="Spotify" filter="url" />
<field name="social_soundcloud_url" type="url" default="" label="SoundCloud" filter="url" />
<field name="social_flickr_url" type="url" default="" label="Flickr" filter="url" />
<field name="social_vimeo_url" type="url" default="" label="Vimeo" filter="url" />
<field name="social_linktree_url" type="url" default="" label="Linktree" filter="url" />
<field name="social_facebook_url" type="url" default="" label="Facebook" filter="url" validate="url" />
<field name="social_twitter_url" type="url" default="" label="X / Twitter" filter="url" validate="url" />
<field name="social_instagram_url" type="url" default="" label="Instagram" filter="url" validate="url" />
<field name="social_linkedin_url" type="url" default="" label="LinkedIn" filter="url" validate="url" />
<field name="social_youtube_url" type="url" default="" label="YouTube" filter="url" validate="url" />
<field name="social_github_url" type="url" default="" label="GitHub" filter="url" validate="url" />
<field name="social_bluesky_url" type="url" default="" label="Bluesky" filter="url" validate="url" />
<field name="social_threads_url" type="url" default="" label="Threads" filter="url" validate="url" />
<field name="social_discord_url" type="url" default="" label="Discord" filter="url" validate="url" />
<field name="social_tiktok_url" type="url" default="" label="TikTok" filter="url" validate="url" />
<field name="social_reddit_url" type="url" default="" label="Reddit" filter="url" validate="url" />
<field name="social_pinterest_url" type="url" default="" label="Pinterest" filter="url" validate="url" />
<field name="social_snapchat_url" type="url" default="" label="Snapchat" filter="url" validate="url" />
<field name="social_telegram_url" type="url" default="" label="Telegram" filter="url" validate="url" />
<field name="social_whatsapp_url" type="url" default="" label="WhatsApp" filter="url" validate="url" />
<field name="social_tumblr_url" type="url" default="" label="Tumblr" filter="url" validate="url" />
<field name="social_twitch_url" type="url" default="" label="Twitch" filter="url" validate="url" />
<field name="social_spotify_url" type="url" default="" label="Spotify" filter="url" validate="url" />
<field name="social_soundcloud_url" type="url" default="" label="SoundCloud" filter="url" validate="url" />
<field name="social_flickr_url" type="url" default="" label="Flickr" filter="url" validate="url" />
<field name="social_vimeo_url" type="url" default="" label="Vimeo" filter="url" validate="url" />
<field name="social_linktree_url" type="url" default="" label="Linktree" filter="url" validate="url" />
<field name="social_mail_url" type="url" default="" label="Email" description="TPL_MOKOONYX_SOCIAL_MAIL_DESC" filter="string" />
</fieldset>