HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/vangogh.komma.pro/resources/js/kms/entity/preventNavigationController.js
/**
 * Tracks all inputs, selects and textareas in the given wrapper element for changes.
 *
 * If the user clicked an anchor the user first must confirm that he's going to lose changes before the anchor is followed.
 */
class PreventNavigationController
{
    /**
     * inputs in the wrapper element will be tracked for changes
     *
     * @param WrapperElement {Element}
     */
    constructor(WrapperElement) {
        //Create properties
        this._inputs = []; //Inputs, textareas, selects
        this._wrapperElement = null;
        this._translation = {
            headerText: '',
            message: '',
            confirmText: '',
            cancelText: '',
        };
        this._anchorsAndConfirmationControllers = []; //Contains objects containing properties element and controller to keep track of all anchors and their confirmation controllers.
        this._changed = false; //Automatically set to true when one of the inputs was changed

        //validate stuff
        if(!WrapperElement) {
            console.error('PreventNavigationController: No wrapper element given. Not preventing navigation.');
            return;
        }
        if(!WrapperElement.dataset.translation) {
            console.error('PreventNavigationController: No translation present. Not preventing navigation when not all changes have been saved.');
            return;
        }
        this._wrapperElement = WrapperElement;

        //Make sure event handlers have the correct this
        this._inputChanged = this._inputChanged.bind(this);

        //Delegate control to specialist parts of the class
        this._loadTranslation();

        this.refresh();
    }

    /**
     * Returns true if the given input element is an HTMLElement of tag Input.
     *
     * @param InputElement {HTMLElement}
     * @returns {boolean}
     * @private
     */
    _isTrackableElement(InputElement) {
        return (InputElement.tagName === "INPUT" || InputElement.tagName === "SELECT" || InputElement.tagName === "TEXTAREA")
    }

    /**
     * Add / remove listeners on inputs
     *
     * @param inputs {array}
     * @param add {boolean} true when you want to add the listeners. false if not.
     * @private
     */
    _setListenersOnInputs(inputs, add = true)
    {
        let nInputs = inputs.length;
        for(let currentInputNumber = 0; currentInputNumber < nInputs; currentInputNumber++)
        {
            if(add) {
                inputs[currentInputNumber].addEventListener('change', this._inputChanged);
            } else {
                inputs[currentInputNumber].removeEventListener('change', this._inputChanged);
            }
        }
    }

    /**
     * Triggered in response to a changed input
     *
     * @param event
     * @private
     */
    _inputChanged(event) {
        // console.log('Form changed. Source: ', event.target);
        this._changed = true;
    }


    /**
     * Loads the translations. They are defined on the wrapper element
     * @private
     */
    _loadTranslation()
    {
        this._translation = JSON.parse(this._wrapperElement.dataset.translation);
    }

    /**
     * Scans the documents for inputs to monitor changes on. These inputs will be stored as a reference.
     * We then intercept clicks on all elements that cause navigations (<a> tags).
     * And finally we set listeners on the found inputs to detect changes. This ultimately will cause
     * a model to pop up when navigating. If the page is changed somehow, you may also call this method to
     * make the controller aware of changes.
     */
    refresh()
    {
        this._inputs = this._findAllInputsIn(this._wrapperElement);
        this._interceptClicksOnAnchors();
        this._setListenersOnInputs(this._inputs);
    }

    /**
     * Scans the document for anchor tags. Intercepts click events on them when one of the inputs
     * has changed. When a click is intercepted, a modal is shown asking for confirmation.
     * When the action gets confirmed, a new click on the anchor tag is done, without interception.
     *
     * @private
     */
    _interceptClicksOnAnchors()
    {
        this._clearAnchorsAndControllers();

        let anchors = document.getElementsByTagName('A');
        let nAnchors = anchors.length;

        for(let linkNumber = 0; linkNumber < nAnchors; linkNumber++)
        {
            let currentAnchor = anchors[linkNumber];

            let anchorHref = currentAnchor.getAttribute('href');
            if(anchorHref && anchorHref.substr(0,1) !== "#") { //The anchor has an href value and that is not a hash.
                let controller = new ConfirmationController(currentAnchor);
                controller.setHeaderText(this._translation.headerText)
                        .setMessage(this._translation.message)
                        .setConfirmText(this._translation.confirmText)
                        .setCancelText(this._translation.cancelText)
                        .setConfirmCallback(function() {
                            window.location = anchorHref;
                        })
                        .setOnlyConfirmIfTrueCallback((function (controller) {
                            return function()
                            {
                                return controller._hasChanged();
                            }
                        })(this));

                this._anchorsAndConfirmationControllers.push({
                    'anchor': currentAnchor,
                    'controller': controller
                })
            }
        }
    }

    /**
     * Clears all anchors and controllers.
     * Also disables event listeners in those controllers.
     *
     * @private
     */
    _clearAnchorsAndControllers() {
        let anchorControllerObject;
        while(anchorControllerObject = this._anchorsAndConfirmationControllers.pop()) {
            /** @var {ConfirmationController} controller **/
            // console.log('Clearing old confirmation controller: ', anchorControllerObject);
            let controller = anchorControllerObject.controller;
            controller.disableListeners();
            controller = null;
        }
    }

    /**
     * Returns true if one of the input has changed
     * @returns {boolean}
     * @private
     */
    _hasChanged()
    {
        // console.log('Something changed! Preventing navigation');
        return this._changed;
    }

    /**
     * Finds all html elements witch match elements specified in the isTrackableElement method
     *
     * @param WrapperElement {Element}
     * @param inputs
     * @returns {Array}
     * @private
     */
    _findAllInputsIn(WrapperElement, inputs = []) {
        let nChildren = WrapperElement.children.length;
        if(nChildren === 0) return inputs;

        for(let index = 0; index < nChildren; index++)
        {
            let currentChild = WrapperElement.children[index];
            if(this._isTrackableElement(currentChild)) {
                inputs.push(currentChild);
            }
            if(currentChild.children.length > 0) {
                let childInputs = this._findAllInputsIn(currentChild, inputs);
                inputs.concat(childInputs);
            }
        }

        return inputs;
    }
}