diff --git a/lms/templates/seq_block.html b/lms/templates/seq_block.html index 3c8699bc8f..9c7c900e50 100644 --- a/lms/templates/seq_block.html +++ b/lms/templates/seq_block.html @@ -4,6 +4,23 @@ from django.conf import settings %> + +
% else: % for idx, item in enumerate(items): -
  • +
  • % endif + % if show_dropdown: + ## <%include file='seq_dropdown.html' args="items=items[15:], start_index=15"/> + % endif
    diff --git a/xmodule/js/src/sequence/display.js b/xmodule/js/src/sequence/display.js index 0c136a5b58..c8a692fbbf 100644 --- a/xmodule/js/src/sequence/display.js +++ b/xmodule/js/src/sequence/display.js @@ -4,6 +4,8 @@ (function() { 'use strict'; + const { HtmlUtils } = window.edx; + this.Sequence = (function() { function Sequence(element, runtime) { var self = this; @@ -26,6 +28,9 @@ this.goto = function(event) { return Sequence.prototype.goto.apply(self, [event]); }; + this.toggleDropdown = function(event) { + return Sequence.prototype.toggleDropdown.apply(self, [event]); + }; this.toggleArrows = function() { return Sequence.prototype.toggleArrows.apply(self); }; @@ -38,6 +43,12 @@ this.displayTabTooltip = function(event) { return Sequence.prototype.displayTabTooltip.apply(self, [event]); }; + this.renderDropdown = function() { + return Sequence.prototype.renderDropdown.apply(self); + } + this.handleClickOutsideDropdown = function(event) { + return Sequence.prototype.handleClickOutsideDropdown.apply(self, [event]); + } this.arrowKeys = { LEFT: 37, UP: 38, @@ -62,23 +73,87 @@ this.showCompletion = this.el.data('show-completion'); this.keydownHandler($(element).find('#sequence-list .tab')); this.base_page_title = ($('title').data('base-title') || '').trim(); + this.dropdownButtonTpl = _.template($('#dropdown-button-tpl').text())({}); + this.renderDropdown(); this.bind(); this.render(parseInt(this.el.data('position'), 10)); } + Sequence.prototype.renderDropdown = function() { + // Renders the dropdown when there isn't enough space for all units in the bar + // Hide the dropdown by default and only show if needed. + this.$('#sequence-list > #dropdown-container').hide(); + this.$(`#sequence-list > li.sequence-list-item`).show(); + // Calculate the number of tabs that can fit comfortably and if the + // number of units is greater we show the dropdown. + const tabListWidth = this.$('#sequence-list').width(); + const singleTabWidth = this.$('#sequence-list > li:first').width(); + const tabCount = this.$('#sequence-list > li.sequence-list-item').length; + const overFlowCount = Math.floor(tabListWidth / singleTabWidth); + // Reduce 1 to offsets index and another one to accommodate the button + const overFlowIdx = overFlowCount - 2; + const showDropdown = overFlowCount < tabCount; + if (!showDropdown) { + return; + } + // If the dropdown button doesn't exist add it, otherwise move the + // existing button to the correct place. + if (this.$('#sequence-list > #dropdown-container').length === 0) { + // xss-lint: disable=javascript-jquery-insertion + this.$('#sequence-list > li.sequence-list-item').eq(overFlowIdx).after(this.dropdownButtonTpl); + } else { + this.$('#sequence-list > li.sequence-list-item').eq(overFlowIdx) + // xss-lint: disable=javascript-jquery-insertion + .after(this.$('#sequence-list > #dropdown-container')); + } + // Show the dropdown UX and hide all the overflowing unit buttons. + this.$('#sequence-list > #dropdown-container').show(); + this.$(`#sequence-list > li.sequence-list-item:lt(${overFlowIdx + 1})`).show(); + this.$(`#sequence-list > li.sequence-list-item:gt(${overFlowIdx})`).hide(); + const dropdownList = this.$('#dropdown-sequence-list > ol'); + // The dropdown buttons are modified copies of the unit nav buttons. + dropdownList.empty(); + this.$(`#sequence-list > li.sequence-list-item:gt(${overFlowIdx})`).each((idx, el) => { + const cloneEl = $(el).clone(); + const navButton = cloneEl.find("button"); + const unitTitle = navButton.data('page-title'); + navButton.click(self.goto); + navButton.find(".sequence-tooltip").remove(); + navButton.find("span.icon").after( + HtmlUtils.joinHtml(HtmlUtils.HTML(''), unitTitle, HtmlUtils.HTML('')).toString() + ); + //xss-lint: disable=javascript-jquery-insert-into-target + cloneEl.show().appendTo(dropdownList); + }); + } + Sequence.prototype.$ = function(selector) { return $(selector, this.el); }; Sequence.prototype.bind = function() { this.$('#sequence-list .nav-item').click(this.goto); + $(document).click(this.handleClickOutsideDropdown); + this.$('#dropdown-sequence-list .dropdown-item').click(this.goto); + this.$('#dropdown-sequence-list-button').click(this.toggleDropdown); this.$('#sequence-list .nav-item').keypress(this.keyDownHandler); this.el.on('bookmark:add', this.addBookmarkIconToActiveNavItem); this.el.on('bookmark:remove', this.removeBookmarkIconFromActiveNavItem); this.$('#sequence-list .nav-item').on('focus mouseenter', this.displayTabTooltip); this.$('#sequence-list .nav-item').on('blur mouseleave', this.hideTabTooltip); + $(window).on('resize', _.debounce(this.renderDropdown.bind(this), 200)); }; + Sequence.prototype.handleClickOutsideDropdown = function(event) { + if(!this.$('#dropdown-container')?.[0]?.contains(event.target)) { + this.$('#dropdown-sequence-list').hide(); + } + } + + Sequence.prototype.toggleDropdown = function() { + $('#dropdown-sequence-list').toggle(); + } + Sequence.prototype.previousNav = function(focused, index) { var $navItemList, $sequenceList = $(focused).parent().parent(); @@ -289,6 +364,7 @@ Sequence.prototype.goto = function(event) { var alertTemplate, alertText, isBottomNav, newPosition, widgetPlacement; event.preventDefault(); + this.$('#dropdown-sequence-list').hide(); // Links from courseware ..., was .target_tab if ($(event.currentTarget).hasClass('seqnav')) { diff --git a/xmodule/static/css-builtin-blocks/SequenceBlockDisplay.css b/xmodule/static/css-builtin-blocks/SequenceBlockDisplay.css index 53671879f6..cbc5b19f79 100644 --- a/xmodule/static/css-builtin-blocks/SequenceBlockDisplay.css +++ b/xmodule/static/css-builtin-blocks/SequenceBlockDisplay.css @@ -78,6 +78,13 @@ position: relative; height: 100%; flex-grow: 1; + max-width: calc(100% - 80px); +} + +@media (min-width: 768px) { + .xmodule_display.xmodule_SequenceBlock .sequence-nav .sequence-list-wrapper { + max-width: calc(100% - 160px); + } } @media (max-width: 575.98px) { @@ -91,7 +98,7 @@ display: flex; } -.xmodule_display.xmodule_SequenceBlock .sequence-nav ol li { +.xmodule_display.xmodule_SequenceBlock .sequence-nav ol li, .dropdown-toggle { box-sizing: border-box; min-width: 40px; flex-grow: 1; @@ -104,7 +111,11 @@ border-right-style: solid; } -.xmodule_display.xmodule_SequenceBlock .sequence-nav ol li button { +.xmodule_display.xmodule_SequenceBlock .sequence-nav ol li .dropdown-toggle { + height: 49px !important; +} + +.xmodule_display.xmodule_SequenceBlock .sequence-nav ol li button, .dropdown-toggle { width: 100%; height: 49px; position: relative; @@ -119,6 +130,22 @@ overflow: visible; } +.xmodule_display.xmodule_SequenceBlock .sequence-nav #dropdown-container ol li button { + display: flex; + align-items: center; + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.xmodule_display.xmodule_SequenceBlock .sequence-nav #dropdown-container ol li button .unit-title { + display: flex; + flex-grow: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin: 0 0.5rem; +} + .xmodule_display.xmodule_SequenceBlock .sequence-nav ol li button .icon { display: inline-block; line-height: 100%;