<commit_before>function FeedbackControl($scope, FeedbackControlService, FeedbackSignal, Template, FeedbackService) {

    var qs = (function(a) {
        if (a == "") return {};
        var b = {};
        for (var i = 0; i < a.length; ++i) {
            var p=a[i].split('=');
            if (p.length != 2) continue;
            b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
        }
        return b;
    })(window.location.search.substr(1).split('&'));

    var is_empty = function(obj) {
        return Object.keys(obj).length === 0;
    }

    var update_selected = function(action, id) {
        if (action == 'add' && $scope.selected.indexOf(id) == -1) {
            $scope.selected.push(id);
        }

        if (action == 'remove' && $scope.selected.indexOf(id) != -1) {
            $scope.selected.splice($scope.selected.indexOf(id), 1);
        }
    }

    var insert_param = function(key, value) {

        var key = escape(key); value = escape(value);
        var kvp = document.location.search.substr(1).split('&');
        if (kvp == '') {
            document.location.search = '?' + key + '=' + value;
        } else {
            var i=kvp.length;
            var x;
            while(i--) {
                x = kvp[i].split('=');

                if (x[0]==key) {
                    x[1] = value;
                    kvp[i] = x.join('=');
                    break;
                }
            }

            if (i<0) {
                kvp[kvp.length] = [key,value].join('=');
            }

            //this will reload the page, it's likely better to store this until finished
            document.location.search = kvp.join('&');
        }
    }

    $scope.selected = [];
    var checkboxes = $(".feed-checkbox");
    $scope.status_select_value = 'none';

    $scope.date_filter     = (!is_empty(qs) && qs['date']) ? qs['date'] : 'none';
    $scope.priority_filter = (!is_empty(qs) && qs['priority']) ? qs['priority'] : 'none';
    $scope.status_filter   = (!is_empty(qs) && qs['status']) ? qs['status'] : 'none';
    $scope.rating_filter   = (!is_empty(qs) && qs['rating']) ? qs['rating'] : 'none';

    $scope.feedback_status = function($event) {

        var currentUrl = window.location.pathname;
        var target = $($event.target);
        var feed = $.parseJSON(target.attr('data-feed'));

        if(currentUrl.match(/filed/g)) {
            var current = {
                id: feed.id
              , catid: feed.catid
              , status: feed.status
              , origin: Template.current_inbox_state
            }
        } else {
            var current = {
                id: feed.id
              , catid: Template.default_category_id
              , status: feed.status
              , origin: Template.current_inbox_state
            }
        }

        FeedbackSignal.current_state(current);
        feed.origin = Template.current_inbox_state;
        FeedbackControlService.change_status(feed, true);
    }

    $scope.flag_feedback = function($event) {
        var flag = $(event.target);
        var feed = $.parseJSON(target.attr('data-feed'));
        console.log(flag.attr('return-policy'));
    }

    $scope.update_selection = function($event, id) {
        var checkbox = $event.target;
        var action = (checkbox.checked ? 'add' : 'remove');
        update_selected(action, id);
    }

    $scope.change_value_status = function() {
        if( $('input[type=checkbox].feed-checkbox:checked').length > 0 ) {

            var currentUrl = window.location.pathname;
            var selected = [];
            var catids = [];
            var ids = $scope.selected;
            var mode = $scope.status_select_value;

            for(var i=0; i < ids.length; ++i) {
                var me = ids[i];

                var entity = $(".dashboard-feedback[feedback=" + me + "]");
                var entity_parent = entity.parents('.feedback-group');
                var score = entity.attr('score');
                var permission = entity.attr('permission');
                var catid = entity.attr('catid');

                if(
                      (score >= 3 && permission == 1)
                   || ((score >= 3 && permission == 0) && (mode == 'delete' || mode == 'restore' || mode == 'remove'))
                   || ((score == 1 || score == 2) && (mode == 'delete' || mode == 'restore' || mode == 'remove'))
                  )  {

                    entity.hide();

                    var child_count = entity_parent.children('.dashboard-feedback:visible');

                    if(child_count.length == 0) {
                        entity_parent.hide();
                    }

                    selected.push(me);
                    catids.push(catid);
                }
            }

            if(selected.length > 0) {

                if(currentUrl.match(/filed/g)) {
                    var feed = {
                        id: selected
                      , status: $scope.status_select_value
                      , catid:  catids
                      , origin: Template.current_inbox_state
                    }
                } else {
                    var feed = {
                        id: selected
                      , status: $scope.status_select_value
                      , catid: Template.default_category_id
                      , origin: Template.current_inbox_state
                    }
                }
                FeedbackSignal.current_state(feed);
                FeedbackControlService.change_status(feed, true);
                $(".checky-box-container").show();
            } else {
                alert("Action is not allowed!");
            }

            selected = [];
            $scope.selected = [];
            $scope.status_select_value = 'none';
        }
    }

    $scope.select_all = function($event) {

        var checkbox = $event.target;
        var action = (checkbox.checked ? 'add' : 'remove');

        for(var i=0; i < checkboxes.length; i++) {

            var entity = $(checkboxes[i]);
            var myparent = entity.parents('.dashboard-feedback');

            //if action is add then click on checkboxes if not click again to deselect
            if(action == "add") {
                if(!entity.is(":checked")) {
                    entity.click();
                }
            } else {
                entity.click();
            }

            update_selected(action, parseInt(entity.val(), 10));
        }
    }

    $scope.filter = function(filter_type) {
        if(filter_type == 'date') {
            if($scope.date_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('date', $scope.date_filter);
            }
        }

        if(filter_type == 'priority') {
            if($scope.priority_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('priority', $scope.priority_filter);
            }
        }

        if(filter_type == 'status') {
            if($scope.status_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('status', $scope.status_filter);
            }
        }

        if(filter_type == 'rating') {
            if($scope.rating_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('rating', $scope.rating_filter);
            }
        }
    }

    $scope.fast_forward = function(email, feedid) {
        FeedbackService.send_fastforward(email, feedid);
    }

    $scope.$on('expungeFeedId', function() {
        //console.log("expunged");
        FeedbackSignal.data.id = [];
        $scope.selected = [];
    })
}

function CheckyBox($scope, FeedbackSignal, FeedbackControlService) {

    $scope.status_selection;

    $scope.undo = function() {
        FeedbackControlService.change_status(FeedbackSignal.data, false);
    }

    $scope.$on('checkFeedbackStatus', function() {
        $scope.status_selection = FeedbackSignal.data.status;
    });
}
<commit_msg>extra virign<commit_after>function FeedbackControl($scope, FeedbackControlService, FeedbackSignal, Template, FeedbackService) {

    var qs = (function(a) {
        if (a == "") return {};
        var b = {};
        for (var i = 0; i < a.length; ++i) {
            var p=a[i].split('=');
            if (p.length != 2) continue;
            b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
        }
        return b;
    })(window.location.search.substr(1).split('&'));

    var is_empty = function(obj) {
        return Object.keys(obj).length === 0;
    }

    var update_selected = function(action, id) {
        if (action == 'add' && $scope.selected.indexOf(id) == -1) {
            $scope.selected.push(id);
        }

        if (action == 'remove' && $scope.selected.indexOf(id) != -1) {
            $scope.selected.splice($scope.selected.indexOf(id), 1);
        }
    }

    var insert_param = function(key, value) {

        var key = escape(key); value = escape(value);
        var kvp = document.location.search.substr(1).split('&');
        if (kvp == '') {
            document.location.search = '?' + key + '=' + value;
        } else {
            var i=kvp.length;
            var x;
            while(i--) {
                x = kvp[i].split('=');

                if (x[0]==key) {
                    x[1] = value;
                    kvp[i] = x.join('=');
                    break;
                }
            }

            if (i<0) {
                kvp[kvp.length] = [key,value].join('=');
            }

            //this will reload the page, it's likely better to store this until finished
            document.location.search = kvp.join('&');
        }
    }

    $scope.selected = [];
    var checkboxes = $(".feed-checkbox");
    $scope.status_select_value = 'none';

    $scope.date_filter     = (!is_empty(qs) && qs['date']) ? qs['date'] : 'none';
    $scope.priority_filter = (!is_empty(qs) && qs['priority']) ? qs['priority'] : 'none';
    $scope.status_filter   = (!is_empty(qs) && qs['status']) ? qs['status'] : 'none';
    $scope.rating_filter   = (!is_empty(qs) && qs['rating']) ? qs['rating'] : 'none';

    $scope.feedback_status = function($event) {

        var currentUrl = window.location.pathname;
        var target = $($event.target);
        var feed = $.parseJSON(target.attr('data-feed'));

        if(currentUrl.match(/filed/g)) {
            var current = {
                id: feed.id
              , catid: feed.catid
              , status: feed.status
              , origin: Template.current_inbox_state
            }
        } else {
            var current = {
                id: feed.id
              , catid: Template.default_category_id
              , status: feed.status
              , origin: Template.current_inbox_state
            }
        }

        FeedbackSignal.current_state(current);
        feed.origin = Template.current_inbox_state;
        FeedbackControlService.change_status(feed, true);
    }

    $scope.flag_feedback = function($event) {
        var flag = $(event.target);
        var feed = $.parseJSON(flag.attr('data-feed'));
        console.log(flag.attr('return-policy'));
    }

    $scope.update_selection = function($event, id) {
        var checkbox = $event.target;
        var action = (checkbox.checked ? 'add' : 'remove');
        update_selected(action, id);
    }

    $scope.change_value_status = function() {
        if( $('input[type=checkbox].feed-checkbox:checked').length > 0 ) {

            var currentUrl = window.location.pathname;
            var selected = [];
            var catids = [];
            var ids = $scope.selected;
            var mode = $scope.status_select_value;

            for(var i=0; i < ids.length; ++i) {
                var me = ids[i];

                var entity = $(".dashboard-feedback[feedback=" + me + "]");
                var entity_parent = entity.parents('.feedback-group');
                var score = entity.attr('score');
                var permission = entity.attr('permission');
                var catid = entity.attr('catid');

                if(
                      (score >= 3 && permission == 1)
                   || ((score >= 3 && permission == 0) && (mode == 'delete' || mode == 'restore' || mode == 'remove'))
                   || ((score == 1 || score == 2) && (mode == 'delete' || mode == 'restore' || mode == 'remove'))
                  )  {

                    entity.hide();

                    var child_count = entity_parent.children('.dashboard-feedback:visible');

                    if(child_count.length == 0) {
                        entity_parent.hide();
                    }

                    selected.push(me);
                    catids.push(catid);
                }
            }

            if(selected.length > 0) {

                if(currentUrl.match(/filed/g)) {
                    var feed = {
                        id: selected
                      , status: $scope.status_select_value
                      , catid:  catids
                      , origin: Template.current_inbox_state
                    }
                } else {
                    var feed = {
                        id: selected
                      , status: $scope.status_select_value
                      , catid: Template.default_category_id
                      , origin: Template.current_inbox_state
                    }
                }
                FeedbackSignal.current_state(feed);
                FeedbackControlService.change_status(feed, true);
                $(".checky-box-container").show();
            } else {
                alert("Action is not allowed!");
            }

            selected = [];
            $scope.selected = [];
            $scope.status_select_value = 'none';
        }
    }

    $scope.select_all = function($event) {

        var checkbox = $event.target;
        var action = (checkbox.checked ? 'add' : 'remove');

        for(var i=0; i < checkboxes.length; i++) {

            var entity = $(checkboxes[i]);
            var myparent = entity.parents('.dashboard-feedback');

            //if action is add then click on checkboxes if not click again to deselect
            if(action == "add") {
                if(!entity.is(":checked")) {
                    entity.click();
                }
            } else {
                entity.click();
            }

            update_selected(action, parseInt(entity.val(), 10));
        }
    }

    $scope.filter = function(filter_type) {
        if(filter_type == 'date') {
            if($scope.date_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('date', $scope.date_filter);
            }
        }

        if(filter_type == 'priority') {
            if($scope.priority_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('priority', $scope.priority_filter);
            }
        }

        if(filter_type == 'status') {
            if($scope.status_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('status', $scope.status_filter);
            }
        }

        if(filter_type == 'rating') {
            if($scope.rating_filter == 'none') {
                window.location = window.location.pathname;
            } else {
                insert_param('rating', $scope.rating_filter);
            }
        }
    }

    $scope.fast_forward = function(email, feedid) {
        FeedbackService.send_fastforward(email, feedid);
    }

    $scope.$on('expungeFeedId', function() {
        //console.log("expunged");
        FeedbackSignal.data.id = [];
        $scope.selected = [];
    })
}

function CheckyBox($scope, FeedbackSignal, FeedbackControlService) {

    $scope.status_selection;

    $scope.undo = function() {
        FeedbackControlService.change_status(FeedbackSignal.data, false);
    }

    $scope.$on('checkFeedbackStatus', function() {
        $scope.status_selection = FeedbackSignal.data.status;
    });
}
<|endoftext|>


<commit_before>/*

Viento CSS animation library by Austin Jackson
http://austinj.net/viento

Please star me on github, I would super duper love it
https://github.com/au5ton/viento

*/

function Viento() {
    //Viento constructor
    //Empty because Viento should be used statically
}

//Number of times the fire() function has been called
Viento.prototype.callInteration = 0;

//Thanks: http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
Viento.prototype.isFunction = function(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}

Viento.prototype.fire = function(t) {
    console.log("call iteration: "+Viento.prototype.callInteration);
    Viento.prototype.callInteration++;
    var error = false;

    //If these are undefined, set a default value to stop errors
    if(t === undefined) {
        console.error("You didn't define anything!");
        error = true;
    }
    else {
        if(t.element === undefined) {
            console.error("You didn't define an element.");
            error = true;
        }
        else {
            var elem = $(t.element);
        }
        if(t.animation === undefined) {
            console.error("You didn't define an animation.");
            error = true;
        }
        else {
            if(t.animation.beforeDelay === undefined) {
                t.animation.beforeDelay = 0;
            }
            if(t.animation.afterDelay === undefined) {
                t.animation.afterDelay = 0;
            }
            if(t.animation.resetAfter === undefined) {
                t.animation.resetAfter = true;
            }
        }
        if(t.callback === undefined) {
            t.callback = function() {};
        }
        if(t.withAnimation === undefined){
            t.withAnimation = function() {};
        }
    }
    if(error === true) {
        return; //If there is an error, stop the execution immediately
    }

    //If this is an entrance animation, wait for the animation to start to reveal the element
    if(t.animation.type === "entrance") {
        elem.on("animationstart webkitAnimationStart oanimationstart MSAnimationStart",function(){
            event.target.style.visibility = "visible";
        });
    }
    //When the animation ends... (this code isn't run immediately, the interpreter skips to what is after it
    elem.on("animationend webkitAnimationEnd oanimationend MSAnimationEnd",function(){

        //Uses setTimeout so we can use "afterDelay"
        setTimeout(function(){
            //If this is an exit animation, hide the element
            if(t.animation.type === "exit") {
                t.element.style.visibility = "hidden";
            }
            //If resetAfter is enabled (it is by default), reset the CSS animation properties and involved classes
            if(t.animation.resetAfter === true){
                //If this element has an entrance animation, remove the "hidden" class from it and reset the visibility to default
                if(t.animation.type === "entrance") {
                    $(t.element).removeClass("hidden");
                    t.element.style.visibility= "";
                }

                //Standard
                t.element.style.animationName = "";
                t.element.style.animationDuration = "";
                t.element.style.animationDelay = "";
                t.element.style.animationDirection = "";
                t.element.style.animationFillMode = "";
                t.element.style.animationIterationCount = "";
                t.element.style.animationPlayState = "";
                t.element.style.animationTimingFunctions = "";
                t.element.style.animation = "";
                //-webkit-
                t.element.style.webkitAnimationName = "";
                t.element.style.webkitAnimationDuration = "";
                t.element.style.webkitAnimationDelay = "";
                t.element.style.webkitAnimationDirection = "";
                t.element.style.webkitAnimationFillMode = "";
                t.element.style.webkitAnimationIterationCount = "";
                t.element.style.webkitAnimationPlayState = "";
                t.element.style.webkitAnimationTimingFunctions = "";
            }
            //Everything is done, run the callback function
            elem.off("animationstart webkitAnimationStart oanimationstart MSAnimationStart");
            elem.off("animationend webkitAnimationEnd oanimationend MSAnimationEnd");
            t.callback();
        },t.animation.afterDelay);

    });

    //After the listeners are set for the end of the animation, let's start the animation
    //Uses setTimeout so we can use "beforeDelay"
    setTimeout(function(){

        //Set the given animation properties, thus running the CSS animation (the actual code that starts the animation)
        //Standard
        t.element.style.animationName = t.animation.name;
        t.element.style.animationDuration = t.animation.duration;
        t.element.style.animationDelay = t.animation.delay;
        t.element.style.animationDirection = t.animation.direction;
        t.element.style.animationFillMode = t.animation.fillMode;
        t.element.style.animationIterationCount = t.animation.iterationCount;
        t.element.style.animationPlayState = t.animation.playState;
        t.element.style.animationTimingFunctions = t.animation.timingFunctions;
        //-webkit-
        t.element.style.webkitAnimationName = t.animation.name;
        t.element.style.webkitAnimationDuration = t.animation.duration;
        t.element.style.webkitAnimationDelay = t.animation.delay;
        t.element.style.webkitAnimationDirection = t.animation.direction;
        t.element.style.webkitAnimationFillMode = t.animation.fillMode;
        t.element.style.webkitAnimationIterationCount = t.animation.iterationCount;
        t.element.style.webkitAnimationPlayState = t.animation.playState;
        t.element.style.webkitAnimationTimingFunctions = t.animation.timingFunctions;
        //Style.animation will overwrite everything before it, so we only want to set it if it has data. This is to keep us from setting everything to undefined or default values.
        if(t.animation.animation !== undefined) {
            t.element.style.animation = t.animation.animation;
            t.element.style.webkitAnimation = t.animation.animation;
        }
    },t.animation.beforeDelay);
}

//Viento.burst() let's you fire animations on a group of elements in one function call
Viento.prototype.burst = function(b) {

    var errors = false;

    //If these are undefined, set a default value to stop errors
    if(b === undefined){
        return;
    }
    else {
        if(b.elements === undefined) {
            console.error("No elements were specified.");
            errors = true;
        }
        if(b.mode === undefined) {
            console.log("You didn't define a method to use. Doing all at once by default.");
            b.mode = "allAtOnce";
        }
        if(b.sortingMethod === undefined) {
            //This is ok, just use jQuery's default sorting
        }
        if(b.animation ===  undefined) {
            console.error("No animation was specified.");
            errors = true;
        }
        else {
            if(b.animation.name === undefined) {
                console.error("No animation name was specified.");
            }
        }
        if(errors === true) {
            return;
        }
    }

    //If we're animating them all at once, we can use a simple array to loop through all the elements because Viento.fire() is asynchronous
    //Thus, they run "allAtOnce"
    //Easy peasy
    if(b.mode === "allAtOnce") {
        for(var i = 0; i < b.elements.length; i++) {
            Viento.prototype.fire({
                element: b.elements[i],
                animation: b.animation
            });
        }
    }
    //If we're animating them one at a time, this is a little tricky. More on that in a sec.
    if(b.mode === "oneAtATime") {

        //b.elements is an Array, allowing access to the native JS function Array.sort()
        //Viento.prototype.sortXYZ are functions that get the properties of the elements and determine the order of the elements so when burst()...
        //...if fired, the elements animate from topToBottom, or in other ways. Developers can write their own, as well.
        if(b.sortingMethod === "topToBottom") {
            b.elements = b.elements.sort(Viento.prototype.sortElementsFromTopToBottom);
        }
        else if(b.sortingMethod === "bottomToTop") {
            b.elements = b.elements.sort(Viento.prototype.sortElementsFromBottomToTop);
        }
        else if(Viento.prototype.isFunction(b.sortingMethod) === true) {
            b.elements = b.elements.sort(b.sortingMethod);
        }

        /*
        Here's the tricky part. I actually opened up an issue for this on Github and reached out to friends for support.
        I hope you learn something, because I sure did:
        https://github.com/au5ton/viento/issues/6

        Now, what I wanted to do was use the callback function in Viento.fire() to tell it to go to the next loop.
        I tried something like this:

        ```
            var ready = false;
            for(var i = 0; i < b.elements.length; i++) {
                ready = false;
                while(ready === false) {
                    Viento.prototype.fire({
                        element: b.elements[i],
                        animation: b.animation,
                        callback: function() {
                            ready = true;
                        }
                    });
                }
            }
        ```

        Now, unfortunately for me, this didn't work. Due to JavaScript's nature of asynchronous actions and scoping, ...
        ...this resulted in an infinite loop and froze every tab I tried modifications of it on.

        Thankfully, @tarunbod knew exactly what to do. Instead of using the index variable i from the loop, he made `i`
        a higher-up variable accessible from the function, and made it recursive! That way, the function can call itself,
        and continute to iterate through the array.
        */

        //Index starts at 0
        var i = 0;
        //Recursive function
        function recursive(inc) {
            //Runs the animation for the given index (at first, 0)
            Viento.prototype.fire({
                element: b.elements[inc],
                animation: b.animation,
                callback: function(){
                    //Once the animation has finished,
                    //If we should continue, increment `i` and call itself again
                    if(i < b.elements.length-1) {
                        i++;
                        recursive(i);
                    }
                    //If we don't "call itself again", then the endless loop ends, and goes to where it says ENDS UP HERE.
                }
            });

        }
        //Calls the function for the first time
        recursive(i);
        //ENDS UP HERE

        //Your burst is finished
    }

}

//Comparing function compatible with Array.prototype.sort()
Viento.prototype.sortElementsFromTopToBottom = function(a,b) {
    if(a.getBoundingClientRect().top < b.getBoundingClientRect().top) { //If a is higher up vertically than b
        return -1; //Return a
    }
    else if(a.getBoundingClientRect().top > b.getBoundingClientRect().top) { //If b is higher up vertically than a
        return 1; //Return b
    }
    else {
        if(a.getBoundingClientRect().left < b.getBoundingClientRect().left) { //If a is closer to the left horizontally than b
            return -1; //Return a
        }
        else if(a.getBoundingClientRect().left > b.getBoundingClientRect().left) { //If b is closer to the left horizontally than b
            return 1; //Return b
        }
        else {
            return 0; //Return equal, or no preference
        }
    }
}

Viento.prototype.sortElementsFromBottomToTop = function(a,b) {
    if(a.getBoundingClientRect().top < b.getBoundingClientRect().top) { //If a is higher up vertically than b
        return 1; //Return b
    }
    else if(a.getBoundingClientRect().top > b.getBoundingClientRect().top) { //If b is higher up vertically than a
        return -1; //Return a
    }
    else {
        if(a.getBoundingClientRect().left < b.getBoundingClientRect().left) { //If a is closer to the left horizontally than b
            return -1; //Return a
        }
        else if(a.getBoundingClientRect().left > b.getBoundingClientRect().left) { //If b is closer to the left horizontally than b
            return 1; //Return b
        }
        else {
            return 0; //Return equal, or no preference
        }
    }
}
<commit_msg>Removed some recursive redundancies<commit_after>/*

Viento CSS animation library by Austin Jackson
http://austinj.net/viento

Please star me on github, I would super duper love it
https://github.com/au5ton/viento

*/

function Viento() {
    //Viento constructor
    //Empty because Viento should be used statically
}

//Number of times the fire() function has been called
Viento.prototype.callInteration = 0;

//Thanks: http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
Viento.prototype.isFunction = function(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}

Viento.prototype.fire = function(t) {
    console.log("call iteration: "+Viento.prototype.callInteration);
    Viento.prototype.callInteration++;
    var error = false;

    //If these are undefined, set a default value to stop errors
    if(t === undefined) {
        console.error("You didn't define anything!");
        error = true;
    }
    else {
        if(t.element === undefined) {
            console.error("You didn't define an element.");
            error = true;
        }
        else {
            var elem = $(t.element);
        }
        if(t.animation === undefined) {
            console.error("You didn't define an animation.");
            error = true;
        }
        else {
            if(t.animation.beforeDelay === undefined) {
                t.animation.beforeDelay = 0;
            }
            if(t.animation.afterDelay === undefined) {
                t.animation.afterDelay = 0;
            }
            if(t.animation.resetAfter === undefined) {
                t.animation.resetAfter = true;
            }
        }
        if(t.callback === undefined) {
            t.callback = function() {};
        }
        if(t.withAnimation === undefined){
            t.withAnimation = function() {};
        }
    }
    if(error === true) {
        return; //If there is an error, stop the execution immediately
    }

    //If this is an entrance animation, wait for the animation to start to reveal the element
    if(t.animation.type === "entrance") {
        elem.on("animationstart webkitAnimationStart oanimationstart MSAnimationStart",function(){
            event.target.style.visibility = "visible";
        });
    }
    //When the animation ends... (this code isn't run immediately, the interpreter skips to what is after it
    elem.on("animationend webkitAnimationEnd oanimationend MSAnimationEnd",function(){

        //Uses setTimeout so we can use "afterDelay"
        setTimeout(function(){
            //If this is an exit animation, hide the element
            if(t.animation.type === "exit") {
                t.element.style.visibility = "hidden";
            }
            //If resetAfter is enabled (it is by default), reset the CSS animation properties and involved classes
            if(t.animation.resetAfter === true){
                //If this element has an entrance animation, remove the "hidden" class from it and reset the visibility to default
                if(t.animation.type === "entrance") {
                    $(t.element).removeClass("hidden");
                    t.element.style.visibility= "";
                }

                //Standard
                t.element.style.animationName = "";
                t.element.style.animationDuration = "";
                t.element.style.animationDelay = "";
                t.element.style.animationDirection = "";
                t.element.style.animationFillMode = "";
                t.element.style.animationIterationCount = "";
                t.element.style.animationPlayState = "";
                t.element.style.animationTimingFunctions = "";
                t.element.style.animation = "";
                //-webkit-
                t.element.style.webkitAnimationName = "";
                t.element.style.webkitAnimationDuration = "";
                t.element.style.webkitAnimationDelay = "";
                t.element.style.webkitAnimationDirection = "";
                t.element.style.webkitAnimationFillMode = "";
                t.element.style.webkitAnimationIterationCount = "";
                t.element.style.webkitAnimationPlayState = "";
                t.element.style.webkitAnimationTimingFunctions = "";
            }
            //Everything is done, run the callback function
            elem.off("animationstart webkitAnimationStart oanimationstart MSAnimationStart");
            elem.off("animationend webkitAnimationEnd oanimationend MSAnimationEnd");
            t.callback();
        },t.animation.afterDelay);

    });

    //After the listeners are set for the end of the animation, let's start the animation
    //Uses setTimeout so we can use "beforeDelay"
    setTimeout(function(){

        //Set the given animation properties, thus running the CSS animation (the actual code that starts the animation)
        //Standard
        t.element.style.animationName = t.animation.name;
        t.element.style.animationDuration = t.animation.duration;
        t.element.style.animationDelay = t.animation.delay;
        t.element.style.animationDirection = t.animation.direction;
        t.element.style.animationFillMode = t.animation.fillMode;
        t.element.style.animationIterationCount = t.animation.iterationCount;
        t.element.style.animationPlayState = t.animation.playState;
        t.element.style.animationTimingFunctions = t.animation.timingFunctions;
        //-webkit-
        t.element.style.webkitAnimationName = t.animation.name;
        t.element.style.webkitAnimationDuration = t.animation.duration;
        t.element.style.webkitAnimationDelay = t.animation.delay;
        t.element.style.webkitAnimationDirection = t.animation.direction;
        t.element.style.webkitAnimationFillMode = t.animation.fillMode;
        t.element.style.webkitAnimationIterationCount = t.animation.iterationCount;
        t.element.style.webkitAnimationPlayState = t.animation.playState;
        t.element.style.webkitAnimationTimingFunctions = t.animation.timingFunctions;
        //Style.animation will overwrite everything before it, so we only want to set it if it has data. This is to keep us from setting everything to undefined or default values.
        if(t.animation.animation !== undefined) {
            t.element.style.animation = t.animation.animation;
            t.element.style.webkitAnimation = t.animation.animation;
        }
    },t.animation.beforeDelay);
}

//Viento.burst() let's you fire animations on a group of elements in one function call
Viento.prototype.burst = function(b) {

    var errors = false;

    //If these are undefined, set a default value to stop errors
    if(b === undefined){
        return;
    }
    else {
        if(b.elements === undefined) {
            console.error("No elements were specified.");
            errors = true;
        }
        if(b.mode === undefined) {
            console.log("You didn't define a method to use. Doing all at once by default.");
            b.mode = "allAtOnce";
        }
        if(b.sortingMethod === undefined) {
            //This is ok, just use jQuery's default sorting
        }
        if(b.animation ===  undefined) {
            console.error("No animation was specified.");
            errors = true;
        }
        else {
            if(b.animation.name === undefined) {
                console.error("No animation name was specified.");
            }
        }
        if(errors === true) {
            return;
        }
    }

    //If we're animating them all at once, we can use a simple array to loop through all the elements because Viento.fire() is asynchronous
    //Thus, they run "allAtOnce"
    //Easy peasy
    if(b.mode === "allAtOnce") {
        for(var i = 0; i < b.elements.length; i++) {
            Viento.prototype.fire({
                element: b.elements[i],
                animation: b.animation
            });
        }
    }
    //If we're animating them one at a time, this is a little tricky. More on that in a sec.
    if(b.mode === "oneAtATime") {

        //b.elements is an Array, allowing access to the native JS function Array.sort()
        //Viento.prototype.sortXYZ are functions that get the properties of the elements and determine the order of the elements so when burst()...
        //...if fired, the elements animate from topToBottom, or in other ways. Developers can write their own, as well.
        if(b.sortingMethod === "topToBottom") {
            b.elements = b.elements.sort(Viento.prototype.sortElementsFromTopToBottom);
        }
        else if(b.sortingMethod === "bottomToTop") {
            b.elements = b.elements.sort(Viento.prototype.sortElementsFromBottomToTop);
        }
        else if(Viento.prototype.isFunction(b.sortingMethod) === true) {
            b.elements = b.elements.sort(b.sortingMethod);
        }

        /*
        Here's the tricky part. I actually opened up an issue for this on Github and reached out to friends for support.
        I hope you learn something, because I sure did:
        https://github.com/au5ton/viento/issues/6

        Now, what I wanted to do was use the callback function in Viento.fire() to tell it to go to the next loop.
        I tried something like this:

        ```
            var ready = false;
            for(var i = 0; i < b.elements.length; i++) {
                ready = false;
                while(ready === false) {
                    Viento.prototype.fire({
                        element: b.elements[i],
                        animation: b.animation,
                        callback: function() {
                            ready = true;
                        }
                    });
                }
            }
        ```

        Now, unfortunately for me, this didn't work. Due to JavaScript's nature of asynchronous actions and scoping, ...
        ...this resulted in an infinite loop and froze every tab I tried modifications of it on.

        Thankfully, @tarunbod knew exactly what to do. Instead of using the index variable i from the loop, he made `i`
        a higher-up variable accessible from the function, and made it recursive! That way, the function can call itself,
        and continute to iterate through the array.
        */

        //Index starts at 0
        var i = 0;
        //Recursive function
        function recursive() {
            //Runs the animation for the given index (at first, 0)
            Viento.prototype.fire({
                element: b.elements[inc],
                animation: b.animation,
                callback: function(){
                    //Once the animation has finished,
                    //If we should continue, increment `i` and call itself again
                    if(i < b.elements.length-1) {
                        i++;
                        recursive();
                    }
                    //If we don't "call itself again", then the endless loop ends, and goes to where it says ENDS UP HERE.
                }
            });

        }
        //Calls the function for the first time
        recursive();
        //ENDS UP HERE

        //Your burst is finished
    }

}

//Comparing function compatible with Array.prototype.sort()
Viento.prototype.sortElementsFromTopToBottom = function(a,b) {
    if(a.getBoundingClientRect().top < b.getBoundingClientRect().top) { //If a is higher up vertically than b
        return -1; //Return a
    }
    else if(a.getBoundingClientRect().top > b.getBoundingClientRect().top) { //If b is higher up vertically than a
        return 1; //Return b
    }
    else {
        if(a.getBoundingClientRect().left < b.getBoundingClientRect().left) { //If a is closer to the left horizontally than b
            return -1; //Return a
        }
        else if(a.getBoundingClientRect().left > b.getBoundingClientRect().left) { //If b is closer to the left horizontally than b
            return 1; //Return b
        }
        else {
            return 0; //Return equal, or no preference
        }
    }
}

Viento.prototype.sortElementsFromBottomToTop = function(a,b) {
    if(a.getBoundingClientRect().top < b.getBoundingClientRect().top) { //If a is higher up vertically than b
        return 1; //Return b
    }
    else if(a.getBoundingClientRect().top > b.getBoundingClientRect().top) { //If b is higher up vertically than a
        return -1; //Return a
    }
    else {
        if(a.getBoundingClientRect().left < b.getBoundingClientRect().left) { //If a is closer to the left horizontally than b
            return -1; //Return a
        }
        else if(a.getBoundingClientRect().left > b.getBoundingClientRect().left) { //If b is closer to the left horizontally than b
            return 1; //Return b
        }
        else {
            return 0; //Return equal, or no preference
        }
    }
}
<|endoftext|>


<commit_before>
'use strict';

const _ = require('lodash');
const Script = require('smooch-bot').Script;

const scriptRules = require('./script.json');

var myDate = new Date();
var groet = '';

/* hour is before noon */
if ( myDate.getHours() < 12 )
{
groet = "Goeiemorgen";
}
else  /* Hour is from noon to 5pm (actually to 5:59 pm) */
if ( myDate.getHours() >= 12 && myDate.getHours() <= 17 )
{
groet = "Goedendag";
}
else  /* the hour is after 5pm, so it is between 6pm and midnight */
if ( myDate.getHours() > 17 && myDate.getHours() <= 24 )
{
groet = "Goedenavond";
}
else  /* the hour is not between 0 and 24, so something is wrong */
{
groet = "Welkom";
}

function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

module.exports = new Script({
processing: {
  prompt: (bot) => bot.say('Geen probleem.'),

//prompt: (bot) => bot.say('...'),
receive: () => 'processing'
},

start: {
receive: (bot,message) => {
  const test = 'Hello World';

  if( test.indexOf('World') >= 0){
    // Found world
    console.log("gevonden")
  }

  const name = message.text;
  return bot.setProp('name', name)
      .then(() => bot.say(`Great! I\'ll call you ${name}`))
      .then(() =>  bot.say(`${groet} - welkom bij Independer.  %[Button label here](http://anyurl.com)`))
      .then(() => 'askName');
}
},

bye: {
prompt: (bot) => bot.say('Geen probleem.'),
receive: () => 'processing'
},

/*
askName: {
prompt: (bot) => bot.say('Hoe heet je?'),
receive: (bot,message) => {
const name = message.text;
return bot.setProp('name', name)
.then(() => bot.say(`Ok ${name}, hoe kan ik je helpen? \n
%[Heb je schade](postback:schade) %[Zoek je een verzekering](postback:zoek)`))
.then(() => 'processing');
},
},
*/

askName: {
    prompt: (bot) => bot.say('Postback is working. What\'s your name?'),
    receive: (bot, message) => {
        const name = message.text;
        return bot.setProp('name', name)
            .then(() => bot.say(`Great! I\'ll call you ${name}`))
            .then(()=> bot.say('Is that OK? %[Yes](postback:yes) %[No](postback:nee)'))
            .then(() => 'speak');
    }
},

error: {
prompt: (bot) => bot.say('Sorry - kun je dat nog eens zeggen?  Er ging iets mis...'),
receive: () => ''
},

finish: {
receive: () => 'finish'
},

  speak: {
          receive: (bot, message) => {

              let upperText = message.text.trim().toUpperCase();

              function updateSilent() {
                  switch (upperText) {
                      case "CONNECT ME":
                          return bot.setProp("silent", true);
                      case "DISCONNECT":
                          return bot.setProp("silent", false);
                      default:
                          return Promise.resolve();
                  }
              }

              function getSilent() {
                  return bot.getProp("silent");
              }

              function processMessage(isSilent) {
                  if (isSilent) {
                      return Promise.resolve("speak");
                  }

                  if (!_.has(scriptRules, upperText)) {
                      return bot.say(`So, I'm good at structured conversations but stickers, emoji and sentences still confuse me. Say 'more' to chat about something else.`).then(() => 'speak');
                  }

                  var response = scriptRules[upperText];
                  var lines = response.split('\n');

                  var p = Promise.resolve();
                  _.each(lines, function(line) {
                      line = line.trim();
                      p = p.then(function() {
                          console.log(line);
                          return wait(50).then(function() {
                              return bot.say(line);
                          });
                      });
                  });

                  return p.then(() => 'speak');
              }

              return updateSilent()
                  .then(getSilent)
                  .then(processMessage);
          }
      }
});
<commit_msg>script<commit_after>
'use strict';

const _ = require('lodash');
const Script = require('smooch-bot').Script;

const scriptRules = require('./script.json');

var myDate = new Date();
var groet = '';

/* hour is before noon */
if ( myDate.getHours() < 12 )
{
groet = "Goeiemorgen";
}
else  /* Hour is from noon to 5pm (actually to 5:59 pm) */
if ( myDate.getHours() >= 12 && myDate.getHours() <= 17 )
{
groet = "Goedendag";
}
else  /* the hour is after 5pm, so it is between 6pm and midnight */
if ( myDate.getHours() > 17 && myDate.getHours() <= 24 )
{
groet = "Goedenavond";
}
else  /* the hour is not between 0 and 24, so something is wrong */
{
groet = "Welkom";
}

function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

module.exports = new Script({
processing: {
//prompt: (bot) => bot.say('...'),
receive: () => 'processing'
},

start: {
prompt: (bot) => bot.say(`${groet}`)
receive: (bot,message) => {

  const test = 'Hello World';

  if( test.indexOf('World') >= 0){
    // Found world
    console.log("gevonden")
  }

  const name = message.text;
  return bot.setProp('name', name)
      .then(() => bot.say(`Great! I\'ll call you ${name}`))
      .then(() =>  bot.say(`${groet} - welkom bij Independer.  %[Button label here](http://anyurl.com)`))
      .then(() => 'askName');
}
},

bye: {
prompt: (bot) => bot.say('Geen probleem.'),
receive: () => 'processing'
},

/*
askName: {
prompt: (bot) => bot.say('Hoe heet je?'),
receive: (bot,message) => {
const name = message.text;
return bot.setProp('name', name)
.then(() => bot.say(`Ok ${name}, hoe kan ik je helpen? \n
%[Heb je schade](postback:schade) %[Zoek je een verzekering](postback:zoek)`))
.then(() => 'processing');
},
},
*/

askName: {
    prompt: (bot) => bot.say('Postback is working. What\'s your name?'),
    receive: (bot, message) => {
        const name = message.text;
        return bot.setProp('name', name)
            .then(() => bot.say(`Great! I\'ll call you ${name}`))
            .then(()=> bot.say('Is that OK? %[Yes](postback:yes) %[No](postback:nee)'))
            .then(() => 'speak');
    }
},

error: {
prompt: (bot) => bot.say('Sorry - kun je dat nog eens zeggen?  Er ging iets mis...'),
receive: () => ''
},

finish: {
receive: () => 'finish'
},

  speak: {
          receive: (bot, message) => {

              let upperText = message.text.trim().toUpperCase();

              function updateSilent() {
                  switch (upperText) {
                      case "CONNECT ME":
                          return bot.setProp("silent", true);
                      case "DISCONNECT":
                          return bot.setProp("silent", false);
                      default:
                          return Promise.resolve();
                  }
              }

              function getSilent() {
                  return bot.getProp("silent");
              }

              function processMessage(isSilent) {
                  if (isSilent) {
                      return Promise.resolve("speak");
                  }

                  if (!_.has(scriptRules, upperText)) {
                      return bot.say(`So, I'm good at structured conversations but stickers, emoji and sentences still confuse me. Say 'more' to chat about something else.`).then(() => 'speak');
                  }

                  var response = scriptRules[upperText];
                  var lines = response.split('\n');

                  var p = Promise.resolve();
                  _.each(lines, function(line) {
                      line = line.trim();
                      p = p.then(function() {
                          console.log(line);
                          return wait(50).then(function() {
                              return bot.say(line);
                          });
                      });
                  });

                  return p.then(() => 'speak');
              }

              return updateSilent()
                  .then(getSilent)
                  .then(processMessage);
          }
      }
});
<|endoftext|>


<commit_before>/* global kjua jQuery introJs*/
import { setupFirebasePush } from './notificationService/indexFirebase';
import { sendShownCallback, sendReadCallback} from './notificationService/callback';
import { iFrameListen } from './helpers/iFrameResize';

iFrameListen();

var $contactHPIModal;
var $contactAdminModal;

var $contactHPIModal;
var $contactAdminModal;

if (window.opener && window.opener !== window) {
    window.isInline = true;
}

function toggleMobileNav() {
    document.querySelector('aside.nav-sidebar').classList.toggle('active');
    this.classList.toggle('active');
}

function toggleMobileSearch() {
    document.querySelector('.search-wrapper .input-group').classList.toggle('active');
    document.querySelector('.search-wrapper .mobile-search-toggle .fa').classList.toggle('fa-search');
    document.querySelector('.search-wrapper .mobile-search-toggle .fa').classList.toggle('fa-times');
}

function togglePresentationMode() {
    const contentArea = $('#main-content');
    const toggleButton = $('.btn-fullscreen');
    $('body').toggleClass('fullscreen');
    toggleButton.children('i').toggleClass('fa-compress');
    toggleButton.children('i').toggleClass('fa-expand');
}

let fullscreen = false;

function fullscreenBtnClicked() {
    togglePresentationMode();
    fullscreen = !fullscreen;
    sessionStorage.setItem("fullscreen", JSON.stringify(fullscreen));
}

function sendFeedback(modal, e) {
    let fmodal = $(modal);
    e.preventDefault();

	const type = (fmodal[0].className.includes('contactHPI-modal')) ? 'contactHPI' : 'contactAdmin';
	const subject = (type === 'contactHPI') ? 'Feedback' : `Problem ${fmodal.find('#title').val()}`;
	const title = fmodal.find('#wishTitle').val() || fmodal.find('#problemTitle').val();

    $.ajax({
        url: '/helpdesk',
        type: 'POST',
        data: {
			type,
			subject,
			title,
            category: fmodal.find('#category').val(),
            role: fmodal.find('#role').val(),
            desire: fmodal.find('#desire').val(),
            benefit: fmodal.find("#benefit").val(),
            acceptanceCriteria: fmodal.find("#acceptance_criteria").val(),
            currentState: fmodal.find('#hasHappened').val(),
            targetState: fmodal.find('#supposedToHappen').val()
        },
        success: function (result) {
            showAJAXSuccess("Feedback erfolgreich versendet!", fmodal);
        },
        error: function (result) {
            showAJAXError({}, "Fehler beim senden des Feedbacks", result);
        }
    });
    $('.contactHPI-modal').find('.btn-submit').prop("disabled", true);
};

function showAJAXSuccess(message, modal) {
    modal.modal('hide');
    $.showNotification(message, "success", true);
}

$(document).ready(function () {
    // Init mobile nav
    var mobileNavToggle = document.querySelector('.mobile-nav-toggle');
    var mobileSearchToggle = document.querySelector('.mobile-search-toggle');
    if (mobileNavToggle) {
        mobileNavToggle.addEventListener('click', toggleMobileNav);
    }
    if (mobileSearchToggle) {
        mobileSearchToggle.addEventListener('click', toggleMobileSearch);
    }

    // Init modals
    var $modals = $('.modal');
    $contactHPIModal = document.querySelector('.contactHPI-modal');
    var $featureModal = $('.feature-modal');
    $contactAdminModal = document.querySelector('.contactAdmin-modal');

    $('.submit-contactHPI').on('click', function (e) {
        e.preventDefault();

        $('.contactHPI-modal').find('.btn-submit').prop("disabled", false);
        populateModalForm($($contactHPIModal), {
            title: 'Wunsch oder Problem senden',
            closeLabel: 'Abbrechen',
            submitLabel: 'Senden',
            fields: {
                feedbackType: "userstory"
            }
        });

        $($contactHPIModal).appendTo('body').modal('show');
    });
    $contactHPIModal.querySelector('.modal-form').addEventListener("submit", sendFeedback.bind(this, $contactHPIModal));

    $('.submit-contactAdmin').on('click', function (e) {
        e.preventDefault();

        $('.contactAdmin-modal').find('.btn-submit').prop("disabled", false);
        populateModalForm($($contactAdminModal), {
            title: 'Admin deiner Schule kontaktieren',
            closeLabel: 'Abbrechen',
            submitLabel: 'Senden'
        });
        $($contactAdminModal).appendTo('body').modal('show');
    });

    $contactAdminModal.querySelector('.modal-form').addEventListener("submit", sendFeedback.bind(this, $contactAdminModal));

    $modals.find('.close, .btn-close').on('click', function () {
        $modals.modal('hide');
    });

    $('.notification-dropdown-toggle').on('click', function () {
        $(this).removeClass('recent');

        $('.notification-dropdown .notification-item.unread').each(function () {
            if ($(this).data('read') == true) return;

            sendShownCallback({notificationId: $(this).data('notification-id')});
            sendReadCallback($(this).data('notification-id'));
            $(this).data('read', true);
        });
    });

    $('.btn-create-qr').on('click', function () {
        // create qr code for current page
        let image = kjua({text: window.location.href, render: 'image'});
        let $qrbox = $('.qr-show');
        $qrbox.empty();
        $qrbox.append(image);
    });

    // Init mobile nav
    if (document.getElementById('searchBar') instanceof Object) {
        document.querySelector('.mobile-nav-toggle').addEventListener('click', toggleMobileNav);
        document.querySelector('.mobile-search-toggle').addEventListener('click', toggleMobileSearch);
    }

    if (!fullscreen) {
        fullscreen = JSON.parse(sessionStorage.getItem("fullscreen")) || false;
        if (fullscreen) {
            togglePresentationMode();
        }
    }
    if(document.querySelector('.btn-fullscreen')){
        document.querySelector('.btn-fullscreen').addEventListener('click', fullscreenBtnClicked);
    }

    $('.btn-cancel').on('click', function (e) {
        e.stopPropagation();
        e.preventDefault();
        let $cancelModal = $('.cancel-modal');
        populateModalForm($cancelModal, {
            title: 'Bist du dir sicher, dass du die Änderungen verwerfen möchtest?',
        });
        $cancelModal.appendTo('body').modal('show');
    });

    populateModalForm($featureModal, {
        title: 'Neue Features sind verfügbar',
        closeLabel: 'Abbrechen'
    });

    // from: https://stackoverflow.com/a/187557
    jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function (arg) {
        return function (elem) {
            return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
        };
    });
    // js course search/filter
    $("input.js-search").on("keyup", e => {
        if (e.key === "Escape") $(e.target).val("");
        if (e.key === "Unidentified") {
            return false;
        }
        $(".sc-card-title").find('.title:not(:Contains("' + $(e.target).val() + '"))').parents(".sc-card-wrapper").fadeOut(400);
        $(".sc-card-title").find('.title:Contains("' + $(e.target).val() + '")').parents(".sc-card-wrapper").fadeIn(400);

        return !(e.key === "Unidentified");
    });
});

function showAJAXError(req, textStatus, errorThrown) {
    $($contactHPIModal).modal('hide');
    $($contactAdminModal).modal('hide');
    if (textStatus === "timeout") {
        $.showNotification("Zeitüberschreitung der Anfrage", "warn", true);
    } else {
        $.showNotification(errorThrown, "danger", true);
    }
}

window.addEventListener('DOMContentLoaded', function() {
    if (!/^((?!chrome).)*safari/i.test(navigator.userAgent)) {
        setupFirebasePush();
    }

    let feedbackSelector = document.querySelector('#feedbackType');
    if(feedbackSelector){
        feedbackSelector.onchange = function(){
            if(feedbackSelector.value === "problem"){
                document.getElementById("problemDiv").style.display = "block";
                document.getElementById("userstoryDiv").style.display = "none";
                document.querySelectorAll("#problemDiv input, #problemDiv textarea, #problemDiv select").forEach((node)=>{
                    node.required=true;
                });
                document.querySelectorAll("#userstoryDiv input, #userstoryDiv textarea, #userstoryDiv select").forEach((node)=>{
                    node.required=false;
                });
            } else {
                document.getElementById("problemDiv").style.display = "none";
                document.getElementById("userstoryDiv").style.display = "block";
                document.querySelectorAll("#problemDiv input, #problemDiv textarea, #problemDiv select").forEach((node)=>{
                    node.required=false;
                });
                document.querySelectorAll("#userstoryDiv input, #userstoryDiv textarea, #userstoryDiv select").forEach((node)=>{
                    node.required=true;
                });
                document.getElementById("acceptance_criteria").required = false;
            }
        };
    }
});

// loading animation
document.addEventListener("DOMContentLoaded", function (e) {
    document.querySelector("body").classList.add("loaded");
});
window.addEventListener("beforeunload", function (e) {
    document.querySelector("body").classList.remove("loaded");

});
window.addEventListener("pageshow", function (e) {
    document.querySelector("body").classList.add("loaded");

});

function changeNavBarPositionToAbsolute() {
    var navBar = document.querySelector('.nav-sidebar');
    navBar.classList.add("position-absolute");
}

function changeNavBarPositionToFixed() {
    var navBar = document.querySelector('.nav-sidebar');
    navBar.classList.remove("position-absolute");
}

function startIntro() {
    changeNavBarPositionToAbsolute();
    introJs()
    .setOptions({
        nextLabel: "Weiter",
        prevLabel: "Zurück",
        doneLabel: "Fertig",
        skipLabel: "Überspringen"
    })
    .start()
    .oncomplete(changeNavBarPositionToFixed);
}

window.addEventListener("load", () => {
    var continueTuorial=localStorage.getItem('Tutorial');
    if(continueTuorial=='true') {
        startIntro();
        localStorage.setItem('Tutorial', false);
    }
    if ('serviceWorker' in navigator) {
        // enable sw for half of users only
        let testUserGroup = parseInt(document.getElementById('testUserGroup').value);
        if(testUserGroup == 1) {
            navigator.serviceWorker.register('/sw.js');
        }
    }
    document.getElementById("intro-loggedin").addEventListener("click", startIntro, false);
});

document.querySelectorAll('#main-content a').forEach((a) => {
    const href = a.getAttribute('href');
    if (a.querySelector('img, .fa') == null && href) {
        if (!(href.startsWith('https://schul-cloud.org') || href.startsWith('#') || href.startsWith('/') || href === '')) {
            if (!a.getAttribute('target')) {
                a.setAttribute('target', '_blank');
            }
            a.classList.add('externalLink');
        }
    }
});
<commit_msg>fix helpdesk submissions<commit_after>/* global kjua jQuery introJs*/
import { setupFirebasePush } from './notificationService/indexFirebase';
import { sendShownCallback, sendReadCallback} from './notificationService/callback';
import { iFrameListen } from './helpers/iFrameResize';

iFrameListen();

var $contactHPIModal;
var $contactAdminModal;

var $contactHPIModal;
var $contactAdminModal;

if (window.opener && window.opener !== window) {
    window.isInline = true;
}

function toggleMobileNav() {
    document.querySelector('aside.nav-sidebar').classList.toggle('active');
    this.classList.toggle('active');
}

function toggleMobileSearch() {
    document.querySelector('.search-wrapper .input-group').classList.toggle('active');
    document.querySelector('.search-wrapper .mobile-search-toggle .fa').classList.toggle('fa-search');
    document.querySelector('.search-wrapper .mobile-search-toggle .fa').classList.toggle('fa-times');
}

function togglePresentationMode() {
    const contentArea = $('#main-content');
    const toggleButton = $('.btn-fullscreen');
    $('body').toggleClass('fullscreen');
    toggleButton.children('i').toggleClass('fa-compress');
    toggleButton.children('i').toggleClass('fa-expand');
}

let fullscreen = false;

function fullscreenBtnClicked() {
    togglePresentationMode();
    fullscreen = !fullscreen;
    sessionStorage.setItem("fullscreen", JSON.stringify(fullscreen));
}

function sendFeedback(modal, e) {
    let fmodal = $(modal);
    e.preventDefault();

	const type = (fmodal[0].className.includes('contactHPI-modal')) ? 'contactHPI' : 'contactAdmin';
	const subject = (type === 'contactHPI') ? 'Feedback' : `Problem ${fmodal.find('#title').val()}`;
	const title = fmodal.find('#wishTitle').val() || fmodal.find('#problemTitle').val();

    $.ajax({
        url: '/helpdesk',
        type: 'POST',
        data: {
			type,
			subject,
			title,
            category: fmodal.find('#category').val(),
            role: fmodal.find('#role').val(),
            desire: fmodal.find('#desire').val(),
            benefit: fmodal.find("#benefit").val(),
            acceptanceCriteria: fmodal.find("#acceptance_criteria").val(),
            currentState: fmodal.find('#hasHappened').val(),
            targetState: fmodal.find('#supposedToHappen').val()
        },
        beforeSend(xhr) {
			xhr.setRequestHeader('Csrf-Token', csrftoken);
		},
        success: function (result) {
            showAJAXSuccess("Feedback erfolgreich versendet!", fmodal);
        },
        error: function (result) {
            showAJAXError({}, "Fehler beim senden des Feedbacks", result);
        }
    });
    $('.contactHPI-modal').find('.btn-submit').prop("disabled", true);
};

function showAJAXSuccess(message, modal) {
    modal.modal('hide');
    $.showNotification(message, "success", true);
}

$(document).ready(function () {
    // Init mobile nav
    var mobileNavToggle = document.querySelector('.mobile-nav-toggle');
    var mobileSearchToggle = document.querySelector('.mobile-search-toggle');
    if (mobileNavToggle) {
        mobileNavToggle.addEventListener('click', toggleMobileNav);
    }
    if (mobileSearchToggle) {
        mobileSearchToggle.addEventListener('click', toggleMobileSearch);
    }

    // Init modals
    var $modals = $('.modal');
    $contactHPIModal = document.querySelector('.contactHPI-modal');
    var $featureModal = $('.feature-modal');
    $contactAdminModal = document.querySelector('.contactAdmin-modal');

    $('.submit-contactHPI').on('click', function (e) {
        e.preventDefault();

        $('.contactHPI-modal').find('.btn-submit').prop("disabled", false);
        populateModalForm($($contactHPIModal), {
            title: 'Wunsch oder Problem senden',
            closeLabel: 'Abbrechen',
            submitLabel: 'Senden',
            fields: {
                feedbackType: "userstory"
            }
        });

        $($contactHPIModal).appendTo('body').modal('show');
    });
    $contactHPIModal.querySelector('.modal-form').addEventListener("submit", sendFeedback.bind(this, $contactHPIModal));

    $('.submit-contactAdmin').on('click', function (e) {
        e.preventDefault();

        $('.contactAdmin-modal').find('.btn-submit').prop("disabled", false);
        populateModalForm($($contactAdminModal), {
            title: 'Admin deiner Schule kontaktieren',
            closeLabel: 'Abbrechen',
            submitLabel: 'Senden'
        });
        $($contactAdminModal).appendTo('body').modal('show');
    });

    $contactAdminModal.querySelector('.modal-form').addEventListener("submit", sendFeedback.bind(this, $contactAdminModal));

    $modals.find('.close, .btn-close').on('click', function () {
        $modals.modal('hide');
    });

    $('.notification-dropdown-toggle').on('click', function () {
        $(this).removeClass('recent');

        $('.notification-dropdown .notification-item.unread').each(function () {
            if ($(this).data('read') == true) return;

            sendShownCallback({notificationId: $(this).data('notification-id')});
            sendReadCallback($(this).data('notification-id'));
            $(this).data('read', true);
        });
    });

    $('.btn-create-qr').on('click', function () {
        // create qr code for current page
        let image = kjua({text: window.location.href, render: 'image'});
        let $qrbox = $('.qr-show');
        $qrbox.empty();
        $qrbox.append(image);
    });

    // Init mobile nav
    if (document.getElementById('searchBar') instanceof Object) {
        document.querySelector('.mobile-nav-toggle').addEventListener('click', toggleMobileNav);
        document.querySelector('.mobile-search-toggle').addEventListener('click', toggleMobileSearch);
    }

    if (!fullscreen) {
        fullscreen = JSON.parse(sessionStorage.getItem("fullscreen")) || false;
        if (fullscreen) {
            togglePresentationMode();
        }
    }
    if(document.querySelector('.btn-fullscreen')){
        document.querySelector('.btn-fullscreen').addEventListener('click', fullscreenBtnClicked);
    }

    $('.btn-cancel').on('click', function (e) {
        e.stopPropagation();
        e.preventDefault();
        let $cancelModal = $('.cancel-modal');
        populateModalForm($cancelModal, {
            title: 'Bist du dir sicher, dass du die Änderungen verwerfen möchtest?',
        });
        $cancelModal.appendTo('body').modal('show');
    });

    populateModalForm($featureModal, {
        title: 'Neue Features sind verfügbar',
        closeLabel: 'Abbrechen'
    });

    // from: https://stackoverflow.com/a/187557
    jQuery.expr[":"].Contains = jQuery.expr.createPseudo(function (arg) {
        return function (elem) {
            return jQuery(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
        };
    });
    // js course search/filter
    $("input.js-search").on("keyup", e => {
        if (e.key === "Escape") $(e.target).val("");
        if (e.key === "Unidentified") {
            return false;
        }
        $(".sc-card-title").find('.title:not(:Contains("' + $(e.target).val() + '"))').parents(".sc-card-wrapper").fadeOut(400);
        $(".sc-card-title").find('.title:Contains("' + $(e.target).val() + '")').parents(".sc-card-wrapper").fadeIn(400);

        return !(e.key === "Unidentified");
    });
});

function showAJAXError(req, textStatus, errorThrown) {
    $($contactHPIModal).modal('hide');
    $($contactAdminModal).modal('hide');
    if (textStatus === "timeout") {
        $.showNotification("Zeitüberschreitung der Anfrage", "warn", true);
    } else {
        $.showNotification(errorThrown, "danger", true);
    }
}

window.addEventListener('DOMContentLoaded', function() {
    if (!/^((?!chrome).)*safari/i.test(navigator.userAgent)) {
        setupFirebasePush();
    }

    let feedbackSelector = document.querySelector('#feedbackType');
    if(feedbackSelector){
        feedbackSelector.onchange = function(){
            if(feedbackSelector.value === "problem"){
                document.getElementById("problemDiv").style.display = "block";
                document.getElementById("userstoryDiv").style.display = "none";
                document.querySelectorAll("#problemDiv input, #problemDiv textarea, #problemDiv select").forEach((node)=>{
                    node.required=true;
                });
                document.querySelectorAll("#userstoryDiv input, #userstoryDiv textarea, #userstoryDiv select").forEach((node)=>{
                    node.required=false;
                });
            } else {
                document.getElementById("problemDiv").style.display = "none";
                document.getElementById("userstoryDiv").style.display = "block";
                document.querySelectorAll("#problemDiv input, #problemDiv textarea, #problemDiv select").forEach((node)=>{
                    node.required=false;
                });
                document.querySelectorAll("#userstoryDiv input, #userstoryDiv textarea, #userstoryDiv select").forEach((node)=>{
                    node.required=true;
                });
                document.getElementById("acceptance_criteria").required = false;
            }
        };
    }
});

// loading animation
document.addEventListener("DOMContentLoaded", function (e) {
    document.querySelector("body").classList.add("loaded");
});
window.addEventListener("beforeunload", function (e) {
    document.querySelector("body").classList.remove("loaded");

});
window.addEventListener("pageshow", function (e) {
    document.querySelector("body").classList.add("loaded");

});

function changeNavBarPositionToAbsolute() {
    var navBar = document.querySelector('.nav-sidebar');
    navBar.classList.add("position-absolute");
}

function changeNavBarPositionToFixed() {
    var navBar = document.querySelector('.nav-sidebar');
    navBar.classList.remove("position-absolute");
}

function startIntro() {
    changeNavBarPositionToAbsolute();
    introJs()
    .setOptions({
        nextLabel: "Weiter",
        prevLabel: "Zurück",
        doneLabel: "Fertig",
        skipLabel: "Überspringen"
    })
    .start()
    .oncomplete(changeNavBarPositionToFixed);
}

window.addEventListener("load", () => {
    var continueTuorial=localStorage.getItem('Tutorial');
    if(continueTuorial=='true') {
        startIntro();
        localStorage.setItem('Tutorial', false);
    }
    if ('serviceWorker' in navigator) {
        // enable sw for half of users only
        let testUserGroup = parseInt(document.getElementById('testUserGroup').value);
        if(testUserGroup == 1) {
            navigator.serviceWorker.register('/sw.js');
        }
    }
    document.getElementById("intro-loggedin").addEventListener("click", startIntro, false);
});

document.querySelectorAll('#main-content a').forEach((a) => {
    const href = a.getAttribute('href');
    if (a.querySelector('img, .fa') == null && href) {
        if (!(href.startsWith('https://schul-cloud.org') || href.startsWith('#') || href.startsWith('/') || href === '')) {
            if (!a.getAttribute('target')) {
                a.setAttribute('target', '_blank');
            }
            a.classList.add('externalLink');
        }
    }
});
<|endoftext|>


<commit_before>/*jslint node: true, white: true, nomen: true */

var gulp = require('gulp'),
    gutil = require('gulp-util'),
    karma = require('karma').server,
    webpack = require('webpack');

gulp.task('test', ["webpack"], function(done) {
  "use strict";
  karma.start({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done);
});

gulp.task('tdd', function (done) {
  "use strict";
  karma.start({
    configFile: __dirname + '/karma.conf.js',
    singleRun: false
  }, done);
});

gulp.task("webpack", function(callback) {
  "use strict";
    // run webpack
  webpack({
    entry: "./src/structure-assertions.js",
    output: {
        path: __dirname + '/dist',
        filename: "structure-assertions.js"
    },
  }, function(err, stats) {
      if(err) {
        throw new gutil.PluginError("webpack", err);
      }

      gutil.log("[webpack]", stats.toString({
          // output options
      }));
      callback();
  });
});

gulp.task("serve", ["webpack"], function() {
  "use strict";

	gulp.watch(["src/**/*"], ["webpack"]);
});
<commit_msg>renamed serve task to just watch instead<commit_after>/*jslint node: true, white: true, nomen: true */

var gulp = require('gulp'),
    gutil = require('gulp-util'),
    karma = require('karma').server,
    webpack = require('webpack');

gulp.task('test', ["webpack"], function(done) {
  "use strict";
  karma.start({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done);
});

gulp.task('tdd', function (done) {
  "use strict";
  karma.start({
    configFile: __dirname + '/karma.conf.js',
    singleRun: false
  }, done);
});

gulp.task("webpack", function(callback) {
  "use strict";
    // run webpack
  webpack({
    entry: "./src/structure-assertions.js",
    output: {
        path: __dirname + '/dist',
        filename: "structure-assertions.js"
    },
  }, function(err, stats) {
      if(err) {
        throw new gutil.PluginError("webpack", err);
      }

      gutil.log("[webpack]", stats.toString({
          // output options
      }));
      callback();
  });
});

gulp.task("watch", ["webpack"], function() {
  "use strict";

	gulp.watch(["src/**/*"], ["webpack"]);
});
<|endoftext|>


<commit_before>'use strict';

const mappings = require('./mappings');
const utils = require('./utils');
const postProcessing = require('./post-processing');

function merge(query, next) {

	if (Array.isArray(query)) {
		return query.map(q => merge(q, next))
	}
	if (query.aggregator) {
		query.body.forEach(q => {
			merge(q, next);
		})
		return query;
	} else if (query.functions) {
		query.functions = query.functions.concat(next);
		return query;
	} else {
		return [query].concat(next)
	}
}

function parseFilter (filter) {
	let filterConf = /^([^!~\?]*)(=|!=|>|<|~|!~|\?|!\?)([^=~\?]*)$/.exec(filter);
	if (filterConf) {
		if (!filterConf[1] && !filterConf[3]) {
			throw new Error(`Filter ${filter} must specify a property name to filter on and a value to match e.g. ->filter(user.uuid${filter}abcd)`);
		}
		if (!filterConf[1]) {
			throw new Error(`Filter ${filter} must specify a property name to filter on on the left hand side e.g. ->filter(user.uuid${filter})`);
		}
		if (!filterConf[3]) {
			if (filterConf[2] === '?' || filterConf[2] === '!?') {
				throw new Error(`Filter ${filter} must specify one or more comma-separated values to match on the right hand side e.g ->filter(${filter}apple,pear,cherry)`);
			} else {
				throw new Error(`Filter ${filter} must specify a value to match on the right hand side e.g ->filter(${filter}apple)`);
			}
		}

		if (filterConf[2] === '!?') {
			// TODO be carefuller around quoted commas
			return filterConf[3]
				.split(',')
				.map(val => {
					return {
						property_name: filterConf[1],
						operator: mappings.filterShorthands['!='].operator,
						property_value: utils.transformValue(val)
					}
				});
		}

		return {
			property_name: filterConf[1],
			operator: mappings.filterShorthands[filterConf[2]].operator,
			property_value: utils.transformValue(filterConf[3], mappings.filterShorthands[filterConf[2]].handleList)
		};
	}

	const unary = /^(\!)?(.+)$/.exec(filter);
	if (unary) {
		return {
			property_name: unary[2],
			operator: 'exists',
			property_value: !unary[1]
		};
	}

	throw new Error('Filter structure not recognised');
}

function analyseFunctionCall (func, body) {
	func = func.trim();
	if (func.charAt(0) === '@') {
		return {aggregator: func, body};
	}
	if (typeof body === 'string') {
		if (/^('|").*\1/.test(body)) {
			body = body.substr(1, body.length -2);
		}
		if (body) {
			if (func === 'filter') {
				body = [parseFilter(body)];
			} else {
				body = body.split(',').map(str => str.trim())
			}
		} else {
			body = [];
		}
	}
	return {func, params: body};
}

const extractionNames = ['with', 'min', 'max', 'sum', 'avg', 'med', 'count', 'select', 'median', 'pct', 'percentile'];

function validateStructure (conf) {
	return typeof conf !== 'string';
}

function validateExtraction (conf) {
	if (conf.func !== 'count' && !conf.params.length) {
		throw new Error(`Extraction ${conf.func} must be passed a property e.g. ->${conf.func}(user.uuid)`);
	}
}

function validateParams (conf, length) {
	if (!conf.params.length) {
		throw new Error(`${conf.func} must be passed a value e.g. ->${conf.func}(thingummy)`);
	}

	if (length && conf.params.length !== length) {
		throw new Error(`${conf.func} must be passed ${length} values`);
	}
	return true;
}

const reduceStrategies = Object.assign({all: true}, require('./post-processing/reduce').reduceStrategies);

function validateReduce(conf) {
	if (!reduceStrategies[conf.params[0]]) {
		throw new Error(`Invalid reduction ${conf.params[0]}`);
	}
}

// function validateSort(conf) {
	// TODO proper sort validation
	// if (!reduceStrategies[conf.params[0]]) {
	// 	throw new Error(`Invalid reduction ${conf.params[0]}`);
	// }
// }

function validateRelTime(conf) {
	if (Number(conf.params[0]) && parseInt(conf.params[0], 10) !== Number(conf.params[0])) {
		throw new Error('->relTime() must be passed an integer to specify number of days');
	}

	if (!/(this_|previous_)?\d+(_(minutes|hours|days|weeks|months|years))?/.test(conf.params[0])) {
		throw new Error('Invalid timeframe string passed to ->relTime() (see cheatsheet or ask a dev)');
	}

}

const acceptedIntervals = Object.keys(mappings.intervalUnits)
	.reduce((arr, k) => {
		return arr.concat([k, mappings.intervalUnits[k]]);
	}, []);

const acceptedIntervalsString = Object.keys(mappings.intervalUnits)
	.map(k => {
		return `${k} or ${mappings.intervalUnits[k]}`;
	}).join(', ');

function validateInterval(conf) {
	const intervalUnit = conf.params[0].replace(/^(every_)?\d+_/, '');
	if (acceptedIntervals.indexOf(intervalUnit) === -1) {
		throw new Error(`Invalid interval passed in to ->interval(). Accepted values are ${acceptedIntervalsString} (possibly prefixed by e.g. '2_')`);
	}
}

function validateAbsTime(conf) {
	conf.params.forEach(str => {
		const t = new Date(str).getTime();
		if (t !== t) { // NaN
			throw new Error('Invalid date string passed to ->absTime()');
		}
	})
}

function validateRound(conf) {
	if (conf.params.length) {
		if (conf.params.length > 1) {
			throw new Error('->round() only accepts a single number');
		}
		if (parseInt(conf.params[0], 10) !== Number(conf.params[0])) {
			throw new Error('->round() must be passed an integer');
		}
	}
}

function validateFunction (conf) {
	const name = typeof conf === 'string' ? conf : conf.func;
	const isValidStructure = validateStructure(conf);

	if (extractionNames.indexOf(name) > -1) {
		return isValidStructure && validateExtraction(conf);
	}

	if (['filter', 'group'].indexOf(name) > -1) {
		return isValidStructure && validateParams(conf);
	}

	if (name === 'reduce') {
		return isValidStructure && validateParams(conf) && validateReduce(conf);
	}

	if (['sortAsc', 'sortDesc', 'plotThreshold', 'cutoff', 'top', 'bottom', 'sortProp', 'relabel'].indexOf(name) > -1) {
		return isValidStructure;
		// return isValidStructure && validateSort(conf);
	}

	if (name === 'multiply' || name === 'divide') {
		return isValidStructure && validateParams(conf);
	}

	if (name === 'relTime') {
		return isValidStructure && validateParams(conf, 1) && validateRelTime(conf);
	}

	if (name === 'absTime') {
		return isValidStructure && validateParams(conf, 2) && validateAbsTime(conf);
	}

	if (name === 'interval') {
		return isValidStructure && validateParams(conf, 1) && validateInterval(conf);
	}

	if (name === 'round') {
		return isValidStructure && validateRound(conf);
	}

	if (['raw', 'tidy'].indexOf(name) > -1) {
		return isValidStructure;
	}

	if (isValidStructure) {
		throw new Error(`Invalid function name ${name}`);
	} else {
		const func = conf.replace(/\([^)]*$/, '');
		throw new Error(`Function ${func} must end with opening and closing parantheses e.g. ->${func}() or ->${func}(thing)`);
	}
}

class Deconstructor {
	constructor (aggregator, parent) {
		this.aggregator = aggregator;
		this.openSingleQuotes = 0;
		this.openDoubleQuotes = 0;
		this.parent = parent;
	}

	deconstructAggregator (str) {

		this.str = str.trim();

		// if (str.charAt(0) === '@') {
		// 	const query = this.deconstruct(str);
		// 	query.print = !this.parent && this.print
		// 	query.postProcessing = this.postProcessing;
		// 	return query;
		// }

		const queries = [];
		while(this.str) {
			const str = this.str.trim();
			this.str = null;
			queries.push(str.charAt(0) === '@' ? this.deconstruct(str) : this.deconstructQuery(str));
		}

		if (str.charAt(0) === '@' && queries.length === 1) {
			const query = queries[0];
			query.print = !this.parent && this.print
			query.postProcessing = this.postProcessing;
			return query;
		}

		return queries;
	}

	deconstructQuery (str) {
		str = str.trim();
		if (str.charAt(0) === '@') {
			return this.deconstructAggregator(str);
		}
		const firstCut = str.split('->');
		const eventName = firstCut.shift().trim();
		if (!eventName || eventName.indexOf('(') > -1) {
			throw new Error('Queries must begin with an event name e.g. page:view, dwell');
		}

		str = firstCut.join('->');

		let functions = this.deconstruct(str);
		// take care of the edge case where e.g. we want to validate dwell->count
		if (typeof functions === 'string') {
			functions = [functions];
		}
		if (!Array.isArray(functions) || !functions.some(conf => {
			return extractionNames.indexOf(conf.func) > -1
		})) {
			throw new Error('An extraction e.g. ->count(), must be specified');
		}

		if (functions.filter(conf => {
			return extractionNames.indexOf(conf.func) > -1
		}).length > 1) {
			throw new Error('Only one extraction e.g. ->count(), must be specified');
		}

		this.postProcessing = this.postProcessing || [];
		functions.forEach(validateFunction);
		this.postProcessing.forEach(validateFunction);

		return {
			event: eventName,
			functions: functions,
			print: this.print,
			postProcessing: this.postProcessing || []
		}
	}

	setPrint (method) {
		if (this.parent) {
			this.parent.setPrint(method)
		} else {
			this.print = method;
		}
	}

	setReduction (opts) {
		this.postProcessing = this.postProcessing || [];
		this.postProcessing.push(opts);
	}

	deconstruct (str) {
		str = str.trim();
		if (!str) {
			return str;
		}

		if (this.aggregator && str.indexOf(',') === 0) {
			this.str = str.substr(1);
			return;
		}

		str = str.replace(/^->/, '');

		let openParantheses = 0;
		let char;
		let func;
		let body;

		for (let i = 0, il = str.length; i < il; i++) {
			char = str.charAt(i);
			switch (char) {
				case '(':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses++;
					break;
				case ')':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses--;
					break;
				case '"':
					!this.openSingleQuotes && (this.openDoubleQuotes = Math.abs(this.openDoubleQuotes - 1));
					break;
				case '\'':
					!this.openDoubleQuotes && (this.openSingleQuotes = Math.abs(this.openSingleQuotes - 1));
					break;
				case 'default':
					break;
			}

			if (openParantheses && !func) {
				func = str.substr(0, i);
			} else if (func && !openParantheses && !body) {
				body = str.substr(func.length + 1, i - (func.length + 1))
				if (func.charAt(0) === '@') {
					body = new Deconstructor(func.substr(1), this).deconstructAggregator(body);
				}
				if (func === 'print') {
					this.setPrint(body);
					return;
				}

				let res = analyseFunctionCall(func, body);
				if (postProcessing.isPostProcessor(func)) {
					this.setReduction(res);
				}

				const next = this.deconstruct(str.substr(i + 1), this.openSingleQuotes, this.openDoubleQuotes)
				if (postProcessing.isPostProcessor(func)) {
					return next;
				} else {
					return next ? merge(res, next) : res.aggregator ? res : [res]
				}

			}
		}
		return str;
	}
}

function deconstruct (str) {
	str = str.trim();
	const deconstructor = new Deconstructor();
	deconstructor.str = str;
	const conf = deconstructor.deconstructQuery(str);
	return conf;
}

module.exports = deconstruct;
module.exports.parseFilter = parseFilter;
<commit_msg>fix parsing of filters with unusual characters<commit_after>'use strict';

const mappings = require('./mappings');
const utils = require('./utils');
const postProcessing = require('./post-processing');

function merge(query, next) {

	if (Array.isArray(query)) {
		return query.map(q => merge(q, next))
	}
	if (query.aggregator) {
		query.body.forEach(q => {
			merge(q, next);
		})
		return query;
	} else if (query.functions) {
		query.functions = query.functions.concat(next);
		return query;
	} else {
		return [query].concat(next)
	}
}

function parseFilter (filter) {
	let filterConf = /^([^!~\?]*)(=|!=|>|<|~|!~|\?|!\?)(.*)$/.exec(filter);
	if (filterConf) {
		if (!filterConf[1] && !filterConf[3]) {
			throw new Error(`Filter ${filter} must specify a property name to filter on and a value to match e.g. ->filter(user.uuid${filter}abcd)`);
		}
		if (!filterConf[1]) {
			throw new Error(`Filter ${filter} must specify a property name to filter on on the left hand side e.g. ->filter(user.uuid${filter})`);
		}
		if (!filterConf[3]) {
			if (filterConf[2] === '?' || filterConf[2] === '!?') {
				throw new Error(`Filter ${filter} must specify one or more comma-separated values to match on the right hand side e.g ->filter(${filter}apple,pear,cherry)`);
			} else {
				throw new Error(`Filter ${filter} must specify a value to match on the right hand side e.g ->filter(${filter}apple)`);
			}
		}

		if (filterConf[2] === '!?') {
			// TODO be carefuller around quoted commas
			return filterConf[3]
				.split(',')
				.map(val => {
					return {
						property_name: filterConf[1],
						operator: mappings.filterShorthands['!='].operator,
						property_value: utils.transformValue(val)
					}
				});
		}

		return {
			property_name: filterConf[1],
			operator: mappings.filterShorthands[filterConf[2]].operator,
			property_value: utils.transformValue(filterConf[3], mappings.filterShorthands[filterConf[2]].handleList)
		};
	}

	const unary = /^(\!)?(.+)$/.exec(filter);
	if (unary) {
		return {
			property_name: unary[2],
			operator: 'exists',
			property_value: !unary[1]
		};
	}

	throw new Error('Filter structure not recognised');
}

function analyseFunctionCall (func, body) {
	func = func.trim();
	if (func.charAt(0) === '@') {
		return {aggregator: func, body};
	}
	if (typeof body === 'string') {
		if (/^('|").*\1/.test(body)) {
			body = body.substr(1, body.length -2);
		}
		if (body) {
			if (func === 'filter') {
				body = [parseFilter(body)];
			} else {
				body = body.split(',').map(str => str.trim())
			}
		} else {
			body = [];
		}
	}
	return {func, params: body};
}

const extractionNames = ['with', 'min', 'max', 'sum', 'avg', 'med', 'count', 'select', 'median', 'pct', 'percentile'];

function validateStructure (conf) {
	return typeof conf !== 'string';
}

function validateExtraction (conf) {
	if (conf.func !== 'count' && !conf.params.length) {
		throw new Error(`Extraction ${conf.func} must be passed a property e.g. ->${conf.func}(user.uuid)`);
	}
}

function validateParams (conf, length) {
	if (!conf.params.length) {
		throw new Error(`${conf.func} must be passed a value e.g. ->${conf.func}(thingummy)`);
	}

	if (length && conf.params.length !== length) {
		throw new Error(`${conf.func} must be passed ${length} values`);
	}
	return true;
}

const reduceStrategies = Object.assign({all: true}, require('./post-processing/reduce').reduceStrategies);

function validateReduce(conf) {
	if (!reduceStrategies[conf.params[0]]) {
		throw new Error(`Invalid reduction ${conf.params[0]}`);
	}
}

// function validateSort(conf) {
	// TODO proper sort validation
	// if (!reduceStrategies[conf.params[0]]) {
	// 	throw new Error(`Invalid reduction ${conf.params[0]}`);
	// }
// }

function validateRelTime(conf) {
	if (Number(conf.params[0]) && parseInt(conf.params[0], 10) !== Number(conf.params[0])) {
		throw new Error('->relTime() must be passed an integer to specify number of days');
	}

	if (!/(this_|previous_)?\d+(_(minutes|hours|days|weeks|months|years))?/.test(conf.params[0])) {
		throw new Error('Invalid timeframe string passed to ->relTime() (see cheatsheet or ask a dev)');
	}

}

const acceptedIntervals = Object.keys(mappings.intervalUnits)
	.reduce((arr, k) => {
		return arr.concat([k, mappings.intervalUnits[k]]);
	}, []);

const acceptedIntervalsString = Object.keys(mappings.intervalUnits)
	.map(k => {
		return `${k} or ${mappings.intervalUnits[k]}`;
	}).join(', ');

function validateInterval(conf) {
	const intervalUnit = conf.params[0].replace(/^(every_)?\d+_/, '');
	if (acceptedIntervals.indexOf(intervalUnit) === -1) {
		throw new Error(`Invalid interval passed in to ->interval(). Accepted values are ${acceptedIntervalsString} (possibly prefixed by e.g. '2_')`);
	}
}

function validateAbsTime(conf) {
	conf.params.forEach(str => {
		const t = new Date(str).getTime();
		if (t !== t) { // NaN
			throw new Error('Invalid date string passed to ->absTime()');
		}
	})
}

function validateRound(conf) {
	if (conf.params.length) {
		if (conf.params.length > 1) {
			throw new Error('->round() only accepts a single number');
		}
		if (parseInt(conf.params[0], 10) !== Number(conf.params[0])) {
			throw new Error('->round() must be passed an integer');
		}
	}
}

function validateFunction (conf) {
	const name = typeof conf === 'string' ? conf : conf.func;
	const isValidStructure = validateStructure(conf);

	if (extractionNames.indexOf(name) > -1) {
		return isValidStructure && validateExtraction(conf);
	}

	if (['filter', 'group'].indexOf(name) > -1) {
		return isValidStructure && validateParams(conf);
	}

	if (name === 'reduce') {
		return isValidStructure && validateParams(conf) && validateReduce(conf);
	}

	if (['sortAsc', 'sortDesc', 'plotThreshold', 'cutoff', 'top', 'bottom', 'sortProp', 'relabel'].indexOf(name) > -1) {
		return isValidStructure;
		// return isValidStructure && validateSort(conf);
	}

	if (name === 'multiply' || name === 'divide') {
		return isValidStructure && validateParams(conf);
	}

	if (name === 'relTime') {
		return isValidStructure && validateParams(conf, 1) && validateRelTime(conf);
	}

	if (name === 'absTime') {
		return isValidStructure && validateParams(conf, 2) && validateAbsTime(conf);
	}

	if (name === 'interval') {
		return isValidStructure && validateParams(conf, 1) && validateInterval(conf);
	}

	if (name === 'round') {
		return isValidStructure && validateRound(conf);
	}

	if (['raw', 'tidy'].indexOf(name) > -1) {
		return isValidStructure;
	}

	if (isValidStructure) {
		throw new Error(`Invalid function name ${name}`);
	} else {
		const func = conf.replace(/\([^)]*$/, '');
		throw new Error(`Function ${func} must end with opening and closing parantheses e.g. ->${func}() or ->${func}(thing)`);
	}
}

class Deconstructor {
	constructor (aggregator, parent) {
		this.aggregator = aggregator;
		this.openSingleQuotes = 0;
		this.openDoubleQuotes = 0;
		this.parent = parent;
	}

	deconstructAggregator (str) {

		this.str = str.trim();

		// if (str.charAt(0) === '@') {
		// 	const query = this.deconstruct(str);
		// 	query.print = !this.parent && this.print
		// 	query.postProcessing = this.postProcessing;
		// 	return query;
		// }

		const queries = [];
		while(this.str) {
			const str = this.str.trim();
			this.str = null;
			queries.push(str.charAt(0) === '@' ? this.deconstruct(str) : this.deconstructQuery(str));
		}

		if (str.charAt(0) === '@' && queries.length === 1) {
			const query = queries[0];
			query.print = !this.parent && this.print
			query.postProcessing = this.postProcessing;
			return query;
		}

		return queries;
	}

	deconstructQuery (str) {
		str = str.trim();
		if (str.charAt(0) === '@') {
			return this.deconstructAggregator(str);
		}
		const firstCut = str.split('->');
		const eventName = firstCut.shift().trim();
		if (!eventName || eventName.indexOf('(') > -1) {
			throw new Error('Queries must begin with an event name e.g. page:view, dwell');
		}

		str = firstCut.join('->');

		let functions = this.deconstruct(str);
		// take care of the edge case where e.g. we want to validate dwell->count
		if (typeof functions === 'string') {
			functions = [functions];
		}
		if (!Array.isArray(functions) || !functions.some(conf => {
			return extractionNames.indexOf(conf.func) > -1
		})) {
			throw new Error('An extraction e.g. ->count(), must be specified');
		}

		if (functions.filter(conf => {
			return extractionNames.indexOf(conf.func) > -1
		}).length > 1) {
			throw new Error('Only one extraction e.g. ->count(), must be specified');
		}

		this.postProcessing = this.postProcessing || [];
		functions.forEach(validateFunction);
		this.postProcessing.forEach(validateFunction);

		return {
			event: eventName,
			functions: functions,
			print: this.print,
			postProcessing: this.postProcessing || []
		}
	}

	setPrint (method) {
		if (this.parent) {
			this.parent.setPrint(method)
		} else {
			this.print = method;
		}
	}

	setReduction (opts) {
		this.postProcessing = this.postProcessing || [];
		this.postProcessing.push(opts);
	}

	deconstruct (str) {
		str = str.trim();
		if (!str) {
			return str;
		}

		if (this.aggregator && str.indexOf(',') === 0) {
			this.str = str.substr(1);
			return;
		}

		str = str.replace(/^->/, '');

		let openParantheses = 0;
		let char;
		let func;
		let body;

		for (let i = 0, il = str.length; i < il; i++) {
			char = str.charAt(i);
			switch (char) {
				case '(':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses++;
					break;
				case ')':
					!this.openDoubleQuotes && !this.openSingleQuotes && openParantheses--;
					break;
				case '"':
					!this.openSingleQuotes && (this.openDoubleQuotes = Math.abs(this.openDoubleQuotes - 1));
					break;
				case '\'':
					!this.openDoubleQuotes && (this.openSingleQuotes = Math.abs(this.openSingleQuotes - 1));
					break;
				case 'default':
					break;
			}

			if (openParantheses && !func) {
				func = str.substr(0, i);
			} else if (func && !openParantheses && !body) {
				body = str.substr(func.length + 1, i - (func.length + 1))
				if (func.charAt(0) === '@') {
					body = new Deconstructor(func.substr(1), this).deconstructAggregator(body);
				}
				if (func === 'print') {
					this.setPrint(body);
					return;
				}

				let res = analyseFunctionCall(func, body);
				if (postProcessing.isPostProcessor(func)) {
					this.setReduction(res);
				}

				const next = this.deconstruct(str.substr(i + 1), this.openSingleQuotes, this.openDoubleQuotes)
				if (postProcessing.isPostProcessor(func)) {
					return next;
				} else {
					return next ? merge(res, next) : res.aggregator ? res : [res]
				}

			}
		}
		return str;
	}
}

function deconstruct (str) {
	str = str.trim();
	const deconstructor = new Deconstructor();
	deconstructor.str = str;
	const conf = deconstructor.deconstructQuery(str);
	return conf;
}

module.exports = deconstruct;
module.exports.parseFilter = parseFilter;
<|endoftext|>