From 77c2d3a611b8afd5e3a2537a458b53773e27cbbc Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Tue, 3 Feb 2026 17:17:55 -0600 Subject: [PATCH 1/7] Add JSDoc to Adapt singleton --- js/adapt.js | 212 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 165 insertions(+), 47 deletions(-) diff --git a/js/adapt.js b/js/adapt.js index 99a50e0b..8ba9d145 100644 --- a/js/adapt.js +++ b/js/adapt.js @@ -1,6 +1,54 @@ +/** + * @file Adapt Singleton - Core framework controller and event bus + * @module core/js/adapt + * @description The Adapt singleton is the central controller for the Adapt Learning Framework. + * It extends {@link LockingModel} to provide state management, event coordination, and lifecycle + * control for the entire course. + * + * **Architecture:** + * - Singleton instance (only one exists per course) + * - Global event bus (all framework events flow through Adapt.trigger/on/off) + * - Lifecycle coordinator (initialization, navigation, teardown) + * - State manager (_canScroll, _isStarted, completion tracking) + * - Plugin coordination (manages plugin wait queues and readiness) + * + * **Key Responsibilities:** + * - Framework initialization and startup sequence + * - Completion checking coordination across components + * - View lifecycle management (create/remove) + * - RTL/LTR direction handling + * - Animation control + * - Relative string parsing for navigation + * + * **Public Events Triggered:** + * - `adapt:preInitialize` - Before initialization begins + * - `adapt:start` - Framework starting + * - `adapt:initialize` - Framework initialized and ready + * - `preRemove` - Before view removal + * - `remove` - During view removal + * - `postRemove` - After view removal + * - `plugins:ready` - All plugins loaded (deprecated) + * + * **State Properties:** + * - `_canScroll` {boolean} - Whether scrolling is allowed (lockable) + * - `_outstandingCompletionChecks` {number} - Pending async completion checks + * - `_pluginWaitCount` {number} - Plugins still loading (deprecated) + * - `_isStarted` {boolean} - Framework has completed initialization + * - `_shouldDestroyContentObjects` {boolean} - Whether to destroy views on navigation + * + * **Important:** Many properties have been moved to dedicated services. + * Use `import service from 'core/js/service'` instead of `Adapt.service`. + */ + import wait from 'core/js/wait'; import LockingModel from 'core/js/models/lockingModel'; +/** + * @class AdaptSingleton + * @classdesc Core framework singleton managing lifecycle, state, and event coordination. + * Exported as single instance. Do not instantiate directly. + * @extends {LockingModel} + */ class AdaptSingleton extends LockingModel { initialize() { @@ -23,6 +71,27 @@ class AdaptSingleton extends LockingModel { }; } + /** + * Initializes the Adapt framework. + * Orchestrates the complete startup sequence: direction setup, animation config, + * plugin loading, and history initialization. + * + * **Initialization Sequence:** + * 1. Apply RTL/LTR direction to DOM + * 2. Configure animation settings + * 3. Trigger `adapt:preInitialize` event + * 4. Wait for all async operations + * 5. Wait for completion checks to finish + * 6. Trigger `adapt:start` event + * 7. Start Backbone history (routing) + * 8. Mark as started + * 9. Trigger `adapt:initialize` event + * + * @async + * @fires adapt:preInitialize + * @fires adapt:start + * @fires adapt:initialize + */ async init() { this.addDirection(); this.disableAnimation(); @@ -49,7 +118,13 @@ class AdaptSingleton extends LockingModel { } /** - * call when entering an asynchronous completion check + * Increments the outstanding completion check counter. + * Call when entering an asynchronous completion check to prevent framework + * initialization from proceeding until the check completes. + * @example + * Adapt.checkingCompletion(); + * await someAsyncCompletionCheck(); + * Adapt.checkedCompletion(); */ checkingCompletion() { const outstandingChecks = this.get('_outstandingCompletionChecks'); @@ -57,7 +132,13 @@ class AdaptSingleton extends LockingModel { } /** - * call when exiting an asynchronous completion check + * Decrements the outstanding completion check counter. + * Call when exiting an asynchronous completion check. + * When counter reaches zero, initialization can proceed. + * @example + * Adapt.checkingCompletion(); + * await someAsyncCompletionCheck(); + * Adapt.checkedCompletion(); */ checkedCompletion() { const outstandingChecks = this.get('_outstandingCompletionChecks'); @@ -65,8 +146,16 @@ class AdaptSingleton extends LockingModel { } /** - * wait until there are no outstanding completion checks - * @param {Function} [callback] Function to be called after all completion checks have been completed + * Waits until all outstanding completion checks have finished. + * Used internally during initialization to ensure all async completion + * checks complete before framework starts. + * @async + * @param {Function} [callback=() => {}] - Callback invoked when all checks complete + * @returns {Promise} Resolves when all completion checks finished + * @example + * await Adapt.deferUntilCompletionChecked(() => { + * Adapt.trigger('adapt:start'); + * }); */ async deferUntilCompletionChecked(callback = () => {}) { if (this.get('_outstandingCompletionChecks') === 0) return callback(); @@ -81,11 +170,19 @@ class AdaptSingleton extends LockingModel { }); } + /** + * @deprecated Use wait.isWaiting() instead + * @returns {boolean} True if waiting for plugins + */ isWaitingForPlugins() { this.log.deprecated('Use wait.isWaiting() as Adapt.isWaitingForPlugins() will be removed in the future'); return wait.isWaiting(); } + /** + * @deprecated Use wait.isWaiting() instead + * @returns {void} + */ checkPluginsReady() { this.log.deprecated('Use wait.isWaiting() as Adapt.checkPluginsReady() will be removed in the future'); if (this.isWaitingForPlugins()) { @@ -95,46 +192,37 @@ class AdaptSingleton extends LockingModel { } /** - * Relative strings describe the number and type of hops in the model hierarchy - * @param {string} relativeString - * Trickle uses this function to determine where it should scrollTo after it unlocks. - * Branching uses this function to determine where it should branch to. - * This function would return the following for a single offset directive: - * { - * type: "component", - * offset: 1, - * inset: null - * } - * "@component+1" returns the next component outside this container, or undefined - * "@component-1" returns the previous component outside of this container, or undefined - * "@block+0" or "@block" returns this block, the first ancestor block, or undefined - * "@type+0" or "@type" returns this of type, the first ancestor of type, or undefined - * This function would return the following for a single inset directive: - * { - * type: "component", - * offset: null, - * inset: 0 - * } - * "@article=0" returns the first article inside this container, or undefined - * "@article=-1" returns the last article inside this container, or undefined - * "@type=n" returns the relatively positioned of type inside this container, or undefined - * This function would return the following for multiple inset and offset directives: - * [ - * { - * type: "block", - * offset: 2, - * inset: null - * }, - * { - * type: "component", - * offset: null, - * inset: 0 - * } - * ] - * "@block+2 @component=0" move two blocks forward and return its first component - * "@block-1 @component=-2" move one block backward and return its second to last component - * "@article+2 @block=1 @component=-1" move two articles forward, find the second block and return its last component - * "@article @component=-1" find the first ancestor article and return its last component + * Parses relative navigation strings into structured directives. + * Used by Trickle to determine scroll targets and by Branching to resolve navigation paths. + * + * **Syntax:** + * - **Offset directives**: `@type+n` or `@type-n` (move n steps forward/back) + * - **Inset directives**: `@type=n` (select nth child, 0-indexed, -1 for last) + * - **Multiple directives**: Space-separated for nested navigation + * + * **Directive Behavior:** + * - Offset (`+`/`-`): Navigate to ancestor or sibling + * - Inset (`=`): Navigate to descendant + * - Omit number: Defaults to 0 (current/first) + * + * @param {string} relativeString - Navigation directive string + * @returns {Object|Array} Parsed directive(s) with `type`, `offset`, `inset` properties + * @example + * Adapt.parseRelativeString('@component+1'); + * + * Adapt.parseRelativeString('@block+0'); + * + * Adapt.parseRelativeString('@article=0'); + * + * Adapt.parseRelativeString('@article=-1'); + * + * Adapt.parseRelativeString('@block+2 @component=0'); + * + * Adapt.parseRelativeString('@block-1 @component=-2'); + * + * Adapt.parseRelativeString('@article+2 @block=1 @component=-1'); + * + * Adapt.parseRelativeString('@article @component=-1'); */ parseRelativeString(relativeString) { const parts = relativeString @@ -167,6 +255,12 @@ class AdaptSingleton extends LockingModel { : parsed; } + /** + * Applies text direction (LTR/RTL) to the DOM. + * Sets `dir` attribute and CSS class on `` element based on config. + * Called during initialization. + * @private + */ addDirection() { const defaultDirection = this.config.get('_defaultDirection'); @@ -175,10 +269,16 @@ class AdaptSingleton extends LockingModel { .attr('dir', defaultDirection); } + /** + * Configures animation settings based on config and browser detection. + * Checks `_disableAnimationFor` array for CSS selectors matching ``. + * If match found, disables animations globally. + * Called during initialization. + * @private + */ disableAnimation() { const disableAnimationArray = this.config.get('_disableAnimationFor'); const disableAnimation = this.config.get('_disableAnimation'); - // Check if animations should be disabled if (disableAnimationArray) { for (let i = 0, l = disableAnimationArray.length; i < l; i++) { @@ -193,6 +293,24 @@ class AdaptSingleton extends LockingModel { $('html').toggleClass('disable-animation', (disableAnimation === true)); } + /** + * Removes the current view and resets child state. + * Called during navigation to clean up previous content before rendering new content. + * Triggers lifecycle events for view teardown coordination. + * + * **Removal Sequence:** + * 1. Mark children as not ready/rendered + * 2. Trigger `preRemove` event + * 3. Wait for async operations + * 4. Destroy view if `_shouldDestroyContentObjects` is true + * 5. Trigger `remove` event + * 6. Trigger `postRemove` event (deferred) + * + * @async + * @fires preRemove + * @fires remove + * @fires postRemove + */ async remove() { const currentView = this.parentView; if (currentView) { @@ -282,8 +400,8 @@ class AdaptSingleton extends LockingModel { get wait() {} /** - * Allows a selector to be passed in and Adapt will navigate to this element. Resolves - * asynchronously when the element has been navigated to. /** + * Allows a selector to be passed in and Adapt will navigate to this element. + * Resolves asynchronously when the element has been navigated to. * @deprecated Please use router.navigateToElement instead. * @param {string} selector CSS selector of the Adapt element you want to navigate to e.g. `".co-05"` * @param {Object} [settings] The settings for the `$.scrollTo` function (See https://github.com/flesler/jquery.scrollTo#settings). From 0456e95709f79bb7c93ec1f3348b52d8efe63189 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Tue, 3 Feb 2026 17:19:42 -0600 Subject: [PATCH 2/7] Add JSDoc header to app --- js/app.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/js/app.js b/js/app.js index 955557d6..472cb62f 100644 --- a/js/app.js +++ b/js/app.js @@ -1,3 +1,42 @@ +/** + * @file Application Bootstrap - Entry point for Adapt Learning Framework + * @module core/js/app + * @description Bootstrap file that orchestrates the Adapt Framework initialization sequence. + * This module loads all core services, initializes the data layer, and starts the framework. + * + * **Initialization Sequence:** + * 1. Import all core services and utilities (wait, device, router, drawer, etc.) + * 2. Import and register all plugins + * 3. Display loading screen to user + * 4. Initialize data service (loads config.json and course data) + * 5. Wait for data ready event + * 6. Initialize Adapt framework (triggers adapt:initialize) + * 7. Start Backbone history and routing + * + * **Architecture:** + * - Side-effect imports initialize singletons (drawer, notify, router, etc.) + * - Data service coordinates async loading of JSON manifests + * - Adapt.init() triggers framework startup after data is ready + * - Loading screen remains visible until first route renders + * + * **Service Registration:** + * The import order matters for some services: + * - `wait` must load early (provides async coordination primitives) + * - `data` must initialize before Adapt.init() is called + * - `router` must load before navigation can begin + * - `startController` must load to handle _start configuration + * - `plugins` must load last to ensure core is ready + * + * **Public Events:** + * This module doesn't trigger events directly but orchestrates the sequence: + * - `data:ready` - Data service finished loading (triggers Adapt.init) + * - `adapt:preInitialize` - Adapt about to initialize (from Adapt.init) + * - `adapt:start` - Framework starting (from Adapt.init) + * - `adapt:initialize` - Framework ready (from Adapt.init) + * + * @example + * import 'core/js/app'; + */ import Adapt from 'core/js/adapt'; import 'core/js/wait'; import 'core/js/deprecated.js'; From af206147f9dc2ccd7e5a685d47d6ca5e667234e4 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Tue, 3 Feb 2026 17:30:11 -0600 Subject: [PATCH 3/7] Add JSDoc to components registry --- js/components.js | 243 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 230 insertions(+), 13 deletions(-) diff --git a/js/components.js b/js/components.js index 81e62e64..b9c2dc1d 100644 --- a/js/components.js +++ b/js/components.js @@ -1,6 +1,68 @@ +/** + * @file Components Registry - Central registration system for Adapt views and models + * @module core/js/components + * @description Singleton service managing the registration and retrieval of Backbone + * Model and View classes for the Adapt Learning Framework. Provides type resolution + * for dynamic component instantiation based on JSON data. + * + * **Architecture:** + * - Singleton controller (exported as instance) + * - Maintains registry mapping component names to Model/View class pairs + * - Supports multiple naming patterns (_component, _type, _view, _model) + * - Handles legacy view-only registrations with deprecation warnings + * - Used by data service for model instantiation and router for view rendering + * + * **Registration Patterns:** + * - Standard: `components.register('name', { model: ModelClass, view: ViewClass })` + * - View-only (deprecated): `components.register('name', { view: ViewClass })` + * - Multiple names: `components.register(['name1', 'name2'], { model, view })` + * - Space-separated: `components.register('name1 name2', { model, view })` + * + * **Type Resolution Priority:** + * 1. `_view` property (explicit view override) + * 2. `_component` property (standard component type) + * 3. `_type` property (generic type identifier) + * 4. Falls back to last available name if no registry match + * + * **Framework Integration:** + * - Data service uses `getModelClass()` to instantiate models from JSON + * - Router uses `getViewClass()` to create views for navigation + * - AdaptView uses `getViewClass()` to render child components + * - Plugins register during initialization via `components.register()` + * + * @example + * import components from 'core/js/components'; + * + * components.register('hotgraphic', { + * model: HotGraphicModel, + * view: HotGraphicView + * }); + * + * @example + * components.register(['article', 'page'], { + * view: ArticleView + * }); + * + * @example + * const ViewClass = components.getViewClass({ + * _component: 'hotgraphic' + * }); + * const view = new ViewClass({ model }); + * + * @example + * const ModelClass = components.getModelClass(model); + * const newModel = new ModelClass(json); + */ + import Backbone from 'backbone'; import logging from 'core/js/logging'; +/** + * @class Components + * @classdesc Component registry controller managing Backbone Model and View class + * registration and retrieval. Singleton instance exported as `components`. + * @extends {Backbone.Controller} + */ class Components extends Backbone.Controller { initialize() { @@ -13,9 +75,53 @@ class Components extends Backbone.Controller { } /** - * Used to register models and views - * @param {string|Array} name The name(s) of the model/view to be registered - * @param {object} object Object containing properties `model` and `view` or (legacy) an object representing the view + * Registers Model and/or View classes for a component type. + * Supports multiple registration patterns: single name, array of names, or space-separated names. + * Automatically assigns template name to view if not specified. Validates that classes extend + * appropriate Backbone base classes. + * + * **Registration Patterns:** + * - Single: `register('hotgraphic', { model, view })` + * - Multiple: `register(['article', 'page'], { view })` + * - Space-separated: `register('menu course', { model })` + * - View-only (deprecated): `register('name', { view })` or `register('name', ViewClass)` + * + * **Validation:** + * - Model must extend `Backbone.Model` or be a Function + * - View must extend `Backbone.View` or be a Function + * - Throws Error if validation fails + * + * **Auto-Configuration:** + * - Sets `view.template = name` if template not already defined + * - Merges with existing registration (allows separate model/view registration) + * + * @param {string|Array} name - Component name(s) to register + * @param {Object} object - Registration object containing model and/or view classes + * @param {Function} [object.model] - Backbone.Model subclass or factory function + * @param {Function} [object.view] - Backbone.View subclass or factory function + * @returns {Object} The registered object (for chaining) + * @throws {Error} If model is not Backbone.Model subclass or Function + * @throws {Error} If view is not Backbone.View subclass or Function + * @example + * components.register('hotgraphic', { + * model: HotGraphicModel, + * view: HotGraphicView + * }); + * + * @example + * components.register(['article', 'page'], { + * view: ArticleView + * }); + * + * @example + * components.register('block', { model: BlockModel }); + * components.register('block', { view: BlockView }); + * + * @example + * components.register('course menu', { + * model: CourseModel, + * view: BoxMenuView + * }); */ register(name, object) { if (Array.isArray(name)) { @@ -59,8 +165,37 @@ class Components extends Backbone.Controller { } /** - * Parses a view class name. - * @param {string|Backbone.Model|Backbone.View|object} nameModelViewOrData The name of the view class you want to fetch e.g. `"hotgraphic"` or its model or its json data + * Resolves the view class name from various input types. + * Supports string names, Backbone.Model instances, Backbone.View instances, and JSON data objects. + * Uses priority resolution: `_view` > `_component` > `_type`. + * + * **Resolution Logic:** + * - String input: Returns as-is + * - Backbone.Model: Extracts JSON and resolves from data + * - Backbone.View: Searches registry for matching view instance + * - Object: Checks `_view`, `_component`, `_type` properties in order + * + * **Fallback Behavior:** + * - Returns first name with registered view class + * - If no match, returns last available property name + * - Throws Error if no name can be derived + * + * @param {string|Backbone.Model|Backbone.View|Object} nameModelViewOrData - Input to resolve view name from + * @returns {string} Resolved view class name + * @throws {Error} If view class name cannot be derived from input + * @example + * const name = components.getViewName('hotgraphic'); + * + * @example + * const name = components.getViewName(model); + * + * @example + * const name = components.getViewName({ + * _component: 'hotgraphic' + * }); + * + * @example + * const name = components.getViewName(viewInstance); */ getViewName(nameModelViewOrData) { if (typeof nameModelViewOrData === 'string') { @@ -95,9 +230,35 @@ class Components extends Backbone.Controller { } /** - * Fetches a view class from the components. For a usage example, see either HotGraphic or Narrative - * @param {string|Backbone.Model|Backbone.View|object} nameModelViewOrData The name of the view class you want to fetch e.g. `"hotgraphic"` or its model or its json data - * @returns {Backbone.View} Reference to the view class + * Retrieves the registered View class for a component. + * Resolves the view name using `getViewName()`, then returns the corresponding class from + * the registry. Supports factory functions (returns result of calling function). + * + * **Resolution Process:** + * 1. Resolve view name from input using `getViewName()` + * 2. Look up registration object in registry + * 3. If view is Function (not Backbone.View subclass), call and return result + * 4. Otherwise return view class directly + * + * **Usage Context:** + * - Router calls this to instantiate views during navigation + * - AdaptView calls this to render child components + * - NotifyPopupView calls this to render notification content + * + * @param {string|Backbone.Model|Backbone.View|Object} nameModelViewOrData - Input to resolve view class from + * @returns {Function|undefined} View class constructor or undefined if not registered + * @example + * const ViewClass = components.getViewClass('hotgraphic'); + * const view = new ViewClass({ model }); + * + * @example + * const ViewClass = components.getViewClass(model); + * const view = new ViewClass({ model }); + * + * @example + * const ViewClass = components.getViewClass({ + * _component: 'narrative' + * }); */ getViewClass(nameModelViewOrData) { const name = this.getViewName(nameModelViewOrData); @@ -114,8 +275,37 @@ class Components extends Backbone.Controller { } /** - * Parses a model class name. - * @param {string|Backbone.Model|object} name The name of the model you want to fetch e.g. `"hotgraphic"`, the model to process or its json data + * Resolves the model class name from various input types. + * Supports string names, Backbone.Model instances, and JSON data objects. + * Uses priority resolution: `_model` > `_component` > `_type`. + * + * **Resolution Logic:** + * - String input: Returns as-is + * - Backbone.Model: Extracts JSON and resolves from data + * - Object: Checks `_model`, `_component`, `_type` properties in order + * + * **Special Cases:** + * - View-only question components: Returns 'question' model with deprecation warning + * - Allows plugins to register view without model by using framework question model + * + * **Fallback Behavior:** + * - Returns first name with registered model class + * - If no match, returns last available property name + * - Throws Error if no name can be derived + * + * @param {string|Backbone.Model|Object} nameModelOrData - Input to resolve model name from + * @returns {string} Resolved model class name + * @throws {Error} If model class name cannot be derived from input + * @example + * const name = components.getModelName('hotgraphic'); + * + * @example + * const name = components.getModelName(model); + * + * @example + * const name = components.getModelName({ + * _component: 'mcq' + * }); */ getModelName(nameModelOrData) { if (typeof nameModelOrData === 'string') { @@ -148,9 +338,36 @@ class Components extends Backbone.Controller { } /** - * Fetches a model class from the components. For a usage example, see either HotGraphic or Narrative - * @param {string|Backbone.Model|object} name The name of the model you want to fetch e.g. `"hotgraphic"` or its json data - * @returns {Backbone.Model} Reference to the view class + * Retrieves the registered Model class for a component. + * Resolves the model name using `getModelName()`, then returns the corresponding class from + * the registry. Supports factory functions (returns result of calling function). + * + * **Resolution Process:** + * 1. Resolve model name from input using `getModelName()` + * 2. Look up registration object in registry + * 3. If model is Function (not Backbone.Model subclass), call and return result + * 4. Otherwise return model class directly + * + * **Usage Context:** + * - Data service calls this during collection instantiation from JSON + * - Router calls this when creating runtime models (menu tracking) + * - Trickle extension uses this to instantiate button models + * + * @param {string|Backbone.Model|Object} nameModelOrData - Input to resolve model class from + * @returns {Function|undefined} Model class constructor or undefined if not registered + * @example + * const ModelClass = components.getModelClass('hotgraphic'); + * const model = new ModelClass(json); + * + * @example + * const ModelClass = components.getModelClass({ + * _component: 'mcq' + * }); + * const model = new ModelClass(data, { parse: true }); + * + * @example + * const ModelClass = components.getModelClass({ _type: 'page' }); + * const page = new ModelClass({ _id: 'p-10' }); */ getModelClass(nameModelOrData) { const name = this.getModelName(nameModelOrData); From ec1be904a473735f196093714c821100ac2e1173 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 4 Feb 2026 08:30:45 -0600 Subject: [PATCH 4/7] Add JSDoc to data service --- js/data.js | 350 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 334 insertions(+), 16 deletions(-) diff --git a/js/data.js b/js/data.js index c2c8c798..d9032c8d 100644 --- a/js/data.js +++ b/js/data.js @@ -1,3 +1,62 @@ +/** + * @file Data Service - Central data store and model collection manager + * @module core/js/data + * @description Singleton service managing all course content models in the Adapt Learning Framework. + * Loads JSON data files, instantiates models, provides lookup utilities, and validates data integrity. + * + * **Architecture:** + * - Singleton collection extending AdaptCollection (Backbone.Collection) + * - Contains all course content models (course, contentObjects, articles, blocks, components) + * - Maintains fast lookup index (`_byAdaptID`) for O(1) model retrieval + * - Coordinates data loading sequence with config and build services + * - Validates data integrity (duplicate IDs, orphaned content, missing parents) + * + * **Data Loading Sequence:** + * 1. Load build.min.js (framework version, course directory) + * 2. Load config.json (language, settings) + * 3. Load language_data_manifest.js (list of JSON files) + * 4. Load all JSON files in parallel (course, contentObjects, etc.) + * 5. Instantiate course model first (allows other models to access course config) + * 6. Instantiate remaining models using component registry + * 7. Validate data integrity (check IDs, tracking IDs, parent references) + * 8. Trigger app:dataLoaded (models can setup) + * 9. Trigger app:dataReady (framework can start) + * + * **Model Instantiation:** + * - Uses components.getModelClass() to determine model type from JSON + * - Respects _component, _type, _model properties for type resolution + * - Falls back to LockingModel if no registered model class + * + * **Public Events Triggered:** + * - `loading` - Starting to load data files + * - `loaded` - Finished loading data files + * - `reset` - Collection reset with new data + * - `ready` - Data service ready for use + * - `courseModel:dataLoading` - About to create course model + * - `courseModel:dataLoaded` - Course model created + * - `app:dataLoaded` - All models instantiated, can call setupModel() + * - `app:dataReady` - All models ready, framework can start + * - `app:languageChanged` - Language changed, data reloaded + * + * **Properties:** + * - `isReady` {boolean} - Service has completed initialization + * - `_byAdaptID` {Object} - Fast lookup index mapping _id to model + * - `Adapt.course` {CourseModel} - Course model (set during load) + * - `Adapt.config` {ConfigModel} - Config model (set during load) + * - `Adapt.build` {BuildModel} - Build metadata (set during load) + * + * @example + * import data from 'core/js/data'; + * + * await data.whenReady(); + * const model = data.findById('c-05'); + * console.log(model.get('title')); + * + * @example + * data.on('ready', () => { + * console.log('Data loaded, models ready'); + * }); + */ import Adapt from 'core/js/adapt'; import offlineStorage from 'core/js/offlineStorage'; import wait from 'core/js/wait'; @@ -10,8 +69,29 @@ import logging from 'core/js/logging'; import location from 'core/js/location'; import _ from 'underscore'; +/** + * @class Data + * @classdesc Central data store managing all course content models. + * Singleton instance exported as `data`. Do not instantiate directly. + * @extends {AdaptCollection} + */ class Data extends AdaptCollection { + /** + * Factory method for creating models from JSON data. + * Called by Backbone.Collection when adding items to the collection. + * Uses component registry to determine appropriate model class. + * + * **Type Resolution:** + * 1. Call components.getModelClass(json) to resolve model type + * 2. Check _component, _type, _model properties + * 3. Look up registered model class in component registry + * 4. Fall back to LockingModel if no registration found + * + * @param {Object} json - Raw JSON data for model + * @returns {Backbone.Model} Instantiated model of appropriate type + * @private + */ model(json) { const ModelClass = components.getModelClass(json); if (!ModelClass) { @@ -31,6 +111,23 @@ class Data extends AdaptCollection { }); } + /** + * Initializes the data loading sequence. + * Loads build metadata, sets framework version, and loads config data. + * Called by app.js during framework bootstrap. + * + * **Initialization Steps:** + * 1. Reset collection (clear any existing data) + * 2. Reset _byAdaptID lookup index + * 3. Load build.min.js (framework version, course directory) + * 4. Set data-adapt-framework-version attribute on html element + * 5. Load config.json and set up language change listeners + * + * @async + * @example + * import data from 'core/js/data'; + * await data.init(); + */ async init () { this.reset(); this._byAdaptID = {}; @@ -40,14 +137,32 @@ class Data extends AdaptCollection { this.loadConfigData(); } + /** + * Handles model addition to collection. + * Updates fast lookup index when model added. + * @param {Backbone.Model} model - Model being added + * @private + */ onAdded(model) { this._byAdaptID[model.get('_id')] = model; } + /** + * Handles model removal from collection. + * Updates fast lookup index when model removed. + * @param {Backbone.Model} model - Model being removed + * @private + */ onRemoved(model) { delete this._byAdaptID[model.get('_id')]; } + /** + * Loads config.json and sets up configuration change listeners. + * Creates ConfigModel instance and listens for language/direction changes. + * Called by init() after build metadata loads. + * @private + */ loadConfigData() { Adapt.config = new ConfigModel(null, { url: Adapt.build.get('coursedir') + '/config.' + Adapt.build.get('jsonext') + `?timestamp=${Adapt.build.get('timestamp')}`, reset: true }); this.listenToOnce(Adapt, 'configModel:loadCourseData', this.onLoadCourseData); @@ -57,6 +172,13 @@ class Data extends AdaptCollection { }); } + /** + * Handles text direction changes. + * Updates html element classes and dir attribute when _defaultDirection changes. + * @param {ConfigModel} model - Config model + * @param {string} direction - New direction ('rtl' or 'ltr') + * @private + */ onDirectionChange(model, direction) { if (direction === 'rtl') { $('html').removeClass('dir-ltr').addClass('dir-rtl').attr('dir', 'rtl'); @@ -66,8 +188,10 @@ class Data extends AdaptCollection { } /** - * Before we actually go to load the course data, we first need to check to see if a language has been set - * If it has we can go ahead and start loading; if it hasn't, apply the defaultLanguage from config.json + * Handles configModel:loadCourseData event to start course data loading. + * Ensures _activeLanguage is set before loading course data. + * If no language set, applies _defaultLanguage from config.json. + * @private */ onLoadCourseData() { if (!Adapt.config.get('_activeLanguage')) { @@ -77,6 +201,14 @@ class Data extends AdaptCollection { this.loadCourseData(); } + /** + * Handles language changes and reloads course data. + * Saves new language to offline storage and resets framework state. + * @param {ConfigModel} model - Config model + * @param {string} language - New language code + * @async + * @private + */ async onLanguageChange(model, language) { await wait.queue(); const previousAttributes = model.previousAttributes(); @@ -91,9 +223,29 @@ class Data extends AdaptCollection { await this.loadCourseData(language, previousLanguage); } + // All code that needs to run before adapt starts should go here + + /** + * Loads course data files for the active language. + * Orchestrates the complete data loading sequence. + * + * **Loading Sequence:** + * 1. Load language_data_manifest.js (list of JSON files) + * 2. Load all JSON files in parallel + * 3. Instantiate course model first + * 4. Instantiate remaining models + * 5. Validate data integrity + * 6. Trigger app:dataLoaded event + * 7. Trigger app:dataReady event + * 8. Mark service as ready + * + * @param {string} [newLanguage] - New language code if language changed + * @param {string} [previousLanguage] - Previous language code if language changed + * @async + * @private + */ async loadCourseData(newLanguage, previousLanguage) { - // All code that needs to run before adapt starts should go here const language = Adapt.config.get('_activeLanguage'); const courseFolder = Adapt.build.get('coursedir') + '/' + language + '/'; @@ -107,6 +259,13 @@ class Data extends AdaptCollection { } + /** + * Loads a JSON file via AJAX. + * Wraps jQuery.getJSON in a Promise and adds __path__ property to loaded data. + * @param {string} path - Path to JSON file + * @returns {Promise} Loaded JSON data with __path__ property + * @private + */ getJSON(path) { return new Promise((resolve, reject) => { $.getJSON(path, data => { @@ -119,6 +278,33 @@ class Data extends AdaptCollection { }); } + /** + * Loads all course content JSON files for a language. + * Reads manifest file for list of JSON files, loads them in parallel, + * flattens data, creates models, and validates integrity. + * + * **Process:** + * 1. Trigger 'loading' event + * 2. Reset collection and lookup index + * 3. Load language_data_manifest.js + * 4. Load all JSON files listed in manifest (parallel) + * 5. Flatten array/object data into single model array + * 6. Create course model first (other models need course config) + * 7. Create remaining models using component registry + * 8. Validate data integrity (checkData) + * 9. Trigger 'reset' and 'loaded' events + * + * **Manifest Fallback:** + * If manifest file not found, falls back to traditional file list: + * course.json, contentObjects.json, articles.json, blocks.json, components.json + * + * @param {string} languagePath - Path to language folder (e.g., 'course/en/') + * @async + * @fires loading + * @fires reset + * @fires loaded + * @private + */ async loadManifestFiles(languagePath) { this.trigger('loading'); this.reset(); @@ -183,10 +369,18 @@ class Data extends AdaptCollection { await wait.queue(); } + /** + * Triggers app:dataLoaded event after all models instantiated. + * Calls setupModel() on each model to allow initialization logic. + * Extensions can listen to this event to setup listeners on new models. + * @async + * @fires app:dataLoaded + * @private + */ async triggerDataLoaded() { + // Setup the newly added models logging.debug('Firing app:dataLoaded'); try { - // Setup the newly added models this.forEach(model => model.setupModel?.()); await wait.queue(); Adapt.trigger('app:dataLoaded'); @@ -196,6 +390,17 @@ class Data extends AdaptCollection { await wait.queue(); } + /** + * Triggers app:dataReady event to signal framework can start. + * If language changed, triggers app:languageChanged first. + * Extensions can listen to this event to perform final setup before framework starts. + * @param {string} [newLanguage] - New language code if changed + * @param {string} [previousLanguage] - Previous language code if changed + * @async + * @fires app:languageChanged + * @fires app:dataReady + * @private + */ async triggerDataReady(newLanguage, previousLanguage) { if (newLanguage) { Adapt.trigger('app:languageChanged', newLanguage, previousLanguage); @@ -210,11 +415,31 @@ class Data extends AdaptCollection { await wait.queue(); } + /** + * Marks data service as ready and triggers ready event. + * Called after all data loading and setup completes. + * @fires ready + * @private + */ triggerInit() { this.isReady = true; this.trigger('ready'); } + /** + * Returns a Promise that resolves when data service is ready. + * If already ready, resolves immediately. Otherwise waits for ready event. + * @returns {Promise} Resolves when data service ready + * @example + * await data.whenReady(); + * console.log('Data loaded'); + * + * @example + * data.whenReady().then(() => { + * const course = Adapt.course; + * console.log(course.get('title')); + * }); + */ whenReady() { if (this.isReady) return Promise.resolve(); return new Promise(resolve => { @@ -223,18 +448,33 @@ class Data extends AdaptCollection { } /** - * Checks if a model _id exists - * @param {string} id The id of the item e.g. "co-05" - * @returns {boolean} + * Checks if a content model with the given ID exists in the collection. + * Fast O(1) lookup using internal index. + * @param {string} id - Model ID to check (e.g., 'co-05', 'a-10', 'c-15') + * @returns {boolean} True if model exists + * @example + * if (data.hasId('c-05')) { + * console.log('Component exists'); + * } */ hasId(id) { return Boolean(this._byAdaptID[id]); } /** - * Looks up a model by its `_id` property - * @param {string} id The id of the item e.g. "co-05" - * @return {Backbone.Model} + * Finds a content model by its _id property. + * Fast O(1) lookup using internal index. Logs warning if not found. + * @param {string} id - Model ID to find (e.g., 'co-05', 'a-10', 'c-15') + * @returns {Backbone.Model|undefined} Model instance or undefined if not found + * @example + * const component = data.findById('c-05'); + * if (component) { + * console.log(component.get('title')); + * } + * + * @example + * const block = data.findById('b-10'); + * const components = block.getChildren(); */ findById(id) { const model = this._byAdaptID[id]; @@ -246,9 +486,26 @@ class Data extends AdaptCollection { } /** - * Looks up a view by its model `_id` property - * @param {string} id The id of the item e.g. "co-05" - * @return {Backbone.View} + * Finds a rendered view by its model's _id property. + * Walks the view tree from current location to find the target view. + * Only returns views that are children of the current location (Adapt.parentView). + * + * **Navigation Context:** + * - Only finds views within current location's view tree + * - Will not find views on different pages/menus + * - Returns undefined if view not rendered or not in current location + * + * @param {string} id - Model ID to find view for (e.g., 'c-05', 'b-10') + * @returns {Backbone.View|undefined} View instance or undefined if not found + * @example + * const view = data.findViewByModelId('c-05'); + * if (view) { + * view.$el.addClass('highlight'); + * } + * + * @example + * const blockView = data.findViewByModelId('b-10'); + * const componentViews = blockView.getChildViews(); */ findViewByModelId(id) { const model = this.findById(id); @@ -276,9 +533,27 @@ class Data extends AdaptCollection { } /** - * Returns the model represented by the trackingPosition. - * @param {Array} trackingPosition Represents the relative location of a model to a _trackingId - * @returns {Backbone.Model} + * Finds a content model by its tracking position. + * Tracking positions are [trackingId, relativeIndex] arrays where: + * - Positive index: descendant of tracking ID model + * - Negative index: ancestor of tracking ID model + * - Zero index: tracking ID model itself + * + * **Tracking Position Format:** + * - `[123, 0]` - Model with _trackingId=123 + * - `[123, 1]` - First trackable descendant of tracking ID 123 + * - `[123, -1]` - Immediate parent of tracking ID 123 + * - `[123, -2]` - Grandparent of tracking ID 123 + * + * @param {Array} trackingPosition - [trackingId, relativeIndex] array + * @returns {Backbone.Model|undefined} Model at tracking position or undefined + * @example + * const model = data.findByTrackingPosition([123, 0]); + * console.log(model.get('_trackingId')); + * + * @example + * const firstDescendant = data.findByTrackingPosition([123, 1]); + * const parent = data.findByTrackingPosition([123, -1]); */ findByTrackingPosition(trackingPosition) { const [ trackingId, indexInTrackingIdDescendants ] = trackingPosition; @@ -299,17 +574,44 @@ class Data extends AdaptCollection { return trackingIdAncestors[ancestorDistance]; } + /** + * Logs error when view fails to become ready and forces descendants to ready state. + * Called by router when view rendering times out. + * Forces _isReady=true on all not-ready descendants to allow framework to continue. + * @param {Backbone.View} view - View that failed to become ready + * @private + */ logReadyError(view) { const notReadyDescendants = view.model.getAllDescendantModels(true).filter(model => !model.get('_isReady')); logging.error(`View ${notReadyDescendants.map(model => `${model.get('_id')} (${model.get('_component') ?? model.get('_type')})`).join(', ')} failed to become ready, forcing ready status.`); notReadyDescendants.reverse().forEach(model => model.set('_isReady', true)); } + /** + * Validates data integrity after loading all models. + * Runs multiple validation checks and throws errors if problems found. + * Called automatically after manifest files load. + * @private + */ checkData() { this.checkIds(); this.checkTrackingIds(); } + /** + * Validates content model ID integrity. + * Checks for duplicate IDs, orphaned content, missing parents, and empty containers. + * Throws Error with code 10011 if validation fails. + * + * **Validation Checks:** + * - Duplicate _id values (same ID used multiple times) + * - Orphaned content (no _parentId or parent doesn't exist) + * - Missing parent IDs (referenced parent doesn't exist in data) + * - Empty containers (non-component with no children) + * + * @throws {Error} If validation fails (error.number = 10011) + * @private + */ checkIds() { const items = this.toJSON(); // Index and group @@ -363,6 +665,22 @@ class Data extends AdaptCollection { } } + /** + * Validates tracking ID integrity. + * Checks for missing and duplicate _trackingId values on trackable content. + * Throws Error with code 10011 if validation fails. + * + * **Validation Checks:** + * - Missing _trackingId on trackable content (determined by trackingIdType) + * - Duplicate _trackingId values (same tracking ID used multiple times) + * + * **Tracking ID Type:** + * Configured in build.min.js as trackingIdType (default: 'block') + * Typically blocks are tracked, but can be configured for other types. + * + * @throws {Error} If validation fails (error.number = 10011) + * @private + */ checkTrackingIds() { const items = this.toJSON(); const trackingIdType = Adapt.build.get('trackingIdType') || 'block'; From eff2cbcf2b238f7aab3942aeb1462b0d8787cd49 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 4 Feb 2026 08:43:27 -0600 Subject: [PATCH 5/7] Enhance router with docs, preview & navigation --- js/router.js | 417 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 411 insertions(+), 6 deletions(-) diff --git a/js/router.js b/js/router.js index cfa0ea3d..66f61b56 100644 --- a/js/router.js +++ b/js/router.js @@ -1,3 +1,79 @@ +/** + * @file Router Service - Core navigation controller for the Adapt Learning Framework + * @module core/js/router + * @description Singleton service managing all navigation, routing, and content rendering + * for the Adapt Learning Framework. Handles URL routing, content object rendering, + * view lifecycle management, loading states, and navigation history. + * + * **Architecture:** + * - Singleton controller extending Backbone.Router + * - Manages four route patterns: home, id-based, preview, and plugin routes + * - Coordinates with location service for state management + * - Coordinates with data service for model lookups + * - Controls navigation protection during rendering (_canNavigate flag) + * - Handles circular navigation protection to prevent infinite loops + * + * **Route Patterns:** + * - `#/` - Navigate to root content object (course/menu) + * - `#/id/:id` - Navigate to specific content object or sub-content by ID + * - `#/preview/:id` - Navigate to preview mode for content (creates containers) + * - `#/:pluginName/*location/*action` - Navigate to plugin-specific routes + * + * **Navigation Flow:** + * 1. Route triggered (URL change or programmatic navigation) + * 2. `handleRoute()` checks `_canNavigate` flag and circular navigation protection + * 3. Triggers `router:navigate` event (extensions can cancel navigation) + * 4. Sets `_canNavigate` to false (prevents navigation during rendering) + * 5. Calls appropriate handler (handleId, handleIdPreview, handlePluginRouter) + * 6. Updates location service state + * 7. Removes previous view and renders new view + * 8. Sets `_canNavigate` to true (allows navigation again) + * + * **Circular Navigation Protection:** + * - Prevents infinite loops when URL changes while `_canNavigate` is false + * - Uses `_isCircularNavigationInProgress` flag to track redirection attempts + * - Automatically corrects URL back to current location if navigation blocked + * + * **Public Events Triggered:** + * - `router:navigate` - Before navigation begins (can be canceled) + * - `router:navigationCancelled` - Navigation was blocked by `_canNavigate` + * - `router:location` - Location has changed (after state update) + * - `router:contentObject` - Content object will be rendered + * - `router:{type}` - Specific content type will be rendered (router:menu, router:page) + * - `router:plugin` - Plugin route handler called + * - `router:plugin:{pluginName}` - Specific plugin route handler called + * - `{type}:scrollTo` - Before scrolling to element + * - `{type}:scrolledTo` - After scrolling to element completed + * + * **State Management:** + * - Uses `router.model` (RouterModel) to track `_canNavigate` and `_shouldNavigateFocus` + * - Updates `location` service with current/previous model and ID + * - Manages loading visibility via HTML classes and DOM elements + * - Tracks backward navigation for history correction + * + * **Preview Mode:** + * - Creates temporary container models (page, article, block) for component preview + * - Clones content models to prevent modification of original data + * - Marks preview content with `_isPreview` flag for cleanup + * - Syncs preview state changes back to original models + * + * @example + * import router from 'core/js/router'; + * + * router.navigateToElement('.c-05'); + * + * @example + * router.navigate('#/id/co-05', { trigger: true }); + * + * @example + * router.navigateToParent(); + * + * @example + * Adapt.on('router:location', (location) => { + * console.log('Navigated to:', location._currentId); + * }); + */ + import Adapt from 'core/js/adapt'; import wait from 'core/js/wait'; import components from 'core/js/components'; @@ -7,8 +83,27 @@ import RouterModel from 'core/js/models/routerModel'; import logging from 'core/js/logging'; import location from 'core/js/location'; +/** + * @class Router + * @classdesc Core navigation controller managing routing, content rendering, and navigation state. + * Singleton instance exported as `router`. Do not instantiate directly. + * @extends {Backbone.Router} + */ class Router extends Backbone.Router { + /** + * Defines URL route patterns and their handler methods. + * Called by Backbone.Router during initialization. + * + * **Route Patterns:** + * - `''` - Root route (navigates to course or root content object) + * - `'id/:id'` - Navigate to content by ID + * - `'preview/:id'` - Preview mode navigation (creates container models) + * - `':pluginName(/*location)(/*action)'` - Plugin-specific routes + * + * @returns {Object} Route configuration mapping patterns to handler method names + * @private + */ routes() { return { '': 'handleRoute', @@ -37,24 +132,62 @@ class Router extends Backbone.Router { this.listenToOnce(Adapt, 'configModel:dataLoaded', this.onConfigLoaded); } + /** + * Gets the root navigation model (navigation starting point). + * Returns custom root if set via setter, otherwise returns Adapt.course. + * Used by role selector and other extensions to change navigation hierarchy. + * + * @returns {AdaptModel} Root content object model for navigation + * @example + * const root = router.rootModel; + */ get rootModel() { return this._navigationRoot || Adapt.course; } + /** + * Sets a custom root navigation model. + * Allows extensions to override the default course root for navigation. + * + * @param {AdaptModel} model - New root content object for navigation + * @example + * router.rootModel = roleBasedStartPage; + */ set rootModel(model) { this._navigationRoot = model; } + /** + * Shows the loading screen. + * Adds `is-loading-visible` class to html element and displays `.js-loading` element. + * Called automatically during content object navigation. + */ showLoading() { $('html').removeClass('is-loading-hidden').addClass('is-loading-visible'); $('.js-loading').show(); } + /** + * Hides the loading screen. + * Adds `is-loading-hidden` class to html element and hides `.js-loading` element. + * Called automatically after content object rendering completes. + */ hideLoading() { $('html').addClass('is-loading-hidden').removeClass('is-loading-visible'); $('.js-loading').hide(); } + /** + * Sets the browser document title based on current location. + * Combines root model title with current model title if available. + * Updates on next `contentObjectView:preRender` event. + * + * **Title Format:** + * - Root only: "Course Title" + * - With sub-content: "Course Title | Page Title" + * + * @private + */ setDocumentTitle() { const currentModel = location._currentModel; const hasSubTitle = (currentModel && currentModel !== router.rootModel && currentModel.get('title')); @@ -68,6 +201,19 @@ class Router extends Backbone.Router { }); } + /** + * Handles navigation triggered by legacy `Adapt.trigger('router:navigateTo')` pattern. + * Converts arguments to appropriate URL format and calls navigate(). + * + * **Argument Patterns:** + * - Single ID: Converts to `#/id/:id` route + * - Multiple args (≤3): Joins as `#/arg1/arg2/arg3` + * - More than 3: Falls back to direct `handleRoute()` call (deprecated) + * + * @param {Array} args - Navigation arguments from event trigger + * @private + * @deprecated Prefer using Backbone.history.navigate or window.location.href + */ navigateToArguments(args) { args = args.filter(v => v !== null); const options = { trigger: false, replace: false }; @@ -83,11 +229,37 @@ class Router extends Backbone.Router { this.handleRoute(...args); } + /** + * Handles preview route pattern (`#/preview/:id`). + * Sets preview mode flag and delegates to `handleRoute()`. + * + * @param {...string} args - Route parameters (id, optional additional params) + * @private + */ handlePreview(...args) { this.isPreviewMode = true; this.handleRoute(...args); } + /** + * Primary route handler for all navigation. + * Coordinates navigation protection, circular navigation detection, and routing delegation. + * Called automatically when URL changes or navigation is triggered programmatically. + * + * **Navigation Protection:** + * - Checks `_canNavigate` flag to prevent navigation during rendering + * - If blocked, triggers `router:navigationCancelled` and corrects URL + * - Uses `_isCircularNavigationInProgress` to prevent infinite redirect loops + * + * **Route Delegation:** + * - 0-1 args: Calls `handleId()` or `handleIdPreview()` (content object navigation) + * - 2+ args: Calls `handlePluginRouter()` (plugin-specific routes) + * + * @param {...string} args - Route parameters extracted from URL + * @fires router:navigate + * @fires router:navigationCancelled + * @private + */ handleRoute(...args) { if (this._shouldIgnoreNextRouteAction) { this._shouldIgnoreNextRouteAction = false; @@ -139,6 +311,23 @@ class Router extends Backbone.Router { this._isBackward = false; } + /** + * Handles plugin-specific routes (`#/:pluginName/*location/*action`). + * Updates location service and triggers plugin-specific events. + * Allows plugins to manage their own routing and rendering. + * + * **Plugin Events:** + * - `router:plugin:{pluginName}` - Specific plugin route triggered + * - `router:plugin` - Generic plugin route triggered + * + * @param {string} pluginName - Name of the plugin handling the route + * @param {string} [location] - Plugin-specific location parameter + * @param {string} [action] - Plugin-specific action parameter + * @async + * @fires router:plugin:{pluginName} + * @fires router:plugin + * @private + */ async handlePluginRouter(pluginName, location, action) { const pluginLocation = [ pluginName, @@ -152,6 +341,32 @@ class Router extends Backbone.Router { this.model.set('_canNavigate', true, { pluginName: 'adapt' }); } + /** + * Handles navigation to content objects by ID. + * Primary navigation method for rendering course content (menus and pages). + * + * **Navigation Logic:** + * 1. Validates ID and finds model in data collection + * 2. Checks for content locking and start controller restrictions + * 3. If navigating to sub-content (article/block/component), scrolls without re-rendering + * 4. For content objects, removes current view and renders new view + * 5. Updates location service and triggers appropriate events + * 6. Waits for view ready before allowing further navigation + * + * **Sub-Content Navigation:** + * - If target is within current content object, scrolls to element instead of re-rendering + * - Preserves view state and improves performance + * + * **Locking:** + * - Respects `_isLocked` property when `_forceRouteLocking` config is enabled + * - Navigates back or to home if attempting to access locked content + * + * @param {string} [id] - Content object ID to navigate to (undefined navigates to root) + * @async + * @fires router:{type} + * @fires router:contentObject + * @private + */ async handleId(id) { const rootModel = router.rootModel; let model = (!id) ? rootModel : data.findById(id); @@ -249,6 +464,28 @@ class Router extends Backbone.Router { } + /** + * Handles preview mode navigation for content. + * Creates temporary container models (page, article, block) if previewing non-content-object. + * Clones the target content to prevent modification of original data. + * + * **Preview Container Generation:** + * - If previewing component: Creates page > article > block > component hierarchy + * - If previewing block: Creates page > article > block hierarchy + * - If previewing article: Creates page > article hierarchy + * - All preview containers marked with `_isPreview: true` + * + * **Content Cloning:** + * - Deep clones target content to create isolated preview instance + * - Makes content available and unlocked regardless of original state + * - Syncs preview state changes back to original model + * + * @param {string} [id] - Content ID to preview (undefined previews root) + * @async + * @fires router:{type} + * @fires router:contentObject + * @private + */ async handleIdPreview(id) { const rootModel = router.rootModel; let model = (!id) ? rootModel : data.findById(id); @@ -365,11 +602,44 @@ class Router extends Backbone.Router { } + /** + * Removes all preview content from data collection. + * Cleans up temporary models created by `handleIdPreview()`. + * Called before rendering new content to prevent preview model accumulation. + * + * @async + * @private + */ async removePreviews() { const previews = data.filter(model => model.get('_isPreview')); previews.forEach(model => data.remove(model)); } + /** + * Updates the location service state with new navigation context. + * Stores previous location for navigation history and triggers location change event. + * + * **Location Properties Updated:** + * - `_previousModel` / `_currentModel` - Model references + * - `_previousId` / `_currentId` - Content IDs + * - `_previousContentType` / `_contentType` - Content types (menu/page) + * - `_currentLocation` - Location string (e.g., "page-co-05", "course") + * - `_lastVisitedType` / `_lastVisitedMenu` / `_lastVisitedPage` - History tracking + * + * **Side Effects:** + * - Updates document title via `setDocumentTitle()` + * - Updates HTML/wrapper classes via `setGlobalClasses()` + * - Triggers `router:location` event + * - Waits for async operations via `wait.queue()` + * + * @param {string} currentLocation - Location identifier (e.g., "page-co-05") + * @param {string} type - Content type ("menu", "page", or null for plugin routes) + * @param {string} id - Content object ID (or null for plugin routes) + * @param {AdaptModel} currentModel - Current content model (or null for plugin routes) + * @async + * @fires router:location + * @private + */ async updateLocation(currentLocation, type, id, currentModel) { if (location._currentId === id && id === null) return; @@ -405,6 +675,22 @@ class Router extends Backbone.Router { await wait.queue(); } + /** + * Applies CSS classes to HTML and wrapper elements based on current location. + * Adds location-type and location-id classes for CSS targeting. + * Removes previous classes to prevent accumulation. + * + * **Applied Classes:** + * - `location-{type}` - Content type (location-menu, location-page) + * - `location-id-{id}` - Content ID (location-id-co-05) + * - `location-{currentLocation}` - For plugin routes + * - Model's `_htmlClasses` - Custom classes from content + * + * **Applied Attributes:** + * - `data-location` - Current location string + * + * @private + */ setGlobalClasses() { const currentModel = location._currentModel; @@ -427,6 +713,13 @@ class Router extends Backbone.Router { location._previousClasses = currentClasses; } + /** + * Sets accessibility focus to body element after navigation. + * Forces screen readers to start reading from the top of the new content. + * Only applies if `_shouldNavigateFocus` flag is true in router model. + * + * @private + */ handleNavigationFocus() { if (!this.model.get('_shouldNavigateFocus')) return; // Body will be forced to accept focus to start the @@ -434,11 +727,23 @@ class Router extends Backbone.Router { a11y.focus('body'); } + /** + * Navigates backward in browser history. + * Sets `_isBackward` flag to support URL correction during circular navigation protection. + */ navigateBack() { this._isBackward = true; Backbone.history.history.back(); } + /** + * Re-navigates to the current content object. + * Useful for refreshing content or correcting navigation state. + * + * @param {boolean} [force=false] - If true, bypasses `_canNavigate` check + * @example + * router.navigateToCurrentRoute(); + */ navigateToCurrentRoute(force) { if (!this.model.get('_canNavigate') && !force) { return; @@ -452,6 +757,20 @@ class Router extends Backbone.Router { this.navigate(route, { trigger: true, replace: true }); } + /** + * Navigates to the previous route in history. + * Intelligent navigation that handles different content types appropriately. + * + * **Navigation Logic:** + * - If no current model: Calls browser back + * - If current is menu: Navigates to parent + * - If previous model exists: Calls browser back + * - Otherwise: Navigates to parent + * + * @param {boolean} [force=false] - If true, bypasses `_canNavigate` check + * @example + * router.navigateToPreviousRoute(); + */ navigateToPreviousRoute(force) { // Sometimes a plugin might want to stop the default navigation. // Check whether default navigation has changed. @@ -472,6 +791,14 @@ class Router extends Backbone.Router { this.navigateToParent(); } + /** + * Navigates to the parent content object of the current location. + * If parent is root, navigates to home route. + * + * @param {boolean} [force=false] - If true, bypasses `_canNavigate` check + * @example + * router.navigateToParent(); + */ navigateToParent(force) { if (!this.model.get('_canNavigate') && !force) { return; @@ -483,6 +810,13 @@ class Router extends Backbone.Router { this.navigate(route, { trigger: true }); } + /** + * Navigates to the home route (root content object). + * + * @param {boolean} [force=false] - If true, bypasses `_canNavigate` check + * @example + * router.navigateToHomeRoute(); + */ navigateToHomeRoute(force) { if (!this.model.get('_canNavigate') && !force) { return; @@ -491,12 +825,63 @@ class Router extends Backbone.Router { } /** - * Allows a selector or id to be passed in and Adapt will navigate to this element. Resolves - * asynchronously when the element has been navigated to. - * @param {JQuery|string} selector CSS selector or id of the Adapt element you want to navigate to e.g. `".co-05"` or `"co-05"` - * @param {Object} [settings] The settings for the `$.scrollTo` function (See https://github.com/flesler/jquery.scrollTo#settings). - * @param {boolean} [settings.addSubContentRouteToHistory=false] Set to `true` if you want to add a sub content route to the browser's history. - * @param {boolean} [settings.replace=false] Set to `true` if you want to update the URL without creating an entry in the browser's history. + * Navigates to and scrolls to a specific element in the course. + * Most versatile navigation method supporting content objects, sub-content, and CSS selectors. + * Handles cross-content-object navigation, rendering, scrolling, and accessibility focus. + * + * **Navigation Modes:** + * - Content object not rendered: Navigates to content object and renders + * - Sub-content not rendered: Renders sub-content then scrolls + * - Element exists: Scrolls to element + * + * **Selector Resolution:** + * - Accepts model ID ("co-05") or CSS selector (".co-05") + * - Converts pure IDs to CSS class selectors automatically + * - Validates selector exists in DOM before scrolling + * + * **History Management:** + * - `addSubContentRouteToHistory`: Adds sub-content URL to browser history + * - `replace`: Updates URL without creating history entry + * + * **Scroll Behavior:** + * - Respects `_disableAnimation` config for instant scrolling + * - Calculates offset from wrapper padding and aria-label height + * - Waits for scroll animation before resolving + * - Sets accessibility focus after scroll completes + * + * **Events:** + * - Triggers `{type}:scrollTo` before scrolling + * - Triggers `{type}:scrolledTo` after scrolling completes + * + * @param {jQuery|string} selector - CSS selector or model ID to navigate to + * @param {Object} [settings={}] - Navigation and scroll configuration + * @param {boolean} [settings.addSubContentRouteToHistory=false] - Add sub-content route to browser history + * @param {boolean} [settings.replace=false] - Update URL without creating history entry + * @param {number} [settings.duration] - Scroll animation duration in milliseconds + * @param {Object} [settings.offset] - Scroll offset configuration + * @param {number} [settings.offset.top] - Top offset in pixels + * @param {number} [settings.offset.left] - Left offset in pixels + * @async + * @fires {type}:scrollTo + * @fires {type}:scrolledTo + * @example + * await router.navigateToElement('.c-05'); + * + * @example + * await router.navigateToElement('c-05', { + * duration: 600, + * replace: true + * }); + * + * @example + * await router.navigateToElement('.b-10', { + * addSubContentRouteToHistory: true + * }); + * + * @example + * await router.navigateToElement('.component', { + * offset: { top: -100, left: 0 } + * }); */ async navigateToElement(selector, settings = {}) { const currentModelId = typeof selector === 'string' && selector.replace(/\./g, '').split(' ')[0]; @@ -595,6 +980,14 @@ class Router extends Backbone.Router { }); } + /** + * Adds a route to browser history without triggering navigation. + * Used for sub-content navigation to update URL while staying on same content object. + * Prevents duplicate history entries if already at target route. + * + * @param {string} hash - URL hash to add to history (e.g., "#/id/co-05") + * @private + */ addRouteToHistory(hash) { const isCurrentRoute = (window.location.hash === hash); if (isCurrentRoute) return; @@ -606,11 +999,23 @@ class Router extends Backbone.Router { }); } + /** + * Gets a property from the router model. + * @param {...*} args - Arguments passed to router.model.get() + * @returns {*} Property value from router model + * @deprecated Please use router.model.get() instead + */ get(...args) { logging.deprecated('router.get, please use router.model.get'); return this.model.get(...args); } + /** + * Sets a property on the router model. + * @param {...*} args - Arguments passed to router.model.set() + * @returns {RouterModel} Router model for chaining + * @deprecated Please use router.model.set() instead + */ set(...args) { logging.deprecated('router.set, please use router.model.set'); return this.model.set(...args); From e77076d22cedc5d29ac71fbf8ad1d3b0447f010c Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Wed, 4 Feb 2026 09:04:08 -0600 Subject: [PATCH 6/7] Add docs to startController --- js/startController.js | 139 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 5 deletions(-) diff --git a/js/startController.js b/js/startController.js index 2d6c6696..9912bd0e 100644 --- a/js/startController.js +++ b/js/startController.js @@ -1,8 +1,70 @@ +/** + * @file Start Controller - Manages course start location and routing behavior + * @module core/js/startController + * @description Singleton service that controls where learners begin the course based on + * `_start` configuration in course.json. Supports conditional start pages, forced routing, + * and return-to-start functionality. + * + * **Architecture:** + * - Singleton controller extending Backbone.Controller + * - Loads `_start` configuration from Adapt.course model + * - Coordinates with router service for navigation + * - Coordinates with data service for model lookups + * - Manages session state to prevent duplicate start location logic + * + * **Configuration (_start in course.json):** + * ```json + * { + * "_isEnabled": true, + * "_force": false, + * "_id": "co-05", + * "_startIds": [ + * { + * "_id": "co-10", + * "_skipIfComplete": true, + * "_className": ".brand-a" + * } + * ] + * } + * ``` + * + * **Start Location Resolution:** + * 1. Check if `_isEnabled` is true (if false, use default routing) + * 2. Check `_startIds` array for conditional start pages + * 3. For each start ID, check `_className` against html element + * 4. Skip start ID if `_skipIfComplete` is true and content is complete + * 5. Use first matching start ID, or fall back to `_id` + * 6. If `_force` is true, ignore URL hash and use start location + * + * **Use Cases:** + * - Role-based start pages (different entry points for different learner types) + * - Continuation from last incomplete page + * - Device-specific start pages (mobile vs desktop) + * - Language-specific entry points + * + * **Public Events Triggered:** + * This service responds to events but doesn't trigger custom events. + * + * **Public Events Listened To:** + * - `adapt:start` - Framework starting, set initial location + * - `navigation:returnToStart` - User clicked return-to-start button + * + * @example + * import startController from 'core/js/startController'; + * + * Adapt.trigger('navigation:returnToStart'); + */ import Adapt from 'core/js/adapt'; import LockingModel from 'core/js/models/lockingModel'; import router from 'core/js/router'; import data from 'core/js/data'; +/** + * @class StartController + * @classdesc Controller managing course start location based on _start configuration. + * Singleton instance exported as `startController`. Do not instantiate directly. + * @extends {Backbone.Controller} + */ class StartController extends Backbone.Controller { initialize(...args) { @@ -23,32 +85,76 @@ class StartController extends Backbone.Controller { this.model = new LockingModel(Adapt.course.get('_start')); } + /** + * Sets the initial course navigation location based on _start configuration. + * On first call, updates browser history without triggering navigation. + * On subsequent calls (language change), navigates to start location. + * + * **Behavior:** + * - First call: Uses history.replaceState to update URL without navigation + * - Subsequent calls: Uses router.navigate to trigger full navigation + * - Respects _isEnabled flag (falls back to default routing if disabled) + * + * @private + * @example + * this.setStartLocation(); + * + * @example + * this.setStartLocation(); + */ setStartLocation() { if (!this._isSessionInProgress) { this._isSessionInProgress = true; if (!this.isEnabled()) return; return window.history.replaceState('', '', this.getStartHash()); } + // ensure we can return to the start page even if it is completed const hash = this.isEnabled() ? this.getStartHash(false) : '#/'; router.navigate(hash, { trigger: true, replace: true }); } /** - * Called via `Adapt.trigger('navigation:returnToStart')` or by including a button in the top navigation bar with the attribute `data-event="returnToStart"` + * Navigates back to the course start location. + * Resets `_skipIfComplete` flags to ensure completed start pages are accessible. + * Can be triggered via event or button click. + * + * **Triggering Methods:** + * - Event: `Adapt.trigger('navigation:returnToStart')` + * - Button: Add `data-event="returnToStart"` to navigation button + * - Direct: `startController.returnToStartLocation()` + * + * @example + * Adapt.trigger('navigation:returnToStart'); + * + * @example + * startController.returnToStartLocation(); */ returnToStartLocation() { const startIds = this.model.get('_startIds'); if (startIds) { - // ensure we can return to the start page even if it is completed startIds.forEach(startId => (startId._skipIfComplete = false)); } window.location.hash = this.getStartHash(true); } /** - * Returns a string in URL.hash format representing the route that the course should be sent to - * @param {boolean} [alwaysForce] Ignore any route specified in location.hash and force use of the start page instead - * @return {string} + * Calculates the URL hash for the course start location. + * Determines whether to use configured start location or existing URL hash. + * + * **Resolution Logic:** + * 1. Get start ID from getStartId() (handles conditional start pages) + * 2. Check if URL already has a hash (learner bookmarked a page) + * 3. If alwaysForce=true or _force=true, ignore existing hash + * 4. If start ID exists and should be used, return `#/id/{startId}` + * 5. Otherwise return existing hash or `#/` (default menu) + * + * @param {boolean} [alwaysForce=false] - Ignore existing URL hash and force start location + * @returns {string} URL hash string (e.g., '#/id/co-05' or '#/') + * @example + * const hash = startController.getStartHash(); + * + * @example + * const hash = startController.getStartHash(true); */ getStartHash(alwaysForce) { const startId = this.getStartId(); @@ -64,6 +170,29 @@ class StartController extends Backbone.Controller { return Boolean(this.model?.get('_isEnabled')); } + /** + * Resolves the actual start content ID based on conditional logic. + * Evaluates `_startIds` array to find first matching start location. + * + * **Resolution Process:** + * 1. Start with default `_id` from _start configuration + * 2. If `_startIds` array exists, evaluate each entry in order: + * - Check if content model exists (skip if not found) + * - Check if `_skipIfComplete` is true and content is complete (skip if so) + * - Check if `_className` matches html element (use if matches or no className) + * - Use first matching ID and stop evaluation + * 3. Return resolved start ID + * + * **Conditional Start Page Examples:** + * - Role-based: `_className: ".role-manager"` for managers + * - Device-based: `_className: ".size-small"` for mobile + * - Continuation: `_skipIfComplete: true` to skip completed intro + * + * @returns {string} Content model ID to use as start location + * @example + * const startId = startController.getStartId(); + * router.navigate(`#/id/${startId}`, { trigger: true }); + */ getStartId() { let startId = this.model.get('_id'); const startIds = this.model.get('_startIds'); From 66f7b1778b197a8a2680905b1e6a8fcc63de5c76 Mon Sep 17 00:00:00 2001 From: Joseph Replin Date: Thu, 5 Feb 2026 16:09:08 -0600 Subject: [PATCH 7/7] Enhance JSDoc typedefs and error docs --- js/adapt.js | 41 ++++++++++++++++++++++++++++++++++++----- js/components.js | 16 +++++++--------- js/data.js | 25 +++++++------------------ js/startController.js | 21 ++++++++++++++++----- 4 files changed, 66 insertions(+), 37 deletions(-) diff --git a/js/adapt.js b/js/adapt.js index 8ba9d145..5b5586fb 100644 --- a/js/adapt.js +++ b/js/adapt.js @@ -135,10 +135,6 @@ class AdaptSingleton extends LockingModel { * Decrements the outstanding completion check counter. * Call when exiting an asynchronous completion check. * When counter reaches zero, initialization can proceed. - * @example - * Adapt.checkingCompletion(); - * await someAsyncCompletionCheck(); - * Adapt.checkedCompletion(); */ checkedCompletion() { const outstandingChecks = this.get('_outstandingCompletionChecks'); @@ -191,6 +187,13 @@ class AdaptSingleton extends LockingModel { this.trigger('plugins:ready'); } + /** + * @typedef {Object} ParsedDirective + * @property {string} type - Content type (component, block, article, page, menu) + * @property {number} [offset] - Number of siblings to offset (+/-) + * @property {number} [inset] - Child index to select (0-indexed, -1 for last) + */ + /** * Parses relative navigation strings into structured directives. * Used by Trickle to determine scroll targets and by Branching to resolve navigation paths. @@ -206,7 +209,7 @@ class AdaptSingleton extends LockingModel { * - Omit number: Defaults to 0 (current/first) * * @param {string} relativeString - Navigation directive string - * @returns {Object|Array} Parsed directive(s) with `type`, `offset`, `inset` properties + * @returns {ParsedDirective|Array} Single directive or array for multi-step navigation * @example * Adapt.parseRelativeString('@component+1'); * @@ -331,71 +334,99 @@ class AdaptSingleton extends LockingModel { /** * @deprecated Please use core/js/a11y instead + * @see module:core/js/a11y + * @readonly */ get a11y() {} /** * @deprecated Please use core/js/components instead + * @see module:core/js/components + * @readonly */ get componentStore() {} /** * @deprecated Please use core/js/data instead + * @see module:core/js/data + * @readonly */ get data() {} /** * @deprecated Please use core/js/device instead + * @see module:core/js/device + * @readonly */ get device() {} /** * @deprecated Please use core/js/drawer instead + * @see module:core/js/drawer + * @readonly */ get drawer() {} /** * @deprecated Please use core/js/location instead + * @see module:core/js/location + * @readonly */ get location() {} /** * @deprecated Please use core/js/notify instead + * @see module:core/js/notify + * @readonly */ get notify() {} /** * @deprecated Please use core/js/offlineStorage instead + * @see module:core/js/offlineStorage + * @readonly */ get offlineStorage() {} /** * @deprecated Please use core/js/router instead + * @see module:core/js/router + * @readonly */ get router() {} /** * @deprecated Please use core/js/scrolling instead + * @see module:core/js/scrolling + * @readonly */ get scrolling() {} /** * @deprecated Please use core/js/startController instead + * @see module:core/js/startController + * @readonly */ get startController() {} /** * @deprecated Please use core/js/components instead + * @see module:core/js/components + * @readonly */ get store() {} /** * @deprecated Please use core/js/tracking instead + * @see module:core/js/tracking + * @readonly */ get tracking() {} /** * @deprecated Please use core/js/wait instead + * @see module:core/js/wait + * @readonly */ get wait() {} diff --git a/js/components.js b/js/components.js index b9c2dc1d..f3a80d82 100644 --- a/js/components.js +++ b/js/components.js @@ -57,6 +57,12 @@ import Backbone from 'backbone'; import logging from 'core/js/logging'; +/** + * @typedef {Object} ComponentRegistration + * @property {Function} [model] - Backbone.Model subclass or factory function + * @property {Function} [view] - Backbone.View subclass or factory function + */ + /** * @class Components * @classdesc Component registry controller managing Backbone Model and View class @@ -80,12 +86,6 @@ class Components extends Backbone.Controller { * Automatically assigns template name to view if not specified. Validates that classes extend * appropriate Backbone base classes. * - * **Registration Patterns:** - * - Single: `register('hotgraphic', { model, view })` - * - Multiple: `register(['article', 'page'], { view })` - * - Space-separated: `register('menu course', { model })` - * - View-only (deprecated): `register('name', { view })` or `register('name', ViewClass)` - * * **Validation:** * - Model must extend `Backbone.Model` or be a Function * - View must extend `Backbone.View` or be a Function @@ -96,9 +96,7 @@ class Components extends Backbone.Controller { * - Merges with existing registration (allows separate model/view registration) * * @param {string|Array} name - Component name(s) to register - * @param {Object} object - Registration object containing model and/or view classes - * @param {Function} [object.model] - Backbone.Model subclass or factory function - * @param {Function} [object.view] - Backbone.View subclass or factory function + * @param {ComponentRegistration} object - Registration object containing model and/or view classes * @returns {Object} The registered object (for chaining) * @throws {Error} If model is not Backbone.Model subclass or Function * @throws {Error} If view is not Backbone.View subclass or Function diff --git a/js/data.js b/js/data.js index d9032c8d..c13b2e0b 100644 --- a/js/data.js +++ b/js/data.js @@ -116,14 +116,9 @@ class Data extends AdaptCollection { * Loads build metadata, sets framework version, and loads config data. * Called by app.js during framework bootstrap. * - * **Initialization Steps:** - * 1. Reset collection (clear any existing data) - * 2. Reset _byAdaptID lookup index - * 3. Load build.min.js (framework version, course directory) - * 4. Set data-adapt-framework-version attribute on html element - * 5. Load config.json and set up language change listeners - * * @async + * @throws {Error} If build.min.js cannot be loaded + * @throws {Error} If config.json cannot be loaded * @example * import data from 'core/js/data'; * await data.init(); @@ -264,6 +259,8 @@ class Data extends AdaptCollection { * Wraps jQuery.getJSON in a Promise and adds __path__ property to loaded data. * @param {string} path - Path to JSON file * @returns {Promise} Loaded JSON data with __path__ property + * @throws {Error} If file not found (404) or network request fails + * @throws {SyntaxError} If JSON data is malformed * @private */ getJSON(path) { @@ -283,23 +280,15 @@ class Data extends AdaptCollection { * Reads manifest file for list of JSON files, loads them in parallel, * flattens data, creates models, and validates integrity. * - * **Process:** - * 1. Trigger 'loading' event - * 2. Reset collection and lookup index - * 3. Load language_data_manifest.js - * 4. Load all JSON files listed in manifest (parallel) - * 5. Flatten array/object data into single model array - * 6. Create course model first (other models need course config) - * 7. Create remaining models using component registry - * 8. Validate data integrity (checkData) - * 9. Trigger 'reset' and 'loaded' events - * * **Manifest Fallback:** * If manifest file not found, falls back to traditional file list: * course.json, contentObjects.json, articles.json, blocks.json, components.json * * @param {string} languagePath - Path to language folder (e.g., 'course/en/') * @async + * @throws {Error} If manifest file not found and fallback files not found + * @throws {Error} If JSON files fail to load + * @throws {SyntaxError} If JSON data is malformed * @fires loading * @fires reset * @fires loaded diff --git a/js/startController.js b/js/startController.js index 9912bd0e..4881a629 100644 --- a/js/startController.js +++ b/js/startController.js @@ -59,6 +59,22 @@ import LockingModel from 'core/js/models/lockingModel'; import router from 'core/js/router'; import data from 'core/js/data'; +/** + * @typedef {Object} StartIdConfig + * @property {string} _id - Content object ID to use as start location + * @property {boolean} [_skipIfComplete=false] - Skip this start location if content is complete + * @property {string} [_className] - CSS selector to match against html element + */ + +/** + * @typedef {Object} StartConfiguration + * @property {boolean} _isEnabled - Whether start location control is enabled + * @property {boolean} [_force=false] - Force start location ignoring URL hash + * @property {string} _id - Default start content object ID + * @property {Array} [_startIds] - Conditional start location configurations + * @property {boolean} [_isMenuDisabled=false] - Prevent navigation to root menu when _isEnabled + */ + /** * @class StartController * @classdesc Controller managing course start location based on _start configuration. @@ -96,11 +112,6 @@ class StartController extends Backbone.Controller { * - Respects _isEnabled flag (falls back to default routing if disabled) * * @private - * @example - * this.setStartLocation(); - * - * @example - * this.setStartLocation(); */ setStartLocation() { if (!this._isSessionInProgress) {