module typedoc { var hasPositionSticky = $html.hasClass('csspositionsticky'); /** * Defines the known ways to make the navigation sticky. */ enum StickyMode { /** * The navigation is not sticky at all. */ None, /** * The entire secondary navigation will stick to the top. */ Secondary, /** * Only the current root navigation item will stick to the top. */ Current } /** * Controls the sticky behaviour of the secondary menu. */ export class MenuSticky extends Backbone.View { /** * jQuery instance of the current navigation item. */ private $current:JQuery; /** * jQuery instance of the parent representing the entire navigation. */ private $navigation:JQuery; /** * jQuery instance of the parent representing entire sticky container. */ private $container:JQuery; /** * The current state of the menu. */ private state:string = ''; /** * The current mode for determining the sticky position. */ private stickyMode:StickyMode = StickyMode.None; /** * The threshold at which the menu is attached to the top. */ private stickyTop:number; /** * The threshold at which the menu is attached to the bottom. */ private stickyBottom:number; /** * Create a new MenuSticky instance. * * @param options Backbone view constructor options. */ constructor(options:Backbone.ViewOptions) { super(options); this.$current = this.$el.find('> ul.current'); this.$navigation = this.$el.parents('.menu-sticky-wrap'); this.$container = this.$el.parents('.row'); this.listenTo(viewport, 'resize', this.onResize); if (!hasPositionSticky) { this.listenTo(viewport, 'scroll', this.onScroll); } this.onResize(viewport.width, viewport.height); } /** * Set the current sticky state. * * @param state The new sticky state. */ private setState(state:string) { if (this.state == state) return; if (this.state != '') this.$navigation.removeClass(this.state); this.state = state; if (this.state != '') this.$navigation.addClass(this.state); } /** * Triggered after the viewport was resized. * * @param width The width of the viewport. * @param height The height of the viewport. */ private onResize(width:number, height:number) { this.stickyMode = StickyMode.None; this.setState(''); var containerTop = this.$container.offset().top; var containerHeight = this.$container.height(); var bottom = containerTop + containerHeight; if (this.$navigation.height() < containerHeight) { var elHeight = this.$el.height(); var elTop = this.$el.offset().top; if (this.$current.length) { var currentHeight = this.$current.height(); var currentTop = this.$current.offset().top; this.$navigation.css('top', containerTop - currentTop + 20); if (currentHeight < height) { this.stickyMode = StickyMode.Current; this.stickyTop = currentTop; this.stickyBottom = bottom - elHeight + (currentTop - elTop) - 20; } } if (elHeight < height) { this.$navigation.css('top', containerTop - elTop + 20); this.stickyMode = StickyMode.Secondary; this.stickyTop = elTop; this.stickyBottom = bottom - elHeight - 20; } } if (!hasPositionSticky) { this.$navigation.css('left', this.$navigation.offset().left); this.onScroll(viewport.scrollTop); } else { if (this.stickyMode == StickyMode.Current) { this.setState('sticky-current'); } else if (this.stickyMode == StickyMode.Secondary) { this.setState('sticky'); } else { this.setState(''); } } } /** * Triggered after the viewport was scrolled. * * @param scrollTop The current vertical scroll position. */ private onScroll(scrollTop:number) { if (this.stickyMode == StickyMode.Current) { if (scrollTop > this.stickyBottom) { this.setState('sticky-bottom'); } else { this.setState(scrollTop + 20 > this.stickyTop ? 'sticky-current' : ''); } } else if (this.stickyMode == StickyMode.Secondary) { if (scrollTop > this.stickyBottom) { this.setState('sticky-bottom'); } else { this.setState(scrollTop + 20 > this.stickyTop ? 'sticky' : ''); } } } } /** * Register this component. */ registerComponent(MenuSticky, '.menu-sticky'); }