Source: public/modules/core/directives/scale-question.client.directive.js

'use strict';

/**
 * The `scaleQuestion` directive is used to dynamically create a Likert-type
 * scale question into a view. It is used by the {@link
    * Angular.questionnaireDirective|questionnaire} directive to build whole
 * questionnaires.
 *
 * @class Angular.scaleQuestionDirective
 * @see Angular.questionnaireDirective
 */

angular.module('core').directive('scaleQuestion', [
    '$compile',
    'TrialData',
    '$log',
    function($compile, TrialData, $log) {

        $log.debug('Compiling scaleQuestion directive.');

        /**
         * The data used to build the scale question is found in the `scope`
         * argument passed to the directive's `#link` function. When a
         * {@link Angular.questionnaireDirective|questionnaire} directive
         * employs a `scaleQuestion` directive, it passes
         * the appropriate {@link Angular.questionnaireDirective#data~questionnaireStructureEntry|questionnaireStructureEntry}
         * for this parameter.
         *
         * @name scope
         * @memberof Angular.scaleQuestionDirective
         * @inner
         * @type {{}}
         * @see Angular.questionnaireDirective
         */

        // Build a header (main question text) row
        function buildHeaderRow(labelType, labelHTML) {

            var row;
            row = angular.element('<div></div>');
            row.addClass('row');

            var columns = angular.element('<div></div>');
            columns.addClass('col-md-12');

            var label;

            if (labelType === 'labelLeft') {
                label = angular.element('<label></label>');
            } else {
                label = angular.element('<h3></h3>');
            }

            label.attr('translate', '');
            label.html(labelHTML);

            row.append(columns);
            columns.append(label);

            return row;
        }

        // Build a row to hold the images
        function buildImageRow(singleImgSrc, leftImgSrc, rightImgSrc) {

            var row;

            // If there is either a single image source or there are both
            // left and right image sources
            if (typeof singleImgSrc === 'string' ||
                (typeof leftImgSrc === 'string' &&
                 typeof rightImgSrc === 'string')) {

                // Main row
                row = angular.element('<div></div>');
                row.addClass('row likert-image-row');

                var twoColumns = angular.element('<div></div>');
                twoColumns.addClass('col-md-2');

                // If there is only one image
                if (singleImgSrc) {

                    var image = angular.element('<img>');
                    image.attr('src', singleImgSrc);

                    var eightColumns = angular.element('<div></div>');
                    eightColumns.addClass('col-md-8');
                    eightColumns.addClass('text-center');

                    eightColumns.append(image);

                    row.append(
                        twoColumns.clone(),
                        eightColumns,
                        twoColumns.clone()
                    );

                // If we have two side images
                } else if (leftImgSrc && rightImgSrc) {

                    var leftImage = angular.element('<img>');
                    leftImage.attr('src', leftImgSrc);

                    var leftImageColumns = twoColumns.clone();
                    leftImageColumns.append(leftImage);

                    var rightImage = angular.element('<img>');
                    rightImage.attr('src', rightImgSrc);

                    var rightImageColumns = twoColumns.clone();
                    rightImageColumns.append(rightImage);

                    var fourColumns = angular.element('<div></div>');
                    fourColumns.addClass('col-md-4');

                    row.append(
                        twoColumns.clone(),
                        leftImageColumns,
                        fourColumns.clone(),
                        rightImageColumns,
                        twoColumns.clone()
                    );
                }
            }

            return row;
        }

        // Build a row that holds descriptions under each radio button
        function buildDescriptionsRow(minimumDesc, maximumDesc) {

            var descriptions;
            if (minimumDesc && maximumDesc) {

                // Main row div
                descriptions = angular.element('<div></div>');
                descriptions.addClass('row');
                descriptions.addClass('row-likert-descriptions');

                // Side and center spacers
                var sideSpacer = angular.element('<div></div>');
                sideSpacer.addClass('col-md-2');

                var centerSpacer = angular.element('<div></div>');
                centerSpacer.addClass('col-md-4');

                var descriptionsInnerRow = angular.element('<div></div>');
                descriptionsInnerRow.addClass('col-md-8');
                descriptionsInnerRow.addClass('likert-descriptions-container');

                var fifthsColumnSpacer = angular.element('<div></div>');
                fifthsColumnSpacer.addClass('col-md-5ths');

                // Left and right text blocks
                var leftTextBlock = angular.element('<div translate></div>');
                leftTextBlock.addClass('col-md-5ths');
                leftTextBlock.addClass('small');
                leftTextBlock.addClass('text-center');
                leftTextBlock.addClass('likert-minimum-description');
                leftTextBlock.html(minimumDesc);

                var rightTextBlock = leftTextBlock.clone();
                rightTextBlock.removeClass('likert-minimum-description');
                rightTextBlock.addClass('likert-maximum-description');
                rightTextBlock.html(maximumDesc);

                // Append text blocks and spacers to inner row
                descriptionsInnerRow.append(leftTextBlock);
                descriptionsInnerRow.append(
                    fifthsColumnSpacer.clone(),
                    fifthsColumnSpacer.clone(),
                    fifthsColumnSpacer.clone()
                );
                descriptionsInnerRow.append(rightTextBlock);

                // Append children to main row
                descriptions.append(
                    sideSpacer.clone(),
                    descriptionsInnerRow,
                    sideSpacer.clone()
                );
            }

            return descriptions;
        }

        function questionIsValid(questionOptions,
                                         singleImgSrc,
                                         leftImgSrc,
                                         rightImgSrc,
                                         minimumDescription,
                                         maximumDescription) {

            // questionOptions must be present...
            if (questionOptions !== undefined) {

                // ...must be an object...
                if (typeof questionOptions !== 'object' ||
                    Array.isArray(questionOptions)) {
                    $log.error('questionOptions was not an object.');
                    return false;
                }

                // ...and must have a choices property...
                if (!questionOptions.hasOwnProperty('choices')) {
                    $log.error(
                        'questionOptions did not have a choice property.'
                    );
                    return false;
                }

                // questionOptions.choices must be an array...
                if (!Array.isArray(questionOptions.choices)) {
                    $log.error('questionOptions.choices was not an array.');
                    return false;
                }

                // ...must have five entries...
                if (questionOptions.choices.length !== 5) {
                    $log.error('questionOptions.choices did not have five' +
                        ' choices.');
                    return false;
                }

                for (var i in questionOptions.choices) {
                    var choice = questionOptions.choices[i];

                    // ...that are objects...
                    if (typeof choice !== 'object' || Array.isArray(choice)) {
                        $log.error('questionOptions.choices[' + i + '] was ' +
                            'not an object.');
                        return false;
                    }

                    // ...that each have a label property...
                    if (!choice.hasOwnProperty('label')) {
                        $log.error('questionOptions.choices[' + i + '] did ' +
                            'not have a label property.');
                        return false;
                    }

                    // ...that are each strings
                    if (typeof choice.label !== 'string') {
                        $log.error('questionOptions.choices[' + i + '].label ' +
                            'was not a string.');
                        return false;
                    }
                }
            }

            // If one image source was provided, it must be a string
            if (singleImgSrc !== undefined &&
                typeof singleImgSrc !== 'string') {
                $log.error('single-img-src was not a string.');
                return false;
            }

            // If one of the two image sources was provided, we must have the
            // other
            if ((leftImgSrc !== undefined && rightImgSrc === undefined) ||
                (rightImgSrc !== undefined && leftImgSrc === undefined)) {

                $log.error('Either left-img-src or right-img-src was given,' +
                    ' but the other was not.');
                return false;
            }

            // If two image sources were provided, both must be strings
            if (leftImgSrc !== undefined && rightImgSrc !== undefined) {
                if (typeof leftImgSrc !== 'string' ||
                    typeof rightImgSrc !== 'string') {

                    $log.error('Either left-img-src or right-img-src was' +
                        ' not a string');
                    return false;
                }
            }

            // If one of the two descriptions was provided, we must have the
            // other
            if ((minimumDescription !== undefined &&
                maximumDescription === undefined) ||
                (maximumDescription !== undefined &&
                minimumDescription === undefined)) {

                $log.error('Either minimum-description or maximum-description' +
                    ' was given, but the other was not.');
                return false;
            }

            // If two descriptions were provided, both must be strings
            if (minimumDescription !== undefined &&
                maximumDescription !== undefined) {

                if (typeof minimumDescription !== 'string' ||
                    typeof maximumDescription !== 'string') {

                    $log.error('Either minimum-description or' +
                        ' maximum-description was not a string.');
                    return false;
                }
            }

            return true;
        }

        function buildOptionLabelsRow(questionOptions) {

            var optionLabelsRow;

            if (questionOptions !== undefined &&
                questionOptions.hasOwnProperty('choices')) {

                var choices = questionOptions.choices;

                var div = angular.element('<div></div>');

                optionLabelsRow = div.clone();
                optionLabelsRow.addClass('row row-likert-option-label');

                var spacer = div.clone();
                spacer.addClass('col-md-2 option-label-spacer');

                optionLabelsRow.append(spacer.clone());

                var choiceContainer = div.clone();
                choiceContainer.addClass(
                    'col-md-8 likert-option-label-container text-center'
                );

                var choice = div.clone();
                choice.addClass('col-md-5ths likert-option-label text-center');
                choice.attr('translate', '');

                for (var i in choices) {
                    var thisChoice = choice.clone();
                    thisChoice.html(
                        '{{ \'' + choices[i].label + '\' }}'
                    );
                    choiceContainer.append(thisChoice);
                }

                optionLabelsRow.append(choiceContainer);

                optionLabelsRow.append(spacer.clone());
            }

            return optionLabelsRow;
        }

        function buildRadiosRow(questionId, questionRequired) {

            // Don't require question if explicitly told not to do so in
            // data
            var responseRequired = false;
            if (questionRequired !== false) {
                responseRequired = true;
            }

            var div = angular.element('<div></div>');

            var radioDiv = div.clone();
            radioDiv.addClass('col-md-5ths text-center');

            var centerContainer = div.clone();
            centerContainer.addClass('col-md-8 text-center');

            var radioInput = angular.element('<input>');
            radioInput.attr('type', 'radio');
            radioInput.attr('name', questionId + 'RadioGroup');
            radioInput.attr('ng-model', questionId + 'RadioGroup');

            if (responseRequired) {
                radioInput.attr('required', '');
            }

            for (var i = 1; i <= 5; i++) {
                var thisRadioDiv = radioDiv.clone();

                var thisRadio = radioInput.clone();
                thisRadio.attr('id', questionId + 'RadioGroup' + i);
                thisRadio.attr('value', i);

                thisRadioDiv.append(thisRadio);
                centerContainer.append(thisRadioDiv);
            }

            var radiosRow = div.clone();
            radiosRow.addClass('row');

            var spacer = div.clone();
            spacer.addClass('col-md-2');

            radiosRow.append(
                spacer.clone(),
                centerContainer,
                spacer.clone()
            );

            return radiosRow;
        }

        return {
            restrict: 'E',
            scope: {},
            controller: function scaleQuestionController() {
                this.questionOptionsAreValid = questionIsValid;
            },

            link: function(scope, element, attrs, ctrl) {

                // Validate the questionOptions
                if (!questionIsValid(
                        element.data('questionOptions'),
                        attrs.singleImgSrc,
                        attrs.leftImgSrc,
                        attrs.rightImgSrc,
                        attrs.minimumDescription,
                        attrs.maximumDescription)
                    ) {
                    return;
                }

                // Bind input to TrialData
                scope.sendToTrialData = function(path, value) {
                    if (!attrs.associatedToMedia) {
                        TrialData.setValueForPath(path, value);
                    } else {
                        TrialData.setValueForPathForCurrentMedia(path, value);
                    }
                };

                // Initialize a dynamically-named variable to keep track of
                // changing responses to null
                scope[attrs.questionId + 'RadioGroup'] = null;

                // Watch that dynamically named variable and update
                // TrialData when it changes
                scope.$watch(
                    attrs.questionId + 'RadioGroup',
                    function(newValue) {
                        scope.sendToTrialData(
                            attrs.controllerDataPath,
                            parseInt(newValue)
                        );
                    });

                var headerRow = buildHeaderRow(
                    attrs.labelType,
                    attrs.questionLabel
                );

                var imageRow = buildImageRow(
                    attrs.singleImgSrc,
                    attrs.leftImgSrc,
                    attrs.rightImgSrc
                );

                var radiosRow = buildRadiosRow(
                    attrs.questionId,
                    element.data('questionRequired')
                );

                var optionLabelsRow = buildOptionLabelsRow(
                    element.data('questionOptions')
                );

                var descriptionsRow = buildDescriptionsRow(
                    attrs.minimumDescription,
                    attrs.maximumDescription
                );

                // Wrap everything in a div with well and row classes
                var wellRow = angular.element('<div></div>');
                wellRow.addClass('row well');

                // Append everything we built
                wellRow.append(
                    headerRow,
                    imageRow,
                    optionLabelsRow,
                    radiosRow,
                    descriptionsRow
                );

                // Add well div to element and compile the element
                element.append(wellRow);
                $compile(element.contents())(scope);
            }
        };
    }]);