Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import "@hotwired/turbo-rails"
import "controllers"
import "loading_spinner"
import "matomo_events"

// Show the progress bar after 200 milliseconds, not the default 500
Turbo.config.drive.progressBarDelay = 200;
10 changes: 9 additions & 1 deletion app/javascript/filters.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { trackFilterChange } from 'matomo_events'

// These elements aren't loaded with the initial DOM, they appear later.
function initFilterToggle() {
var filter_toggle = document.getElementById('filter-toggle');
Expand All @@ -9,7 +11,13 @@ function initFilterToggle() {
});
[...filter_categories].forEach(element => {
element.addEventListener('click', event => {
element.getElementsByClassName('filter-label')[0].classList.toggle('expanded');
const label = element.getElementsByClassName('filter-label')[0];
label.classList.toggle('expanded');

// Track filter category expansion
const filterName = label ? label.textContent.trim() : 'unknown';
const isExpanded = label.classList.contains('expanded');
trackFilterChange(filterName, 'category', isExpanded ? 'expanded' : 'collapsed');
});
});
}
Expand Down
9 changes: 9 additions & 0 deletions app/javascript/loading_spinner.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { trackPagination } from 'matomo_events'

// Update the tab UI to reflect the newly-requested state. This function is called
// by a click event handler in the tab UI. It follows a two-step process:
function swapTabs(new_target) {
Expand Down Expand Up @@ -56,6 +58,13 @@ document.addEventListener('click', function(event) {

// Handle pagination clicks
if (clickedElement.matches('.first a, .previous a, .next a')) {
// Track pagination
const urlParams = new URLSearchParams(clickedElement.href);
const pageParam = urlParams.get('page');
if (pageParam) {
trackPagination(parseInt(pageParam));
}

// Throw the spinner on the search results immediately
document.getElementById('search-results').classList.add('spinner');

Expand Down
145 changes: 145 additions & 0 deletions app/javascript/matomo_events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Matomo Tag Manager Event Dispatcher
*
* Pushes custom events to window._mtm for Tag Manager to capture.
* This module automatically tracks page views on turbo:load (history changes)
* and provides helper functions for tracking custom interactions.
*
* Ensures _mtm queue exists and safely pushes event objects for Tag Manager processing.
*/

// Ensure _mtm queue exists
window._mtm = window._mtm || [];

/**
* Push a custom event object to Matomo Tag Manager queue
* @param {Object} eventData - Event object with custom properties (e.g., {event: 'search_submitted', query: 'test'})
*/
function pushEvent(eventData) {
if (!window._mtm) {
console.warn('Matomo Tag Manager (_mtm) not available');
return;
}
window._mtm.push(eventData);
}

/**
* Track a page view when URL/history changes (for SPA navigation)
* Pushes page metadata to Tag Manager for processing
* @param {string} pageUrl - Current page URL (defaults to window.location.href)
* @param {string} pageTitle - Page title (defaults to document.title)
*/
export function trackPageView(pageUrl = window.location.href, pageTitle = document.title) {
pushEvent({
'event': 'page_view',
'page_url': pageUrl,
'page_path': new URL(pageUrl).pathname + new URL(pageUrl).search,
'page_title': pageTitle,
'timestamp': new Date().toISOString()
});
}

/**
* Track a search submission
* @param {string} query - Search query text
* @param {Object} options - Additional event properties (searchType, filters, etc.)
*/
export function trackSearch(query, options = {}) {
pushEvent({
'event': 'search_submitted',
'search_query': query,
'search_type': options.searchType || 'keyword',
'timestamp': new Date().toISOString(),
...options
});
}

/**
* Track a filter change/interaction
* @param {string} filterName - Name of the filter (e.g., 'language', 'content_type')
* @param {string|Array} filterValue - Selected filter value(s)
* @param {string} action - Action type ('applied', 'removed', 'changed')
*/
export function trackFilterChange(filterName, filterValue, action = 'applied') {
pushEvent({
'event': 'filter_interaction',
'filter_name': filterName,
'filter_value': Array.isArray(filterValue) ? filterValue.join(',') : filterValue,
'filter_action': action,
'timestamp': new Date().toISOString()
});
}

/**
* Track a tab/source selection change
* @param {string} tabName - Tab name (e.g., 'primo', 'timdex', 'all')
*/
export function trackTabChange(tabName) {
pushEvent({
'event': 'tab_selected',
'tab_name': tabName,
'timestamp': new Date().toISOString()
});
}

/**
* Track pagination interaction
* @param {number} pageNumber - Page number selected
* @param {Object} options - Additional properties (per_page, total_results, etc.)
*/
export function trackPagination(pageNumber, options = {}) {
pushEvent({
'event': 'pagination',
'page_number': pageNumber,
'timestamp': new Date().toISOString(),
...options
});
}

/**
* Track a custom interaction
* @param {string} eventName - Name of the event
* @param {Object} eventData - Custom event properties
*/
export function trackCustomEvent(eventName, eventData = {}) {
pushEvent({
'event': eventName,
'timestamp': new Date().toISOString(),
...eventData
});
}

/**
* Signal to Tag Manager that DOM content has been updated and Element Visibility conditions should be re-evaluated
* This is crucial for Matomo Tag Manager's Element Visibility triggers to work on page navigation without refresh
* Tag Manager creates Element Visibility triggers that only evaluate on initial page load—calling this
* after turbo:load tells Tag Manager to re-check visibility conditions for elements that may have appeared/disappeared
* @see https://matomo.org/guide/matomo-tag-manager/element-visibility/
*/
export function notifyDOMUpdated() {
pushEvent({
'event': 'dom_updated',
'timestamp': new Date().toISOString()
});
}

/**
* Listen for Turbo navigation and track page views automatically
* This allows Tag Manager to track history changes (page views without full page refresh)
*/
function initTurboTracking() {
let previousPageUrl = null;

document.addEventListener('turbo:load', function(event) {
// Only track if URL actually changed (not the initial page load)
if (previousPageUrl && previousPageUrl !== window.location.href) {
trackPageView(window.location.href, document.title);
// Signal to Tag Manager that DOM has been updated so Element Visibility triggers are re-evaluated
notifyDOMUpdated();
}
previousPageUrl = window.location.href;
});
}

// Initialize automatic Turbo tracking when module loads
initTurboTracking();
32 changes: 32 additions & 0 deletions app/javascript/search_form.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { trackSearch, trackCustomEvent } from 'matomo_events'

var keywordField = document.getElementById('basic-search-main');
var advancedPanel = document.getElementById('advanced-search-panel');
var geoboxPanel = document.getElementById('geobox-search-panel');
Expand Down Expand Up @@ -61,20 +63,50 @@ function updateKeywordPlaceholder() {
// panel. In all other TIMDEX UI apps, it's just the advanced panel.
if (Array.from(allPanels).includes(geoboxPanel && geodistancePanel)) {
document.getElementById('geobox-summary').addEventListener('click', () => {
trackCustomEvent('search_panel_toggled', { panel_type: 'geobox' });
togglePanelState(geoboxPanel);
});

document.getElementById('geodistance-summary').addEventListener('click', () => {
trackCustomEvent('search_panel_toggled', { panel_type: 'geodistance' });
togglePanelState(geodistancePanel);
});

document.getElementById('advanced-summary').addEventListener('click', () => {
trackCustomEvent('search_panel_toggled', { panel_type: 'advanced' });
togglePanelState(advancedPanel);
});
} else {
document.getElementById('advanced-summary').addEventListener('click', () => {
trackCustomEvent('search_panel_toggled', { panel_type: 'advanced' });
togglePanelState(advancedPanel);
});
}

// Track form submission
const searchForm = document.querySelector('form[data-turbo-confirm], form[action*="/results"]');
if (searchForm) {
searchForm.addEventListener('submit', (e) => {
const query = keywordField.value;
const searchType = determineSearchType();

if (query) {
trackSearch(query, {
searchType: searchType,
advanced_search: advancedPanel.open,
geobox_search: geoboxPanel ? geoboxPanel.open : false,
geodistance_search: geodistancePanel ? geodistancePanel.open : false
});
}
});
}

// Helper function to determine which search type is active
function determineSearchType() {
if (geoboxPanel && geoboxPanel.open) return 'geobox';
if (geodistancePanel && geodistancePanel.open) return 'geodistance';
if (advancedPanel && advancedPanel.open) return 'advanced';
return 'keyword';
}

console.log('search_form.js loaded');
13 changes: 13 additions & 0 deletions app/javascript/source_tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Source: https://css-tricks.com/container-adapting-tabs-with-more-button/
// ===========================================================================

import { trackTabChange } from 'matomo_events'

// Store references to relevant selectors
const container = document.querySelector('#tabs')
const primary = container.querySelector('.primary')
Expand Down Expand Up @@ -35,6 +37,17 @@ moreBtn.addEventListener('click', (e) => {
moreBtn.setAttribute('aria-expanded', container.classList.contains('--show-secondary'))
})

// Track tab selection when a tab link is clicked
container.addEventListener('click', (e) => {
const tabLink = e.target.closest('a[href*="tab="]')
if (tabLink) {
const tabMatch = tabLink.href.match(/tab=([^&]*)/)
if (tabMatch) {
trackTabChange(tabMatch[1])
}
}
})

// adapt tabs
const doAdapt = () => {

Expand Down
10 changes: 10 additions & 0 deletions app/views/layouts/_head.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='<%= ENV['MATOMO_CONTAINER_URL'] %>'; s.parentNode.insertBefore(g,s);
})();

// Track initial page view for Tag Manager
// (matomo_events.js will track subsequent page views on turbo:load)
_mtm.push({
'event': 'page_view',
'page_url': window.location.href,
'page_path': window.location.pathname + window.location.search,
'page_title': document.title,
'timestamp': new Date().toISOString()
});
</script>
<!-- End Matomo Tag Manager -->
<% elsif (ENV['MATOMO_URL'].present? && ENV['MATOMO_SITE_ID'].present?) %>
Expand Down
1 change: 1 addition & 0 deletions config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pin "application", preload: true
pin "loading_spinner", preload: true
pin "matomo_events", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
Expand Down
Loading
Loading