<commit_before>window.up = window.up || {};

window.up.startSurveyPortlet = function(window, _, params) {
    'use strict';

    var surveyName = params.surveyName || '';
    var n = params.n;

    var MODULE_NAME = n + '-survey-portlet';
    var USER = 'admin';

    if (!window.angular) {
        var ANGULAR_SCRIPT_ID = 'angular-uportal-script';

        var scr = document.getElementById(ANGULAR_SCRIPT_ID);

        if (!scr) {
            scr = document.createElement('script');
            scr.id =  ANGULAR_SCRIPT_ID;
            scr.type =  'text/javascript';
            scr.async =  true;
            scr.charset =  'utf-8';
            scr.src =  'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js';

            document.body.appendChild(scr);
        }

        scr.addEventListener('load', bootstrap);
    } else {
        register(window.up.ngApp);
    }

    function bootstrap() {
        var app = angular.module(MODULE_NAME, []);
        register(app);
        angular.bootstrap(document.getElementById(n + '-survey-portlet'), [MODULE_NAME]);
    }

    function register(app) {
        app
        .service('StudentProfile', function($http, $q, $timeout) {
            var sp = this;

            var PROFILE_ROOT = 'https://portal-mock-api-dev.herokuapp.com/api/';

            /**
             * @ngdoc
             * @methodOf cccPortal.service:StudentProfile
             * @name cccPortal.service:StudentProfile#get
             * @param {String} endpoint The endpoint/data type to query.
             * @param {Object} cfg Additional configuration for the request.
             * Most useful for params.
             * @description Get some data from the user profile.
             * @returns {Promise} A promise that will be resolved with the
             * value returned by the profile.
             */
            sp.get = function(endpoint, cfg) {
                var deferred = $q.defer();
                if (!endpoint) {
                    $timeout(function() {
                        deferred.reject('Missing endpoint for StudentProfile.get');
                    });
                }
                cfg = cfg || {};

                $http(_.defaults({
                    method: 'GET',
                    url: PROFILE_ROOT + endpoint
                }, cfg)).success(deferred.resolve).error(deferred.reject);

                return deferred.promise;
            };

            sp.save = function(endpoint, data, cfg) {
                var deferred = $q.defer();

                if (!endpoint) {
                    $timeout(function() {
                        deferred.reject('Missing endpoint for StudentProfile.save');
                    });
                }
                cfg = cfg || {};
                data = data || {};

                var verb = data.id ? 'POST' : 'PUT';
                var url = PROFILE_ROOT + endpoint + (data.id ? '/' + data.id : '');

                $http(_.defaults({
                    method: verb,
                    url: url,
                    data: data
                }, cfg))
                    .success(deferred.resolve)
                    .error(deferred.reject);

                return deferred.promise;
            };
        })
        .service('SurveyMeta', function($http, $filter, $q) {
            var sm = this;

            var root = '/survey-portlet/v1/surveys/';

            function newQuestion (q) {
                return $http({
                    method: 'POST',
                    data: q,
                    url: root + 'questions/'
                });
            }

            function newSurveyQuestion (s, sq) {
                return newQuestion(sq.question).success(function(newQ) {
                    _.extend(sq.question, newQ);

                    return $http({
                        method: 'POST',
                        url: root + s.id + '/questions/' + q.id,
                        data: sq
                    });
                });
            }

            /**
             * @ngdoc
             * @propertyOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#surveysById
             * @description An index of surveys by their id
             */
            sm.surveysById = {};

            /**
             * @ngdoc
             * @propertyOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#surveys
             * @description An array of surveys
             */
            sm.surveys = [];

            sm.surveys.clear = function() {
                while (this.length) {
                    this.pop();
                }
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#getSurveys
             * @description Retrieve the surveys from the metadata service and
             * refresh the sm.surveys array.
             * @returns {Promise} A promise that will be resolved when
             */
            sm.getSurveys = function() {
                return $http.get(root)
                    .success(function(surveys) {
                        _.each(surveys, function(s) {
                            if (sm.surveysById[s.id]) {
                                _.merge(sm.surveysById[s.id], s);
                            } else {
                                sm.surveys.push(s);
                            }

                            //TODO clean up deleted surveys? (any not returned by $http.get)
                        });

                        return surveys;
                    });
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#saveSurvey
             * @description Update the survey definition stored on the server.
             */
            sm.saveSurvey = function(survey) {
                var method = survey.id ? 'PUT' : 'POST';
                var url = survey.id ? root + survey.id : root ;

                var requests = [];

                requests.push($http({
                    method: method,
                    url: url,
                    data: survey,
                }));

                survey = angular.copy(survey);

                _.each(survey.surveyQuestions, function(q, i) {

                    if ( q.question.id ){
                        requests.push($http({
                            method: 'PUT',
                            url: root + 'questions/' + q.question.id,
                            data: q.question
                        }));
                    } else {
                        requests.push(newSurveyQuestion(survey, q));
                    }
                });

                return $q.all(requests);
            };

            var qaDef = {
                sequence: 0,
                canonicalName: null,
                answer: {
                    text: null,
                    imgUrl: null,
                    helpText: null,
                    altText: null,
                    imgHeight: 0,
                    imgWidth: 0,
                }
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#addQa
             * @param {Question} question A Question object that will have a
             * new answer.
             * @description Adds an empty answer to a given question
             */
            sm.addQa = function(question) {
                question.questionAnswers = question.questionAnswers || [];

                var newA = angular.copy(qaDef);
                newA.sequence = question.questionAnswers.length;

                question.questionAnswers.push(newA);
            };

            var sqDef = {
                sequence: 1,
                question: {
                    canonicalName: '',
                    text: '',
                    altText: '',
                    helpText: ''
                },
                numAllowedAnswers: 1
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#addSq
             * @param {Object} survey The survey to add a question to.
             * @description Addsa a new survey question to a survey
             */
            sm.addSq = function(survey) {
                survey.surveyQuestions = survey.surveyQuestions || [];

                var newSq = angular.copy(sqDef);
                newSq.sequence = survey.surveyQuestions.length;

                survey.surveyQuestions.push(newSq);
            };

            var surveyDef = {
                surveyQuestions: [],
                canonicalName: '',
                text: '',
                title: '',
                description: '',
                altText: '',
                helpText: ''
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#newSurvey
             * @description Returns a new survey object with appropriate keys.
             */
            sm.newSurvey = function() {
                return angular.copy(surveyDef);
            };
        })
        .controller('SurveyCtrl', function ($scope, $filter, SurveyMeta, StudentProfile) {
            /**
             * @ngdoc function
             * @name ngPortalApp.controller:SurveyCtrl
             * @description
             * # SurveyCtrl
             * Controller of the ngPortalApp
             */

            $scope.surveys = SurveyMeta.surveys;
            SurveyMeta.getSurveys();

            $scope.toggle = function(o) {
                o = o || {};
                o.shown = !o.shown;
            };

            StudentProfile.get('surveyAnswers', {
                params: {
                    user: USER,
                }
            }).then(function success(d) {
                if (d && d.length && d[0].answers && d[0].answers.length) {
                    _.each(d[0].answers, function(ans) {
                        $scope.surveyData[ans.question] = ans.answer;
                    });
                    $scope.surveyData.id = d[0].id;
                }
            });

            $scope.saveAnswers = function(answers) {
                delete answers.user;

                var data = {
                    answers: _(answers)
                        .filter(function(a, q) {
                            return q != 'id';
                        }).map(function(a, q) {
                        return {question: q, answer: a};
                        }).value(),
                    id: answers.id,
                    user: USER
                };

                StudentProfile.save('surveyAnswers', data)
                .then(function success(savedAnswers) {
                    answers.id = savedAnswers.id;
                });
            };

            $scope.addQ = SurveyMeta.addSq;
            $scope.addA = SurveyMeta.addQa;

            $scope.save = SurveyMeta.saveSurvey;

            $scope.swapSeq = function(ele1, ele2) {
                var tmp = ele1.sequence;
                ele1.sequence = ele2.sequence;
                ele2.sequence = tmp;
            };

            $scope.surveyData = {};
        })
        .directive('surveyQuestion', function () {
            /**
             * @ngdoc directive
             * @name ngPortalApp.directive#SurveyQuestion
             * @description A directive to display a survey question.
             * @restrict E
             */
            return {
                template: '
' +
                    '{{def.question.text}}' +
                    '
' +
                    '' +
                    '' +
                    '<input ng-if="def.numAllowedAnswers === 1" type="radio" ng-model="survey[def.question.id]" ng-value="ans.answer.id" />' +
                    '<input ng-if="def.numAllowedAnswers > 1" type="checkbox" ng-model="survey[def.question.id][ans.answer.id]"/>' +
                    '{{ans.answer.text}}' +
                    '' +
                    '' +
                    '
' +
                    '
',
                restrict: 'E',
                scope: {
                    def: '=',
                    survey: '=',
                },
                link: function postLink($scope, iEle, iAttrs) {
                }
            };
        })
        .directive('ngDrag', function (DragData) {
            return {
                restrict: 'A',
                link: function postLink(scope, element, iAttrs) {
                    element.attr('draggable', 'true');

                    element.on('dragstart', function(e) {
                        e.stopPropagation();
                        e.originalEvent.dataTransfer.setData('ngdrag/type', iAttrs.ngDrag || 'ngdrag/id');
                        e.originalEvent.dataTransfer.setData(iAttrs.ngDrag || 'ngdrag/id', scope.$id);
                        DragData.add(scope);
                    });
                }
            };
        })
        .directive('ngDrop', function(DragData) {

            /**
             * @ngdoc directive
             * @name uni.directive:ngDrop
             * @param ngDrop The expression to evaluate upon drop. If dropped element came from ngDrag, the ngDrag $scope is
             * available as $from.
             * @description Allows an expression to be evaluated upon drop. Event available as $event.
             * @restrict A
             */
            return {
                restrict: 'A',
                link: function postLink($scope, element, iAttrs) {

                    element.on('dragover', function(e) { e.preventDefault(); });

                    element.on('dragenter', function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        var type = e.originalEvent.dataTransfer.getData('ngdrag/type');
                        if(type === iAttrs.allowDrop) {
                            event.dataTransfer.dropEffect = 'move';
                        } else {
                            event.dataTransfer.dropEffect = 'none';
                        }
                    });

                    element.on('drop', function(e) {
                        e.preventDefault();

                        var id = e.originalEvent.dataTransfer.getData(iAttrs.allowDrop || 'ngdrag/id');
                        if(!id) { return; }
                        var from = DragData.get(id);

                        $scope.$apply(function() {
                            $scope.$eval(iAttrs.ngDrop, {
                                $from: from,
                                $event: e
                            });
                        });
                    });
                }
            };
        })
        .service('DragData', function () {
            /**
             * @ngdoc service
             * @name uni.service:DragData
             * @description
             * # dragger service in the sabApp.  */
            var index = {};

            this.add = function(scope) {
                if(scope.$id) {
                    index[scope.$id] = scope;
                }
            };

            this.get = function(id) {
                if(index[id]) {
                    return index[id];
                }
            };
        })
        .directive('modal', function ($document) {
            return {
                template: '<ng-transclude></ng-transclude>',
                transclude: true,
                restrict: 'E',
                scope: {
                    shown: '=',
                    modalHide: '&?',
                    modalShow: '&?'
                },
                link: function postLink($scope, iEle) {
                    iEle.addClass('hidden');
                    var modalHider = angular.element('

');

                    var hasBeenShown;

                    $scope.$watch('shown', function(shown) {
                        if(shown) {
                            hasBeenShown = true;

                            iEle.removeClass('hidden');
                            modalHider.on('click', function() {
                                //Apparently $scope.$apply can't wrap the function passed to on()
                                $scope.$apply(function() {
                                    $scope.shown = !$scope.shown;
                                });
                            });

                            angular.element(document.body).append(modalHider);

                            if($scope.modalShow) {
                                $scope.modalShow();
                            }
                        } else {
                            iEle.addClass('hidden');

                            if(hasBeenShown && $scope.modalHide) {
                                $scope.modalHide();
                            }

                            if(modalHider) {
                                modalHider.remove();
                            }
                        }
                    });
                }
            };
        });
    }
};
<commit_msg>Change empty strings to null<commit_after>window.up = window.up || {};

window.up.startSurveyPortlet = function(window, _, params) {
    'use strict';

    var surveyName = params.surveyName || '';
    var n = params.n;

    var MODULE_NAME = n + '-survey-portlet';
    var USER = 'admin';

    if (!window.angular) {
        var ANGULAR_SCRIPT_ID = 'angular-uportal-script';

        var scr = document.getElementById(ANGULAR_SCRIPT_ID);

        if (!scr) {
            scr = document.createElement('script');
            scr.id =  ANGULAR_SCRIPT_ID;
            scr.type =  'text/javascript';
            scr.async =  true;
            scr.charset =  'utf-8';
            scr.src =  'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.15/angular.js';

            document.body.appendChild(scr);
        }

        scr.addEventListener('load', bootstrap);
    } else {
        register(window.up.ngApp);
    }

    function bootstrap() {
        var app = angular.module(MODULE_NAME, []);
        register(app);
        angular.bootstrap(document.getElementById(n + '-survey-portlet'), [MODULE_NAME]);
    }

    function register(app) {
        app
        .service('StudentProfile', function($http, $q, $timeout) {
            var sp = this;

            var PROFILE_ROOT = 'https://portal-mock-api-dev.herokuapp.com/api/';

            /**
             * @ngdoc
             * @methodOf cccPortal.service:StudentProfile
             * @name cccPortal.service:StudentProfile#get
             * @param {String} endpoint The endpoint/data type to query.
             * @param {Object} cfg Additional configuration for the request.
             * Most useful for params.
             * @description Get some data from the user profile.
             * @returns {Promise} A promise that will be resolved with the
             * value returned by the profile.
             */
            sp.get = function(endpoint, cfg) {
                var deferred = $q.defer();
                if (!endpoint) {
                    $timeout(function() {
                        deferred.reject('Missing endpoint for StudentProfile.get');
                    });
                }
                cfg = cfg || {};

                $http(_.defaults({
                    method: 'GET',
                    url: PROFILE_ROOT + endpoint
                }, cfg)).success(deferred.resolve).error(deferred.reject);

                return deferred.promise;
            };

            sp.save = function(endpoint, data, cfg) {
                var deferred = $q.defer();

                if (!endpoint) {
                    $timeout(function() {
                        deferred.reject('Missing endpoint for StudentProfile.save');
                    });
                }
                cfg = cfg || {};
                data = data || {};

                var verb = data.id ? 'POST' : 'PUT';
                var url = PROFILE_ROOT + endpoint + (data.id ? '/' + data.id : '');

                $http(_.defaults({
                    method: verb,
                    url: url,
                    data: data
                }, cfg))
                    .success(deferred.resolve)
                    .error(deferred.reject);

                return deferred.promise;
            };
        })
        .service('SurveyMeta', function($http, $filter, $q) {
            var sm = this;

            var currentId = -1;

            var root = '/survey-portlet/v1/surveys/';

            function newQuestion (q) {
                return $http({
                    method: 'POST',
                    data: q,
                    url: root + 'questions'
                });
            }

            function newSurveyQuestion (s, sq) {
                return newQuestion(sq.question).success(function(newQ) {
                    _.extend(sq.question, newQ);

                    return $http({
                        method: 'POST',
                        url: root + s.id + '/questions/' + q.id,
                        data: sq
                    });
                });
            }

            /**
             * @ngdoc
             * @propertyOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#surveysById
             * @description An index of surveys by their id
             */
            sm.surveysById = {};

            /**
             * @ngdoc
             * @propertyOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#surveys
             * @description An array of surveys
             */
            sm.surveys = [];

            sm.surveys.clear = function() {
                while (this.length) {
                    this.pop();
                }
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#getSurveys
             * @description Retrieve the surveys from the metadata service and
             * refresh the sm.surveys array.
             * @returns {Promise} A promise that will be resolved when
             */
            sm.getSurveys = function() {
                return $http.get(root)
                    .success(function(surveys) {
                        _.each(surveys, function(s) {
                            if (sm.surveysById[s.id]) {
                                _.merge(sm.surveysById[s.id], s);
                            } else {
                                sm.surveys.push(s);
                            }

                            //TODO clean up deleted surveys? (any not returned by $http.get)
                        });

                        return surveys;
                    });
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#saveSurvey
             * @description Update the survey definition stored on the server.
             */
            sm.saveSurvey = function(survey) {
                var method = survey.id ? 'PUT' : 'POST';
                var url = survey.id ? root + survey.id : root ;

                var requests = [];

                requests.push($http({
                    method: method,
                    url: url,
                    data: survey,
                }));

                survey = angular.copy(survey);

                _.each(survey.surveyQuestions, function(q, i) {

                    if ( q.question.id ){
                        requests.push($http({
                            method: 'PUT',
                            url: root + 'questions/' + q.question.id,
                            data: q.question
                        }));
                    } else {
                        requests.push(newSurveyQuestion(survey, q));
                    }
                });

                return $q.all(requests);
            };

            var qaDef = {
                sequence: 0,
                canonicalName: null,
                answer: {
                    text: null,
                    imgUrl: null,
                    helpText: null,
                    altText: null,
                    imgHeight: 0,
                    imgWidth: 0
                }
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#addQa
             * @param {Question} question A Question object that will have a
             * new answer.
             * @description Adds an empty answer to a given question
             */
            sm.addQa = function(question) {
                question.questionAnswers = question.questionAnswers || [];

                var newA = angular.copy(qaDef);
                newA.sequence = question.questionAnswers.length + 1;

                question.questionAnswers.push(newA);
            };

            var sqDef = {
                sequence: 1,
                question: {
                    canonicalName: null,
                    text: null,
                    altText: null,
                    helpText: null,
                },
                numAllowedAnswers: 1
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#addSq
             * @param {Object} survey The survey to add a question to.
             * @description Addsa a new survey question to a survey
             */
            sm.addSq = function(survey) {
                survey.surveyQuestions = survey.surveyQuestions || [];

                var newSq = angular.copy(sqDef);
                newSq.sequence = survey.surveyQuestions.length + 1;

                survey.surveyQuestions.push(newSq);
            };

            var surveyDef = {
                canonicalName: null,
                text: null,
                title: null,
                description: null,
                altText: null,
                helpText: null
            };

            /**
             * @ngdoc
             * @methodOf cccPortal.service:SurveyMeta
             * @name cccPortal.service:SurveyMeta#newSurvey
             * @description Returns a new survey object with appropriate keys.
             */
            sm.newSurvey = function() {
                return angular.copy(surveyDef);
            };
        })
        .controller('SurveyCtrl', function ($scope, $filter, SurveyMeta, StudentProfile) {
            /**
             * @ngdoc function
             * @name ngPortalApp.controller:SurveyCtrl
             * @description
             * # SurveyCtrl
             * Controller of the ngPortalApp
             */

            $scope.surveys = SurveyMeta.surveys;
            SurveyMeta.getSurveys();

            $scope.toggle = function(o) {
                o = o || {};
                o.shown = !o.shown;
            };

            StudentProfile.get('surveyAnswers', {
                params: {
                    user: USER,
                }
            }).then(function success(d) {
                if (d && d.length && d[0].answers && d[0].answers.length) {
                    _.each(d[0].answers, function(ans) {
                        $scope.surveyData[ans.question] = ans.answer;
                    });
                    $scope.surveyData.id = d[0].id;
                }
            });

            $scope.saveAnswers = function(answers) {
                delete answers.user;

                var data = {
                    answers: _(answers)
                        .filter(function(a, q) {
                            return q != 'id';
                        }).map(function(a, q) {
                        return {question: q, answer: a};
                        }).value(),
                    id: answers.id,
                    user: USER
                };

                StudentProfile.save('surveyAnswers', data)
                .then(function success(savedAnswers) {
                    answers.id = savedAnswers.id;
                });
            };

            $scope.addQ = SurveyMeta.addSq;
            $scope.addA = SurveyMeta.addQa;

            $scope.save = SurveyMeta.saveSurvey;

            $scope.swapSeq = function(ele1, ele2) {
                var tmp = ele1.sequence;
                ele1.sequence = ele2.sequence;
                ele2.sequence = tmp;
            };

            $scope.surveyData = {};
        })
        .directive('surveyQuestion', function () {
            /**
             * @ngdoc directive
             * @name ngPortalApp.directive#SurveyQuestion
             * @description A directive to display a survey question.
             * @restrict E
             */
            return {
                template: '
' +
                    '{{def.question.text}}' +
                    '
' +
                    '' +
                    '' +
                    '<input ng-if="def.numAllowedAnswers === 1" type="radio" ng-model="survey[def.question.id]" ng-value="ans.answer.id" />' +
                    '<input ng-if="def.numAllowedAnswers > 1" type="checkbox" ng-model="survey[def.question.id][ans.answer.id]"/>' +
                    '{{ans.answer.text}}' +
                    '' +
                    '' +
                    '
' +
                    '
',
                restrict: 'E',
                scope: {
                    def: '=',
                    survey: '=',
                },
                link: function postLink($scope, iEle, iAttrs) {
                }
            };
        })
        .directive('ngDrag', function (DragData) {
            return {
                restrict: 'A',
                link: function postLink(scope, element, iAttrs) {
                    element.attr('draggable', 'true');

                    element.on('dragstart', function(e) {
                        e.stopPropagation();
                        e.originalEvent.dataTransfer.setData('ngdrag/type', iAttrs.ngDrag || 'ngdrag/id');
                        e.originalEvent.dataTransfer.setData(iAttrs.ngDrag || 'ngdrag/id', scope.$id);
                        DragData.add(scope);
                    });
                }
            };
        })
        .directive('ngDrop', function(DragData) {

            /**
             * @ngdoc directive
             * @name uni.directive:ngDrop
             * @param ngDrop The expression to evaluate upon drop. If dropped element came from ngDrag, the ngDrag $scope is
             * available as $from.
             * @description Allows an expression to be evaluated upon drop. Event available as $event.
             * @restrict A
             */
            return {
                restrict: 'A',
                link: function postLink($scope, element, iAttrs) {

                    element.on('dragover', function(e) { e.preventDefault(); });

                    element.on('dragenter', function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        var type = e.originalEvent.dataTransfer.getData('ngdrag/type');
                        if(type === iAttrs.allowDrop) {
                            event.dataTransfer.dropEffect = 'move';
                        } else {
                            event.dataTransfer.dropEffect = 'none';
                        }
                    });

                    element.on('drop', function(e) {
                        e.preventDefault();

                        var id = e.originalEvent.dataTransfer.getData(iAttrs.allowDrop || 'ngdrag/id');
                        if(!id) { return; }
                        var from = DragData.get(id);

                        $scope.$apply(function() {
                            $scope.$eval(iAttrs.ngDrop, {
                                $from: from,
                                $event: e
                            });
                        });
                    });
                }
            };
        })
        .service('DragData', function () {
            /**
             * @ngdoc service
             * @name uni.service:DragData
             * @description
             * # dragger service in the sabApp.  */
            var index = {};

            this.add = function(scope) {
                if(scope.$id) {
                    index[scope.$id] = scope;
                }
            };

            this.get = function(id) {
                if(index[id]) {
                    return index[id];
                }
            };
        })
        .directive('modal', function ($document) {
            return {
                template: '<ng-transclude></ng-transclude>',
                transclude: true,
                restrict: 'E',
                scope: {
                    shown: '=',
                    modalHide: '&?',
                    modalShow: '&?'
                },
                link: function postLink($scope, iEle) {
                    iEle.addClass('hidden');
                    var modalHider = angular.element('

');

                    var hasBeenShown;

                    $scope.$watch('shown', function(shown) {
                        if(shown) {
                            hasBeenShown = true;

                            iEle.removeClass('hidden');
                            modalHider.on('click', function() {
                                //Apparently $scope.$apply can't wrap the function passed to on()
                                $scope.$apply(function() {
                                    $scope.shown = !$scope.shown;
                                });
                            });

                            angular.element(document.body).append(modalHider);

                            if($scope.modalShow) {
                                $scope.modalShow();
                            }
                        } else {
                            iEle.addClass('hidden');

                            if(hasBeenShown && $scope.modalHide) {
                                $scope.modalHide();
                            }

                            if(modalHider) {
                                modalHider.remove();
                            }
                        }
                    });
                }
            };
        });
    }
};
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash'),
    Path = require('path'),
    fs = require('fs'),
    fm = require('front-matter'),
    async = require('async');

var Files = {
    markdown: function(config, source, next) {
        var result = {
            relative: source,
            target: Path.join(config.target, source.replace(/[.]md$/, '.html')),
            source: Path.join(config.source, source)
        };

        front(result.source, function(err, data) {
            if (err) next(err);
            next(err, _.extend(result, data));
        });
    },

    file: function(config, source, next) {
        return next(null, {
            relative: source,
            source: Path.join(config.source, source),
            target: Path.join(config.target, source)
        });
    }
};

function front(path, next) {
    fs.readFile(path, function(err, data) {
        if (err) return next(err);
        return next(null, fm(data.toString()));
    });
}

function parse(config, source, next) {
    if (/.md$/.test(source))
        return Files.markdown(config, source, next);

    return Files.file(config, source, next);
}

function meta(config, source, next) {
    var relative = source.replace(new RegExp('^' + config.source), '');

    parse(config, relative, function(err, result) {
        config.sources.push(result);
        return next(err, config);
    });
}

function metaData(config, done) {
    var sources = config.sources;

    config.sources = [];

    async.reduce(sources, config, meta, function(err, config) {
        done(err, config);
    });

}

module.exports = metaData;
<commit_msg>all the fiddly bits in one place<commit_after>'use strict';

var _ = require('lodash'),
    Path = require('path'),
    fs = require('fs'),
    fm = require('front-matter'),
    async = require('async');

var Files = {
    markdown: function(config, source, next) {
        var target = source.replace(/[.]md$/, '.html'),
            result = {
                link: target,
                type: 'markdown',
                relative: source,
                target: Path.join(config.target, target),
                source: Path.join(config.source, source)
            };

        front(result.source, function(err, data) {
            if (err) next(err);
            next(err, _.extend(result, data));
        });
    },

    file: function(config, source, next) {
        var target = source;

        return next(null, {
            link: target,
            type: Path.extname(source).replace(/^[.]/, ''),
            relative: source,
            source: Path.join(config.source, source),
            target: Path.join(config.target, source)
        });
    }
};

function front(path, next) {
    fs.readFile(path, function(err, data) {
        if (err) return next(err);
        return next(null, fm(data.toString()));
    });
}

function parse(config, source, next) {
    if (/.md$/.test(source))
        return Files.markdown(config, source, next);

    return Files.file(config, source, next);
}

function meta(config, source, next) {
    var relative = source.replace(new RegExp('^' + config.source), '');

    parse(config, relative, function(err, result) {
        config.sources.push(result);
        return next(err, config);
    });
}

function metaData(config, done) {
    var sources = config.sources;

    config.sources = [];

    async.reduce(sources, config, meta, function(err, config) {
        done(err, config);
    });

}

module.exports = metaData;
<|endoftext|>


<commit_before>'use strict';

function warning(message, position, obj) {
    obj.severity = 'warning';
    obj.message = message;
    obj.position = position;
    return obj;
}

// log an error -- not an error effect
function error(message, position, obj) {
    obj.severity = 'error';
    obj.message = message;
    obj.position = position;
    return obj;
}

var allows_metadata = {
    'table': 1, 'set': 1, 'list': 1,
    'vector': 1, 'function': 1, 'symbol': 1
};

function metadata_value(node, state, log) {
    if ( node.meta.length > 0 ) {
        if ( !allows_metadata.hasOwnProperty(node.type) ) {
            log.push(error('form does not support metadata', node.pos, {'type': node.type}));
        }
    }
}

var can_be_metadata = {
    'keyword': 1, 'autokey': 1,
    'symbol': 1, 'string': 1, 'table': 1
};

function metadata_meta(node, state, log) {
    var rwss = node.meta.map(function(m) {
        if ( !can_be_metadata.hasOwnProperty(m.type) ) {
            log.push(error('invalid metadata type', m.pos, {'type': m.type}));
        }
    });
}

function ratio_denominator(node, state, log) {
    var is_all_zeros = true,
        den = node.tree.denominator;
    for (var i = 0; i < den.length; i++) {
        if ( den[i] !== '0' ) {
            is_all_zeros = false;
        }
    }
    if ( is_all_zeros ) {
        log.push(error('divide by 0', node.pos, {}));
    }
}

function integer_base(node, state, log) {
    var base = parseInt(node.tree.base, 10);
    if ( base > 36 ) {
        log.push(error('radix too large', node.pos, {'radix': base}));
    } else if ( base <= 1 ) {
        log.push(error('radix too small', node.pos, {'radix': base}));
    }
}

function integer_digits(node, state, log) {
    var base = parseInt(node.tree.base, 10);
    node.tree.digits.split('').map(function(d) {
        var norm,
            c = d.charCodeAt();
        if ( ( c >= 48 ) && ( c <= 57 ) ) { // 0-9
            norm = (c - 48);
        } else if ( ( c >= 65 ) && ( c <= 90 ) ) { // A-Z
            norm = ((c - 65) + 10);
        } else if ( ( c >= 97 ) && ( c <= 122 ) ) { // a-z
            norm = ((c - 97) + 10);
        } else {
            throw new Error('unrecognized digit in ' + JSON.stringify(node));
        }
        if ( norm >= base ) {
            console.log(norm + ' -- ' + base);
            log.push(error('out of range digit', node.pos, {'digit': d})); // TODO wrong position
        }
    });
}

function table_even(node, state, log) {
    if ( ( node.elems.length % 2 ) === 1 ) {
        log.push(error('uneven number of elements in table', node.pos, {'number': node.elems.length}));
    }
}

function type(t) {
    return function(node) {
        return (t === node.type);
    };
}

var checks = [
    [type('integer')            , [integer_base, integer_digits]    ],
    [function() {return true;}  , [metadata_value, metadata_meta]   ],
    [type('table')              , [table_even]                      ],
    [type('ratio')              , [ratio_denominator]               ]
];

function traverse(node, state, log) {
    var fs = [],
        self,
        pred,
        recurse,
        i;
    for (i = 0; i < checks.length; i++) {
        pred = checks[i][0];
        if ( pred(node) ) {
            fs = fs.concat(checks[i][1]);
        }
    }
    fs.map(function(f) {
        return f(node, state, log);
    });
    switch (node._tag) {
        case 'token':
            break;
        case 'struct':
            node.elems.map(function(e) { traverse(e, state, log); });
            break;
        case 'leaf':
            traverse(node.form, state, log);
            break;
        default:
            throw new Error('unexpected AST node tag -- ' + JSON.stringify(node));
    }
    // TODO we need to also recurse into the metadata
}

module.exports = {
    'traverse': traverse
};

<commit_msg>checks: recurse into metadata as well<commit_after>'use strict';

function warning(message, position, obj) {
    obj.severity = 'warning';
    obj.message = message;
    obj.position = position;
    return obj;
}

// log an error -- not an error effect
function error(message, position, obj) {
    obj.severity = 'error';
    obj.message = message;
    obj.position = position;
    return obj;
}

var allows_metadata = {
    'table': 1, 'set': 1, 'list': 1,
    'vector': 1, 'function': 1, 'symbol': 1
};

function metadata_value(node, state, log) {
    if ( node.meta.length > 0 ) {
        if ( !allows_metadata.hasOwnProperty(node.type) ) {
            log.push(error('form does not support metadata', node.pos, {'type': node.type}));
        }
    }
}

var can_be_metadata = {
    'keyword': 1, 'autokey': 1,
    'symbol': 1, 'string': 1, 'table': 1
};

function metadata_meta(node, state, log) {
    var rwss = node.meta.map(function(m) {
        if ( !can_be_metadata.hasOwnProperty(m.type) ) {
            log.push(error('invalid metadata type', m.pos, {'type': m.type}));
        }
    });
}

function ratio_denominator(node, state, log) {
    var is_all_zeros = true,
        den = node.tree.denominator;
    for (var i = 0; i < den.length; i++) {
        if ( den[i] !== '0' ) {
            is_all_zeros = false;
        }
    }
    if ( is_all_zeros ) {
        log.push(error('divide by 0', node.pos, {}));
    }
}

function integer_base(node, state, log) {
    var base = parseInt(node.tree.base, 10);
    if ( base > 36 ) {
        log.push(error('radix too large', node.pos, {'radix': base}));
    } else if ( base <= 1 ) {
        log.push(error('radix too small', node.pos, {'radix': base}));
    }
}

function integer_digits(node, state, log) {
    var base = parseInt(node.tree.base, 10);
    node.tree.digits.split('').map(function(d) {
        var norm,
            c = d.charCodeAt();
        if ( ( c >= 48 ) && ( c <= 57 ) ) { // 0-9
            norm = (c - 48);
        } else if ( ( c >= 65 ) && ( c <= 90 ) ) { // A-Z
            norm = ((c - 65) + 10);
        } else if ( ( c >= 97 ) && ( c <= 122 ) ) { // a-z
            norm = ((c - 97) + 10);
        } else {
            throw new Error('unrecognized digit in ' + JSON.stringify(node));
        }
        if ( norm >= base ) {
            console.log(norm + ' -- ' + base);
            log.push(error('out of range digit', node.pos, {'digit': d})); // TODO wrong position
        }
    });
}

function table_even(node, state, log) {
    if ( ( node.elems.length % 2 ) === 1 ) {
        log.push(error('uneven number of elements in table', node.pos, {'number': node.elems.length}));
    }
}

function type(t) {
    return function(node) {
        return (t === node.type);
    };
}

var checks = [
    [type('integer')            , [integer_base, integer_digits]    ],
    [function() {return true;}  , [metadata_value, metadata_meta]   ],
    [type('table')              , [table_even]                      ],
    [type('ratio')              , [ratio_denominator]               ]
];

function traverse(node, state, log) {
    var fs = [],
        self,
        pred,
        recurse,
        i;
    for (i = 0; i < checks.length; i++) {
        pred = checks[i][0];
        if ( pred(node) ) {
            fs = fs.concat(checks[i][1]);
        }
    }
    fs.map(function(f) {
        return f(node, state, log);
    });
    switch (node._tag) {
        case 'token':
            break;
        case 'struct':
            node.elems.map(function(e) { traverse(e, state, log); });
            break;
        case 'leaf':
            traverse(node.form, state, log);
            break;
        default:
            throw new Error('unexpected AST node tag -- ' + JSON.stringify(node));
    }
    node.meta.map(function(m) { traverse(m, state, log); });
}

module.exports = {
    'traverse': traverse
};

<|endoftext|>


<commit_before>'use strict';

var startsWith = require('lodash/startsWith');
var keys = require('../util/keys');
var createSurfaceId = require('../util/createSurfaceId');
var Coordinate = require('../model/Coordinate');
var Component = require('./Component');

function IsolatedNodeComponent() {
  IsolatedNodeComponent.super.apply(this, arguments);

  this.name = this.props.node.id;
  this._id = createSurfaceId(this);
  this._state = {
    selectionFragment: null
  };

  this.handleAction('escape', this._escape);
}

IsolatedNodeComponent.Prototype = function() {

  var _super = IsolatedNodeComponent.super.prototype;

  this._isIsolatedNodeComponent = true;

  // InlineNode uses 'span'
  this.__elementTag = 'div';
  this.__slugChar = "|";

  this.getChildContext = function() {
    return {
      surfaceParent: this
    };
  };

  this.getInitialState = function() {
    var selState = this.context.documentSession.getSelectionState();
    return this._deriveStateFromSelectionState(selState);
  };

  this.didMount = function() {
    _super.didMount.call(this);

    var docSession = this.context.documentSession;
    docSession.on('update', this.onSessionUpdate, this);
  };

  this.dispose = function() {
    _super.dispose.call(this);

    var docSession = this.context.documentSession;
    docSession.off(this);
  };

  this.render = function($$) {
    var node = this.props.node;
    // console.log('##### IsolatedNodeComponent.render()', $$.capturing);
    var el = _super.render.apply(this, arguments);
    el.tagName = this.__elementTag;

    var ContentClass = this._getContentClass(node) || Component;

    el.addClass('sc-isolated-node')
      .addClass('sm-'+this.props.node.type)
      .attr("data-id", node.id);

    if (this.state.mode) {
      el.addClass('sm-'+this.state.mode);
    } else {
      el.addClass('sm-not-selected');
    }

    if (!ContentClass.static.noStyle) {
      el.addClass('sm-default-style');
    }

    el.on('mousedown', this.onMousedown);
    // shadowing handlers of the parent surface
    // TODO: extract this into a helper so that we can reuse it anywhere where we want
    // to prevent propagation to the parent surface
    el.on('keydown', this.onKeydown)
      .on('keypress', this._stopPropagation)
      .on('keyup', this._stopPropagation)
      .on('compositionstart', this._stopPropagation)
      .on('textInput', this._stopPropagation);

    el.append(
      $$(this.__elementTag).addClass('se-slug').addClass('sm-before').ref('before')
        // NOTE: better use a regular character otherwise Edge has problems
        .append(this.__slugChar)
    );

    var level = this._getLevel();

    var container = $$(this.__elementTag).addClass('se-container')
      .attr('contenteditable', false)
      .css({ 'z-index': 2*level });

    if (ContentClass.static.fullWidth) {
      container.addClass('sm-full-width');
    }

    if (this.state.mode === 'cursor' && this.state.position === 'before') {
      container.append(
        $$(this.__elementTag).addClass('se-cursor').addClass('sm-before').attr('contenteditable', false)
      );
    }
    container.append(this.renderContent($$, node));

    if (this._isDisabled() || this.state.mode === 'co-focused') {
      container.addClass('sm-disabled');
      // NOTE: there are some content implementations which work better without a blocker
      var blocker = $$(this.__elementTag).addClass('se-blocker')
        .css({ 'z-index': 2*level+1 });
      container.append(blocker);
    }

    if (this.state.mode === 'cursor' && this.state.position === 'after') {
      container.append(
        $$(this.__elementTag).addClass('se-cursor').addClass('sm-after').attr('contenteditable', false)
      );
    }

    el.append(container);

    el.append(
      $$(this.__elementTag).addClass('se-slug').addClass('sm-after').ref('after')
        // NOTE: better use a regular character otherwise Edge has problems
        .append(this.__slugChar)
    );

    return el;
  };

  this.renderContent = function($$, node) {
    var ComponentClass = this._getContentClass(node);
    if (!ComponentClass) {
      console.error('Could not resolve a component for type: ' + node.type);
      return $$(this.__elementTag);
    } else {
      var props = {
        node: node,
        disabled: this._isDisabled()
      };
      if (this.state.mode === 'focused') {
        props.focused = true;
      }
      return $$(ComponentClass, props).ref('content');
    }
  };

  this.getId = function() {
    return this._id;
  };

  this.getMode = function() {
    return this.state.mode;
  };

  this.isNotSelected = function() {
    return !this.state.mode;
  };

  this.isSelected = function() {
    return this.state.mode === 'selected';
  };

  this.isCoSelected = function() {
    return this.state.mode === 'co-selected';
  };

  this.isFocused = function() {
    return this.state.mode === 'focused';
  };

  this.isCoFocused = function() {
    return this.state.mode === 'co-focused';
  };

  this._getContentClass = function(node) {
    var componentRegistry = this.context.componentRegistry;
    var ComponentClass = componentRegistry.get(node.type);
    return ComponentClass;
  };

  this._isDisabled = function() {
    return !this.state.mode || ['co-selected', 'cursor'].indexOf(this.state.mode) > -1;
  };

  this._getSurfaceParent = function() {
    return this.context.surface;
  };

  this._getLevel = function() {
    var level = 1;
    var parent = this._getSurfaceParent();
    while (parent) {
      level++;
      parent = parent._getSurfaceParent();
    }
    return level;
  };

  this.onSessionUpdate = function(update) {
    if (update.selection) {
      var documentSession = this.context.documentSession;
      var newState = this._deriveStateFromSelectionState(documentSession.getSelectionState());
      if (!newState && this.state.mode) {
        this.setState({});
      } else if (newState && newState.mode !== this.state.mode) {
        this.setState(newState);
      }
    }
  };

  this._deriveStateFromSelectionState = function(selState) {
    var sel = selState.getSelection();
    var surfaceId = sel.surfaceId;
    if (!surfaceId) return;
    var id = this.getId();
    var nodeId = this.props.node.id;
    var parentId = this._getSurfaceParent().getId();
    var inParentSurface = (surfaceId === parentId);
    // detect cases where this node is selected or co-selected by inspecting the selection
    if (inParentSurface) {
      if (sel.isNodeSelection() && sel.getNodeId() === nodeId) {
        if (sel.isFull()) {
          return { mode: 'selected' };
        } else if (sel.isBefore()) {
          return { mode: 'cursor', position: 'before' };
        } else if (sel.isAfter()) {
          return { mode: 'cursor', position: 'after' };
        }
      }
      if (sel.isContainerSelection() && sel.containsNodeFragment(nodeId)) {
        return { mode: 'co-selected' };
      }
      return;
    }
    // for all other cases (focused / co-focused) the surface id prefix must match
    if (!startsWith(surfaceId, id)) return;

    // Note: trying to distinguisd focused
    // surfaceIds are a sequence of names joined with '/'
    // a surface inside this node will have a path with length+1.
    // a custom selection might just use the id of this IsolatedNode
    var p1 = id.split('/');
    var p2 = surfaceId.split('/');
    if (p2.length >= p1.length && p2.length <= p1.length+1) {
      return { mode: 'focused' };
    } else {
      return { mode: 'co-focused' };
    }
  };

  this.onMousedown = function(event) {
    // console.log('IsolatedNodeComponent.onMousedown', this.getId());
    event.stopPropagation();
    switch (this.state.mode) {
      case 'selected':
      case 'focused':
        break;
      default:
        event.preventDefault();
        this._selectNode();
        this.setState({ mode: 'selected' });
    }
  };

  this.onKeydown = function(event) {
    event.stopPropagation();
    // console.log('####', event.keyCode, event.metaKey, event.ctrlKey, event.shiftKey);
    // TODO: while this works when we have an isolated node with input or CE,
    // there is no built-in way of receiving key events in other cases
    // We need a global event listener for keyboard events which dispatches to the current isolated node
    if (event.keyCode === keys.ESCAPE && this.state.mode === 'focused') {
      event.preventDefault();
      this._escape();
    }
  };

  this._escape = function() {
    this._selectNode();
    // TODO: Is this still necessary?
    // The state should be set during the next update cycle.
    this.setState({ mode: 'selected' });
  };

  this._stopPropagation = function(event) {
    event.stopPropagation();
  };

  this._selectNode = function() {
    // console.log('IsolatedNodeComponent: selecting node.');
    var surface = this.context.surface;
    var doc = surface.getDocument();
    var nodeId = this.props.node.id;
    surface.setSelection(doc.createSelection({
      type: 'node',
      containerId: surface.getContainerId(),
      nodeId: nodeId,
      mode: 'full'
    }));
  };

};

Component.extend(IsolatedNodeComponent);

IsolatedNodeComponent.getCoordinate = function(surfaceEl, node) {
  // special treatment for block-level isolated-nodes
  var parent = node.getParent();
  if (node.isTextNode() && parent.is('.se-slug')) {
    var boundary = parent;
    var isolatedNodeEl = boundary.getParent();
    var nodeId = isolatedNodeEl.getAttribute('data-id');
    if (nodeId) {
      var charPos = boundary.is('sm-after') ? 1 : 0;
      return new Coordinate([nodeId], charPos);
    } else {
      console.error('FIXME: expecting a data-id attribute on IsolatedNodeComponent');
    }
  }
  return null;
};

IsolatedNodeComponent.getDOMCoordinate = function(comp, coor) {
  var domCoor;
  if (coor.offset === 0) {
    domCoor = {
      container: comp.refs.before.getNativeElement(),
      offset: 0
    };
  } else {
    domCoor = {
      container: comp.refs.after.getNativeElement(),
      offset: 1
    };
  }
  return domCoor;
};

IsolatedNodeComponent.getDOMCoordinates = function(comp) {
  return {
    start: {
      container: comp.refs.before.getNativeElement(),
      offset: 0
    },
    end: {
      container: comp.refs.after.getNativeElement(),
      offset: 1
    }
  };
};

module.exports = IsolatedNodeComponent;
<commit_msg>Compute focused/co-focused correctly.<commit_after>'use strict';

var startsWith = require('lodash/startsWith');
var keys = require('../util/keys');
var createSurfaceId = require('../util/createSurfaceId');
var Coordinate = require('../model/Coordinate');
var Component = require('./Component');

function IsolatedNodeComponent() {
  IsolatedNodeComponent.super.apply(this, arguments);

  this.name = this.props.node.id;
  this._id = createSurfaceId(this);
  this._state = {
    selectionFragment: null
  };

  this.handleAction('escape', this._escape);
}

IsolatedNodeComponent.Prototype = function() {

  var _super = IsolatedNodeComponent.super.prototype;

  this._isIsolatedNodeComponent = true;

  // InlineNode uses 'span'
  this.__elementTag = 'div';
  this.__slugChar = "|";

  this.getChildContext = function() {
    return {
      surfaceParent: this
    };
  };

  this.getInitialState = function() {
    var selState = this.context.documentSession.getSelectionState();
    return this._deriveStateFromSelectionState(selState);
  };

  this.didMount = function() {
    _super.didMount.call(this);

    var docSession = this.context.documentSession;
    docSession.on('update', this.onSessionUpdate, this);
  };

  this.dispose = function() {
    _super.dispose.call(this);

    var docSession = this.context.documentSession;
    docSession.off(this);
  };

  this.render = function($$) {
    var node = this.props.node;
    // console.log('##### IsolatedNodeComponent.render()', $$.capturing);
    var el = _super.render.apply(this, arguments);
    el.tagName = this.__elementTag;

    var ContentClass = this._getContentClass(node) || Component;

    el.addClass('sc-isolated-node')
      .addClass('sm-'+this.props.node.type)
      .attr("data-id", node.id);

    if (this.state.mode) {
      el.addClass('sm-'+this.state.mode);
    } else {
      el.addClass('sm-not-selected');
    }

    if (!ContentClass.static.noStyle) {
      el.addClass('sm-default-style');
    }

    el.on('mousedown', this.onMousedown);
    // shadowing handlers of the parent surface
    // TODO: extract this into a helper so that we can reuse it anywhere where we want
    // to prevent propagation to the parent surface
    el.on('keydown', this.onKeydown)
      .on('keypress', this._stopPropagation)
      .on('keyup', this._stopPropagation)
      .on('compositionstart', this._stopPropagation)
      .on('textInput', this._stopPropagation);

    el.append(
      $$(this.__elementTag).addClass('se-slug').addClass('sm-before').ref('before')
        // NOTE: better use a regular character otherwise Edge has problems
        .append(this.__slugChar)
    );

    var level = this._getLevel();

    var container = $$(this.__elementTag).addClass('se-container')
      .attr('contenteditable', false)
      .css({ 'z-index': 2*level });

    if (ContentClass.static.fullWidth) {
      container.addClass('sm-full-width');
    }

    if (this.state.mode === 'cursor' && this.state.position === 'before') {
      container.append(
        $$(this.__elementTag).addClass('se-cursor').addClass('sm-before').attr('contenteditable', false)
      );
    }
    container.append(this.renderContent($$, node));

    if (this._isDisabled() || this.state.mode === 'co-focused') {
      container.addClass('sm-disabled');
      // NOTE: there are some content implementations which work better without a blocker
      var blocker = $$(this.__elementTag).addClass('se-blocker')
        .css({ 'z-index': 2*level+1 });
      container.append(blocker);
    }

    if (this.state.mode === 'cursor' && this.state.position === 'after') {
      container.append(
        $$(this.__elementTag).addClass('se-cursor').addClass('sm-after').attr('contenteditable', false)
      );
    }

    el.append(container);

    el.append(
      $$(this.__elementTag).addClass('se-slug').addClass('sm-after').ref('after')
        // NOTE: better use a regular character otherwise Edge has problems
        .append(this.__slugChar)
    );

    return el;
  };

  this.renderContent = function($$, node) {
    var ComponentClass = this._getContentClass(node);
    if (!ComponentClass) {
      console.error('Could not resolve a component for type: ' + node.type);
      return $$(this.__elementTag);
    } else {
      var props = {
        node: node,
        disabled: this._isDisabled()
      };
      if (this.state.mode === 'focused') {
        props.focused = true;
      }
      return $$(ComponentClass, props).ref('content');
    }
  };

  this.getId = function() {
    return this._id;
  };

  this.getMode = function() {
    return this.state.mode;
  };

  this.isNotSelected = function() {
    return !this.state.mode;
  };

  this.isSelected = function() {
    return this.state.mode === 'selected';
  };

  this.isCoSelected = function() {
    return this.state.mode === 'co-selected';
  };

  this.isFocused = function() {
    return this.state.mode === 'focused';
  };

  this.isCoFocused = function() {
    return this.state.mode === 'co-focused';
  };

  this._getContentClass = function(node) {
    var componentRegistry = this.context.componentRegistry;
    var ComponentClass = componentRegistry.get(node.type);
    return ComponentClass;
  };

  this._isDisabled = function() {
    return !this.state.mode || ['co-selected', 'cursor'].indexOf(this.state.mode) > -1;
  };

  this._getSurfaceParent = function() {
    return this.context.surface;
  };

  this._getLevel = function() {
    var level = 1;
    var parent = this._getSurfaceParent();
    while (parent) {
      level++;
      parent = parent._getSurfaceParent();
    }
    return level;
  };

  this.onSessionUpdate = function(update) {
    if (update.selection) {
      var documentSession = this.context.documentSession;
      var newState = this._deriveStateFromSelectionState(documentSession.getSelectionState());
      if (!newState && this.state.mode) {
        this.setState({});
      } else if (newState && newState.mode !== this.state.mode) {
        this.setState(newState);
      }
    }
  };

  this._deriveStateFromSelectionState = function(selState) {
    var sel = selState.getSelection();
    var surfaceId = sel.surfaceId;
    if (!surfaceId) return;
    var id = this.getId();
    var nodeId = this.props.node.id;
    var parentId = this._getSurfaceParent().getId();
    var inParentSurface = (surfaceId === parentId);
    // detect cases where this node is selected or co-selected by inspecting the selection
    if (inParentSurface) {
      if (sel.isNodeSelection() && sel.getNodeId() === nodeId) {
        if (sel.isFull()) {
          return { mode: 'selected' };
        } else if (sel.isBefore()) {
          return { mode: 'cursor', position: 'before' };
        } else if (sel.isAfter()) {
          return { mode: 'cursor', position: 'after' };
        }
      }
      if (sel.isContainerSelection() && sel.containsNodeFragment(nodeId)) {
        return { mode: 'co-selected' };
      }
      return;
    }
    if (id === surfaceId) {
      return { mode: 'focused' };
    } else if (startsWith(surfaceId, id)) {
      return { mode: 'co-focused' };
    }
  };

  this.onMousedown = function(event) {
    // console.log('IsolatedNodeComponent.onMousedown', this.getId());
    event.stopPropagation();
    switch (this.state.mode) {
      case 'selected':
      case 'focused':
        break;
      default:
        event.preventDefault();
        this._selectNode();
        this.setState({ mode: 'selected' });
    }
  };

  this.onKeydown = function(event) {
    event.stopPropagation();
    // console.log('####', event.keyCode, event.metaKey, event.ctrlKey, event.shiftKey);
    // TODO: while this works when we have an isolated node with input or CE,
    // there is no built-in way of receiving key events in other cases
    // We need a global event listener for keyboard events which dispatches to the current isolated node
    if (event.keyCode === keys.ESCAPE && this.state.mode === 'focused') {
      event.preventDefault();
      this._escape();
    }
  };

  this._escape = function() {
    this._selectNode();
    // TODO: Is this still necessary?
    // The state should be set during the next update cycle.
    this.setState({ mode: 'selected' });
  };

  this._stopPropagation = function(event) {
    event.stopPropagation();
  };

  this._selectNode = function() {
    // console.log('IsolatedNodeComponent: selecting node.');
    var surface = this.context.surface;
    var doc = surface.getDocument();
    var nodeId = this.props.node.id;
    surface.setSelection(doc.createSelection({
      type: 'node',
      containerId: surface.getContainerId(),
      nodeId: nodeId,
      mode: 'full'
    }));
  };

};

Component.extend(IsolatedNodeComponent);

IsolatedNodeComponent.getCoordinate = function(surfaceEl, node) {
  // special treatment for block-level isolated-nodes
  var parent = node.getParent();
  if (node.isTextNode() && parent.is('.se-slug')) {
    var boundary = parent;
    var isolatedNodeEl = boundary.getParent();
    var nodeId = isolatedNodeEl.getAttribute('data-id');
    if (nodeId) {
      var charPos = boundary.is('sm-after') ? 1 : 0;
      return new Coordinate([nodeId], charPos);
    } else {
      console.error('FIXME: expecting a data-id attribute on IsolatedNodeComponent');
    }
  }
  return null;
};

IsolatedNodeComponent.getDOMCoordinate = function(comp, coor) {
  var domCoor;
  if (coor.offset === 0) {
    domCoor = {
      container: comp.refs.before.getNativeElement(),
      offset: 0
    };
  } else {
    domCoor = {
      container: comp.refs.after.getNativeElement(),
      offset: 1
    };
  }
  return domCoor;
};

IsolatedNodeComponent.getDOMCoordinates = function(comp) {
  return {
    start: {
      container: comp.refs.before.getNativeElement(),
      offset: 0
    },
    end: {
      container: comp.refs.after.getNativeElement(),
      offset: 1
    }
  };
};

module.exports = IsolatedNodeComponent;
<|endoftext|>


<commit_before>import { combineReducers } from 'redux';
import _ from 'lodash'

import GraphUtils from '../components/graph/GraphUtils'
import NodeUtils from '../components/graph/NodeUtils'

import UndoRedoReducer from '../undoredo/UndoRedoReducer'

function settingsReducer(state = {loggedUser: {}, featuresSettings: {}}, action) {
  switch (action.type) {
    case "LOGGED_USER": {
      return {
        ...state,
          loggedUser: action.user
      }
    }
    case "FEATURES_SETTINGS": {
      return {
        ...state,
        featuresSettings: action.featuresSettings
      }
    }
    case "PROCESS_DEFINITION_DATA": {
      return {
        ...state,
          processDefinitionData: action.processDefinitionData
      }
    }
    case "AVAILABLE_QUERY_STATES": {
      return {
        ...state,
        availableQueryableStates: action.availableQueryableStates
      }
    }
    default:
      return state
  }
}

const emptyUiState = {
  leftPanelIsOpened: false,
  showNodeDetailsModal: false,
  showEdgeDetailsModal: false,
  confirmDialog: {},
  modalDialog: {},
  expandedGroups: []
}
function uiStateReducer(state = emptyUiState, action) {
  const withAllModalsClosed = (newState) => {
    const allModalsClosed = !(newState.modalDialog.openDialog || newState.showNodeDetailsModal || newState.showEdgeDetailsModal || newState.confirmDialog.isOpen)
    return { ...newState, allModalsClosed: allModalsClosed}
  }
  switch (action.type) {
    case "TOGGLE_LEFT_PANEL": {
      return withAllModalsClosed({
        ...state,
        leftPanelIsOpened: action.leftPanelIsOpened
      })
    }
    case "CLOSE_MODALS": {
      return withAllModalsClosed({
        ...state,
        showNodeDetailsModal: false,
        showEdgeDetailsModal: false
      })
    }
    case "DISPLAY_MODAL_NODE_DETAILS": {
      return withAllModalsClosed({
        ...state,
        showNodeDetailsModal: true
      })
    }
    case "DISPLAY_MODAL_EDGE_DETAILS": {
      return withAllModalsClosed({
        ...state,
        showEdgeDetailsModal: true
      })
    }
    case "TOGGLE_CONFIRM_DIALOG": {
      return withAllModalsClosed({
        ...state,
        confirmDialog: {
          isOpen: action.isOpen,
          text: action.text,
          onConfirmCallback: action.onConfirmCallback
        }
      })
    }
    case "TOGGLE_MODAL_DIALOG": {
      return withAllModalsClosed({
        ...state,
        modalDialog: {
          openDialog: action.openDialog
        }
      })
    }
    case "EXPAND_GROUP": {
      return {
        ...state,
        expandedGroups: _.concat(state.expandedGroups, [action.id])
      }
    }
    case "COLLAPSE_GROUP": {
      return {
        ...state,
        expandedGroups: state.expandedGroups.filter(g => g != action.id)
      }
    }
    case "EDIT_GROUP": {
      return {
        ...state,
        expandedGroups: state.expandedGroups.map(id => id == action.oldGroupId ? action.newGroup.id : id)
      }
    }
    default:
      return withAllModalsClosed(state)
  }
}

const emptyGraphState = {
  graphLoading: false,
  processToDisplay: {},
  fetchedProcessDetails: {},
  nodeToDisplay: {},
  edgeToDisplay: {},
  layout: [],
  testCapabilities: {},
  groupingState: null,
  processCounts: {},
  testResults: {}
}

function graphReducer(state, action) {
  switch (action.type) {
    case "PROCESS_LOADING": {
      return {
        ...state,
        graphLoading: true
      }
    }
    case "UPDATE_IMPORTED_PROCESS": {
      const oldNodeIds = _.sortBy(state.processToDisplay.nodes.map((n) => n.id))
      const newNodeids = _.sortBy(action.processJson.nodes.map((n) => n.id))
      const newLayout = _.isEqual(oldNodeIds, newNodeids) ? state.layout : null

      return {
        ...state,
        graphLoading: false,
        processToDisplay: action.processJson,
        layout: newLayout
      }
    }
    case "UPDATE_TEST_CAPABILITIES": {
      return {
        ...state,
        testCapabilities: action.capabilities
      }
    }
    case "DISPLAY_PROCESS": {
      return {
        ...state,
        processToDisplay: action.fetchedProcessDetails.json,
        fetchedProcessDetails: action.fetchedProcessDetails,
        graphLoading: false,
        nodeToDisplay: action.fetchedProcessDetails.json.properties,
        layout: [] //potrzebne np przy pokazywaniu historycznej wersji
      }
    }
    case "LOADING_FAILED": {
      return {
        ...state,
        graphLoading: false
      }
    }
    case "CLEAR_PROCESS": {
      return {
        ...state,
        processToDisplay: {},
        fetchedProcessDetails: {},
        testResults: null
      }
    }
    case "DISPLAY_MODAL_NODE_DETAILS":
    case "DISPLAY_NODE_DETAILS":
      if (state.groupingState) {
        const newNodeId = action.nodeToDisplay.id
        return {
           ...state,
          //FIXME: dodanie do grupy
          groupingState: canGroup(state, action.nodeToDisplay) ? _.concat(state.groupingState, newNodeId) : state.groupingState
        }
      } else {
        return {
          ...state,
          nodeToDisplay: action.nodeToDisplay
        }
      }

    case "DISPLAY_MODAL_EDGE_DETAILS": {
      return {
        ...state,
        edgeToDisplay: action.edgeToDisplay
      }
    }

    case "EDIT_EDGE": {
      const processToDisplay = GraphUtils.mapProcessWithNewEdge(state.processToDisplay, action.before, action.after)
      return {
        ...state,
        processToDisplay: {
          ...processToDisplay,
          validationResult: action.validationResult
        },
        edgeToDisplay: action.after,
      }
    }
    case "EDIT_NODE": {
      const stateAfterNodeRename = updateAfterNodeIdChange(state, action.before.id, action.after.id);
      const processToDisplay = GraphUtils.mapProcessWithNewNode(stateAfterNodeRename.processToDisplay, action.before, action.after)
      return {
        ...stateAfterNodeRename,
        processToDisplay: {
          ...processToDisplay,
          validationResult: action.validationResult
        },
        nodeToDisplay: action.after,
      }
    }
    case "DELETE_NODE": {
      var idToDelete = action.id
      const stateAfterNodeDelete = updateAfterNodeDelete(state, idToDelete)
      const processToDisplay = GraphUtils.deleteNode(stateAfterNodeDelete.processToDisplay, idToDelete);
      return {
        ...stateAfterNodeDelete,
        processToDisplay: processToDisplay,
        nodeToDisplay: processToDisplay.properties,
      }
    }
    case "URL_CHANGED": {
      return {
        ...state,
        ...emptyGraphState,
      }
    }
    case "NODES_CONNECTED": {
      const baseEdge = {from: action.fromNode.id, to: action.toNode.id}
      const edgeType = NodeUtils.edgeType(state.processToDisplay.edges, action.fromNode, action.processDefinitionData)
      const edge = edgeType ? {...baseEdge, edgeType: edgeType} : baseEdge
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          edges: _.concat(state.processToDisplay.edges, edge)
        }
      }
    }
    case "NODES_DISCONNECTED": {
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          edges: _.reject(state.processToDisplay.edges, (e) => e.from == action.from && e.to == action.to)
        }
      }
    }
    case "NODE_ADDED": {
      var newId = createUniqueNodeId(state.processToDisplay.nodes, state.processToDisplay.nodes.length)
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          nodes: _.concat(state.processToDisplay.nodes, {
            ... action.node,
            id: newId
          })
        },
        layout: _.concat(state.layout, {id: newId, position: action.position})
      }
    }
    //TODO: jakos inaczej to obsluzyc??
    case "LAYOUT_CHANGED": {
      return {
        ...state,
        layout: action.layout
      }
    }
    case "DISPLAY_PROCESS_COUNTS": {
      return {
        ...state,
        processCounts: action.processCounts
      }
    }
    case "DISPLAY_TEST_RESULTS_DETAILS": {
      return {
        ...state,
        testResults: action.testResults,
        graphLoading: false
      }
    }
    case "HIDE_RUN_PROCESS_DETAILS": {
      return {
        ...state,
        testResults: null,
        processCounts: null
      }
    }
    case "START_GROUPING": {
      return {
        ...state,
        groupingState: [],
        nodeToDisplay: state.processToDisplay.properties
      }
    }
    case "FINISH_GROUPING": {
      const withUpdatedGroups = state.groupingState.length > 1 ?
        {
          ...state,
          processToDisplay: NodeUtils.createGroup(state.processToDisplay, state.groupingState),
          layout: []
        } :  state;
      return _.omit(withUpdatedGroups, 'groupingState')
    }
    case "CANCEL_GROUPING": {
      return _.omit(state, 'groupingState')
    }
    case "UNGROUP": {
      return {
        processToDisplay: NodeUtils.ungroup(state.processToDisplay, action.groupToRemove),
        layout: [],
        nodeToDisplay: state.processToDisplay.properties,
      }
    }
    case "EXPAND_GROUP":
    case "COLLAPSE_GROUP": {
      return {
        ...state,
        layout: []
      }
    }
    case "EDIT_GROUP": {
      return {
        ...state,
        processToDisplay: NodeUtils.editGroup(state.processToDisplay, action.oldGroupId, action.newGroup),
        nodeToDisplay: action.newGroup,
        layout: updateLayoutAfterNodeIdChange(state.layout, action.oldGroupId, action.newGroup.id)
      }
    }
    default:
      return state
  }

  function canGroup(state, newNode) {
    const newNodeId = newNode.id
    const currentGrouping = state.groupingState
    return !NodeUtils.nodeIsGroup(newNode) && currentGrouping.length == 0 ||
      currentGrouping.find(nodeId => state.processToDisplay.edges.find(edge => edge.from == nodeId && edge.to == newNodeId ||  edge.to == nodeId && edge.from == newNodeId))
  }

  function updateAfterNodeIdChange(state, oldId, newId) {
    const newLayout = updateLayoutAfterNodeIdChange(state.layout, oldId, newId);
    const withGroupsUpdated = NodeUtils.updateGroupsAfterNodeIdChange(state.processToDisplay, oldId, newId);
    return {
      ...state,
      processToDisplay: withGroupsUpdated,
      layout: newLayout
    }
  }

  function updateLayoutAfterNodeIdChange(layout, oldId, newId) {
    return _.map(layout, (n) => {
      if (oldId === n.id) {
        return {
          ...n,
          id: newId
        }
      } else return n;
    });
  }

  function updateAfterNodeDelete(state, idToDelete) {
    const layoutWithoutNode = _.filter(state.layout, (n) => n.id !== idToDelete);
    const withGroupsUpdated = NodeUtils.updateGroupsAfterNodeDelete(state.processToDisplay, idToDelete);
    return {
      ...state,
      processToDisplay: withGroupsUpdated,
      layout: layoutWithoutNode
    }
  }

  function createUniqueNodeId(nodes, nodeCounter) {
    var newId = `node${nodeCounter}`
    return _.some(nodes, (n) => {return n.id == newId}) ? createUniqueNodeId(nodes, nodeCounter + 1) : newId
  }
}

const emptyProcessActivity = {
  comments: [],
  attachments: []
}

function processActivityReducer(state = emptyProcessActivity, action) {
  switch (action.type) {
    case "DISPLAY_PROCESS_ACTIVITY": {
      return {
        ...state,
        comments: action.comments,
        attachments: action.attachments
      }
    }
    default:
      return state
  }
}

const espUndoableConfig = {
  blacklist: ["CLEAR_PROCESS", "PROCESS_LOADING", "URL_CHANGED"]
}
const rootReducer = combineReducers({
  graphReducer: UndoRedoReducer.espUndoable(graphReducer, espUndoableConfig),
  settings: settingsReducer,
  ui: uiStateReducer,
  processActivity: processActivityReducer
});

export default rootReducer;
<commit_msg>ungroup button fix<commit_after>import { combineReducers } from 'redux';
import _ from 'lodash'

import GraphUtils from '../components/graph/GraphUtils'
import NodeUtils from '../components/graph/NodeUtils'

import UndoRedoReducer from '../undoredo/UndoRedoReducer'

function settingsReducer(state = {loggedUser: {}, featuresSettings: {}}, action) {
  switch (action.type) {
    case "LOGGED_USER": {
      return {
        ...state,
          loggedUser: action.user
      }
    }
    case "FEATURES_SETTINGS": {
      return {
        ...state,
        featuresSettings: action.featuresSettings
      }
    }
    case "PROCESS_DEFINITION_DATA": {
      return {
        ...state,
          processDefinitionData: action.processDefinitionData
      }
    }
    case "AVAILABLE_QUERY_STATES": {
      return {
        ...state,
        availableQueryableStates: action.availableQueryableStates
      }
    }
    default:
      return state
  }
}

const emptyUiState = {
  leftPanelIsOpened: false,
  showNodeDetailsModal: false,
  showEdgeDetailsModal: false,
  confirmDialog: {},
  modalDialog: {},
  expandedGroups: []
}
function uiStateReducer(state = emptyUiState, action) {
  const withAllModalsClosed = (newState) => {
    const allModalsClosed = !(newState.modalDialog.openDialog || newState.showNodeDetailsModal || newState.showEdgeDetailsModal || newState.confirmDialog.isOpen)
    return { ...newState, allModalsClosed: allModalsClosed}
  }
  switch (action.type) {
    case "TOGGLE_LEFT_PANEL": {
      return withAllModalsClosed({
        ...state,
        leftPanelIsOpened: action.leftPanelIsOpened
      })
    }
    case "CLOSE_MODALS": {
      return withAllModalsClosed({
        ...state,
        showNodeDetailsModal: false,
        showEdgeDetailsModal: false
      })
    }
    case "DISPLAY_MODAL_NODE_DETAILS": {
      return withAllModalsClosed({
        ...state,
        showNodeDetailsModal: true
      })
    }
    case "DISPLAY_MODAL_EDGE_DETAILS": {
      return withAllModalsClosed({
        ...state,
        showEdgeDetailsModal: true
      })
    }
    case "TOGGLE_CONFIRM_DIALOG": {
      return withAllModalsClosed({
        ...state,
        confirmDialog: {
          isOpen: action.isOpen,
          text: action.text,
          onConfirmCallback: action.onConfirmCallback
        }
      })
    }
    case "TOGGLE_MODAL_DIALOG": {
      return withAllModalsClosed({
        ...state,
        modalDialog: {
          openDialog: action.openDialog
        }
      })
    }
    case "EXPAND_GROUP": {
      return {
        ...state,
        expandedGroups: _.concat(state.expandedGroups, [action.id])
      }
    }
    case "COLLAPSE_GROUP": {
      return {
        ...state,
        expandedGroups: state.expandedGroups.filter(g => g != action.id)
      }
    }
    case "EDIT_GROUP": {
      return {
        ...state,
        expandedGroups: state.expandedGroups.map(id => id == action.oldGroupId ? action.newGroup.id : id)
      }
    }
    default:
      return withAllModalsClosed(state)
  }
}

const emptyGraphState = {
  graphLoading: false,
  processToDisplay: {},
  fetchedProcessDetails: {},
  nodeToDisplay: {},
  edgeToDisplay: {},
  layout: [],
  testCapabilities: {},
  groupingState: null,
  processCounts: {},
  testResults: {}
}

function graphReducer(state, action) {
  switch (action.type) {
    case "PROCESS_LOADING": {
      return {
        ...state,
        graphLoading: true
      }
    }
    case "UPDATE_IMPORTED_PROCESS": {
      const oldNodeIds = _.sortBy(state.processToDisplay.nodes.map((n) => n.id))
      const newNodeids = _.sortBy(action.processJson.nodes.map((n) => n.id))
      const newLayout = _.isEqual(oldNodeIds, newNodeids) ? state.layout : null

      return {
        ...state,
        graphLoading: false,
        processToDisplay: action.processJson,
        layout: newLayout
      }
    }
    case "UPDATE_TEST_CAPABILITIES": {
      return {
        ...state,
        testCapabilities: action.capabilities
      }
    }
    case "DISPLAY_PROCESS": {
      return {
        ...state,
        processToDisplay: action.fetchedProcessDetails.json,
        fetchedProcessDetails: action.fetchedProcessDetails,
        graphLoading: false,
        nodeToDisplay: action.fetchedProcessDetails.json.properties,
        layout: [] //potrzebne np przy pokazywaniu historycznej wersji
      }
    }
    case "LOADING_FAILED": {
      return {
        ...state,
        graphLoading: false
      }
    }
    case "CLEAR_PROCESS": {
      return {
        ...state,
        processToDisplay: {},
        fetchedProcessDetails: {},
        testResults: null
      }
    }
    case "DISPLAY_MODAL_NODE_DETAILS":
    case "DISPLAY_NODE_DETAILS":
      if (state.groupingState) {
        const newNodeId = action.nodeToDisplay.id
        return {
           ...state,
          //FIXME: dodanie do grupy
          groupingState: canGroup(state, action.nodeToDisplay) ? _.concat(state.groupingState, newNodeId) : state.groupingState
        }
      } else {
        return {
          ...state,
          nodeToDisplay: action.nodeToDisplay
        }
      }

    case "DISPLAY_MODAL_EDGE_DETAILS": {
      return {
        ...state,
        edgeToDisplay: action.edgeToDisplay
      }
    }

    case "EDIT_EDGE": {
      const processToDisplay = GraphUtils.mapProcessWithNewEdge(state.processToDisplay, action.before, action.after)
      return {
        ...state,
        processToDisplay: {
          ...processToDisplay,
          validationResult: action.validationResult
        },
        edgeToDisplay: action.after,
      }
    }
    case "EDIT_NODE": {
      const stateAfterNodeRename = updateAfterNodeIdChange(state, action.before.id, action.after.id);
      const processToDisplay = GraphUtils.mapProcessWithNewNode(stateAfterNodeRename.processToDisplay, action.before, action.after)
      return {
        ...stateAfterNodeRename,
        processToDisplay: {
          ...processToDisplay,
          validationResult: action.validationResult
        },
        nodeToDisplay: action.after,
      }
    }
    case "DELETE_NODE": {
      var idToDelete = action.id
      const stateAfterNodeDelete = updateAfterNodeDelete(state, idToDelete)
      const processToDisplay = GraphUtils.deleteNode(stateAfterNodeDelete.processToDisplay, idToDelete);
      return {
        ...stateAfterNodeDelete,
        processToDisplay: processToDisplay,
        nodeToDisplay: processToDisplay.properties,
      }
    }
    case "URL_CHANGED": {
      return {
        ...state,
        ...emptyGraphState,
      }
    }
    case "NODES_CONNECTED": {
      const baseEdge = {from: action.fromNode.id, to: action.toNode.id}
      const edgeType = NodeUtils.edgeType(state.processToDisplay.edges, action.fromNode, action.processDefinitionData)
      const edge = edgeType ? {...baseEdge, edgeType: edgeType} : baseEdge
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          edges: _.concat(state.processToDisplay.edges, edge)
        }
      }
    }
    case "NODES_DISCONNECTED": {
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          edges: _.reject(state.processToDisplay.edges, (e) => e.from == action.from && e.to == action.to)
        }
      }
    }
    case "NODE_ADDED": {
      var newId = createUniqueNodeId(state.processToDisplay.nodes, state.processToDisplay.nodes.length)
      return {
        ...state,
        processToDisplay: {
          ...state.processToDisplay,
          nodes: _.concat(state.processToDisplay.nodes, {
            ... action.node,
            id: newId
          })
        },
        layout: _.concat(state.layout, {id: newId, position: action.position})
      }
    }
    //TODO: jakos inaczej to obsluzyc??
    case "LAYOUT_CHANGED": {
      return {
        ...state,
        layout: action.layout
      }
    }
    case "DISPLAY_PROCESS_COUNTS": {
      return {
        ...state,
        processCounts: action.processCounts
      }
    }
    case "DISPLAY_TEST_RESULTS_DETAILS": {
      return {
        ...state,
        testResults: action.testResults,
        graphLoading: false
      }
    }
    case "HIDE_RUN_PROCESS_DETAILS": {
      return {
        ...state,
        testResults: null,
        processCounts: null
      }
    }
    case "START_GROUPING": {
      return {
        ...state,
        groupingState: [],
        nodeToDisplay: state.processToDisplay.properties
      }
    }
    case "FINISH_GROUPING": {
      const withUpdatedGroups = state.groupingState.length > 1 ?
        {
          ...state,
          processToDisplay: NodeUtils.createGroup(state.processToDisplay, state.groupingState),
          layout: []
        } :  state;
      return _.omit(withUpdatedGroups, 'groupingState')
    }
    case "CANCEL_GROUPING": {
      return _.omit(state, 'groupingState')
    }
    case "UNGROUP": {
      return {
        ...state,
        processToDisplay: NodeUtils.ungroup(state.processToDisplay, action.groupToRemove),
        layout: [],
        nodeToDisplay: state.processToDisplay.properties,
      }
    }
    case "EXPAND_GROUP":
    case "COLLAPSE_GROUP": {
      return {
        ...state,
        layout: []
      }
    }
    case "EDIT_GROUP": {
      return {
        ...state,
        processToDisplay: NodeUtils.editGroup(state.processToDisplay, action.oldGroupId, action.newGroup),
        nodeToDisplay: action.newGroup,
        layout: updateLayoutAfterNodeIdChange(state.layout, action.oldGroupId, action.newGroup.id)
      }
    }
    default:
      return state
  }

  function canGroup(state, newNode) {
    const newNodeId = newNode.id
    const currentGrouping = state.groupingState
    return !NodeUtils.nodeIsGroup(newNode) && currentGrouping.length == 0 ||
      currentGrouping.find(nodeId => state.processToDisplay.edges.find(edge => edge.from == nodeId && edge.to == newNodeId ||  edge.to == nodeId && edge.from == newNodeId))
  }

  function updateAfterNodeIdChange(state, oldId, newId) {
    const newLayout = updateLayoutAfterNodeIdChange(state.layout, oldId, newId);
    const withGroupsUpdated = NodeUtils.updateGroupsAfterNodeIdChange(state.processToDisplay, oldId, newId);
    return {
      ...state,
      processToDisplay: withGroupsUpdated,
      layout: newLayout
    }
  }

  function updateLayoutAfterNodeIdChange(layout, oldId, newId) {
    return _.map(layout, (n) => {
      if (oldId === n.id) {
        return {
          ...n,
          id: newId
        }
      } else return n;
    });
  }

  function updateAfterNodeDelete(state, idToDelete) {
    const layoutWithoutNode = _.filter(state.layout, (n) => n.id !== idToDelete);
    const withGroupsUpdated = NodeUtils.updateGroupsAfterNodeDelete(state.processToDisplay, idToDelete);
    return {
      ...state,
      processToDisplay: withGroupsUpdated,
      layout: layoutWithoutNode
    }
  }

  function createUniqueNodeId(nodes, nodeCounter) {
    var newId = `node${nodeCounter}`
    return _.some(nodes, (n) => {return n.id == newId}) ? createUniqueNodeId(nodes, nodeCounter + 1) : newId
  }
}

const emptyProcessActivity = {
  comments: [],
  attachments: []
}

function processActivityReducer(state = emptyProcessActivity, action) {
  switch (action.type) {
    case "DISPLAY_PROCESS_ACTIVITY": {
      return {
        ...state,
        comments: action.comments,
        attachments: action.attachments
      }
    }
    default:
      return state
  }
}

const espUndoableConfig = {
  blacklist: ["CLEAR_PROCESS", "PROCESS_LOADING", "URL_CHANGED"]
}
const rootReducer = combineReducers({
  graphReducer: UndoRedoReducer.espUndoable(graphReducer, espUndoableConfig),
  settings: settingsReducer,
  ui: uiStateReducer,
  processActivity: processActivityReducer
});

export default rootReducer;
<|endoftext|>


<commit_before>// @flow

import { StatusBar } from 'react-native';
import { Immersive } from 'react-native-immersive';

import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
import {
    CONFERENCE_FAILED,
    CONFERENCE_JOINED,
    CONFERENCE_LEFT,
    CONFERENCE_WILL_JOIN,
    SET_AUDIO_ONLY,
    getCurrentConference
} from '../../base/conference';
import { Platform } from '../../base/react';
import { MiddlewareRegistry } from '../../base/redux';

import { _setImmersiveListener as _setImmersiveListenerA } from './actions';
import { _SET_IMMERSIVE_LISTENER } from './actionTypes';

/**
 * Middleware that captures conference actions and activates or deactivates the
 * full screen mode. On iOS it hides the status bar, and on Android it uses the
 * immersive mode:
 * https://developer.android.com/training/system-ui/immersive.html
 * In immersive mode the status and navigation bars are hidden and thus the
 * entire screen will be covered by our application.
 *
 * @param {Store} store - The redux store.
 * @returns {Function}
 */
MiddlewareRegistry.register(store => next => action => {
    switch (action.type) {
    case _SET_IMMERSIVE_LISTENER:
        return _setImmersiveListenerF(store, next, action);

    case APP_WILL_MOUNT: {
        const result = next(action);

        store.dispatch(
            _setImmersiveListenerA(_onImmersiveChange.bind(undefined, store)));

        return result;
    }

    case APP_WILL_UNMOUNT:
        store.dispatch(_setImmersiveListenerA(undefined));
        break;

    case CONFERENCE_WILL_JOIN:
    case CONFERENCE_JOINED:
    case SET_AUDIO_ONLY: {
        const result = next(action);
        const { audioOnly } = store.getState()['features/base/conference'];
        const conference = getCurrentConference(store);

        _setFullScreen(conference ? !audioOnly : false);

        return result;
    }

    case CONFERENCE_FAILED:
    case CONFERENCE_LEFT: {
        const result = next(action);

        _setFullScreen(false);

        return result;
    }
    }

    return next(action);
});

/**
 * Handler for Immersive mode changes. This will be called when Android's
 * immersive mode changes. This can happen without us wanting, so re-evaluate if
 * immersive mode is desired and reactivate it if needed.
 *
 * @param {Object} store - The redux store.
 * @private
 * @returns {void}
 */
function _onImmersiveChange({ getState }) {
    const state = getState();
    const { appState } = state['features/background'];

    if (appState === 'active') {
        const { audioOnly } = state['features/base/conference'];
        const conference = getCurrentConference(state);
        const fullScreen = conference ? !audioOnly : false;

        _setFullScreen(fullScreen);
    }
}

/**
 * Activates/deactivates the full screen mode. On iOS it will hide the status
 * bar, and on Android it will turn immersive mode on.
 *
 * @param {boolean} fullScreen - True to set full screen mode, false to
 * deactivate it.
 * @private
 * @returns {void}
 */
function _setFullScreen(fullScreen: boolean) {
    // XXX The React Native module Immersive is only implemented on Android and
    // throws on other platforms.
    if (Platform.OS === 'android') {
        fullScreen ? Immersive.on() : Immersive.off();
    } else {
        // On platforms other than Android go with whatever React Native itself
        // supports.
        StatusBar.setHidden(fullScreen, 'slide');
    }
}

/**
 * Notifies the feature filmstrip that the action
 * {@link _SET_IMMERSIVE_LISTENER} is being dispatched within a specific redux
 * store.
 *
 * @param {Store} store - The redux store in which the specified action is being
 * dispatched.
 * @param {Dispatch} next - The redux dispatch function to dispatch the
 * specified action to the specified store.
 * @param {Action} action - The redux action {@code _SET_IMMERSIVE_LISTENER}
 * which is being dispatched in the specified store.
 * @private
 * @returns {Object} The value returned by {@code next(action)}.
 */
function _setImmersiveListenerF({ getState }, next, action) {
    // XXX The React Native module Immersive is only implemented on Android and
    // throws on other platforms.
    if (Platform.OS === 'android') {
        // Remove the old Immersive listener and add the new one.
        const { listener: oldListener } = getState()['features/full-screen'];
        const result = next(action);
        const { listener: newListener } = getState()['features/full-screen'];

        if (oldListener !== newListener) {
            oldListener && Immersive.removeImmersiveListener(oldListener);
            newListener && Immersive.addImmersiveListener(newListener);
        }

        return result;
    }

    return next(action);
}
<commit_msg>ref(full-screen/middleware): use StateListenerRegistry<commit_after>// @flow

import { StatusBar } from 'react-native';
import { Immersive } from 'react-native-immersive';

import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
import { getCurrentConference } from '../../base/conference';
import { Platform } from '../../base/react';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';

import { _setImmersiveListener as _setImmersiveListenerA } from './actions';
import { _SET_IMMERSIVE_LISTENER } from './actionTypes';

/**
 * Middleware that captures conference actions and activates or deactivates the
 * full screen mode. On iOS it hides the status bar, and on Android it uses the
 * immersive mode:
 * https://developer.android.com/training/system-ui/immersive.html
 * In immersive mode the status and navigation bars are hidden and thus the
 * entire screen will be covered by our application.
 *
 * @param {Store} store - The redux store.
 * @returns {Function}
 */
MiddlewareRegistry.register(store => next => action => {
    switch (action.type) {
    case _SET_IMMERSIVE_LISTENER:
        return _setImmersiveListenerF(store, next, action);

    case APP_WILL_MOUNT: {
        const result = next(action);

        store.dispatch(
            _setImmersiveListenerA(_onImmersiveChange.bind(undefined, store)));

        return result;
    }

    case APP_WILL_UNMOUNT:
        store.dispatch(_setImmersiveListenerA(undefined));
        break;

    }

    return next(action);
});

StateListenerRegistry.register(
    /* selector */ state => {
        const { audioOnly } = state['features/base/conference'];
        const conference = getCurrentConference(state);

        return conference ? !audioOnly : false;
    },
    /* listener */ fullScreen => _setFullScreen(fullScreen)
);

/**
 * Handler for Immersive mode changes. This will be called when Android's
 * immersive mode changes. This can happen without us wanting, so re-evaluate if
 * immersive mode is desired and reactivate it if needed.
 *
 * @param {Object} store - The redux store.
 * @private
 * @returns {void}
 */
function _onImmersiveChange({ getState }) {
    const state = getState();
    const { appState } = state['features/background'];

    if (appState === 'active') {
        const { audioOnly } = state['features/base/conference'];
        const conference = getCurrentConference(state);
        const fullScreen = conference ? !audioOnly : false;

        _setFullScreen(fullScreen);
    }
}

/**
 * Activates/deactivates the full screen mode. On iOS it will hide the status
 * bar, and on Android it will turn immersive mode on.
 *
 * @param {boolean} fullScreen - True to set full screen mode, false to
 * deactivate it.
 * @private
 * @returns {void}
 */
function _setFullScreen(fullScreen: boolean) {
    // XXX The React Native module Immersive is only implemented on Android and
    // throws on other platforms.
    if (Platform.OS === 'android') {
        fullScreen ? Immersive.on() : Immersive.off();
    } else {
        // On platforms other than Android go with whatever React Native itself
        // supports.
        StatusBar.setHidden(fullScreen, 'slide');
    }
}

/**
 * Notifies the feature filmstrip that the action
 * {@link _SET_IMMERSIVE_LISTENER} is being dispatched within a specific redux
 * store.
 *
 * @param {Store} store - The redux store in which the specified action is being
 * dispatched.
 * @param {Dispatch} next - The redux dispatch function to dispatch the
 * specified action to the specified store.
 * @param {Action} action - The redux action {@code _SET_IMMERSIVE_LISTENER}
 * which is being dispatched in the specified store.
 * @private
 * @returns {Object} The value returned by {@code next(action)}.
 */
function _setImmersiveListenerF({ getState }, next, action) {
    // XXX The React Native module Immersive is only implemented on Android and
    // throws on other platforms.
    if (Platform.OS === 'android') {
        // Remove the old Immersive listener and add the new one.
        const { listener: oldListener } = getState()['features/full-screen'];
        const result = next(action);
        const { listener: newListener } = getState()['features/full-screen'];

        if (oldListener !== newListener) {
            oldListener && Immersive.removeImmersiveListener(oldListener);
            newListener && Immersive.addImmersiveListener(newListener);
        }

        return result;
    }

    return next(action);
}
<|endoftext|>