brainworxx/kreXX

View on GitHub
src/View/Skins/TypeScript/SkinTs/Hans.ts

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * kreXX: Krumo eXXtended
 *
 * kreXX is a debugging tool, which displays structured information
 * about any PHP object. It is a nice replacement for print_r() or var_dump()
 * which are used by a lot of PHP developers.
 *
 * kreXX is a fork of Krumo, which was originally written by:
 * Kaloyan K. Tsvetkov <kaloyan@kaloyan.info>
 *
 * @author
 *   brainworXX GmbH <info@brainworxx.de>
 *
 * @license
 *   http://opensource.org/licenses/LGPL-2.1
 *
 *   GNU Lesser General Public License Version 2.1
 *
 *   kreXX Copyright (C) 2014-2024 Brainworxx GmbH
 *
 *   This library is free software; you can redistribute it and/or modify it
 *   under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation; either version 2.1 of the License, or (at
 *   your option) any later version.
 *   This library is distributed in the hope that it will be useful, but WITHOUT
 *   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 *   FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 *   for more details.
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with this library; if not, write to the Free Software Foundation,
 *   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

class Hans
{
    /**
     * kreXX dom tools.
     *
     * @var {Kdt}
     */
    protected kdt:Kdt;

    /**
     * Our dragable lib.
     *
     * @var {Draxx}
     */
    protected draxx:Draxx;

    /**
     * Out DOM search.
     *
     * @var {Search}
     */
    protected search:Search;

    /**
     * The event handler.
     *
     * @var {Evenhandler}
     */
    protected eventHandler:Eventhandler;

    /**
     * Here we store the selectors for the ruin initialization.
     *
     * @var {Selectors}
     */
    protected selectors:Selectors;

    /**
     * The last jumpTo to interval.
     *
     * @var {number}
     */
    protected jumpToInterval:number = 0;

    /**
     * Defining all selectors for the run method.
     */
    constructor()
    {
        this.selectors = new Selectors();
        this.selectors.eventHandler = '.kwrapper.kouterwrapper, .kfatalwrapper-outer';
        this.selectors.moveToBottom = '.kouterwrapper';
        this.selectors.close = '.kwrapper .kheadnote-wrapper .kclose, .kwrapper .kfatal-headnote .kclose';
        this.selectors.toggle = '.kwrapper .kexpand';
        this.selectors.setSetting = '.kwrapper .keditable select, .kwrapper .keditable input:not(.ksearchfield)';
        this.selectors.resetSetting = '.kwrapper .kresetbutton';
        this.selectors.copyFrom = '.kwrapper .kcopyFrom';
        this.selectors.displaySearch = '.kwrapper .ksearchbutton, .kwrapper .ksearch .kclose';
        this.selectors.performSearch = '.kwrapper .ksearchnow';
        this.selectors.collapse = '.kwrapper .kolps';
        this.selectors.generateCode = '.kwrapper .kgencode';
        this.selectors.preventBubble = '.kodsp';
        this.selectors.displayInfoBox = '.kwrapper .kchild .kinfobutton';
        this.selectors.moveToViewport = '.kouterwrapper';
    }

    /**
     * Getting our act together.
     */
    public run(): void
    {
        // Init our libs before usage.
        this.kdt = new Kdt();
        this.kdt.setJumpTo(this.jumpTo);
        this.eventHandler = new Eventhandler(this.selectors.eventHandler);
        this.search = new Search(this.eventHandler, this.jumpTo);

        // In case we are handling a broken html structure, we must move everything
        // to the bottom.
        this.kdt.moveToBottom(this.selectors.moveToBottom);

        // Initialize the draggable.
        this.initDraxx();

        /**
         * Register krexx close button function.
         *
         * @event click
         *   Displays a closing animation of the corresponding
         *   krexx output "window" and then removes it from the markup.
         */
        this.eventHandler.addEvent(this.selectors.close, 'click', this.close);

        /**
         * Register toggling to the elements.
         *
         * @event click
         *   Expands a krexx node when it is not expanded.
         *   When it is already expanded, it closes it.
         */
        this.eventHandler.addEvent(this.selectors.toggle, 'click', this.toggle);

        /**
         * Register functions for the local dev-settings.
         *
         * @event change
         *   Changes on the krexx html forms.
         *   All changes will automatically be written to the browser cookies.
         */
        this.eventHandler.addEvent(this.selectors.setSetting, 'change', this.kdt.setSetting);

        /**
         * Register cookie reset function on the reset button.
         *
         * @event click
         *   Resets the local settings in the settings cookie,
         *   when the reset button ic clicked.
         */
        this.eventHandler.addEvent(this.selectors.resetSetting, 'click', this.kdt.resetSetting);

        /**
         * Register the recursions resolving.
         *
         * @event click
         *   When a recursion is clicked, krexx tries to locate the
         *   first output of the object and highlight it.
         */
        this.eventHandler.addEvent(this.selectors.copyFrom, 'click', this.kdt.copyFrom);

        /**
         * Register the displaying of the search menu
         *
         * @event click
         *   When the button is clicked, krexx will display the
         *   search menu associated this the same output window.
         */
        this.eventHandler.addEvent(this.selectors.displaySearch, 'click', this.displaySearch);

        /**
         * Register the search event on the next button.
         *
         * @event click
         *   When the button is clicked, krexx will start searching.
         */
        this.eventHandler.addEvent(this.selectors.performSearch, 'click', this.search.performSearch);

        /**
         * Register the Collapse-All functions on its symbol
         *
         * @event click
         */
        this.eventHandler.addEvent(this.selectors.collapse, 'click', this.kdt.collapse);

        /**
         * Register the code generator on the P symbol.
         *
         * @event click
         */
        this.eventHandler.addEvent(this.selectors.generateCode, 'click', this.generateCode);

        /**
         * Prevents the click-event-bubbling on the generated code.
         *
         * @event click
         */
        this.eventHandler.addEvent(this.selectors.preventBubble, 'click', this.eventHandler.preventBubble);

        /**
         * Display the content of the info box.
         *
         * @event click
         */
        this.eventHandler.addEvent(this.selectors.displayInfoBox, 'click', this.displayInfoBox);

        // Disable form-buttons in case a logfile is opened local.
        if (window.location.protocol === 'file:') {
            this.disableForms();
        }

        // Move the output into the viewport. Debugging onepager is so annoying, otherwise.
        this.draxx.moveToViewport(this.selectors.moveToViewport);
    }

    /**
     * Initialize the draggable.
     */
    protected initDraxx(): void
    {
        this.draxx = new Draxx(
            '.kwrapper',
            '.kheadnote',
            function () {
                let searchWrapper:NodeList = document.querySelectorAll('.search-wrapper');
                let viewportOffset:DOMRect;
                for (let i = 0; i < searchWrapper.length; i++) {
                    viewportOffset = (searchWrapper[i] as HTMLElement).getBoundingClientRect();
                    (searchWrapper[i] as HTMLElement).style.position = 'fixed';
                    (searchWrapper[i] as HTMLElement).style.top = viewportOffset.top + 'px';
                }
            },
            function () {
                let searchWrapper = document.querySelectorAll('.search-wrapper');
                for (let i = 0; i < searchWrapper.length; i++) {
                    (searchWrapper[i] as HTMLElement).style.position = 'absolute';
                    (searchWrapper[i] as HTMLElement).style.top = '';
                }
            }
        );
    }

    /**
     * Hides or displays the nest under an expandable element.
     *
     * @event click
     * @param {Event} event
     *   The click event.
     * @param {Node} element
     *   The element that was clicked.
     */
    protected toggle = (event:Event, element:Element): void =>
    {
        this.kdt.toggleClass(element, 'kopened');

        // Toggle all siblings.
        let sibling:Element = element.nextElementSibling;
        do {
           this.kdt.toggleClass(sibling, 'khidden');
           sibling = sibling.nextElementSibling;
        } while (sibling);
    };

    /**
     * Removes the old highlight and sets the new one.
     *
     * @param {Element} el
     * @param {boolean} noHighlight
     */
    protected setHighlighting(el:Element, noHighlight:boolean): void
    {
        let nests:Node[] = this.kdt.getParents(el, '.knest');

        // Show them.
        this.kdt.removeClass(nests, 'khidden');
        // We need to expand them all.
        for (let i = 0; i < nests.length; i++) {
            this.kdt.addClass([(nests[i] as Element).previousElementSibling], 'kopened');
        }

        if (noHighlight !== true) {
            // Remove old highlighting.
            this.kdt.removeClass('.highlight-jumpto', 'highlight-jumpto');
            // Highlight new one.
            this.kdt.addClass([el], 'highlight-jumpto');
        }
    }

    /**
     * "Jumps" to an element in the markup and highlights it.
     *
     * It is used when we are facing a recursion in our analysis.
     *
     * @event search
     * @param {Element} el
     *   The element you want to focus on.
     * @param {boolean} noHighlight
     *   Do we need to highlight the element we are jumping to?
     */
    protected jumpTo = (el:Element, noHighlight:boolean): void =>
    {
        this.setHighlighting(el, noHighlight);

        // Getting our scroll container
        let destination:number;
        let container:Element|null = document.querySelector('.kfatalwrapper-outer');
        if (container === null) {
            // Normal scrolling
            container = document.querySelector('html');
            // The html container may not accept any scrollTop value.
            ++container.scrollTop;
            if (container.scrollTop === 0 || container.scrollHeight <= container.clientHeight) {
                container = document.querySelector('body');
            }
            --container.scrollTop;
            destination = el.getBoundingClientRect().top + container.scrollTop - 50;
        } else {
            // Fatal Error scrolling.
            destination = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop - 50;
        }

        let diff:number = Math.abs(container.scrollTop - destination);
        if (diff < 250) {
            // No need to jump there
            return;
        }

        // Getting the direction
        let step:number;
        if (container.scrollTop < destination) {
            // Forward.
            step = Math.round(diff / 12);
        } else {
            // Backward.
            step = Math.round(diff / 12) * -1;
        }

        // We also need to check if the setting of the new value was successful.
        let lastValue:number = container.scrollTop;

        // Make sure to end the last interval before starting a new one.
        clearInterval(this.jumpToInterval);
        let interval:number = this.jumpToInterval = setInterval(function () {
            container.scrollTop += step;
            if (Math.abs(container.scrollTop - destination) <= Math.abs(step) || container.scrollTop === lastValue) {
                // We are here now, the next step would take us too far.
                // So we jump there right now and then clear the interval.
                container.scrollTop = destination;
                clearInterval(interval);
            }
            lastValue = container.scrollTop;
        }, 10);
    };

    /**
     * Shows a "fast" closing animation and then removes the krexx window from the markup.
     *
     * @event click
     * @param {Event} event
     *   The click event.
     * @param {Element} element
     *   The element that was clicked.
     */
    protected close = (event:Event, element:Element): void =>
    {
        let instance:string = this.kdt.getDataset(element, 'instance');
        let elInstance:HTMLElement = document.querySelector('#' + instance);

        // Remove it nice and "slow".
        let opacity:number = 1;
        let interval:number = setInterval(function () {
            if (opacity < 0) {
                // It's invisible now, so we clear the timer and remove it from the DOM.
                clearInterval(interval);
                elInstance.parentNode.removeChild(elInstance);
                return;
            }
            opacity -= 0.1;
            elInstance.style.opacity = opacity.toString();
        }, 20);
    };

    /**
     * Disables the editing functions, when a krexx output is loaded as a file.
     *
     * These local settings would actually do
     * nothing at all, because they would land inside a cookie
     * for that file, and not for the server.
     */
    protected disableForms(): void
    {
        let elements:NodeList = document.querySelectorAll('.kwrapper .keditable input, .kwrapper .keditable select');
        for (let i = 0; i < elements.length; i++) {
            (elements[i] as HTMLInputElement).disabled = true;
        }
    }

    /**
     * The kreXX code generator.
     *
     * @event click
     * @param {Event} event
     *   The click event.
     * @param {Element} element
     *   The element that was clicked.
     */
    protected generateCode = (event:Event, element:Element): void =>
    {
        // We don't want to bubble the click any further.
        event.stop = true;

        let codedisplay:HTMLElement = (element.nextElementSibling as HTMLElement);
        let resultArray:string[] = [];
        let resultString:string = '';
        let sourcedata:string;
        let domid:string;
        let wrapperLeft:string = '';
        let wrapperRight:string = '';

        // Get the first element
        let el:Element|Node = (this.kdt.getParents(element, 'li.kchild')[0] as Element);

        // Start the loop to collect all the date
        while (el) {
            // Get the domid
            domid = this.kdt.getDataset((el as Element), 'domid');
            sourcedata = this.kdt.getDataset((el as Element), 'source');

            wrapperLeft = this.kdt.getDataset((el as Element), 'codewrapperLeft');
            wrapperRight = this.kdt.getDataset((el as Element), 'codewrapperRight');

            if (sourcedata === '. . .') {
                if (domid !== '') {
                    // We need to get a new el, because we are facing a recursion, and the
                    // current path is not really reachable.
                    el = document.querySelector('#' + domid).parentNode;
                    // Get the source, again.
                    resultArray.push(this.kdt.getDataset((el as Element), 'source'));
                }
            }
            if (sourcedata !== '') {
                resultArray.push(sourcedata);
            }
            // Get the next el.
            el = this.kdt.getParents(el, 'li.kchild')[0];
        }
        // Now we reverse our result, so that we can resolve it from the beginning.
        resultArray.reverse();

        for (let i = 0; i < resultArray.length; i++) {
            // We must check if our value is actually reachable.
                // '. . .' means it is not reachable,
                // we will stop right here and display a comment stating this.
            if (resultArray[i] === '. . .') {
                resultString = '// Value is either protected or private.<br /> // Sorry . . ';
                break;
            }

            // Check if we are facing a ;stop; instruction
            if (resultArray[i] === ';stop;') {
                resultString = '';
                resultArray[i] = '';
            }

            // We're good, value can be reached!
            if (resultArray[i].indexOf(';firstMarker;') !== -1) {
                // We add our result so far into the "source template"
                resultString = resultArray[i].replace(';firstMarker;', resultString);
            } else {
                // Normal concatenation.
                resultString = resultString + resultArray[i];
            }
        }

        // Add the wrapper that we collected so far
        resultString = wrapperLeft + resultString + wrapperRight;

        // 3. Add the text
        codedisplay.innerHTML = '<div class="kcode-inner">' + resultString + '</div>';
        if (codedisplay.style.display === 'none') {
            codedisplay.style.display = '';
            this.kdt.selectText(codedisplay);
        } else {
            codedisplay.style.display = 'none';
        }
    };

    /**
     * Checks if the search form is inside the viewport. If not, fixes it on top.
     * Gets triggered on,y when scolling the fatal error handler.
     */
    protected checkSearchInViewport = (): void =>
    {
        // Get the search
        let search:HTMLElement = document.querySelector('.kfatalwrapper-outer .search-wrapper');
        // Reset the inline styles
        search.style.position = '';
        search.style.top = '';

        // Measure it!
        let rect = search.getBoundingClientRect();
        if (rect.top < 0) {
            // Set it to the top
            search.style.position = 'fixed';
            search.style.top = '0px';
        }
    };

    /**
     * Toggle the display of the infobox.
     *
     * @param {Event} event
     * @param {Element} element
     *
     * @event keyUp
     */
    protected displayInfoBox = (event:Event, element:Element): void =>
    {
        // We don't want to bubble the click any further.
        event.stop = true;

        // Find the corresponding info box.
        let box:HTMLElement = (element.nextElementSibling as HTMLElement);

        if (box.style.display === 'none') {
            box.style.display = '';
        } else {
            box.style.display = 'none';
        }
    };

    /**
     * Display the search dialog
     *
     * @event click
     * @param {Event} event
     *   The click event.
     * @param {Node} element
     *   The element that was clicked.
     */
    protected displaySearch = (event:Event, element:Node): void =>
    {
        let instance:string = this.kdt.getDataset((element as Element), 'instance');
        let search:HTMLElement = document.querySelector('#search-' + instance);
        let viewportOffset;

        // Toggle display / hidden.
        if (this.kdt.hasClass(search, 'khidden')) {
            // Display it.
            this.kdt.toggleClass(search, 'khidden');
            (search.querySelector('.ksearchfield') as HTMLElement).focus();
            search.style.position = 'absolute';
            search.style.top = '';
            viewportOffset = search.getBoundingClientRect();
            search.style.position = 'fixed';
            search.style.top = viewportOffset.top + 'px';
        } else {
            // Hide it.
            this.kdt.toggleClass(search, 'khidden');
            this.kdt.removeClass('.ksearch-found-highlight', 'ksearch-found-highlight');
            search.style.position = 'absolute';
            search.style.top = '';
        }
    };
}