<commit_before>(function() {
  'use strict';

  angular.module('foundation.modal', ['foundation.core'])
    .directive('zfModal', modalDirective)
    .factory('ModalFactory', ModalFactory)
    .service('FoundationModal', FoundationModal)
  ;

  FoundationModal.$inject = ['FoundationApi', 'ModalFactory'];

  function FoundationModal(foundationApi, ModalFactory) {
    var service    = {};

    service.activate = activate;
    service.deactivate = deactivate;
    service.newModal = newModal;

    return service;

    //target should be element ID
    function activate(target) {
      foundationApi.publish(target, 'show');
    }

    //target should be element ID
    function deactivate(target) {
      foundationApi.publish(target, 'hide');
    }

    //new modal has to be controlled via the new instance
    function newModal(config) {
      return new ModalFactory(config);
    }
  }

  modalDirective.$inject = ['FoundationApi'];

  function modalDirective(foundationApi) {

    var directive = {
      restrict: 'EA',
      templateUrl: 'components/modal/modal.html',
      transclude: true,
      scope: true,
      replace: true,
      compile: compile
    };

    return directive;

    function compile(tElement, tAttrs, transclude) {
      var type = 'modal';

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs, controller) {
          iAttrs.$set('zf-closable', type);
      }

      function postLink(scope, element, attrs) {
        var dialog = angular.element(element.children()[0]);

        scope.active = scope.active || false;
        scope.overlay = attrs.overlay === 'false' ? false : true;
        scope.overlayClose = attrs.overlayClose === 'false' ? false : true;

        var animationIn = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';

        var overlayIn = 'fadeIn';
        var overlayOut = 'fadeOut';

        scope.hideOverlay = function() {
          if(scope.overlayClose) {
            foundationApi.publish(attrs.id, 'close');
          }
        };

        scope.hide = function() {
          scope.active = false;
          animate();
          return;
        };

        scope.show = function() {
          scope.active = true;
          animate();
          dialog.tabIndex = -1;
          dialog[0].focus();
          return;
        };

        scope.toggle = function() {
          scope.active = !scope.active;
          animate();
          return;
        };

        init();

        //setup
        foundationApi.subscribe(attrs.id, function(msg) {
          if(msg === 'show' || msg === 'open') {
            scope.show();
          } else if (msg === 'close' || msg === 'hide') {
            scope.hide();
          } else if (msg === 'toggle') {
            scope.toggle();
          }

          if (scope.$root && !scope.$root.$$phase) {
            scope.$apply();
          }

          return;
        });

        function animate() {
          //animate both overlay and dialog
          if(!scope.overlay) {
            element.css('background', 'transparent');
          }

          foundationApi.animate(element, scope.active, overlayIn, overlayOut);
          foundationApi.animate(dialog, scope.active, animationIn, animationOut);
        }

        function init() {
          if(scope.active) {
            scope.show();
          }
        }
      }
    }
  }

  ModalFactory.$inject = ['$http', '$templateCache', '$rootScope', '$compile', '$timeout', '$q', 'FoundationApi'];

  function ModalFactory($http, $templateCache, $rootScope, $compile, $timeout, $q, foundationApi) {
    return modalFactory;

    function modalFactory(config) {
      var self = this, //for prototype functions
          container = angular.element(config.container || document.body),
          id = config.id || foundationApi.generateUuid(),
          attached = false,
          destroyed = false,
          html,
          element,
          fetched,
          scope,
          contentScope
      ;

      var props = [
        'animationIn',
        'animationOut',
        'overlay',
        'overlayClose',
        'class'
      ];

      if(config.templateUrl) {
        //get template
        fetched = $http.get(config.templateUrl, {
          cache: $templateCache
        }).then(function (response) {
          html = response.data;
          assembleDirective();
        });

      } else if(config.template) {
        //use provided template
        fetched = true;
        html = config.template;
        assembleDirective();
      }

      self.activate = activate;
      self.deactivate = deactivate;
      self.toggle = toggle;
      self.destroy = destroy;

      return {
        isActive: isActive,
        activate: activate,
        deactivate: deactivate,
        toggle: toggle,
        destroy: destroy
      };

      function checkStatus() {
        if(destroyed) {
          throw "Error: Modal was destroyed. Delete the object and create a new ModalFactory instance."
        }
      }

      function isActive() {
        return !destroyed && scope && scope.active === true;
      }

      function activate() {
        checkStatus();
        $timeout(function() {
          init(true);
          foundationApi.publish(id, 'show');
        }, 0, false);
      }

      function deactivate() {
        checkStatus();
        $timeout(function() {
          init(false);
          foundationApi.publish(id, 'hide');
        }, 0, false);
      }

      function toggle() {
        checkStatus();
        $timeout(function() {
          init(true);
          foundationApi.publish(id, 'toggle');
        }, 0, false);
      }

      function init(state) {
        $q.when(fetched).then(function() {
          if(!attached && html.length > 0) {
            var modalEl = container.append(element);

            $compile(element)(scope);

            attached = true;
          }

          scope.active = state;
        });
      }

      function assembleDirective() {
        // check for duplicate elements to prevent factory from cloning modals
        if (document.getElementById(id)) {
          return;
        }

        html = '<zf-modal id="' + id + '">' + html + '</zf-modal>';

        element = angular.element(html);

        scope = $rootScope.$new();

        // account for directive attributes and modal classes
        for(var i = 0; i < props.length; i++) {
          var prop = props[i];

          if(config[prop]) {
            switch (prop) {
              case 'animationIn':
                element.attr('animation-in', config[prop]);
                break;
              case 'animationOut':
                element.attr('animation-out', config[prop]);
                break;
              case 'overlayClose':
                element.attr('overlay-close', config[prop] ? 'true' : 'false'); // must be string, see postLink() above
                break;
              case 'class':
                if (angular.isString(config[prop])) {
                  config[prop].split(' ').forEach(function(klass) {
                    element.addClass(klass);
                  });
                } else if (angular.isArray(config[prop])) {
                  config[prop].forEach(function(klass) {
                    element.addClass(klass);
                  });
                }
                break;
              default:
                element.attr(prop, config[prop]);
                break;
            }
          }
        }
        // access view scope variables
        if (config.contentScope) {
          contentScope = config.contentScope;
          for (var prop in config.contentScope) {
            if (config.contentScope.hasOwnProperty(prop)) {
              scope[prop] = config.contentScope[prop];
            }
          }
        }
      }

      function destroy() {
        self.deactivate();
        setTimeout(function() {
          scope.$destroy();
          element.remove();
          destroyed = true;
        }, 3000);
        foundationApi.unsubscribe(id);
      }

    }

  }

})();
<commit_msg>Fixed entering modal animations by bypassing the animation method and manually adding the 'is-active' class for now. Until we can find a way to utilize better animation directives with sequential animations, this will allow the dialog to animate with the overlay being present.<commit_after>(function() {
  'use strict';

  angular.module('foundation.modal', ['foundation.core'])
    .directive('zfModal', modalDirective)
    .factory('ModalFactory', ModalFactory)
    .service('FoundationModal', FoundationModal)
  ;

  FoundationModal.$inject = ['FoundationApi', 'ModalFactory'];

  function FoundationModal(foundationApi, ModalFactory) {
    var service    = {};

    service.activate = activate;
    service.deactivate = deactivate;
    service.newModal = newModal;

    return service;

    //target should be element ID
    function activate(target) {
      foundationApi.publish(target, 'show');
    }

    //target should be element ID
    function deactivate(target) {
      foundationApi.publish(target, 'hide');
    }

    //new modal has to be controlled via the new instance
    function newModal(config) {
      return new ModalFactory(config);
    }
  }

  modalDirective.$inject = ['FoundationApi'];

  function modalDirective(foundationApi) {

    var directive = {
      restrict: 'EA',
      templateUrl: 'components/modal/modal.html',
      transclude: true,
      scope: true,
      replace: true,
      compile: compile
    };

    return directive;

    function compile(tElement, tAttrs, transclude) {
      var type = 'modal';

      return {
        pre: preLink,
        post: postLink
      };

      function preLink(scope, iElement, iAttrs, controller) {
          iAttrs.$set('zf-closable', type);
      }

      function postLink(scope, element, attrs) {
        var dialog = angular.element(element.children()[0]);

        scope.active = scope.active || false;
        scope.overlay = attrs.overlay === 'false' ? false : true;
        scope.overlayClose = attrs.overlayClose === 'false' ? false : true;

        var animationIn = attrs.animationIn || 'fadeIn';
        var animationOut = attrs.animationOut || 'fadeOut';

        var overlayIn = 'fadeIn';
        var overlayOut = 'fadeOut';

        scope.hideOverlay = function() {
          if(scope.overlayClose) {
            foundationApi.publish(attrs.id, 'close');
          }
        };

        scope.hide = function() {
          scope.active = false;
          animate();
          return;
        };

        scope.show = function() {
          scope.active = true;
          animate();
          dialog.tabIndex = -1;
          dialog[0].focus();
          return;
        };

        scope.toggle = function() {
          scope.active = !scope.active;
          animate();
          return;
        };

        init();

        //setup
        foundationApi.subscribe(attrs.id, function(msg) {
          if(msg === 'show' || msg === 'open') {
            scope.show();
          } else if (msg === 'close' || msg === 'hide') {
            scope.hide();
          } else if (msg === 'toggle') {
            scope.toggle();
          }

          if (scope.$root && !scope.$root.$$phase) {
            scope.$apply();
          }

          return;
        });

        function animate() {
          //animate both overlay and dialog
          if(!scope.overlay) {
            element.css('background', 'transparent');
          }

          // work around for modal animations
          // due to a bug where the overlay fadeIn is essentially covering up
          // the dialog's animation
          if (!scope.active) {
            foundationApi.animate(element, scope.active, overlayIn, overlayOut);
          }
          else {
            element.addClass('is-active');
          }

          foundationApi.animate(dialog, scope.active, animationIn, animationOut);
        }

        function init() {
          if(scope.active) {
            scope.show();
          }
        }
      }
    }
  }

  ModalFactory.$inject = ['$http', '$templateCache', '$rootScope', '$compile', '$timeout', '$q', 'FoundationApi'];

  function ModalFactory($http, $templateCache, $rootScope, $compile, $timeout, $q, foundationApi) {
    return modalFactory;

    function modalFactory(config) {
      var self = this, //for prototype functions
          container = angular.element(config.container || document.body),
          id = config.id || foundationApi.generateUuid(),
          attached = false,
          destroyed = false,
          html,
          element,
          fetched,
          scope,
          contentScope
      ;

      var props = [
        'animationIn',
        'animationOut',
        'overlay',
        'overlayClose',
        'class'
      ];

      if(config.templateUrl) {
        //get template
        fetched = $http.get(config.templateUrl, {
          cache: $templateCache
        }).then(function (response) {
          html = response.data;
          assembleDirective();
        });

      } else if(config.template) {
        //use provided template
        fetched = true;
        html = config.template;
        assembleDirective();
      }

      self.activate = activate;
      self.deactivate = deactivate;
      self.toggle = toggle;
      self.destroy = destroy;

      return {
        isActive: isActive,
        activate: activate,
        deactivate: deactivate,
        toggle: toggle,
        destroy: destroy
      };

      function checkStatus() {
        if(destroyed) {
          throw "Error: Modal was destroyed. Delete the object and create a new ModalFactory instance."
        }
      }

      function isActive() {
        return !destroyed && scope && scope.active === true;
      }

      function activate() {
        checkStatus();
        $timeout(function() {
          init(true);
          foundationApi.publish(id, 'show');
        }, 0, false);
      }

      function deactivate() {
        checkStatus();
        $timeout(function() {
          init(false);
          foundationApi.publish(id, 'hide');
        }, 0, false);
      }

      function toggle() {
        checkStatus();
        $timeout(function() {
          init(true);
          foundationApi.publish(id, 'toggle');
        }, 0, false);
      }

      function init(state) {
        $q.when(fetched).then(function() {
          if(!attached && html.length > 0) {
            var modalEl = container.append(element);

            $compile(element)(scope);

            attached = true;
          }

          scope.active = state;
        });
      }

      function assembleDirective() {
        // check for duplicate elements to prevent factory from cloning modals
        if (document.getElementById(id)) {
          return;
        }

        html = '<zf-modal id="' + id + '">' + html + '</zf-modal>';

        element = angular.element(html);

        scope = $rootScope.$new();

        // account for directive attributes and modal classes
        for(var i = 0; i < props.length; i++) {
          var prop = props[i];

          if(config[prop]) {
            switch (prop) {
              case 'animationIn':
                element.attr('animation-in', config[prop]);
                break;
              case 'animationOut':
                element.attr('animation-out', config[prop]);
                break;
              case 'overlayClose':
                element.attr('overlay-close', config[prop] ? 'true' : 'false'); // must be string, see postLink() above
                break;
              case 'class':
                if (angular.isString(config[prop])) {
                  config[prop].split(' ').forEach(function(klass) {
                    element.addClass(klass);
                  });
                } else if (angular.isArray(config[prop])) {
                  config[prop].forEach(function(klass) {
                    element.addClass(klass);
                  });
                }
                break;
              default:
                element.attr(prop, config[prop]);
                break;
            }
          }
        }
        // access view scope variables
        if (config.contentScope) {
          contentScope = config.contentScope;
          for (var prop in config.contentScope) {
            if (config.contentScope.hasOwnProperty(prop)) {
              scope[prop] = config.contentScope[prop];
            }
          }
        }
      }

      function destroy() {
        self.deactivate();
        setTimeout(function() {
          scope.$destroy();
          element.remove();
          destroyed = true;
        }, 3000);
        foundationApi.unsubscribe(id);
      }

    }

  }

})();
<|endoftext|>


<commit_before>/* eslint-disable no-undef */
const chai = require('chai');
const assert = chai.assert;
chai.should();

describe('Testing Hierarchy', () => {
  beforeEach(() => {
    browser.url('/');
    browser.pause(1000);
  });

  it('Should render hierarchy', () => {
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);

    const text = browser.getText('.selected .dk5')[0];
    assert.equal(text, '40-49', 'title is present');

    const level = browser.getText('.selected .hierarchy-level')[0];
    assert.include(level, 'Geografi og rejser i alm.', 'sublevel is present');
  });

  it('Click on sublevel', () => {
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);
    browser.click('[href="#!/hierarchy/40"]');
    browser.pause(1000);

    const sel = browser.getText('.selected');
    console.log('sel', sel);
    const dk5 = browser.getText('.dk5');
    console.log('dk5', dk5);
    const text2 = browser.getText('.selected .dk5');
    console.log('text2', text2);
    const h2 = browser.getText('.h2');
    console.log('h2', h2);
    const text = browser.getText('.selected h2 .dk5')[0];
    console.log('text', text);
    assert.equal(text, '40', 'title is present');

    const topics = browser.getText('.selected .hierarchy-topics');
    assert.include(topics[0], 'Flyveulykker', 'first topic is present');
  });

  it('Click between levels', () => {
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40-49', 'toplevel is selected');

    browser.click('[href="#!/hierarchy/40"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40', '2. level is selected');

    browser.click('[href="#!/hierarchy/40.6"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5'), '40.6', '3. level is selected');

    browser.click('[href="#!/hierarchy/40-49"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40-49', 'toplevel is selected');
  });

  it('Should display the first five aspects only', () => {
    browser.url('/#!/hierarchy/40');

    const topics = browser.getText('.selected .hierarchy-topics');

    assert.include(topics[0], 'Atlanterhavet\nBjerge\nFlyveulykker\nGeografi\nHave');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.notInclude(topics[0], 'Lande');
  });

  it('Should display all aspects when "Vis Flere" is clicked', () => {
    browser.url('/#!/hierarchy/40');

    let topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(browser.getText('.selected .hidden .hierarchy-topics'), 0);

    browser.click('.toggle-button');

    topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(topics[1].split('\n'), 12);
    assert.isTrue(browser.isExisting('.selected .show .hierarchy-topics'));
  });

  it('Should hide all but 5 aspects when "Vis Flere" is clicked twice', () => {
    browser.url('/#!/hierarchy/40');

    browser.click('.toggle-button');

    let topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(topics[1].split('\n'), 12);
    assert.isTrue(browser.isExisting('.selected .show .hierarchy-topics'));

    browser.click('.toggle-button');

    topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(browser.getText('.selected .hidden .hierarchy-topics'), 0);
  });
});
<commit_msg>increase pause<commit_after>/* eslint-disable no-undef */
const chai = require('chai');
const assert = chai.assert;
chai.should();

describe('Testing Hierarchy', () => {

  it('Should render hierarchy', () => {
    browser.url('/');
    browser.pause(5000);
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);

    const text = browser.getText('.selected .dk5')[0];
    assert.equal(text, '40-49', 'title is present');

    const level = browser.getText('.selected .hierarchy-level')[0];
    assert.include(level, 'Geografi og rejser i alm.', 'sublevel is present');
  });

  it('Click on sublevel', () => {
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);
    browser.click('[href="#!/hierarchy/40"]');
    browser.pause(1000);

    const sel = browser.getText('.selected');
    console.log('sel', sel);
    const dk5 = browser.getText('.dk5');
    console.log('dk5', dk5);
    const text2 = browser.getText('.selected .dk5');
    console.log('text2', text2);
    const h2 = browser.getText('.h2');
    console.log('h2', h2);
    const text = browser.getText('.selected h2 .dk5')[0];
    console.log('text', text);
    assert.equal(text, '40', 'title is present');

    const topics = browser.getText('.selected .hierarchy-topics');
    assert.include(topics[0], 'Flyveulykker', 'first topic is present');
  });

  it('Click between levels', () => {
    browser.url('/#!/hierarchy/40-49');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40-49', 'toplevel is selected');

    browser.click('[href="#!/hierarchy/40"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40', '2. level is selected');

    browser.click('[href="#!/hierarchy/40.6"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5'), '40.6', '3. level is selected');

    browser.click('[href="#!/hierarchy/40-49"]');
    browser.pause(1000);
    assert.equal(browser.getText('.selected .dk5')[0], '40-49', 'toplevel is selected');
  });

  it('Should display the first five aspects only', () => {
    browser.url('/#!/hierarchy/40');

    const topics = browser.getText('.selected .hierarchy-topics');

    assert.include(topics[0], 'Atlanterhavet\nBjerge\nFlyveulykker\nGeografi\nHave');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.notInclude(topics[0], 'Lande');
  });

  it('Should display all aspects when "Vis Flere" is clicked', () => {
    browser.url('/#!/hierarchy/40');

    let topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(browser.getText('.selected .hidden .hierarchy-topics'), 0);

    browser.click('.toggle-button');

    topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(topics[1].split('\n'), 12);
    assert.isTrue(browser.isExisting('.selected .show .hierarchy-topics'));
  });

  it('Should hide all but 5 aspects when "Vis Flere" is clicked twice', () => {
    browser.url('/#!/hierarchy/40');

    browser.click('.toggle-button');

    let topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(topics[1].split('\n'), 12);
    assert.isTrue(browser.isExisting('.selected .show .hierarchy-topics'));

    browser.click('.toggle-button');

    topics = browser.getText('.selected .hierarchy-topics');
    assert.lengthOf(topics[0].split('\n'), 5);
    assert.lengthOf(browser.getText('.selected .hidden .hierarchy-topics'), 0);
  });
});
<|endoftext|>


<commit_before>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Throws an assertion error if mutually exclusive options are specified.
 *
 * @example
 * assertMutuallyExclusiveOptions( { tree:1, flower:2 }, [ 'tree' ], [ 'flower' ] ) => error
 * assertMutuallyExclusiveOptions( { flower:2 }, [ 'tree' ], [ 'flower' ] ) => no error
 * assertMutuallyExclusiveOptions( { tree:1 }, [ 'tree' ], [ 'flower' ] ) => no error
 * assertMutuallyExclusiveOptions( { tree:1, mountain:2 }, [ 'tree', 'mountain' ], [ 'flower' ] ) => no error
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */

import phetCore from './phetCore.js';

/**
 * @param {Object|null|undefined} options - an options object.  Could be before or after merge, and may therefore
 *                                        - be null or undefined
 * @param {string[][]} sets - families of mutually exclusive option keys, see examples above.
 */
const assertMutuallyExclusiveOptions = function( options, ...sets ) {
  if ( assert && options ) {

    // Determine which options are used from each set
    const usedElementsFromEachSet = sets.map( set => Object.keys( _.pick( options, ...set ) ) );

    // If any element is used from more than one set...
    if ( usedElementsFromEachSet.filter( usedElements => usedElements.length > 0 ).length > 1 ) {

      // Output the errant options.
      assert && assert( false, `Cannot simultaneously specify ${usedElementsFromEachSet.join( ' and ' )}` );
    }
  }
};

phetCore.register( 'assertMutuallyExclusiveOptions', assertMutuallyExclusiveOptions );
export default assertMutuallyExclusiveOptions;<commit_msg>Fix type annotation<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Throws an assertion error if mutually exclusive options are specified.
 *
 * @example
 * assertMutuallyExclusiveOptions( { tree:1, flower:2 }, [ 'tree' ], [ 'flower' ] ) => error
 * assertMutuallyExclusiveOptions( { flower:2 }, [ 'tree' ], [ 'flower' ] ) => no error
 * assertMutuallyExclusiveOptions( { tree:1 }, [ 'tree' ], [ 'flower' ] ) => no error
 * assertMutuallyExclusiveOptions( { tree:1, mountain:2 }, [ 'tree', 'mountain' ], [ 'flower' ] ) => no error
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */

import phetCore from './phetCore.js';

/**
 * @param {Object|null|undefined} options - an options object.  Could be before or after merge, and may therefore
 *                                        - be null or undefined
 * @param {...string[]} sets - families of mutually exclusive option keys, see examples above.
 */
const assertMutuallyExclusiveOptions = function( options, ...sets ) {
  if ( assert && options ) {

    // Determine which options are used from each set
    const usedElementsFromEachSet = sets.map( set => Object.keys( _.pick( options, ...set ) ) );

    // If any element is used from more than one set...
    if ( usedElementsFromEachSet.filter( usedElements => usedElements.length > 0 ).length > 1 ) {

      // Output the errant options.
      assert && assert( false, `Cannot simultaneously specify ${usedElementsFromEachSet.join( ' and ' )}` );
    }
  }
};

phetCore.register( 'assertMutuallyExclusiveOptions', assertMutuallyExclusiveOptions );
export default assertMutuallyExclusiveOptions;<|endoftext|>


<commit_before>/*!
 NAAM VAN ONS PROJECT, v1.0
 Created by Kiani Lannoye & Gilles Vandewiele, commissioned by UZ Ghent
 https://github.com/kianilannoye/Chronicals

 This file contains the controller to add and modify headaches.
 */

angular.module('Chronic').controller('headacheController', function($scope, dataService){

    ons.ready(function() {
        $('.hidden').removeClass("hidden");
    });

  $scope.headache = dataService.getCurrentHeadache();

  if($scope.headache == null){
  	$scope.headache = { intensityValues: [], end: null, location: null, triggers: dataService.getTriggers(), symptoms: dataService.getSymptoms()};
  }

  $scope.setEnd = function(endDate, endTime){
  	if(endDate != null && endTime != null){
  		$scope.headache.end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate(), endTime.getHours(), endTime.getMinutes());
  	}
  };

  if($scope.headache != null){
	  if($scope.end != null){
	  	$scope.end = new Date($scope.headache.end);
	  	$scope.endDate = $scope.end;
	  	$scope.endTime = $scope.end;
	  }
  } else $scope.end = null;

  $scope.setEndDate = function(endDate){
  	if(endDate != null){
  		if($scope.end == null) $scope.end = new Date();
	  	$scope.end.setFullYear(endDate.getFullYear());
	  	$scope.end.setMonth(endDate.getMonth());
	  	$scope.end.setDate(endDate.getDate());
  	}
  };

  $scope.setEndTime = function(endTime){
  	if(endTime != null){
  		if($scope.end == null) $scope.end = new Date();
	  	$scope.end.setHours(endTime.getHours());
	  	$scope.end.setMinutes(endTime.getMinutes());
	}
  };

  $scope.getIndexOfHeadache = function(){
  	headaches = dataService.getHeadacheList();
  	if(headaches == null || headaches.length == 0) return -1;
  	for(headache in headaches){
  		equalIntensityValues = true;
  		for(value in headache.intensityValues){
  			if(headache.intensityValues[value] != $scope.headache.intensityValues[value]) equalIntensityValues = false;
  		}
  		equalEnd = $scope.headache.end == headaches[headache].end;
  		equalLocation = $scope.headache.location == headaches[headache].location;
  		equalSymptoms = true;
  		for(symptom in headache.symptoms){
  			if(headache.symptoms[symptom] != $scope.headache.symptoms[symptom]) equalSymptoms = false;
  		}
  		equalTriggers = true;
  		for(trigger in headache.triggers){
  			if(headache.triggers[trigger] != $scope.headache.triggers[trigger]) equalTriggers = false;
  		}
  		if(equalIntensityValues && equalEnd && equalLocation && equalSymptoms && equalTriggers) return headache;
  		return -1;
  	}
  };

  $scope.headacheIndex = $scope.getIndexOfHeadache();

  /* Create a nice short time string from the start date and time */

  $scope.updateStartTimeString = function(){
  	if($scope.headache.intensityValues[0] == null) {
  		$scope.startTimeString = "";
  		return;
  	}
  	var months = ["jan.", "feb.", "mrt.", "apr.", "mei", "jun.", "jul.", "aug.", "sept.", "okt.", "nov.", "dec."];
  	var month = months[(new Date($scope.headache.intensityValues[0].key).getMonth())];
  	var day = (new Date($scope.headache.intensityValues[0].key).getDate().toString());
  	var hour =(new Date($scope.headache.intensityValues[0].key).getHours().toString());
  	if(hour < 10) hour = "0"+hour;
  	var minute = new Date($scope.headache.intensityValues[0].key).getMinutes().toString();
  	if(minute < 10) minute = "0"+minute;
  	$scope.startTimeString = day + " " + month + " " + hour + ":" + minute;
  };

  $scope.$watch('headache.intensityValues[0]', $scope.updateStartTimeString);

  /* closeAndSave is called when the user pressed the "Sla op" button */

  $scope.closeAndSave = function(){

  	if($scope.headacheIndex != -1){
	  	list = dataService.getHeadacheList();
	  	list[$scope.headacheIndex] = $scope.headache;
	  	dataService.setHeadacheList(list);
  	} else dataService.addHeadache($scope.headache);

  	dataService.setCurrentHeadache(null);
  	location.href="dashboard.html";
  };

  $scope.cancel = function(){
  	dataService.setCurrentHeadache(null);
  	location.href="dashboard.html";
  };

  /* Some ugly hack with jQuery to link a popover the the corresponding help buttons */
  $scope.message = "";

  var searchIndexById = function(list, id){
  	// Search the index of an id in a list of objects with ids
  	for(object in list){
  		if(list[object].id == id) return object;
  	}
  	return -1;
  };

  for(trigger in $scope.headache.triggers){ // Close your eyes and pretend this is not here ;)
  	// Initialize function on each helpButton for each trigger
  	$(document).on("click", '#helpButtonTrigger'+$scope.headache.triggers[trigger].id, function(){
  		var id = ($(this)[0].id).split('helpButtonTrigger');
  		var index = searchIndexById($scope.headache.triggers, id[1]);
  		$scope.message = $scope.headache.triggers[index].description;
  		$scope.popover.show("#"+$(this)[0].id);
  	});
  };

  for(symptom in $scope.headache.symptoms){ // Close your eyes and pretend this is not here ;)
  	// Initialize function on each helpButton for each trigger
  	$(document).on("click", '#helpButtonSymptom'+$scope.headache.symptoms[symptom].id, function(){
  		var id = ($(this)[0].id).split('helpButtonSymptom');
  		var index = searchIndexById($scope.headache.symptoms, id[1]);
  		$scope.message = $scope.headache.symptoms[index].description;
  		$scope.popover.show("#"+$(this)[0].id);
  	});
  };

  $(document).on("click", '.popover', function(){
  	$scope.popover.hide();
  });

  document.addEventListener("backbutton", onBackKeyDown, false);
	function onBackKeyDown(e) {
		alert("dit is een test");
	  e.preventDefault();
	}

  ons.createPopover('popover.html').then(function(popover) {
  	// Create a popover for the help buttons
    $scope.popover = popover;
  });

  /* All variables and functions used to add intensity values and delete them */

  $scope.newHeadacheValue;
  $scope.newHeadacheDate;
  $scope.newHeadacheTime;

  $scope.deleteEntry = function(item){
  	$scope.headache.intensityValues.splice($scope.headache.intensityValues.indexOf(item), 1);
  	if($scope.headache.intensityValues.length == 0) $("#endDateForm").hide();
  };

    $scope.setNewHeadacheValue = function(newValue){
        $scope.newHeadacheValue = newValue;
    };

  $scope.addIntensityValue = function(){
  	/* This function is called when we want to add an Intensity Value (it doesn't add it to the list yet...) */
  	$scope.newHeadacheValue = 5;
  	$scope.newHeadacheDate = new Date();
  	$scope.newHeadacheTime = $scope.newHeadacheDate;
  };

  $scope.saveIntensityValue = function(navigator, page){
  	var start = new Date($scope.newHeadacheDate.getFullYear(), $scope.newHeadacheDate.getMonth(), $scope.newHeadacheDate.getDate(), $scope.newHeadacheTime.getHours(), $scope.newHeadacheTime.getMinutes());
	$scope.headache.intensityValues.push({key: start, value: $scope.newHeadacheValue});
	$scope.headache.intensityValues.sort(function(a, b){
		if(a.key < b.key) return -1;
		if(a.key > b.key) return 1;
		else return 0;
	});
  	if($scope.headache.intensityValues.length == 1) $("#endDateForm").show();
  	navigator.popPage(page); // We're in the add intensity form. Popping a page will return to the list intensity form
  };

    $scope.setValues = function (v, d, t) {
        $scope.newHeadacheValue = v;
        $scope.newHeadacheDate = d;
        $scope.newHeadacheTime = t;
    };

});

angular.module('Chronic').directive('ngModel', function( $filter ) {
	// This is used to remove seconds and milliseconds in time pickers
    return {
        require: '?ngModel',
        link: function(scope, elem, attr, ngModel) {
            if( !ngModel )
                return;
            if( attr.type !== 'time' )
                return;

            ngModel.$formatters.unshift(function(value) {
                return value.replace(/(:\d\d)(:.*)$/, '\$1');
            });
        }
    };
});

angular.module('Chronic').directive('validenddate', function() {
  return {
    require: 'ngModel',
    link: function($scope, ele, attrs, c) {
  		console.log($("#"+attrs.button));
    	c.$validators.validEndDate = function(modelValue, viewValue){
	    	$scope.setEndDate(modelValue);
	    	if(c.$isEmpty(modelValue)) {
		        // consider empty models to be valid
		        $("."+attrs.button).prop('disabled', false);
		        return true;
	    	}
	    	if($scope.headache.intensityValues.length != 0 && $scope.end >= $scope.headache.intensityValues[$scope.headache.intensityValues.length-1].key){
		        $("."+attrs.button).prop('disabled', false);
	    		return true;
	    	}
		    $("."+attrs.button).prop('disabled', true);
	    	return false;
		};
    }
  };
});

angular.module('Chronic').directive('validendtime', function() {
  return {
    require: 'ngModel',
    link: function($scope, ele, attrs, c) {
    	c.$validators.validEndTime = function(modelValue, viewValue){
        	$scope.setEndTime(modelValue);
	    	if(c.$isEmpty(modelValue)) {
		        // consider empty models to be valid
		        $("."+attrs.button).prop('disabled', false);
		        return true;
        	}
        	if($scope.headache.intensityValues.length != 0 && $scope.end >= new Date($scope.headache.intensityValues[$scope.headache.intensityValues.length-1].key)){
		        $("."+attrs.button).prop('disabled', false);
        		return true;
        	}
		    $("."+attrs.button).prop('disabled', true);
        	return false;
    	};
    }
  };
});
<commit_msg>duplicate headaches?<commit_after>/*!
 NAAM VAN ONS PROJECT, v1.0
 Created by Kiani Lannoye & Gilles Vandewiele, commissioned by UZ Ghent
 https://github.com/kianilannoye/Chronicals

 This file contains the controller to add and modify headaches.
 */

angular.module('Chronic').controller('headacheController', function($scope, dataService){

    ons.ready(function() {
        $('.hidden').removeClass("hidden");
    });

  $scope.headache = dataService.getCurrentHeadache();

  if($scope.headache == null){
  	$scope.headache = { intensityValues: [], end: null, location: null, triggers: dataService.getTriggers(), symptoms: dataService.getSymptoms()};
  }

  $scope.setEnd = function(endDate, endTime){
  	if(endDate != null && endTime != null){
  		$scope.headache.end = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate(), endTime.getHours(), endTime.getMinutes(), endTime.getSeconds());
  	}
  };

  if($scope.headache != null){
	  if($scope.end != null){
	  	$scope.end = new Date($scope.headache.end);
	  	$scope.endDate = $scope.end;
	  	$scope.endTime = $scope.end;
	  }
  } else $scope.end = null;

  $scope.setEndDate = function(endDate){
  	if(endDate != null){
  		if($scope.end == null) $scope.end = new Date();
	  	$scope.end.setFullYear(endDate.getFullYear());
	  	$scope.end.setMonth(endDate.getMonth());
	  	$scope.end.setDate(endDate.getDate());
  	}
  };

  $scope.setEndTime = function(endTime){
  	if(endTime != null){
  		if($scope.end == null) $scope.end = new Date();
	  	$scope.end.setHours(endTime.getHours());
	  	$scope.end.setMinutes(endTime.getMinutes());
	  	$scope.end.setSeconds(endTime.getSeconds());
	}
  };

  $scope.getIndexOfHeadache = function(){
  	headaches = dataService.getHeadacheList();
  	if(headaches == null || headaches.length == 0) return -1;
  	for(headache in headaches){
  		equalIntensityValues = true;
  		for(value in headache.intensityValues){
  			if(headache.intensityValues[value] != $scope.headache.intensityValues[value]) equalIntensityValues = false;
  		}
  		equalEnd = $scope.headache.end == headaches[headache].end;
  		equalLocation = $scope.headache.location == headaches[headache].location;
  		equalSymptoms = true;
  		for(symptom in headache.symptoms){
  			if(headache.symptoms[symptom] != $scope.headache.symptoms[symptom]) equalSymptoms = false;
  		}
  		equalTriggers = true;
  		for(trigger in headache.triggers){
  			if(headache.triggers[trigger] != $scope.headache.triggers[trigger]) equalTriggers = false;
  		}
  		if(equalIntensityValues && equalEnd && equalLocation && equalSymptoms && equalTriggers) return headache;
  		return -1;
  	}
  };

  $scope.headacheIndex = $scope.getIndexOfHeadache();

  /* Create a nice short time string from the start date and time */

  $scope.updateStartTimeString = function(){
  	if($scope.headache.intensityValues[0] == null) {
  		$scope.startTimeString = "";
  		return;
  	}
  	var months = ["jan.", "feb.", "mrt.", "apr.", "mei", "jun.", "jul.", "aug.", "sept.", "okt.", "nov.", "dec."];
  	var month = months[(new Date($scope.headache.intensityValues[0].key).getMonth())];
  	var day = (new Date($scope.headache.intensityValues[0].key).getDate().toString());
  	var hour =(new Date($scope.headache.intensityValues[0].key).getHours().toString());
  	if(hour < 10) hour = "0"+hour;
  	var minute = new Date($scope.headache.intensityValues[0].key).getMinutes().toString();
  	if(minute < 10) minute = "0"+minute;
  	$scope.startTimeString = day + " " + month + " " + hour + ":" + minute;
  };

  $scope.$watch('headache.intensityValues[0]', $scope.updateStartTimeString);

  /* closeAndSave is called when the user pressed the "Sla op" button */

  $scope.closeAndSave = function(){

  	if($scope.headacheIndex != -1){
	  	list = dataService.getHeadacheList();
	  	list[$scope.headacheIndex] = $scope.headache;
	  	dataService.setHeadacheList(list);
  	} else dataService.addHeadache($scope.headache);

  	dataService.setCurrentHeadache(null);
  	location.href="dashboard.html";
  };

  $scope.cancel = function(){
  	dataService.setCurrentHeadache(null);
  	location.href="dashboard.html";
  };

  /* Some ugly hack with jQuery to link a popover the the corresponding help buttons */
  $scope.message = "";

  var searchIndexById = function(list, id){
  	// Search the index of an id in a list of objects with ids
  	for(object in list){
  		if(list[object].id == id) return object;
  	}
  	return -1;
  };

  for(trigger in $scope.headache.triggers){ // Close your eyes and pretend this is not here ;)
  	// Initialize function on each helpButton for each trigger
  	$(document).on("click", '#helpButtonTrigger'+$scope.headache.triggers[trigger].id, function(){
  		var id = ($(this)[0].id).split('helpButtonTrigger');
  		var index = searchIndexById($scope.headache.triggers, id[1]);
  		$scope.message = $scope.headache.triggers[index].description;
  		$scope.popover.show("#"+$(this)[0].id);
  	});
  };

  for(symptom in $scope.headache.symptoms){ // Close your eyes and pretend this is not here ;)
  	// Initialize function on each helpButton for each trigger
  	$(document).on("click", '#helpButtonSymptom'+$scope.headache.symptoms[symptom].id, function(){
  		var id = ($(this)[0].id).split('helpButtonSymptom');
  		var index = searchIndexById($scope.headache.symptoms, id[1]);
  		$scope.message = $scope.headache.symptoms[index].description;
  		$scope.popover.show("#"+$(this)[0].id);
  	});
  };

  $(document).on("click", '.popover', function(){
  	$scope.popover.hide();
  });

  document.addEventListener("backbutton", onBackKeyDown, false);
	function onBackKeyDown(e) {
		alert("dit is een test");
	  e.preventDefault();
	}

  ons.createPopover('popover.html').then(function(popover) {
  	// Create a popover for the help buttons
    $scope.popover = popover;
  });

  /* All variables and functions used to add intensity values and delete them */

  $scope.newHeadacheValue;
  $scope.newHeadacheDate;
  $scope.newHeadacheTime;

  $scope.deleteEntry = function(item){
  	$scope.headache.intensityValues.splice($scope.headache.intensityValues.indexOf(item), 1);
  	if($scope.headache.intensityValues.length == 0) $("#endDateForm").hide();
  };

    $scope.setNewHeadacheValue = function(newValue){
        $scope.newHeadacheValue = newValue;
    };

  $scope.addIntensityValue = function(){
  	/* This function is called when we want to add an Intensity Value (it doesn't add it to the list yet...) */
  	$scope.newHeadacheValue = 5;
  	$scope.newHeadacheDate = new Date();
  	$scope.newHeadacheTime = $scope.newHeadacheDate;
  };

  $scope.saveIntensityValue = function(navigator, page){
  	var start = new Date($scope.newHeadacheDate.getFullYear(), $scope.newHeadacheDate.getMonth(), $scope.newHeadacheDate.getDate(), $scope.newHeadacheTime.getHours(), $scope.newHeadacheTime.getMinutes(), $scope.newHeadacheTime.getSeconds());
	$scope.headache.intensityValues.push({key: start, value: $scope.newHeadacheValue});
	$scope.headache.intensityValues.sort(function(a, b){
		if(a.key < b.key) return -1;
		if(a.key > b.key) return 1;
		else return 0;
	});
  	if($scope.headache.intensityValues.length == 1) $("#endDateForm").show();
  	navigator.popPage(page); // We're in the add intensity form. Popping a page will return to the list intensity form
  };

    $scope.setValues = function (v, d, t) {
        $scope.newHeadacheValue = v;
        $scope.newHeadacheDate = d;
        $scope.newHeadacheTime = t;
    };

});

angular.module('Chronic').directive('ngModel', function( $filter ) {
	// This is used to remove seconds and milliseconds in time pickers
    return {
        require: '?ngModel',
        link: function(scope, elem, attr, ngModel) {
            if( !ngModel )
                return;
            if( attr.type !== 'time' )
                return;

            ngModel.$formatters.unshift(function(value) {
                return value.replace(/(:\d\d)(:.*)$/, '\$1');
            });
        }
    };
});

angular.module('Chronic').directive('validenddate', function() {
  return {
    require: 'ngModel',
    link: function($scope, ele, attrs, c) {
  		console.log($("#"+attrs.button));
    	c.$validators.validEndDate = function(modelValue, viewValue){
	    	$scope.setEndDate(modelValue);
	    	if(c.$isEmpty(modelValue)) {
		        // consider empty models to be valid
		        $("."+attrs.button).prop('disabled', false);
		        return true;
	    	}
	    	if($scope.headache.intensityValues.length != 0 && $scope.end >= $scope.headache.intensityValues[$scope.headache.intensityValues.length-1].key){
		        $("."+attrs.button).prop('disabled', false);
	    		return true;
	    	}
		    $("."+attrs.button).prop('disabled', true);
	    	return false;
		};
    }
  };
});

angular.module('Chronic').directive('validendtime', function() {
  return {
    require: 'ngModel',
    link: function($scope, ele, attrs, c) {
    	c.$validators.validEndTime = function(modelValue, viewValue){
        	$scope.setEndTime(modelValue);
	    	if(c.$isEmpty(modelValue)) {
		        // consider empty models to be valid
		        $("."+attrs.button).prop('disabled', false);
		        return true;
        	}
        	if($scope.headache.intensityValues.length != 0 && $scope.end >= new Date($scope.headache.intensityValues[$scope.headache.intensityValues.length-1].key)){
		        $("."+attrs.button).prop('disabled', false);
        		return true;
        	}
		    $("."+attrs.button).prop('disabled', true);
        	return false;
    	};
    }
  };
});
<|endoftext|>


<commit_before>/**
 * Developer: Stepan Burguchev
 * Date: 8/20/2014
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 * Published under the MIT license
 */

"use strict";

import '../../libApi';
import template from '../templates/gridheader.hbs';
import GlobalEventService from '../../services/GlobalEventService';

/*
*
* Fires: columnsResize(this, { changes: { 0: 0.5, 1: 0.5 } })
*
* */

/**
 * @name GridHeaderView
 * @memberof module:core.list.views
 * @class GridHeaderView
 * @constructor
 * @description View используемый для отображения заголовка (шапки) списка
 * @extends Marionette.ItemView
 * @param {Object} options Constructor options
 * @param {Array} options.columns массив колонок
 * @param {Object} options.gridEventAggregator ?
 * @param {Backbone.View} options.gridColumnHeaderView View используемый для отображения заголовка (шапки) списка
 * */
let GridHeaderView = Marionette.ItemView.extend({
    initialize: function (options) {
        if (!options.columns) {
            throw new Error('You must provide columns definition ("columns" option)');
        }
        if (!options.gridEventAggregator) {
            throw new Error('You must provide grid event aggregator ("gridEventAggregator" option)');
        }
        if (!options.gridColumnHeaderView) {
            throw new Error('You must provide grid column header view ("gridColumnHeaderView" option)');
        }

        this.gridEventAggregator = options.gridEventAggregator;
        this.gridColumnHeaderView = options.gridColumnHeaderView;
        this.gridColumnHeaderViewOptions = options.gridColumnHeaderViewOptions;
        this.columns = options.columns;
        this.$document = $(document);
        _.bindAll(this, '__draggerMouseUp', '__draggerMouseMove', '__handleResizeInternal', '__handleColumnSort', 'handleResize');
        this.listenTo(GlobalEventService, 'resize', this.handleResize);
    },

    template: template,

    className: 'grid-header',

    ui: {
        gridHeaderColumn: '.grid-header-column',
        gridHeaderColumnContent: '.grid-header-column-content-view'
    },

    events: {
        'mousedown .grid-header-dragger': '__handleDraggerMousedown'
    },

    constants: {
        MIN_COLUMN_WIDTH: 20
    },

    templateHelpers: function () {
        return {
            columns: this.columns
        };
    },

    onRender: function () {
        var self = this;
        this.ui.gridHeaderColumnContent.each(function (i, el) {
            var column = self.columns[i];
            var view = new self.gridColumnHeaderView(_.extend(self.gridColumnHeaderViewOptions || {}, {
                model: column.viewModel,
                column: column
            }));
            self.listenTo(view, 'columnSort', self.__handleColumnSort);
            var childEl = view.render().el;
            el.appendChild(childEl);
        });
    },

    onShow: function () {
        this.__handleResizeInternal();
    },

    updateSorting: function () {
        this.render();
        this.__handleResizeInternal();
    },

    resizeColumns () {
        this.__handleResize();
    },

    __handleColumnSort: function (sender, args) {
        var column = args.column;
        var sorting = column.sorting;
        var comparator;
        _.each(this.columns, function (c) {
            c.sorting = null;
        });
        switch (sorting) {
            case 'asc':
                column.sorting = 'desc';
                comparator = column.sortDesc;
                break;
            case 'desc':
                column.sorting = 'asc';
                comparator = column.sortAsc;
                break;
            default:
                column.sorting = 'asc';
                comparator = column.sortAsc;
                break;
        }
        this.updateSorting();

        this.trigger('onColumnSort', column, comparator);
    },

    __handleDraggerMousedown: function (e) {
        this.__stopDrag();
        this.__startDrag(e);
        return false;
    },

    __getElementOuterWidth: function (el) {
        return $(el)[0].getBoundingClientRect().width;
    },

    __startDrag: function (e) {
        var $dragger = $(e.target);
        var $column = $dragger.parent();

        var affectedColumns = _.chain($column.nextAll()).toArray().map(function (el) {
            return {
                $el: $(el),
                initialWidth: this.__getElementOuterWidth(el)
            };
        }, this).value();
        var draggedColumn = {
            $el: $column,
            initialWidth: this.__getElementOuterWidth($column),
            index: $column.index()
        };
        var unaffectedWidth = _.reduce($column.prevAll(), function (m, v) {
            return m + this.__getElementOuterWidth(v);
        }.bind(this), 0);
        var fullWidth = this.__getFullWidth();

        this.dragContext = {
            pageOffsetX: e.pageX,
            $dragger: $dragger,
            fullWidth: fullWidth,
            unaffectedWidth: unaffectedWidth,
            draggedColumn: draggedColumn,
            affectedColumns: affectedColumns,
            maxColumnWidth: fullWidth - affectedColumns.length * this.constants.MIN_COLUMN_WIDTH - unaffectedWidth
        };

        $dragger.addClass('active');
        this.$document.mousemove(this.__draggerMouseMove).mouseup(this.__draggerMouseUp);
    },

    __stopDrag: function () {
        if (!this.dragContext) {
            return;
        }

        this.dragContext.$dragger.removeClass('active');
        this.dragContext = null;
        this.$document.unbind('mousemove', this.__draggerMouseMove);
        this.$document.unbind('mouseup', this.__draggerMouseUp);
    },

    __draggerMouseMove: function (e) {
        if (!this.dragContext) {
            return;
        }

        var ctx = this.dragContext;
        let delta = e.pageX - ctx.pageOffsetX;
        if (delta !== 0) {
            var draggedColumn = ctx.draggedColumn;
            var index = ctx.draggedColumn.index;
            var changes = {};

            this.columns[index].absWidth = Math.min(ctx.maxColumnWidth, Math.max(this.constants.MIN_COLUMN_WIDTH, draggedColumn.initialWidth + delta));
            delta = this.columns[index].absWidth - draggedColumn.initialWidth;
            draggedColumn.$el.outerWidth(this.columns[index].absWidth);

            var newColumnWidthPc = this.columns[index].absWidth / ctx.fullWidth;
            this.columns[index].width = newColumnWidthPc;
            changes[index] = this.columns[index].absWidth;
                index++;

            var affectedColumnsWidth = ctx.fullWidth - ctx.unaffectedWidth - draggedColumn.initialWidth,
                sumDelta = 0,
                sumGap = 0;

            for (let i = 0;  i < ctx.affectedColumns.length; i++) {
                let c = ctx.affectedColumns[i],
                    newColumnWidth = c.initialWidth - delta * c.initialWidth / affectedColumnsWidth;

                if (newColumnWidth < this.constants.MIN_COLUMN_WIDTH) {
                    sumDelta += this.constants.MIN_COLUMN_WIDTH - newColumnWidth;
                    newColumnWidth = this.constants.MIN_COLUMN_WIDTH;
                } else {
                    sumGap += newColumnWidth - this.constants.MIN_COLUMN_WIDTH;
                }

                this.columns[index].absWidth = newColumnWidth;
                index++;
            }

            var fullSum = 0;
            index = ctx.draggedColumn.index + 1;
            for (let i = 0; i < ctx.affectedColumns.length; i++) {
                let c = ctx.affectedColumns[i];
                if (sumDelta > 0 && this.columns[index].absWidth > this.constants.MIN_COLUMN_WIDTH) {
                    let delta = (this.columns[index].absWidth - this.constants.MIN_COLUMN_WIDTH) * sumDelta / sumGap;
                    this.columns[index].absWidth -= delta;
                }

                fullSum += this.columns[index].absWidth;

                if (i === ctx.affectedColumns.length - 1) {
                    var sumDelta = ctx.fullWidth - ctx.unaffectedWidth - this.columns[ctx.draggedColumn.index].absWidth - fullSum;
                    this.columns[index].absWidth += sumDelta;
                }

                var newColumnWidthPc = this.columns[index].absWidth / ctx.fullWidth;
                this.columns[index].width = newColumnWidthPc;
                c.$el.outerWidth(this.columns[index].absWidth);
                changes[index] = this.columns[index].absWidth;
                index++;
            }

            this.gridEventAggregator.trigger('columnsResize');
        }

        return false;
    },

    __draggerMouseUp: function () {
        this.__stopDrag();
        return false;
    },

    handleResize: function () {
        this.__handleResizeInternal();
        this.gridEventAggregator.trigger('columnsResize');
    },

    __getFullWidth: function () {
        return this.$el.parent().width() - 2; // Magic cross browser pixels, don't remove them
    },

    __handleResizeInternal: function () {
        var fullWidth = this.__getFullWidth(), // Grid header's full width
            columnWidth = fullWidth / this.columns.length, // Default column width
            sumWidth = 0; // Columns' sum width

        // Iterate all but first columns counting their sum width
        this.ui.gridHeaderColumn.not(':first').each(function (i, el) {
            var child = $(el);
            var col = this.columns[i + 1];
            if (col.width) { // If column has it's custom width
                col.absWidth = Math.floor(col.width * fullWidth); // Calculate absolute custom column width (rounding it down)
            } else {
                col.absWidth = Math.floor(columnWidth); // Otherwise take default column width (rounding it down)
            }
            child.outerWidth(col.absWidth); // Set absolute column width
            sumWidth += col.absWidth; // And add it to columns' sum width
        }.bind(this));

        // Take remaining (or only) first column to calculate it's absolute width as difference between grid header's full width and
        // other columns' sum width. This logic is necessary because other columns' widths may have been rounded down during calculations
        if (this.columns.length) {
            this.columns[0].absWidth = Math.floor(fullWidth - sumWidth); // Calculate remainig (or only) first column's absolute width
            this.ui.gridHeaderColumn.first().outerWidth(this.columns[0].absWidth); // And set it
        }
    }
});

export default GridHeaderView;
<commit_msg>Revert "Grid column resizing API method"<commit_after>/**
 * Developer: Stepan Burguchev
 * Date: 8/20/2014
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 * Published under the MIT license
 */

"use strict";

import '../../libApi';
import template from '../templates/gridheader.hbs';
import GlobalEventService from '../../services/GlobalEventService';

/*
*
* Fires: columnsResize(this, { changes: { 0: 0.5, 1: 0.5 } })
*
* */

/**
 * @name GridHeaderView
 * @memberof module:core.list.views
 * @class GridHeaderView
 * @constructor
 * @description View используемый для отображения заголовка (шапки) списка
 * @extends Marionette.ItemView
 * @param {Object} options Constructor options
 * @param {Array} options.columns массив колонок
 * @param {Object} options.gridEventAggregator ?
 * @param {Backbone.View} options.gridColumnHeaderView View используемый для отображения заголовка (шапки) списка
 * */
let GridHeaderView = Marionette.ItemView.extend({
    initialize: function (options) {
        if (!options.columns) {
            throw new Error('You must provide columns definition ("columns" option)');
        }
        if (!options.gridEventAggregator) {
            throw new Error('You must provide grid event aggregator ("gridEventAggregator" option)');
        }
        if (!options.gridColumnHeaderView) {
            throw new Error('You must provide grid column header view ("gridColumnHeaderView" option)');
        }

        this.gridEventAggregator = options.gridEventAggregator;
        this.gridColumnHeaderView = options.gridColumnHeaderView;
        this.gridColumnHeaderViewOptions = options.gridColumnHeaderViewOptions;
        this.columns = options.columns;
        this.$document = $(document);
        _.bindAll(this, '__draggerMouseUp', '__draggerMouseMove', '__handleResizeInternal', '__handleColumnSort', 'handleResize');
        this.listenTo(GlobalEventService, 'resize', this.handleResize);
    },

    template: template,

    className: 'grid-header',

    ui: {
        gridHeaderColumn: '.grid-header-column',
        gridHeaderColumnContent: '.grid-header-column-content-view'
    },

    events: {
        'mousedown .grid-header-dragger': '__handleDraggerMousedown'
    },

    constants: {
        MIN_COLUMN_WIDTH: 20
    },

    templateHelpers: function () {
        return {
            columns: this.columns
        };
    },

    onRender: function () {
        var self = this;
        this.ui.gridHeaderColumnContent.each(function (i, el) {
            var column = self.columns[i];
            var view = new self.gridColumnHeaderView(_.extend(self.gridColumnHeaderViewOptions || {}, {
                model: column.viewModel,
                column: column
            }));
            self.listenTo(view, 'columnSort', self.__handleColumnSort);
            var childEl = view.render().el;
            el.appendChild(childEl);
        });
    },

    onShow: function () {
        this.__handleResizeInternal();
    },

    updateSorting: function () {
        this.render();
        this.__handleResizeInternal();
    },

    __handleColumnSort: function (sender, args) {
        var column = args.column;
        var sorting = column.sorting;
        var comparator;
        _.each(this.columns, function (c) {
            c.sorting = null;
        });
        switch (sorting) {
            case 'asc':
                column.sorting = 'desc';
                comparator = column.sortDesc;
                break;
            case 'desc':
                column.sorting = 'asc';
                comparator = column.sortAsc;
                break;
            default:
                column.sorting = 'asc';
                comparator = column.sortAsc;
                break;
        }
        this.updateSorting();

        this.trigger('onColumnSort', column, comparator);
    },

    __handleDraggerMousedown: function (e) {
        this.__stopDrag();
        this.__startDrag(e);
        return false;
    },

    __getElementOuterWidth: function (el) {
        return $(el)[0].getBoundingClientRect().width;
    },

    __startDrag: function (e) {
        var $dragger = $(e.target);
        var $column = $dragger.parent();

        var affectedColumns = _.chain($column.nextAll()).toArray().map(function (el) {
            return {
                $el: $(el),
                initialWidth: this.__getElementOuterWidth(el)
            };
        }, this).value();
        var draggedColumn = {
            $el: $column,
            initialWidth: this.__getElementOuterWidth($column),
            index: $column.index()
        };
        var unaffectedWidth = _.reduce($column.prevAll(), function (m, v) {
            return m + this.__getElementOuterWidth(v);
        }.bind(this), 0);
        var fullWidth = this.__getFullWidth();

        this.dragContext = {
            pageOffsetX: e.pageX,
            $dragger: $dragger,
            fullWidth: fullWidth,
            unaffectedWidth: unaffectedWidth,
            draggedColumn: draggedColumn,
            affectedColumns: affectedColumns,
            maxColumnWidth: fullWidth - affectedColumns.length * this.constants.MIN_COLUMN_WIDTH - unaffectedWidth
        };

        $dragger.addClass('active');
        this.$document.mousemove(this.__draggerMouseMove).mouseup(this.__draggerMouseUp);
    },

    __stopDrag: function () {
        if (!this.dragContext) {
            return;
        }

        this.dragContext.$dragger.removeClass('active');
        this.dragContext = null;
        this.$document.unbind('mousemove', this.__draggerMouseMove);
        this.$document.unbind('mouseup', this.__draggerMouseUp);
    },

    __draggerMouseMove: function (e) {
        if (!this.dragContext) {
            return;
        }

        var ctx = this.dragContext;
        let delta = e.pageX - ctx.pageOffsetX;
        if (delta !== 0) {
            var draggedColumn = ctx.draggedColumn;
            var index = ctx.draggedColumn.index;
            var changes = {};

            this.columns[index].absWidth = Math.min(ctx.maxColumnWidth, Math.max(this.constants.MIN_COLUMN_WIDTH, draggedColumn.initialWidth + delta));
            delta = this.columns[index].absWidth - draggedColumn.initialWidth;
            draggedColumn.$el.outerWidth(this.columns[index].absWidth);

            var newColumnWidthPc = this.columns[index].absWidth / ctx.fullWidth;
            this.columns[index].width = newColumnWidthPc;
            changes[index] = this.columns[index].absWidth;
                index++;

            var affectedColumnsWidth = ctx.fullWidth - ctx.unaffectedWidth - draggedColumn.initialWidth,
                sumDelta = 0,
                sumGap = 0;

            for (let i = 0;  i < ctx.affectedColumns.length; i++) {
                let c = ctx.affectedColumns[i],
                    newColumnWidth = c.initialWidth - delta * c.initialWidth / affectedColumnsWidth;

                if (newColumnWidth < this.constants.MIN_COLUMN_WIDTH) {
                    sumDelta += this.constants.MIN_COLUMN_WIDTH - newColumnWidth;
                    newColumnWidth = this.constants.MIN_COLUMN_WIDTH;
                } else {
                    sumGap += newColumnWidth - this.constants.MIN_COLUMN_WIDTH;
                }

                this.columns[index].absWidth = newColumnWidth;
                index++;
            }

            var fullSum = 0;
            index = ctx.draggedColumn.index + 1;
            for (let i = 0; i < ctx.affectedColumns.length; i++) {
                let c = ctx.affectedColumns[i];
                if (sumDelta > 0 && this.columns[index].absWidth > this.constants.MIN_COLUMN_WIDTH) {
                    let delta = (this.columns[index].absWidth - this.constants.MIN_COLUMN_WIDTH) * sumDelta / sumGap;
                    this.columns[index].absWidth -= delta;
                }

                fullSum += this.columns[index].absWidth;

                if (i === ctx.affectedColumns.length - 1) {
                    var sumDelta = ctx.fullWidth - ctx.unaffectedWidth - this.columns[ctx.draggedColumn.index].absWidth - fullSum;
                    this.columns[index].absWidth += sumDelta;
                }

                var newColumnWidthPc = this.columns[index].absWidth / ctx.fullWidth;
                this.columns[index].width = newColumnWidthPc;
                c.$el.outerWidth(this.columns[index].absWidth);
                changes[index] = this.columns[index].absWidth;
                index++;
            }

            this.gridEventAggregator.trigger('columnsResize');
        }

        return false;
    },

    __draggerMouseUp: function () {
        this.__stopDrag();
        return false;
    },

    handleResize: function () {
        this.__handleResizeInternal();
        this.gridEventAggregator.trigger('columnsResize');
    },

    __getFullWidth: function () {
        return this.$el.parent().width() - 2; // Magic cross browser pixels, don't remove them
    },

    __handleResizeInternal: function () {
        var fullWidth = this.__getFullWidth(), // Grid header's full width
            columnWidth = fullWidth / this.columns.length, // Default column width
            sumWidth = 0; // Columns' sum width

        // Iterate all but first columns counting their sum width
        this.ui.gridHeaderColumn.not(':first').each(function (i, el) {
            var child = $(el);
            var col = this.columns[i + 1];
            if (col.width) { // If column has it's custom width
                col.absWidth = Math.floor(col.width * fullWidth); // Calculate absolute custom column width (rounding it down)
            } else {
                col.absWidth = Math.floor(columnWidth); // Otherwise take default column width (rounding it down)
            }
            child.outerWidth(col.absWidth); // Set absolute column width
            sumWidth += col.absWidth; // And add it to columns' sum width
        }.bind(this));

        // Take remaining (or only) first column to calculate it's absolute width as difference between grid header's full width and
        // other columns' sum width. This logic is necessary because other columns' widths may have been rounded down during calculations
        if (this.columns.length) {
            this.columns[0].absWidth = Math.floor(fullWidth - sumWidth); // Calculate remainig (or only) first column's absolute width
            this.ui.gridHeaderColumn.first().outerWidth(this.columns[0].absWidth); // And set it
        }
    }
});

export default GridHeaderView;
<|endoftext|>


<commit_before>import { extend } from 'flarum/extend';
import Page from 'flarum/components/Page';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
import icon from 'flarum/helpers/icon';
import DiscussionList from 'flarum/components/DiscussionList';
import WelcomeHero from 'flarum/components/WelcomeHero';
import DiscussionComposer from 'flarum/components/DiscussionComposer';
import LogInModal from 'flarum/components/LogInModal';
import DiscussionPage from 'flarum/components/DiscussionPage';
import Select from 'flarum/components/Select';
import Button from 'flarum/components/Button';
import LinkButton from 'flarum/components/LinkButton';
import SelectDropdown from 'flarum/components/SelectDropdown';

/**
 * The `IndexPage` component displays the index page, including the welcome
 * hero, the sidebar, and the discussion list.
 */
export default class IndexPage extends Page {
  init() {
    super.init();

    // If the user is returning from a discussion page, then take note of which
    // discussion they have just visited. After the view is rendered, we will
    // scroll down so that this discussion is in view.
    if (app.previous instanceof DiscussionPage) {
      this.lastDiscussion = app.previous.discussion;
    }

    // If the user is coming from the discussion list, then they have either
    // just switched one of the parameters (filter, sort, search) or they
    // probably want to refresh the results. We will clear the discussion list
    // cache so that results are reloaded.
    if (app.previous instanceof IndexPage) {
      app.cache.discussionList = null;
    }

    const params = this.params();

    if (app.cache.discussionList) {
      // Compare the requested parameters (sort, search query) to the ones that
      // are currently present in the cached discussion list. If they differ, we
      // will clear the cache and set up a new discussion list component with
      // the new parameters.
      Object.keys(params).some(key => {
        if (app.cache.discussionList.props.params[key] !== params[key]) {
          app.cache.discussionList = null;
          return true;
        }
      });
    }

    if (!app.cache.discussionList) {
      app.cache.discussionList = new DiscussionList({params});
    }

    app.history.push('index', icon('bars'));

    this.bodyClass = 'App--index';
  }

  onunload() {
    // Save the scroll position so we can restore it when we return to the
    // discussion list.
    app.cache.scrollTop = $(window).scrollTop();
  }

  view() {
    return (

        {this.hero()}

{listItems(this.sidebarItems().toArray())}

{listItems(this.viewItems().toArray())}

{listItems(this.actionItems().toArray())}

            {app.cache.discussionList.render()}

    );
  }

  config(isInitialized, context) {
    super.config(...arguments);

    if (isInitialized) return;

    extend(context, 'onunload', () => $('#app').css('min-height', ''));

    app.setTitle('');
    app.setTitleCount(0);

    // Work out the difference between the height of this hero and that of the
    // previous hero. Maintain the same scroll position relative to the bottom
    // of the hero so that the 'fixed' sidebar doesn't jump around.
    const heroHeight = this.$('.Hero').outerHeight();
    const scrollTop = app.cache.scrollTop;

    $('#app').css('min-height', $(window).height() + heroHeight);

    // Scroll to the remembered position. We do this after a short delay so that
    // it happens after the browser has done its own "back button" scrolling,
    // which isn't right. https://github.com/flarum/core/issues/835
    setTimeout(() => $(window).scrollTop(scrollTop - app.cache.heroHeight + heroHeight), 1);

    app.cache.heroHeight = heroHeight;

    // If we've just returned from a discussion page, then the constructor will
    // have set the `lastDiscussion` property. If this is the case, we want to
    // scroll down to that discussion so that it's in view.
    if (this.lastDiscussion) {
      const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);

      if ($discussion.length) {
        const indexTop = $('#header').outerHeight();
        const indexBottom = $(window).height();
        const discussionTop = $discussion.offset().top;
        const discussionBottom = discussionTop + $discussion.outerHeight();

        if (discussionTop < scrollTop + indexTop || discussionBottom > scrollTop + indexBottom) {
          $(window).scrollTop(discussionTop - indexTop);
        }
      }
    }
  }

  /**
   * Get the component to display as the hero.
   *
   * @return {MithrilComponent}
   */
  hero() {
    return WelcomeHero.component();
  }

  /**
   * Build an item list for the sidebar of the index page. By default this is a
   * "New Discussion" button, and then a DropdownSelect component containing a
   * list of navigation items.
   *
   * @return {ItemList}
   */
  sidebarItems() {
    const items = new ItemList();
    const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;

    items.add('newDiscussion',
      Button.component({
        children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
        icon: 'edit',
        className: 'Button Button--primary IndexPage-newDiscussion',
        itemClassName: 'App-primaryControl',
        onclick: this.newDiscussion.bind(this),
        disabled: !canStartDiscussion
      })
    );

    items.add('nav',
      SelectDropdown.component({
        children: this.navItems(this).toArray(),
        buttonClassName: 'Button',
        className: 'App-titleControl'
      })
    );

    return items;
  }

  /**
   * Build an item list for the navigation in the sidebar of the index page. By
   * default this is just the 'All Discussions' link.
   *
   * @return {ItemList}
   */
  navItems() {
    const items = new ItemList();
    const params = this.stickyParams();

    items.add('allDiscussions',
      LinkButton.component({
        href: app.route('index', params),
        children: app.translator.trans('core.forum.index.all_discussions_link'),
        icon: 'comments-o'
      }),
      100
    );

    return items;
  }

  /**
   * Build an item list for the part of the toolbar which is concerned with how
   * the results are displayed. By default this is just a select box to change
   * the way discussions are sorted.
   *
   * @return {ItemList}
   */
  viewItems() {
    const items = new ItemList();

    const sortOptions = {};
    for (const i in app.cache.discussionList.sortMap()) {
      sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
    }

    items.add('sort',
      Select.component({
        options: sortOptions,
        value: this.params().sort,
        onchange: this.changeSort.bind(this)
      })
    );

    return items;
  }

  /**
   * Build an item list for the part of the toolbar which is about taking action
   * on the results. By default this is just a "mark all as read" button.
   *
   * @return {ItemList}
   */
  actionItems() {
    const items = new ItemList();

    items.add('refresh',
      Button.component({
        title: app.translator.trans('core.forum.index.refresh_tooltip'),
        icon: 'refresh',
        className: 'Button Button--icon',
        onclick: () => app.cache.discussionList.refresh()
      })
    );

    if (app.session.user) {
      items.add('markAllAsRead',
        Button.component({
          title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
          icon: 'check',
          className: 'Button Button--icon',
          onclick: this.markAllAsRead.bind(this)
        })
      );
    }

    return items;
  }

  /**
   * Return the current search query, if any. This is implemented to activate
   * the search box in the header.
   *
   * @see Search
   * @return {String}
   */
  searching() {
    return this.params().q;
  }

  /**
   * Redirect to the index page without a search filter. This is called when the
   * 'x' is clicked in the search box in the header.
   *
   * @see Search
   */
  clearSearch() {
    const params = this.params();
    delete params.q;

    m.route(app.route(this.props.routeName, params));
  }

  /**
   * Redirect to the index page using the given sort parameter.
   *
   * @param {String} sort
   */
  changeSort(sort) {
    const params = this.params();

    if (sort === Object.keys(app.cache.discussionList.sortMap())[0]) {
      delete params.sort;
    } else {
      params.sort = sort;
    }

    m.route(app.route(this.props.routeName, params));
  }

  /**
   * Get URL parameters that stick between filter changes.
   *
   * @return {Object}
   */
  stickyParams() {
    return {
      sort: m.route.param('sort'),
      q: m.route.param('q')
    };
  }

  /**
   * Get parameters to pass to the DiscussionList component.
   *
   * @return {Object}
   */
  params() {
    const params = this.stickyParams();

    params.filter = m.route.param('filter');

    return params;
  }

  /**
   * Log the user in and then open the composer for a new discussion.
   *
   * @return {Promise}
   */
  newDiscussion() {
    const deferred = m.deferred();

    if (app.session.user) {
      this.composeNewDiscussion(deferred);
    } else {
      app.modal.show(
        new LogInModal({
          onlogin: this.composeNewDiscussion.bind(this, deferred)
        })
      );
    }

    return deferred.promise;
  }

  /**
   * Initialize the composer for a new discussion.
   *
   * @param {Deferred} deferred
   * @return {Promise}
   */
  composeNewDiscussion(deferred) {
    const component = new DiscussionComposer({user: app.session.user});

    app.composer.load(component);
    app.composer.show();

    deferred.resolve(component);

    return deferred.promise;
  }

  /**
   * Mark all discussions as read.
   *
   * @return void
   */
  markAllAsRead() {
    const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));

    if (confirmation) {
      app.session.user.save({readTime: new Date()});
    }
  }
}
<commit_msg>Fix regression with maintenance of scroll position when jumping between discussion list filters<commit_after>import { extend } from 'flarum/extend';
import Page from 'flarum/components/Page';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
import icon from 'flarum/helpers/icon';
import DiscussionList from 'flarum/components/DiscussionList';
import WelcomeHero from 'flarum/components/WelcomeHero';
import DiscussionComposer from 'flarum/components/DiscussionComposer';
import LogInModal from 'flarum/components/LogInModal';
import DiscussionPage from 'flarum/components/DiscussionPage';
import Select from 'flarum/components/Select';
import Button from 'flarum/components/Button';
import LinkButton from 'flarum/components/LinkButton';
import SelectDropdown from 'flarum/components/SelectDropdown';

/**
 * The `IndexPage` component displays the index page, including the welcome
 * hero, the sidebar, and the discussion list.
 */
export default class IndexPage extends Page {
  init() {
    super.init();

    // If the user is returning from a discussion page, then take note of which
    // discussion they have just visited. After the view is rendered, we will
    // scroll down so that this discussion is in view.
    if (app.previous instanceof DiscussionPage) {
      this.lastDiscussion = app.previous.discussion;
    }

    // If the user is coming from the discussion list, then they have either
    // just switched one of the parameters (filter, sort, search) or they
    // probably want to refresh the results. We will clear the discussion list
    // cache so that results are reloaded.
    if (app.previous instanceof IndexPage) {
      app.cache.discussionList = null;
    }

    const params = this.params();

    if (app.cache.discussionList) {
      // Compare the requested parameters (sort, search query) to the ones that
      // are currently present in the cached discussion list. If they differ, we
      // will clear the cache and set up a new discussion list component with
      // the new parameters.
      Object.keys(params).some(key => {
        if (app.cache.discussionList.props.params[key] !== params[key]) {
          app.cache.discussionList = null;
          return true;
        }
      });
    }

    if (!app.cache.discussionList) {
      app.cache.discussionList = new DiscussionList({params});
    }

    app.history.push('index', icon('bars'));

    this.bodyClass = 'App--index';
  }

  onunload() {
    // Save the scroll position so we can restore it when we return to the
    // discussion list.
    app.cache.scrollTop = $(window).scrollTop();
  }

  view() {
    return (

        {this.hero()}

{listItems(this.sidebarItems().toArray())}

{listItems(this.viewItems().toArray())}

{listItems(this.actionItems().toArray())}

            {app.cache.discussionList.render()}

    );
  }

  config(isInitialized, context) {
    super.config(...arguments);

    if (isInitialized) return;

    extend(context, 'onunload', () => $('#app').css('min-height', ''));

    app.setTitle('');
    app.setTitleCount(0);

    // Work out the difference between the height of this hero and that of the
    // previous hero. Maintain the same scroll position relative to the bottom
    // of the hero so that the sidebar doesn't jump around.
    const oldHeroHeight = app.cache.heroHeight;
    const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
    const scrollTop = app.cache.scrollTop;

    $('#app').css('min-height', $(window).height() + heroHeight);

    // Scroll to the remembered position. We do this after a short delay so that
    // it happens after the browser has done its own "back button" scrolling,
    // which isn't right. https://github.com/flarum/core/issues/835
    const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
    scroll();
    setTimeout(scroll, 1);

    // If we've just returned from a discussion page, then the constructor will
    // have set the `lastDiscussion` property. If this is the case, we want to
    // scroll down to that discussion so that it's in view.
    if (this.lastDiscussion) {
      const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);

      if ($discussion.length) {
        const indexTop = $('#header').outerHeight();
        const indexBottom = $(window).height();
        const discussionTop = $discussion.offset().top;
        const discussionBottom = discussionTop + $discussion.outerHeight();

        if (discussionTop < scrollTop + indexTop || discussionBottom > scrollTop + indexBottom) {
          $(window).scrollTop(discussionTop - indexTop);
        }
      }
    }
  }

  /**
   * Get the component to display as the hero.
   *
   * @return {MithrilComponent}
   */
  hero() {
    return WelcomeHero.component();
  }

  /**
   * Build an item list for the sidebar of the index page. By default this is a
   * "New Discussion" button, and then a DropdownSelect component containing a
   * list of navigation items.
   *
   * @return {ItemList}
   */
  sidebarItems() {
    const items = new ItemList();
    const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;

    items.add('newDiscussion',
      Button.component({
        children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
        icon: 'edit',
        className: 'Button Button--primary IndexPage-newDiscussion',
        itemClassName: 'App-primaryControl',
        onclick: this.newDiscussion.bind(this),
        disabled: !canStartDiscussion
      })
    );

    items.add('nav',
      SelectDropdown.component({
        children: this.navItems(this).toArray(),
        buttonClassName: 'Button',
        className: 'App-titleControl'
      })
    );

    return items;
  }

  /**
   * Build an item list for the navigation in the sidebar of the index page. By
   * default this is just the 'All Discussions' link.
   *
   * @return {ItemList}
   */
  navItems() {
    const items = new ItemList();
    const params = this.stickyParams();

    items.add('allDiscussions',
      LinkButton.component({
        href: app.route('index', params),
        children: app.translator.trans('core.forum.index.all_discussions_link'),
        icon: 'comments-o'
      }),
      100
    );

    return items;
  }

  /**
   * Build an item list for the part of the toolbar which is concerned with how
   * the results are displayed. By default this is just a select box to change
   * the way discussions are sorted.
   *
   * @return {ItemList}
   */
  viewItems() {
    const items = new ItemList();

    const sortOptions = {};
    for (const i in app.cache.discussionList.sortMap()) {
      sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
    }

    items.add('sort',
      Select.component({
        options: sortOptions,
        value: this.params().sort,
        onchange: this.changeSort.bind(this)
      })
    );

    return items;
  }

  /**
   * Build an item list for the part of the toolbar which is about taking action
   * on the results. By default this is just a "mark all as read" button.
   *
   * @return {ItemList}
   */
  actionItems() {
    const items = new ItemList();

    items.add('refresh',
      Button.component({
        title: app.translator.trans('core.forum.index.refresh_tooltip'),
        icon: 'refresh',
        className: 'Button Button--icon',
        onclick: () => app.cache.discussionList.refresh()
      })
    );

    if (app.session.user) {
      items.add('markAllAsRead',
        Button.component({
          title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
          icon: 'check',
          className: 'Button Button--icon',
          onclick: this.markAllAsRead.bind(this)
        })
      );
    }

    return items;
  }

  /**
   * Return the current search query, if any. This is implemented to activate
   * the search box in the header.
   *
   * @see Search
   * @return {String}
   */
  searching() {
    return this.params().q;
  }

  /**
   * Redirect to the index page without a search filter. This is called when the
   * 'x' is clicked in the search box in the header.
   *
   * @see Search
   */
  clearSearch() {
    const params = this.params();
    delete params.q;

    m.route(app.route(this.props.routeName, params));
  }

  /**
   * Redirect to the index page using the given sort parameter.
   *
   * @param {String} sort
   */
  changeSort(sort) {
    const params = this.params();

    if (sort === Object.keys(app.cache.discussionList.sortMap())[0]) {
      delete params.sort;
    } else {
      params.sort = sort;
    }

    m.route(app.route(this.props.routeName, params));
  }

  /**
   * Get URL parameters that stick between filter changes.
   *
   * @return {Object}
   */
  stickyParams() {
    return {
      sort: m.route.param('sort'),
      q: m.route.param('q')
    };
  }

  /**
   * Get parameters to pass to the DiscussionList component.
   *
   * @return {Object}
   */
  params() {
    const params = this.stickyParams();

    params.filter = m.route.param('filter');

    return params;
  }

  /**
   * Log the user in and then open the composer for a new discussion.
   *
   * @return {Promise}
   */
  newDiscussion() {
    const deferred = m.deferred();

    if (app.session.user) {
      this.composeNewDiscussion(deferred);
    } else {
      app.modal.show(
        new LogInModal({
          onlogin: this.composeNewDiscussion.bind(this, deferred)
        })
      );
    }

    return deferred.promise;
  }

  /**
   * Initialize the composer for a new discussion.
   *
   * @param {Deferred} deferred
   * @return {Promise}
   */
  composeNewDiscussion(deferred) {
    const component = new DiscussionComposer({user: app.session.user});

    app.composer.load(component);
    app.composer.show();

    deferred.resolve(component);

    return deferred.promise;
  }

  /**
   * Mark all discussions as read.
   *
   * @return void
   */
  markAllAsRead() {
    const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));

    if (confirmation) {
      app.session.user.save({readTime: new Date()});
    }
  }
}
<|endoftext|>