define(['jquery', 'lodash', 'ui/component', 'ui/hider', 'ui/stacker', 'handlebars', 'lib/handlebars/helpers'], function ($$1, _, componentFactory, hider, stackerFactory, Handlebars, Helpers0) { 'use strict';

    $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
    _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
    componentFactory = componentFactory && Object.prototype.hasOwnProperty.call(componentFactory, 'default') ? componentFactory['default'] : componentFactory;
    hider = hider && Object.prototype.hasOwnProperty.call(hider, 'default') ? hider['default'] : hider;
    stackerFactory = stackerFactory && Object.prototype.hasOwnProperty.call(stackerFactory, 'default') ? stackerFactory['default'] : stackerFactory;
    Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
    Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;

    if (!Helpers0.__initialized) {
      Helpers0(Handlebars);
      Helpers0.__initialized = true;
    }
    var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
      this.compilerInfo = [4,'>= 1.0.0'];
    helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
      var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression, self=this;

    function program1(depth0,data) {
      
      var buffer = "", stack1, helper;
      buffer += " ";
      if (helper = helpers.className) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.className); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1);
      return buffer;
      }

    function program3(depth0,data) {
      
      var buffer = "", stack1, helper;
      buffer += "<span class=\"icon icon-";
      if (helper = helpers.icon) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.icon); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1);
      stack1 = helpers.unless.call(depth0, (depth0 && depth0.text), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\"></span>";
      return buffer;
      }
    function program4(depth0,data) {
      
      
      return " no-label";
      }

    function program6(depth0,data) {
      
      var buffer = "", stack1, helper;
      buffer += "<span class=\"text\">";
      if (helper = helpers.text) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.text); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "</span>";
      return buffer;
      }

      buffer += "<li\n    data-control=\"";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "\"\n    class=\"small btn-info action ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.className), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\"\n    title=\"";
      if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "\"\n    aria-label=\"";
      if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "\"\n    aria-haspopup=\"listbox\"\n    role=\"button\"\n>\n    <a class=\"li-inner\" data-control=\"";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "-button\" href=\"#\" aria-hidden=\"true\">\n        ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.icon), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\n        ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.text), {hash:{},inverse:self.noop,fn:self.program(6, program6, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\n        &nbsp; <span class=\"icon icon-up\"></span>\n    </a>\n    <div data-control=\"";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "-menu\" class=\"hidden ";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "-menu\" tabindex=\"1\">\n        <ul\n            data-control=\"";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "-list\"\n            class=\"menu ";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "-list\"\n            role=\"listbox\"\n        ></ul>\n    </div>\n</li>\n";
      return buffer;
      });
    function menuTpl(data, options, asString) {
      var html = Template(data, options);
      return (asString || true) ? html : $(html);
    }

    if (!Helpers0.__initialized) {
      Helpers0(Handlebars);
      Helpers0.__initialized = true;
    }
    var Template$1 = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
      this.compilerInfo = [4,'>= 1.0.0'];
    helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
      var buffer = "", stack1, helper, functionType="function", escapeExpression=this.escapeExpression, self=this;

    function program1(depth0,data) {
      
      var buffer = "", stack1, helper;
      buffer += " ";
      if (helper = helpers.className) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.className); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1);
      return buffer;
      }

    function program3(depth0,data) {
      
      var buffer = "", stack1, helper;
      buffer += "\n        role=\"";
      if (helper = helpers.role) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.role); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "\"\n    ";
      return buffer;
      }

      buffer += "<li\n    class=\"small action menu-item ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.className), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\"\n    data-control=\"";
      if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "\"\n    tabindex=\"-1\"\n    ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.role), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\n>\n    <a class=\"li-inner menu-inner\">\n        <span class=\"icon icon-checkbox\"></span><span class=\"icon icon-checkbox-checked\"></span>\n        <span class=\"label\">";
      if (helper = helpers.text) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.text); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "</span>\n    </a>\n</li>";
      return buffer;
      });
    function menuItemTpl(data, options, asString) {
      var html = Template$1(data, options);
      return (asString || true) ? html : $(html);
    }

    /**
     * This program is free software; you can redistribute it and/or
     * modify it under the terms of the GNU General Public License
     * as published by the Free Software Foundation; under version 2
     * of the License (non-upgradable).
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU General Public License for more details.
     *
     * You should have received a copy of the GNU General Public License
     * along with this program; if not, write to the Free Software
     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
     *
     * Copyright (c) 2017-2020 (original work) Open Assessment Technologies SA;
     */
    var keyCodes = {
      TAB: 9,
      ESC: 27,
      ENTER: 13,
      SPACE: 32,
      LEFT: 37,
      UP: 38,
      RIGHT: 39,
      DOWN: 40
    };
    var stacker = stackerFactory('test-runner');
    var menuComponentApi = {
      /**
       * Initialise the menu
       */
      initMenu: function initMenu() {
        this.id = this.config.control;
        this.navType = this.config.navType ? this.config.navType : 'fromLast';
        this.menuItems = [];
      },
      /**
       * Get the type of the component
       */
      getType: function getType() {
        return 'menu';
      },
      /**
       * Get the menu Id
       * @returns {String}
       */
      getId: function getId() {
        return this.id;
      },
      /**
       * Set the menu as active, essentially meaning that the menu panel is opened
       */
      turnOn: function turnOn() {
        this.setState('active', true);
      },
      /**
       * Set the menu as inactive
       */
      turnOff: function turnOff() {
        this.setState('active', false);
      },
      /**
       * =====================
       * Actions on menu panel
       * =====================
       */

      /**
       * open/close the menu
       */
      toggleMenu: function showMenu() {
        if (!this.is('disabled')) {
          if (this.is('opened')) {
            this.closeMenu();
          } else {
            this.openMenu();
          }
        }
      },
      /**
       * It needs to find closest visible item.
       *
       * @param {Number} last - index to stop.
       * @param {-1|1} inc - incrementor. -1 - navigate to the top,  1 - to the bottom.
       *
       * @returns {Number} returns index > 0 if a visible item was found and -1 otherwise.
       */
      indexOfClosestVisibleItem(inc, last) {
        if (!this.menuItems.length) {
          return -1;
        }
        let elem;
        let position = this.hoverIndex;
        do {
          position += inc;
          if (position === last) {
            return -1;
          }
          elem = this.menuItems[position].getElement();
        } while (elem && elem.hasClass('hidden'));
        return position;
      },
      /**
       * Changes hoverIndex and hover item.
       *
       * @param {Number} index - item index to hover.
       *
       * @returns the menu item.
       */
      hoverByIndex(index) {
        const elem = this.menuItems[index];
        this.hoverIndex = index;
        if (elem) {
          this.hoverItem(elem.id);
        }
        return elem;
      },
      hoverNextVisibleItem() {
        const index = this.indexOfClosestVisibleItem(1, this.menuItems.length);
        const elem = this.hoverByIndex(index);
        return elem;
      },
      hoverPreviousVisibleItem() {
        const index = this.indexOfClosestVisibleItem(-1, -1);
        const elem = this.hoverByIndex(index);
        return elem;
      },
      /**
       * open the menu
       */
      openMenu: function openMenu() {
        // show the DOM element
        hider.show(this.$menuContainer);
        stacker.bringToFront(this.$menuContent);

        // change the menu button icon
        this.$menuStateIcon.removeClass('icon-up');
        this.$menuStateIcon.addClass('icon-down');

        // turn on the menu button
        this.turnOn();

        // setup keyboard navigation & highlighting
        this.enableShortcuts();
        this.hoverOffAll();
        if (document.activeElement) {
          document.activeElement.blur();
        }
        const activeItemIndex = _.findIndex(this.menuItems, item => item.is('active'));
        if (activeItemIndex >= 0) {
          this.hoverIndex = activeItemIndex;
          this.$menuItems[this.hoverIndex].focus();
          this.hoverItem(this.menuItems[activeItemIndex].id);
        } else if (this.navType === 'fromLast') {
          // fromLast (default) navigation: focus on button and then using UP go to last item
          this.hoverIndex = this.menuItems.length; // we start on the button, not at the max array index
          // which would be menuItems.length-1
          this.hoverPreviousVisibleItem();
        } else if (this.navType === 'fromFirst') {
          // fromFirst navigation: focus on button and then using DOWN go to first item
          this.hoverIndex = -1; // we start on the button, not the first element
          // which would be 0
          this.hoverNextVisibleItem();
        }

        // component inner state
        this.setState('opened', true);
        this.trigger('openmenu', this);
      },
      /**
       * close the menu
       */
      closeMenu() {
        // hide the DOM element
        hider.hide(this.$menuContainer);

        // change the menu button icon
        this.$menuStateIcon.removeClass('icon-down');
        this.$menuStateIcon.addClass('icon-up');

        // turn off the button
        this.turnOff();

        // disable keyboard navigation & highlighting
        this.disableShortcuts();
        this.hoverOffAll();

        // component inner state
        this.setState('opened', false);
        this.trigger('closemenu', this);

        // Move focus if the menu wasn't disabled before the close action was launched.
        if (!this.is('disabled') && !this.$component.prop('disabled')) {
          this.$menuButton.parent().focus(); // It needs for screenreaders to correctly read menu button after submenu was closed
        }
      },

      /**
       * =====================
       * Actions on menu items
       * =====================
       */

      /**
       * Look for a item in the internal item registry
       * @param {String} itemId
       * @returns {Object|undefined}
       */
      getItemById: function getItemById(itemId) {
        return _.find(this.menuItems, function (item) {
          return item.getId() === itemId;
        });
      },
      /**
       * Adds an item to the menu
       * @param {Component} item
       */
      addItem: function addItem(item) {
        if (item) {
          this.menuItems.push(item);
        }
      },
      /**
       * Render menu items into the menu panel
       */
      renderItems: function renderItems() {
        var self = this;
        this.menuItems.forEach(function (item) {
          item.setTemplate(menuItemTpl); // the item has been created as generic. Let's give him now the menu entry template
          item.render(self.$menuContent);
          item.enable();
        });

        // bind mouse behavior on menu items
        this.$menuItems = this.$menuContent.find('.menu-item');
        this.$menuItems.on('mouseenter', function highlightHoveredEntry(e) {
          var itemId = e.currentTarget.getAttribute('data-control');
          self.mouseOverItem(itemId);
        });
      },
      /**
       * Highlight the currently hovered item
       * @param {String} itemId
       */
      mouseOverItem: function mouseOverItem(itemId) {
        var self = this;

        // look for item index
        this.menuItems.forEach(function (item, index) {
          if (item.id === itemId) {
            self.hoverIndex = index;
          }
        });
        this.hoverItem(itemId);
      },
      /**
       * Check that the menu has at least one of its entries displayed
       * @returns {boolean}
       */
      hasDisplayedItems: function hasDisplayedItems() {
        return this.menuItems.some(function (item) {
          return !item.is('disabled') && !item.is('hidden');
        });
      },
      /**
       * Set all entries in the menu to inactive
       */
      turnOffAll: function turnOffAll() {
        this.menuItems.forEach(function (current) {
          current.turnOff();
        });
      },
      /**
       * =====================
       * Menu items navigation
       * =====================
       */

      /**
       * register the event handlers for keyboard navigation
       */
      enableShortcuts: function enableShortcuts() {
        var self = this;
        this.$menuContainer.on('keydown.menuNavigation', function (e) {
          var currentKeyCode = e.keyCode ? e.keyCode : e.charCode;
          e.preventDefault();
          switch (currentKeyCode) {
            case keyCodes.ESC:
            case keyCodes.TAB:
              self.closeMenu();
              break;
            case keyCodes.SPACE:
            case keyCodes.ENTER:
              self.triggerHighlightedItem();
              e.stopPropagation();
              break;
            case keyCodes.LEFT:
            case keyCodes.UP:
              self.moveUp();
              e.stopPropagation();
              break;
            case keyCodes.RIGHT:
            case keyCodes.DOWN:
              self.moveDown();
              e.stopPropagation();
              break;
          }
        });
        this.$menuButton.on('keydown.menuNavigation', function (e) {
          var currentKeyCode = e.keyCode ? e.keyCode : e.charCode;
          function setFocusToItem(index) {
            self.hoverIndex = index;
            self.$menuContainer.focus();
            self.hoverItem(self.menuItems[self.hoverIndex].id);
          }
          if (currentKeyCode === keyCodes.UP && self.navType === 'fromLast') {
            e.stopPropagation();
            setFocusToItem(self.menuItems.length - 1);
          }
          if (currentKeyCode === keyCodes.DOWN && self.navType === 'fromFirst') {
            e.stopPropagation();
            setFocusToItem(0);
          }
        });
      },
      /**
       * remove the event handlers for keyboard navigation
       */
      disableShortcuts: function disableShortcuts() {
        this.$menuContainer.off('.menuNavigation');
        this.$menuButton.off('.menuNavigation');
      },
      /**
       * Move the highlight to the previous not hidden item
       */
      moveUp: function moveUp() {
        if (this.hoverIndex > 0) {
          const elem = this.hoverPreviousVisibleItem();
          if (!elem) {
            this.closeMenu();
          }
          // move to the menu button
        } else if (this.hoverIndex === 0) {
          this.hoverIndex--;
          this.closeMenu();
        }
      },
      /**
       * Move the highlight to the next not hidden item, or to the menu button if we are on the last item
       */
      moveDown: function moveDown() {
        // move to the next item
        if (this.hoverIndex < this.menuItems.length - 1) {
          const elem = this.hoverNextVisibleItem();
          if (!elem) {
            this.closeMenu();
          }
          // move to the menu button
        } else if (this.hoverIndex === this.menuItems.length - 1) {
          this.hoverIndex++;
          this.closeMenu();
        }
      },
      /**
       * Highlight the given item
       * @param {String} itemId
       */
      hoverItem: function hoverItem(itemId) {
        var itemToHover = this.getItemById(itemId);
        this.hoverOffAll();
        if (itemToHover) {
          itemToHover.hoverOn();
          itemToHover.getElement().focus();
        }
      },
      /**
       * Remove highlight from all items
       */
      hoverOffAll: function hoverOffAll() {
        this.menuItems.forEach(function (current) {
          if (current) {
            current.hoverOff();
          }
        });
      },
      /**
       * Run a click event on the DOM element of the currently highlighted item
       */
      triggerHighlightedItem: function triggerHighlightedItem() {
        var activeItem;
        if (this.menuItems[this.hoverIndex]) {
          activeItem = this.menuItems[this.hoverIndex];
          activeItem.getElement().trigger('click');
          this.closeMenu();

          // give back the focus
          this.$component.focus();
        }
      },
      /**
       * Set navigation type
       * @param {String} type - 'fromLast', 'fromFirst'
       */
      setNavigationType: function setNavigationType(type) {
        if (['fromLast', 'fromFirst'].includes(type)) {
          this.navType = type;
        }
      }
    };

    /**
     * The menu component factory
     */
    function menuComponentFactory(specs, defaults) {
      var _defaults, menuComponent;
      _defaults = {
        $component: $$1(),
        $menuButton: $$1(),
        $menuContainer: $$1(),
        $menuContent: $$1(),
        $menuItems: $$1(),
        $menuStateIcon: $$1(),
        hoverIndex: null,
        id: null,
        menuItems: []
      };
      specs = _.defaults(specs || {}, menuComponentApi);
      menuComponent = componentFactory(specs, defaults).setTemplate(menuTpl).on('enable', function enable() {
        if (this.is('rendered')) {
          this.$component.removeProp('disabled');
        }
      }).on('disable', function disable() {
        if (this.is('rendered')) {
          this.$component.prop('disabled', true);
          this.closeMenu();
          this.turnOff();
        }
      }).on('hide', function disable() {
        if (this.is('rendered')) {
          this.closeMenu();
        }
      }).on('init', function init() {
        this.initMenu();
      }).on('render', function render() {
        var self = this;

        // get access to DOM elements
        this.$menuButton = this.$component.find(`[data-control="${this.config.control}-button"]`);
        this.$menuContainer = this.$component.find(`[data-control="${this.config.control}-menu"]`);
        this.$menuContent = this.$component.find(`[data-control="${this.config.control}-list"]`);
        this.$menuStateIcon = this.$menuButton.find('.icon-up');
        this.disable(); // always render disabled by default

        // add behavior
        this.$component.on('click', function toggleMenu(e) {
          e.preventDefault();
          if (!self.is('opened')) {
            e.stopPropagation(); // prevent higher handler to auto-close the menu on click
          }

          self.toggleMenu();
        });
        this.$menuContainer.on('click', function closeMenuOnItemClick(e) {
          e.preventDefault();
          e.stopPropagation(); // so the menu doesn't get toggled again when the event bubble to the component
          self.closeMenu();
        });
        this.$menuContent.on('mouseleave', this.hoverOffAll);
      }).on('destroy', function () {
        if (this.is('rendered')) {
          this.$menuContainer.off('.menuNavigation');
          this.$menuButton.off('.menuNavigation');
        }
      });

      // Apply default properties to the menuComponent
      _.defaults(menuComponent, _defaults);
      return menuComponent;
    }

    return menuComponentFactory;

});
