<commit_before>yox.themes.wall = function(data, options){
    var elements = {},
        containerWidth,
        self = this,
        isLoading, // Flag indicating whether new contents are currently being fetched
        loadedAllItems = false; // Flag indicating whether all the items have been loaded (all the possible items, after loading all pages)

    this.name = "wall";

    var loadingClass = this.getThemeClass("loading"),
        thumbs = [],
        currentRowWidth = 0;

    this.config = {
        thumbnails: {
            createThumbnail: function(itemIndex, item, totalItems){
                var thumbnail = document.createElement("a"),
                    thumbnailImg = document.createElement("img");

                var dimensions = { height: options.thumbnailsMaxHeight, width: Math.round(options.thumbnailsMaxHeight / item.ratio) };
                thumbnail.dimensions = dimensions;

                thumbnailImg.addEventListener("load", onImageLoad, false);

                thumbnailImg.src = item.thumbnail.src;
                thumbnail.appendChild(thumbnailImg);
                thumbnail.setAttribute("href", item.url);
                thumbnail.style.display = "none";

                calculateDimensions(thumbnail, itemIndex, totalItems);
                return thumbnail;
            },
            events: {
                beforeSelect: function(e){
                    if (options.scrollToElementOnSelect && e.newItem){
                        yox.utils.dom.scrollIntoView(e.newItem.thumbnail.element, self.container, options.scrollAnimationDuration, options.scrollOffset);
                    }
                },
                create: onScroll
            }
        }
    };

    // This function does the resizing that creates the wall effect:
    function calculateDimensions(thumbnail, index, totalThumbnailsCount, isUpdate){
        currentRowWidth += thumbnail.dimensions.width;
        thumbs.push(thumbnail);

        var isLastThumbnail = index === totalThumbnailsCount - 1,
            totalBordersWidth = (thumbs.length - 1) * options.borderWidth,
            isFullRow = currentRowWidth + totalBordersWidth >= containerWidth;

        // Gathered enough thumbnails to fill the current row:
        if (isFullRow || isLastThumbnail){
            var rowAspectRatio = (containerWidth - totalBordersWidth) / currentRowWidth,
                rowHeight = Math.round(thumbs[0].dimensions.height * rowAspectRatio),
                setWidth = true,
                showThumbnail = isFullRow || loadedAllItems,
                finalRowWidth = totalBordersWidth;

            if (rowHeight > options.thumbnailsMaxHeight){
                rowHeight = options.thumbnailsMaxHeight;
                setWidth = false;
            }

            for(var i=0, thumb; thumb = thumbs[i]; i++){
                var width = Math.floor(thumb.dimensions.width * rowAspectRatio);
                finalRowWidth += width;

                thumb.style.height = rowHeight + "px";
                if (setWidth)
                    thumb.style.width = width + "px";
                else if (isLastThumbnail)
                    thumb.style.width = thumb.dimensions.width + "px";

                if (showThumbnail)
                    thumb.style.removeProperty("display");
            }

            // Due to the rounding in image widths, a small fix is required to arrange the thumbnails pixel-perfectly:
            for(var thumbIndex = thumbs.length; thumbIndex-- && finalRowWidth < containerWidth; finalRowWidth++){
                thumb = thumbs[thumbIndex];
                thumb.style.width = (parseInt(thumb.style.width, 10) + 1) + "px";
            }

            // Finally, the last thumbnail in the row's right margin is removed and the row is closed:
            if (!isLastThumbnail || isFullRow){
                thumbnail.style.marginRight = "0";
                thumbs = [];
                currentRowWidth = 0;
            }
        }
        else if (isUpdate)
            thumbnail.style.removeProperty("margin-right");

    }

    var thumbnailsResizeTimeoutId;
    function updateThumbnails(){
        var thumbnails = self.modules.thumbnails.thumbnails;
        if (!thumbnails)
            return;

        var thumbnailsCount = thumbnails.length;

        for(var i=0, thumbnail; thumbnail = thumbnails[i]; i++){
            calculateDimensions(thumbnail, i, thumbnailsCount, true);
        }
    }

    var dataSource,
        totalItems;

    setDataSource(data.getData());

    // Used for infinite scrolling to get the next batch of items.
    // TODO: Try to make this part of the data module itself, so other themes may benefit.
    function loadMoreItems(){
        if (!dataSource)
            return false;

        dataSource.offset = data.countItems() + 1;
        data.addSources([ dataSource ]);
    }

    function setDataSource(loadedDataSources){
        if (loadedDataSources.length){
            var loadedDataSource = loadedDataSources[0];
            if (!dataSource){
                dataSource = loadedDataSource.source;
                totalItems = loadedDataSource.totalItems;
                dataSource.type = loadedDataSource.sourceType;
            }

            if (data.countItems() >= totalItems){
                self.triggerEvent("loadedAllItems");
            }
        }
        isLoading = false;
        $(self.container).removeClass(loadingClass);
    }

    function onImageLoad(e){
        this.style.visibility = "visible";
        this.style.setProperty(yox.utils.browser.getCssPrefix() + "transform", "scale(1)");
        this.removeEventListener("load", onImageLoad, false);
    }

    function loadItems(){
        isLoading = true;
        $(self.container).addClass(loadingClass);
        loadMoreItems();
    }

    // Used for infinite scrolling:
    function onScroll(){
        // When reaching the scroll limit, check for new contents:
        if (!isLoading && elements.scrollElementForMeasure.scrollTop >= elements.scrollElementForMeasure.scrollHeight - elements.scrollElementForMeasure.clientHeight - options.thumbnailsMaxHeight){
            loadItems();
        }
    }

    data.addEventListener("loadSources", setDataSource);
    data.addEventListener("clear", function(){
        dataSource = null;
        thumbs = [];
        currentRowWidth = 0;
        if (loadedAllItems){
            loadedAllItems = false;
            elements.scrollElement.addEventListener("scroll", onScroll, false);
            data.addEventListener("loadSources", setDataSource);
            $(self.container).removeClass(self.getThemeClass("loadedAll"));
        }
    });

    this.create = function(container){
        this.container = container;
        var containerClass = this.getThemeClass();

        function getContainerWidth(){
            containerWidth = container.clientWidth - options.padding * 2;
        }

        $(container).addClass(containerClass).addClass(loadingClass);
        elements.wall = document.createElement("div");
        elements.wall.className = this.getThemeClass("thumbnails") + " yoxthumbnails";
        elements.wall.style.padding = options.padding + "px";
        container.appendChild(elements.wall);
        getContainerWidth();

        var styleEl = document.createElement("style"),
            thumbnailStyle = [
                "margin-right: " + options.borderWidth + "px",
                "margin-bottom: " + options.borderWidth + "px"
            ];

        styleEl.innerHTML = " ." + containerClass + " a[data-yoxthumbindex]{ " + thumbnailStyle.join("; ") + " }";
        document.getElementsByTagName("head")[0].appendChild(styleEl);

        $(window).on("resize", function(e){
            clearTimeout(thumbnailsResizeTimeoutId);
            thumbnailsResizeTimeoutId = setTimeout(function(){
                getContainerWidth();
                thumbs = [];
                currentRowWidth = 0;
                updateThumbnails();
            }, 50);
        });

        elements.scrollElement = container === document.body ? document : container;
        elements.scrollElementForMeasure = container;

        // All non-webkit browsers measure scrollTop for the body element in the HTML element rather than the document (Firefox 13, IE9, Opera 11.62):
        if (!$.browser.webkit && container === document.body)
            elements.scrollElementForMeasure = document.documentElement;

        elements.loader = document.createElement("div");
        elements.loader.className = this.getThemeClass("loader");
        elements.loader.style.paddingBottom = (options.borderWidth + options.padding) + "px";

        container.appendChild(elements.loader);

        elements.scrollElement.addEventListener("scroll", onScroll, false);

        self.addEventListener("loadedAllItems", function(){
            elements.scrollElement.removeEventListener("scroll", onScroll, false);
            data.removeEventListener("loadSources", setDataSource);
            loadedAllItems = true;
            $(container).addClass(self.getThemeClass("loadedAll"));
        });
    };
}

yox.themes.wall.defaults = {
    borderWidth: 7, // The size, in pixels, of the space between thumbnails
    loadItemsOnScroll: false, // Whether to get more results from the data source when scrolling down
    padding: 10, // The padding arround the thumbnails (padding for the element that contains all the thumbnails)
    scrollAnimationDuration: 500, // The time, in milliseconds, for the scroll animation, when a thumbnail is brought into view.
    scrollOffset: 60, // When scrolling a thumbnail into view, this number of pixels will be added to the scroll distance, so the thumbnail isn't at the very limit of the visible area.
    scrollToElementOnSelect: false, // If set to true, the theme's container will be scrolled to the selected thumbnail when its item is selected
    thumbnailsMaxHeight: 200 // The maximum height allowed for each thumbnail
};

yox.themes.wall.prototype = new yox.theme();<commit_msg>All thumbnails don't resize simultaneously, even if already cached - they load one at a time (or a maximum of three at a time), which looks better and improves performance on load.<commit_after>yox.themes.wall = function(data, options){
    var elements = {},
        containerWidth,
        self = this,
        isLoading, // Flag indicating whether new contents are currently being fetched
        loadedAllItems = false, // Flag indicating whether all the items have been loaded (all the possible items, after loading all pages)
        enlargeThumbnailQueue = [],
        enlargeThumbnailTimeoutId,
        enlargingThumbnails = 0,
        enlargeThumbnailsTimer = 100,
        concurrentEnlargingThumbnails = 3;

    this.name = "wall";

    var loadingClass = this.getThemeClass("loading"),
        thumbs = [],
        currentRowWidth = 0,
        throttledScrollIntoView = yox.utils.performance.throttle(function(element){
            yox.utils.dom.scrollIntoView(element, self.container, options.scrollAnimationDuration, options.scrollOffset);
        }, 300);

    this.config = {
        thumbnails: {
            createThumbnail: function(itemIndex, item, totalItems){
                var thumbnail = document.createElement("a"),
                    thumbnailImg = document.createElement("img");

                var dimensions = { height: options.thumbnailsMaxHeight, width: Math.round(options.thumbnailsMaxHeight / item.ratio) };
                thumbnail.dimensions = dimensions;

                thumbnailImg.addEventListener("load", onImageLoad, false);

                thumbnailImg.src = item.thumbnail.src;
                thumbnail.appendChild(thumbnailImg);
                thumbnail.setAttribute("href", item.url);
                thumbnail.style.display = "none";

                calculateDimensions(thumbnail, itemIndex, totalItems);
                return thumbnail;
            },
            events: {
                beforeSelect: function(e){
                    if (options.scrollToElementOnSelect && e.newItem){
                        throttledScrollIntoView(e.newItem.thumbnail.element);
                    }
                },
                create: onScroll
            }
        }
    };

    // This function does the resizing that creates the wall effect:
    function calculateDimensions(thumbnail, index, totalThumbnailsCount, isUpdate){
        currentRowWidth += thumbnail.dimensions.width;
        thumbs.push(thumbnail);

        var isLastThumbnail = index === totalThumbnailsCount - 1,
            totalBordersWidth = (thumbs.length - 1) * options.borderWidth,
            isFullRow = currentRowWidth + totalBordersWidth >= containerWidth;

        // Gathered enough thumbnails to fill the current row:
        if (isFullRow || isLastThumbnail){
            var rowAspectRatio = (containerWidth - totalBordersWidth) / currentRowWidth,
                rowHeight = Math.round(thumbs[0].dimensions.height * rowAspectRatio),
                setWidth = true,
                showThumbnail = isFullRow || loadedAllItems,
                finalRowWidth = totalBordersWidth;

            if (rowHeight > options.thumbnailsMaxHeight){
                rowHeight = options.thumbnailsMaxHeight;
                setWidth = false;
            }

            for(var i=0, thumb; thumb = thumbs[i]; i++){
                var width = Math.floor(thumb.dimensions.width * rowAspectRatio);
                finalRowWidth += width;

                thumb.style.height = rowHeight + "px";
                if (setWidth)
                    thumb.style.width = width + "px";
                else if (isLastThumbnail)
                    thumb.style.width = thumb.dimensions.width + "px";

                if (showThumbnail)
                    thumb.style.removeProperty("display");
            }

            // Due to the rounding in image widths, a small fix is required to arrange the thumbnails pixel-perfectly:
            for(var thumbIndex = thumbs.length; thumbIndex-- && finalRowWidth < containerWidth; finalRowWidth++){
                thumb = thumbs[thumbIndex];
                thumb.style.width = (parseInt(thumb.style.width, 10) + 1) + "px";
            }

            // Finally, the last thumbnail in the row's right margin is removed and the row is closed:
            if (!isLastThumbnail || isFullRow){
                thumbnail.style.marginRight = "0";
                thumbs = [];
                currentRowWidth = 0;
            }
        }
        else if (isUpdate)
            thumbnail.style.removeProperty("margin-right");

    }

    function updateThumbnails(){
        var thumbnails = self.modules.thumbnails.thumbnails;
        if (!thumbnails)
            return;

        var thumbnailsCount = thumbnails.length;

        for(var i=0, thumbnail; thumbnail = thumbnails[i]; i++){
            calculateDimensions(thumbnail, i, thumbnailsCount, true);
        }
    }

    var dataSource,
        totalItems;

    setDataSource(data.getData());

    // Used for infinite scrolling to get the next batch of items.
    // TODO: Try to make this part of the data module itself, so other themes may benefit.
    function loadMoreItems(){
        if (!dataSource)
            return false;

        dataSource.offset = data.countItems() + 1;
        data.addSources([ dataSource ]);
    }

    function setDataSource(loadedDataSources){
        if (loadedDataSources.length){
            var loadedDataSource = loadedDataSources[0];
            if (!dataSource){
                dataSource = loadedDataSource.source;
                totalItems = loadedDataSource.totalItems;
                dataSource.type = loadedDataSource.sourceType;
            }

            if (data.countItems() >= totalItems){
                self.triggerEvent("loadedAllItems");
            }
        }
        isLoading = false;
        $(self.container).removeClass(loadingClass);
    }

    function onImageLoad(e){
        if (enlargingThumbnails < concurrentEnlargingThumbnails){
            enlargingThumbnails++;
            this.style.visibility = "visible";
            this.style.setProperty(yox.utils.browser.getCssPrefix() + "transform", "scale(1)");
            this.removeEventListener("load", onImageLoad, false);
            setTimeout(function(){
                enlargingThumbnails--;
                if (enlargeThumbnailQueue.length){
                    onImageLoad.call(enlargeThumbnailQueue.shift());
                }
            }, enlargeThumbnailsTimer);
        }
        else{
            enlargeThumbnailQueue.push(this);
        }
    }

    function loadItems(){
        isLoading = true;
        $(self.container).addClass(loadingClass);
        loadMoreItems();
    }

    // Used for infinite scrolling:
    function onScroll(){
        // When reaching the scroll limit, check for new contents:
        if (!isLoading && elements.scrollElementForMeasure.scrollTop >= elements.scrollElementForMeasure.scrollHeight - elements.scrollElementForMeasure.clientHeight - options.thumbnailsMaxHeight){
            loadItems();
        }
    }

    data.addEventListener("loadSources", setDataSource);
    data.addEventListener("clear", function(){
        dataSource = null;
        thumbs = [];
        currentRowWidth = 0;
        if (loadedAllItems){
            loadedAllItems = false;
            elements.scrollElement.addEventListener("scroll", onScroll, false);
            data.addEventListener("loadSources", setDataSource);
            $(self.container).removeClass(self.getThemeClass("loadedAll"));
        }
    });

    this.create = function(container){
        this.container = container;
        var containerClass = this.getThemeClass();

        function getContainerWidth(){
            containerWidth = container.clientWidth - options.padding * 2;
        }

        $(container).addClass(containerClass).addClass(loadingClass);
        elements.wall = document.createElement("div");
        elements.wall.className = this.getThemeClass("thumbnails") + " yoxthumbnails";
        elements.wall.style.padding = options.padding + "px";
        container.appendChild(elements.wall);
        getContainerWidth();

        var styleEl = document.createElement("style"),
            thumbnailStyle = [
                "margin-right: " + options.borderWidth + "px",
                "margin-bottom: " + options.borderWidth + "px"
            ];

        styleEl.innerHTML = " ." + containerClass + " a[data-yoxthumbindex]{ " + thumbnailStyle.join("; ") + " }";
        document.getElementsByTagName("head")[0].appendChild(styleEl);

        $(window).on("resize", yox.utils.performance.throttle(function(){
            $(container).addClass(self.getThemeClass("resizing"));
            getContainerWidth();
            thumbs = [];
            currentRowWidth = 0;
            updateThumbnails();
            setTimeout(function(){
                $(self.container).removeClass(self.getThemeClass("resizing"));
            }, 5);
        }, 50));

        elements.scrollElement = container === document.body ? document : container;
        elements.scrollElementForMeasure = container;

        // All non-webkit browsers measure scrollTop for the body element in the HTML element rather than the document (Firefox 13, IE9, Opera 11.62):
        if (!$.browser.webkit && container === document.body)
            elements.scrollElementForMeasure = document.documentElement;

        elements.loader = document.createElement("div");
        elements.loader.className = this.getThemeClass("loader");
        elements.loader.style.paddingBottom = (options.borderWidth + options.padding) + "px";

        container.appendChild(elements.loader);

        elements.scrollElement.addEventListener("scroll", onScroll, false);

        self.addEventListener("loadedAllItems", function(){
            elements.scrollElement.removeEventListener("scroll", onScroll, false);
            data.removeEventListener("loadSources", setDataSource);
            loadedAllItems = true;
            $(container).addClass(self.getThemeClass("loadedAll"));
        });
    };
}

yox.themes.wall.defaults = {
    borderWidth: 7, // The size, in pixels, of the space between thumbnails
    loadItemsOnScroll: false, // Whether to get more results from the data source when scrolling down
    padding: 10, // The padding arround the thumbnails (padding for the element that contains all the thumbnails)
    scrollAnimationDuration: 500, // The time, in milliseconds, for the scroll animation, when a thumbnail is brought into view.
    scrollOffset: 60, // When scrolling a thumbnail into view, this number of pixels will be added to the scroll distance, so the thumbnail isn't at the very limit of the visible area.
    scrollToElementOnSelect: false, // If set to true, the theme's container will be scrolled to the selected thumbnail when its item is selected
    thumbnailsMaxHeight: 200 // The maximum height allowed for each thumbnail
};

yox.themes.wall.prototype = new yox.theme();<|endoftext|>


<commit_before>// Test the test runner

var assert = require('assert');
var chai = require('chai');
var expect = chai.expect;
var should = chai.should;
var _ = require('underscore');

// always-true-mocha.js provides a template for other tests and can be used to debug the mocha test runner.

    describe('always true', function() {
        it('is always true', function() {
            expect(true).to.equal(true);
        });
    });
<commit_msg>test fail build<commit_after>// Test the test runner

var assert = require('assert');
var chai = require('chai');
var expect = chai.expect;
var should = chai.should;
var _ = require('underscore');

// always-true-mocha.js provides a template for other tests and can be used to debug the mocha test runner.

    describe('always true', function() {
        it('is always true', function() {
            expect(false).to.equal(true);
        });
    });
<|endoftext|>


<commit_before>angular.module('ui.sortable.multiselection', [])
  .constant('uiSortableMultiSelectionClass', 'ui-sortable-selected')
  .directive('uiSortableSelectable', [
    'uiSortableMultiSelectionClass',
    function(selectedItemClass) {
      return {
        link: function(scope, element/*, attrs*/) {
          element.on('click', function (e) {
            if (e.ctrlKey || e.metaKey) {
              var $this = angular.element(this);
              $this.toggleClass(selectedItemClass);
            }
          });
        }
      };
    }
  ])
  .factory('uiSortableMultiSelectionMethods', [
    'uiSortableMultiSelectionClass',
    function (selectedItemClass) {
      function fixIndex (oldPosition, newPosition, x) {
        if (oldPosition < x && (newPosition === undefined || (oldPosition < newPosition && x <= newPosition))) {
          return x - 1;
        } else if (x < oldPosition && (newPosition === undefined || (newPosition < oldPosition && newPosition <= x))) {
          return x + 1;
        }
        return x;
      }

      function groupIndexes (indexes, oldPosition, newPosition) {
        var above = [],
            below = [];

        for (var i = 0; i < indexes.length; i++) {
          var x = indexes[i];
          if (x < oldPosition) {
            above.push(fixIndex(oldPosition, newPosition, x));
          } else if (oldPosition < x) {
            below.push(fixIndex(oldPosition, newPosition, x));
          }
        }

        return {
          above: above,
          below: below
        };
      }

      function getModelsFromIndexes (ngModel, indexes) {
        var result = [];
        for (var i = indexes.length - 1; i >= 0; i--) {
          result.push(ngModel.splice(indexes[i], 1)[0]);
        }
        result.reverse();
        return result;
      }

      return {
        helper: function (e, item) {
          // when starting to sort an unhighlighted item ,
          // deselect any existing highlighted items
          if (!item.hasClass(selectedItemClass)) {
              item.addClass(selectedItemClass)
                .siblings()
                .removeClass(selectedItemClass);
          }

          var selectedElements = item.parent().children('.' + selectedItemClass);
          var selectedSiblings = item.siblings('.' + selectedItemClass);

          // indexes of the selected siblings
          var indexes = angular.element.map(selectedSiblings, function (element) {
            return angular.element(element).index();
          });

          item.sortableMultiSelect = {
            indexes: indexes
          };

          // Clone the selected items and to put them inside the helper
          var elements = selectedElements.clone();

          // like `helper: 'clone'` does, hide the dragged elements
          selectedSiblings.hide();

          // Create the helper to act as a bucket for the cloned elements
          var helperTag = item[0].tagName;
          var helper = angular.element('<' + helperTag + '/>');
          return helper.append(elements);
        },
        start: function(e, ui) {
          ui.item.sortableMultiSelect.sourceElement = ui.item.parent();
        },
        update: function(e, ui) {
          if(!ui.item.sortable.received) {

          }
          if (ui.item.sortable.received && !ui.item.sortable.isCanceled()) {
            var scope = ui.item.sortable.droptarget.scope();

            scope.$apply(function () {
              var ngModel = scope.$eval(ui.item.sortable.droptarget.attr('ng-model')),
                  newPosition = ui.item.sortable.dropindex,
                  models = ui.item.sortableMultiSelect.moved;

              // add the models to the target list
              Array.prototype.splice.apply(
                ngModel,
                [newPosition+ 1, 0]
                .concat(models.below));

              Array.prototype.splice.apply(
                ngModel,
                [newPosition, 0]
                .concat(models.above));
            });
          }
        },
        remove: function(e, ui) {
          if (!ui.item.sortable.isCanceled()) {
            var scope = ui.item.sortableMultiSelect.sourceElement.scope();

            scope.$apply(function () {
              var ngModel = scope.$eval(ui.item.sortableMultiSelect.sourceElement.attr('ng-model')),
                  oldPosition = ui.item.sortable.index;

              var indexes = groupIndexes(ui.item.sortableMultiSelect.indexes, oldPosition);

              // get the models and remove them from the original list
              // the code should run in reverse order,
              // so that the indexes will not break
              ui.item.sortableMultiSelect.moved = {
                below: getModelsFromIndexes(ngModel, indexes.below),
                above: getModelsFromIndexes(ngModel, indexes.above)
              };
            });
          }
        },
        stop: function (e, ui) {
          if (!ui.item.sortable.received &&
             ('dropindex' in ui.item.sortable) &&
             !ui.item.sortable.isCanceled()) {
            var sourceElement = ui.item.sortableMultiSelect.sourceElement || ui.item.parent(),
                ngModel = sourceElement.scope().$eval(sourceElement.attr('ng-model')),
                oldPosition = ui.item.sortable.index,
                newPosition = ui.item.sortable.dropindex;

            var draggedElementIndexes = ui.item.sortableMultiSelect.indexes;
            if (!draggedElementIndexes.length) {
              return;
            }

            var indexes = groupIndexes(draggedElementIndexes, oldPosition, newPosition);

            // get the model of the dragged item
            // so that we can locate its position
            // after we remove the co-dragged elements
            var draggedModel = ngModel[newPosition];

            // get the models and remove them from the list
            // the code should run in reverse order,
            // so that the indexes will not break
            var models = {
              below: getModelsFromIndexes(ngModel, indexes.below),
              above: getModelsFromIndexes(ngModel, indexes.above)
            };

            // add the models to the list
            Array.prototype.splice.apply(
              ngModel,
              [ngModel.indexOf(draggedModel) + 1, 0]
              .concat(models.below));

            Array.prototype.splice.apply(
              ngModel,
              [ngModel.indexOf(draggedModel), 0]
              .concat(models.above));

            ui.item.parent().find('> .' + selectedItemClass).removeClass('' + selectedItemClass).show();
          }
        }
      };
    }]);<commit_msg>feat: add extendOptions method<commit_after>angular.module('ui.sortable.multiselection', [])
  .constant('uiSortableMultiSelectionClass', 'ui-sortable-selected')
  .directive('uiSortableSelectable', [
    'uiSortableMultiSelectionClass',
    function(selectedItemClass) {
      return {
        link: function(scope, element/*, attrs*/) {
          element.on('click', function (e) {
            if (e.ctrlKey || e.metaKey) {
              var $this = angular.element(this);
              $this.toggleClass(selectedItemClass);
            }
          });
        }
      };
    }
  ])
  .factory('uiSortableMultiSelectionMethods', [
    'uiSortableMultiSelectionClass',
    function (selectedItemClass) {
      function fixIndex (oldPosition, newPosition, x) {
        if (oldPosition < x && (newPosition === undefined || (oldPosition < newPosition && x <= newPosition))) {
          return x - 1;
        } else if (x < oldPosition && (newPosition === undefined || (newPosition < oldPosition && newPosition <= x))) {
          return x + 1;
        }
        return x;
      }

      function groupIndexes (indexes, oldPosition, newPosition) {
        var above = [],
            below = [];

        for (var i = 0; i < indexes.length; i++) {
          var x = indexes[i];
          if (x < oldPosition) {
            above.push(fixIndex(oldPosition, newPosition, x));
          } else if (oldPosition < x) {
            below.push(fixIndex(oldPosition, newPosition, x));
          }
        }

        return {
          above: above,
          below: below
        };
      }

      function getModelsFromIndexes (ngModel, indexes) {
        var result = [];
        for (var i = indexes.length - 1; i >= 0; i--) {
          result.push(ngModel.splice(indexes[i], 1)[0]);
        }
        result.reverse();
        return result;
      }

      function combineCallbacks(first,second){
        if(second && (typeof second === 'function')) {
          return function(e, ui) {
            first(e, ui);
            second(e, ui);
          };
        }
        return first;
      }

      return {
        extendOptions: function (sortableOptions) {
          sortableOptions = sortableOptions || {};
          var result = $.extend({}, this, sortableOptions);

          for (var prop in sortableOptions) {
            if (sortableOptions.hasOwnProperty(prop)) {
              if (this[prop]) {
                result[prop] = combineCallbacks(this[prop], sortableOptions[prop]);
              }
            }
          }

          return result;
        },
        helper: function (e, item) {
          // when starting to sort an unhighlighted item ,
          // deselect any existing highlighted items
          if (!item.hasClass(selectedItemClass)) {
              item.addClass(selectedItemClass)
                .siblings()
                .removeClass(selectedItemClass);
          }

          var selectedElements = item.parent().children('.' + selectedItemClass);
          var selectedSiblings = item.siblings('.' + selectedItemClass);

          // indexes of the selected siblings
          var indexes = angular.element.map(selectedSiblings, function (element) {
            return angular.element(element).index();
          });

          item.sortableMultiSelect = {
            indexes: indexes
          };

          // Clone the selected items and to put them inside the helper
          var elements = selectedElements.clone();

          // like `helper: 'clone'` does, hide the dragged elements
          selectedSiblings.hide();

          // Create the helper to act as a bucket for the cloned elements
          var helperTag = item[0].tagName;
          var helper = angular.element('<' + helperTag + '/>');
          return helper.append(elements);
        },
        start: function(e, ui) {
          ui.item.sortableMultiSelect.sourceElement = ui.item.parent();
        },
        update: function(e, ui) {
          if(!ui.item.sortable.received) {

          }
          if (ui.item.sortable.received && !ui.item.sortable.isCanceled()) {
            var scope = ui.item.sortable.droptarget.scope();

            scope.$apply(function () {
              var ngModel = scope.$eval(ui.item.sortable.droptarget.attr('ng-model')),
                  newPosition = ui.item.sortable.dropindex,
                  models = ui.item.sortableMultiSelect.moved;

              // add the models to the target list
              Array.prototype.splice.apply(
                ngModel,
                [newPosition+ 1, 0]
                .concat(models.below));

              Array.prototype.splice.apply(
                ngModel,
                [newPosition, 0]
                .concat(models.above));
            });
          }
        },
        remove: function(e, ui) {
          if (!ui.item.sortable.isCanceled()) {
            var scope = ui.item.sortableMultiSelect.sourceElement.scope();

            scope.$apply(function () {
              var ngModel = scope.$eval(ui.item.sortableMultiSelect.sourceElement.attr('ng-model')),
                  oldPosition = ui.item.sortable.index;

              var indexes = groupIndexes(ui.item.sortableMultiSelect.indexes, oldPosition);

              // get the models and remove them from the original list
              // the code should run in reverse order,
              // so that the indexes will not break
              ui.item.sortableMultiSelect.moved = {
                below: getModelsFromIndexes(ngModel, indexes.below),
                above: getModelsFromIndexes(ngModel, indexes.above)
              };
            });
          }
        },
        stop: function (e, ui) {
          if (!ui.item.sortable.received &&
             ('dropindex' in ui.item.sortable) &&
             !ui.item.sortable.isCanceled()) {
            var sourceElement = ui.item.sortableMultiSelect.sourceElement || ui.item.parent(),
                ngModel = sourceElement.scope().$eval(sourceElement.attr('ng-model')),
                oldPosition = ui.item.sortable.index,
                newPosition = ui.item.sortable.dropindex;

            var draggedElementIndexes = ui.item.sortableMultiSelect.indexes;
            if (!draggedElementIndexes.length) {
              return;
            }

            var indexes = groupIndexes(draggedElementIndexes, oldPosition, newPosition);

            // get the model of the dragged item
            // so that we can locate its position
            // after we remove the co-dragged elements
            var draggedModel = ngModel[newPosition];

            // get the models and remove them from the list
            // the code should run in reverse order,
            // so that the indexes will not break
            var models = {
              below: getModelsFromIndexes(ngModel, indexes.below),
              above: getModelsFromIndexes(ngModel, indexes.above)
            };

            // add the models to the list
            Array.prototype.splice.apply(
              ngModel,
              [ngModel.indexOf(draggedModel) + 1, 0]
              .concat(models.below));

            Array.prototype.splice.apply(
              ngModel,
              [ngModel.indexOf(draggedModel), 0]
              .concat(models.above));

            ui.item.parent().find('> .' + selectedItemClass).removeClass('' + selectedItemClass).show();
          }
        }
      };
    }]);<|endoftext|>


<commit_before>// Web page retrieval using server-side proxy support to get around CORS restrictions

requirejs(["sanitizeHTML", "vendor/purify"], function(sanitizeHTML, DOMPurify) {

    let content

    // const url = "http://kurtz-fernhout.com/"
    const url = "http://pdfernhout.net/"

    const crossOriginService = "/api/proxy"

    m.request({
        method: "POST",
        url: crossOriginService,
        data: { url },
    }).then((result) => {
        console.log("result", result)
        content = result.content
        m.redraw()
    }).catch((error) => {
        console.log("An error occured:", error)
        content = JSON.stringify(error)
        m.redraw()
    })

    Twirlip7.show(() => {
        return m("div", [
            content ?
                // Two examples of ways to sanitize web content for display locally -- but always a bit of a risk to display remote content
                sanitizeHTML.generateSanitizedHTMLForMithrilWithAttributes(m, DOMParser, content, {allowLinks: true, allowImages: true, allowIds: true, baseURL: url})
                // m.trust(DOMPurify.sanitize(content))
                :
                "loading..."
        ])
    }, { title: url })
})
<commit_msg>Improve error reporting in proxy-web-page.js<commit_after>// Web page retrieval using server-side proxy support to get around CORS restrictions
// Proxying will only work if using the Twirlip7 server
// So, proxy requests won't work if running from GitHub and rawgit.com.

requirejs(["sanitizeHTML", "vendor/purify"], function(sanitizeHTML, DOMPurify) {

    let content

    // const url = "http://kurtz-fernhout.com/"
    const url = "http://pdfernhout.net/"

    const crossOriginService = "/api/proxy"

    m.request({
        method: "POST",
        url: crossOriginService,
        data: { url },
    }).then((result) => {
        console.log("result", result)
        content = result.content
        m.redraw()
    }).catch((error) => {
        console.log("An error occured:", error)
        content = "ERROR: " + error
        m.redraw()
    })

    Twirlip7.show(() => {
        return m("div", [
            content ?
                // Two examples of ways to sanitize web content for display locally -- but always a bit of a risk to display remote content
                sanitizeHTML.generateSanitizedHTMLForMithrilWithAttributes(m, DOMParser, content, {allowLinks: true, allowImages: true, allowIds: true, baseURL: url})
                // m.trust(DOMPurify.sanitize(content))
                :
                "loading..."
        ])
    }, { title: url })
})
<|endoftext|>


<commit_before>import { freqScale, NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE } from "./globals";
import { calcTipCounts } from "./treeCountingHelpers";
import { getTraitFromNode } from "./treeMiscHelpers";
import { warningNotification } from "../actions/notifications";

export const getVisibleDateRange = (nodes, visibility) => nodes
  .filter((node, idx) => (visibility[idx] === NODE_VISIBLE && !node.hasChildren))
  .reduce((acc, node) => {
    const nodeDate = getTraitFromNode(node, "num_date");
    return nodeDate ? [Math.min(nodeDate, acc[0]), Math.max(nodeDate, acc[1])] : acc;
  }, [100000, -100000]);

export const strainNameToIdx = (nodes, name) => {
  let i;
  for (i = 0; i < nodes.length; i++) {
    if (nodes[i].name === name) {
      return i;
    }
  }
  console.error("strainNameToIdx couldn't find strain");
  return 0;
};

/**
 * Find the node with the given label name & value
 * NOTE: if there are multiple nodes with the same label then the first encountered is returned
 * @param {Array} nodes tree nodes (flat)
 * @param {string} labelName label name
 * @param {string} labelValue label value
 * @returns {int} the index of the matching node (0 if no match found)
 */
export const getIdxMatchingLabel = (nodes, labelName, labelValue, dispatch) => {
  let i;
  let found = 0;
  for (i = 0; i < nodes.length; i++) {
    if (
      nodes[i].branch_attrs &&
      nodes[i].branch_attrs.labels !== undefined &&
      nodes[i].branch_attrs.labels[labelName] === labelValue
    ) {
      if (found === 0) {
        found = i;
      } else {
        console.error(`getIdxMatchingLabel found multiple labels ${labelName}===${labelValue}`);
        dispatch(warningNotification({
          message: "Specified Zoom Label Found Multiple Times!",
          details: "Multiple nodes in the tree are labelled '"+labelName+" "+labelValue+"' - no zoom performed"
        }));
        return 0;
      }
    }
  }
  if (found === 0) {
    console.error(`getIdxMatchingLabel couldn't find label ${labelName}===${labelValue}`);
    dispatch(warningNotification({
      message: "Specified Zoom Label Value Not Found!",
      details: "The label '"+labelName+"' value '"+labelValue+"' was not found in the tree - no zoom performed"
    }));
  }
  return found;
};

/** calcBranchThickness **
* returns an array of node (branch) thicknesses based on the tipCount at each node
* If the node isn't visible, the thickness is 1.
* Relies on the `tipCount` property of the nodes having been updated.
* Pure.
* @param nodes - JSON nodes
* @param visibility - visibility array (1-1 with nodes)
* @returns array of thicknesses (numeric)
*/
const calcBranchThickness = (nodes, visibility) => {
  let maxTipCount = nodes[0].tipCount;
  /* edge case: no tips selected */
  if (!maxTipCount) {
    maxTipCount = 1;
  }
  return nodes.map((d, idx) => {
    if (visibility[idx] === NODE_VISIBLE) {
      return freqScale((d.tipCount + 5) / (maxTipCount + 5));
    }
    return 0.5;
  });
};

/* recursively mark the parents of a given node active
by setting the node idx to true in the param visArray */
const makeParentVisible = (visArray, node) => {
  if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) {
    return; // this is the root of the tree or the parent was already visibile
  }
  visArray[node.parent.arrayIdx] = true;
  makeParentVisible(visArray, node.parent);
};

/* Recursively hide nodes that do not have more than one child node in
 * the param visArray.
 * Relies on visArray having been updated by `makeParentVisible` */
const hideNodesAboveVisibleCommonAncestor = (visArray, node) => {
  if (!node.hasChildren) {
    return; // Terminal node without children
  }
  const visibleChildren = node.children.filter((child) => visArray[child.arrayIdx]);
  if (visibleChildren.length > 1) {
    return; // This is the common ancestor of visible children
  }
  visArray[node.arrayIdx] = false;
  visibleChildren.forEach((child) => hideNodesAboveVisibleCommonAncestor(visArray, child));
};

/* calcVisibility
USES:
inView: attribute of phyloTree.nodes, but accessible through redux.tree.nodes[idx].shell.inView
  Bool. Set by phyloTree, determines if the tip is within the view.
controls.filters
use dates NOT controls.dateMin & controls.dateMax

RETURNS:
visibility: array of integers in {0, 1, 2}
 - 0: not displayed by map. Potentially displayed by tree as a thin branch.
 - 1: available for display by the map. Displayed by tree as a thin branch.
 - 2: Displayed by both the map and the tree.

ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED:
 - inView filtering (reflects tree zooming): Nodes which are not inView always have visibility=0
 - time filtering is simple - all nodes (internal + terminal) not within (tmin, tmax) are excluded.
- filters are a bit more tricky - the visibile tips are calculated, and the parent
    branches back to the MRCA are considered visibile. This is then intersected with
    the time & inView visibile stuff

FILTERS:
 - controls.filters (redux) is a dict of trait name -> values
 - filters (in this code) is a list of filters to apply
   e.g. [{trait: "country", values: [...]}, ...]
*/
export const calcVisibility = (tree, controls, dates) => {
  if (tree.nodes) {
    /* inView represents nodes that are within the current view window (i.e. not off the screen) */
    let inView;
    try {
      inView = tree.nodes.map((d) => d.shell.inView);
    } catch (e) {
      /* edge case: this fn may be called before the shell structure of the nodes
       * has been created (i.e. phyloTree's not run yet). In this case, it's
       * safe to assume that everything's in view */
      inView = tree.nodes.map((d) => d.inView !== undefined ? d.inView : true);
    }

    // FILTERS
    let filtered; // array of bools, same length as tree.nodes. true -> that node should be visible
    const filters = [];
    Reflect.ownKeys(controls.filters).forEach((filterName) => {
      const items = controls.filters[filterName];
      const activeFilterItems = items.filter((item) => item.active).map((item) => item.value);
      if (activeFilterItems.length) {
        filters.push({trait: filterName, values: activeFilterItems});
      }
    });
    if (filters.length) {
      /* find the terminal nodes that were (a) already visibile and (b) match the filters */
      filtered = tree.nodes.map((d, idx) => (
        !d.hasChildren && inView[idx] && filters.every((f) => f.values.includes(getTraitFromNode(d, f.trait)))
      ));
      const idxsOfFilteredTips = filtered.reduce((a, e, i) => {
        if (e) {a.push(i);}
        return a;
      }, []);
      /* for each visibile tip, make the parent nodes visible (recursively) */
      for (let i = 0; i < idxsOfFilteredTips.length; i++) {
        makeParentVisible(filtered, tree.nodes[idxsOfFilteredTips[i]]);
      }
      /* Recursivley hide ancestor nodes that are not the last common
       * ancestor of selected nodes, starting from the root of the tree */
      hideNodesAboveVisibleCommonAncestor(filtered, tree.nodes[0]);
    }
    /* intersect the various arrays contributing to visibility */
    const visibility = tree.nodes.map((node, idx) => {
      if (inView[idx] && (filtered ? filtered[idx] : true)) {
        const nodeDate = getTraitFromNode(node, "num_date");
        const parentNodeDate = getTraitFromNode(node.parent, "num_date");
        if (!nodeDate || !parentNodeDate) {
          return NODE_VISIBLE;
        }
        /* if branchLengthsToDisplay is "divOnly", then ensure node displayed */
        if (controls.branchLengthsToDisplay === "divOnly") {
          return NODE_VISIBLE;
        }
        /* is the actual node date (the "end" of the branch) in the time slice? */
        if (nodeDate >= dates.dateMinNumeric && nodeDate <= dates.dateMaxNumeric) {
          return NODE_VISIBLE;
        }
        /* is any part of the (parent date -> node date) in the time slice? */
        if (!(nodeDate < dates.dateMinNumeric || parentNodeDate > dates.dateMaxNumeric)) {
          return NODE_VISIBLE_TO_MAP_ONLY;
        }
      }
      return NODE_NOT_VISIBLE;
    });
    return visibility;
  }
  console.error("calcVisibility ran without tree.nodes");
  return NODE_VISIBLE;
};

export const calculateVisiblityAndBranchThickness = (tree, controls, dates) => {
  const visibility = calcVisibility(tree, controls, dates);
  /* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */
  calcTipCounts(tree.nodes[0], visibility);
  /* re-calculate branchThickness (inline) */
  return {
    visibility: visibility,
    visibilityVersion: tree.visibilityVersion + 1,
    branchThickness: calcBranchThickness(tree.nodes, visibility),
    branchThicknessVersion: tree.branchThicknessVersion + 1
  };
};
<commit_msg>Update hideNodesAboveVisibleCommonAncestor() to return index of LCA<commit_after>import { freqScale, NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE } from "./globals";
import { calcTipCounts } from "./treeCountingHelpers";
import { getTraitFromNode } from "./treeMiscHelpers";
import { warningNotification } from "../actions/notifications";

export const getVisibleDateRange = (nodes, visibility) => nodes
  .filter((node, idx) => (visibility[idx] === NODE_VISIBLE && !node.hasChildren))
  .reduce((acc, node) => {
    const nodeDate = getTraitFromNode(node, "num_date");
    return nodeDate ? [Math.min(nodeDate, acc[0]), Math.max(nodeDate, acc[1])] : acc;
  }, [100000, -100000]);

export const strainNameToIdx = (nodes, name) => {
  let i;
  for (i = 0; i < nodes.length; i++) {
    if (nodes[i].name === name) {
      return i;
    }
  }
  console.error("strainNameToIdx couldn't find strain");
  return 0;
};

/**
 * Find the node with the given label name & value
 * NOTE: if there are multiple nodes with the same label then the first encountered is returned
 * @param {Array} nodes tree nodes (flat)
 * @param {string} labelName label name
 * @param {string} labelValue label value
 * @returns {int} the index of the matching node (0 if no match found)
 */
export const getIdxMatchingLabel = (nodes, labelName, labelValue, dispatch) => {
  let i;
  let found = 0;
  for (i = 0; i < nodes.length; i++) {
    if (
      nodes[i].branch_attrs &&
      nodes[i].branch_attrs.labels !== undefined &&
      nodes[i].branch_attrs.labels[labelName] === labelValue
    ) {
      if (found === 0) {
        found = i;
      } else {
        console.error(`getIdxMatchingLabel found multiple labels ${labelName}===${labelValue}`);
        dispatch(warningNotification({
          message: "Specified Zoom Label Found Multiple Times!",
          details: "Multiple nodes in the tree are labelled '"+labelName+" "+labelValue+"' - no zoom performed"
        }));
        return 0;
      }
    }
  }
  if (found === 0) {
    console.error(`getIdxMatchingLabel couldn't find label ${labelName}===${labelValue}`);
    dispatch(warningNotification({
      message: "Specified Zoom Label Value Not Found!",
      details: "The label '"+labelName+"' value '"+labelValue+"' was not found in the tree - no zoom performed"
    }));
  }
  return found;
};

/** calcBranchThickness **
* returns an array of node (branch) thicknesses based on the tipCount at each node
* If the node isn't visible, the thickness is 1.
* Relies on the `tipCount` property of the nodes having been updated.
* Pure.
* @param nodes - JSON nodes
* @param visibility - visibility array (1-1 with nodes)
* @returns array of thicknesses (numeric)
*/
const calcBranchThickness = (nodes, visibility) => {
  let maxTipCount = nodes[0].tipCount;
  /* edge case: no tips selected */
  if (!maxTipCount) {
    maxTipCount = 1;
  }
  return nodes.map((d, idx) => {
    if (visibility[idx] === NODE_VISIBLE) {
      return freqScale((d.tipCount + 5) / (maxTipCount + 5));
    }
    return 0.5;
  });
};

/* recursively mark the parents of a given node active
by setting the node idx to true in the param visArray */
const makeParentVisible = (visArray, node) => {
  if (node.arrayIdx === 0 || visArray[node.parent.arrayIdx]) {
    return; // this is the root of the tree or the parent was already visibile
  }
  visArray[node.parent.arrayIdx] = true;
  makeParentVisible(visArray, node.parent);
};

/* Recursively hide nodes that do not have more than one child node by updating
 * the boolean values in the param visArray.
 * Relies on visArray having been updated by `makeParentVisible`
 * Returns the index of the visible commonn ancestor. */
const hideNodesAboveVisibleCommonAncestor = (visArray, node) => {
  if (!node.hasChildren) {
    return node.arrayIdx; // Terminal node without children
  }
  const visibleChildren = node.children.filter((child) => visArray[child.arrayIdx]);
  if (visibleChildren.length > 1) {
    return node.arrayIdx; // This is the common ancestor of visible children
  }
  visArray[node.arrayIdx] = false;
  for (let i = 0; i < visibleChildren.length; i++) {
    const commonAncestorIdx = hideNodesAboveVisibleCommonAncestor(visArray, visibleChildren[i]);
    if (commonAncestorIdx) return commonAncestorIdx;
  }
  // If there is no visible common ancestor, then return null
  return null;
};

/* calcVisibility
USES:
inView: attribute of phyloTree.nodes, but accessible through redux.tree.nodes[idx].shell.inView
  Bool. Set by phyloTree, determines if the tip is within the view.
controls.filters
use dates NOT controls.dateMin & controls.dateMax

RETURNS:
visibility: array of integers in {0, 1, 2}
 - 0: not displayed by map. Potentially displayed by tree as a thin branch.
 - 1: available for display by the map. Displayed by tree as a thin branch.
 - 2: Displayed by both the map and the tree.

ROUGH DESCRIPTION OF HOW FILTERING IS APPLIED:
 - inView filtering (reflects tree zooming): Nodes which are not inView always have visibility=0
 - time filtering is simple - all nodes (internal + terminal) not within (tmin, tmax) are excluded.
- filters are a bit more tricky - the visibile tips are calculated, and the parent
    branches back to the MRCA are considered visibile. This is then intersected with
    the time & inView visibile stuff

FILTERS:
 - controls.filters (redux) is a dict of trait name -> values
 - filters (in this code) is a list of filters to apply
   e.g. [{trait: "country", values: [...]}, ...]
*/
export const calcVisibility = (tree, controls, dates) => {
  if (tree.nodes) {
    /* inView represents nodes that are within the current view window (i.e. not off the screen) */
    let inView;
    try {
      inView = tree.nodes.map((d) => d.shell.inView);
    } catch (e) {
      /* edge case: this fn may be called before the shell structure of the nodes
       * has been created (i.e. phyloTree's not run yet). In this case, it's
       * safe to assume that everything's in view */
      inView = tree.nodes.map((d) => d.inView !== undefined ? d.inView : true);
    }

    // FILTERS
    let filtered; // array of bools, same length as tree.nodes. true -> that node should be visible
    const filters = [];
    Reflect.ownKeys(controls.filters).forEach((filterName) => {
      const items = controls.filters[filterName];
      const activeFilterItems = items.filter((item) => item.active).map((item) => item.value);
      if (activeFilterItems.length) {
        filters.push({trait: filterName, values: activeFilterItems});
      }
    });
    if (filters.length) {
      /* find the terminal nodes that were (a) already visibile and (b) match the filters */
      filtered = tree.nodes.map((d, idx) => (
        !d.hasChildren && inView[idx] && filters.every((f) => f.values.includes(getTraitFromNode(d, f.trait)))
      ));
      const idxsOfFilteredTips = filtered.reduce((a, e, i) => {
        if (e) {a.push(i);}
        return a;
      }, []);
      /* for each visibile tip, make the parent nodes visible (recursively) */
      for (let i = 0; i < idxsOfFilteredTips.length; i++) {
        makeParentVisible(filtered, tree.nodes[idxsOfFilteredTips[i]]);
      }
      /* Recursivley hide ancestor nodes that are not the last common
       * ancestor of selected nodes, starting from the root of the tree */
      hideNodesAboveVisibleCommonAncestor(filtered, tree.nodes[0]);
    }
    /* intersect the various arrays contributing to visibility */
    const visibility = tree.nodes.map((node, idx) => {
      if (inView[idx] && (filtered ? filtered[idx] : true)) {
        const nodeDate = getTraitFromNode(node, "num_date");
        const parentNodeDate = getTraitFromNode(node.parent, "num_date");
        if (!nodeDate || !parentNodeDate) {
          return NODE_VISIBLE;
        }
        /* if branchLengthsToDisplay is "divOnly", then ensure node displayed */
        if (controls.branchLengthsToDisplay === "divOnly") {
          return NODE_VISIBLE;
        }
        /* is the actual node date (the "end" of the branch) in the time slice? */
        if (nodeDate >= dates.dateMinNumeric && nodeDate <= dates.dateMaxNumeric) {
          return NODE_VISIBLE;
        }
        /* is any part of the (parent date -> node date) in the time slice? */
        if (!(nodeDate < dates.dateMinNumeric || parentNodeDate > dates.dateMaxNumeric)) {
          return NODE_VISIBLE_TO_MAP_ONLY;
        }
      }
      return NODE_NOT_VISIBLE;
    });
    return visibility;
  }
  console.error("calcVisibility ran without tree.nodes");
  return NODE_VISIBLE;
};

export const calculateVisiblityAndBranchThickness = (tree, controls, dates) => {
  const visibility = calcVisibility(tree, controls, dates);
  /* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */
  calcTipCounts(tree.nodes[0], visibility);
  /* re-calculate branchThickness (inline) */
  return {
    visibility: visibility,
    visibilityVersion: tree.visibilityVersion + 1,
    branchThickness: calcBranchThickness(tree.nodes, visibility),
    branchThicknessVersion: tree.branchThicknessVersion + 1
  };
};
<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import moment from 'moment';
import { UIDatabase } from '../database';

import { formInputStrings } from '../localization';

/**
 * File contains constants and config objects which declaritively define
 * form inputs to be used with `FormControl`.
 *
 * For a particular form, add a field to FIELD_CONFIGS, which will define what
 * form input configs to use.
 *
 * FormInputConfig have the following structure:
 * {
 *     type: 'text' : A ValidationTextInput component
 *     initialValue : The value to seed the form input component with.
 *     key: A key for the underlying record.
 *     isRequired : Indicator whether this input is required to be filled for completion.
 *     label: A label for the form input.
 *     invalidMessage: A message when the input is invalid.
 *     validator: A function to call to determine if the current input is valid.
 * }
 */

const FORM_INPUT_KEYS = {
  FIRST_NAME: 'firstName',
  LAST_NAME: 'lastName',
  CODE: 'code',
  DATE_OF_BIRTH: 'dateOfBirth',
  EMAIL: 'emailAddress',
  PHONE: 'phoneNumber',
  COUNTRY: 'country',
  ADDRESS_ONE: 'addressOne',
  ADDRESS_TWO: 'addressTwo',
  REGISTRATION_CODE: 'registrationCode',
  POLICY_NUMBER_FAMILY: 'policyNumberFamily',
  POLICY_NUMBER_PERSON: 'policyNumberPerson',
  POLICY_PROVIDER: 'insuranceProvider',
  POLICY_TYPE: 'policyType',
  IS_ACTIVE: 'isActive',
  DISCOUNT_RATE: 'discountRate',
};

const FORM_INPUT_CONFIGS = seedObject => ({
  [FORM_INPUT_KEYS.FIRST_NAME]: {
    type: 'text',
    initialValue: '',
    key: 'firstName',
    validator: input => input.length > 0,
    isRequired: true,
    label: formInputStrings.first_name,
    invalidMessage: formInputStrings.must_not_be_empty,
  },
  [FORM_INPUT_KEYS.LAST_NAME]: {
    type: 'text',
    initialValue: '',
    key: 'lastName',
    validator: input => input.length > 0,
    isRequired: true,
    label: formInputStrings.last_name,
    invalidMessage: formInputStrings.must_not_be_empty,
  },
  [FORM_INPUT_KEYS.CODE]: {
    type: 'text',
    initialValue: '',
    key: 'code',
    validator: input => input.length > 0 && input.length < 20,
    isRequired: true,
    label: formInputStrings.code,
    invalidMessage: `${formInputStrings.must_not_be_empty} ${formInputStrings.and} ${formInputStrings.less_than_20_characters}`,
  },
  [FORM_INPUT_KEYS.DATE_OF_BIRTH]: {
    type: 'date',
    initialValue: new Date(),
    key: 'dateOfBirth',
    invalidMessage: formInputStrings.must_be_a_date,
    isRequired: true,
    validator: input => {
      const inputDate = moment(input, 'DD/MM/YYYY', null, true);
      const isValid = inputDate.isValid();
      const isDateOfBirth = inputDate.isSameOrBefore(new Date());
      return isValid && isDateOfBirth;
    },
    label: formInputStrings.date_of_birth,
  },
  [FORM_INPUT_KEYS.GENDER]: {
    type: 'toggle',
    initialValue: 'false',
    key: 'female',
    options: [true, false],
    optionLabels: [formInputStrings.female, formInputStrings.male],
    label: formInputStrings.gender,
  },
  [FORM_INPUT_KEYS.EMAIL]: {
    type: 'text',
    initialValue: '',
    key: 'emailAddress',
    isRequired: false,
    label: formInputStrings.email,
  },
  [FORM_INPUT_KEYS.PHONE]: {
    type: 'text',
    initialValue: '',
    key: 'phoneNumber',
    isRequired: false,
    label: formInputStrings.phone,
  },
  [FORM_INPUT_KEYS.COUNTRY]: {
    type: 'text',
    initialValue: '',
    key: 'country',
    validator: input => input.length < 20,
    isRequired: false,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_20_characters}`,
    label: formInputStrings.country,
  },
  [FORM_INPUT_KEYS.ADDRESS_ONE]: {
    type: 'text',
    initialValue: '',
    key: 'addressOne',
    validator: input => input.length < 50,
    isRequired: false,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_50_characters}`,
    label: formInputStrings.address_one,
  },
  [FORM_INPUT_KEYS.ADDRESS_TWO]: {
    type: 'text',
    initialValue: '',
    key: 'addressTwo',
    validator: input => input.length < 50,
    isRequired: false,
    label: formInputStrings.address_two,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_50_characters}`,
  },
  [FORM_INPUT_KEYS.REGISTRATION_CODE]: {
    type: 'text',
    initialValue: '',
    key: 'registrationCode',
    validator: input => input.length > 0 && input.length < 50,
    isRequired: true,
    label: formInputStrings.registration_code,
    invalidMessage: formInputStrings.must_be_between_0_and_50,
  },
  [FORM_INPUT_KEYS.POLICY_NUMBER_PERSON]: {
    type: 'text',
    initialValue: '',
    key: 'policyNumberPerson',
    validator: input =>
      input.length > 0 &&
      input.length < 50 &&
      UIDatabase.objects('InsurancePolicy').filtered(
        'policyNumberPerson == $0 && id != $1',
        input,
        seedObject?.id ?? ''
      ).length === 0,
    isRequired: true,
    invalidMessage: formInputStrings.unique_personal_policy,
    label: formInputStrings.personal_policy_number,
  },
  [FORM_INPUT_KEYS.POLICY_NUMBER_FAMILY]: {
    type: 'text',
    initialValue: '',
    key: 'policyNumberFamily',
    validator: input => input.length > 0 && input.length < 50,
    isRequired: true,
    invalidMessage: formInputStrings.must_be_between_0_and_50,
    label: formInputStrings.family_policy_number,
  },
  [FORM_INPUT_KEYS.POLICY_PROVIDER]: {
    type: 'dropdown',
    initialValue: UIDatabase.objects('InsuranceProvider')[0],
    key: 'insuranceProvider',
    label: formInputStrings.policy_provider,
    options: UIDatabase.objects('InsuranceProvider'),
    optionKey: 'name',
  },
  [FORM_INPUT_KEYS.IS_ACTIVE]: {
    type: 'toggle',
    initialValue: true,
    key: 'isActive',
    options: [true, false],
    optionLabels: [formInputStrings.yes, formInputStrings.no],
    label: formInputStrings.is_active,
  },
  [FORM_INPUT_KEYS.POLICY_TYPE]: {
    type: 'toggle',
    initialValue: 'personal',
    key: 'type',
    options: ['personal', 'business'],
    optionLabels: [formInputStrings.personal, formInputStrings.business],
    label: formInputStrings.policy_type,
  },
  [FORM_INPUT_KEYS.DISCOUNT_RATE]: {
    type: 'slider',
    initialValue: 25,
    key: 'discountRate',
    maximumValue: 100,
    minimumValue: 0,
    step: 0.1,
    label: formInputStrings.discount_rate,
  },
});

const FORM_CONFIGS = {
  patient: [
    FORM_INPUT_KEYS.FIRST_NAME,
    FORM_INPUT_KEYS.LAST_NAME,
    FORM_INPUT_KEYS.DATE_OF_BIRTH,
    FORM_INPUT_KEYS.GENDER,
    FORM_INPUT_KEYS.EMAIL,
    FORM_INPUT_KEYS.PHONE,
    FORM_INPUT_KEYS.ADDRESS_ONE,
    FORM_INPUT_KEYS.ADDRESS_TWO,
    FORM_INPUT_KEYS.COUNTRY,
  ],
  prescriber: [
    FORM_INPUT_KEYS.FIRST_NAME,
    FORM_INPUT_KEYS.LAST_NAME,
    FORM_INPUT_KEYS.REGISTRATION_CODE,
    FORM_INPUT_KEYS.EMAIL,
    FORM_INPUT_KEYS.PHONE,
    FORM_INPUT_KEYS.ADDRESS_ONE,
    FORM_INPUT_KEYS.ADDRESS_TWO,
  ],
  insurancePolicy: [
    FORM_INPUT_KEYS.POLICY_NUMBER_PERSON,
    FORM_INPUT_KEYS.POLICY_NUMBER_FAMILY,
    FORM_INPUT_KEYS.DISCOUNT_RATE,
    FORM_INPUT_KEYS.POLICY_PROVIDER,
    FORM_INPUT_KEYS.IS_ACTIVE,
    FORM_INPUT_KEYS.POLICY_TYPE,
  ],
};

export const getFormInputConfig = (formName, seedObject) => {
  const formInputConfigs = FORM_INPUT_CONFIGS(seedObject);
  const formConfig = FORM_CONFIGS[formName].map(config => formInputConfigs[config]);

  if (!seedObject) return formConfig;

  return formConfig.map(({ key, initialValue, ...restOfConfig }) => ({
    ...restOfConfig,
    key,
    initialValue: seedObject[key] ?? initialValue,
  }));
};
<commit_msg>Add adjustment of string -> bool<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

import moment from 'moment';
import { UIDatabase } from '../database';

import { formInputStrings } from '../localization';

/**
 * File contains constants and config objects which declaritively define
 * form inputs to be used with `FormControl`.
 *
 * For a particular form, add a field to FIELD_CONFIGS, which will define what
 * form input configs to use.
 *
 * FormInputConfig have the following structure:
 * {
 *     type: 'text' : A ValidationTextInput component
 *     initialValue : The value to seed the form input component with.
 *     key: A key for the underlying record.
 *     isRequired : Indicator whether this input is required to be filled for completion.
 *     label: A label for the form input.
 *     invalidMessage: A message when the input is invalid.
 *     validator: A function to call to determine if the current input is valid.
 * }
 */

const FORM_INPUT_KEYS = {
  FIRST_NAME: 'firstName',
  LAST_NAME: 'lastName',
  CODE: 'code',
  DATE_OF_BIRTH: 'dateOfBirth',
  EMAIL: 'emailAddress',
  PHONE: 'phoneNumber',
  COUNTRY: 'country',
  ADDRESS_ONE: 'addressOne',
  ADDRESS_TWO: 'addressTwo',
  REGISTRATION_CODE: 'registrationCode',
  POLICY_NUMBER_FAMILY: 'policyNumberFamily',
  POLICY_NUMBER_PERSON: 'policyNumberPerson',
  POLICY_PROVIDER: 'insuranceProvider',
  POLICY_TYPE: 'policyType',
  IS_ACTIVE: 'isActive',
  DISCOUNT_RATE: 'discountRate',
};

const FORM_INPUT_CONFIGS = seedObject => ({
  [FORM_INPUT_KEYS.FIRST_NAME]: {
    type: 'text',
    initialValue: '',
    key: 'firstName',
    validator: input => input.length > 0,
    isRequired: true,
    label: formInputStrings.first_name,
    invalidMessage: formInputStrings.must_not_be_empty,
  },
  [FORM_INPUT_KEYS.LAST_NAME]: {
    type: 'text',
    initialValue: '',
    key: 'lastName',
    validator: input => input.length > 0,
    isRequired: true,
    label: formInputStrings.last_name,
    invalidMessage: formInputStrings.must_not_be_empty,
  },
  [FORM_INPUT_KEYS.CODE]: {
    type: 'text',
    initialValue: '',
    key: 'code',
    validator: input => input.length > 0 && input.length < 20,
    isRequired: true,
    label: formInputStrings.code,
    invalidMessage: `${formInputStrings.must_not_be_empty} ${formInputStrings.and} ${formInputStrings.less_than_20_characters}`,
  },
  [FORM_INPUT_KEYS.DATE_OF_BIRTH]: {
    type: 'date',
    initialValue: new Date(),
    key: 'dateOfBirth',
    invalidMessage: formInputStrings.must_be_a_date,
    isRequired: true,
    validator: input => {
      const inputDate = moment(input, 'DD/MM/YYYY', null, true);
      const isValid = inputDate.isValid();
      const isDateOfBirth = inputDate.isSameOrBefore(new Date());
      return isValid && isDateOfBirth;
    },
    label: formInputStrings.date_of_birth,
  },
  [FORM_INPUT_KEYS.GENDER]: {
    type: 'toggle',
    initialValue: false,
    key: 'female',
    options: [true, false],
    optionLabels: [formInputStrings.female, formInputStrings.male],
    label: formInputStrings.gender,
  },
  [FORM_INPUT_KEYS.EMAIL]: {
    type: 'text',
    initialValue: '',
    key: 'emailAddress',
    isRequired: false,
    label: formInputStrings.email,
  },
  [FORM_INPUT_KEYS.PHONE]: {
    type: 'text',
    initialValue: '',
    key: 'phoneNumber',
    isRequired: false,
    label: formInputStrings.phone,
  },
  [FORM_INPUT_KEYS.COUNTRY]: {
    type: 'text',
    initialValue: '',
    key: 'country',
    validator: input => input.length < 20,
    isRequired: false,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_20_characters}`,
    label: formInputStrings.country,
  },
  [FORM_INPUT_KEYS.ADDRESS_ONE]: {
    type: 'text',
    initialValue: '',
    key: 'addressOne',
    validator: input => input.length < 50,
    isRequired: false,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_50_characters}`,
    label: formInputStrings.address_one,
  },
  [FORM_INPUT_KEYS.ADDRESS_TWO]: {
    type: 'text',
    initialValue: '',
    key: 'addressTwo',
    validator: input => input.length < 50,
    isRequired: false,
    label: formInputStrings.address_two,
    invalidMessage: `${formInputStrings.must_be} ${formInputStrings.less_than_50_characters}`,
  },
  [FORM_INPUT_KEYS.REGISTRATION_CODE]: {
    type: 'text',
    initialValue: '',
    key: 'registrationCode',
    validator: input => input.length > 0 && input.length < 50,
    isRequired: true,
    label: formInputStrings.registration_code,
    invalidMessage: formInputStrings.must_be_between_0_and_50,
  },
  [FORM_INPUT_KEYS.POLICY_NUMBER_PERSON]: {
    type: 'text',
    initialValue: '',
    key: 'policyNumberPerson',
    validator: input =>
      input.length > 0 &&
      input.length < 50 &&
      UIDatabase.objects('InsurancePolicy').filtered(
        'policyNumberPerson == $0 && id != $1',
        input,
        seedObject?.id ?? ''
      ).length === 0,
    isRequired: true,
    invalidMessage: formInputStrings.unique_personal_policy,
    label: formInputStrings.personal_policy_number,
  },
  [FORM_INPUT_KEYS.POLICY_NUMBER_FAMILY]: {
    type: 'text',
    initialValue: '',
    key: 'policyNumberFamily',
    validator: input => input.length > 0 && input.length < 50,
    isRequired: true,
    invalidMessage: formInputStrings.must_be_between_0_and_50,
    label: formInputStrings.family_policy_number,
  },
  [FORM_INPUT_KEYS.POLICY_PROVIDER]: {
    type: 'dropdown',
    initialValue: UIDatabase.objects('InsuranceProvider')[0],
    key: 'insuranceProvider',
    label: formInputStrings.policy_provider,
    options: UIDatabase.objects('InsuranceProvider'),
    optionKey: 'name',
  },
  [FORM_INPUT_KEYS.IS_ACTIVE]: {
    type: 'toggle',
    initialValue: true,
    key: 'isActive',
    options: [true, false],
    optionLabels: [formInputStrings.yes, formInputStrings.no],
    label: formInputStrings.is_active,
  },
  [FORM_INPUT_KEYS.POLICY_TYPE]: {
    type: 'toggle',
    initialValue: 'personal',
    key: 'type',
    options: ['personal', 'business'],
    optionLabels: [formInputStrings.personal, formInputStrings.business],
    label: formInputStrings.policy_type,
  },
  [FORM_INPUT_KEYS.DISCOUNT_RATE]: {
    type: 'slider',
    initialValue: 25,
    key: 'discountRate',
    maximumValue: 100,
    minimumValue: 0,
    step: 0.1,
    label: formInputStrings.discount_rate,
  },
});

const FORM_CONFIGS = {
  patient: [
    FORM_INPUT_KEYS.FIRST_NAME,
    FORM_INPUT_KEYS.LAST_NAME,
    FORM_INPUT_KEYS.DATE_OF_BIRTH,
    FORM_INPUT_KEYS.GENDER,
    FORM_INPUT_KEYS.EMAIL,
    FORM_INPUT_KEYS.PHONE,
    FORM_INPUT_KEYS.ADDRESS_ONE,
    FORM_INPUT_KEYS.ADDRESS_TWO,
    FORM_INPUT_KEYS.COUNTRY,
  ],
  prescriber: [
    FORM_INPUT_KEYS.FIRST_NAME,
    FORM_INPUT_KEYS.LAST_NAME,
    FORM_INPUT_KEYS.REGISTRATION_CODE,
    FORM_INPUT_KEYS.EMAIL,
    FORM_INPUT_KEYS.PHONE,
    FORM_INPUT_KEYS.ADDRESS_ONE,
    FORM_INPUT_KEYS.ADDRESS_TWO,
  ],
  insurancePolicy: [
    FORM_INPUT_KEYS.POLICY_NUMBER_PERSON,
    FORM_INPUT_KEYS.POLICY_NUMBER_FAMILY,
    FORM_INPUT_KEYS.DISCOUNT_RATE,
    FORM_INPUT_KEYS.POLICY_PROVIDER,
    FORM_INPUT_KEYS.IS_ACTIVE,
    FORM_INPUT_KEYS.POLICY_TYPE,
  ],
};

export const getFormInputConfig = (formName, seedObject) => {
  const formInputConfigs = FORM_INPUT_CONFIGS(seedObject);
  const formConfig = FORM_CONFIGS[formName].map(config => formInputConfigs[config]);

  if (!seedObject) return formConfig;

  return formConfig.map(({ key, initialValue, ...restOfConfig }) => ({
    ...restOfConfig,
    key,
    initialValue: seedObject[key] ?? initialValue,
  }));
};
<|endoftext|>


<commit_before>function BufferLoader(context, urlList, callback, progresscallback, onError) {
  this.context = context;
  this.urlList = urlList;
  this.onload = callback;
  this.onError = onError;
  this.progresscallback = progresscallback;
  this.bufferList = new Array();
  this.loadCount = 0;
  this.percentageLoadedPerFile = [];
}

BufferLoader.prototype.loadBuffer = function(url, index) {
  var loader = this;
  var unitsLoaded = 0;

  // Load buffer asynchronously
  var request = new XMLHttpRequest();
  request.open("GET", url, true);
  request.responseType = "arraybuffer";
  request.onprogress = function (evt) {
    loader.percentageLoadedPerFile[index] = evt.loaded / evt.total;

    var total = 0;
    for (var i = loader.percentageLoadedPerFile.length - 1; i >= 0; i--) {
      total += loader.percentageLoadedPerFile[i];
    };

    var percentage = total/loader.urlList.length;
    if(loader.progresscallback) loader.progresscallback(percentage);
  };

  request.onload = function() {
    // Asynchronously decode the audio file data in request.response
    loader.context.decodeAudioData(
      request.response,
      function(buffer) {
        if (!buffer) {
          alert('error decoding file data: ' + url);
          return;
        }
        loader.bufferList[index] = buffer;
        if (++loader.loadCount == loader.urlList.length)
          loader.onload(loader.bufferList);
      },
      function(error) {
        if(loader.onError) loader.onError(error);
      }
    );
  }

  request.onerror = function() {
    alert('BufferLoader: XHR error');
  }

  request.send();
}

BufferLoader.prototype.load = function() {
  for (var i = 0; i < this.urlList.length; ++i){
    this.percentageLoadedPerFile.push(0);
    this.loadBuffer(this.urlList[i], i);
  }
}<commit_msg>even more error handling<commit_after>function BufferLoader(context, urlList, callback, progresscallback, onError) {
  this.context = context;
  this.urlList = urlList;
  this.onload = callback;
  this.onError = onError;
  this.progresscallback = progresscallback;
  this.bufferList = new Array();
  this.loadCount = 0;
  this.percentageLoadedPerFile = [];
}

BufferLoader.prototype.loadBuffer = function(url, index) {
  var loader = this;
  var unitsLoaded = 0;

  // Load buffer asynchronously
  var request = new XMLHttpRequest();
  request.open("GET", url, true);
  request.responseType = "arraybuffer";
  request.onprogress = function (evt) {
    loader.percentageLoadedPerFile[index] = evt.loaded / evt.total;

    var total = 0;
    for (var i = loader.percentageLoadedPerFile.length - 1; i >= 0; i--) {
      total += loader.percentageLoadedPerFile[i];
    };

    var percentage = total/loader.urlList.length;
    if(loader.progresscallback) loader.progresscallback(percentage);
  };

  request.onload = function() {
    // Asynchronously decode the audio file data in request.response
    loader.context.decodeAudioData(
      request.response,
      function(buffer) {
        if (!buffer) {
          if(loader.onError) loader.onError(error);
          return;
        }
        loader.bufferList[index] = buffer;
        if (++loader.loadCount == loader.urlList.length)
          loader.onload(loader.bufferList);
      },
      function(error) {
        if(loader.onError) loader.onError(error);
        return;
      }
    );
  }

  request.onerror = function() {
    alert('BufferLoader: XHR error');
  }

  request.send();
}

BufferLoader.prototype.load = function() {
  for (var i = 0; i < this.urlList.length; ++i){
    this.percentageLoadedPerFile.push(0);
    this.loadBuffer(this.urlList[i], i);
  }
}<|endoftext|>


<commit_before>define([
	"dojo/date", // date date.compare
	"dojo/date/locale", // locale.regexp
	"dojo/date/stamp", // stamp.fromISOString stamp.toISOString
	"dojo/_base/declare", // declare
	"dojo/_base/lang", // lang.getObject
	"./RangeBoundTextBox",
	"../_HasDropDown",
	"dojo/text!./templates/DropDownBox.html"
], function(date, locale, stamp, declare, lang, RangeBoundTextBox, _HasDropDown, template){

	// module:
	//		dijit/form/_DateTimeTextBox

	new Date("X"); // workaround for #11279, new Date("") == NaN

	var _DateTimeTextBox = declare("dijit.form._DateTimeTextBox", [RangeBoundTextBox, _HasDropDown], {
		// summary:
		//		Base class for validating, serializable, range-bound date or time text box.

		templateString: template,

		// hasDownArrow: [const] Boolean
		//		Set this textbox to display a down arrow button, to open the drop down list.
		hasDownArrow: true,

		/*=====
		// constraints: _DateTimeTextBox.__Constraints
		//		Despite the name, this parameter specifies both constraints on the input
		//		(including starting/ending dates/times allowed) as well as
		//		formatting options like whether the date is displayed in long (ex: December 25, 2005)
		//		or short (ex: 12/25/2005) format.  See `dijit.form._DateTimeTextBox.__Constraints` for details.
		constraints: {},
		======*/

		// Override ValidationTextBox.pattern.... we use a reg-ex generating function rather
		// than a straight regexp to deal with locale  (plus formatting options too?)
		pattern: locale.regexp,

		// datePackage: String
		//		JavaScript namespace to find calendar routines.	 If unspecified, uses Gregorian calendar routines
		//		at dojo/date and dojo/date/locale.
		datePackage: "",
		//		TODO: for 2.0, replace datePackage with dateModule and dateLocalModule attributes specifying MIDs,
		//		or alternately just get rid of this completely and tell user to use module ID remapping
		//		via require

		postMixInProperties: function(){
			this.inherited(arguments);
			this._set("type", "text"); // in case type="date"|"time" was specified which messes up parse/format
		},

		// Override _FormWidget.compare() to work for dates/times
		compare: function(/*Date*/ val1, /*Date*/ val2){
			var isInvalid1 = this._isInvalidDate(val1);
			var isInvalid2 = this._isInvalidDate(val2);
			return isInvalid1 ? (isInvalid2 ? 0 : -1) : (isInvalid2 ? 1 : date.compare(val1, val2, this._selector));
		},

		// flag to _HasDropDown to make drop down Calendar width == <input> width
		forceWidth: true,

		format: function(/*Date*/ value, /*locale.__FormatOptions*/ constraints){
			// summary:
			//		Formats the value as a Date, according to specified locale (second argument)
			// tags:
			//		protected
			if(!value){ return ''; }
			return this.dateLocaleModule.format(value, constraints);
		},

		"parse": function(/*String*/ value, /*locale.__FormatOptions*/ constraints){
			// summary:
			//		Parses as string as a Date, according to constraints
			// tags:
			//		protected

			return this.dateLocaleModule.parse(value, constraints) || (this._isEmpty(value) ? null : undefined);	 // Date
		},

		// Overrides ValidationTextBox.serialize() to serialize a date in canonical ISO format.
		serialize: function(/*anything*/ val, /*Object?*/ options){
			if(val.toGregorian){
				val = val.toGregorian();
			}
			return stamp.toISOString(val, options);
		},

		// dropDownDefaultValue: Date
		//		The default value to focus in the popupClass widget when the textbox value is empty.
		dropDownDefaultValue : new Date(),

		// value: Date
		//		The value of this widget as a JavaScript Date object.  Use get("value") / set("value", val) to manipulate.
		//		When passed to the parser in markup, must be specified according to `dojo.date.stamp.fromISOString`
		value: new Date(""),	// value.toString()="NaN"

		_blankValue: null,	// used by filter() when the textbox is blank

		// popupClass: [protected extension] String
		//		Name of the popup widget class used to select a date/time.
		//		Subclasses should specify this.
		popupClass: "", // default is no popup = text only

		// _selector: [protected extension] String
		//		Specifies constraints.selector passed to dojo.date functions, should be either
		//		"date" or "time".
		//		Subclass must specify this.
		_selector: "",

		constructor: function(params /*===== , srcNodeRef =====*/){
			// summary:
			//		Create the widget.
			// params: Object|null
			//		Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
			//		and functions, typically callbacks like onClick.
			// srcNodeRef: DOMNode|String?
			//		If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree

			this.dateModule = params.datePackage ? lang.getObject(params.datePackage, false) : date;
			this.dateClassObj = this.dateModule.Date || Date;
			this.dateLocaleModule = params.datePackage ? lang.getObject(params.datePackage+".locale", false) : locale;
			this._set('pattern', this.dateLocaleModule.regexp);
			this._invalidDate = this.constructor.prototype.value.toString();
		},

		buildRendering: function(){
			this.inherited(arguments);

			if(!this.hasDownArrow){
				this._buttonNode.style.display = "none";
			}

			// If hasDownArrow is false, we basically just want to treat the whole widget as the
			// button.
			if(!this.hasDownArrow){
				this._buttonNode = this.domNode;
				this.baseClass += " dijitComboBoxOpenOnClick";
			}
		},

		_setConstraintsAttr: function(/*Object*/ constraints){
			constraints.selector = this._selector;
			constraints.fullYear = true; // see #5465 - always format with 4-digit years
			var fromISO = stamp.fromISOString;
			if(typeof constraints.min == "string"){ constraints.min = fromISO(constraints.min); }
			if(typeof constraints.max == "string"){ constraints.max = fromISO(constraints.max); }
			this.inherited(arguments);
		},

		_isInvalidDate: function(/*Date*/ value){
			// summary:
			//		Runs various tests on the value, checking for invalid conditions
			// tags:
			//		private
			return !value || isNaN(value) || typeof value != "object" || value.toString() == this._invalidDate;
		},

		_setValueAttr: function(/*Date|String*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){
			// summary:
			//		Sets the date on this textbox. Note: value can be a JavaScript Date literal or a string to be parsed.
			if(value !== undefined){
				if(typeof value == "string"){
					value = stamp.fromISOString(value);
				}
				if(this._isInvalidDate(value)){
					value = null;
				}
				if(value instanceof Date && !(this.dateClassObj instanceof Date)){
					value = new this.dateClassObj(value);
				}
			}
			this.inherited(arguments);
			if(this.value instanceof Date){
				this.filterString = "";
			}
			if(this.dropDown){
				this.dropDown.set('value', value, false);
			}
		},

		_set: function(attr, value){
			// Avoid spurious watch() notifications when value is changed to new Date object w/the same value
			if(attr == "value" && this.value instanceof Date && this.compare(value, this.value) == 0){
				return;
			}
			this.inherited(arguments);
		},

		_setDropDownDefaultValueAttr: function(/*Date*/ val){
			if(this._isInvalidDate(val)){
				// convert null setting into today's date, since there needs to be *some* default at all times.
				 val = new this.dateClassObj();
			}
			this.dropDownDefaultValue = val;
		},

		openDropDown: function(/*Function*/ callback){
			// rebuild drop down every time, so that constraints get copied (#6002)
			if(this.dropDown){
				this.dropDown.destroy();
			}
			var PopupProto = lang.isString(this.popupClass) ? lang.getObject(this.popupClass, false) : this.popupClass,
				textBox = this,
				value = this.get("value");
			this.dropDown = new PopupProto({
				onChange: function(value){
					// this will cause InlineEditBox and other handlers to do stuff so make sure it's last
					textBox.set('value', value, true);
				},
				id: this.id + "_popup",
				dir: textBox.dir,
				lang: textBox.lang,
				value: value,
				currentFocus: !this._isInvalidDate(value) ? value : this.dropDownDefaultValue,
				constraints: textBox.constraints,
				filterString: textBox.filterString, // for TimeTextBox, to filter times shown
				datePackage: textBox.params.datePackage,
				isDisabledDate: function(/*Date*/ date){
					// summary:
					//		disables dates outside of the min/max of the _DateTimeTextBox
					return !textBox.rangeCheck(date, textBox.constraints);
				}
			});

			this.inherited(arguments);
		},

		_getDisplayedValueAttr: function(){
			return this.textbox.value;
		},

		_setDisplayedValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange){
			this._setValueAttr(this.parse(value, this.constraints), priorityChange, value);
		}
	});

	/*=====
	 _DateTimeTextBox.__Constraints = declare([RangeBoundTextBox.__Constraints, locale.__FormatOptions], {
		 // summary:
		 //		Specifies both the rules on valid/invalid values (first/last date/time allowed),
		 //		and also formatting options for how the date/time is displayed.
		 // example:
		 //		To restrict to dates within 2004, displayed in a long format like "December 25, 2005":
		 //	|		{min:'2004-01-01',max:'2004-12-31', formatLength:'long'}
	 });
	 =====*/

	return _DateTimeTextBox;
});
<commit_msg>Fix hover effect for down arrow of !DateTextBox and !TimeTextBox, refs #15196 !strict.<commit_after>define([
	"dojo/date", // date date.compare
	"dojo/date/locale", // locale.regexp
	"dojo/date/stamp", // stamp.fromISOString stamp.toISOString
	"dojo/_base/declare", // declare
	"dojo/_base/lang", // lang.getObject
	"./RangeBoundTextBox",
	"../_HasDropDown",
	"dojo/text!./templates/DropDownBox.html"
], function(date, locale, stamp, declare, lang, RangeBoundTextBox, _HasDropDown, template){

	// module:
	//		dijit/form/_DateTimeTextBox

	new Date("X"); // workaround for #11279, new Date("") == NaN

	var _DateTimeTextBox = declare("dijit.form._DateTimeTextBox", [RangeBoundTextBox, _HasDropDown], {
		// summary:
		//		Base class for validating, serializable, range-bound date or time text box.

		templateString: template,

		// hasDownArrow: [const] Boolean
		//		Set this textbox to display a down arrow button, to open the drop down list.
		hasDownArrow: true,

		// Set classes like dijitDownArrowButtonHover depending on mouse action over button node
		cssStateNodes: {
			"_buttonNode": "dijitDownArrowButton"
		},

		/*=====
		// constraints: _DateTimeTextBox.__Constraints
		//		Despite the name, this parameter specifies both constraints on the input
		//		(including starting/ending dates/times allowed) as well as
		//		formatting options like whether the date is displayed in long (ex: December 25, 2005)
		//		or short (ex: 12/25/2005) format.  See `dijit.form._DateTimeTextBox.__Constraints` for details.
		constraints: {},
		======*/

		// Override ValidationTextBox.pattern.... we use a reg-ex generating function rather
		// than a straight regexp to deal with locale  (plus formatting options too?)
		pattern: locale.regexp,

		// datePackage: String
		//		JavaScript namespace to find calendar routines.	 If unspecified, uses Gregorian calendar routines
		//		at dojo/date and dojo/date/locale.
		datePackage: "",
		//		TODO: for 2.0, replace datePackage with dateModule and dateLocalModule attributes specifying MIDs,
		//		or alternately just get rid of this completely and tell user to use module ID remapping
		//		via require

		postMixInProperties: function(){
			this.inherited(arguments);
			this._set("type", "text"); // in case type="date"|"time" was specified which messes up parse/format
		},

		// Override _FormWidget.compare() to work for dates/times
		compare: function(/*Date*/ val1, /*Date*/ val2){
			var isInvalid1 = this._isInvalidDate(val1);
			var isInvalid2 = this._isInvalidDate(val2);
			return isInvalid1 ? (isInvalid2 ? 0 : -1) : (isInvalid2 ? 1 : date.compare(val1, val2, this._selector));
		},

		// flag to _HasDropDown to make drop down Calendar width == <input> width
		forceWidth: true,

		format: function(/*Date*/ value, /*locale.__FormatOptions*/ constraints){
			// summary:
			//		Formats the value as a Date, according to specified locale (second argument)
			// tags:
			//		protected
			if(!value){ return ''; }
			return this.dateLocaleModule.format(value, constraints);
		},

		"parse": function(/*String*/ value, /*locale.__FormatOptions*/ constraints){
			// summary:
			//		Parses as string as a Date, according to constraints
			// tags:
			//		protected

			return this.dateLocaleModule.parse(value, constraints) || (this._isEmpty(value) ? null : undefined);	 // Date
		},

		// Overrides ValidationTextBox.serialize() to serialize a date in canonical ISO format.
		serialize: function(/*anything*/ val, /*Object?*/ options){
			if(val.toGregorian){
				val = val.toGregorian();
			}
			return stamp.toISOString(val, options);
		},

		// dropDownDefaultValue: Date
		//		The default value to focus in the popupClass widget when the textbox value is empty.
		dropDownDefaultValue : new Date(),

		// value: Date
		//		The value of this widget as a JavaScript Date object.  Use get("value") / set("value", val) to manipulate.
		//		When passed to the parser in markup, must be specified according to `dojo.date.stamp.fromISOString`
		value: new Date(""),	// value.toString()="NaN"

		_blankValue: null,	// used by filter() when the textbox is blank

		// popupClass: [protected extension] String
		//		Name of the popup widget class used to select a date/time.
		//		Subclasses should specify this.
		popupClass: "", // default is no popup = text only

		// _selector: [protected extension] String
		//		Specifies constraints.selector passed to dojo.date functions, should be either
		//		"date" or "time".
		//		Subclass must specify this.
		_selector: "",

		constructor: function(params /*===== , srcNodeRef =====*/){
			// summary:
			//		Create the widget.
			// params: Object|null
			//		Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
			//		and functions, typically callbacks like onClick.
			// srcNodeRef: DOMNode|String?
			//		If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree

			this.dateModule = params.datePackage ? lang.getObject(params.datePackage, false) : date;
			this.dateClassObj = this.dateModule.Date || Date;
			this.dateLocaleModule = params.datePackage ? lang.getObject(params.datePackage+".locale", false) : locale;
			this._set('pattern', this.dateLocaleModule.regexp);
			this._invalidDate = this.constructor.prototype.value.toString();
		},

		buildRendering: function(){
			this.inherited(arguments);

			if(!this.hasDownArrow){
				this._buttonNode.style.display = "none";
			}

			// If hasDownArrow is false, we basically just want to treat the whole widget as the
			// button.
			if(!this.hasDownArrow){
				this._buttonNode = this.domNode;
				this.baseClass += " dijitComboBoxOpenOnClick";
			}
		},

		_setConstraintsAttr: function(/*Object*/ constraints){
			constraints.selector = this._selector;
			constraints.fullYear = true; // see #5465 - always format with 4-digit years
			var fromISO = stamp.fromISOString;
			if(typeof constraints.min == "string"){ constraints.min = fromISO(constraints.min); }
			if(typeof constraints.max == "string"){ constraints.max = fromISO(constraints.max); }
			this.inherited(arguments);
		},

		_isInvalidDate: function(/*Date*/ value){
			// summary:
			//		Runs various tests on the value, checking for invalid conditions
			// tags:
			//		private
			return !value || isNaN(value) || typeof value != "object" || value.toString() == this._invalidDate;
		},

		_setValueAttr: function(/*Date|String*/ value, /*Boolean?*/ priorityChange, /*String?*/ formattedValue){
			// summary:
			//		Sets the date on this textbox. Note: value can be a JavaScript Date literal or a string to be parsed.
			if(value !== undefined){
				if(typeof value == "string"){
					value = stamp.fromISOString(value);
				}
				if(this._isInvalidDate(value)){
					value = null;
				}
				if(value instanceof Date && !(this.dateClassObj instanceof Date)){
					value = new this.dateClassObj(value);
				}
			}
			this.inherited(arguments);
			if(this.value instanceof Date){
				this.filterString = "";
			}
			if(this.dropDown){
				this.dropDown.set('value', value, false);
			}
		},

		_set: function(attr, value){
			// Avoid spurious watch() notifications when value is changed to new Date object w/the same value
			if(attr == "value" && this.value instanceof Date && this.compare(value, this.value) == 0){
				return;
			}
			this.inherited(arguments);
		},

		_setDropDownDefaultValueAttr: function(/*Date*/ val){
			if(this._isInvalidDate(val)){
				// convert null setting into today's date, since there needs to be *some* default at all times.
				 val = new this.dateClassObj();
			}
			this.dropDownDefaultValue = val;
		},

		openDropDown: function(/*Function*/ callback){
			// rebuild drop down every time, so that constraints get copied (#6002)
			if(this.dropDown){
				this.dropDown.destroy();
			}
			var PopupProto = lang.isString(this.popupClass) ? lang.getObject(this.popupClass, false) : this.popupClass,
				textBox = this,
				value = this.get("value");
			this.dropDown = new PopupProto({
				onChange: function(value){
					// this will cause InlineEditBox and other handlers to do stuff so make sure it's last
					textBox.set('value', value, true);
				},
				id: this.id + "_popup",
				dir: textBox.dir,
				lang: textBox.lang,
				value: value,
				currentFocus: !this._isInvalidDate(value) ? value : this.dropDownDefaultValue,
				constraints: textBox.constraints,
				filterString: textBox.filterString, // for TimeTextBox, to filter times shown
				datePackage: textBox.params.datePackage,
				isDisabledDate: function(/*Date*/ date){
					// summary:
					//		disables dates outside of the min/max of the _DateTimeTextBox
					return !textBox.rangeCheck(date, textBox.constraints);
				}
			});

			this.inherited(arguments);
		},

		_getDisplayedValueAttr: function(){
			return this.textbox.value;
		},

		_setDisplayedValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange){
			this._setValueAttr(this.parse(value, this.constraints), priorityChange, value);
		}
	});

	/*=====
	 _DateTimeTextBox.__Constraints = declare([RangeBoundTextBox.__Constraints, locale.__FormatOptions], {
		 // summary:
		 //		Specifies both the rules on valid/invalid values (first/last date/time allowed),
		 //		and also formatting options for how the date/time is displayed.
		 // example:
		 //		To restrict to dates within 2004, displayed in a long format like "December 25, 2005":
		 //	|		{min:'2004-01-01',max:'2004-12-31', formatLength:'long'}
	 });
	 =====*/

	return _DateTimeTextBox;
});
<|endoftext|>