PageSourceSearch

https://www.hotelceilidh-donia.co.uk/bundles/eviivo/google-analyti…bvjFcvUDqJqowNDEpvTv8UqtDiKxFkJK0SqGjub3rA1

js hotelceilidh-donia.co.uk collected 2026-09-26 01:23:32 UTC 15,943 bytes, 364 lines download raw bytes

1/**
2 * Google Analytics 4 Ecommerce Tracking Helpers
3 * Namespace: window.eviivo.googleAnalytics
4 * 
5 * Provides consistent item object construction for client-side tracking.
6 * This helper ensures that Google Analytics 4 ecommerce items are tracked consistently
7 * across all events (view_item, view_item_list, select_item, add_to_cart, etc.)
8 */
9(function (window, $) {
10    'use strict';
11
12    // Initialize eviivo namespace
13    window.eviivo = window.eviivo || {};
14    window.eviivo.googleAnalytics = window.eviivo.googleAnalytics || {};
15
16    /**
17     * Helper function to safely decode HTML entities in a string value
18     * Uses eviivo.util.decodedHtml if available, falls back to raw string
19     * 
20     * @param {*} value - The value to decode (will be converted to string)
21     * @returns {string} The decoded string value, or original string if decoding unavailable
22     */
23    function decodeHtmlString(value) {
24        if (value === null || value === undefined) {
25            return '';
26        }
27        const stringValue = String(value);
28        return window.eviivo?.util?.decodedHtml 
29            ? window.eviivo.util.decodedHtml(stringValue) 
30            : stringValue;
31    }
32
33      /**
34       * Builds a Google Analytics 4 ecommerce item object with validation
35       * Constructs a standardized item object for GA4 event tracking
36       * 
37       * GA4 Item Fields (all items must have these):
38       * - item_id: Room type unique identifier
39       * - item_name: Room display name (HTML entities will be decoded)
40       * - item_brand: Property name (branding) (HTML entities will be decoded)
41       * - price: Room rate/price
42       * - quantity: Default 1 (qty booked)
43       * 
44        * Optional enrichment fields:
45        * - item_variant: Rate plan name (highly recommended for proper ecommerce tracking) (HTML entities will be decoded)
46        * - item_category: GA4 item category (e.g., 'Unit') (HTML entities will be decoded)
47        * - item_category2: GA4 secondary category (e.g., 'Vacation Rental') (HTML entities will be decoded)
48        * - index: Position in list (auto-set if not provided)
49        * - discount: Discount amount applied
50        * - affiliation: Referrer/channel attribution (HTML entities will be decoded)
51       * 
52       * HTML Entity Decoding:
53       * All string fields (itemName, itemBrand, itemVariant, itemCategory, itemCategory2, itemAffiliation) 
54       * are automatically decoded to convert HTML entities (', &, ", ô, etc.) to their actual characters.
55       * This is necessary because Razor templates HTML-encode data attributes for security, and GA4
56       * expects the decoded values. Decoding is only performed if eviivo.util.decodedHtml is available.
57       * 
58       * Note: Check-in/check-out dates belong at event level, not on items
59       * 
60       * @param {Object} options - Item properties
61       * @param {number} options.itemId - Required: Unique item identifier
62       * @param {string} options.itemName - Required: Item display name (will be decoded)
63       * @param {string} options.itemBrand - Required: Brand name (property name) (will be decoded)
64       * @param {number} options.price - Required: Item price
65        * @param {string} [options.itemVariant] - Optional but recommended: Item variant (rate plan name) (will be decoded)
66        * @param {number} [options.quantity=1] - Quantity
67        * @param {string} [options.itemCategory] - Optional: Item category (will be decoded)
68        * @param {string} [options.itemCategory2] - Optional: Item secondary category (will be decoded)
69        * @param {number} [options.discount] - Optional: Discount amount
70        * @param {number} [options.index] - Optional: Item position in list
71        * @param {string} [options.affiliation] - Optional: Affiliation (referrer/channel) (will be decoded)
72        * @param {string} [options.itemAffiliation] - Optional: Point of sale name (will be decoded)
73       * @returns {Object|null} GA4 item object or null if validation fails
74       */
75       function buildItemObject(options) {
76          // Validate core required fields (id, name, brand are mandatory)
77          // item_variant is highly recommended but optional due to UI limitations in non-search mode
78          const { itemId, itemName, itemBrand, itemVariant, ...rest } = options || {};
79
80          if (!itemId || !itemName || !itemBrand) {
81              console.warn('GA4 Helper: Missing required fields. itemId, itemName, and itemBrand are required for GA4 tracking.');
82              return null;
83          }
84
85          // Log warning if item_variant is missing - it should be populated for proper ecommerce tracking
86          if (!itemVariant) {
87              console.warn('GA4 Helper: itemVariant (rate plan name) is missing - GA4 ecommerce tracking will have incomplete item data. This may occur in non-search modes where rate plan data is unavailable.');
88          }
89
90          // Decode all HTML entities from string fields
91          // Razor automatically encodes special characters in data attributes for security (XSS prevention)
92          // We decode here before sending to GA4 to ensure proper data representation
93          const item = {
94              item_id: Number(itemId),
95              item_name: decodeHtmlString(itemName),
96              item_brand: decodeHtmlString(itemBrand),
97              price: Number(rest.price) || 0,
98              quantity: Number(rest.quantity) || 1
99          };
100
101          // Add item_variant only if provided (optional in UI tracking, but highly recommended)
102          if (itemVariant) {
103              item.item_variant = decodeHtmlString(itemVariant);
104          }
105
106           if (rest.itemCategory) {
107               item.item_category = decodeHtmlString(rest.itemCategory);
108           }
109
110           if (rest.itemCategory2) {
111               item.item_category2 = decodeHtmlString(rest.itemCategory2);
112           }
113
114          if (rest.discount && Number(rest.discount) > 0) {
115             item.discount = Number(rest.discount);
116         }
117
118         if (typeof rest.index !== 'undefined' && rest.index !== null) {
119             item.index = Number(rest.index);
120         }
121
122          if (rest.itemAffiliation) {
123              item.affiliation = decodeHtmlString(rest.itemAffiliation);
124          }
125
126          return item;
127      }
128
129      /**
130       * Builds a GA4 item object from DOM data attributes
131       * Used for client-side event tracking (click events on room selection buttons)
132       * 
133        * Expected DOM attributes on element:
134        * - data-room-type-id: Room type ID (required)
135        * - data-item-name: Room name (required) - will be HTML-decoded
136        * - data-rate-plan-name: Rate plan variant (recommended) - will be HTML-decoded
137        * - data-price: Room price (required)
138        * - data-discount: Discount amount (optional)
139        * 
140        * HTML entities in data attributes (e.g., ', &, ") are automatically decoded
141        * before sending to GA4, since Razor encodes these for HTML security.
142        * 
143        * @param {jQuery} $element - Element with data attributes (room button/link)
144        * @param {Object} contextData - Additional context from page
145        * @param {string} contextData.propertyName - Required: Property name (used for item_brand) - will be HTML-decoded
146        * @param {string} [contextData.itemCategory] - Optional: Item category - will be HTML-decoded
147        * @param {string} [contextData.itemCategory2] - Optional: Item secondary category - will be HTML-decoded
148        * @param {string} [contextData.affiliation] - Optional: Affiliation (referrer/channel)
149        * @param {string} [contextData.itemAffiliation] - Optional: Point of sale name
150       * @returns {Object|null} GA4 item object or null if required data is missing
151       */
152     function buildItemFromDOM($element, contextData) {
153        if (!$element?.length) {
154            console.warn('GA4 Helper: Invalid element provided to buildItemFromDOM');
155            return null;
156        }
157
158        if (!contextData?.propertyName) {
159            console.warn('GA4 Helper: Missing propertyName in contextData - required for item_brand in GA4 events');
160            return null;
161        }
162
163        // Extract DOM data in single operation
164        const roomTypeId = $element.data('room-type-id');
165        const itemName = $element.data('item-name');
166        const ratePlanName = $element.data('rate-plan-name');
167        const price = $element.data('price');
168        const discount = $element.data('discount');
169
170        // Validate core required properties
171        if (!roomTypeId || !itemName) {
172            console.warn('GA4 Helper: Missing required DOM attributes for item');
173            return null;
174        }
175
176        // Log warning if rate plan name is missing, but don't block the event
177        if (!ratePlanName) {
178            console.warn('GA4 Helper: Missing data-rate-plan-name attribute - item_variant will be empty. This may occur in non-search modes or when rate plan data is unavailable.');
179        }
180
181         return buildItemObject({
182             itemId: roomTypeId,
183             itemName: itemName,
184             itemBrand: contextData.propertyName,
185             itemVariant: ratePlanName,
186             price: price,
187             discount: discount,
188             itemCategory: contextData.itemCategory,
189             itemCategory2: contextData.itemCategory2,
190             affiliation: contextData.affiliation,
191             itemAffiliation: contextData.itemAffiliation
192         });
193    }
194
195     /**
196      * Validates that gtag function is available before tracking
197      * Logs warning if gtag is not defined, which indicates GA4 initialization issue
198      * 
199      * @returns {boolean} True if gtag function is available, false otherwise
200      */
201     function isGtagAvailable() {
202         if (typeof gtag === 'undefined') {
203             console.warn('GA4 Helper: gtag is not defined - GA4 tracking will not work');
204             return false;
205         }
206         return true;
207     }
208
209     /**
210      * Safely sends a Google Analytics 4 event to gtag
211      * Includes error handling and gtag availability check
212      * 
213      * @param {string} eventName - GA4 event name (e.g., "view_item", "purchase")
214      * @param {Object} eventParams - Event parameters (items array, currency, value, etc.)
215      * @returns {void}
216      */
217     function sendEvent(eventName, eventParams) {
218         if (!window.eviivo.googleAnalytics.isGtagAvailable()) {
219             return;
220         }
221
222        try {
223            gtag('event', eventName, eventParams);
224        } catch (error) {
225            console.error(`GA4 Helper: Error sending event "${eventName}":`, error);
226        }
227    }
228
229    window.eviivo.googleAnalytics.buildItemObject = buildItemObject;
230    window.eviivo.googleAnalytics.buildItemFromDOM = buildItemFromDOM;
231    window.eviivo.googleAnalytics.isGtagAvailable = isGtagAvailable;
232    window.eviivo.googleAnalytics.sendEvent = sendEvent;
233
234})(window, jQuery);
235
236/**
237 * Google Analytics 4 Event Wiring Engine
238 * Namespace: window.eviivo.googleAnalytics.events
239 * 
240 * Auto-discovers DOM elements with Google Analytics 4 event data attributes and wires event listeners
241 */
242(function (window, $) {
243    'use strict';
244
245    // Initialize eviivo namespace
246    window.eviivo = window.eviivo || {};
247    window.eviivo.googleAnalytics = window.eviivo.googleAnalytics || {};
248    window.eviivo.googleAnalytics.events = window.eviivo.googleAnalytics.events || {};
249
250    var options;
251
252    const eventDefinitions = {
253        'view_item': {
254            gaEventName: 'view_item',
255            selector: '[data-ga4-event="view_item"]',
256            handler: handleViewItemEvent,
257        },
258        // Future events can be added here like the one above
259    };
260
261      /**
262       * Handles 'view_item' event for Google Analytics 4
263       * Fired when user clicks "More..." button to view unit details in results
264       * 
265       * GA4 Event: view_item
266       * Purpose: Track which specific unit item user is interested in
267       * Includes: Unit details, currency, price, check-in/out dates
268       * 
269       * @param {Event} e - Click event from view details button
270       * @returns {void}
271       */
272     function handleViewItemEvent(e) {
273         if (!window.eviivo?.googleAnalytics?.isGtagAvailable?.()) {
274             return;
275         }
276
277         const $button = $(this);
278         const { propertyName, propertyType, propertyShortName, pointOfSaleName, constants, checkInDate, checkOutDate, singularRoomTypeTranslation } = options || {};
279
280         const contextData = {
281             itemCategory: singularRoomTypeTranslation ?? constants?.unit,
282             itemCategory2: propertyType ?? constants?.accommodation,
283             itemAffiliation: pointOfSaleName,
284             propertyName: propertyName,
285             checkInDate: checkInDate,
286             checkOutDate: checkOutDate
287         };
288
289         // Build Google Analytics 4 item from DOM attributes
290         const itemData = window.eviivo.googleAnalytics.buildItemFromDOM($button, contextData);
291
292         if (!itemData) {
293             console.warn('GA4 Event Wiring: Could not build item data for view_item event');
294             return;
295         }
296
297         // Resolve item_list_id and item_list_name from constants or use defaults
298         const roomListId = constants?.room_list ?? 'room_list';
299         const roomListName = constants?.available_rooms ?? 'Available Rooms';
300
301          // Send Google Analytics 4 event
302          const eventData = {
303              currency: $button.data('currency-code'),
304              value: itemData.price,
305              item_list_id: roomListId,
306              item_list_name: roomListName,
307              items: [itemData]
308          };
309
310          if (checkInDate?.trim?.()) {
311              eventData.check_in_date = checkInDate;
312          }
313
314          if (checkOutDate?.trim?.()) {
315              eventData.check_out_date = checkOutDate;
316          }
317
318          window.eviivo.googleAnalytics.sendEvent('view_item', eventData);
319     }
320
321    /**
322     * Generic click event handler dispatcher
323     * Routes clicks to the appropriate event handler based on event definition
324     * @param {Event} e - Click event
325     */
326    function onGoogleAnalyticsEventClick(e) {
327        const $element = $(this);
328
329        const eventType = $element.data('ga4-event');
330        const definition = eventDefinitions[eventType];
331        const { handler } = definition ?? {};
332
333        if (!handler) {
334            console.warn(`GA4 Event Wiring: No handler found for event type: ${eventType}`);
335            return;
336        }
337
338        // Call the event-specific handler
339        handler.call(this, e);
340    }
341
342    /**
343     * Initialize Google Analytics 4 event wiring
344     * Auto-discovers all elements with Google Analytics 4 event data attributes and attaches listeners
345     * Should be called after DOM is ready and Google Analytics 4 helpers are loaded
346     */
347    function init(customOptions) {
348        if (!window.eviivo?.googleAnalytics) {
349            console.warn('GA4 Event Wiring: eviivo.googleAnalytics helpers not found. Make sure eviivo.googleAnalytics.helpers.js is loaded first.');
350            return;
351        }
352
353        options = $.extend({}, options, customOptions);
354
355        // Attach event listeners for each defined event
356        Object.entries(eventDefinitions).forEach(([, definition]) => {
357            $(document).on('click', definition.selector, onGoogleAnalyticsEventClick);
358        });
359    }
360
361    window.eviivo.googleAnalytics.events.init = init;
362
363})(window, jQuery);
364

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.