UNPKG

bitandblack-matomo-optout

Version:

Custom OptOut in Matomo (Piwik) with AJAX. Doesn't need an iframe.

368 lines (317 loc) 10.8 kB
/** * MatomoOptOut * * @copyright Copyright (c) 2020, Bit&Black * @author Tobias Köngeter <hello@bitandblack.com> * @link https://www.bitandblack.com */ import httpJsonp from "http-jsonp"; /** * MatomoOptOut. */ class MatomoOptOut { /** * The root URL to Matomo. * * @private */ private readonly matomoRoot: string; /** * The whole tracking URL. * * @private */ private readonly matomoTrackingURL: string; /** * The current tracking status. * * @private */ private isTrackingActive: boolean; /** * The HTML element for error messages. * * @private */ private errorMessage: HTMLElement = null; /** * The interval for fetching the status. * * @private */ private interval: number; /** * The HTML element when tracking is enabled. * * @private */ private readonly trackingEnabledElement: HTMLElement; /** * The HTML element when tracking is disabled. * * @private */ private readonly trackingDisabledElement: HTMLElement; /** * The callback when an error appears. * * @private */ private errorCallback: () => void = null; /** * The callback when the status has changed. * * @private */ private onStatusChangeCallback: (isTrackingActive: boolean) => void = null; /** * The CSS class to make an element visible. * * @private */ private classElementIsVisible: string = null; /** * The CSS class to make an element invisible. * * @private */ private classElementIsInvisible: string = null; /** * If classes should be used to show or hide HTML. * * @private */ private useClassesForVisibility: boolean = false; /** * Constructor. * * @param matomoRoot The root URL to your Matomo instance. * @param trackingEnabledElement The html element holding text and checkbox when tracking is enabled. * @param trackingDisabledElement The html element holding text and checkbox when tracking is disabled. */ constructor(matomoRoot: string, trackingEnabledElement: HTMLElement, trackingDisabledElement: HTMLElement) { this.matomoRoot = matomoRoot.replace(/\/+$/, ""); this.trackingEnabledElement = trackingEnabledElement; this.trackingDisabledElement = trackingDisabledElement; this.matomoTrackingURL = `${this.matomoRoot}/index.php?module=API&format=json&method=AjaxOptOut`; this.updateHTMLOutput.bind(this); const inputs: Array<HTMLInputElement> = this.getElements( this.trackingEnabledElement.querySelectorAll("input[type='checkbox']"), this.trackingDisabledElement.querySelectorAll("input[type='checkbox']") ); inputs.forEach((input) => { input.addEventListener("change", (event) => { event.stopPropagation(); event.preventDefault(); const doTrack = this.isChildOf( event.target as HTMLElement, this.trackingDisabledElement ); this.setMatomoTrackingStatus( doTrack, () => { this.getMatomoTrackingStatus( (response) => this.updateHTMLOutput(response) ); } ); }); }); /** * Initial call */ this.getMatomoTrackingStatus( (response) => this.updateHTMLOutput(response) ); } /** * Returns if a element is child of another element. * * @param child * @param parent */ private isChildOf = (child: HTMLElement, parent: HTMLElement) => { let node = child.parentNode; while (node !== null) { if (node === parent) { return true; } node = node.parentNode; } return false; } /** * Updates the HTML output. This will be called as a callback after requesting the tracking status. * * @param response */ private updateHTMLOutput = (response) => { const status: boolean = response.value; if (true === this.useClassesForVisibility) { this.trackingEnabledElement.classList.remove(this.classElementIsVisible); this.trackingDisabledElement.classList.remove(this.classElementIsVisible); this.trackingEnabledElement.classList.add(this.classElementIsInvisible); this.trackingDisabledElement.classList.add(this.classElementIsInvisible); } else { this.trackingEnabledElement.style.display = "none"; this.trackingDisabledElement.style.display = "none"; } const inputs: Array<HTMLInputElement> = this.getElements( this.trackingEnabledElement.querySelectorAll("input[type='checkbox']"), this.trackingDisabledElement.querySelectorAll("input[type='checkbox']") ); let currentTrackingElement: HTMLElement = true === status ? this.trackingEnabledElement : this.trackingDisabledElement ; inputs.forEach((input) => { input.checked = status; }); if (true === this.useClassesForVisibility) { currentTrackingElement.classList.remove(this.classElementIsInvisible); currentTrackingElement.classList.add(this.classElementIsVisible); } else { currentTrackingElement.style.display = "block"; } this.checkStatusChange(status); }; /** * Gets the matomo tracking status. * * @param callback The callback when requesting the tracking status. */ getMatomoTrackingStatus = (callback?: (response) => void) => { httpJsonp({ url: `${this.matomoTrackingURL}.isTracked`, callbackProp: "callback", callback: callback || {}, }); }; /** * Sets the tracking status. * * @param status The new status * @param callback The callback when the changing the status */ private setMatomoTrackingStatus = (status: boolean, callback = null) => { const setter = true === status ? ".doTrack" : ".doIgnore"; httpJsonp({ url: this.matomoTrackingURL + setter, callbackProp: "callback", callback: callback || {}, }); }; /** * Checks if the status changed. * * @param statusNew */ private checkStatusChange = (statusNew) => { if (this.isTrackingActive === statusNew) { if (null !== this.errorMessage) { if (true === this.useClassesForVisibility) { this.errorMessage.classList.remove(this.classElementIsInvisible); this.errorMessage.classList.add(this.classElementIsVisible); } else { this.errorMessage.style.display = "block"; } } if (null !== this.errorCallback) { this.errorCallback(); } return false; } this.isTrackingActive = statusNew; if (null !== this.errorMessage) { if (true === this.useClassesForVisibility && this.errorMessage.classList.contains(this.classElementIsVisible) ) { this.errorMessage.classList.remove(this.classElementIsVisible); this.errorMessage.classList.add(this.classElementIsInvisible); } else if (this.errorMessage.style.display === "block") { this.errorMessage.style.display = "none"; } } if (null !== this.onStatusChangeCallback) { this.onStatusChangeCallback(this.isTrackingActive); } } /** * Sets an HTML element with a custom error message in case the tracking status could not changed. * * @param element */ setErrorMessage = (element: HTMLElement) => { this.errorMessage = element; this.setErrorMessageVisibility(); return this; }; /** * Sets a custom error callback. * * @param errorCallback */ setErrorCallback = (errorCallback: () => void) => { this.errorCallback = errorCallback; return this; }; /** * Sets a custom callback for when the tracking status has changed. * * @param onStatusChangeCallback */ setOnStatusChangeCallback = (onStatusChangeCallback: (isTrackingActive: boolean) => void) => { this.onStatusChangeCallback = onStatusChangeCallback; return this; } /** * Sets CSS classes to handle the visibility of the HTML elements. * * @param classElementIsVisible The class name for visible elements. * @param classElementIsInvisible The class name for invisible elements. */ setCSSClasses = (classElementIsVisible: string, classElementIsInvisible: string) => { this.classElementIsVisible = classElementIsVisible; this.classElementIsInvisible = classElementIsInvisible; this.useClassesForVisibility = true; this.setErrorMessageVisibility(); return this; } /** * Enables a constant check for the current tracking status. * * @param seconds The interval in seconds. */ watchStatusChange = (seconds: number) => { clearInterval(this.interval); this.interval = window.setInterval( this.getMatomoTrackingStatus, seconds * 1000 ); return this; } /** * Concat multiple selectors together. * * @param elements */ private getElements = (...elements) => { let elementsAll = []; elements.forEach((element) => { elementsAll = elementsAll.concat(Array.prototype.slice.call(element)); }); return elementsAll; } private setErrorMessageVisibility = () => { if (true === this.useClassesForVisibility) { this.errorMessage.style.removeProperty("display"); this.errorMessage.classList.remove(this.classElementIsVisible, this.classElementIsInvisible); this.errorMessage.classList.add(this.classElementIsInvisible); } else { this.errorMessage.style.display = "none"; } } } export { MatomoOptOut };