UNPKG

affiliate

Version:

A platform agnostic tool to quickly add affiliate links onto your website

229 lines 9.41 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const features_1 = require("./shared/features"); const log_1 = __importDefault(require("./shared/log")); const nodeTools_1 = require("./shared/nodeTools"); /** * Manages stateful affiliation */ class Affiliate { constructor(config) { var _a; this.state = { attached: false, config: { tags: [], }, hosts: [], }; this.observer = undefined; /** * Modify the URL of a matching link while preserving the original link state */ this.modifyURL = (url, node) => { // Check if URL is already modified const linkData = (0, nodeTools_1.getNodeData)(node); if (linkData.is && linkData.is === url.href) return; // Preserve the original URL const originalURL = url.href; this.log(false, 'Discovered URL: ' + url.href); const modifiedUrl = this.convert(url); // Update the href tag and save the url to the DOM node node.href = modifiedUrl; (0, nodeTools_1.setNodeData)(node, { was: originalURL, is: url.href, }); }; /** * Modify a manually provided URL */ this.convert = (url) => { var _a, _b; // Convert input URL object to string if (typeof url === 'object') url = url.href; // Check if URL global exists if (!features_1.hasURL) { this.log(true, 'This browser needs a URL polyfill.'); return url; } // Parse the URL natively const modURL = new URL(url, window.location.origin); // Only modify host provided if (!this.state.hosts.includes(modURL.host)) return modURL.href; // Go through each tag for (const tag of this.state.config.tags) { // Check if the host matches if (tag.hosts.includes(modURL.host)) { // Change query variables if (tag.query) { Object.keys((_a = tag.query) !== null && _a !== void 0 ? _a : {}).forEach((key) => { if (typeof tag.query === 'object') modURL.searchParams.set(key, tag.query[key]); }); } // Run the modification function if (typeof tag.modify === 'function') { try { let returnedURL = tag.modify(modURL); if (typeof returnedURL === 'object') returnedURL = returnedURL.href; modURL.href = returnedURL; } catch (e) { (0, log_1.default)(true, e); } } // Replace certain parts of the url (_b = tag.replace) === null || _b === void 0 ? void 0 : _b.forEach((replacement) => { modURL.href = modURL.href.replace(replacement.from, replacement.to); }); return modURL.href; } } return modURL.href; }; /** * Attach the mutation observer */ this.attach = () => { // Cannot attach twice, cannot attach for node if (this.state.attached || typeof document === 'undefined') return this; // Get readyState, or the loading state of the DOM const { readyState } = document; if (readyState === 'complete' || readyState === 'interactive') { // Set attached to true this.state.attached = true; // Run through the entire body tag this.traverse(); if (features_1.hasMutationObserver && this.observer) { // Attach the observer this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true, attributeFilter: ['href'], }); } else { this.log(false, 'Browser does not support MutationObserver.'); } } else { // Wait until the DOM loads window.addEventListener('DOMContentLoaded', this.attach); } return this; }; /** * Detach the mutation observer */ this.detach = () => { if (!features_1.hasMutationObserver || !this.observer) return this; this.state.attached = false; this.observer.disconnect(); this.log(false, 'Observer disconnected.'); return this; }; // Extend the configuration config = config !== null && config !== void 0 ? config : {}; config.tags = (_a = config.tags) !== null && _a !== void 0 ? _a : []; config.tags.map((tag, i) => { if (!config || !config.tags) return; // Convert a single host to an array if (typeof tag.hosts === 'string') tag.hosts = [tag.hosts]; // Extend proper tag configuration config.tags[i] = { query: {}, replace: [], ...tag, }; // Append hosts to full list this.state.hosts = [ ...this.state.hosts, ...config.tags[i].hosts, ]; }); // Set logging function this.log = config.log ? log_1.default : () => undefined; this.log(false, 'New Instance', config); // Check is MutationObserver is supported if (features_1.hasMutationObserver) { // Initialize MutationObserver this.observer = new window.MutationObserver((mutations) => { // This function is called for every DOM mutation // Has a mutation been logged let emitted = false; mutations.forEach((mutation) => { // If the attributes of the link have been modified if (mutation.type === 'attributes') { // Skip links without an href if (mutation.attributeName !== 'href') return; const href = mutation.target.href; const linkData = (0, nodeTools_1.getNodeData)(mutation.target); // Skip links without a modified href if (linkData.is && linkData.is === href) return; } // Only calls on first mutation if (!emitted) { this.log(false, 'DOM Mutation', mutation); emitted = true; } // Scan the node and subnodes if there are any this.traverse(mutation.target); }); }); } // Set internal state this.state.config = config; } /** * Manual function to search the DOM for unaffiliated links */ traverse(nodeSet = document.body) { if (typeof nodeSet !== 'object' || typeof nodeSet.getElementsByTagName !== 'function') return this; if (!features_1.hasURL) { this.log(true, 'This browser needs a URL polyfill.'); return this; } this.log(false, 'Traversing DOM...'); // Reduce link collection to array const collection = nodeSet.getElementsByTagName('a'); let nodes = Object.values(collection); // If the nodeSet is a single link, turn to array if (nodeSet.nodeName.toLowerCase() === 'a') nodes = [nodeSet]; this.log(false, `Found ${nodes.length + 1} nodes...`); // Go through each link nodes.forEach((node) => { var _a; // Check if it is actually linking if (!node || !('href' in node)) return; // Parse the URL natively const url = new URL((_a = node.href) !== null && _a !== void 0 ? _a : '', window.location.origin); // Only modify hosts provided. if (!this.state.hosts.includes(url.host)) return; this.modifyURL(url, node); }); return this; } } exports.default = Affiliate; //# sourceMappingURL=Affiliate.js.map