<commit_before>/**
 * AngularJS Directive to allow autocomplete and dropdown suggestions
 * on textareas.
 *
 * Homepage: https://github.com/aurbano/smart-area
 *
 * @author Alejandro U. Alvarez (http://urbanoalvarez.es)
 * @license AGPLv3 (See LICENSE)
 */

angular.module('smartArea', [])
    .directive('smartArea', function($compile) {
    return {
        restrict: 'A',
        scope: {
            areaConfig: '=smartArea',
            areaData: '=ngModel'
        },
        replace: true,
        link: function(scope, textArea){
            if(textArea[0].tagName.toLowerCase() !== 'textarea'){
                console.warn("smartArea can only be used on textareas");
                return false;
            }

            // Caret tracking inspired by
            // https://github.com/component/textarea-caret-position
            // Properties to be copied over from the textarea
            var properties = [
                'direction',  // RTL support
                'boxSizing',
                'width',  // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
                'overflowX',
                'overflowY',  // copy the scrollbar for IE
                'color',
                'height',

                'borderTopWidth',
                'borderRightWidth',
                'borderBottomWidth',
                'borderLeftWidth',

                'borderTopColor',
                'borderRightColor',
                'borderBottomColor',
                'borderLeftColor',

                'borderTopStyle',
                'borderRightStyle',
                'borderBottomStyle',
                'borderLeftStyle',
                'borderRadius',

                'backgroundColor',

                'paddingTop',
                'paddingRight',
                'paddingBottom',
                'paddingLeft',

                // https://developer.mozilla.org/en-US/docs/Web/CSS/font
                'fontStyle',
                'fontVariant',
                'fontWeight',
                'fontStretch',
                'fontSize',
                'fontSizeAdjust',
                'lineHeight',
                'fontFamily',

                'textAlign',
                'textTransform',
                'textIndent',
                'textDecoration',  // might not make a difference, but better be safe

                'letterSpacing',
                'wordSpacing',
                'whiteSpace',
                'wordBreak',
                'wordWrap'
            ];

            // Build the HTML structure
            var mainWrap = angular.element('

'),
                isFirefox = !(window.mozInnerScreenX === null);

            scope.fakeAreaElement = angular.element($compile('

')(scope))
                .appendTo(mainWrap);

            scope.dropdown.element = angular.element($compile('
 0"><input type="text" class="form-control" ng-model="dropdown.filter" ng-show="dropdown.showFilter"/>

')(scope))
                .appendTo(mainWrap);

            scope.dropdown.filterElement = scope.dropdown.element.find('input');
            scope.dropdown.filterElement.bind('keydown', scope.keyboardEvents);

            // Default textarea css for the div
            scope.fakeAreaElement.css('whiteSpace', 'pre-wrap');
            scope.fakeAreaElement.css('wordWrap', 'break-word');

            // Transfer the element's properties to the div
            properties.forEach(function (prop) {
                scope.fakeAreaElement.css(prop, textArea.css(prop));
            });

            scope.fakeAreaElement.css('width',(parseInt(textArea.outerWidth()) + 1) + 'px');

            // Special considerations for Firefox
//            if (isFirefox) {
//                scope.fakeAreaElement.css('width',parseInt(textArea.width()) - 2 + 'px');  // Firefox adds 2 pixels to the padding - https://bugzilla.mozilla.org/show_bug.cgi?id=753662
//                // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
//                if (textArea.scrollHeight > parseInt(textArea.height)){
//                    scope.fakeAreaElement.css('overflowY', 'scroll');
//                }
//            }

            // Insert the HTML elements
            mainWrap.insertBefore(textArea);
            textArea.appendTo(mainWrap).addClass('sa-realArea').attr('ng-trim',false);
            $compile(textArea);

            // Dirty hack to maintain the height
            textArea.on('keyup', function(){
                scope.fakeAreaElement.height(textArea.height());
            });

            return mainWrap;
        },
        controller: ['$scope', '$element', '$timeout', '$sce', function($scope, $element, $timeout, $sce){
            /* +----------------------------------------------------+
             * +                     Scope Data                     +
             * +----------------------------------------------------+ */

            $scope.fakeArea = $scope.areaData;
            $scope.dropdownContent = 'Dropdown';
            $scope.dropdown = {
                content: [],
                element: null,
                current: 0,
                select: null,
                customSelect: null,
                filter: '',
                match: '',
                mode: 'append',
                showFilter: false,
                filterElement: null
            };

            /* +----------------------------------------------------+
             * +                   Scope Watches                    +
             * +----------------------------------------------------+ */
            $scope.$watch('dropdown.filter', function(){
                $scope.dropdown.current = 0;
            });

            $scope.$watch('areaData', function(){
                $scope.trackCaret();

                // TODO Track caret on another fake area, so I don't have to recalculate autocomplete triggers every time the cursor moves.
                checkTriggers();
            });

            /* +----------------------------------------------------+
             * +                  Scope Functions                   +
             * +----------------------------------------------------+ */

            /**
             * Update the Dropdown position according to the current caret position
             * on the textarea
             */
            $scope.trackCaret = function(){
                var text = $scope.areaData,
                    position = getCharacterPosition();

                $scope.fakeArea = $sce.trustAsHtml(text.substring(0, position) + '' + text.substring(position));

                // Tracking span
                $timeout(function(){
                    var span = $scope.fakeAreaElement.find('span.sa-tracking');
                    if(span.length > 0){
                        var spanOffset = span.position();
                        // Move the dropdown
                        $scope.dropdown.element.css({
                            top: (spanOffset.top + parseInt($element.css('fontSize')) + 2)+'px',
                            left: (spanOffset.left)+'px'
                        });
                    }
                    highlightText();
                }, 0);
            };

            /**
             * Keyboard event reacting. This function is triggered by
             * keydown events in the dropdown filter and the main textarea
             *
             * @param event JavaScript event
             */
            $scope.keyboardEvents = function(event){
                if($scope.dropdown.content.length > 0) {
                    var code = event.keyCode || event.which;
                    if (code === 13) { // Enter
                        event.preventDefault();
                        event.stopPropagation();
                        // Add the selected word from the Dropdown
                        // to the areaData in the current position
                        $timeout(function(){
                            $scope.dropdown.selected($scope.dropdown.content[$scope.dropdown.current]);
                        },0);
                    }else if(code === 38){ // Up
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.current--;
                            if($scope.dropdown.current < 0){
                                $scope.dropdown.current = $scope.dropdown.content.length - 1; // Wrap around
                            }
                        },0);
                    }else if(code === 40){ // Down
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.current++;
                            if($scope.dropdown.current >= $scope.dropdown.content.length){
                                $scope.dropdown.current = 0; // Wrap around
                            }
                        },0);
                    }else if(code === 27){ // Esc
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.content = [];
                            $element[0].focus();
                        },0);
                    }else if(code === 8){ // Backspace
                        if($scope.dropdown.filter.length < 1){
                            $timeout(function(){
                                $scope.dropdown.content = [];
                                $element[0].focus();
                            },0);
                        }else{
                            event.stopPropagation();
                        }
                    }else{
                        $scope.dropdown.filterElement.focus();
                    }
                }
            };

            /**
             * Add an item to the textarea, this is called
             * when selecting an element from the dropdown.
             * @param item Selected object
             */
            $scope.dropdown.selected = function(item){
                if($scope.dropdown.customSelect !== null){
                    var append = $scope.dropdown.mode === 'append';
                    addSelectedDropdownText($scope.dropdown.customSelect(item), append);
                }else{
                    addSelectedDropdownText(item.display);
                }
                $scope.dropdown.content = [];
            };

            /* +----------------------------------------------------+
             * +                Internal Functions                  +
             * +----------------------------------------------------+ */

            /**
             * Add text to the textarea, this handles positioning the text
             * at the caret position, and also either replacing the last word
             * or appending as new content.
             *
             * @param selectedWord Word to add to the textarea
             * @param append Whether it should be appended or replace the last word
             */
            function addSelectedDropdownText(selectedWord, append){

                $scope.dropdown.showFilter = false;
                $scope.dropdown.filter = '';

                var text = $scope.areaData,
                    position = getCharacterPosition(),
                    lastWord = text.substr(0, position).split(/[\s\b{}]/),
                    remove = lastWord[lastWord.length - 1].length;

                if(!append && $scope.dropdown.match){
                  remove = $scope.dropdown.match.length;
                }

                if(append || remove < 0){
                    remove = 0;
                }

                // Now remove the last word, and replace with the dropped down one
                $scope.areaData = text.substr(0, position - remove) +
                    selectedWord +
                    text.substr(position);

                if(!append && $scope.dropdown.match){
                  position = position - $scope.dropdown.match.length + selectedWord.toString().length;
                }

                // Now reset the caret position
                if($element[0].selectionStart) {
                    $timeout(function(){
                        $element[0].focus();
                        $element[0].setSelectionRange(position - remove + selectedWord.toString().length, position - remove + selectedWord.toString().length);
                        checkTriggers();
                    }, 100);
                }

            }

            /**
             * Perform the "syntax" highlighting of autocomplete words that have
             * a cssClass specified.
             */
            function highlightText(){
                var text = $scope.areaData;

                if(typeof($scope.areaConfig.autocomplete) === 'undefined' || $scope.areaConfig.autocomplete.length === 0){
                    return;
                }

                $scope.areaConfig.autocomplete.forEach(function(autoList){
                    for(var i=0; i<autoList.words.length; i++){
                        if(typeof(autoList.words[i]) === "string"){
                            text = text.replace(new RegExp("([^\\w]|\\b)("+autoList.words[i]+")([^\\w]|\\b)", 'g'), '$1$2$3');
                        }else{
                            text = text.replace(autoList.words[i], function(match){
                                return ''+match+'';
                            });
                        }
                    }
                });
                // Add to the fakeArea
                $scope.fakeArea = $sce.trustAsHtml(text);
            }

            /**
             * Check all the triggers
             */
            function checkTriggers(){
                triggerDropdownAutocomplete();
                triggerDropdownAdvanced();
            }

            /**
             * Trigger the advanced dropdown system, this will check
             * all the specified triggers in the configuration object under dropdown,
             * and if any of them match it will call it's list() function and add the
             * elements returned from it to the dropdown.
             */
            function triggerDropdownAdvanced(){
                $scope.dropdown.showFilter = false;
                $scope.dropdown.match = false;

                if(typeof($scope.areaConfig.dropdown) === 'undefined' || $scope.areaConfig.dropdown.length === 0){
                    return;
                }

                $scope.areaConfig.dropdown.forEach(function(element){
                    // Check if the trigger is under the cursor
                    var text = $scope.areaData,
                        position = getCharacterPosition();
                    if(typeof(element.trigger) === 'string' && element.trigger === text.substr(position - element.trigger.length, element.trigger.length)){
                        // The cursor is exactly at the end of the trigger

                        element.list(function(data){
                            $scope.dropdown.content = data.map(function(el){
                              el.display = $sce.trustAsHtml(el.display);
                              return el;
                            });

                            $scope.dropdown.customSelect = element.onSelect;
                            $scope.dropdown.mode = element.mode || 'append';
                            $scope.dropdown.match = '';
                            $scope.dropdown.showFilter = element.filter || false;

                            $timeout(function(){
                                $scope.dropdown.filterElement.focus();
                            }, 10);
                        });
                    }else if(typeof(element.trigger) === 'object'){
                        // I need to get the index of the last match
                        var searchable = text.substr(0, position),
                            match, found = false, lastPosition = 0;
                        while ((match = element.trigger.exec(searchable)) !== null){
                            if(match.index === lastPosition){
                                break;
                            }
                            lastPosition = match.index;
                            if(match.index + match[0].length === position){
                                found = true;
                                break;
                            }
                        }
                        if(found){
                            element.list(match, function(data){
                                $scope.dropdown.content = data.map(function(el){
                                  el.display = $sce.trustAsHtml(el.display);
                                  return el;
                                });

                                $scope.dropdown.customSelect = element.onSelect;
                                $scope.dropdown.mode = element.mode || 'append';
                                $scope.dropdown.match = match[1];
                                $scope.dropdown.showFilter = element.filter || false;
                            });

                        }
                    }
                });
            }

            /**
             * Set the scroll on the fake area
             */
            function resetScroll(){
              $timeout(function(){
                $scope.fakeAreaElement.scrollTop($element.scrollTop());
              }, 5);
            }

            /**
             * Trigger a simple autocomplete, this checks the last word and determines
             * whether any word on the autocomplete lists matches it
             */
            function triggerDropdownAutocomplete(){
                // First check with the autocomplete words (the ones that are not objects
                var autocomplete = [],
                    suggestions = [],
                    text = $scope.areaData,
                    position = getCharacterPosition(),
                    lastWord = text.substr(0, position).split(/[\s\b{}]/);

                // Get the last typed word
                lastWord = lastWord[lastWord.length-1];

                $scope.areaConfig.autocomplete.forEach(function(autoList){
                    autoList.words.forEach(function(word){
                        if(typeof(word) === 'string' && autocomplete.indexOf(word) < 0){
                            if(lastWord.length > 0 || lastWord.length < 1 && autoList.autocompleteOnSpace){
                                autocomplete.push(word);
                            }
                        }
                    });
                });

                if ($scope.areaConfig.dropdown !== undefined){
                    $scope.areaConfig.dropdown.forEach(function(element){
                        if(typeof(element.trigger) === 'string' && autocomplete.indexOf(element.trigger) < 0){
                            autocomplete.push(element.trigger);
                        }
                    });
                }

                // Now with the list, filter and return
                autocomplete.forEach(function(word){
                    if(lastWord.length < word.length && word.toLowerCase().substr(0, lastWord.length) === lastWord.toLowerCase()){
                        suggestions.push({
                            display: $sce.trustAsHtml(word),
                            data: null
                        });
                    }
                });

                $scope.dropdown.customSelect = null;
                $scope.dropdown.current = 0;
                $scope.dropdown.content = suggestions;
            }

            /**
             * Get Character count on an editable field
             * http://stackoverflow.com/questions/4767848/get-caret-cursor-position-in-contenteditable-area-containing-html-content
             */
            function getCharacterPosition() {
              var el = $element[0];
              if (typeof(el.selectionEnd) == "number") {
                return el.selectionEnd;
              }
          }

            /* +----------------------------------------------------+
             * +                   Event Binding                    +
             * +----------------------------------------------------+ */

            $element.bind('keyup click focus', function () {
                $timeout(function(){
                    $scope.trackCaret();
                }, 0);
            });

            $element.bind('keydown', function(event){
              resetScroll();
              $scope.keyboardEvents(event);
            });
        }]
    };
});
<commit_msg>Remove menu filter<commit_after>/**
 * AngularJS Directive to allow autocomplete and dropdown suggestions
 * on textareas.
 *
 * Homepage: https://github.com/aurbano/smart-area
 *
 * @author Alejandro U. Alvarez (http://urbanoalvarez.es)
 * @license AGPLv3 (See LICENSE)
 */

angular.module('smartArea', [])
    .directive('smartArea', function($compile) {
    return {
        restrict: 'A',
        scope: {
            areaConfig: '=smartArea',
            areaData: '=ngModel'
        },
        replace: true,
        link: function(scope, textArea){
            if(textArea[0].tagName.toLowerCase() !== 'textarea'){
                console.warn("smartArea can only be used on textareas");
                return false;
            }

            // Caret tracking inspired by
            // https://github.com/component/textarea-caret-position
            // Properties to be copied over from the textarea
            var properties = [
                'direction',  // RTL support
                'boxSizing',
                'width',  // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
                'overflowX',
                'overflowY',  // copy the scrollbar for IE
                'color',
                'height',

                'borderTopWidth',
                'borderRightWidth',
                'borderBottomWidth',
                'borderLeftWidth',

                'borderTopColor',
                'borderRightColor',
                'borderBottomColor',
                'borderLeftColor',

                'borderTopStyle',
                'borderRightStyle',
                'borderBottomStyle',
                'borderLeftStyle',
                'borderRadius',

                'backgroundColor',

                'paddingTop',
                'paddingRight',
                'paddingBottom',
                'paddingLeft',

                // https://developer.mozilla.org/en-US/docs/Web/CSS/font
                'fontStyle',
                'fontVariant',
                'fontWeight',
                'fontStretch',
                'fontSize',
                'fontSizeAdjust',
                'lineHeight',
                'fontFamily',

                'textAlign',
                'textTransform',
                'textIndent',
                'textDecoration',  // might not make a difference, but better be safe

                'letterSpacing',
                'wordSpacing',
                'whiteSpace',
                'wordBreak',
                'wordWrap'
            ];

            // Build the HTML structure
            var mainWrap = angular.element('

'),
                isFirefox = !(window.mozInnerScreenX === null);

            scope.fakeAreaElement = angular.element($compile('

')(scope))
                .appendTo(mainWrap);

            scope.dropdown.element = angular.element($compile('
 0"><input type="text" class="form-control" ng-show="dropdown.showFilter"/>

')(scope))
                .appendTo(mainWrap);

            scope.dropdown.filterElement = scope.dropdown.element.find('input');
            scope.dropdown.filterElement.bind('keydown', scope.keyboardEvents);

            // Default textarea css for the div
            scope.fakeAreaElement.css('whiteSpace', 'pre-wrap');
            scope.fakeAreaElement.css('wordWrap', 'break-word');

            // Transfer the element's properties to the div
            properties.forEach(function (prop) {
                scope.fakeAreaElement.css(prop, textArea.css(prop));
            });

            scope.fakeAreaElement.css('width',(parseInt(textArea.outerWidth()) + 1) + 'px');

            // Special considerations for Firefox
//            if (isFirefox) {
//                scope.fakeAreaElement.css('width',parseInt(textArea.width()) - 2 + 'px');  // Firefox adds 2 pixels to the padding - https://bugzilla.mozilla.org/show_bug.cgi?id=753662
//                // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
//                if (textArea.scrollHeight > parseInt(textArea.height)){
//                    scope.fakeAreaElement.css('overflowY', 'scroll');
//                }
//            }

            // Insert the HTML elements
            mainWrap.insertBefore(textArea);
            textArea.appendTo(mainWrap).addClass('sa-realArea').attr('ng-trim',false);
            $compile(textArea);

            // Dirty hack to maintain the height
            textArea.on('keyup', function(){
                scope.fakeAreaElement.height(textArea.height());
            });

            return mainWrap;
        },
        controller: ['$scope', '$element', '$timeout', '$sce', function($scope, $element, $timeout, $sce){
            /* +----------------------------------------------------+
             * +                     Scope Data                     +
             * +----------------------------------------------------+ */

            $scope.fakeArea = $scope.areaData;
            $scope.dropdownContent = 'Dropdown';
            $scope.dropdown = {
                content: [],
                element: null,
                current: 0,
                select: null,
                customSelect: null,
                filter: '',
                match: '',
                mode: 'append',
                showFilter: false,
                filterElement: null
            };

            /* +----------------------------------------------------+
             * +                   Scope Watches                    +
             * +----------------------------------------------------+ */
            // $scope.$watch('dropdown.filter', function(){
            //     $scope.dropdown.current = 0;
            // });

            $scope.$watch('areaData', function(){
                $scope.trackCaret();

                // TODO Track caret on another fake area, so I don't have to recalculate autocomplete triggers every time the cursor moves.
                checkTriggers();
            });

            /* +----------------------------------------------------+
             * +                  Scope Functions                   +
             * +----------------------------------------------------+ */

            /**
             * Update the Dropdown position according to the current caret position
             * on the textarea
             */
            $scope.trackCaret = function(){
                var text = $scope.areaData,
                    position = getCharacterPosition();

                $scope.fakeArea = $sce.trustAsHtml(text.substring(0, position) + '' + text.substring(position));

                // Tracking span
                $timeout(function(){
                    var span = $scope.fakeAreaElement.find('span.sa-tracking');
                    if(span.length > 0){
                        var spanOffset = span.position();
                        // Move the dropdown
                        $scope.dropdown.element.css({
                            top: (spanOffset.top + parseInt($element.css('fontSize')) + 2)+'px',
                            left: (spanOffset.left)+'px'
                        });
                    }
                    highlightText();
                }, 0);
            };

            /**
             * Keyboard event reacting. This function is triggered by
             * keydown events in the dropdown filter and the main textarea
             *
             * @param event JavaScript event
             */
            $scope.keyboardEvents = function(event){
                if($scope.dropdown.content.length > 0) {
                    var code = event.keyCode || event.which;
                    if (code === 13) { // Enter
                        event.preventDefault();
                        event.stopPropagation();
                        // Add the selected word from the Dropdown
                        // to the areaData in the current position
                        $timeout(function(){
                            $scope.dropdown.selected($scope.dropdown.content[$scope.dropdown.current]);
                        },0);
                    }else if(code === 38){ // Up
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.current--;
                            if($scope.dropdown.current < 0){
                                $scope.dropdown.current = $scope.dropdown.content.length - 1; // Wrap around
                            }
                        },0);
                    }else if(code === 40){ // Down
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.current++;
                            if($scope.dropdown.current >= $scope.dropdown.content.length){
                                $scope.dropdown.current = 0; // Wrap around
                            }
                        },0);
                    }else if(code === 27){ // Esc
                        event.preventDefault();
                        event.stopPropagation();
                        $timeout(function(){
                            $scope.dropdown.content = [];
                            $element[0].focus();
                        },0);
                    }else{
                        $scope.dropdown.filterElement.focus();
                    }
                }
            };

            /**
             * Add an item to the textarea, this is called
             * when selecting an element from the dropdown.
             * @param item Selected object
             */
            $scope.dropdown.selected = function(item){
                if($scope.dropdown.customSelect !== null){
                    var append = $scope.dropdown.mode === 'append';
                    addSelectedDropdownText($scope.dropdown.customSelect(item), append);
                }else{
                    addSelectedDropdownText(item.display);
                }
                $scope.dropdown.content = [];
            };

            /* +----------------------------------------------------+
             * +                Internal Functions                  +
             * +----------------------------------------------------+ */

            /**
             * Add text to the textarea, this handles positioning the text
             * at the caret position, and also either replacing the last word
             * or appending as new content.
             *
             * @param selectedWord Word to add to the textarea
             * @param append Whether it should be appended or replace the last word
             */
            function addSelectedDropdownText(selectedWord, append){

                $scope.dropdown.showFilter = false;

                var text = $scope.areaData,
                    position = getCharacterPosition(),
                    lastWord = text.substr(0, position).split(/[\s\b{}]/),
                    remove = lastWord[lastWord.length - 1].length;

                if(!append && $scope.dropdown.match){
                  remove = $scope.dropdown.match.length;
                }

                if(append || remove < 0){
                    remove = 0;
                }

                // Now remove the last word, and replace with the dropped down one
                $scope.areaData = text.substr(0, position - remove) +
                    selectedWord +
                    text.substr(position);

                if(!append && $scope.dropdown.match){
                  position = position - $scope.dropdown.match.length + selectedWord.toString().length;
                }

                // Now reset the caret position
                if($element[0].selectionStart) {
                    $timeout(function(){
                        $element[0].focus();
                        $element[0].setSelectionRange(position - remove + selectedWord.toString().length, position - remove + selectedWord.toString().length);
                        checkTriggers();
                    }, 100);
                }

            }

            /**
             * Perform the "syntax" highlighting of autocomplete words that have
             * a cssClass specified.
             */
            function highlightText(){
                var text = $scope.areaData;

                if(typeof($scope.areaConfig.autocomplete) === 'undefined' || $scope.areaConfig.autocomplete.length === 0){
                    return;
                }

                $scope.areaConfig.autocomplete.forEach(function(autoList){
                    for(var i=0; i<autoList.words.length; i++){
                        if(typeof(autoList.words[i]) === "string"){
                            text = text.replace(new RegExp("([^\\w]|\\b)("+autoList.words[i]+")([^\\w]|\\b)", 'g'), '$1$2$3');
                        }else{
                            text = text.replace(autoList.words[i], function(match){
                                return ''+match+'';
                            });
                        }
                    }
                });
                // Add to the fakeArea
                $scope.fakeArea = $sce.trustAsHtml(text);
            }

            /**
             * Check all the triggers
             */
            function checkTriggers(){
                triggerDropdownAutocomplete();
                triggerDropdownAdvanced();
            }

            /**
             * Trigger the advanced dropdown system, this will check
             * all the specified triggers in the configuration object under dropdown,
             * and if any of them match it will call it's list() function and add the
             * elements returned from it to the dropdown.
             */
            function triggerDropdownAdvanced(){
                $scope.dropdown.showFilter = false;
                $scope.dropdown.match = false;

                if(typeof($scope.areaConfig.dropdown) === 'undefined' || $scope.areaConfig.dropdown.length === 0){
                    return;
                }

                $scope.areaConfig.dropdown.forEach(function(element){
                    // Check if the trigger is under the cursor
                    var text = $scope.areaData,
                        position = getCharacterPosition();
                    if(typeof(element.trigger) === 'string' && element.trigger === text.substr(position - element.trigger.length, element.trigger.length)){
                        // The cursor is exactly at the end of the trigger

                        element.list(function(data){
                            $scope.dropdown.content = data.map(function(el){
                              el.display = $sce.trustAsHtml(el.display);
                              return el;
                            });

                            $scope.dropdown.customSelect = element.onSelect;
                            $scope.dropdown.mode = element.mode || 'append';
                            $scope.dropdown.match = '';
                            $scope.dropdown.showFilter = element.filter || false;

                            $timeout(function(){
                                $scope.dropdown.filterElement.focus();
                            }, 10);
                        });
                    }else if(typeof(element.trigger) === 'object'){
                        // I need to get the index of the last match
                        var searchable = text.substr(0, position),
                            match, found = false, lastPosition = 0;
                        while ((match = element.trigger.exec(searchable)) !== null){
                            if(match.index === lastPosition){
                                break;
                            }
                            lastPosition = match.index;
                            if(match.index + match[0].length === position){
                                found = true;
                                break;
                            }
                        }
                        if(found){
                            element.list(match, function(data){
                                $scope.dropdown.content = data.map(function(el){
                                  el.display = $sce.trustAsHtml(el.display);
                                  return el;
                                });

                                $scope.dropdown.customSelect = element.onSelect;
                                $scope.dropdown.mode = element.mode || 'append';
                                $scope.dropdown.match = match[1];
                                $scope.dropdown.showFilter = element.filter || false;
                            });

                        }
                    }
                });
            }

            /**
             * Set the scroll on the fake area
             */
            function resetScroll(){
              $timeout(function(){
                $scope.fakeAreaElement.scrollTop($element.scrollTop());
              }, 5);
            }

            /**
             * Trigger a simple autocomplete, this checks the last word and determines
             * whether any word on the autocomplete lists matches it
             */
            function triggerDropdownAutocomplete(){
                // First check with the autocomplete words (the ones that are not objects
                var autocomplete = [],
                    suggestions = [],
                    text = $scope.areaData,
                    position = getCharacterPosition(),
                    lastWord = text.substr(0, position).split(/[\s\b{}]/);

                // Get the last typed word
                lastWord = lastWord[lastWord.length-1];

                $scope.areaConfig.autocomplete.forEach(function(autoList){
                    autoList.words.forEach(function(word){
                        if(typeof(word) === 'string' && autocomplete.indexOf(word) < 0){
                            if(lastWord.length > 0 || lastWord.length < 1 && autoList.autocompleteOnSpace){
                                autocomplete.push(word);
                            }
                        }
                    });
                });

                if ($scope.areaConfig.dropdown !== undefined){
                    $scope.areaConfig.dropdown.forEach(function(element){
                        if(typeof(element.trigger) === 'string' && autocomplete.indexOf(element.trigger) < 0){
                            autocomplete.push(element.trigger);
                        }
                    });
                }

                // Now with the list, filter and return
                autocomplete.forEach(function(word){
                    if(lastWord.length < word.length && word.toLowerCase().substr(0, lastWord.length) === lastWord.toLowerCase()){
                        suggestions.push({
                            display: $sce.trustAsHtml(word),
                            data: null
                        });
                    }
                });

                $scope.dropdown.customSelect = null;
                $scope.dropdown.current = 0;
                $scope.dropdown.content = suggestions;
            }

            /**
             * Get Character count on an editable field
             * http://stackoverflow.com/questions/4767848/get-caret-cursor-position-in-contenteditable-area-containing-html-content
             */
            function getCharacterPosition() {
              var el = $element[0];
              if (typeof(el.selectionEnd) == "number") {
                return el.selectionEnd;
              }
          }

            /* +----------------------------------------------------+
             * +                   Event Binding                    +
             * +----------------------------------------------------+ */

            $element.bind('keyup click focus', function () {
                $timeout(function(){
                    $scope.trackCaret();
                }, 0);
            });

            $element.bind('keydown', function(event){
              resetScroll();
              $scope.keyboardEvents(event);
            });
        }]
    };
});
<|endoftext|>


<commit_before>({
	init : function(component) {
		component.set("v.isView", true);
		this.loadAfflMappings(component);
	},

	loadAfflMappings : function(component) {
		var action = component.get("c.getAfflMappings");
		action.setCallback(this, function(response) {
			if(response.getState() === "SUCCESS") {
				var settings = response.getReturnValue();
				if(namespacePrefix && namespacePrefix.length > 0) {
		    		var settings_no_prefix = {};
		    		//Remove package prefix from each custom field
		    		for(var key in settings) { //Iterate over each row
		    			settings_no_prefix[key] = {};
		    			for(var key2 in settings[key]) { //Iterate over each field in each row
			    			if(key2.endsWith('__c')) {
			    				var key2_no_prefix = key2.replace(namespacePrefix, '');
			    				settings_no_prefix[key][key2_no_prefix] = settings[key][key2];
			    			} else {
			    				settings_no_prefix[key][key2] = settings[key][key2];
			    			}
		    			}
		    		}
		    		component.set("v.afflMappings", settings_no_prefix);
				} else {
					component.set("v.afflMappings", response.getReturnValue());
				}
	    	} else if(response.getState() === "ERROR") {
	    		this.displayError(response);
	    	}
		});
		$A.enqueueAction(action);
	},

	resetSettings : function(component) {
		this.loadAfflMappings(component);
	},

	saveAfflSettings : function(component) {
		var saveAction = component.get("c.saveAfflMappings");
		var settings = component.get("v.afflMappings");
		if(namespacePrefix && namespacePrefix.length > 0) {
			var settings_w_prefix = {};
			//Add package prefix to each custom field
			for(var key in settings) {
				if(key.endsWith('__c')) {
					var key_w_prefix = namespacePrefix + key;
	    			settings_w_prefix[key_w_prefix] = settings[key];
				} else {
					settings_w_prefix[key] = settings[key];
				}
			}
			saveAction.setParams({"afflMappings" : settings_w_prefix});
		} else {
			saveAction.setParams({"afflMappings" : settings});
		}
		saveAction.setCallback(this, function(response) {
			if(response.getState() === "ERROR") {
				this.displayError(response);
			}
		});
		$A.enqueueAction(saveAction);
	},

	displayError : function(response) {
		var errors = response.getError();
		if (errors && errors[0].pageErrors[0] && errors[0].pageErrors[0].message) {
			$A.error("Error message: " + errors[0].message);
		} else {
			$A.error("Unknown error");
		}
	}
})<commit_msg>Component expects and array of objects. Also, moving logic into separate method for easier testing.<commit_after>({
	init : function(component) {
		component.set("v.isView", true);
		this.loadAfflMappings(component);
	},

	loadAfflMappings : function(component) {
		var action = component.get("c.getAfflMappings");
		action.setCallback(this, function(response) {
			if(response.getState() === "SUCCESS") {
				var settings = response.getReturnValue();
				component.set("v.afflMappings", this.getProcessedSettings(settings));
	    	} else if(response.getState() === "ERROR") {
	    		this.displayError(response);
	    	}
		});
		$A.enqueueAction(action);
	},

	getProcessedSettings : function(settings) {
		if(namespacePrefix && namespacePrefix.length > 0) {
    		var settings_no_prefix = [];
    		//Remove package prefix from each custom field
    		for(var key in settings) { //Iterate over each row - key is just a numeric index here
    			settings_no_prefix[key] = {};
    			for(var key2 in settings[key]) { //Iterate over each field in each row - key2 is the actual field name
    				if(key2.endsWith('__c')) {
	    				var key2_no_prefix = key2.replace(namespacePrefix, '');
	    				settings_no_prefix[key].key2_no_prefix = settings[key][key2];
	    			} else {
	    				settings_no_prefix[key].key2 = settings[key][key2];
	    			}
    			}
    		}
    		return settings_no_prefix;
		} else {
			return settings;
		}
	},

	resetSettings : function(component) {
		this.loadAfflMappings(component);
	},

	saveAfflSettings : function(component) {
		var saveAction = component.get("c.saveAfflMappings");
		var settings = component.get("v.afflMappings");
		if(namespacePrefix && namespacePrefix.length > 0) {
			var settings_w_prefix = {};
			//Add package prefix to each custom field
			for(var key in settings) {
				if(key.endsWith('__c')) {
					var key_w_prefix = namespacePrefix + key;
	    			settings_w_prefix[key_w_prefix] = settings[key];
				} else {
					settings_w_prefix[key] = settings[key];
				}
			}
			saveAction.setParams({"afflMappings" : settings_w_prefix});
		} else {
			saveAction.setParams({"afflMappings" : settings});
		}
		saveAction.setCallback(this, function(response) {
			if(response.getState() === "ERROR") {
				this.displayError(response);
			}
		});
		$A.enqueueAction(saveAction);
	},

	displayError : function(response) {
		var errors = response.getError();
		if (errors && errors[0].pageErrors[0] && errors[0].pageErrors[0].message) {
			$A.error("Error message: " + errors[0].message);
		} else {
			$A.error("Unknown error");
		}
	}
})<|endoftext|>


<commit_before>const puppeteer = require('puppeteer');
const fileUrl = require('file-url');

const argv = require('yargs')
    .usage('Usage: $0 [input-html-file] [output-pdf-file]')
    .demandCommand(2)
    .boolean('background')
    .default('background', true)
    .default('marginTop', '6.35mm')
    .default('marginRight', '6.35mm')
    .default('marginBottom', '14.11mm')
    .default('marginLeft', '6.35mm')
    .default('format', 'Letter')
    .argv;

async function convert({ htmlPath, pdfPath }) {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto(fileUrl(htmlPath));

    await page.pdf({
        path: pdfPath,
        format: argv.format,
        printBackground: argv.background,
        margin: {
            top: argv.marginTop,
            right: argv.marginRight,
            bottom: argv.marginBottom,
            left: argv.marginLeft
        }
    });

    await browser.close();
}

(async () => {
    console.log(argv);

    try {
        await convert({
            htmlPath: argv._[0],
            pdfPath: argv._[1]
        });
    } catch (err) {
        console.error('Failed to generate pdf:', err);
        process.exit(1);
    }
})();<commit_msg>Switch to multiple-command architecture<commit_after>const puppeteer = require('puppeteer');
const fileUrl = require('file-url');

const argv = require('yargs')
    .command({
        command: 'print <input> <output>',
        desc: 'Print an html file to pdf',
        builder: {
            background: {
                default: true
            },
            marginTop: {
                default: '6.25mm'
            },
            marginRight: {
                default: '6.25mm'
            },
            marginBottom: {
                default: '14.11mm'
            },
            marginLeft: {
                default: '6.25mm'
            },
            format: {
                default: 'Letter'
            }
        },
        handler: async argv => {
            try {
                await print(argv);
            } catch (err) {
                console.error('Failed to generate pdf:', err);
                process.exit(1);
            }
        }
    })
    .demandCommand()
    .help()
    .argv;

async function print(argv) {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    const url = fileUrl(argv.input);

    console.log(`Loading ${url}`);
    await page.goto(fileUrl(argv.input));

    console.log(`Writing ${argv.output}`);
    await page.pdf({
        path: argv.output,
        format: argv.format,
        printBackground: argv.background,
        margin: {
            top: argv.marginTop,
            right: argv.marginRight,
            bottom: argv.marginBottom,
            left: argv.marginLeft
        }
    });

    console.log('Done');
    await browser.close();
}<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// List of sites we match against Topsites in order to identify sites
// that should be converted to search Topsites
const SEARCH_SHORTCUTS = [
  {keyword: "@amazon", shortURL: "amazon", url: "https://amazon.com", searchIdentifier: /^amazon/},
  {keyword: "@baidu", shortURL: "baidu", url: "https://baidu.com", searchIdentifier: /^baidu/},
  {keyword: "@google", shortURL: "google", url: "https://google.com", searchIdentifier: /^google/},
  {keyword: "@yandex", shortURL: "yandex", url: "https://yandex.com", searchIdentifier: /^yandex/}
];
this.SEARCH_SHORTCUTS = SEARCH_SHORTCUTS;

// These can be added via the editor but will not be added organically
this.CUSTOM_SEARCH_SHORTCUTS = [
  ...SEARCH_SHORTCUTS,
  {keyword: "@bing", shortURL: "bing", url: "https://bing.com", searchIdentifier: /^bing/},
  {keyword: "@duckduckgo", shortURL: "duckduckgo", url: "https://duckduckgo.com", searchIdentifier: /^ddg/},
  {keyword: "@ebay", shortURL: "ebay", url: "https://ebay.com", searchIdentifier: /^ebay/},
  {keyword: "@twitter", shortURL: "twitter", url: "https://twitter.com", searchIdentifier: /^twitter/},
  {keyword: "@wikipedia", shortURL: "wikipedia", url: "https://wikipedia.org", searchIdentifier: /^wikipedia/}
];

// Note: you must add the activity stream branch to the beginning of this if using outside activity stream
this.SEARCH_SHORTCUTS_EXPERIMENT = "improvesearch.topSiteSearchShortcuts";
this.SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF = "improvesearch.topSiteSearchShortcuts.searchEngines";
this.SEARCH_SHORTCUTS_HAVE_PINNED_PREF = "improvesearch.topSiteSearchShortcuts.havePinned";

function getSearchProvider(candidateShortURL) {
  return SEARCH_SHORTCUTS.filter(match => candidateShortURL === match.shortURL)[0] || null;
}
this.getSearchProvider = getSearchProvider;

const EXPORTED_SYMBOLS = ["getSearchProvider", "SEARCH_SHORTCUTS", "CUSTOM_SEARCH_SHORTCUTS", "SEARCH_SHORTCUTS_EXPERIMENT",
  "SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF", "SEARCH_SHORTCUTS_HAVE_PINNED_PREF"];
<commit_msg>chore(mc): Port Bug 1482565 - Support and show @Яндекс and @百度 searches for yandex and baidu r=mkaply (#4329)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// List of sites we match against Topsites in order to identify sites
// that should be converted to search Topsites
const SEARCH_SHORTCUTS = [
  {keyword: "@amazon", shortURL: "amazon", url: "https://amazon.com", searchIdentifier: /^amazon/},
  {keyword: "@\u767E\u5EA6", shortURL: "baidu", url: "https://baidu.com", searchIdentifier: /^baidu/},
  {keyword: "@google", shortURL: "google", url: "https://google.com", searchIdentifier: /^google/},
  {keyword: "@\u044F\u043D\u0434\u0435\u043A\u0441", shortURL: "yandex", url: "https://yandex.com", searchIdentifier: /^yandex/}
];
this.SEARCH_SHORTCUTS = SEARCH_SHORTCUTS;

// These can be added via the editor but will not be added organically
this.CUSTOM_SEARCH_SHORTCUTS = [
  ...SEARCH_SHORTCUTS,
  {keyword: "@bing", shortURL: "bing", url: "https://bing.com", searchIdentifier: /^bing/},
  {keyword: "@duckduckgo", shortURL: "duckduckgo", url: "https://duckduckgo.com", searchIdentifier: /^ddg/},
  {keyword: "@ebay", shortURL: "ebay", url: "https://ebay.com", searchIdentifier: /^ebay/},
  {keyword: "@twitter", shortURL: "twitter", url: "https://twitter.com", searchIdentifier: /^twitter/},
  {keyword: "@wikipedia", shortURL: "wikipedia", url: "https://wikipedia.org", searchIdentifier: /^wikipedia/}
];

// Note: you must add the activity stream branch to the beginning of this if using outside activity stream
this.SEARCH_SHORTCUTS_EXPERIMENT = "improvesearch.topSiteSearchShortcuts";
this.SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF = "improvesearch.topSiteSearchShortcuts.searchEngines";
this.SEARCH_SHORTCUTS_HAVE_PINNED_PREF = "improvesearch.topSiteSearchShortcuts.havePinned";

function getSearchProvider(candidateShortURL) {
  return SEARCH_SHORTCUTS.filter(match => candidateShortURL === match.shortURL)[0] || null;
}
this.getSearchProvider = getSearchProvider;

const EXPORTED_SYMBOLS = ["getSearchProvider", "SEARCH_SHORTCUTS", "CUSTOM_SEARCH_SHORTCUTS", "SEARCH_SHORTCUTS_EXPERIMENT",
  "SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF", "SEARCH_SHORTCUTS_HAVE_PINNED_PREF"];
<|endoftext|>


<commit_before>import Immutable from 'seamless-immutable';
import {
  DECK_DELETION_REQUEST,
  DECK_DELETION_REQUEST_FAILURE,
  DECK_DELETION_REQUEST_SUCCESS,
  REQUEST_DECK_LIST,
  REQUEST_DECK_LIST_FAILURE,
  REQUEST_DECK_LIST_SUCCESS,
} from 'actions/deckManagementActions';

const initialState = Immutable({
  listOfDecks: [],
  sentRequestForDecksList: false,
  receivedList: false,
  listErrorMessage: '',
  sentDeletionRequestFor: null,
  deletionErrorMessage: '',
});

function deckManagementReducer(state = initialState, action) {
  switch (action.type) {
    case REQUEST_DECK_LIST:
      return Immutable.merge(state, {
        sentRequestForDecksList: true,
        receivedList: false,
        sendDeletionRequest: false,
      });
    case REQUEST_DECK_LIST_SUCCESS:
      return Immutable.merge(state, {
        sentRequestForDeckList: false,
        receivedList: true,
        listErrorMessage: '',
        listOfDecks: state.listOfDecks.concat(action.payload.listOfDecks),
      });
    case REQUEST_DECK_LIST_FAILURE:
      return Immutable.merge(state, {
        sentRequestForDecksList: false,
        receivedList: false,
        listErrorMessage: action.payload,
      });
    case DECK_DELETION_REQUEST:
      return Immutable.merge(state, {
        sentDeletionRequestFor: action.payload,
      });
    case DECK_DELETION_REQUEST_FAILURE:
      return Immutable.merge(state, {
        sentDeletionRequestFor: null,
        deletionErrorMessage: action.payload,
      });
    case DECK_DELETION_REQUEST_SUCCESS:
      return Immutable.merge(state, {
        listOfDecks: state.listOfDecks
          .concat()
          .filter(deck => deck.deckId !== state.sentDeletionRequestFor),
        sentDeletionRequestFor: null,
        deletionErrorMessage: '',
      });
    default:
      return state;
  }
}

export default deckManagementReducer;
<commit_msg>fixed mistake (null instead of false)<commit_after>import Immutable from 'seamless-immutable';
import {
  DECK_DELETION_REQUEST,
  DECK_DELETION_REQUEST_FAILURE,
  DECK_DELETION_REQUEST_SUCCESS,
  REQUEST_DECK_LIST,
  REQUEST_DECK_LIST_FAILURE,
  REQUEST_DECK_LIST_SUCCESS,
} from 'actions/deckManagementActions';

const initialState = Immutable({
  listOfDecks: [],
  sentRequestForDecksList: false,
  receivedList: false,
  listErrorMessage: '',
  sentDeletionRequestFor: null,
  deletionErrorMessage: '',
});

function deckManagementReducer(state = initialState, action) {
  switch (action.type) {
    case REQUEST_DECK_LIST:
      return Immutable.merge(state, {
        sentRequestForDecksList: true,
        receivedList: false,
        sentDeletionRequestFor: null,
      });
    case REQUEST_DECK_LIST_SUCCESS:
      return Immutable.merge(state, {
        sentRequestForDecksList: false,
        receivedList: true,
        listErrorMessage: '',
        listOfDecks: state.listOfDecks.concat(action.payload.listOfDecks),
      });
    case REQUEST_DECK_LIST_FAILURE:
      return Immutable.merge(state, {
        sentRequestForDecksList: false,
        receivedList: false,
        listErrorMessage: action.payload,
      });
    case DECK_DELETION_REQUEST:
      return Immutable.merge(state, {
        sentDeletionRequestFor: action.payload,
      });
    case DECK_DELETION_REQUEST_FAILURE:
      return Immutable.merge(state, {
        sentDeletionRequestFor: null,
        deletionErrorMessage: action.payload,
      });
    case DECK_DELETION_REQUEST_SUCCESS:
      return Immutable.merge(state, {
        listOfDecks: state.listOfDecks
          .concat()
          .filter(deck => deck.deckId !== state.sentDeletionRequestFor),
        sentDeletionRequestFor: null,
        deletionErrorMessage: '',
      });
    default:
      return state;
  }
}

export default deckManagementReducer;
<|endoftext|>


<commit_before>/// Enumeration
var fieldType = {
    "text": 0,
    "number": 1,
    "bool": 2,
    "choice": 3};

/// Backbonize a metamodel, set default values for model
///
/// @return Constructor of Backbone model
function backbonizeModel(metamodel) {
    var defaults = {};

    _.each(metamodel.fields,
          function(f) {
              if (!(_.isUndefined(f.default)))
                  defaults[f.name] = f.default;
              else
                  defaults[f.name] = null;
          });

    var M = Backbone.Model.extend({
        "defaults": defaults,
        "urlRoot": "."
    });

    return M;
}

/// Build choice field list for use with Mustache templates
function processChoice(choice, value) {
    return _.map(choice,
                 function(c) {
                     return {"value": c,
                             "selected": c == value}});
}

/// Backbonize a metamodel, producing a view which renders associated
/// Backbone model to form
///
/// @param context Dictionary with keys `name` and `value`
///
/// @return Constructor of Backbone view
function backbonizeView(metamodel) {
    var V = Backbone.View.extend({
        "initialize": function() {
            this.render();
        },

        "render": function() {
            var j = this.model.toJSON();
            $(this.el).empty();

            /// @todo To dict
            var lt = $("#longtext-field-template").html();
            var tt = $("#text-field-template").html();
            var ct = $("#choice-field-template").html();

            var contents = "";

            /// Pick an appropriate form widget for each metamodel
            /// field type and render actual model value in it
            _.each(metamodel.fields,
                   function(f) {
                       var ctx = {field: f, value: j[f.name]};
                       switch (f.type) {
                       case fieldType.number:
                       case fieldType.text:
                           contents += Mustache.render(tt, ctx);
                           break
                       case fieldType.longText:
                           contents += Mustache.render(lt, ctx);
                           break
                       case fieldType.choice:
                           contents += Mustache.render(ct, {field: f, value: j[f.name],
                                                            choice: processChoice(f.choice, j[f.name])});
                           break
                       }
                   });

            /// @todo Unbreak my XML
            $(this.el).html(contents);
        }});

    return V;
}
<commit_msg>Inverse mapping from view to model.<commit_after>/// Enumeration
var fieldType = {
    "text": 0,
    "number": 1,
    "bool": 2,
    "choice": 3};

/// Backbonize a metamodel, set default values for model
///
/// @return Constructor of Backbone model
function backbonizeModel(metamodel) {
    var defaults = {};

    _.each(metamodel.fields,
          function(f) {
              if (!(_.isUndefined(f.default)))
                  defaults[f.name] = f.default;
              else
                  defaults[f.name] = null;
          });

    var M = Backbone.Model.extend({
        "defaults": defaults,
        "initialize": function() {
            this.bind("change",
                      function () {
                          if (!this.hasChanged("id"))
                              this.save();
                      },
                      this);
        },
        "urlRoot": "."

    });

    return M;
}

/// Build choice field list for use with Mustache templates
///
/// @todo Difference between values and option labels
function processChoice(choice, value) {
    return _.map(choice,
                 function(c) {
                     return {"value": c,
                             "selected": c == value}});
}

/// Backbonize a metamodel, producing a view which renders associated
/// Backbone model to form
///
/// @param context Dictionary with keys `name` and `value`
///
/// @return Constructor of Backbone view
function backbonizeView(metamodel) {
    var V = Backbone.View.extend({
        "initialize": function() {
            this.render();
        },

        /// Inverse mapping from view to model
        "fromView": function() {
            var newAttrs = {};
            _.each(metamodel.fields,
                   function (f) {
                       var v = this.$(".field." + f.name).val();
                       if (!!v )
                           newAttrs[f.name] = v;
                       else
                           newAttrs[f.name] = null;
                   });
            this.model.set(newAttrs);
        },

        "render": function() {
            var j = this.model.toJSON();
            /// @todo Update only parts which have changed
            $(this.el).empty();

            /// @todo To dict
            var lt = $("#longtext-field-template").html();
            var tt = $("#text-field-template").html();
            var ct = $("#choice-field-template").html();

            var contents = "";

            /// Pick an appropriate form widget for each metamodel
            /// field type and render actual model value in it
            _.each(metamodel.fields,
                   function(f) {
                       var ctx = {field: f, value: j[f.name]};
                       switch (f.type) {
                       case fieldType.number:
                       case fieldType.text:
                           contents += Mustache.render(tt, ctx);
                           break
                       case fieldType.longText:
                           contents += Mustache.render(lt, ctx);
                           break
                       case fieldType.choice:
                           contents += Mustache.render(ct, {field: f, value: j[f.name],
                                                            choice: processChoice(f.choice, j[f.name])});
                           break
                       }
                   });

            /// @todo Unbreak my XML
            $(this.el).html(contents);
        }});

    return V;
}
<|endoftext|>


<commit_before>const {
  indexRecords,
  destroyIndices
} = require('@cardstack/server/tests/support');
const Searcher = require('@cardstack/elasticsearch/searcher');
const SchemaCache = require('@cardstack/server/schema-cache');
const { uniq } = require('lodash');
const bootstrapSchema = require('@cardstack/server/bootstrap-schema');

describe('searcher', function() {

  let searcher;
  let fixtures = [
    {
      type: 'content-types',
      id: 'people',
      relationships: {
        fields: {
          data: [
            { type: 'fields', id: 'firstName' },
            { type: 'fields', id: 'lastName' },
            { type: 'fields', id: 'age' },
            { type: 'fields', id: 'color' }
          ]
        }
      }
    },
    {
      type: 'content-types',
      id: 'articles',
      relationships: {
        fields: {
          data: [
            { type: 'fields', id: 'title' },
            { type: 'fields', id: 'color' }
          ]
        }
      }
    },
    {
      type: 'fields',
      id: 'firstName',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'title',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'color',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'lastName',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'age',
      attributes: {
        'field-type': '@cardstack/core-types::integer'
      }
    },
    {
      type: 'articles',
      id: '1',
      attributes: {
        hello: 'magic words',
        color: 'red',
      }
    },
    {
      type: 'people',
      id: '1',
      attributes: {
        firstName: 'Quint',
        lastName: 'Faulkner',
        age: 6
      }
    },
    {
      type: 'people',
      id: '2',
      attributes: {
        firstName: 'Arthur',
        lastName: 'Faulkner',
        age: 1,
        color: 'red'
      }
    }
  ];

  before(async function() {
    searcher = new Searcher(new SchemaCache());
    let records = fixtures.slice();
    for (let i = 10; i < 30; i++) {
      records.push({
        type: 'comments',
        id: String(i),
        attributes: {
          body: `comment ${i}`
        }
      });
    }
    await indexRecords(records.concat(bootstrapSchema));
  });

  after(async function() {
    await destroyIndices();
  });

  it('can be searched for all content', async function() {
    let { models } = await searcher.search('master', {
      page: { size: 1000 }
    });
    expect(models).to.have.length(fixtures.length + bootstrapSchema.length + 20);
  });

  it('can be searched via queryString', async function() {
    let { models } = await searcher.search('master', {
      queryString: 'magic'
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.hello', 'magic words');
  });

  it('can be searched via queryString, negative result', async function() {
    let { models } = await searcher.search('master', {
      queryString: 'this-is-an-unused-term'
    });
    expect(models).to.have.length(0);
  });

  it('can filter by type', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'articles'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.hello', 'magic words');
  });

  it('can filter by id', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        id: '1'
      }
    });
    expect(models).to.have.length(2);
    expect(models).includes.something.with.property('type', 'articles');
    expect(models).includes.something.with.property('type', 'people');
  });

  it('can filter a field by one term', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        firstName: 'Quint'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint');
  });

  it('can filter a field by multiple terms', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        firstName: ['Quint', 'Arthur']
      }
    });
    expect(models).to.have.length(2);
  });

  it('can use OR expressions in filters', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        or: [
          { firstName: ['Quint'], type: 'people' },
          { type: 'articles', id: '1' }
        ]
      }
    });
    expect(models).to.have.length(2);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint');
    expect(models).includes.something.with.deep.property('type', 'articles');
  });

  it('can use AND expressions in filters', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        and: [
          { color: 'red' },
          { type: 'people' }
        ]
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by range', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        age: {
          range: {
            lt: '2'
          }
        }
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field existence (string)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: 'true'
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field nonexistence (string)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: 'false'
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint' );
  });

  it('can filter by field existence (bool)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: true
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field nonexistence (bool)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: false
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint' );
  });

  it('gives helpful error when filtering unknown field', async function() {
    try {
      await searcher.search('master', {
        filter: {
          flavor: 'chocolate'
        }
      });
      throw new Error("should not get here");
    } catch (err) {
      if (!err.status) {
        throw err;
      }
      expect(err.status).equals(400);
      expect(err.detail).equals('Cannot filter by unknown field "flavor"');
    }
  });

  it('can sort', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: 'age'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Arthur', 'Quint']);
  });

  it('can sort reverse', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: '-age'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Quint', 'Arthur']);
  });

  it('can sort via field-specific mappings', async function() {
    // string fields are only sortable because of the sortFieldName
    // in @cardstack/core-field-types/string. So this is a test that
    // we're using that capability.
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: 'firstName'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Arthur', 'Quint']);
  });

  it('can sort reverse via field-specific mappings', async function() {
    // string fields are only sortable because of the sortFieldName
    // in @cardstack/core-field-types/string. So this is a test that
    // we're using that capability.
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: '-firstName'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Quint', 'Arthur']);
  });

  it('has helpful error when sorting by nonexistent field', async function() {
    try {
      await searcher.search('master', {
        sort: 'something-that-does-not-exist'
      });
      throw new Error("should not get here");
    } catch (err) {
      if (!err.status) {
        throw err;
      }
      expect(err.status).equals(400);
      expect(err.detail).equals('Cannot sort by unknown field "something-that-does-not-exist"');
    }
  });

  it('can paginate', async function() {
    let response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7
      }
    });
    expect(response.models).length(7);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    let allModels = response.models;

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(7);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    allModels = allModels.concat(response.models);

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(6);
    expect(response.page).has.property('total', 20);
    expect(response.page).not.has.property('cursor');

    allModels = allModels.concat(response.models);

    expect(uniq(allModels.map(m => m.id))).length(20);
  });

  it('can paginate when results exactly fill final page', async function() {
    let response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 10
      }
    });
    expect(response.models).length(10);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    let allModels = response.models;

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 10,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(10);
    expect(response.page).has.property('total', 20);
    expect(response.page).not.has.property('cursor');

    allModels = allModels.concat(response.models);
    expect(uniq(allModels.map(m => m.id))).length(20);
  });

  it('can get an individual record', async function() {
    let model = await searcher.get('master', 'articles', '1');
    expect(model).has.deep.property('attributes.hello', 'magic words');
  });

});
<commit_msg>this coupling is unnecessary<commit_after>const {
  indexRecords,
  destroyIndices
} = require('@cardstack/server/tests/support');
const Searcher = require('@cardstack/elasticsearch/searcher');
const SchemaCache = require('@cardstack/server/schema-cache');
const { uniq } = require('lodash');

describe('searcher', function() {

  let searcher;
  let fixtures = [
    {
      type: 'content-types',
      id: 'people',
      relationships: {
        fields: {
          data: [
            { type: 'fields', id: 'firstName' },
            { type: 'fields', id: 'lastName' },
            { type: 'fields', id: 'age' },
            { type: 'fields', id: 'color' }
          ]
        }
      }
    },
    {
      type: 'content-types',
      id: 'articles',
      relationships: {
        fields: {
          data: [
            { type: 'fields', id: 'title' },
            { type: 'fields', id: 'color' }
          ]
        }
      }
    },
    {
      type: 'fields',
      id: 'firstName',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'title',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'color',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'lastName',
      attributes: {
        'field-type': '@cardstack/core-types::string'
      }
    },
    {
      type: 'fields',
      id: 'age',
      attributes: {
        'field-type': '@cardstack/core-types::integer'
      }
    },
    {
      type: 'articles',
      id: '1',
      attributes: {
        hello: 'magic words',
        color: 'red',
      }
    },
    {
      type: 'people',
      id: '1',
      attributes: {
        firstName: 'Quint',
        lastName: 'Faulkner',
        age: 6
      }
    },
    {
      type: 'people',
      id: '2',
      attributes: {
        firstName: 'Arthur',
        lastName: 'Faulkner',
        age: 1,
        color: 'red'
      }
    }
  ];

  before(async function() {
    searcher = new Searcher(new SchemaCache());
    let records = fixtures.slice();
    for (let i = 10; i < 30; i++) {
      records.push({
        type: 'comments',
        id: String(i),
        attributes: {
          body: `comment ${i}`
        }
      });
    }
    await indexRecords(records);
  });

  after(async function() {
    await destroyIndices();
  });

  it('can be searched for all content', async function() {
    let { models } = await searcher.search('master', {
      page: { size: 1000 }
    });
    expect(models).to.have.length(fixtures.length + 20);
  });

  it('can be searched via queryString', async function() {
    let { models } = await searcher.search('master', {
      queryString: 'magic'
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.hello', 'magic words');
  });

  it('can be searched via queryString, negative result', async function() {
    let { models } = await searcher.search('master', {
      queryString: 'this-is-an-unused-term'
    });
    expect(models).to.have.length(0);
  });

  it('can filter by type', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'articles'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.hello', 'magic words');
  });

  it('can filter by id', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        id: '1'
      }
    });
    expect(models).to.have.length(2);
    expect(models).includes.something.with.property('type', 'articles');
    expect(models).includes.something.with.property('type', 'people');
  });

  it('can filter a field by one term', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        firstName: 'Quint'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint');
  });

  it('can filter a field by multiple terms', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        firstName: ['Quint', 'Arthur']
      }
    });
    expect(models).to.have.length(2);
  });

  it('can use OR expressions in filters', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        or: [
          { firstName: ['Quint'], type: 'people' },
          { type: 'articles', id: '1' }
        ]
      }
    });
    expect(models).to.have.length(2);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint');
    expect(models).includes.something.with.deep.property('type', 'articles');
  });

  it('can use AND expressions in filters', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        and: [
          { color: 'red' },
          { type: 'people' }
        ]
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by range', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        age: {
          range: {
            lt: '2'
          }
        }
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field existence (string)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: 'true'
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field nonexistence (string)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: 'false'
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint' );
  });

  it('can filter by field existence (bool)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: true
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Arthur');
  });

  it('can filter by field nonexistence (bool)', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        color: {
          exists: false
        },
        type: 'people'
      }
    });
    expect(models).to.have.length(1);
    expect(models).includes.something.with.deep.property('attributes.firstName', 'Quint' );
  });

  it('gives helpful error when filtering unknown field', async function() {
    try {
      await searcher.search('master', {
        filter: {
          flavor: 'chocolate'
        }
      });
      throw new Error("should not get here");
    } catch (err) {
      if (!err.status) {
        throw err;
      }
      expect(err.status).equals(400);
      expect(err.detail).equals('Cannot filter by unknown field "flavor"');
    }
  });

  it('can sort', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: 'age'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Arthur', 'Quint']);
  });

  it('can sort reverse', async function() {
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: '-age'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Quint', 'Arthur']);
  });

  it('can sort via field-specific mappings', async function() {
    // string fields are only sortable because of the sortFieldName
    // in @cardstack/core-field-types/string. So this is a test that
    // we're using that capability.
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: 'firstName'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Arthur', 'Quint']);
  });

  it('can sort reverse via field-specific mappings', async function() {
    // string fields are only sortable because of the sortFieldName
    // in @cardstack/core-field-types/string. So this is a test that
    // we're using that capability.
    let { models } = await searcher.search('master', {
      filter: {
        type: 'people'
      },
      sort: '-firstName'
    });
    expect(models.map(r => r.attributes.firstName)).to.deep.equal(['Quint', 'Arthur']);
  });

  it('has helpful error when sorting by nonexistent field', async function() {
    try {
      await searcher.search('master', {
        sort: 'something-that-does-not-exist'
      });
      throw new Error("should not get here");
    } catch (err) {
      if (!err.status) {
        throw err;
      }
      expect(err.status).equals(400);
      expect(err.detail).equals('Cannot sort by unknown field "something-that-does-not-exist"');
    }
  });

  it('can paginate', async function() {
    let response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7
      }
    });
    expect(response.models).length(7);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    let allModels = response.models;

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(7);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    allModels = allModels.concat(response.models);

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 7,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(6);
    expect(response.page).has.property('total', 20);
    expect(response.page).not.has.property('cursor');

    allModels = allModels.concat(response.models);

    expect(uniq(allModels.map(m => m.id))).length(20);
  });

  it('can paginate when results exactly fill final page', async function() {
    let response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 10
      }
    });
    expect(response.models).length(10);
    expect(response.page).has.property('total', 20);
    expect(response.page).has.property('cursor');

    let allModels = response.models;

    response = await searcher.search('master', {
      filter: { type: 'comments' },
      page: {
        size: 10,
        cursor: response.page.cursor
      }
    });

    expect(response.models).length(10);
    expect(response.page).has.property('total', 20);
    expect(response.page).not.has.property('cursor');

    allModels = allModels.concat(response.models);
    expect(uniq(allModels.map(m => m.id))).length(20);
  });

  it('can get an individual record', async function() {
    let model = await searcher.get('master', 'articles', '1');
    expect(model).has.deep.property('attributes.hello', 'magic words');
  });

});
<|endoftext|>


<commit_before>/**
 * Copyright 2014 Nicholas Humfrey
 * Copyright 2013 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

module.exports = function(RED) {
    "use strict";
    var owfs = require("owfs");
    var util = require("util");

    // The OWFS Server Definition - this opens (and closes) the connection
    function OWFSServerNode(n) {
        RED.nodes.createNode(this,n);
        this.server = n.server;
        this.port = n.port;
    }
    RED.nodes.registerType("owfs-server", OWFSServerNode);

    function OwfsNode(n) {
        RED.nodes.createNode(this,n);
        this.server = n.server;
        this.serverConfig = RED.nodes.getNode(this.server);
        var node = this;
        if (this.serverConfig) {
            this.client = new owfs.Client(this.serverConfig.server, this.serverConfig.port);
            this.on("input", function(msg) {
                if (msg.topic) {
                    this.client.read(msg.topic, function(error, result) {
                        if (!error) {
                            msg.payload = parseFloat(result);
                            node.send(msg);
                        } else {
                            this.error(error);
                        }
                    });
                } else {
                    this.error("missing topic in message to owfs");
                }
            });
        } else {
            this.error("missing server configuration for owfs");
        }
    }
    RED.nodes.registerType("owfs",OwfsNode);
}
<commit_msg>Fix for reporting errors that was crashing Node-RED<commit_after>/**
 * Copyright 2014 Nicholas Humfrey
 * Copyright 2013 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 **/

module.exports = function(RED) {
    "use strict";
    var owfs = require("owfs");
    var util = require("util");

    // The OWFS Server Definition - this opens (and closes) the connection
    function OWFSServerNode(n) {
        RED.nodes.createNode(this,n);
        this.server = n.server;
        this.port = n.port;
    }
    RED.nodes.registerType("owfs-server", OWFSServerNode);

    function OwfsNode(n) {
        RED.nodes.createNode(this,n);
        this.server = n.server;
        this.serverConfig = RED.nodes.getNode(this.server);
        var node = this;
        if (this.serverConfig) {
            this.client = new owfs.Client(this.serverConfig.server, this.serverConfig.port);
            this.on("input", function(msg) {
                if (msg.topic) {
                    this.client.read(msg.topic, function(error, result) {
                        if (!error) {
                            msg.payload = parseFloat(result);
                            node.send(msg);
                        } else {
                            node.error(error);
                        }
                    });
                } else {
                    node.error("missing topic in message to owfs");
                }
            });
        } else {
            node.error("missing server configuration for owfs");
        }
    }
    RED.nodes.registerType("owfs",OwfsNode);
}
<|endoftext|>


<commit_before>class WWLTWModal {
    constructor() {
        let modalContainer = `

What did you learn while working on this story?

                        <form id="wwltw-form" class="ui form">

                                Body (markdown supported)
                                <textarea name="learning-body" id="learning-body"></textarea>

                                Tags
                                <select name="tags" multiple="" id="learning-tags" class="ui fluid search dropdown">
                                    <option value="">e.g. java, rails, tdd</option>
                                    ${this.populateTags()}
                                </select>

                                <input type="submit" value="Add to WWLTW story" class="ui submit button">

                        </form>

`;

        this.addElementToBody(modalContainer);
    }

    populateTags() {
        return tagList.map((tag) => {
            return `<option value="${tag}">${tag}</option>`;
        }).join('');
    }

    initialize() {
        $('.ui.fluid.dropdown').dropdown();

        this.wwltwForm = document.querySelector("#wwltw-form");
        this.learningBody = document.querySelector("#learning-body");
        this.$modal = $('.ui.modal').modal();
        return this;
    }

    clearForm() {
        this.learningBody.value = "";
        $('.ui.fluid.dropdown').dropdown('restore defaults');
    };

    bindFormSubmission(wwltwRepository) {
        this.wwltwForm.addEventListener('submit', function (e) {
            e.preventDefault();

            wwltwRepository.add(this.getProjectId(), this.learningBody.value, this.extractSubmittedTags());

            this.clearForm(learningBody);
            this.$modal.modal('hide');
        });
    }

    getProjectId() {
        return /projects\/(\d*)/.exec(window.location)[1];
    };

    extractSubmittedTags() {
        const learningTags = this.wwltwForm.querySelector("#learning-tags");
        const tags = $(learningTags).find('option:selected')
            .map(function (i, option) {
                return option.value;
            });

        return Array.prototype.join.call(tags, ', ');
    };

    addElementToBody(innerHtml) {
        let modalContainer = document.createElement("div");
        modalContainer.innerHTML = innerHtml;
        document.querySelector('body').appendChild(modalContainer);
    }
}<commit_msg>Clear the WWLTW form when the modal is closed/cancelled<commit_after>class WWLTWModal {
    constructor() {
        let modalContainer = `

What did you learn while working on this story?

                        <form id="wwltw-form" class="ui form">

                                Body (markdown supported)
                                <textarea name="learning-body" id="learning-body"></textarea>

                                Tags
                                <select name="tags" multiple="" id="learning-tags" class="ui fluid search dropdown">
                                    <option value="">e.g. java, rails, tdd</option>
                                    ${this.populateTags()}
                                </select>

                                <input type="submit" value="Add to WWLTW story" class="ui submit button">

                        </form>

`;

        this.addElementToBody(modalContainer);
    }

    populateTags() {
        return tagList.map((tag) => {
            return `<option value="${tag}">${tag}</option>`;
        }).join('');
    }

    initialize() {
        $('.ui.fluid.dropdown').dropdown();

        this.wwltwForm = document.querySelector("#wwltw-form");
        this.learningBody = document.querySelector("#learning-body");
        this.$modal = $('.ui.modal').modal({
            onHide: () => {
                this.clearForm()
            }
        });
        return this;
    }

    clearForm() {
        this.learningBody.value = "";
        $('.ui.fluid.dropdown').dropdown('restore defaults');
    };

    bindFormSubmission(wwltwRepository) {
        this.wwltwForm.addEventListener('submit', function (e) {
            e.preventDefault();

            wwltwRepository.add(this.getProjectId(), this.learningBody.value, this.extractSubmittedTags());

            this.clearForm(learningBody);
            this.$modal.modal('hide');
        });
    }

    getProjectId() {
        return /projects\/(\d*)/.exec(window.location)[1];
    };

    extractSubmittedTags() {
        const learningTags = this.wwltwForm.querySelector("#learning-tags");
        const tags = $(learningTags).find('option:selected')
            .map(function (i, option) {
                return option.value;
            });

        return Array.prototype.join.call(tags, ', ');
    };

    addElementToBody(innerHtml) {
        let modalContainer = document.createElement("div");
        modalContainer.innerHTML = innerHtml;
        document.querySelector('body').appendChild(modalContainer);
    }
}<|endoftext|>