bitandblack-matomo-optout
Version:
Custom OptOut in Matomo (Piwik) with AJAX. Doesn't need an iframe.
368 lines (317 loc) • 10.8 kB
text/typescript
/**
* 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 };