| Server IP : 3.93.235.159 / Your IP : 216.73.216.140 Web Server : Apache System : Linux ip-172-26-0-169 5.10.0-46-cloud-amd64 #1 SMP Debian 5.10.262-1 (2026-07-31) x86_64 User : bitnami ( 1000) PHP Version : 8.2.9 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /bitnami/wordpress/wp-content/plugins/send-app/core/assets/js/ |
Upload File : |
import {getFormLocation} from "./utils";
export default abstract class FormFeEvents {
protected observer: MutationObserver | null = null;
protected formSelectors: string[];
protected ajaxUrl: string;
protected nonce: string;
protected viewedThreshold: number;
protected viewedAction: string;
protected abandonedAction: string;
protected debugOn: boolean;
protected idPrefix: string;
protected abandonedEvents: string[] = [ 'beforeunload' ];
constructor() {
this.init();
this.initTracking();
this.observeDynamicForms();
}
abstract init(): void;
abstract getPostId( form: Element ): string;
abstract getFormId( form: Element ): string;
getFormIdAndPosId( form?: Element ): {
formId: string;
postId: string;
} {
if ( !form ) {
return { formId: '', postId: '' };
}
const postId =
document.body.className.match(/page-id-(\d+)/)?.[1] ||
this.getPostId( form );
const formId = this.getFormId( form ) ? this.idPrefix + this.getFormId( form ) : '' ;
return { formId, postId };
}
/**
* Sets up a MutationObserver to monitor the DOM for dynamically added forms.
* Observes the document body for any added child nodes and checks if they match form selectors.
* If a matching form is found, it reruns the tracking logic.
* @private
*/
observeDynamicForms() {
this.observer = new MutationObserver( ( mutations ) => {
mutations.forEach( ( mutation ) => {
if (
mutation.type === 'childList' &&
mutation.addedNodes.length > 0
) {
mutation.addedNodes.forEach( ( node) => {
if ( node instanceof Element ) {
this.handleNewForm( node );
}
} );
}
} );
} );
this.observer.observe( document.body, {
childList: true,
subtree: true,
} );
}
/**
* Processes a newly added DOM element to determine if it is a form or contains forms.
* If the element matches a known form selector and tracking is enabled for that integration,
* it sets up tracking for the form(s).
* @param {Element} element - The newly added DOM element to process.
* @private
*/
handleNewForm( element: Element ) {
const selectorString = this.formSelectors.join(',');
const formsSet = new Set<Element>();
if (element.matches(selectorString)) {
formsSet.add(element);
}
element.querySelectorAll(selectorString).forEach(form => formsSet.add( form ) );
// Process unique forms
formsSet.forEach( form => {
this.trackViewedForms( form );
this.trackAbandonedForms( form );
} );
}
/**
* Initializes tracking for all active and enabled integrations.
* Loads the integrations, determines which are active and enabled,
* and sets up tracking for their forms.
* @private
*/
initTracking(): void {
const selectorString = this.formSelectors.join(',');
const formsSet = new Set<Element>();
document.querySelectorAll( selectorString).forEach(form => formsSet.add( form ) );
formsSet.forEach((form) => {
this.trackViewedForms(form);
this.trackAbandonedForms(form);
});
}
/**
* Tracks when a form is viewed by observing its visibility.
* Sends a request when the form is fully visible.
* @param {Element} form - The form element to track.
* @private
*/
trackViewedForms( form: Element ): void {
const visibilityObserver = new IntersectionObserver(
( entries ) => {
entries.forEach( async ( entry ) => {
if (
entry.isIntersecting &&
entry.intersectionRatio >= this.viewedThreshold
) {
const formData = this.prepareFormData( form, this.viewedAction );
if ( formData.has('form_id') ) {
const response = await fetch(this.ajaxUrl, {method: 'POST', body: formData});
if (!response.ok && this.debugOn) {
console.error('Error tracking form view:', response.statusText);
}
}
visibilityObserver.unobserve(entry.target);
}
});
},
{ threshold: [ this.viewedThreshold ] }
);
visibilityObserver.observe(form);
}
/**
* Tracks when a form is abandoned (user focuses on the form but leaves the page without submitting).
* @param {Element} form - The form element to track.
* @private
*/
trackAbandonedForms( form: Element ): void {
let formFocused = false;
let formSubmitted = false;
form.querySelectorAll( 'input, textarea' ).forEach( ( field) => {
field.addEventListener( 'focus', () => ( formFocused = true ) );
} );
form.addEventListener( 'submit', () => ( formSubmitted = true ) );
this.abandonedEvents.forEach( ( eventName ) => {
window.addEventListener( eventName, async () => {
if ( formFocused && !formSubmitted ) {
const formData = this.prepareFormData( form, this.abandonedAction );
if ( formData.has( 'form_id' ) ) {
const response = await fetch(this.ajaxUrl, {method: 'POST', body: formData});
if ( !response.ok && this.debugOn ) {
console.error('Error tracking abandoned form:', response.statusText);
}
}
}
} )
} );
}
/**
*
* @param {Element} form
* @param action
*
* @private
*/
prepareFormData( form: Element, action: string ): FormData {
const formData = new FormData();
const { formId, postId } = this.getFormIdAndPosId( form );
if ( formId ) {
formData.append( 'action', action );
formData.append( 'nonce', this.nonce );
formData.append( 'form_id', formId );
formData.append( 'page_id', postId );
formData.append( 'location', getFormLocation( form ) );
}
return formData;
}
}