<commit_before>'use strict';

var $ = require('jquery');

function NavMainComponent ($html, rootWindow) {
    this.$html = $html;
    this.window = rootWindow;
    this.$window = $(rootWindow);
};

/**
 * Initialise
 */
NavMainComponent.prototype.init = function () {
    var component = this;

    if (!component.$html.length) {
        throw new Error('html must be passed to NavMainComponent');
    }

    if (!component.$window.length) {
        throw new Error('window must be passed to NavMainComponent');
    }

    component.$body = this.$html.find('body');
    component.$navMain = this.$html.find('.nav-main');
    component.$contentMain = this.$html.find('.content-main');
    component.$brandingLink = this.$html.find('.jadu-branding');
    component.$navPrimary = this.$html.find('.nav-primary');
    component.$navSecondary = this.$html.find('.nav-secondary');
    component.$navTertiary = component.$navMain.find('.nav-tertiary');
    component.$navQuaternary = component.$navMain.find('.nav-quaternary');
    component.$primaryNavLinks = component.$navPrimary.find('.nav-link');
    component.$tertiaryNavLinks = component.$navTertiary.find('.nav-link');
    component.$closeLink = component.$navMain.find('[data-nav-action=close]');
    component.$mobileMenuButton = this.$html.find('.mobile-menu-button');

    // Calculate what primary nav items can be shown and which need to be hidden in more menu
    component.adjustNavItems();

    // Check which tabindexes should be applied to navigation links to ensure WCAG compliance
    component.manageTabIndexes();

    // Open navigation on mobile
    component.$mobileMenuButton.on('click', function(event) {
        var $self = $(this);
        event.stopImmediatePropagation();
        component.$body.toggleClass('open-nav');
        $self.toggleClass('open');

        if ($self.text() === 'Menu') {
            $self.text('Close');
            $self.attr('aria-expanded', 'true');
            component.$brandingLink.attr('tabindex', '3');
            component.$primaryNavLinks.attr('tabindex', '3');
        } else {
            $self.text('Menu');
            $self.attr('aria-expanded', 'false');
            component.$brandingLink.attr('tabindex', '-1');
            component.$primaryNavLinks.attr('tabindex', '-1');
        }
    });

    // More button is shown when window is too short to display all primary nav items
    component.$navMain.on('click', '.more-icon > .nav-link', function(event) {
        var $self = $(this);
        event.preventDefault();
        component.moreIconClickHandler($self)
    });

    // Re-adjust nav items on window resize to calc if more button is needed
    component.$window.resize(function () {
        component.adjustNavItems();
        component.manageTabIndexes();
    });

    // Close navs on main content click
    component.$contentMain.on('click', function () {
        component.closeNavs($(this));
    });

    // Open secondary nav on primary nav item click
    component.$primaryNavLinks.on('click', function (event) {
        component.openSecondaryNav($(this), event);
    });

    // Open quaternary nav on tertiary nav item click
    component.$tertiaryNavLinks.on('click', function (event) {
        var $self = $(this),
            href = $self.attr('href');

        if (href.indexOf('#') !== -1) {
            event.preventDefault();

            // Change aria expanded to true
            $self.attr('aria-expanded', 'true');
        }

        component.openQuaternaryNav(href);
    });

    // Close respective navs on close link click
    component.$closeLink.on('click', function (event) {
        event.preventDefault();
        component.closeNavs($(this));
    });
};

/**
 * Unto the tabindex if the main nav is in responsive mode
 * This maintains the tab order to ensure WCAG compliance
 */
NavMainComponent.prototype.manageTabIndexes = function () {
    var component = this,
        isMobile = !component.window.matchMedia('(min-width: 992px)').matches;

    if (isMobile) {
        component.$brandingLink.attr('tabindex', '-1');
        component.$primaryNavLinks.attr('tabindex', '-1');
    } else {
        component.$brandingLink.attr('tabindex', '1');
        component.$primaryNavLinks.attr('tabindex', '1');
    }
};

/**
 * Open secondary navigation, close all other navs and highlight primary nav item parent
 * @param {jQuery} $linkClicked - jQuery object of the link clicked to open secondary nav
 * @param {Event} event - click event for the primary nav link
 */
NavMainComponent.prototype.openSecondaryNav = function ($linkClicked, event) {
    var component = this,
        target;

    if ($linkClicked[0].hasAttribute('href')) {
        target = $linkClicked.attr('href');
    }
    else if ($linkClicked[0].hasAttribute('data-target')) {
        target = $linkClicked.attr('data-target');
    }

    // Close any previously open navs
    component.closeSecondaryNav();
    component.closeTertiaryNav();
    component.closeQuaternaryNav();

    // Close any open popovers which would appear over the navigation
    component.$html.find('[data-toggle="popover"]').popover('hide');

    // If href is a fragment (therefore opens a sub nav), don't add it to the URL because it breaks the back button
    if (target.indexOf('#') !== -1) {
        event.preventDefault();
        $linkClicked.attr('aria-expanded', 'true');
        component.$navSecondary.addClass('is-open');
        component.$navSecondary.find('.nav-list.is-active').removeClass('is-active');
        component.$navSecondary.find('[data-nav="' + target + '"]').addClass('is-active');
    }

    if (component.$html.find('[data-nav="' + target + '"]').length >= 1) {
        component.$navMain.addClass('is-open');
    } else {
        component.closeNavs($linkClicked);
    }

    component.$navPrimary.find('.is-active').removeClass('is-active');
    component.$navPrimary.find('[href="' + target + '"], [data-target="' + target + '"]').addClass('is-active');
};

/**
 * Open quaternary navigation
 * @param {string} target - href of target nav list
 */
NavMainComponent.prototype.openQuaternaryNav = function (target) {
    var component = this;

    // If menu item has child menu options and therefore is same-page link then open $navQuaternary
    if (target.indexOf('#') !== -1) {
        component.$navQuaternary.addClass('is-open');
    }

    // Show the target nav-list in the opened nav
    component.$navQuaternary.find('[data-nav="' + target + '"]').addClass('is-active');
};

/**
 * Close respective nav(s) depending on which close link was clicked
 * @param {jQuery} $linkClicked - jQuery object of the close link clicked
 */
NavMainComponent.prototype.closeNavs = function ($linkClicked) {
    var component = this,
        $linkParent = $linkClicked.parent();

    if ($linkParent.hasClass('nav-secondary')) {
        component.closeSecondaryNav();
    }

    else if ($linkParent.hasClass('nav-tertiary')) {
        component.closeTertiaryNav();
    }

    else if ($linkParent.hasClass('nav-quaternary')) {
        component.closeQuaternaryNav();
    }

    else {
        component.closeSecondaryNav();
        component.closeTertiaryNav();
        component.closeQuaternaryNav();
    }
}

/**
 * Close secondary navigation
 */
NavMainComponent.prototype.closeSecondaryNav = function () {
    var component = this;

    component.$navMain.removeClass('is-open');
    component.$navMain.find('[aria-expanded=true]').attr( 'aria-expanded', 'false');
    component.$navSecondary.removeClass('is-open');
    component.$primaryNavLinks.removeClass('is-active')
    component.$navSecondary.find('.nav-list').removeClass('is-active');
}

/**
 * Close tertiary navigation
 */
NavMainComponent.prototype.closeTertiaryNav = function () {
    var component = this;

    component.$navTertiary.removeClass('is-open');
    component.$navTertiary.find('.nav-list').removeClass('is-active');

    // Reset aria-expanded on more button
    component.$navMain.find('[aria-expanded=true]')
        .removeClass('is-active')
        .attr('aria-expanded', 'false');
}

/**
 * Close quaternary navigation
 */
NavMainComponent.prototype.closeQuaternaryNav = function () {
    var component = this;

    component.$navQuaternary.removeClass('is-open');
    component.$navQuaternary.find('.nav-list.is-active').removeClass('is-active');

    // Reset aria-expanded on tertiary link
    component.$navTertiary.find('[aria-expanded=true]').attr('aria-expanded', 'false');
}

/**
 * Detect window height, adjust the number of items in the primary nav and check when to add "More" option
 */
NavMainComponent.prototype.adjustNavItems = function () {
    var component = this,
        availableHeight = component.$window.height(),
        navItemsHeight = (component.$html.find('.nav-primary .nav-items').outerHeight(true) + component.$html.find('.jadu-branding').outerHeight(true)),
        moreIconHeight = 72, // Pre calculated height of the "More" nav item
        navItemsCountTotal = component.$html.find('.nav-primary .nav-items li').length,
        numberOfHiddenNavItems = 0;

    // When nav items + more icon height is greater than available window height
    if (navItemsHeight + moreIconHeight > availableHeight) {
        // If there is not enough space hide the last primary nav items
        component.hidePrimaryNavItems(navItemsHeight, moreIconHeight, availableHeight);
        // Get the number of hidden items to make only them visible in the tertiary menu
        numberOfHiddenNavItems = component.$html.find('.nav-primary .nav-items li:hidden').length;
        // Add "More" nav item and check its visibility if already exists
        component.addMoreNavItem(numberOfHiddenNavItems);
        // Hide the primary nav items duplicate in tertiary menu
        component.hideMoreCategoriesTopItems(navItemsCountTotal, numberOfHiddenNavItems);
    } else if (navItemsHeight + moreIconHeight < availableHeight) {
        // Unhide items if they were hidden and there is space in the primary nav
        component.unhidePrimaryNavItems();
        numberOfHiddenNavItems = component.$html.find('.nav-primary .nav-items li:hidden').length;
        component.lastItemSubstitution(numberOfHiddenNavItems);
    }
};

/**
 * Hide primary nav items that don't fit in window
 * @param {number} navItemsHeight - PX height of primary nav items
 * @param {number} moreIconHeight - PX height of more link
 * @param {number} availableHeight - PX height of window
 */
NavMainComponent.prototype.hidePrimaryNavItems = function (navItemsHeight, moreIconHeight, availableHeight) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        lastPrimaryNavItem =  navItems.find('li:last-child'),
        nthChild = 2; // This number represents the item before the last in the nth-last-child;

    while (navItemsHeight + moreIconHeight > availableHeight) {
        // If last nav item is visible hide it
        if (lastPrimaryNavItem.is(':visible')) {
            lastPrimaryNavItem.hide();
        } else {
            // If last nav item is hidden hide the next one up
            navItems.find('li:nth-last-child('+ nthChild +')').hide();
            nthChild++;
        }

        // Recalculate nav items height based on items just hidden
        navItemsHeight = (navItems.outerHeight(true) + component.$html.find('.jadu-branding').outerHeight(true));
    }
};

/**
 * Unhide primary nav items
 */
NavMainComponent.prototype.unhidePrimaryNavItems = function () {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon'),
        firstHiddenPrimaryNavItem = navItems.find('li:hidden').first();

        if ((firstHiddenPrimaryNavItem.length > 0) && (navItemMore.is(':visible'))) {

            // Reset display type to list-item rather than show() to make sure they don't get recreated as inline-block
            firstHiddenPrimaryNavItem.css({display: 'list-item'});
        }
};

/**
 * Add the more link to the primary nav
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.addMoreNavItem = function (numberOfHiddenNavItems) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon');

    // Add the "More" nav item
    if ((numberOfHiddenNavItems > 0) && (!component.$html.find('.more-icon').length)) {
        navItems.append('
<button class="nav-link t-nav-link" aria-haspopup="true" aria-expanded="false" aria-controls="aria-tertiary-nav">More</button>
');
    }

    // Check if "More" nav item is visible
    if (navItemMore.is(':hidden') && (numberOfHiddenNavItems > 0)) {
        navItemMore.show();
    } else {
        component.$html.find('.nav-tertiary').removeClass('is-open');
        component.$html.find('.nav-quaternary').removeClass('is-open');
    }
};

/**
 * Hide the nav items in nav tertiary that are already displayed in the primary nav
 * @param {number} navItemsCountTotal - Total number of nav items
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.hideMoreCategoriesTopItems = function (navItemsCountTotal, numberOfHiddenNavItems) {
    var component = this,
        nthChild = 1, // Number used to iterate nth-child
        itemsToHideCount;

    // Reset hidden nav items
    component.$html.find('.nav-tertiary .nav-items li').show();

    // Calculate the number of visible nav items in primary nav
    itemsToHideCount = navItemsCountTotal - numberOfHiddenNavItems - 1; // 1 is for the "More" nav item

    // Hide top items in "More Categories" equal to the number of visible items of primary nav
    while (itemsToHideCount >= 0) {
        component.$html.find('.nav-tertiary .nav-items li:nth-child('+ nthChild +')').hide();
        nthChild++;
        itemsToHideCount--;
    }
};

/**
 * Hide more link if there are no hidden nav items
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.lastItemSubstitution = function (numberOfHiddenNavItems) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon');

    if (numberOfHiddenNavItems === 0) {
        navItemMore.hide();
        component.$html.find('.nav-tertiary').removeClass('is-open');
        component.$html.find('.nav-quaternary').removeClass('is-open');
    }
};

/**
 * Hide more link if there are no hidden nav items
 * @param {jQuery} $moreLink - jQuery object of the more link
 */
NavMainComponent.prototype.moreIconClickHandler = function ($moreLink) {
    var component = this;

    $moreLink.addClass('is-active');

    // Close secondary nav if already open
    component.closeSecondaryNav();

    // If tertiary nav is already open
    if (component.$navTertiary.find('.nav-list').hasClass('is-active')) {
        $moreLink.attr('aria-expanded', 'false');
        component.$navTertiary.removeClass('is-open');
        component.$navTertiary.find('.nav-list').removeClass('is-active');
    } else {
        $moreLink.attr('aria-expanded', 'true');

        // Open $navTertiary
        component.$navTertiary.find('.nav-list').addClass('is-active');
        component.$navTertiary.addClass('is-open');
    }
};

module.exports = NavMainComponent;
<commit_msg>Make sure quaternary nav works with button links<commit_after>'use strict';

var $ = require('jquery');

function NavMainComponent ($html, rootWindow) {
    this.$html = $html;
    this.window = rootWindow;
    this.$window = $(rootWindow);
};

/**
 * Initialise
 */
NavMainComponent.prototype.init = function () {
    var component = this;

    if (!component.$html.length) {
        throw new Error('html must be passed to NavMainComponent');
    }

    if (!component.$window.length) {
        throw new Error('window must be passed to NavMainComponent');
    }

    component.$body = this.$html.find('body');
    component.$navMain = this.$html.find('.nav-main');
    component.$contentMain = this.$html.find('.content-main');
    component.$brandingLink = this.$html.find('.jadu-branding');
    component.$navPrimary = this.$html.find('.nav-primary');
    component.$navSecondary = this.$html.find('.nav-secondary');
    component.$navTertiary = component.$navMain.find('.nav-tertiary');
    component.$navQuaternary = component.$navMain.find('.nav-quaternary');
    component.$primaryNavLinks = component.$navPrimary.find('.nav-link');
    component.$tertiaryNavLinks = component.$navTertiary.find('.nav-link');
    component.$closeLink = component.$navMain.find('[data-nav-action=close]');
    component.$mobileMenuButton = this.$html.find('.mobile-menu-button');

    // Calculate what primary nav items can be shown and which need to be hidden in more menu
    component.adjustNavItems();

    // Check which tabindexes should be applied to navigation links to ensure WCAG compliance
    component.manageTabIndexes();

    // Open navigation on mobile
    component.$mobileMenuButton.on('click', function(event) {
        var $self = $(this);
        event.stopImmediatePropagation();
        component.$body.toggleClass('open-nav');
        $self.toggleClass('open');

        if ($self.text() === 'Menu') {
            $self.text('Close');
            $self.attr('aria-expanded', 'true');
            component.$brandingLink.attr('tabindex', '3');
            component.$primaryNavLinks.attr('tabindex', '3');
        } else {
            $self.text('Menu');
            $self.attr('aria-expanded', 'false');
            component.$brandingLink.attr('tabindex', '-1');
            component.$primaryNavLinks.attr('tabindex', '-1');
        }
    });

    // More button is shown when window is too short to display all primary nav items
    component.$navMain.on('click', '.more-icon > .nav-link', function(event) {
        var $self = $(this);
        event.preventDefault();
        component.moreIconClickHandler($self)
    });

    // Re-adjust nav items on window resize to calc if more button is needed
    component.$window.resize(function () {
        component.adjustNavItems();
        component.manageTabIndexes();
    });

    // Close navs on main content click
    component.$contentMain.on('click', function () {
        component.closeNavs($(this));
    });

    // Open secondary nav on primary nav item click
    component.$primaryNavLinks.on('click', function (event) {
        component.openSecondaryNav($(this), event);
    });

    // Open quaternary nav on tertiary nav item click
    component.$tertiaryNavLinks.on('click', function (event) {
        var $self = $(this),
            target;

        if ($self[0].hasAttribute('href')) {
            target = $self.attr('href');
        }
        else if ($self[0].hasAttribute('data-target')) {
            target = $self.attr('data-target');
        }

        if (target.indexOf('#') !== -1) {
            event.preventDefault();

            // Change aria expanded to true
            $self.attr('aria-expanded', 'true');
        }

        component.openQuaternaryNav(target);
    });

    // Close respective navs on close link click
    component.$closeLink.on('click', function (event) {
        event.preventDefault();
        component.closeNavs($(this));
    });
};

/**
 * Unto the tabindex if the main nav is in responsive mode
 * This maintains the tab order to ensure WCAG compliance
 */
NavMainComponent.prototype.manageTabIndexes = function () {
    var component = this,
        isMobile = !component.window.matchMedia('(min-width: 992px)').matches;

    if (isMobile) {
        component.$brandingLink.attr('tabindex', '-1');
        component.$primaryNavLinks.attr('tabindex', '-1');
    } else {
        component.$brandingLink.attr('tabindex', '1');
        component.$primaryNavLinks.attr('tabindex', '1');
    }
};

/**
 * Open secondary navigation, close all other navs and highlight primary nav item parent
 * @param {jQuery} $linkClicked - jQuery object of the link clicked to open secondary nav
 * @param {Event} event - click event for the primary nav link
 */
NavMainComponent.prototype.openSecondaryNav = function ($linkClicked, event) {
    var component = this,
        target;

    if ($linkClicked[0].hasAttribute('href')) {
        target = $linkClicked.attr('href');
    }
    else if ($linkClicked[0].hasAttribute('data-target')) {
        target = $linkClicked.attr('data-target');
    }

    // Close any previously open navs
    component.closeSecondaryNav();
    component.closeTertiaryNav();
    component.closeQuaternaryNav();

    // Close any open popovers which would appear over the navigation
    component.$html.find('[data-toggle="popover"]').popover('hide');

    // If href is a fragment (therefore opens a sub nav), don't add it to the URL because it breaks the back button
    if (target.indexOf('#') !== -1) {
        event.preventDefault();
        $linkClicked.attr('aria-expanded', 'true');
        component.$navSecondary.addClass('is-open');
        component.$navSecondary.find('.nav-list.is-active').removeClass('is-active');
        component.$navSecondary.find('[data-nav="' + target + '"]').addClass('is-active');
    }

    if (component.$html.find('[data-nav="' + target + '"]').length >= 1) {
        component.$navMain.addClass('is-open');
    } else {
        component.closeNavs($linkClicked);
    }

    component.$navPrimary.find('.is-active').removeClass('is-active');
    component.$navPrimary.find('[href="' + target + '"], [data-target="' + target + '"]').addClass('is-active');
};

/**
 * Open quaternary navigation
 * @param {string} target - href of target nav list
 */
NavMainComponent.prototype.openQuaternaryNav = function (target) {
    var component = this;

    // If menu item has child menu options and therefore is same-page link then open $navQuaternary
    if (target.indexOf('#') !== -1) {
        component.$navQuaternary.addClass('is-open');
    }

    // Show the target nav-list in the opened nav
    component.$navQuaternary.find('[data-nav="' + target + '"]').addClass('is-active');
};

/**
 * Close respective nav(s) depending on which close link was clicked
 * @param {jQuery} $linkClicked - jQuery object of the close link clicked
 */
NavMainComponent.prototype.closeNavs = function ($linkClicked) {
    var component = this,
        $linkParent = $linkClicked.parent();

    if ($linkParent.hasClass('nav-secondary')) {
        component.closeSecondaryNav();
    }

    else if ($linkParent.hasClass('nav-tertiary')) {
        component.closeTertiaryNav();
    }

    else if ($linkParent.hasClass('nav-quaternary')) {
        component.closeQuaternaryNav();
    }

    else {
        component.closeSecondaryNav();
        component.closeTertiaryNav();
        component.closeQuaternaryNav();
    }
}

/**
 * Close secondary navigation
 */
NavMainComponent.prototype.closeSecondaryNav = function () {
    var component = this;

    component.$navMain.removeClass('is-open');
    component.$navMain.find('[aria-expanded=true]').attr( 'aria-expanded', 'false');
    component.$navSecondary.removeClass('is-open');
    component.$primaryNavLinks.removeClass('is-active')
    component.$navSecondary.find('.nav-list').removeClass('is-active');
}

/**
 * Close tertiary navigation
 */
NavMainComponent.prototype.closeTertiaryNav = function () {
    var component = this;

    component.$navTertiary.removeClass('is-open');
    component.$navTertiary.find('.nav-list').removeClass('is-active');

    // Reset aria-expanded on more button
    component.$navMain.find('[aria-expanded=true]')
        .removeClass('is-active')
        .attr('aria-expanded', 'false');
}

/**
 * Close quaternary navigation
 */
NavMainComponent.prototype.closeQuaternaryNav = function () {
    var component = this;

    component.$navQuaternary.removeClass('is-open');
    component.$navQuaternary.find('.nav-list.is-active').removeClass('is-active');

    // Reset aria-expanded on tertiary link
    component.$navTertiary.find('[aria-expanded=true]').attr('aria-expanded', 'false');
}

/**
 * Detect window height, adjust the number of items in the primary nav and check when to add "More" option
 */
NavMainComponent.prototype.adjustNavItems = function () {
    var component = this,
        availableHeight = component.$window.height(),
        navItemsHeight = (component.$html.find('.nav-primary .nav-items').outerHeight(true) + component.$html.find('.jadu-branding').outerHeight(true)),
        moreIconHeight = 72, // Pre calculated height of the "More" nav item
        navItemsCountTotal = component.$html.find('.nav-primary .nav-items li').length,
        numberOfHiddenNavItems = 0;

    // When nav items + more icon height is greater than available window height
    if (navItemsHeight + moreIconHeight > availableHeight) {
        // If there is not enough space hide the last primary nav items
        component.hidePrimaryNavItems(navItemsHeight, moreIconHeight, availableHeight);
        // Get the number of hidden items to make only them visible in the tertiary menu
        numberOfHiddenNavItems = component.$html.find('.nav-primary .nav-items li:hidden').length;
        // Add "More" nav item and check its visibility if already exists
        component.addMoreNavItem(numberOfHiddenNavItems);
        // Hide the primary nav items duplicate in tertiary menu
        component.hideMoreCategoriesTopItems(navItemsCountTotal, numberOfHiddenNavItems);
    } else if (navItemsHeight + moreIconHeight < availableHeight) {
        // Unhide items if they were hidden and there is space in the primary nav
        component.unhidePrimaryNavItems();
        numberOfHiddenNavItems = component.$html.find('.nav-primary .nav-items li:hidden').length;
        component.lastItemSubstitution(numberOfHiddenNavItems);
    }
};

/**
 * Hide primary nav items that don't fit in window
 * @param {number} navItemsHeight - PX height of primary nav items
 * @param {number} moreIconHeight - PX height of more link
 * @param {number} availableHeight - PX height of window
 */
NavMainComponent.prototype.hidePrimaryNavItems = function (navItemsHeight, moreIconHeight, availableHeight) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        lastPrimaryNavItem =  navItems.find('li:last-child'),
        nthChild = 2; // This number represents the item before the last in the nth-last-child;

    while (navItemsHeight + moreIconHeight > availableHeight) {
        // If last nav item is visible hide it
        if (lastPrimaryNavItem.is(':visible')) {
            lastPrimaryNavItem.hide();
        } else {
            // If last nav item is hidden hide the next one up
            navItems.find('li:nth-last-child('+ nthChild +')').hide();
            nthChild++;
        }

        // Recalculate nav items height based on items just hidden
        navItemsHeight = (navItems.outerHeight(true) + component.$html.find('.jadu-branding').outerHeight(true));
    }
};

/**
 * Unhide primary nav items
 */
NavMainComponent.prototype.unhidePrimaryNavItems = function () {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon'),
        firstHiddenPrimaryNavItem = navItems.find('li:hidden').first();

        if ((firstHiddenPrimaryNavItem.length > 0) && (navItemMore.is(':visible'))) {

            // Reset display type to list-item rather than show() to make sure they don't get recreated as inline-block
            firstHiddenPrimaryNavItem.css({display: 'list-item'});
        }
};

/**
 * Add the more link to the primary nav
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.addMoreNavItem = function (numberOfHiddenNavItems) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon');

    // Add the "More" nav item
    if ((numberOfHiddenNavItems > 0) && (!component.$html.find('.more-icon').length)) {
        navItems.append('
<button class="nav-link t-nav-link" aria-haspopup="true" aria-expanded="false" aria-controls="aria-tertiary-nav">More</button>
');
    }

    // Check if "More" nav item is visible
    if (navItemMore.is(':hidden') && (numberOfHiddenNavItems > 0)) {
        navItemMore.show();
    } else {
        component.$html.find('.nav-tertiary').removeClass('is-open');
        component.$html.find('.nav-quaternary').removeClass('is-open');
    }
};

/**
 * Hide the nav items in nav tertiary that are already displayed in the primary nav
 * @param {number} navItemsCountTotal - Total number of nav items
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.hideMoreCategoriesTopItems = function (navItemsCountTotal, numberOfHiddenNavItems) {
    var component = this,
        nthChild = 1, // Number used to iterate nth-child
        itemsToHideCount;

    // Reset hidden nav items
    component.$html.find('.nav-tertiary .nav-items li').show();

    // Calculate the number of visible nav items in primary nav
    itemsToHideCount = navItemsCountTotal - numberOfHiddenNavItems - 1; // 1 is for the "More" nav item

    // Hide top items in "More Categories" equal to the number of visible items of primary nav
    while (itemsToHideCount >= 0) {
        component.$html.find('.nav-tertiary .nav-items li:nth-child('+ nthChild +')').hide();
        nthChild++;
        itemsToHideCount--;
    }
};

/**
 * Hide more link if there are no hidden nav items
 * @param {number} numberOfHiddenNavItems - Number of hidden nav items
 */
NavMainComponent.prototype.lastItemSubstitution = function (numberOfHiddenNavItems) {
    var component = this,
        navItems = component.$html.find('.nav-primary .nav-items'),
        navItemMore = navItems.find('.more-icon');

    if (numberOfHiddenNavItems === 0) {
        navItemMore.hide();
        component.$html.find('.nav-tertiary').removeClass('is-open');
        component.$html.find('.nav-quaternary').removeClass('is-open');
    }
};

/**
 * Hide more link if there are no hidden nav items
 * @param {jQuery} $moreLink - jQuery object of the more link
 */
NavMainComponent.prototype.moreIconClickHandler = function ($moreLink) {
    var component = this;

    $moreLink.addClass('is-active');

    // Close secondary nav if already open
    component.closeSecondaryNav();

    // If tertiary nav is already open
    if (component.$navTertiary.find('.nav-list').hasClass('is-active')) {
        $moreLink.attr('aria-expanded', 'false');
        component.$navTertiary.removeClass('is-open');
        component.$navTertiary.find('.nav-list').removeClass('is-active');
    } else {
        $moreLink.attr('aria-expanded', 'true');

        // Open $navTertiary
        component.$navTertiary.find('.nav-list').addClass('is-active');
        component.$navTertiary.addClass('is-open');
    }
};

module.exports = NavMainComponent;
<|endoftext|>


<commit_before>import React from 'react';
import 'jest-styled-components';
import { mount, shallow } from 'enzyme';
import Icon from 'components/icon';
import TestRenderer from 'react-test-renderer';
import Button from '.';
import StyledButton from './button.style';
import BaseTheme from '../../style/themes/base';
import classicTheme from '../../style/themes/classic';

import { assertStyleMatch } from '../../__spec_helper__/test-utils';
import { rootTagTest } from '../../utils/helpers/tags/tags-specs';

const render = (props, renderer = shallow) => {
  return renderer(
    <Button { ...props } />
  );
};

const variants = ['primary', 'secondary', 'tertiary', 'destructive', 'darkBackground'];
const sizes = { small: [32, 16], medium: [40, 24], large: [48, 32] };
const classicColorVariants = ['blue', 'grey', 'magenta', 'magenta-dull', 'red', 'white'];

describe('Button', () => {
  describe('when no props other than children are passed into the component', () => {
    it('renders the default props and children to match the snapshot', () => {
      const wrapper = render({ children: 'foo' });
      expect(wrapper.contains(<Icon type='filter' />)).toBeFalsy();
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe('when only the "iconPosition" and "iconType" props are passed into the component', () => {
    it('renders the default props and children to match the snapshot with the Icon before children', () => {
      const wrapper = render({ children: 'foo', iconType: 'filter', iconPosition: 'before' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    it('renders the default props and children to match the snapshot with the Icon after children', () => {
      const wrapper = render({ children: 'foo', iconType: 'filter', iconPosition: 'after' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    describe.each(['before', 'after'])(
      'when position is set to "%s"',
      (position) => {
        it('contains an Icon', () => {
          expect(
            render({
              children: 'foo',
              iconType: 'filter',
              iconPosition: position
            }).contains(
              <Icon type='filter' />
            )
          ).toBeTruthy();
        });
      }
    );
  });

  describe('when a subtext prop is passed into the component', () => {
    it('does not render the subtext if the size prop is not "large"', () => {
      const wrapper = render({ children: 'foo', subtext: 'foo' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    it('renders the subtext if the size prop is "large"', () => {
      const wrapper = render({ children: 'foo', size: 'large', subtext: 'foo' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe('when there are no props passed except children', () => {
    it('matchwes the expect styles for a default button with variants "secondary" and "medium"', () => {
      const wrapper = render({ children: 'foo' }, TestRenderer.create).toJSON();
      assertStyleMatch({
        background: 'transparent',
        borderColor: BaseTheme.colors.primary,
        color: BaseTheme.colors.primary,
        fontSize: '14px',
        height: '40px',
        paddingLeft: '24px',
        paddingRight: '24px'
      }, wrapper);
    });
  });

  describe.each(variants)(
    'when setting the "as" prop to "%s"',
    (variant) => {
      it('matches the expected style', () => {
        const wrapper = render({
          children: 'foo', disabled: true, renderAs: variant
        }, TestRenderer.create).toJSON();
        assertStyleMatch({
          background:
          (variant === 'secondary' || variant === 'tertiary' ? 'transparent' : BaseTheme.disabled.button),
          borderColor: (variant === 'secondary' ? BaseTheme.disabled.button : 'transparent'),
          color: BaseTheme.disabled.text
        }, wrapper);
      });
    }
  );

  describe('when the "disabled" prop is passed', () => {
    it('matches the style for the default Button when no "as" and "size" props are passed', () => {
      const wrapper = render({ children: 'foo', disabled: true }, TestRenderer.create).toJSON();
      assertStyleMatch({
        background: 'transparent',
        borderColor: BaseTheme.disabled.button,
        color: BaseTheme.disabled.text,
        fontSize: '14px',
        height: `${sizes.medium[0].toString()}px`,
        paddingLeft: `${sizes.medium[1].toString()}px`,
        paddingRight: `${sizes.medium[1].toString()}px`
      }, wrapper);
    });

    describe.each(Object.keys(sizes))(
      'when a "%s"',
      (size) => {
        describe.each(variants)(
          ' "%s" button is renderred',
          (variant) => {
            it('matches the expected style', () => {
              const wrapper = render({
                children: 'foo', disabled: true, renderAs: variant, size
              }, TestRenderer.create).toJSON();

              assertStyleMatch({
                background:
                (variant === 'secondary' || variant === 'tertiary' ? 'transparent' : BaseTheme.disabled.button),
                borderColor: (variant === 'secondary' ? BaseTheme.disabled.button : 'transparent'),
                color: BaseTheme.disabled.text,
                fontSize: (size === 'large' ? '16px' : '14px'),
                height: `${sizes[size][0].toString()}px`,
                paddingLeft: `${sizes[size][1].toString()}px`,
                paddingRight: `${sizes[size][1].toString()}px`
              }, wrapper);
            });
          }
        );
      }
    );
  });

  describe.each(classicColorVariants)(
    'when the color variant is set to "%s"',
    (variant) => {
      const wrapper = TestRenderer.create(

      );

      it('matches the snapshot with the default props', () => {
        expect(wrapper).toMatchSnapshot();
      });
    }
  );

  describe('when the classic theme is applied', () => {
    describe.each(classicColorVariants)(
      'setting the color variant to "%s"',
      (variant) => {
        const wrapper = TestRenderer.create(

        );

        it('matches the snapshot when default props are passed', () => {
          expect(wrapper).toMatchSnapshot();
        });
      }
    );

    it('matches the expected style for a "blue" Button with default props', () => {
      const wrapper = TestRenderer.create();
      assertStyleMatch({
        background: 'transparent',
        border: '1px solid #255bc7',
        color: '#255bc7'
      }, wrapper.toJSON());
    });

    it('matches the expected style default "disabled" Button', () => {
      const wrapper = TestRenderer.create();
      assertStyleMatch({
        background: '#e6ebed'
      }, wrapper.toJSON());
    });
  });

  describe('A basic button', () => {
    const defaultButton = render({ children: 'Save' });

    it('renders a button with defaults', () => {
      expect(defaultButton.props().renderAs).toEqual('secondary');
      expect(defaultButton.containsMatchingElement(
        Save
      )).toBeTruthy();
    });
  });

  describe('A primary button', () => {
    const primary = render({
      name: 'Primary Button',
      as: 'primary',
      onClick: jest.fn(),
      children: 'Primary'
    });

    it('renders a primary button', () => {
      expect(primary.props().name).toEqual('Primary Button');
      expect(primary.props().renderAs).toEqual('primary');
      expect(primary.containsMatchingElement(
        Primary
      )).toBeTruthy();
    });
  });

  describe('A secondary button', () => {
    const secondary = render({
      name: 'Secondary Button',
      className: 'customClass',
      theme: 'red',
      children: 'Secondary'
    });

    it('renders a secondary button', () => {
      expect(secondary.props().name).toEqual('Secondary Button');
      expect(secondary.props().renderAs).toEqual('secondary');
      expect(secondary.containsMatchingElement(
        Secondary
      )).toBeTruthy();
    });
  });

  describe('A small button', () => {
    const small = render({
      name: 'Small Button',
      size: 'small',
      children: 'Small'
    });

    it('renders a small button', () => {
      expect(small.props().name).toEqual('Small Button');
      expect(small.props().size).toEqual('small');
      expect(small.containsMatchingElement(
        Small
      )).toBeTruthy();
    });
  });

  describe('A large button', () => {
    const large = render({
      name: 'Large Button',
      size: 'large',
      children: 'Large'
    });

    it('renders a large button', () => {
      expect(large.props().name).toEqual('Large Button');
      expect(large.props().size).toEqual('large');
      expect(large.containsMatchingElement(
        Large
      )).toBeTruthy();
    });
  });

  describe('A disabled button', () => {
    const disabled = render({
      name: 'Disabled Button',
      disabled: true,
      children: 'Disabled'
    });

    it('renders a disabled button', () => {
      expect(disabled.props().name).toEqual('Disabled Button');
      expect(disabled.props().renderAs).toEqual('secondary');
      expect(disabled.containsMatchingElement(
        Disabled
      )).toBeTruthy();
      expect(disabled.props().disabled).toEqual(true);
    });
  });

  describe('render', () => {
    describe('default', () => {
      it('renders a button with no link', () => {
        const wrapper = render({ children: 'foo ' }, mount);
        expect(wrapper.find('a').exists()).toEqual(false);
      });
    });

    describe('with href', () => {
      const anchor = render({
        href: '/foo',
        children: 'Anchor'
      }, mount);

      it('renders an anchor', () => {
        const anchorLink = anchor.find('a');
        expect(anchorLink.exists()).toEqual(true);
      });
    });

    describe('with to', () => {
      const to = render({
        to: '/foo',
        children: 'To'
      }, mount);

      it('renders an anchor', () => {
        const toLink = to.find('a');
        expect(toLink.exists()).toEqual(true);
      });
    });
  });

  describe('subtext prop', () => {
    describe('rendered correctly', () => {
      let wrapper;
      beforeEach(() => {
        wrapper = render({ size: 'large', subtext: 'Test', children: 'A Button' });
      });
      it('adds a modify class and outputs in the correct child element', () => {
        const subtextElement = wrapper.find('[data-element="subtext"]');
        expect(subtextElement.length).toEqual(1);
        expect(subtextElement.text()).toEqual('Test');
      });
    });

    describe.each(['small', 'medium'])(
      'when in an ivalid state and "%s"',
      (size) => {
        it('throws an error', () => {
          const subtext = () => { Button.propTypes.subtext({ subtext: 'test', size }); };
          expect(subtext).toThrowError('subtext prop has no effect unless the button is large');
        });
      }
    );
  });

  describe('tags on component', () => {
    const wrapper = shallow(<Button data-element='bar' data-role='baz'>Test</Button>);

    it('includes correct component, element and role data tags', () => {
      rootTagTest(wrapper, 'button', 'bar', 'baz');
    });
  });
});
<commit_msg>typo fix<commit_after>import React from 'react';
import 'jest-styled-components';
import { mount, shallow } from 'enzyme';
import Icon from 'components/icon';
import TestRenderer from 'react-test-renderer';
import Button from '.';
import StyledButton from './button.style';
import BaseTheme from '../../style/themes/base';
import classicTheme from '../../style/themes/classic';

import { assertStyleMatch } from '../../__spec_helper__/test-utils';
import { rootTagTest } from '../../utils/helpers/tags/tags-specs';

const render = (props, renderer = shallow) => {
  return renderer(
    <Button { ...props } />
  );
};

const variants = ['primary', 'secondary', 'tertiary', 'destructive', 'darkBackground'];
const sizes = { small: [32, 16], medium: [40, 24], large: [48, 32] };
const classicColorVariants = ['blue', 'grey', 'magenta', 'magenta-dull', 'red', 'white'];

describe('Button', () => {
  describe('when no props other than children are passed into the component', () => {
    it('renders the default props and children to match the snapshot', () => {
      const wrapper = render({ children: 'foo' });
      expect(wrapper.contains(<Icon type='filter' />)).toBeFalsy();
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe('when only the "iconPosition" and "iconType" props are passed into the component', () => {
    it('renders the default props and children to match the snapshot with the Icon before children', () => {
      const wrapper = render({ children: 'foo', iconType: 'filter', iconPosition: 'before' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    it('renders the default props and children to match the snapshot with the Icon after children', () => {
      const wrapper = render({ children: 'foo', iconType: 'filter', iconPosition: 'after' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    describe.each(['before', 'after'])(
      'when position is set to "%s"',
      (position) => {
        it('contains an Icon', () => {
          expect(
            render({
              children: 'foo',
              iconType: 'filter',
              iconPosition: position
            }).contains(
              <Icon type='filter' />
            )
          ).toBeTruthy();
        });
      }
    );
  });

  describe('when a subtext prop is passed into the component', () => {
    it('does not render the subtext if the size prop is not "large"', () => {
      const wrapper = render({ children: 'foo', subtext: 'foo' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });

    it('renders the subtext if the size prop is "large"', () => {
      const wrapper = render({ children: 'foo', size: 'large', subtext: 'foo' }, TestRenderer.create);
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe('when there are no props passed except children', () => {
    it('matches the expect styles for a default button with variants "secondary" and "medium"', () => {
      const wrapper = render({ children: 'foo' }, TestRenderer.create).toJSON();
      assertStyleMatch({
        background: 'transparent',
        borderColor: BaseTheme.colors.primary,
        color: BaseTheme.colors.primary,
        fontSize: '14px',
        height: '40px',
        paddingLeft: '24px',
        paddingRight: '24px'
      }, wrapper);
    });
  });

  describe.each(variants)(
    'when setting the "as" prop to "%s"',
    (variant) => {
      it('matches the expected style', () => {
        const wrapper = render({
          children: 'foo', disabled: true, renderAs: variant
        }, TestRenderer.create).toJSON();
        assertStyleMatch({
          background:
          (variant === 'secondary' || variant === 'tertiary' ? 'transparent' : BaseTheme.disabled.button),
          borderColor: (variant === 'secondary' ? BaseTheme.disabled.button : 'transparent'),
          color: BaseTheme.disabled.text
        }, wrapper);
      });
    }
  );

  describe('when the "disabled" prop is passed', () => {
    it('matches the style for the default Button when no "as" and "size" props are passed', () => {
      const wrapper = render({ children: 'foo', disabled: true }, TestRenderer.create).toJSON();
      assertStyleMatch({
        background: 'transparent',
        borderColor: BaseTheme.disabled.button,
        color: BaseTheme.disabled.text,
        fontSize: '14px',
        height: `${sizes.medium[0].toString()}px`,
        paddingLeft: `${sizes.medium[1].toString()}px`,
        paddingRight: `${sizes.medium[1].toString()}px`
      }, wrapper);
    });

    describe.each(Object.keys(sizes))(
      'when a "%s"',
      (size) => {
        describe.each(variants)(
          ' "%s" button is renderred',
          (variant) => {
            it('matches the expected style', () => {
              const wrapper = render({
                children: 'foo', disabled: true, renderAs: variant, size
              }, TestRenderer.create).toJSON();

              assertStyleMatch({
                background:
                (variant === 'secondary' || variant === 'tertiary' ? 'transparent' : BaseTheme.disabled.button),
                borderColor: (variant === 'secondary' ? BaseTheme.disabled.button : 'transparent'),
                color: BaseTheme.disabled.text,
                fontSize: (size === 'large' ? '16px' : '14px'),
                height: `${sizes[size][0].toString()}px`,
                paddingLeft: `${sizes[size][1].toString()}px`,
                paddingRight: `${sizes[size][1].toString()}px`
              }, wrapper);
            });
          }
        );
      }
    );
  });

  describe.each(classicColorVariants)(
    'when the color variant is set to "%s"',
    (variant) => {
      const wrapper = TestRenderer.create(

      );

      it('matches the snapshot with the default props', () => {
        expect(wrapper).toMatchSnapshot();
      });
    }
  );

  describe('when the classic theme is applied', () => {
    describe.each(classicColorVariants)(
      'setting the color variant to "%s"',
      (variant) => {
        const wrapper = TestRenderer.create(

        );

        it('matches the snapshot when default props are passed', () => {
          expect(wrapper).toMatchSnapshot();
        });
      }
    );

    it('matches the expected style for a "blue" Button with default props', () => {
      const wrapper = TestRenderer.create();
      assertStyleMatch({
        background: 'transparent',
        border: '1px solid #255bc7',
        color: '#255bc7'
      }, wrapper.toJSON());
    });

    it('matches the expected style default "disabled" Button', () => {
      const wrapper = TestRenderer.create();
      assertStyleMatch({
        background: '#e6ebed'
      }, wrapper.toJSON());
    });
  });

  describe('A basic button', () => {
    const defaultButton = render({ children: 'Save' });

    it('renders a button with defaults', () => {
      expect(defaultButton.props().renderAs).toEqual('secondary');
      expect(defaultButton.containsMatchingElement(
        Save
      )).toBeTruthy();
    });
  });

  describe('A primary button', () => {
    const primary = render({
      name: 'Primary Button',
      as: 'primary',
      onClick: jest.fn(),
      children: 'Primary'
    });

    it('renders a primary button', () => {
      expect(primary.props().name).toEqual('Primary Button');
      expect(primary.props().renderAs).toEqual('primary');
      expect(primary.containsMatchingElement(
        Primary
      )).toBeTruthy();
    });
  });

  describe('A secondary button', () => {
    const secondary = render({
      name: 'Secondary Button',
      className: 'customClass',
      theme: 'red',
      children: 'Secondary'
    });

    it('renders a secondary button', () => {
      expect(secondary.props().name).toEqual('Secondary Button');
      expect(secondary.props().renderAs).toEqual('secondary');
      expect(secondary.containsMatchingElement(
        Secondary
      )).toBeTruthy();
    });
  });

  describe('A small button', () => {
    const small = render({
      name: 'Small Button',
      size: 'small',
      children: 'Small'
    });

    it('renders a small button', () => {
      expect(small.props().name).toEqual('Small Button');
      expect(small.props().size).toEqual('small');
      expect(small.containsMatchingElement(
        Small
      )).toBeTruthy();
    });
  });

  describe('A large button', () => {
    const large = render({
      name: 'Large Button',
      size: 'large',
      children: 'Large'
    });

    it('renders a large button', () => {
      expect(large.props().name).toEqual('Large Button');
      expect(large.props().size).toEqual('large');
      expect(large.containsMatchingElement(
        Large
      )).toBeTruthy();
    });
  });

  describe('A disabled button', () => {
    const disabled = render({
      name: 'Disabled Button',
      disabled: true,
      children: 'Disabled'
    });

    it('renders a disabled button', () => {
      expect(disabled.props().name).toEqual('Disabled Button');
      expect(disabled.props().renderAs).toEqual('secondary');
      expect(disabled.containsMatchingElement(
        Disabled
      )).toBeTruthy();
      expect(disabled.props().disabled).toEqual(true);
    });
  });

  describe('render', () => {
    describe('default', () => {
      it('renders a button with no link', () => {
        const wrapper = render({ children: 'foo ' }, mount);
        expect(wrapper.find('a').exists()).toEqual(false);
      });
    });

    describe('with href', () => {
      const anchor = render({
        href: '/foo',
        children: 'Anchor'
      }, mount);

      it('renders an anchor', () => {
        const anchorLink = anchor.find('a');
        expect(anchorLink.exists()).toEqual(true);
      });
    });

    describe('with to', () => {
      const to = render({
        to: '/foo',
        children: 'To'
      }, mount);

      it('renders an anchor', () => {
        const toLink = to.find('a');
        expect(toLink.exists()).toEqual(true);
      });
    });
  });

  describe('subtext prop', () => {
    describe('rendered correctly', () => {
      let wrapper;
      beforeEach(() => {
        wrapper = render({ size: 'large', subtext: 'Test', children: 'A Button' });
      });
      it('adds a modify class and outputs in the correct child element', () => {
        const subtextElement = wrapper.find('[data-element="subtext"]');
        expect(subtextElement.length).toEqual(1);
        expect(subtextElement.text()).toEqual('Test');
      });
    });

    describe.each(['small', 'medium'])(
      'when in an ivalid state and "%s"',
      (size) => {
        it('throws an error', () => {
          const subtext = () => { Button.propTypes.subtext({ subtext: 'test', size }); };
          expect(subtext).toThrowError('subtext prop has no effect unless the button is large');
        });
      }
    );
  });

  describe('tags on component', () => {
    const wrapper = shallow(<Button data-element='bar' data-role='baz'>Test</Button>);

    it('includes correct component, element and role data tags', () => {
      rootTagTest(wrapper, 'button', 'bar', 'baz');
    });
  });
});
<|endoftext|>


<commit_before>'use strict';

angular.module('gpConnect')
  .controller('headerController', function ($scope, $rootScope, $state, usSpinnerService, $stateParams, UserService, $modal) {

    // Get current user
    UserService.setCurrentUserFromQueryString();
    $scope.currentUser = UserService.getCurrentUser();

    // Direct different roles to different pages at login
    switch ($scope.currentUser.role) {
    case 'gpconnect':
      $state.go('main-search');
      break;
    case 'phr':
      $state.go('patients-summary', {
        patientId: 9999999000
      }); // id is hard coded
      break;
    default:
      $state.go('patients-summary', {
        patientId: 9999999000
      }); // id is hard coded
    }

    $rootScope.$on('$stateChangeSuccess', function (event, to, toParams, from, toState) {
      $scope.backState = from.name;
	  var previousState = '';
      var pageHeader = '';
      var previousPage = '';

      var mainWidth = 0;
      var detailWidth = 0;

      switch (toState.name) {
        case 'main-search':
          previousState = '';
          pageHeader = 'Welcome';
          previousPage = '';
          mainWidth = 12;
          detailWidth = 0;
          break;
      case 'patients-list':
        previousState = 'main-search';
        pageHeader = 'Patient Lists';
        previousPage = 'Patient Dashboard';
        mainWidth = 12;
        detailWidth = 0;
        break;
      case 'patients-summary':
        previousState = 'patients-list';
        pageHeader = 'Patient Summary';
        previousPage = 'Patient Lists';
        mainWidth = 11;
        detailWidth = 0;
        break;
      case 'patients-lookup':
        previousState = '';
        pageHeader = 'Patients lookup';
        previousPage = '';
        mainWidth = 6;
        detailWidth = 6;
        break;
      default:
        previousState = 'patients-list';
        pageHeader = 'Patients Details';
        previousPage = 'Patient Lists';
        mainWidth = 11;
        detailWidth = 0;
        break;
      }

      $scope.pageHeader = pageHeader;
      $scope.previousState = previousState;
      $scope.previousPage = previousPage;

      $scope.mainWidth = mainWidth;
      $scope.detailWidth = detailWidth;

      $scope.goBack = function (patient) {
		var requestHeader = {
          patientId: $stateParams.patientId
        };

        $state.go($scope.backState, requestHeader);
      };

      $scope.userContextViewExists = ('user-context' in $state.current.views);
      $scope.actionsExists = ('actions' in $state.current.views);

      $scope.go = function (patient) {
        $state.go('patients-summary', {
          patientId: patient.id
        });
      };

      if ($scope.currentUser.role === 'gpconnect') {
        $scope.title = UserService.getContent('gpconnect_title');
        $scope.privacy = UserService.getContent('gpconnect_privacy');
      }
      if ($scope.currentUser.role === 'phr') {
        $scope.title = UserService.getContent('phr_title');
      }

      $scope.footer = UserService.getContent('gpconnect_footer');

      $scope.goHome = function () {
        if ($scope.currentUser.role === 'gpconnect') {
          $state.go('main-search');
        }
        if ($scope.currentUser.role === 'phr') {
          $state.go('patients-summary', {
            patientId: 9999999000
          }); // Id is hardcoded
        }
      };

      $scope.gpConnectInfo = function () {
        $modal.open({
            templateUrl: 'views/application/information-modal.html',
            size: 'lg',
            controller: 'InformationModalCtrl'
        });
      };

    });
  });
<commit_msg>Clean up code<commit_after>'use strict';

angular.module('gpConnect')
  .controller('headerController', function ($scope, $rootScope, $state, usSpinnerService, $stateParams, UserService, $modal) {

    // Get current user
    UserService.setCurrentUserFromQueryString();
    $scope.currentUser = UserService.getCurrentUser();

    // Direct different roles to different pages at login
    switch ($scope.currentUser.role) {
    case 'gpconnect':
      $state.go('main-search');
      break;
    case 'phr':
      $state.go('patients-summary', {
        patientId: 9999999000
      }); // id is hard coded
      break;
    default:
      $state.go('patients-summary', {
        patientId: 9999999000
      }); // id is hard coded
    }

    $rootScope.$on('$stateChangeSuccess', function (event, to, toParams, from, toState) {
      $scope.backState = from.name;
	  var previousState = '';
      var pageHeader = '';
      var previousPage = '';

      var mainWidth = 0;
      var detailWidth = 0;

      switch (toState.name) {
        case 'main-search':
          previousState = '';
          pageHeader = 'Welcome';
          previousPage = '';
          mainWidth = 12;
          detailWidth = 0;
          break;
      case 'patients-list':
        previousState = 'main-search';
        pageHeader = 'Patient Lists';
        previousPage = 'Patient Dashboard';
        mainWidth = 12;
        detailWidth = 0;
        break;
      case 'patients-summary':
        previousState = 'patients-list';
        pageHeader = 'Patient Summary';
        previousPage = 'Patient Lists';
        mainWidth = 11;
        detailWidth = 0;
        break;
      case 'patients-lookup':
        previousState = '';
        pageHeader = 'Patients lookup';
        previousPage = '';
        mainWidth = 6;
        detailWidth = 6;
        break;
      default:
        previousState = 'patients-list';
        pageHeader = 'Patients Details';
        previousPage = 'Patient Lists';
        mainWidth = 11;
        detailWidth = 0;
        break;
      }

      $scope.pageHeader = pageHeader;
      $scope.previousState = previousState;
      $scope.previousPage = previousPage;

      $scope.mainWidth = mainWidth;
      $scope.detailWidth = detailWidth;

      $scope.goBack = function () {
		var requestHeader = {
          patientId: $stateParams.patientId
        };

        $state.go($scope.backState, requestHeader);
      };

      $scope.userContextViewExists = ('user-context' in $state.current.views);
      $scope.actionsExists = ('actions' in $state.current.views);

      $scope.go = function (patient) {
        $state.go('patients-summary', {
          patientId: patient.id
        });
      };

      if ($scope.currentUser.role === 'gpconnect') {
        $scope.title = UserService.getContent('gpconnect_title');
        $scope.privacy = UserService.getContent('gpconnect_privacy');
      }
      if ($scope.currentUser.role === 'phr') {
        $scope.title = UserService.getContent('phr_title');
      }

      $scope.footer = UserService.getContent('gpconnect_footer');

      $scope.goHome = function () {
        if ($scope.currentUser.role === 'gpconnect') {
          $state.go('main-search');
        }
        if ($scope.currentUser.role === 'phr') {
          $state.go('patients-summary', {
            patientId: 9999999000
          }); // Id is hardcoded
        }
      };

      $scope.gpConnectInfo = function () {
        $modal.open({
            templateUrl: 'views/application/information-modal.html',
            size: 'lg',
            controller: 'InformationModalCtrl'
        });
      };

    });
  });
<|endoftext|>


<commit_before>/**
 * Created by hunte on 31/05/2016.
 */
var mainAppCtrls = angular.module("mainAppCtrls");
/**
 * @ngdoc controller
 * @name mainAppCtrls.controller:PasswordResetCtrl
 * @description To reset your password and create a new password
 * @requires restSvcs.Admin
 */
mainAppCtrls.controller('PasswordResetCtrl',['$scope','$alert','$translate','Admin','$stateParams',
    function($scope,$alert,$translate,Admin,$stateParams) {

        $scope.form = {};
        var _translations;
        $translate(['TITLE_ERROR','PASSWORDS_IDENTICAL_ERROR','PASSWORD_EMPTY_ERROR']).then(function(translations){
           _translations = translations;
        });
        var pwAlert;
        var _token = $stateParams.token;
        /**
         * @ngdoc function
         * @name mainAppCtrls.controller:PasswordResetCtrl#validatePW
         * @methodOf mainAppCtrls.controller:PasswordResetCtrl
         * @description checks validity of passwords ( if both are identical )
         */
        $scope.validatePW = function () {
            var pw = $scope.password;
            var pwc = $scope.password_confirm;
            if(pwAlert != null){
                pwAlert.hide();
                pwAlert.destroy();
            }
            if (pw != pwc) {
                pwAlert = $alert({
                    container: '#alert',
                    title: _translations.TITLE_ERROR,
                    content: _translations.PASSWORDS_IDENTICAL_ERROR,
                    show: true,
                    dismissable: false,
                    type: 'danger'
                });
                return false;
            } else {
                if(pwAlert != null) {
                    pwAlert.hide();
                    pwAlert.destroy();
                }
                return true;
            }
        };
        /**
         * @ngdoc function
         * @name mainAppCtrls.controller:PasswordResetCtrl#sendInfo
         * @methodOf mainAppCtrls.controller:PasswordResetCtrl
         * @description checks validity and sends a request to change the password to the server
         */
        $scope.sendInfo = function () {
            if(!$scope.validatePW())
                return;

            var pw = $scope.password;
            if(pw == '' || pw == null){
                pwAlert = $alert({
                    container: '#alert',
                    title: _translations.TITLE_ERROR,
                    content: _translations.PASSWORD_EMPTY_ERROR,
                    show: true,
                    dismissable: false,
                    type: 'danger'
                });
                return;
            }
            var _msg = "";
            var _type = "";
            var _title = "";
            Admin.resetPassword({token: _token},{password: $scope.form.password}).$promise.then(function(httpResponse){
                pwAlert = $alert({
                    container: '#alert',
                    title: "Success",
                    content: _msg,
                    type: "success",
                    show: true,
                    dismissable: false
                });
            },function(httpResponse){
                switch(httpResponse.status){
                    case 404:
                        _msg = "Invalid token";
                        break;
                    case 500:
                        _msg = "Internal server error. Please contact your system admin";
                        break;
                }
                pwAlert = $alert({
                    container: '#alert',
                    title: "Error",
                    content: _msg,
                    type: "danger",
                    show: true,
                    dismissable: false
                });

            });

        };
    }

]);<commit_msg>this should fix #297<commit_after>/**
 * Created by hunte on 31/05/2016.
 */
var mainAppCtrls = angular.module("mainAppCtrls");
/**
 * @ngdoc controller
 * @name mainAppCtrls.controller:PasswordResetCtrl
 * @description To reset your password and create a new password
 * @requires restSvcs.Admin
 */
mainAppCtrls.controller('PasswordResetCtrl',['$scope','$alert','$translate','Admin','$stateParams',
    function($scope,$alert,$translate,Admin,$stateParams) {

        $scope.form = {};
        var _translations;
        $translate(['TITLE_ERROR','PASSWORDS_IDENTICAL_ERROR','PASSWORD_EMPTY_ERROR']).then(function(translations){
           _translations = translations;
        });
        var pwAlert;
        var _token = $stateParams.token;
        /**
         * @ngdoc function
         * @name mainAppCtrls.controller:PasswordResetCtrl#validatePW
         * @methodOf mainAppCtrls.controller:PasswordResetCtrl
         * @description checks validity of passwords ( if both are identical )
         */
        $scope.validatePW = function () {
            var pw = $scope.password;
            var pwc = $scope.password_confirm;
            if(pwAlert != null){
                pwAlert.hide();
                pwAlert.destroy();
            }
            if (pw != pwc) {
                pwAlert = $alert({
                    container: '#alert',
                    title: _translations.TITLE_ERROR,
                    content: _translations.PASSWORDS_IDENTICAL_ERROR,
                    show: true,
                    dismissable: false,
                    type: 'danger'
                });
                return false;
            } else {
                if(pwAlert != null) {
                    pwAlert.hide();
                    pwAlert.destroy();
                }
                return true;
            }
        };
        /**
         * @ngdoc function
         * @name mainAppCtrls.controller:PasswordResetCtrl#sendInfo
         * @methodOf mainAppCtrls.controller:PasswordResetCtrl
         * @description checks validity and sends a request to change the password to the server
         */
        $scope.sendInfo = function () {
            if(!$scope.validatePW())
                return;

            var pw = $scope.password;
            if(pw == '' || pw == null){
                pwAlert = $alert({
                    container: '#alert',
                    title: _translations.TITLE_ERROR,
                    content: _translations.PASSWORD_EMPTY_ERROR,
                    show: true,
                    dismissable: false,
                    type: 'danger'
                });
                return;
            }
            var _msg = "";
            var _type = "";
            var _title = "";
            Admin.resetPassword({token: _token,password: $scope.password}).$promise.then(function(httpResponse){
                pwAlert = $alert({
                    container: '#alert',
                    title: "Success",
                    content: _msg,
                    type: "success",
                    show: true,
                    dismissable: false
                });
            },function(httpResponse){
                switch(httpResponse.status){
                    case 404:
                        _msg = "Invalid token";
                        break;
                    case 500:
                        _msg = "Internal server error. Please contact your system admin";
                        break;
                }
                pwAlert = $alert({
                    container: '#alert',
                    title: "Error",
                    content: _msg,
                    type: "danger",
                    show: true,
                    dismissable: false
                });

            });

        };
    }

]);<|endoftext|>


<commit_before>/* jshint node: true, esversion: 6 */

const Crc32 = require('./crc32');
const Util = require('./util');
const codes = require('./ldpccodes');

/**
 * LDPC codec for 802.11n LDPC codes
 */
class Ldpc {

    //######################################################
    //# C O N S T R U C T O R
    //######################################################

    constructor() {
        this.codes = codes;
        this.makeTables();
    }

    makeTables() {
        Object.keys(codes).forEach(k => {
            let rate = codes[k];
            Object.keys(rate.lengths).forEach(k2 => {
                let code = rate.lengths[k2];
                code.A = this.getA(code);
                code.B = this.getB(code);
                code.C = this.getC(code);
                code.D = this.getD(code);
                code.E = this.getE(code);
                code.T = this.getT(code);
            });
        });
    }

    getA(code) {
        let A = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let row = code.Hb[i];
            let val = row.slice(0, code.kb);
            A.push(val);
        }
        return A;
    }

    //let's just make this an array rather than array[]
    getB(code) {
        let pos = code.kb;
        let B = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let val = code.Hb[i][pos];
            B.push(val);
        }
        return B;
    }

    getC(code) {
        let C = code.Hb[code.mb - 1].slice(0, code.kb);
        return C;
    }

    getD(code) {
        let D = [ code.Hb[code.mb - 1][code.kb] ];
        return D;
    }

    getE(code) {
        let E = code.Hb[code.mb - 1].slice(code.kb + 1);
        return E;
    }

    getT(code) {
        let pos = code.kb + 1;
        let T = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let row = code.Hb[i];
            T.push(row.slice(pos));
        }
        return T;
    }

    //######################################################
    //# U T I L I T Y
    //######################################################

    /**
     * Break up a linear array of bits into z-sized subarrays.
     * NOTE:  this assumes that the input array's length is a multiple of z
     * @param {array} inbits array of bits to break up
     * @param {number} size the size of each subarray
     */
    bitsToZ(inbits, size) {
        let zarr = [];
        for (let i = 0, next = size, len = inbits.length ; i < len ; i = next, next += size) {
            let bits = inbits.slice(i, next);
            zarr.push(bits);
        }
        return zarr;
    }

    /**
     * Flatten a Z array (array of arrays of bits) to a single array of bits
     * @param {array} z the array-of-arrays to flatten
     */
    flatten(z) {
        return z.reduce((acc, child) => acc.concat(child), []);
    }

    /**
     * Rotate an array N places to the right.  This assumes
     * that n is less than or equalt to the array length.
     * @param {array} arr the array of bits to rotate
     * @param {*} n the number of spaces to rotate
     */
    arrayRotate(arr, n) {
        if (!n) {
            return arr.slice(0);
        }
        let pos = arr.length - n;
        return arr.slice(pos).concat(arr.slice(0, pos));
    }

    /**
     * Rotate an array N places to the right.  This assumes
     * that n is less than or equalt to the array length.
     * @param {array} arr the array of bits to rotate
     * @param {*} n the number of spaces to rotate
     */
    arrayRotateDeep(qcArr, bitsArr) {
        let arr = [];
        for (let i=0, len = bitsArr.length ; i < len ; i++) {
            let rotation = qcArr[i];
            let rotated = this.arrayRotate(bitsArr[i], rotation);
            arr[i] = rotated;
        }
        return arr;
    }

   /**
     * Sum two arrays of numbers together
     * @param {array} a
     * @param {array} b
     * @return {array} sum of the two arrays
     */
    arrayAdd(a, b) {
        let len = a.length;
        let arr = new Array(len);
        for (let i = 0; i < len; i++) {
            arr[i] = a[i] + b[i];
        }
        return arr;
    }

    /**
     * XOR two arrays of numbers together.  Intended for binary arrays of 1 and 0
     * @param {array} a array of bits
     * @param {array} b array of bits
     * @return {array} xor of the two arrays
     */
    arrayXor(a, b) {
        let arr = [];
        for (let i = 0, len = a.length; i < len; i++) {
            arr[i] = a[i] ^ b[i];
        }
        return arr;
    }

    /**
     * XOR two arrays of arrays of arrays of numbers together
     * @param {array[]} a
     * @param {array[]} b
     * @return {array} xor of the two arrays
     */
    arrayXorDeep(a, b) {
        let arr = []
        for (let i = 0, len = a.length; i < len; i++) {
            arr[i] = this.arrayXor(a[i], b[i]);
        }
        return arr;
    }

    /**
     * Perform one "lambda" operation on a message and matrix row.
     * Iterate through all of the z-sized subarrays of zbits, and
     * rotate each one right by amount in the associated cell
     * of the Hb array.
     * @param {array} row a row in the quasi-cyclic matrix
     * @param {array[]} zbits array of z-sized arrays of bits of message
     * @param {number} z the size of a z-array
     * @param {number} kb width of parity bits
     * @return p a z-sized array with the modulo-2 sum of all of the
     * rotation matrices.
     */
    lambdaI(row, zbits, z, kb) {
        let p = new Array(z).fill(0);
        for (let j = 0; j < kb ; j++) {
            let rotation = row[j]; //how much to rotate?
            if (rotation >= 0) {
                let mj = zbits[j];
                let rotated = this.arrayRotate(mj, rotation);
                p = this.arrayXor(p, rotated);
            }
        }
        return p;
    }

    /**
     * Encode an array of bytes with the given LDPC code
     * @param bytes {array} the bytes to encode
     * @param rateStr {string} the rate from the tables above
     * @param lengthStr {string} the length from the tables above
     * @return {array} the encoded bits
     */
    encode(bytes, rateStr, lengthStr) {
        let rate = codes[rateStr];
        let code = rate.lengths[lengthStr];
        let z = code.z;
        let Hb = code.Hb; //QC table
        let mb = code.mb; //parity in z-blocks
        let kb = code.kb; //message length in z-blocks
        let bits = Util.bytesToBitsBE(bytes);
        bits = bits.slice(0, code.messageBits);  //just in case
        let zbits = this.bitsToZ(bits, z);
        let parityZbits = [];

        /**
         * First get parity bits p0
         * p0 = sum(0..mb-1)lambdaI
         */
        let p0 = new Array(z).fill(0);
        for (let i = 0; i < mb; i++) {
            let row = Hb[i];
            let p = this.lambdaI(row, zbits, z, kb);
            p0 = this.arrayXor(p0, p);
        }
        parityZbits.push(p0);

        /**
         * Now get the remainder of the parity bits
         * pip0 is p0 shifted 1 position right
         * p1 = lambdaI(0) + pip0
         * p2 = lambdaI(1) + pip1
         */
        let lastp = p0;
        for (let i = 0; i < mb - 1; i++) {
            let row = Hb[i];
            let lambdai = this.lambdaI(row, zbits, z, kb);
            let pipN = this.arrayRotate(lastp, 1);
            let p = this.arrayXor(lambdai, pipN);
            parityZbits.push(p);
            lastp = p;
        }

        /**
         * Done.  Append parity bits to end of message bits
         */
        let outbits = bits.slice(0);
        parityZbits.forEach((arr) => {
            outbits = outbits.concat(arr);
        });
        return outbits;
    }

    /**
     * Rotate an array of z-subarrays by a row of rotations.
     * Sum the rotated subarrays.
     * @param {array[number]} row array of rotations
     * @param {array[array[number]]} arr array of z-sized arrays
     * @param {number} z size of each subarray
     * @return z-sized array of the sum
     */
    multiplyQC(row, arr, z, kb) {
        let sum = new Array(z).fill(0);
        for (let i = 0 ; i < kb ; i++) {
            let rotate = row[i];
            let v = arr[i];
            if (rotate === 0) {
                sum = this.arrayXor(sum, v);
            } else if (rotate >= 0) {
                let rotated = this.arrayRotate(v, rotate);
                sum = this.arrayXor(sum, rotated);
            }
        }
        return sum;
    }

   /**
     * Encode an array of bytes with the given LDPC code
     * @param bytes {array} the bytes to encode
     * @param rateStr {string} the rate from the tables above
     * @param lengthStr {string} the length from the tables above
     * @return {array} the encoded bits
     */
    encode2(bytes, rateStr, lengthStr) {
        let rate = codes[rateStr];
        let code = rate.lengths[lengthStr];
        let z = code.z;
        let bits = Util.bytesToBitsBE(bytes);
        bits = bits.slice(0, code.messageBits);  //just in case
        let zbits = this.bitsToZ(bits, z);
        let parityZbits = [];
        let Hb = code.Hb; //QC table
        let mb = code.mb; //message length in z-blocks
        let mb1 = mb - 1;
        let kb = code.kb; //parity in z-blocks

        /**
         * Step 1:  Compute A x s(T)  and C x s(T)
         * AsT is m-1 rows, 1 column
         * Cst is 1 row, 1 column
         */
        let Ast = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let row = code.A[i];
            let res = this.multiplyQC(row, zbits, z, kb);
            Ast.push(res);
        }
        let Cst = this.multiplyQC(code.C, zbits, z, kb);

        /**
         * Step 2: Compute E x (T-1) * A x s(T)
         * ET1xAst is 1 row, 1 column
         */
        let Et1xAst = new Array(z).fill(0);
        for (let i = 0; i < mb1 ; i++) {
            let bits = Ast[i];
            Et1xAst = this.arrayXor(Et1xAst, bits);
        }

        /**
         * Step 3: Compute p1T by p
         */
        let p1 = this.arrayXor(Et1xAst, Cst);

        /**
         * Step 4: Get Bp1
         * Bp1 is m - 1 rows, 1 column
         */
        let Bp1 = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let val = code.B[i];
            let rotated = this.arrayRotate(p1, val);
            Bp1.push(rotated);
        }

        /**
         * Get Tp1 = Ast + Bp1
         * Tp1 is m - 1 row, k columns
         */
        let Tp1 = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let arow = Ast[i];
            let brow = Bp1[i];
            Tp1.push(this.arrayXorDeep(arow, brow));
        }

        /**
         * Use back substitution to solve
         * Tp1 = AsT + Bp1
         * p2 = mb - 1 z-blocks
         */
        let p2 = new Array(mb1).fill(new Array(z).fill(0));
        for (let i = mb1-1 ; i >= 0 ; i--) {
            let xi = Tp1[i];
            for (let j = i ; j < mb1 ; j++) {
                let amount = code.T[i][j];
                let rot = this.arrayRotate(p2[j], z - amount);
                xi = this.arrayXor(xi, rot)
            }
            p2[i] = xi; //no need to scale
        }

        let p1bits = this.flatten(p1);
        let p2bits = this.flatten(p2);
        let outbits = bits.concat(p1bits).concat(p2bits);
        return outbits;
    }

    /**
     * Encode a string with the given LDPC code
     * @param str {string} the string to encode
     * @param rateStr {string} the rate from the tables above
     * @param lenStr {string} the length from the tables above
     * @return {array} the encoded bits
     */
    encodeString(str, rateStr, lenStr) {
        let bytes = Util.stringToBytes(str);
        return this.encode(bytes, lenStr, rateStr);
    }

    /**
     * Decode an array of LDPC-encoded bits with the given LDPC code
     * @param {array} inbits array of bits
     * @param rateStr {string} the rate from the tables above
     * @param lenStr {string} the length from the tables above
     * @return {array} the output bytes
     */
    decode(inbits, rateStr, lenStr) {
        let outbytes = [];
        /**
         * Step 2 ...  ?
         */
        return outbytes;
    }

    /**
     * Decode an array of LDPC-encoded bits with the given LDPC code
     * @param {array} inbits array of bits
     * @param rateStr {string} the rate from the tables above
     * @param lenStr {string} the length from the tables above
     * @param {string} the output string
     */
    decodeString(inbits, rateStr, lenStr) {
        let outbytes = this.decode(inbits);
        let str = Util.bytesToString(outbytes);
        return str;
    }

}

module.exports = Ldpc;<commit_msg>improve comments<commit_after>/* jshint node: true, esversion: 6 */

const Crc32 = require('./crc32');
const Util = require('./util');
const codes = require('./ldpccodes');

/**
 * LDPC codec for 802.11n LDPC codes
 */
class Ldpc {

    //######################################################
    //# C O N S T R U C T O R
    //######################################################

    constructor() {
        this.codes = codes;
        this.makeTables();
    }

    makeTables() {
        Object.keys(codes).forEach(k => {
            let rate = codes[k];
            Object.keys(rate.lengths).forEach(k2 => {
                let code = rate.lengths[k2];
                code.A = this.getA(code);
                code.B = this.getB(code);
                code.C = this.getC(code);
                code.D = this.getD(code);
                code.E = this.getE(code);
                code.T = this.getT(code);
            });
        });
    }

    getA(code) {
        let A = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let row = code.Hb[i];
            let val = row.slice(0, code.kb);
            A.push(val);
        }
        return A;
    }

    //let's just make this an array rather than array[]
    getB(code) {
        let pos = code.kb;
        let B = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let val = code.Hb[i][pos];
            B.push(val);
        }
        return B;
    }

    getC(code) {
        let C = code.Hb[code.mb - 1].slice(0, code.kb);
        return C;
    }

    getD(code) {
        let D = [ code.Hb[code.mb - 1][code.kb] ];
        return D;
    }

    getE(code) {
        let E = code.Hb[code.mb - 1].slice(code.kb + 1);
        return E;
    }

    getT(code) {
        let pos = code.kb + 1;
        let T = [];
        for (let i = 0, len = code.mb - 1; i < len; i++) {
            let row = code.Hb[i];
            T.push(row.slice(pos));
        }
        return T;
    }

    //######################################################
    //# U T I L I T Y
    //######################################################

    /**
     * Break up a linear array of bits into z-sized subarrays.
     * NOTE:  this assumes that the input array's length is a multiple of z
     * @param {array} inbits array of bits to break up
     * @param {number} size the size of each subarray
     * @return {array[]} array of arrays of bits
     */
    bitsToZ(inbits, size) {
        let zarr = [];
        for (let i = 0, next = size, len = inbits.length ; i < len ; i = next, next += size) {
            let bits = inbits.slice(i, next);
            zarr.push(bits);
        }
        return zarr;
    }

    /**
     * Flatten a Z array (array of arrays of bits) to a single array of bits
     * @param {array[]} z the array-of-arrays to flatten
     * @return {array} linear array of bits
     */
    flatten(z) {
        return z.reduce((acc, child) => acc.concat(child), []);
    }

    /**
     * Rotate an array N places to the right.  This assumes
     * that n is less than or equal to the array length.
     * @param {array} arr the array of bits to rotate
     * @param {number} n the number of spaces to rotate
     * @return {array} a copy of the array rotated n places
     *     to the right
     */
    arrayRotate(arr, n) {
        if (!n) {
            return arr.slice(0);
        }
        let pos = arr.length - n;
        return arr.slice(pos).concat(arr.slice(0, pos));
    }

    /**
     * Rotatethe subarrays of an array N places to the right.
     * This assumes that n is less than or equal to the length
     * of all of the subarrays.
     * @param {array[]} arr the array of bits to rotate
     * @param {array} bitsArr an array with each member as the
     *    number of spaces to rotate
     * @return {array[]} a clone of the array with each of the
     * subarrays rotated by the associated number in bitsArr
     */
    arrayRotateDeep(qcArr, bitsArr) {
        let arr = [];
        for (let i=0, len = bitsArr.length ; i < len ; i++) {
            let rotation = qcArr[i];
            let rotated = this.arrayRotate(bitsArr[i], rotation);
            arr[i] = rotated;
        }
        return arr;
    }

   /**
     * Sum two arrays of numbers together
     * @param {array} a array of numbers
     * @param {array} b array of numbers
     * @return {array} sum of the two arrays
     */
    arrayAdd(a, b) {
        let len = a.length;
        let arr = new Array(len);
        for (let i = 0; i < len; i++) {
            arr[i] = a[i] + b[i];
        }
        return arr;
    }

    /**
     * XOR two arrays of numbers together.  Intended for binary arrays of 1 and 0
     * @param {array} a array of bits
     * @param {array} b array of bits
     * @return {array} xor of the two arrays
     */
    arrayXor(a, b) {
        let arr = [];
        for (let i = 0, len = a.length; i < len; i++) {
            arr[i] = a[i] ^ b[i];
        }
        return arr;
    }

    /**
     * XOR two arrays of arrays of numbers together.  Assumes
     * that their lengths are the same and that their corresponding
     * subarrays are the same length.
     * @param {array[]} a an array of bit arrays
     * @param {array[]} b an array of bit arrays
     * @return {array} xor of the two arrays
     */
    arrayXorDeep(a, b) {
        let arr = []
        for (let i = 0, len = a.length; i < len; i++) {
            arr[i] = this.arrayXor(a[i], b[i]);
        }
        return arr;
    }

    /**
     * Perform one "lambda" operation on a message and matrix row.
     * Iterate through all of the z-sized subarrays of zbits, and
     * rotate each one right by amount in the associated cell
     * of the Hb array.
     * @param {array} row a row in the quasi-cyclic matrix
     * @param {array[]} zbits array of z-sized arrays of bits of message
     * @param {number} z the size of a z-array
     * @param {number} kb width of parity bits
     * @return p a z-sized array with the modulo-2 sum of all of the
     * rotation matrices.
     */
    lambdaI(row, zbits, z, kb) {
        let p = new Array(z).fill(0);
        for (let j = 0; j < kb ; j++) {
            let rotation = row[j]; //how much to rotate?
            if (rotation >= 0) {
                let mj = zbits[j];
                let rotated = this.arrayRotate(mj, rotation);
                p = this.arrayXor(p, rotated);
            }
        }
        return p;
    }

    /**
     * Encode an array of bytes with the given LDPC code
     * @param {array} bytes the bytes to encode
     * @param {string} rateStr the rate from the tables above
     * @param {string} lengthStr the length from the tables above
     * @return {array} the encoded bits
     */
    encode(bytes, rateStr, lengthStr) {
        let rate = codes[rateStr];
        let code = rate.lengths[lengthStr];
        let z = code.z;
        let Hb = code.Hb; //QC table
        let mb = code.mb; //parity in z-blocks
        let kb = code.kb; //message length in z-blocks
        let bits = Util.bytesToBitsBE(bytes);
        bits = bits.slice(0, code.messageBits);  //just in case
        let zbits = this.bitsToZ(bits, z);
        let parityZbits = [];

        /**
         * First get parity bits p0
         * p0 = sum(0..mb-1)lambdaI
         */
        let p0 = new Array(z).fill(0);
        for (let i = 0; i < mb; i++) {
            let row = Hb[i];
            let p = this.lambdaI(row, zbits, z, kb);
            p0 = this.arrayXor(p0, p);
        }
        parityZbits.push(p0);

        /**
         * Now get the remainder of the parity bits
         * pip0 is p0 shifted 1 position right
         * p1 = lambdaI(0) + pip0
         * p2 = lambdaI(1) + pip1
         */
        let lastp = p0;
        for (let i = 0; i < mb - 1; i++) {
            let row = Hb[i];
            let lambdai = this.lambdaI(row, zbits, z, kb);
            let pipN = this.arrayRotate(lastp, 1);
            let p = this.arrayXor(lambdai, pipN);
            parityZbits.push(p);
            lastp = p;
        }

        /**
         * Done.  Append parity bits to end of message bits
         */
        let outbits = bits.slice(0);
        parityZbits.forEach((arr) => {
            outbits = outbits.concat(arr);
        });
        return outbits;
    }

    /**
     * Rotate an array of z-subarrays by a row of rotations.
     * Sum the rotated subarrays.
     * @param {array[number]} row array of rotations
     * @param {array[array[number]]} arr array of z-sized arrays
     * @param {number} z size of each subarray
     * @return {array} z-sized array of the sum
     */
    multiplyQC(row, arr, z, kb) {
        let sum = new Array(z).fill(0);
        for (let i = 0 ; i < kb ; i++) {
            let rotate = row[i];
            let v = arr[i];
            if (rotate === 0) {
                sum = this.arrayXor(sum, v);
            } else if (rotate >= 0) {
                let rotated = this.arrayRotate(v, rotate);
                sum = this.arrayXor(sum, rotated);
            }
        }
        return sum;
    }

   /**
     * Encode an array of bytes with the given LDPC code
     * @param {array} bytes the bytes to encode
     * @param {string} rateStr the rate from the tables above
     * @param {string} lengthStr the length from the tables above
     * @return {array} the encoded bits
     */
    encode2(bytes, rateStr, lengthStr) {
        let rate = codes[rateStr];
        let code = rate.lengths[lengthStr];
        let z = code.z;
        let bits = Util.bytesToBitsBE(bytes);
        bits = bits.slice(0, code.messageBits);  //just in case
        let zbits = this.bitsToZ(bits, z);
        let parityZbits = [];
        let Hb = code.Hb; //QC table
        let mb = code.mb; //message length in z-blocks
        let mb1 = mb - 1;
        let kb = code.kb; //parity in z-blocks

        /**
         * Step 1:  Compute A x s(T)  and C x s(T)
         * AsT is m-1 rows, 1 column
         * Cst is 1 row, 1 column
         */
        let Ast = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let row = code.A[i];
            let res = this.multiplyQC(row, zbits, z, kb);
            Ast.push(res);
        }
        let Cst = this.multiplyQC(code.C, zbits, z, kb);

        /**
         * Step 2: Compute E x (T-1) * A x s(T)
         * ET1xAst is 1 row, 1 column
         */
        let Et1xAst = new Array(z).fill(0);
        for (let i = 0; i < mb1 ; i++) {
            let bits = Ast[i];
            Et1xAst = this.arrayXor(Et1xAst, bits);
        }

        /**
         * Step 3: Compute p1T by p
         */
        let p1 = this.arrayXor(Et1xAst, Cst);

        /**
         * Step 4: Get Bp1
         * Bp1 is m - 1 rows, 1 column
         */
        let Bp1 = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let val = code.B[i];
            let rotated = this.arrayRotate(p1, val);
            Bp1.push(rotated);
        }

        /**
         * Get Tp1 = Ast + Bp1
         * Tp1 is m - 1 row, k columns
         */
        let Tp1 = [];
        for (let i = 0 ; i < mb1 ; i++) {
            let arow = Ast[i];
            let brow = Bp1[i];
            Tp1.push(this.arrayXorDeep(arow, brow));
        }

        /**
         * Use back substitution to solve
         * Tp1 = AsT + Bp1
         * p2 = mb - 1 z-blocks
         */
        let p2 = new Array(mb1).fill(new Array(z).fill(0));
        for (let i = mb1-1 ; i >= 0 ; i--) {
            let xi = Tp1[i];
            for (let j = i ; j < mb1 ; j++) {
                let amount = code.T[i][j];
                let rot = this.arrayRotate(p2[j], z - amount);
                xi = this.arrayXor(xi, rot)
            }
            p2[i] = xi; //no need to scale
        }

        let p1bits = this.flatten(p1);
        let p2bits = this.flatten(p2);
        let outbits = bits.concat(p1bits).concat(p2bits);
        return outbits;
    }

    /**
     * Encode a string with the given LDPC code
     * @param {string} str the string to encode
     * @param {string} rateStr the rate from the tables above
     * @param {string} lenStr length from the tables above
     * @return {array} the encoded bits
     */
    encodeString(str, rateStr, lenStr) {
        let bytes = Util.stringToBytes(str);
        return this.encode(bytes, lenStr, rateStr);
    }

    /**
     * Decode an array of LDPC-encoded bits with the given LDPC code
     * @param {array} inbits array of bits
     * @param {string} rateStr the rate from the tables above
     * @param {string} lenStr the length from the tables above
     * @return {array} the output bytes
     */
    decode(inbits, rateStr, lenStr) {
        let outbytes = [];
        /**
         * Step 2 ...  ?
         */
        return outbytes;
    }

    /**
     * Decode an array of LDPC-encoded bits with the given LDPC code
     * @param {array} inbits array of bits
     * @param {string} rateStr the rate from the tables above
     * @param {string} lenStr the length from the tables above
     * @param {string} the output string
     */
    decodeString(inbits, rateStr, lenStr) {
        let outbytes = this.decode(inbits);
        let str = Util.bytesToString(outbytes);
        return str;
    }

}

module.exports = Ldpc;<|endoftext|>