UNPKG

@financial-times/o-tracking

Version:

Provides tracking for a product. Tracking requests are sent to the Spoor API.

128 lines (104 loc) 3.61 kB
// Trace the element and all of its parents, collecting properties as we go import {sanitise, assignIfUndefined} from '../javascript/utils.js'; // For a given container element, get the number of elements that match the // original element (siblings); and the index of the original element (position). const getSiblingsAndPosition = (el, originalEl, selector) => { const siblings = Array.from(el.querySelectorAll(selector)); const position = siblings.findIndex(item => item === originalEl); if(position === -1) {return;} return { siblings: siblings.length, position, }; }; const elementPropertiesToCollect = [ "nodeName", "className", "id", "href", "text", "role", ]; // Get all (sanitised) properties of a given element. const getAllElementProperties = element => { const properties = {}; for (const property of elementPropertiesToCollect) { const value = element[property] || element.getAttribute(property) || element.hasAttribute(property); if (value !== undefined) { if (typeof value === 'boolean') { properties[property] = value; } else { properties[property] = sanitise(value); } } } return properties; }; const parseRawValue = (rawValue) => { try { const parsedValue = JSON.parse(rawValue); const type = Object.prototype.toString.call(parsedValue); const isJSON = type === '[object Object]' || type === '[object Array]'; return [isJSON, parsedValue]; } catch (error) { return [false, null]; } } const getAttributeValue = (rawValue) => { const [isJSON, value] = parseRawValue(rawValue); return isJSON ? value : rawValue; } // Get some properties of a given element. const getDomPathProps = (attrs, props) => { // Collect any attribute that matches given strings. attrs .filter(attribute => attribute.name.match(/^data-trackable|^data-o-|^aria-/i)) .forEach(attribute => { props[attribute.name] = attribute.value; }); return props; }; // Get only the custom data-trackable-context-? properties of a given element const getContextProps = (attrs, props, isOriginalEl) => { const customProps = {}; // for the original element collect properties like className, nodeName if (isOriginalEl) { elementPropertiesToCollect.forEach(name => { if (typeof props[name] !== 'undefined' && name !== 'id') { customProps[name] = props[name]; } }); } // Collect any attribute that matches given strings. attrs .filter(attribute => attribute.name.match(/^data-trackable-context-/i)) .forEach(attribute => { customProps[attribute.name.replace('data-trackable-context-', '')] = getAttributeValue(attribute.value); }); return customProps; }; export function getTrace (el) { const rootEl = document; const originalEl = el; const selector = originalEl.getAttribute('data-trackable') ? `[data-trackable="${originalEl.getAttribute('data-trackable')}"]` : originalEl.nodeName; const trace = []; const customContext = {}; while (el && el !== rootEl) { const props = getAllElementProperties(el); const attrs = Array.from(el.attributes); let domPathProps = getDomPathProps(attrs, props); // If the element happens to have a data-trackable attribute, get the siblings // and position of the element (relative to the current element). if (domPathProps["data-trackable"]) { domPathProps = Object.assign( domPathProps, getSiblingsAndPosition(el, originalEl, selector) ); } trace.push(domPathProps); const contextProps = getContextProps(attrs, props, el === originalEl); assignIfUndefined(contextProps, customContext); el = el.parentNode; } return { trace, customContext }; }