<commit_before>/*
 * A bootstrap select box with pure angular backing
 * Allows arbitrary values from <input> or from a list of values
 * Valid attributes: ng-model,
 *
 * fzSelectItems:       Expects an Array or Function
 *                      The source list for the select component. If it is a
 *                      function, the return is expected to be either an
 *                      array or a $q promise.
 *                      If the source list is a $q, then the list will not sort
 *                      locally.
 *
 * fzRefresh:           Expects a function
 *                      Requires fzRefreshRate
 *                      signature should look something like:
 *                      function refresh(searchString)
 *                      If set, the component will call the supplied function
 *                      when it comes time to refresh the list.
 *
 * fzRefreshRate:       Expects integer
 *                      Requires fzRefresh
 *                      How often the component will call the refresh function
 *
 * fzMatchAttribute:    Expects string
 *                      if the list contains objects, this is the attribute name
 *                      of the desired value EG: if your list looks like
 *                      [ { a: 'val' }, .... ] setting fzMatchAttribute to 'a',
 *                      the component will return the value of a of the selected
 *                      item.
 *
 * fzReturnAttribute:   Expects string
 *                      Useful if you have a list like [ {name: '', email: ''}
 *                      and want to search by name, but return the email
 *
 * fzIncludeNullOption: Expects boolean
 *                      Set to true if you want a null item prepended to the
 *                      result list to allow for a blank option
 *
 * fzReturnObjects:     Expects boolean
 *                      Component will return objects, even for arbitrary input.
 *                      This is useful in conjunction with fzReturnAttribute.
 *                      If you are searching a list of contacts by name, but
 *                      also want to be able to return some arbitrary email,
 *                      the component will return an object like:
 *                      { email: 'asd@asd.com' } instead of just a string value.
 */

angular.module( "fzSelect", [] )
.directive( "fzSelect", ['$filter', '$timeout', '$parse', '$interval',
  function($filter, $timeout, $parse, $interval){
    return {
      restrict: 'EA',
      scope: true,
      template:
'
'+
  '<input class="fz-select-input" ng-model="searchString" ' +
    'ng-keydown="inputKeyDown($event)"></input>'+
  '' +
    '<button class="fz-select-button" ng-click="showAll()" > ' +
      '&#9660 ' +
      '&#9650 ' +
    '</button>'+
  '' +
'
'+
'
 '+
  '
 '+
    '
{{getItemDisplayString(item)}}
'+
  '
' +
'
',

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

        function initilizeError(message){
          element.html('Error initlizing component.');
          throw message;
        };

        angular.element(element).addClass('fz-select-component');

        var itemsGetter = $parse(attrs.fzSelectItems);
        var valueGetter = $parse(attrs.ngModel);
        var valueSetter = valueGetter.assign;

        $scope.orderObject = {attribute: '', reverse: false};
        var itemAttributeName = null;
        var itemAttributeGetter = null;
        if( attrs.hasOwnProperty('fzMatchAttribute') ){
          itemAttributeGetter = $parse(attrs.fzMatchAttribute);
          itemAttributeName = attrs.fzMatchAttribute;
          $scope.orderObject.attribute = attrs.fzMatchAttribute;
        }

        var itemReturnAttributeName = "value";
        var itemReturnAttributeGetter = null;
        if( attrs.hasOwnProperty('fzReturnAttribute') ){
          itemReturnAttributeGetter = $parse(attrs.fzReturnAttribute);
          itemReturnAttributeName = attrs.fzReturnAttribute;
        } else if( attrs.hasOwnProperty('fzMatchAttribute') ){
          itemReturnAttributeGetter = $parse(attrs.fzMatchAttribute);
          itemReturnAttributeName = attrs.fzMatchAttribute;
        }

        var returnObjects = false;
        if( attrs.hasOwnProperty("fzReturnObjects") ){
          returnObjects = attrs.fzReturnObjects == "true";
        }

        //Either refreshRate and Refresh are set, or neither
        //are set
        if(!(
            (attrs.hasOwnProperty("fzRefreshRate") &&
             attrs.hasOwnProperty("fzRefresh")) ||
            (!attrs.hasOwnProperty("fzRefreshRate") &&
             !attrs.hasOwnProperty("fzRefresh")))
        ){
          initilizeError(
            "fzSelect have both fzRefreshRate and fzRefresh or " +
            "have neither set." )
        }

        // item list is async
        var isAsync =  attrs.hasOwnProperty("fzRefresh");

        var refreshRate = null;
        if(attrs.hasOwnProperty("fzRefreshRate"))
          refreshRate = parseInt( attrs.fzRefreshRate );

        var refreshFunction = null;
        var refreshPromise = null;
        if(attrs.hasOwnProperty("fzRefresh"))
          refreshFunction = $parse(attrs.fzRefresh);

        var includeNullOption = false;
        if(attrs.hasOwnProperty("fzIncludeNullOption"))
          includeNullOption = attrs.fzIncludeNullOption == "true";

        $scope.selectedRowIndex = 0;

        $scope.filteredItems = [];
        $scope.resultsVisible = {value: false};
        $scope.selectedValue = null;
        var valueWasSelected = false;

        //initilize the search string value
        function initSearchString(){
          //if there is no return attribute specified, return the value
          if(itemReturnAttributeGetter == null){
            $scope.searchString = valueGetter($scope);
          //find the object that matches the return value
          } else if(itemAttributeGetter != null){

            var itemList = itemsGetter($scope);
            var value = valueGetter($scope);
            for(var i=0; i < itemList.length; i++){
              if( itemList[i][itemReturnAttributeName] == value){
                $scope.searchString = itemList[i][itemAttributeName];
                $scope.selectedValue = itemList[i];
                break;
              }
            }
          } else {
            $scope.searchString = valueGetter();
          }
        }

        $scope.showResults = function(show){
          $timeout(function(){
            $scope.resultsVisible.value = show;
          });
        };

        $scope.getItemDisplayString = function(item){
          if( itemAttributeGetter != null  ){
            return itemAttributeGetter(item);
          }else{
            return item;
          }
        };

        $scope.showAll = function(){
          //if the results are visible, don't show
          if($scope.resultsVisible.value)
            $scope.showResults(false);
          else
            $scope.filterItems(true);
        };

        $scope.updateSourceValue = function(){
          if($scope.selectedValue != null){
            if( itemReturnAttributeGetter != null  && !returnObjects){
              valueSetter( $scope,
                  itemReturnAttributeGetter($scope.selectedValue));
            } else {
              valueSetter($scope, $scope.selectedValue);
            }
          } else {
            if(returnObjects){
              var returnObject = {}
              returnObject[itemReturnAttributeName] = $scope.searchString;
              valueSetter($scope, returnObject);
            } else {
              valueSetter($scope, $scope.searchString);
            }
          }
        };

        $scope.resultsKeydown = function($event, item){
          function moveResults(isUp){
            $event.preventDefault();
            if( isUp )
              angular.element(angular.element($event.target).prev()).focus();
            else
              angular.element(angular.element($event.target).next()).focus();
          }
          switch($event.keyCode){
            case 40:
              moveResults(false);
              break;
            case 38:
              moveResults(true);
              break;
            case 13:
              $scope.resultItemClicked(item);
            default:
              return;
          }
        }

        function getResultsContainer(){
          var children = angular.element(element).children();
          var container = null;
          angular.forEach(children, function(child){
            if( angular.element(child).hasClass( 'fz-select-results-wrapper' ) )
              container = angular.element(child).children()[0];
          });
          return container;
        }

        $scope.resultFocused = function($event, item){
          $scope.selectedRowIndex = $scope.filteredItems.indexOf(item);
        }

        var focusResults = function($event){
          $scope.resultsVisible.value = true;
          if($event != undefined)
            $event.preventDefault();
          $timeout(function(){
            var container = getResultsContainer()
            if( container != null )
              angular.element(angular.element(container).children()[0]).focus();
          });
        };

        $scope.inputKeyDown = function($event){

          switch($event.keyCode){
            case 40:
              focusResults($event);
              break;
            case 38:
              focusResults($event);
              break;

            default:
              return;
          }
        };

        $scope.resultItemClicked = function(item){
          $scope.selectedValue = item;
          $scope.searchString = $scope.getItemDisplayString(item);
          valueWasSelected = true;
          $scope.updateSourceValue();
          $scope.showResults(false);
        };

        function orderItems(items){
          return $filter('orderBy')(
              items,
              $scope.orderObject.attribute,
              $scope.orderObject.reverse);
        }

        function getItems(){
          return itemsGetter($scope);
        }

        // update items, should not be used for async
        function updateItems(onlyOrder){

          var tempList = getItems();

          //if we are filtering, put it
          if(!onlyOrder)
            tempList = $filter('filter')(tempList, $scope.searchString);

          //order the items if it's not async
          tempList = orderItems(tempList);

          if(includeNullOption)
            tempList.unshift(null);

          $scope.filteredItems = tempList;
        }

        var initialFilter = true;
        /*
         * Filter the item list based on the search string
         * This method is the only place showResults(true) should be called
         * onlyOrder: boolean; only order the list, do not filter
         */
        $scope.filterItems = function(onlyOrder){

          var searchObject = {};
          if( itemAttributeName != null )
            searchObject[itemAttributeName] = $scope.searchString;
          else
            searchObject = $scope.searchString;

          if( !isAsync )
            updateItems(onlyOrder);

          //Do not show results if the search string is set to null and
          //we're not only ordering
          if( $scope.searchString == null && !onlyOrder ){
            $scope.showResults(false);
          } else {
            if( onlyOrder){
              $scope.showResults(true);
            //If it's the initial filter, do not show items.
            //This prevents the results from being shown when the component
            //is initilized
            } else if( $scope.searchString.length > 0
                && !$scope.resultsVisible.value && !initialFilter ) {
              $scope.showResults(true);
            } else if( $scope.searchString.length == 0 &&
                       $scope.resultsVisible.value ){
              $scope.showResults(false);
            }
            initialFilter = false;
          }
        };

        var searchStringChanged = false;

        $scope.$watch('searchString', function(){
          searchStringChanged = true;
          $scope.filterItems();
          $scope.selectedRowIndex = 0;
          if( !valueWasSelected )
            $scope.selectedValue = null;
          else
            valueWasSelected = false;
          $scope.updateSourceValue();
        }, true);

        $scope.$watch('selectedRowIndex', function(){
          if($scope.selectedRowIndex < 0 ||
            $scope.selectedRowIndex > ($scope.filteredItems.length - 1))
            $scope.selectedRowIndex = 0;
        }, true);

        $scope.$on('$destroy', function(){
          $interval.cancel(refreshPromise);
        });

        function callRefreshFunction(searchString){
          refreshFunction($scope)(searchString);
        };

        // code to run once setup is finished
        function initComponent(){
          if(isAsync){
            $scope.searchString = valueGetter($scope);
            callRefreshFunction($scope.searchString);
            refreshPromise = $interval(function(){
              if(searchStringChanged)
                callRefreshFunction($scope.searchString);
              searchStringChanged = false;
              $scope.filteredItems = getItems();
            }, refreshRate);

          } else {
            initSearchString();
          }
        }
        initComponent();
      }
    }
  }
])
<commit_msg>Preventing source model update before component initialized<commit_after>/*
 * A bootstrap select box with pure angular backing
 * Allows arbitrary values from <input> or from a list of values
 * Valid attributes: ng-model,
 *
 * fzSelectItems:       Expects an Array or Function
 *                      The source list for the select component. If it is a
 *                      function, the return is expected to be either an
 *                      array or a $q promise.
 *                      If the source list is a $q, then the list will not sort
 *                      locally.
 *
 * fzRefresh:           Expects a function
 *                      Requires fzRefreshRate
 *                      signature should look something like:
 *                      function refresh(searchString)
 *                      If set, the component will call the supplied function
 *                      when it comes time to refresh the list.
 *
 * fzRefreshRate:       Expects integer
 *                      Requires fzRefresh
 *                      How often the component will call the refresh function
 *
 * fzMatchAttribute:    Expects string
 *                      if the list contains objects, this is the attribute name
 *                      of the desired value EG: if your list looks like
 *                      [ { a: 'val' }, .... ] setting fzMatchAttribute to 'a',
 *                      the component will return the value of a of the selected
 *                      item.
 *
 * fzReturnAttribute:   Expects string
 *                      Useful if you have a list like [ {name: '', email: ''}
 *                      and want to search by name, but return the email
 *
 * fzIncludeNullOption: Expects boolean
 *                      Set to true if you want a null item prepended to the
 *                      result list to allow for a blank option
 *
 * fzReturnObjects:     Expects boolean
 *                      Component will return objects, even for arbitrary input.
 *                      This is useful in conjunction with fzReturnAttribute.
 *                      If you are searching a list of contacts by name, but
 *                      also want to be able to return some arbitrary email,
 *                      the component will return an object like:
 *                      { email: 'asd@asd.com' } instead of just a string value.
 */

angular.module( "fzSelect", [] )
.directive( "fzSelect", ['$filter', '$timeout', '$parse', '$interval',
  function($filter, $timeout, $parse, $interval){
    return {
      restrict: 'EA',
      scope: true,
      template:
'
'+
  '<input class="fz-select-input" ng-model="searchString" ' +
    'ng-keydown="inputKeyDown($event)"></input>'+
  '' +
    '<button class="fz-select-button" ng-click="showAll()" > ' +
      '&#9660 ' +
      '&#9650 ' +
    '</button>'+
  '' +
'
'+
'
 '+
  '
 '+
    '
{{getItemDisplayString(item)}}
'+
  '
' +
'
',

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

        function initilizeError(message){
          element.html('Error initlizing component.');
          throw message;
        };

        angular.element(element).addClass('fz-select-component');

        var itemsGetter = $parse(attrs.fzSelectItems);
        var valueGetter = $parse(attrs.ngModel);
        var valueSetter = valueGetter.assign;

        $scope.orderObject = {attribute: '', reverse: false};
        var itemAttributeName = null;
        var itemAttributeGetter = null;
        if( attrs.hasOwnProperty('fzMatchAttribute') ){
          itemAttributeGetter = $parse(attrs.fzMatchAttribute);
          itemAttributeName = attrs.fzMatchAttribute;
          $scope.orderObject.attribute = attrs.fzMatchAttribute;
        }

        var itemReturnAttributeName = "value";
        var itemReturnAttributeGetter = null;
        if( attrs.hasOwnProperty('fzReturnAttribute') ){
          itemReturnAttributeGetter = $parse(attrs.fzReturnAttribute);
          itemReturnAttributeName = attrs.fzReturnAttribute;
        } else if( attrs.hasOwnProperty('fzMatchAttribute') ){
          itemReturnAttributeGetter = $parse(attrs.fzMatchAttribute);
          itemReturnAttributeName = attrs.fzMatchAttribute;
        }

        var returnObjects = false;
        if( attrs.hasOwnProperty("fzReturnObjects") ){
          returnObjects = attrs.fzReturnObjects == "true";
        }

        //Either refreshRate and Refresh are set, or neither
        //are set
        if(!(
            (attrs.hasOwnProperty("fzRefreshRate") &&
             attrs.hasOwnProperty("fzRefresh")) ||
            (!attrs.hasOwnProperty("fzRefreshRate") &&
             !attrs.hasOwnProperty("fzRefresh")))
        ){
          initilizeError(
            "fzSelect have both fzRefreshRate and fzRefresh or " +
            "have neither set." )
        }

        // item list is async
        var isAsync =  attrs.hasOwnProperty("fzRefresh");

        var refreshRate = null;
        if(attrs.hasOwnProperty("fzRefreshRate"))
          refreshRate = parseInt( attrs.fzRefreshRate );

        var refreshFunction = null;
        var refreshPromise = null;
        if(attrs.hasOwnProperty("fzRefresh"))
          refreshFunction = $parse(attrs.fzRefresh);

        var includeNullOption = false;
        if(attrs.hasOwnProperty("fzIncludeNullOption"))
          includeNullOption = attrs.fzIncludeNullOption == "true";

        $scope.selectedRowIndex = 0;

        $scope.filteredItems = [];
        $scope.resultsVisible = {value: false};
        $scope.selectedValue = null;
        var valueWasSelected = false;

        //initilize the search string value
        function initSearchString(){
          //if there is no return attribute specified, return the value
          if(itemReturnAttributeGetter == null){
            $scope.searchString = valueGetter($scope);
          //find the object that matches the return value
          } else if(itemAttributeGetter != null){

            var itemList = itemsGetter($scope);
            var value = valueGetter($scope);
            for(var i=0; i < itemList.length; i++){
              if( itemList[i][itemReturnAttributeName] == value){
                $scope.searchString = itemList[i][itemAttributeName];
                $scope.selectedValue = itemList[i];
                break;
              }
            }
          } else {
            $scope.searchString = valueGetter();
          }
        }

        $scope.showResults = function(show){
          $timeout(function(){
            $scope.resultsVisible.value = show;
          });
        };

        $scope.getItemDisplayString = function(item){
          if( itemAttributeGetter != null  ){
            return itemAttributeGetter(item);
          }else{
            return item;
          }
        };

        $scope.showAll = function(){
          //if the results are visible, don't show
          if($scope.resultsVisible.value)
            $scope.showResults(false);
          else
            $scope.filterItems(true);
        };

        $scope.updateSourceValue = function(){
          if($scope.selectedValue != null){
            if( itemReturnAttributeGetter != null  && !returnObjects){
              valueSetter( $scope,
                  itemReturnAttributeGetter($scope.selectedValue));
            } else {
              valueSetter($scope, $scope.selectedValue);
            }
          } else {
            if(returnObjects){
              var returnObject = {}
              returnObject[itemReturnAttributeName] = $scope.searchString;
              valueSetter($scope, returnObject);
            } else {
              valueSetter($scope, $scope.searchString);
            }
          }
        };

        $scope.resultsKeydown = function($event, item){
          function moveResults(isUp){
            $event.preventDefault();
            if( isUp )
              angular.element(angular.element($event.target).prev()).focus();
            else
              angular.element(angular.element($event.target).next()).focus();
          }
          switch($event.keyCode){
            case 40:
              moveResults(false);
              break;
            case 38:
              moveResults(true);
              break;
            case 13:
              $scope.resultItemClicked(item);
            default:
              return;
          }
        }

        function getResultsContainer(){
          var children = angular.element(element).children();
          var container = null;
          angular.forEach(children, function(child){
            if( angular.element(child).hasClass( 'fz-select-results-wrapper' ) )
              container = angular.element(child).children()[0];
          });
          return container;
        }

        $scope.resultFocused = function($event, item){
          $scope.selectedRowIndex = $scope.filteredItems.indexOf(item);
        }

        var focusResults = function($event){
          $scope.resultsVisible.value = true;
          if($event != undefined)
            $event.preventDefault();
          $timeout(function(){
            var container = getResultsContainer()
            if( container != null )
              angular.element(angular.element(container).children()[0]).focus();
          });
        };

        $scope.inputKeyDown = function($event){

          switch($event.keyCode){
            case 40:
              focusResults($event);
              break;
            case 38:
              focusResults($event);
              break;

            default:
              return;
          }
        };

        $scope.resultItemClicked = function(item){
          $scope.selectedValue = item;
          $scope.searchString = $scope.getItemDisplayString(item);
          valueWasSelected = true;
          $scope.updateSourceValue();
          $scope.showResults(false);
        };

        function orderItems(items){
          return $filter('orderBy')(
              items,
              $scope.orderObject.attribute,
              $scope.orderObject.reverse);
        }

        function getItems(){
          return itemsGetter($scope);
        }

        // update items, should not be used for async
        function updateItems(onlyOrder){

          var tempList = getItems();

          //if we are filtering, put it
          if(!onlyOrder)
            tempList = $filter('filter')(tempList, $scope.searchString);

          //order the items if it's not async
          tempList = orderItems(tempList);

          if(includeNullOption)
            tempList.unshift(null);

          $scope.filteredItems = tempList;
        }

        var initialFilter = true;
        /*
         * Filter the item list based on the search string
         * This method is the only place showResults(true) should be called
         * onlyOrder: boolean; only order the list, do not filter
         */
        $scope.filterItems = function(onlyOrder){

          var searchObject = {};
          if( itemAttributeName != null )
            searchObject[itemAttributeName] = $scope.searchString;
          else
            searchObject = $scope.searchString;

          if( !isAsync )
            updateItems(onlyOrder);

          //Do not show results if the search string is set to null and
          //we're not only ordering
          if( $scope.searchString == null && !onlyOrder ){
            $scope.showResults(false);
          } else {
            if( onlyOrder){
              $scope.showResults(true);
            //If it's the initial filter, do not show items.
            //This prevents the results from being shown when the component
            //is initilized
            } else if( $scope.searchString.length > 0
                && !$scope.resultsVisible.value && !initialFilter ) {
              $scope.showResults(true);
            } else if( $scope.searchString.length == 0 &&
                       $scope.resultsVisible.value ){
              $scope.showResults(false);
            }
            initialFilter = false;
          }
        };

        var searchStringChanged = false;

        $scope.$watch('searchString', function(){
          if(!componentInitialized)
            return;
          searchStringChanged = true;
          $scope.filterItems();
          $scope.selectedRowIndex = 0;
          if( !valueWasSelected )
            $scope.selectedValue = null;
          else
            valueWasSelected = false;
          $scope.updateSourceValue();
        }, true);

        $scope.$watch('selectedRowIndex', function(){
          if(!componentInitialized)
            return;
          if($scope.selectedRowIndex < 0 ||
            $scope.selectedRowIndex > ($scope.filteredItems.length - 1))
            $scope.selectedRowIndex = 0;
        }, true);

        $scope.$on('$destroy', function(){
          $interval.cancel(refreshPromise);
        });

        function callRefreshFunction(searchString){
          refreshFunction($scope)(searchString);
        };

        var componentInitialized = false;
        function initAsync(){
          $scope.searchString = valueGetter($scope);
          callRefreshFunction($scope.searchString);
          refreshPromise = $interval(function(){
            if(searchStringChanged)
              callRefreshFunction($scope.searchString);
            searchStringChanged = false;
            $scope.filteredItems = getItems();
          }, refreshRate);
          $timeout(function(){
            componentInitialized = true;
          });
        }

        function initSync(){
          initSearchString();
          $timeout(function(){
            componentInitialized = true;
          });
        }

        // code to run once setup is finished
        function initComponent(){
          if(isAsync){
            initAsync();
          } else {
            initSync();
          }
        }
        initComponent();
      }
    }
  }
])
<|endoftext|>


<commit_before>/**
 * @class L.Draw.Rectangle
 * @aka Draw.Rectangle
 * @inherits L.Draw.SimpleShape
 */
L.Draw.Rectangle = L.Draw.SimpleShape.extend({
	statics: {
		TYPE: 'rectangle'
	},

	options: {
		shapeOptions: {
			stroke: true,
			color: '#3388ff',
			weight: 4,
			opacity: 0.5,
			fill: true,
			fillColor: null, //same as color by default
			fillOpacity: 0.2,
			showArea: true,
			clickable: true
		},
		metric: true // Whether to use the metric measurement system or imperial
	},

	// @method initialize(): void
	initialize: function (map, options) {
		// Save the type so super can fire, need to do this as cannot do this.TYPE :(
		this.type = L.Draw.Rectangle.TYPE;

		this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start;

		L.Draw.SimpleShape.prototype.initialize.call(this, map, options);
	},

	// @method disable(): void
	disable: function () {
		if (!this._enabled) {
			return;
		}

		this._twoClicksToggle = false;
		L.Draw.SimpleShape.prototype.disable.call(this);
	},

	_onMouseUp: function (e) {
		if (!this._shape && !this._twoClicksToggle) {
			this._twoClicksToggle = true;
			return;
		}

		// Make sure closing click is on map
		if (this._twoClicksToggle && !_hasAncestor(e.target, 'leaflet-pane')) {
			return;
		}

		L.Draw.SimpleShape.prototype._onMouseUp.call(this);
	},

	_drawShape: function (latlng) {
		if (!this._shape) {
			this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions);
			this._map.addLayer(this._shape);
		} else {
			this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng));
		}
	},

	_fireCreatedEvent: function () {
		var rectangle = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions);
		L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
	},

	_getTooltipText: function () {
		var tooltipText = L.Draw.SimpleShape.prototype._getTooltipText.call(this),
			shape = this._shape,
			showArea = this.options.showArea,
			latLngs, area, subtext;

		if (shape) {
			latLngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs();
			area = L.GeometryUtil.geodesicArea(latLngs);
			subtext = showArea ? L.GeometryUtil.readableArea(area, this.options.metric) : ''
		}

		return {
			text: tooltipText.text,
			subtext: subtext
		};
	}
});

function _hasAncestor (el, cls) {
	while ((el = el.parentElement) && !el.classList.contains(cls));
	return el;
}
<commit_msg>changed _twoClicksToggle to a more descriptive name<commit_after>/**
 * @class L.Draw.Rectangle
 * @aka Draw.Rectangle
 * @inherits L.Draw.SimpleShape
 */
L.Draw.Rectangle = L.Draw.SimpleShape.extend({
	statics: {
		TYPE: 'rectangle'
	},

	options: {
		shapeOptions: {
			stroke: true,
			color: '#3388ff',
			weight: 4,
			opacity: 0.5,
			fill: true,
			fillColor: null, //same as color by default
			fillOpacity: 0.2,
			showArea: true,
			clickable: true
		},
		metric: true // Whether to use the metric measurement system or imperial
	},

	// @method initialize(): void
	initialize: function (map, options) {
		// Save the type so super can fire, need to do this as cannot do this.TYPE :(
		this.type = L.Draw.Rectangle.TYPE;

		this._initialLabelText = L.drawLocal.draw.handlers.rectangle.tooltip.start;

		L.Draw.SimpleShape.prototype.initialize.call(this, map, options);
	},

	// @method disable(): void
	disable: function () {
		if (!this._enabled) {
			return;
		}

		this._isCurrentlyTwoClickDrawing = false;
		L.Draw.SimpleShape.prototype.disable.call(this);
	},

	_onMouseUp: function (e) {
		if (!this._shape && !this._twoClicksToggle) {
			this._twoClicksToggle = true;
			return;
		}

		// Make sure closing click is on map
		if (this._twoClicksToggle && !_hasAncestor(e.target, 'leaflet-pane')) {
			return;
		}

		L.Draw.SimpleShape.prototype._onMouseUp.call(this);
	},

	_drawShape: function (latlng) {
		if (!this._shape) {
			this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions);
			this._map.addLayer(this._shape);
		} else {
			this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng));
		}
	},

	_fireCreatedEvent: function () {
		var rectangle = new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions);
		L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
	},

	_getTooltipText: function () {
		var tooltipText = L.Draw.SimpleShape.prototype._getTooltipText.call(this),
			shape = this._shape,
			showArea = this.options.showArea,
			latLngs, area, subtext;

		if (shape) {
			latLngs = this._shape._defaultShape ? this._shape._defaultShape() : this._shape.getLatLngs();
			area = L.GeometryUtil.geodesicArea(latLngs);
			subtext = showArea ? L.GeometryUtil.readableArea(area, this.options.metric) : ''
		}

		return {
			text: tooltipText.text,
			subtext: subtext
		};
	}
});

function _hasAncestor (el, cls) {
	while ((el = el.parentElement) && !el.classList.contains(cls));
	return el;
}
<|endoftext|>


<commit_before>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @extends Marionette.View
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'buttonView');
        helpers.ensureOption(options, 'panelView');

        _.defaults(this.options, _.clone(defaultOptions), options);

        _.bindAll(this, 'open', 'close', '__onBlur');

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);
    },

    template: false,

    className() {
        return `dropdown ${this.options.class || ''}`;
    },

    ui: {
        button: '.js-button-region'
    },

    events: {
        blur: '__onBlur'
    },

    /**
     * Contains an instance of options.buttonView if the dropdown is rendered, null otherwise.
     * */
    buttonView: null,

    /**
     * Contains an instance of options.panelView if the dropdown is open, null otherwise.
     * The view is created every time (!) the panel is triggered to open.
     * */
    panelView: null,

    onRender() {
        if (this.button) {
            this.stopListening(this.button);
        }
        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.buttonView = this.button;
        this.listenTo(this.button, 'all', (...args) => {
            args[0] = `button:${args[0]}`;
            this.triggerMethod(...args);
        });
        const el = this.button.render().$el;
        this.$el.append(el);

        this.isShown = true;
        this.button.on('change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));

        el.on('click', this.__handleClick.bind(this));

        this.$el.attr('tabindex', -1);
    },

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    },

    __adjustPosition(panelEl) {
        panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const panelWidth = Math.min(Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0), this.maxWidth);

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            panelEl.style.width = `${panelWidth}px`;
        }

        if (panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            panelEl.style.minWidth = `${panelWidth}px`;
        }

        let offsetHeight = panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        this.el.classList.toggle(classes.DROPDOWN_DOWN, position === panelPosition.DOWN);
        this.el.classList.toggle(classes.DROPDOWN_UP, position === panelPosition.UP);
        panelEl.classList.toggle(classes.DROPDOWN_DOWN, position === panelPosition.DOWN);
        panelEl.classList.toggle(classes.DROPDOWN_UP, position === panelPosition.UP);

        offsetHeight = panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        panelEl.style.top = `${top}px`;
        panelEl.style.left = `${buttonRect.left}px`;
    },

    /**
     * Opens the dropdown panel.
     * */
    open() {
        if (this.isOpen) {
            return;
        }
        this.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.triggerMethod(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.el
        });
        this.panelEl = this.panelView.el;

        this.__adjustPosition(this.panelEl);
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.listenTo(this.panelView, 'change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));
        this.__listenToElementMoveOnce(this.el, this.close);
        this.listenTo(GlobalEventService, 'window:keydown:captured', (document, event) => this.__keyAction(event));
        this.listenTo(GlobalEventService, 'window:mousedown:captured', this.__handleGlobalMousedown);
        this.listenTo(WindowService, 'popup:close', this.__onWindowServicePopupClose);

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.isOpen = true;
        this.trigger('open', this);
    },

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.isOpen || !document.body.contains(this.el)) {
            return;
        }
        this.trigger('before:close', this);

        this.el.classList.remove(classes.OPEN);

        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();
        this.stopListening(GlobalEventService);
        this.stopListening(WindowService);
        this.stopListening(this.panelView);
        this.button.$el.focus();
        this.isOpen = false;

        this.trigger('close', this, ...args);
        if (this.options.renderAfterClose && !this.isDestroyed) {
            this.button.render();
        }
    },

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    },

    __handleClick() {
        if (this.options.autoOpen) {
            this.open();
        }
    },

    __isNestedInButton(testedEl) {
        return this.el === testedEl || this.el.contains(testedEl);
    },

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    },

    __handleBlur() {
        if (!this.options.externalBlurHandler(document.activeElement) && !this.__suppressHandlingBlur && !this.__isNestedInButton(document.activeElement) && !this.__isNestedInPanel(document.activeElement)) {
            this.close();
        }
    },

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    },

    __onWindowServicePopupClose(popupId) {
        if (this.isOpen && this.popupId === popupId) {
            this.close();
        }
    },

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur);
        }
        this.isFocused = true;
    },

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur);
        }
    },

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            this.listenTo(GlobalEventService, 'window:wheel:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:mouseup:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();
        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    },

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    },

    __checkElements() {
        setTimeout(() => {
            if (this.isDestroyed()) {
                return;
            }
            this.__observedEntities.forEach(x => {
                const { left, top } = x.el.getBoundingClientRect();
                if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                    x.callback.call(this);
                }
            });
        }, 50);
    }
});
<commit_msg>Fix dropdown width.<commit_after>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @extends Marionette.View
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'buttonView');
        helpers.ensureOption(options, 'panelView');

        _.defaults(this.options, _.clone(defaultOptions), options);

        _.bindAll(this, 'open', 'close', '__onBlur');

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);
    },

    template: false,

    className() {
        return `dropdown ${this.options.class || ''}`;
    },

    ui: {
        button: '.js-button-region'
    },

    events: {
        blur: '__onBlur'
    },

    /**
     * Contains an instance of options.buttonView if the dropdown is rendered, null otherwise.
     * */
    buttonView: null,

    /**
     * Contains an instance of options.panelView if the dropdown is open, null otherwise.
     * The view is created every time (!) the panel is triggered to open.
     * */
    panelView: null,

    onRender() {
        if (this.button) {
            this.stopListening(this.button);
        }
        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.buttonView = this.button;
        this.listenTo(this.button, 'all', (...args) => {
            args[0] = `button:${args[0]}`;
            this.triggerMethod(...args);
        });
        const el = this.button.render().$el;
        this.$el.append(el);

        this.isShown = true;
        this.button.on('change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));

        el.on('click', this.__handleClick.bind(this));

        this.$el.attr('tabindex', -1);
    },

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    },

    adjustPosition() {
        this.__adjustPosition(this.panelEl);
    },

    __adjustPosition(panelEl) {
        panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const computedWidth = Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0);

        const panelWidth = this.maxWidth ? Math.min(computedWidth, this.maxWidth) : computedWidth;

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            panelEl.style.width = `${panelWidth}px`;
        }

        if (panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            panelEl.style.minWidth = `${panelWidth}px`;
        }

        let offsetHeight = panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        this.el.classList.toggle(classes.DROPDOWN_DOWN, position === panelPosition.DOWN);
        this.el.classList.toggle(classes.DROPDOWN_UP, position === panelPosition.UP);
        panelEl.classList.toggle(classes.DROPDOWN_DOWN, position === panelPosition.DOWN);
        panelEl.classList.toggle(classes.DROPDOWN_UP, position === panelPosition.UP);

        offsetHeight = panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        panelEl.style.top = `${top}px`;
        panelEl.style.left = `${buttonRect.left}px`;
    },

    /**
     * Opens the dropdown panel.
     * */
    open() {
        if (this.isOpen) {
            return;
        }
        this.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.triggerMethod(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.el
        });
        this.panelEl = this.panelView.el;

        this.__adjustPosition(this.panelEl);
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.listenTo(this.panelView, 'change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));
        this.__listenToElementMoveOnce(this.el, this.close);
        this.listenTo(GlobalEventService, 'window:keydown:captured', (document, event) => this.__keyAction(event));
        this.listenTo(GlobalEventService, 'window:mousedown:captured', this.__handleGlobalMousedown);
        this.listenTo(WindowService, 'popup:close', this.__onWindowServicePopupClose);

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.isOpen = true;
        this.trigger('open', this);
    },

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.isOpen || !document.body.contains(this.el)) {
            return;
        }
        this.trigger('before:close', this);

        this.el.classList.remove(classes.OPEN);

        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();
        this.stopListening(GlobalEventService);
        this.stopListening(WindowService);
        this.stopListening(this.panelView);
        this.button.$el.focus();
        this.isOpen = false;

        this.trigger('close', this, ...args);
        if (this.options.renderAfterClose && !this.isDestroyed) {
            this.button.render();
        }
    },

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    },

    __handleClick() {
        if (this.options.autoOpen) {
            this.open();
        }
    },

    __isNestedInButton(testedEl) {
        return this.el === testedEl || this.el.contains(testedEl);
    },

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    },

    __handleBlur() {
        if (!this.options.externalBlurHandler(document.activeElement) && !this.__suppressHandlingBlur && !this.__isNestedInButton(document.activeElement) && !this.__isNestedInPanel(document.activeElement)) {
            this.close();
        }
    },

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    },

    __onWindowServicePopupClose(popupId) {
        if (this.isOpen && this.popupId === popupId) {
            this.close();
        }
    },

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur);
        }
        this.isFocused = true;
    },

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur);
        }
    },

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            this.listenTo(GlobalEventService, 'window:wheel:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:mouseup:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();
        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    },

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    },

    __checkElements() {
        setTimeout(() => {
            if (this.isDestroyed()) {
                return;
            }
            this.__observedEntities.forEach(x => {
                const { left, top } = x.el.getBoundingClientRect();
                if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                    x.callback.call(this);
                }
            });
        }, 50);
    }
});
<|endoftext|>


<commit_before>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @extends Marionette.View
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'buttonView');
        helpers.ensureOption(options, 'panelView');

        _.defaults(this.options, Object.assign({}, defaultOptions), options);

        _.bindAll(this, 'open', 'close', '__onBlur');

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);
    },

    template: false,

    className() {
        return `dropdown ${this.options.class || ''}`;
    },

    ui: {
        button: '.js-button-region'
    },

    events: {
        blur: '__onBlur',
        click: '__triggerClick'
    },

    /**
     * Contains an instance of options.buttonView if the dropdown is rendered, null otherwise.
     * */
    buttonView: null,

    /**
     * Contains an instance of options.panelView if the dropdown is open, null otherwise.
     * The view is created every time (!) the panel is triggered to open.
     * */
    panelView: null,

    onRender() {
        if (this.button) {
            this.stopListening(this.button);
        }
        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.buttonView = this.button;
        this.listenTo(this.button, 'all', (...args) => {
            args[0] = `button:${args[0]}`;
            this.triggerMethod(...args);
        });
        const el = this.button.render().$el;
        this.$el.append(el);

        this.isShown = true;
        this.button.on('change:content', () => this.panelEl && this.__adjustPosition(this.panelEl, true));

        el.on('click', this.__handleClick.bind(this));

        this.$el.attr('tabindex', -1);
    },

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    },

    adjustPosition(isNeedToRefreshAnchorPosition) {
        if (this.panelEl) {
            this.__adjustPosition(this.panelEl, isNeedToRefreshAnchorPosition);
        }
    },

    __adjustPosition(panelEl, isNeedToRefreshAnchorPosition) {
        panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const computedWidth = Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0);

        const panelWidth = this.maxWidth ? Math.min(computedWidth, this.maxWidth) : computedWidth;

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            panelEl.style.width = `${panelWidth}px`;
        }

        if (panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            panelEl.style.minWidth = `${panelWidth}px`;
        }

        let leftOffset = buttonRect.left;
        let offsetHeight = panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        if (position === panelPosition.DOWN) {
            this.el.classList.add(classes.DROPDOWN_DOWN);
            panelEl.classList.add(classes.DROPDOWN_DOWN);

            this.el.classList.remove(classes.DROPDOWN_UP);
            panelEl.classList.remove(classes.DROPDOWN_UP);
        } else if (position === panelPosition.UP) {
            this.el.classList.add(classes.DROPDOWN_UP);
            panelEl.classList.add(classes.DROPDOWN_UP);

            this.el.classList.remove(classes.DROPDOWN_DOWN);
            panelEl.classList.remove(classes.DROPDOWN_DOWN);
        }

        offsetHeight = panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        panelEl.style.top = `${top}px`;

        if (panelWidth > buttonRect.width) {
            leftOffset -= panelWidth - buttonRect.width;
        }
        panelEl.style.left = `${leftOffset}px`;

        if (isNeedToRefreshAnchorPosition) {
            this.__updateAnchorPosition(this.el);
        }
    },

    /**
     * Opens the dropdown panel.
     * */
    open() {
        if (this.isOpen) {
            return;
        }
        this.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.triggerMethod(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.el
        });
        this.panelEl = this.panelView.el;

        this.__adjustPosition(this.panelEl);
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.listenTo(this.panelView, 'change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));
        this.__listenToElementMoveOnce(this.el, this.close);
        this.listenTo(GlobalEventService, 'window:keydown:captured', (document, event) => this.__keyAction(event));
        this.listenTo(GlobalEventService, 'window:mousedown:captured', this.__handleGlobalMousedown);
        this.listenTo(WindowService, 'popup:close', this.__onWindowServicePopupClose);

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.isOpen = true;
        this.trigger('open', this);
    },

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.isOpen || !document.body.contains(this.el)) {
            return;
        }
        this.trigger('before:close', this);

        this.el.classList.remove(classes.OPEN);

        this.stopListening(WindowService);
        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();
        this.stopListening(GlobalEventService);

        this.stopListening(this.panelView);
        this.button.$el.focus();
        this.isOpen = false;

        this.trigger('close', this, ...args);
        if (this.options.renderAfterClose && !this.isDestroyed) {
            this.button.render();
        }
    },

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    },

    toggle(...args) {
        if (this.button.isOpen) {
            this.close(...args);
        } else {
            this.open(...args);
        }
    }

    __handleClick() {
        this.trigger('click');
        if (this.options.autoOpen) {
            this.toggle();
        }
    },

    __isNestedInButton(testedEl) {
        return this.el === testedEl || this.el.contains(testedEl);
    },

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    },

    __handleBlur() {
        if (
            !this.options.externalBlurHandler(document.activeElement) &&
            !this.__suppressHandlingBlur &&
            !this.__isNestedInButton(document.activeElement) &&
            !this.__isNestedInPanel(document.activeElement)
        ) {
            this.close();
        }
    },

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    },

    __onWindowServicePopupClose(popupId) {
        if (this.isOpen && this.popupId === popupId) {
            this.close();
        }
    },

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur);
        }
        this.isFocused = true;
    },

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur);
        }
    },

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            this.listenTo(GlobalEventService, 'window:wheel:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:mouseup:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();

        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    },

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    },

    __checkElements(target) {
        if (!this.__isNestedInButton(target) && !this.__isNestedInPanel(target)) {
            setTimeout(() => {
                if (this.isDestroyed()) {
                    return;
                }
                this.__observedEntities.forEach(x => {
                    const { left, top } = x.el.getBoundingClientRect();
                    if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                        x.callback.call(this);
                    }
                });
            }, 50);
        }
    },

    __updateAnchorPosition(el) {
        const observable = this.__observedEntities.find(entrie => entrie.el === el);

        if (observable) {
            const { left, top } = el.getBoundingClientRect();

            observable.anchorViewportPos = {
                left: Math.floor(left),
                top: Math.floor(top)
            };
        }
    },

    __triggerClick() {
        this.trigger('container:click');
    }
});
<commit_msg>Fix misprint.<commit_after>// @flow
import { helpers } from 'utils';
import WindowService from '../../services/WindowService';
import GlobalEventService from '../../services/GlobalEventService';

const THROTTLE_DELAY = 100;

const classes = {
    OPEN: 'open',
    DROPDOWN_DOWN: 'dropdown__wrp_down',
    DROPDOWN_WRP_OVER: 'dropdown__wrp_down-over',
    DROPDOWN_UP: 'dropdown__wrp_up',
    DROPDOWN_UP_OVER: 'dropdown__wrp_up-over',
    VISIBLE_COLLECTION: 'visible-collection'
};

const WINDOW_BORDER_OFFSET = 10;
const MAX_DROPDOWN_PANEL_WIDTH = 200;

const panelPosition = {
    DOWN: 'down',
    UP: 'up'
};

const panelMinWidth = {
    NONE: 'none',
    BUTTON_WIDTH: 'button-width'
};

const defaultOptions = {
    autoOpen: true,
    renderAfterClose: true,
    panelPosition: panelPosition.DOWN,
    panelMinWidth: panelMinWidth.BUTTON_WIDTH,
    allowNestedFocus: true,
    externalBlurHandler: () => false
};

/**
 * @name DropdownView
 * @memberof module:core.dropdown.views
 * @class Composite View that implements dropdown logic similar to SELECT HTML-element.
 * Unlike {@link module:core.dropdown.views.PopoutView PopoutView}, a panel doesn't have a speech bubble triangle and
 * it's min-width is always determined and equal to the width of a button view.
 *
 * A dropdown view contains button and panel regions that can be fully customizable by the properties buttonView and panelView.
 *

 *
Button View is used for displaying a button. Click on that button trigger a panel to open.

 *
Panel View is used to display a panel that drops down.

 *

 *
 * Panel width is determined by its layout but it cannot be less than the button's width. Panel height is fully determined by its layout.
 * A place where the panel appears depends on the panelPosition option.

 * Possible events:

 *
'open' (dropdownView) - fires after the panel has opened.

 *
'close' (dropdownView, ...) - fires after the panel has closed.
 * If the panel was closed via close(...) method, the arguments of this method are transferred into this event.

 *
'button:\*'  - all events the buttonView triggers are repeated by this view with 'button:' prefix.

 *
'panel:\*'  - all events the panelView triggers are repeated by this view with 'panel:' prefix.

 *

 * @constructor
 * @extends Marionette.View
 * @param {Object} options Options object.
 * @param {Marionette.View} options.buttonView View class for displaying the button.
 * @param {(Object|Function)} [options.buttonViewOptions] Options passed into the view on its creation.
 * @param {Marionette.View} options.panelView View class for displaying the panel. The view is created every time the panel is triggered to open.
 * @param {(Object|Function)} [options.panelViewOptions] Options passed into the view on its creation.
 * @param {Boolean} [options.autoOpen=true] Whether click on the button should trigger the panel to open.
 * @param {String} [options.panelPosition='down'] Opening direction:
 *

'down' - opens down.

 *
'down-over' - opens down and the panel is located above the button overlapping it.

 *
'up' - opens up.

 *
'up-over' - opens up and the panel is located above the button overlapping it.

 * @param {Boolean} [options.renderAfterClose=true] Whether to trigger button render when the panel has closed.
 * */

export default Marionette.View.extend({
    initialize(options) {
        helpers.ensureOption(options, 'buttonView');
        helpers.ensureOption(options, 'panelView');

        _.defaults(this.options, Object.assign({}, defaultOptions), options);

        _.bindAll(this, 'open', 'close', '__onBlur');

        this.__observedEntities = [];
        this.maxWidth = options.panelViewOptions && options.panelViewOptions.maxWidth ? options.panelViewOptions.maxWidth : 0;
        this.__checkElements = _.throttle(this.__checkElements.bind(this), THROTTLE_DELAY);
    },

    template: false,

    className() {
        return `dropdown ${this.options.class || ''}`;
    },

    ui: {
        button: '.js-button-region'
    },

    events: {
        blur: '__onBlur',
        click: '__triggerClick'
    },

    /**
     * Contains an instance of options.buttonView if the dropdown is rendered, null otherwise.
     * */
    buttonView: null,

    /**
     * Contains an instance of options.panelView if the dropdown is open, null otherwise.
     * The view is created every time (!) the panel is triggered to open.
     * */
    panelView: null,

    onRender() {
        if (this.button) {
            this.stopListening(this.button);
        }
        this.button = new this.options.buttonView(_.extend({ parent: this }, this.options.buttonViewOptions));
        this.buttonView = this.button;
        this.listenTo(this.button, 'all', (...args) => {
            args[0] = `button:${args[0]}`;
            this.triggerMethod(...args);
        });
        const el = this.button.render().$el;
        this.$el.append(el);

        this.isShown = true;
        this.button.on('change:content', () => this.panelEl && this.__adjustPosition(this.panelEl, true));

        el.on('click', this.__handleClick.bind(this));

        this.$el.attr('tabindex', -1);
    },

    onDestroy() {
        if (this.button) {
            this.button.destroy();
        }
        if (this.isOpen) {
            WindowService.closePopup(this.popupId);
        }
    },

    adjustPosition(isNeedToRefreshAnchorPosition) {
        if (this.panelEl) {
            this.__adjustPosition(this.panelEl, isNeedToRefreshAnchorPosition);
        }
    },

    __adjustPosition(panelEl, isNeedToRefreshAnchorPosition) {
        panelEl.style.height = ''; //resetting custom height

        const viewportHeight = window.innerHeight;
        const dropDownRoot = this.button.$el.closest('.dropdown_root')[0];
        const dropDownRootPositionUp = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_up');
        const dropDownRootPositionDown = dropDownRoot && dropDownRoot.classList.contains('dropdown__wrp_down');
        const buttonEl = dropDownRoot || this.button.el;
        const buttonRect = buttonEl.getBoundingClientRect();

        const bottom = viewportHeight - buttonRect.top - buttonRect.height;

        const computedWidth = Math.max(MAX_DROPDOWN_PANEL_WIDTH, buttonRect.width || 0);

        const panelWidth = this.maxWidth ? Math.min(computedWidth, this.maxWidth) : computedWidth;

        if (this.options.panelMinWidth === panelMinWidth.BUTTON_WIDTH) {
            panelEl.style.width = `${panelWidth}px`;
        }

        if (panelEl.clientWidth < MAX_DROPDOWN_PANEL_WIDTH) {
            panelEl.style.minWidth = `${panelWidth}px`;
        }

        let leftOffset = buttonRect.left;
        let offsetHeight = panelEl.offsetHeight;

        let position = this.options.panelPosition;

        if (dropDownRoot && dropDownRootPositionUp) {
            position = panelPosition.UP;
        } else if (dropDownRoot && dropDownRootPositionDown) {
            position = panelPosition.DOWN;
        } else if (position === panelPosition.DOWN && ((bottom < offsetHeight && buttonRect.top > bottom) || bottom < this.options.minAvailableHeight + offsetHeight)) {
            position = panelPosition.UP;
        } else if (position === panelPosition.UP && buttonRect.top < offsetHeight && bottom > buttonRect.top) {
            position = panelPosition.DOWN;
        }

        // class adjustments
        if (position === panelPosition.DOWN) {
            this.el.classList.add(classes.DROPDOWN_DOWN);
            panelEl.classList.add(classes.DROPDOWN_DOWN);

            this.el.classList.remove(classes.DROPDOWN_UP);
            panelEl.classList.remove(classes.DROPDOWN_UP);
        } else if (position === panelPosition.UP) {
            this.el.classList.add(classes.DROPDOWN_UP);
            panelEl.classList.add(classes.DROPDOWN_UP);

            this.el.classList.remove(classes.DROPDOWN_DOWN);
            panelEl.classList.remove(classes.DROPDOWN_DOWN);
        }

        offsetHeight = panelEl.offsetHeight;

        // panel positioning
        let top: number = 0;
        switch (position) {
            case panelPosition.UP:
                top = buttonRect.top - offsetHeight;
                break;
            case panelPosition.DOWN:
                top = buttonRect.top + buttonRect.height;
                break;
            default:
                break;
        }

        // trying to fit into viewport
        if (top + offsetHeight > viewportHeight - WINDOW_BORDER_OFFSET) {
            top = viewportHeight - WINDOW_BORDER_OFFSET - offsetHeight; // todo add border offset
            if (offsetHeight + WINDOW_BORDER_OFFSET > bottom) {
                const diff = offsetHeight + WINDOW_BORDER_OFFSET - bottom;
                top += diff;
                panelEl.style.height = `${offsetHeight + WINDOW_BORDER_OFFSET - diff}px`;
            }
        }
        if (top <= WINDOW_BORDER_OFFSET) {
            top = WINDOW_BORDER_OFFSET;

            if (offsetHeight > buttonRect.top) {
                const diff = offsetHeight - buttonRect.top;
                panelEl.style.height = `${offsetHeight - diff}px`;
            }
        }

        panelEl.style.top = `${top}px`;

        if (panelWidth > buttonRect.width) {
            leftOffset -= panelWidth - buttonRect.width;
        }
        panelEl.style.left = `${leftOffset}px`;

        if (isNeedToRefreshAnchorPosition) {
            this.__updateAnchorPosition(this.el);
        }
    },

    /**
     * Opens the dropdown panel.
     * */
    open() {
        if (this.isOpen) {
            return;
        }
        this.trigger('before:open', this);

        const panelViewOptions = _.extend(this.options.panelViewOptions || {}, {
            parent: this
        });
        this.el.classList.add(classes.OPEN);
        this.panelView = new this.options.panelView(panelViewOptions);
        this.panelView.on('all', (...args) => {
            args[0] = `panel:${args[0]}`;
            this.triggerMethod(...args);
        });

        this.popupId = WindowService.showTransientPopup(this.panelView, {
            hostEl: this.el
        });
        this.panelEl = this.panelView.el;

        this.__adjustPosition(this.panelEl);
        //const buttonWidth = this.button.el.getBoundingClientRect().width;

        //this.panelView.el.getElementsByClassName(classes.VISIBLE_COLLECTION)[0].style.width = `${panelWidth}`;

        this.listenTo(this.panelView, 'change:content', () => this.panelEl && this.__adjustPosition(this.panelEl));
        this.__listenToElementMoveOnce(this.el, this.close);
        this.listenTo(GlobalEventService, 'window:keydown:captured', (document, event) => this.__keyAction(event));
        this.listenTo(GlobalEventService, 'window:mousedown:captured', this.__handleGlobalMousedown);
        this.listenTo(WindowService, 'popup:close', this.__onWindowServicePopupClose);

        const activeElement = document.activeElement;
        if (!this.__isNestedInButton(activeElement) && !this.__isNestedInPanel(activeElement)) {
            //this.panelView.$el.focus(); todo
        } else {
            this.__focus(activeElement);
        }
        this.__suppressHandlingBlur = false;
        this.isOpen = true;
        this.trigger('open', this);
    },

    /**
     * Closes the dropdown panel.
     * @param {...*} arguments Arguments transferred into the 'close' event.
     * */
    close(...args) {
        if (!this.isOpen || !document.body.contains(this.el)) {
            return;
        }
        this.trigger('before:close', this);

        this.el.classList.remove(classes.OPEN);

        this.stopListening(WindowService);
        WindowService.closePopup(this.popupId);

        this.__stopListeningToElementMove();
        this.stopListening(GlobalEventService);

        this.stopListening(this.panelView);
        this.button.$el.focus();
        this.isOpen = false;

        this.trigger('close', this, ...args);
        if (this.options.renderAfterClose && !this.isDestroyed) {
            this.button.render();
        }
    },

    __keyAction(event) {
        if (event.keyCode === 27) {
            this.close();
        }
    },

    toggle(...args) {
        if (this.isOpen) {
            this.close(...args);
        } else {
            this.open(...args);
        }
    },

    __handleClick() {
        this.trigger('click');
        if (this.options.autoOpen) {
            this.toggle();
        }
    },

    __isNestedInButton(testedEl) {
        return this.el === testedEl || this.el.contains(testedEl);
    },

    __isNestedInPanel(testedEl) {
        const palet = document.getElementsByClassName('sp-container')[0]; //Color picker custom el container;

        return WindowService.get(this.popupId).some(x => x.el.contains(testedEl) || this.el.contains(testedEl)) || (palet && palet.contains(testedEl));
    },

    __handleBlur() {
        if (
            !this.options.externalBlurHandler(document.activeElement) &&
            !this.__suppressHandlingBlur &&
            !this.__isNestedInButton(document.activeElement) &&
            !this.__isNestedInPanel(document.activeElement)
        ) {
            this.close();
        }
    },

    __handleGlobalMousedown(target) {
        if (this.__isNestedInPanel(target) || this.options.externalBlurHandler(target)) {
            this.__suppressHandlingBlur = true;
        } else if (!this.__isNestedInButton(target)) {
            this.close();
        }
    },

    __onWindowServicePopupClose(popupId) {
        if (this.isOpen && this.popupId === popupId) {
            this.close();
        }
    },

    __focus(focusedEl) {
        if (!focusedEl) {
            this.__getFocusableEl().focus();
        } else if (document.activeElement) {
            document.activeElement.addEventListener('blur', this.__onBlur);
        }
        this.isFocused = true;
    },

    __onBlur() {
        _.defer(() => {
            this.isFocused = false;
            this.__handleBlur();
        });
        if (document.activeElement) {
            document.activeElement.removeEventListener('blur', this.__onBlur);
        }
    },

    __listenToElementMoveOnce(el, callback) {
        if (this.__observedEntities.length === 0) {
            this.listenTo(GlobalEventService, 'window:wheel:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:mouseup:captured', this.__checkElements);
            this.listenTo(GlobalEventService, 'window:keydown:captured', this.__checkElements);
        }

        // saving el position relative to the viewport for further check
        const { left, top } = el.getBoundingClientRect();

        this.__observedEntities.push({
            anchorViewportPos: {
                left: Math.floor(left),
                top: Math.floor(top)
            },
            el,
            callback
        });
    },

    __stopListeningToElementMove(el = null) {
        if (!el) {
            this.__observedEntities = [];
        } else {
            this.__observedEntities.splice(this.__observedEntities.findIndex(x => x.el === el), 1);
        }
    },

    __checkElements(target) {
        if (!this.__isNestedInButton(target) && !this.__isNestedInPanel(target)) {
            setTimeout(() => {
                if (this.isDestroyed()) {
                    return;
                }
                this.__observedEntities.forEach(x => {
                    const { left, top } = x.el.getBoundingClientRect();
                    if (Math.floor(left) !== x.anchorViewportPos.left || Math.floor(top) !== x.anchorViewportPos.top) {
                        x.callback.call(this);
                    }
                });
            }, 50);
        }
    },

    __updateAnchorPosition(el) {
        const observable = this.__observedEntities.find(entrie => entrie.el === el);

        if (observable) {
            const { left, top } = el.getBoundingClientRect();

            observable.anchorViewportPos = {
                left: Math.floor(left),
                top: Math.floor(top)
            };
        }
    },

    __triggerClick() {
        this.trigger('container:click');
    }
});
<|endoftext|>


<commit_before>/*
 * FullCalendar v@VERSION Google Calendar Plugin
 *
 * Copyright (c) 2011 Adam Shaw
 * Dual licensed under the MIT and GPL licenses, located in
 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
 *
 * Date: @DATE
 *
 */

(function($) {

var fc = $.fullCalendar;
var formatDate = fc.formatDate;
var parseISO8601 = fc.parseISO8601;
var addDays = fc.addDays;
var applyAll = fc.applyAll;

fc.sourceNormalizers.push(function(sourceOptions) {
	if (sourceOptions.dataType == 'gcal' ||
		sourceOptions.dataType === undefined &&
		(sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) {
			sourceOptions.dataType = 'gcal';
			if (sourceOptions.editable === undefined) {
				sourceOptions.editable = false;
			}
		}
});

fc.sourceFetchers.push(function(sourceOptions, start, end) {
	if (sourceOptions.dataType == 'gcal') {
		return transformOptions(sourceOptions, start, end);
	}
});

function transformOptions(sourceOptions, start, end) {

	var success = sourceOptions.success;
	var data = $.extend({}, sourceOptions.data || {}, {
		'start-min': formatDate(start, 'u'),
		'start-max': formatDate(end, 'u'),
		'singleevents': true,
		'max-results': 9999
	});

	var ctz = sourceOptions.currentTimezone;
	if (ctz) {
		data.ctz = ctz = ctz.replace(' ', '_');
	}

	return $.extend({}, sourceOptions, {
		url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?',
		dataType: 'jsonp',
		data: data,
		startParam: false,
		endParam: false,
		success: function(data) {
			var events = [];
			if (data.feed.entry) {
				$.each(data.feed.entry, function(i, entry) {
					var startStr = entry['gd$when'][0]['startTime'];
					var start = parseISO8601(startStr, true);
					var end = parseISO8601(entry['gd$when'][0]['endTime'], true);
					var allDay = startStr.indexOf('T') == -1;
					var url;
					$.each(entry.link, function(i, link) {
						if (link.type == 'text/html') {
							url = link.href;
							if (ctz) {
								url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + ctz;
							}
						}
					});
					if (allDay) {
						addDays(end, -1); // make inclusive
					}
					events.push({
						id: entry['gCal$uid']['value'],
						title: entry['title']['$t'],
						url: url,
						start: start,
						end: end,
						allDay: allDay,
						location: entry['gd$where'][0]['valueString'],
						description: entry['content']['$t']
					});
				});
			}
			var args = [events].concat(Array.prototype.slice.call(arguments, 1));
			var res = applyAll(success, this, args);
			if ($.isArray(res)) {
				return res;
			}
			return events;
		}
	});

}

// legacy
fc.gcalFeed = function(url, sourceOptions) {
	return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' });
};

})(jQuery);
<commit_msg>Use eventSource.resource for gcal events.<commit_after>/*
 * FullCalendar v@VERSION Google Calendar Plugin
 *
 * Copyright (c) 2011 Adam Shaw
 * Dual licensed under the MIT and GPL licenses, located in
 * MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
 *
 * Date: @DATE
 *
 */

(function($) {

var fc = $.fullCalendar;
var formatDate = fc.formatDate;
var parseISO8601 = fc.parseISO8601;
var addDays = fc.addDays;
var applyAll = fc.applyAll;

fc.sourceNormalizers.push(function(sourceOptions) {
	if (sourceOptions.dataType == 'gcal' ||
		sourceOptions.dataType === undefined &&
		(sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//)) {
			sourceOptions.dataType = 'gcal';
			if (sourceOptions.editable === undefined) {
				sourceOptions.editable = false;
			}
		}
});

fc.sourceFetchers.push(function(sourceOptions, start, end) {
	if (sourceOptions.dataType == 'gcal') {
		return transformOptions(sourceOptions, start, end);
	}
});

function transformOptions(sourceOptions, start, end) {

	var success = sourceOptions.success;
	var data = $.extend({}, sourceOptions.data || {}, {
		'start-min': formatDate(start, 'u'),
		'start-max': formatDate(end, 'u'),
		'singleevents': true,
		'max-results': 9999
	});

	var ctz = sourceOptions.currentTimezone;
	if (ctz) {
		data.ctz = ctz = ctz.replace(' ', '_');
	}

	return $.extend({}, sourceOptions, {
		url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?',
		dataType: 'jsonp',
		data: data,
		startParam: false,
		endParam: false,
		success: function(data) {
			var events = [];
			if (data.feed.entry) {
				$.each(data.feed.entry, function(i, entry) {
					var startStr = entry['gd$when'][0]['startTime'];
					var start = parseISO8601(startStr, true);
					var end = parseISO8601(entry['gd$when'][0]['endTime'], true);
					var allDay = startStr.indexOf('T') == -1;
					var url;
					$.each(entry.link, function(i, link) {
						if (link.type == 'text/html') {
							url = link.href;
							if (ctz) {
								url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + ctz;
							}
						}
					});
					if (allDay) {
						addDays(end, -1); // make inclusive
					}
					events.push({
						id: entry['gCal$uid']['value'],
						title: entry['title']['$t'],
						url: url,
						start: start,
						end: end,
						allDay: allDay,
						location: entry['gd$where'][0]['valueString'],
						description: entry['content']['$t'],
						resource: sourceOptions.resource
					});
				});
			}
			var args = [events].concat(Array.prototype.slice.call(arguments, 1));
			var res = applyAll(success, this, args);
			if ($.isArray(res)) {
				return res;
			}
			return events;
		}
	});

}

// legacy
fc.gcalFeed = function(url, sourceOptions) {
	return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' });
};

})(jQuery);
<|endoftext|>


<commit_before><commit_msg>Add current name to change name input<commit_after><|endoftext|>