Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions js/models/NavigationButtonModel.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
/**
* @file Navigation Button Model - Stores configuration for a single navigation button
* @module core/js/models/NavigationButtonModel
*/
import LockingModel from 'core/js/models/lockingModel';

/**
* @class NavigationButtonModel
* @classdesc Holds display and behaviour config for one button in the navigation bar.
* Consumed by {@link module:core/js/views/NavigationButtonView NavigationButtonView}.
*/
export default class NavigationButtonModel extends LockingModel {

defaults() {
Expand Down
10 changes: 10 additions & 0 deletions js/models/NavigationModel.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
/**
* @file Navigation Model - Stores navigation bar layout configuration
* @module core/js/models/NavigationModel
*/
import LockingModel from 'core/js/models/lockingModel';

/**
* @class NavigationModel
* @classdesc Holds `_navigation` course config used by
* {@link module:core/js/views/navigationView NavigationView} to control alignment,
* label visibility, and touch-device positioning of the navigation bar.
*/
export default class NavigationModel extends LockingModel {

defaults() {
Expand Down
17 changes: 17 additions & 0 deletions js/navigation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
/**
* @file Navigation Controller - Bootstraps and exposes the navigation bar
* @module core/js/navigation
* @description Instantiates {@link module:core/js/views/navigationView NavigationView} and
* starts it with a {@link module:core/js/models/NavigationModel NavigationModel} once course
* data is ready. Exports the running `NavigationView` instance as `Adapt.navigation`.
*
* @example
* import navigation from 'core/js/navigation';
* navigation.addButton(myButtonView);
*/
import Adapt from 'core/js/adapt';
import NavigationView from 'core/js/views/navigationView';
import NavigationModel from './models/NavigationModel';
Expand All @@ -9,6 +20,12 @@ class NavigationController extends Backbone.Controller {
this.listenTo(Adapt, 'adapt:preInitialize', this.addNavigationBar);
}

/**
* Reads `_navigation` course config and starts the navigation bar, unless
* `_isDefaultNavigationDisabled` is set, in which case only the
* `navigation:initialize` event is fired so plugins can provide their own bar.
* @fires navigation:initialize
*/
addNavigationBar() {
const adaptConfig = Adapt.course.get('_navigation');
if (adaptConfig?._isDefaultNavigationDisabled) {
Expand Down
49 changes: 47 additions & 2 deletions js/views/NavigationButtonView.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @file Navigation Button View - Renders a single navigation bar button
* @module core/js/views/NavigationButtonView
* @description Supports three rendering modes: standard Handlebars, JSX (React), and
* injected (an existing DOM element promoted to a managed view). Handles navigation
* events for built-in actions (back, home, parent, skip, return-to-start) and
* delegates custom events to `Adapt.trigger`.
*/
import Adapt from 'core/js/adapt';
import wait from 'core/js/wait';
import { compile, templates } from 'core/js/reactHelpers';
Expand All @@ -8,6 +16,11 @@ import startController from 'core/js/startController';
import a11y from 'core/js/a11y';
import location from 'core/js/location';

/**
* @class NavigationButtonView
* @classdesc Backbone view for one button in the navigation bar. Managed by
* {@link module:core/js/views/navigationView NavigationView}.
*/
export default class NavigationButtonView extends Backbone.View {

tagName() {
Expand Down Expand Up @@ -51,6 +64,11 @@ export default class NavigationButtonView extends Backbone.View {
};
}

/**
* @param {object} options - Backbone view options
* @param {HTMLElement} [options.el] - When provided the view adopts an existing DOM
* element and is treated as an injected button (no framework rendering).
*/
initialize({ el }) {
if (el) {
this.isInjectedButton = true;
Expand All @@ -63,6 +81,12 @@ export default class NavigationButtonView extends Backbone.View {
this.render();
}

/**
* Name of the Handlebars or JSX template used to render the button.
* Subclasses can override this to supply a custom template.
* A `.jsx` extension enables React rendering mode.
* @type {string}
*/
static get template() {
return 'navButton.jsx';
}
Expand Down Expand Up @@ -104,8 +128,11 @@ export default class NavigationButtonView extends Backbone.View {
}

/**
* Re-render
* @param {string} eventName=null Backbone change event name
* Re-renders the button in response to model changes. For JSX buttons a full
* React render is performed; for injected buttons only attributes and the
* label text are updated; for Handlebars buttons only view properties are synced.
* Bubbling Backbone events (names starting with `"bubble"`) are ignored.
* @param {string|null} [eventName=null] - Backbone change event name
*/
changed(eventName = null) {
if (typeof eventName === 'string' && eventName.startsWith('bubble')) {
Expand Down Expand Up @@ -134,6 +161,18 @@ export default class NavigationButtonView extends Backbone.View {
ReactDOM.render(<Template {...props} />, this.el);
}

/**
* Handles button click, prevents default, and fires `navigation:<eventName>` on
* `Adapt`. Built-in `currentEvent` values handled internally:
* `backButton`, `homeButton`, `parentButton`, `skipNavigation`, `returnToStart`.
* Any other value is forwarded as a plain Adapt event.
* @param {jQuery.Event} event - The click event
* @fires navigation:backButton
* @fires navigation:homeButton
* @fires navigation:parentButton
* @fires navigation:skipNavigation
* @fires navigation:returnToStart
*/
triggerEvent(event) {
event.preventDefault();
const currentEvent = $(event.currentTarget).attr('data-event');
Expand All @@ -160,6 +199,12 @@ export default class NavigationButtonView extends Backbone.View {
}
}

/**
* Stops listening, unmounts any React component, and removes the element from
* the DOM. Uses {@link module:core/js/wait wait} to ensure the unmount completes
* before the element is detached.
* @returns {this}
*/
remove() {
this._isRemoved = true;
this.stopListening();
Expand Down
61 changes: 61 additions & 0 deletions js/views/navigationView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
/**
* @file Navigation View - Renders and manages the navigation bar and its buttons
* @module core/js/views/navigationView
* @description Owns the `<nav>` element inserted before `#app`. Manages a list of
* {@link module:core/js/views/NavigationButtonView NavigationButtonView} instances,
* keeps them sorted by `data-order`, and handles alignment changes on device resize.
*
* @example
* import navigation from 'core/js/navigation';
* // navigation is the running NavigationView instance
* navigation.addButton(myButtonView);
* navigation.removeButton(myButtonView);
*/
import Adapt from 'core/js/adapt';
import device from 'core/js/device';
import tooltips from '../tooltips';
import _ from 'underscore';
import NavigationButtonView from './NavigationButtonView';
import NavigationButtonModel from '../models/NavigationButtonModel';

/**
* @class NavigationView
* @classdesc Backbone view for the main navigation bar. Exposed as `Adapt.navigation`
* via {@link module:core/js/navigation navigation.js}.
*/
class NavigationView extends Backbone.View {

className() {
Expand All @@ -22,6 +40,11 @@ class NavigationView extends Backbone.View {
};
}

/**
* All currently registered {@link module:core/js/views/NavigationButtonView NavigationButtonView}
* instances, including both framework buttons and plugin-injected buttons.
* @type {NavigationButtonView[]}
*/
get buttons() {
return (this._buttons = this._buttons || []);
}
Expand All @@ -35,6 +58,12 @@ class NavigationView extends Backbone.View {
this._classSet = new Set(_.result(this, 'className').trim().split(/\s+/));
}

/**
* Binds the view to a model, registers the back-button tooltip, sets up
* listeners, and triggers an initial render. Called once by
* {@link module:core/js/navigation NavigationController} after course data loads.
* @param {NavigationModel} model - Navigation configuration model
*/
start(model) {
tooltips.register({
_id: 'back',
Expand Down Expand Up @@ -106,6 +135,15 @@ class NavigationView extends Backbone.View {
});
}

/**
* Sorts all child elements of `.nav__inner` by their `data-order` attribute,
* avoiding DOM moves that would steal focus. Also reconciles any buttons
* injected directly into the DOM (not via {@link NavigationView#addButton addButton})
* by wrapping them in ephemeral `NavigationButtonView` instances.
* Called automatically by a `MutationObserver` and after every model change.
* @param {MutationRecord[]|null} [changed=null] - Mutation records from the observer,
* or `null` / a Backbone event string when called manually
*/
sortNavigationButtons(changed) {
if (Array.isArray(changed)) {
// Summarize mutation observer changes
Expand Down Expand Up @@ -189,15 +227,27 @@ class NavigationView extends Backbone.View {
this.listenForInjectedButtons();
}

/**
* Hides the back and home buttons when the learner is at the course root.
* @param {Backbone.Model} contentObjectModel - The content object being navigated to
*/
hideNavigationButton(contentObjectModel) {
const shouldHide = (contentObjectModel.get('_type') === 'course');
this.$('.nav__back-btn, .nav__home-btn').toggleClass('u-display-none', shouldHide);
}

/**
* Restores the back and home buttons after they were hidden by
* {@link NavigationView#hideNavigationButton hideNavigationButton}.
*/
showNavigationButton() {
this.$('.nav__back-btn, .nav__home-btn').removeClass('u-display-none');
}

/**
* Registers a button view, appends it to `.nav__inner`, and re-sorts all buttons.
* @param {NavigationButtonView} buttonView - The button view to add
*/
addButton(buttonView) {
this.buttons.push(buttonView);
const container = this.$('.nav__inner');
Expand All @@ -208,10 +258,21 @@ class NavigationView extends Backbone.View {
this.sortNavigationButtons();
}

/**
* Returns the registered button view with the given `_id`, or `undefined`.
* @param {string} id - The `_id` value of the target button model
* @returns {NavigationButtonView|undefined}
*/
getButton(id) {
return this.buttons.find(button => button.model.get('_id') === id);
}

/**
* Unregisters and removes a button view from the DOM. For injected buttons
* only the element is removed; for framework buttons the full Backbone
* `remove()` lifecycle is used.
* @param {NavigationButtonView} buttonView - The button view to remove
*/
removeButton(buttonView) {
this.buttons = this.buttons.filter(view => view !== buttonView);
this.stopListening(buttonView.model, 'change', this.sortNavigationButtons);
Expand Down
Loading