Source: clickoff.js

import core from 'scalejs.core';
import ko from 'knockout';
import _ from 'lodash';



/**
 * A knockout binding that is used to allow detection of clicking on another element i.e. "clicking off"
 * @param {function} clickOff - the function that is called when click off
 * @param {object} clickOff - a configuration object with additional parameters to modify the behaviour of click off
 * @param {function} clickOff.handler -  the function that is called when click off
 * @param {string[]|HTMLElement[]} [clickOff.includes] - an array of class names or html dom elements that when clicked on will invoke the handler
 * @param {string[]|HTMLElement[]} [clickOff.excludes] - an array of class names or html dom elements that when clicked on will <strong>not</strong> invoke the handler
 * 
 * @example <caption>Passing a function to value accessor</caption>
 * clickOff: function() {
 *   alert('it works!');
 * }
 * @example <caption>Passing an object with includes and excludes</caption>
 * clickOff: {
 *    handler: function ( ) {
 *        alert('it works!');
 *    },
 *    includes: ['clickOn', 'mainContent'],
 *    excludes: ['clickOff', 'titleBar']
 * }
 * @module clickOff
 */


    let has = core.object.has;

    /**
     *
     * 1. click off should be invoked if the click target is not the element
     *    or a child of the element bound to click-off
     * 2. click off should also be invoked if the target or one of the parents
     *    of the target include a class name that matches this.includes
     * 3. the opposite applies for this.excludes
     * @private
     * @param  {HTMLElement} element        the element that has click-off bound to it
     * @param  {HTMLElement} clickTarget    the target of the click
     * @return {boolean}
     */

    function canClickOff(element, clickTarget) {
        let cls, index, value;

        //loop from click target to root parent of click target
        while (has(clickTarget)) {

            if (element === clickTarget) {
                return false;
            }

            //clickTarget.className.baseVal is the way to get classNames for SVG elements (path, etc)
            if(has(clickTarget.className, 'baseVal')) {
                cls = clickTarget.className.baseVal.split(' ');
            } else {
                cls = (clickTarget.className || '').split(' ');
            }


            let filterFunc = (value) => {
                return (typeof value === 'string' && cls.indexOf(value) > -1)
                    || (value instanceof Element && value.isEqualNode(clickTarget));
            }


            if(_.some(this.includes, filterFunc)) {
                return true;
            }


            if(_.some(this.excludes, filterFunc)) {
                return false;
            }

            // move up in the dom
            clickTarget = clickTarget.parentNode;
        }
        return true;
    }

    /**
     * clickOff binding - A binding that invokes a handler when the user clicks somewhere else
     * @private
     * @param  {HTMLElement} element        the dom element clickOff is bound to
     * @param  {Function} valueAccessor     the options passed to the clickOff binding
     * @param  {type} allBindings           description
     * @param  {type} viewModel             description
     */
    function init( element, valueAccessor, allBindings, viewModel ) {
        let va = valueAccessor(),
            wasRemoved = false,
            eventListener;

        if (!has(va)) {
            return;
        }

        // Normalize value accessor
        if (va instanceof Function) {
            // convert function to expected object
            va = {
                handler: va,
                includes: va.includes,
                excludes: va.excludes
            };
        }

        // enforce handler function
        if (! (va.handler instanceof Function)) {
            throw new TypeError('clickoff: handler function required');
        }

        va.handler = va.handler.bind(viewModel);

        // provide defaults for includes and excludes
        if(!has(va.includes)) {
            va.includes = ['clickoff'];
        }
        if(!has(va.excludes)) {
            va.excludes = ['no-clickoff'];
        }

        eventListener = function ( event ) {
            if (wasRemoved) { return; }
            if (canClickOff.call(va, element, event.target)) {
                va.handler.apply(this, [arguments,[element]]);
            }
        };

        // add handler to body and create dom removal callback for cleanup
        document.body.addEventListener('click', eventListener);
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            wasRemoved = true;
            document.body.removeEventListener('click', eventListener);
        });
    }

    ko.bindingHandlers.clickOff = {
        init: init
    };