diff --git a/js/drawer.js b/js/drawer.js index 5e020c07..3dd54067 100644 --- a/js/drawer.js +++ b/js/drawer.js @@ -1,3 +1,49 @@ +/** + * @file Drawer Service - Sidebar navigation system with menu items and custom views + * @module core/js/drawer + * @description Singleton service managing the drawer sidebar navigation system. Provides API + * for registering menu items and displaying custom views. Handles drawer lifecycle, state management, + * and integration with navigation toolbar. + * + * **Architecture:** + * - Singleton controller (exported as instance) + * - Manages {@link DrawerCollection} of menu items (sorted by drawerOrder) + * - Creates/destroys {@link DrawerView} on language change + * - Two operational modes: menu list or custom view + * + * **Public Events Triggered:** + * - `drawer:opened` - Drawer opened (any mode) + * - `drawer:openedItemView` - Menu mode shown + * - `drawer:openedCustomView` - Custom view shown + * - `drawer:closed` - Drawer closed + * - `drawer:empty` - Drawer content cleared + * - `drawer:noItems` - No menu items registered + * + * **Usage Pattern:** + * ```javascript + * // Register menu item + * drawer.addItem({ + * title: 'Resources', + * description: 'View course resources', + * className: 'resources-drawer-item', + * drawerOrder: 10 + * }, 'resources:showDrawer'); + * + * // Listen for callback + * Adapt.on('resources:showDrawer', () => { + * drawer.openCustomView(new ResourcesView()); + * }); + * ``` + * + * **Known Issues & Improvements:** + * - **Issue:** DrawerCollection is module-scoped but not accessible via API + * - **Issue:** No way to update existing item without remove/add + * - **Issue:** No validation of drawerObject structure + * - **Enhancement:** Add `updateItem(eventCallback, drawerObject)` method + * - **Enhancement:** Add `getItem(eventCallback)` to retrieve item data + * - **Enhancement:** Add `getAllItems()` to get current menu items + */ + import Backbone from 'backbone'; import Adapt from 'core/js/adapt'; import DrawerView from 'core/js/views/drawerView'; @@ -5,6 +51,11 @@ import tooltips from './tooltips'; const DrawerCollection = new Backbone.Collection(null, { comparator: 'drawerOrder' }); +/** + * @class Drawer + * @classdesc Singleton service for drawer navigation system. Only one instance exists per course. + * @extends {Backbone.Controller} + */ class Drawer extends Backbone.Controller { initialize() { @@ -29,22 +80,85 @@ class Drawer extends Backbone.Controller { this.remove(); } + /** + * Toggles drawer open/closed based on current state. + * Convenience method for open/close logic. + * @example + * Adapt.trigger('navigation:toggleDrawer'); + * + * drawer.toggle(); + */ toggle() { (this.isOpen) ? this.close() : this.open(); } + /** + * Checks if drawer is currently open in menu mode. + * Returns false if drawer is showing custom view or closed. + * @returns {boolean} True if drawer is visible in menu mode + * @example + * if (drawer.isOpen) { + * console.log('Menu is showing'); + * } + */ get isOpen() { return this._drawerView?.isOpen ?? false; } + /** + * Opens drawer in menu mode showing list of registered items. + * If only one item registered, automatically triggers its callback. + * @fires drawer:opened + * @fires drawer:openedItemView + * @example + * drawer.open(); + */ open() { this._drawerView?.showDrawer(true); } + /** + * Opens drawer with custom view content. + * Called by plugins in response to menu item click. + * @param {Backbone.View|jQuery|HTMLElement|string} view - View instance or HTML content to display + * @param {boolean} [hasBackButton=true] - Show back button to return to menu + * @param {string} [position] - Override drawer position ('left'|'right', null uses global config) + * @fires drawer:opened + * @fires drawer:openedCustomView + * @fires drawer:empty + * @example + * Adapt.on('resources:showDrawer', () => { + * const resourcesView = new ResourcesView(); + * drawer.openCustomView(resourcesView, true, 'right'); + * }); + * + * drawer.openCustomView('
Simple HTML
', false); + */ openCustomView(view, hasBackButton, position) { this._drawerView?.openCustomView(view, hasBackButton, position); } + /** + * Registers a menu item in the drawer. + * Replaces existing item with same eventCallback. + * @param {Object} drawerObject - Menu item configuration + * @param {string} drawerObject.title - Display title + * @param {string} drawerObject.description - Description text + * @param {string} [drawerObject.className] - CSS class for styling + * @param {number} [drawerObject.drawerOrder=0] - Sort order (lower numbers first) + * @param {string} eventCallback - Event name to trigger when clicked + * @example + * drawer.addItem({ + * title: 'Resources', + * description: 'View downloadable resources', + * className: 'resources-item', + * drawerOrder: 20 + * }, 'resources:showDrawer'); + * + * Adapt.on('resources:showDrawer', () => { + * drawer.openCustomView(new ResourcesView()); + * }); + */ addItem(drawerObject, eventCallback) { if (this.hasItem(eventCallback)) { DrawerCollection.remove(DrawerCollection.find(item => item.eventCallback === eventCallback)); @@ -53,14 +167,40 @@ class Drawer extends Backbone.Controller { DrawerCollection.add(drawerObject); } + /** + * Checks if menu item is registered. + * @param {string} eventCallback - Event callback to check + * @returns {boolean} True if item exists + * @example + * if (!drawer.hasItem('resources:showDrawer')) { + * drawer.addItem({ title: 'Resources' }, 'resources:showDrawer'); + * } + */ hasItem(eventCallback) { return Boolean(DrawerCollection.find(item => item.eventCallback === eventCallback)); } + /** + * Closes the drawer immediately. + * Called automatically on navigation. + * @param {jQuery} [$toElement=null] - Element to focus after closing + * @fires drawer:closed + * @example + * drawer.close(); + * + * drawer.close($('.js-nav-home-btn')); + */ close($toElement = null) { this._drawerView?.hideDrawer($toElement, { force: true }); } + /** + * Destroys the drawer view and cleans up. + * Called automatically on language change. + * @fires drawer:empty + * @example + * drawer.remove(); + */ remove() { this._drawerView?.remove(); this._drawerView = null; diff --git a/js/views/drawerItemView.js b/js/views/drawerItemView.js index c9a06106..1cef2310 100644 --- a/js/views/drawerItemView.js +++ b/js/views/drawerItemView.js @@ -1,5 +1,38 @@ +/** + * @file Drawer Item View - Individual menu item in drawer list + * @module core/js/views/drawerItemView + * @description Simple view rendering individual menu items in drawer menu mode. + * Created by {@link DrawerView} for each model in collection. Handles click events + * and triggers registered callback. + * + * **Lifecycle:** + * - Created by DrawerView.renderItems() + * - Appends itself to `.drawer__holder` + * - Listens for `drawer:empty` → self-removes + * - No explicit remove() call needed + * + * **Model Structure Expected:** + * ```javascript + * { + * eventCallback: 'extension:openSettings', + * title: 'Settings', + * description: 'Configure course settings', + * className: 'settings-item', + * drawerOrder: 10 + * } + * ``` + * + * **Important:** Created internally by {@link DrawerView}. Use {@link module:core/js/drawer#addItem} + * to register items, not direct instantiation. + */ + import Adapt from 'core/js/adapt'; +/** + * @class DrawerItemView + * @classdesc Renders single drawer menu item button with title and description. + * @extends {Backbone.View} + */ class DrawerItemView extends Backbone.View { className() { @@ -30,6 +63,16 @@ class DrawerItemView extends Backbone.View { return this; } + /** + * Handles menu item click. + * Triggers the callback event registered with this item. + * Drawer service listens to callback and opens custom view. + * @param {jQuery.Event} event - Click event + * @example + * Adapt.on('resources:showDrawer', () => { + * drawer.openCustomView(new ResourcesView()); + * }); + */ onDrawerItemClicked(event) { event.preventDefault(); const eventCallback = this.model.get('eventCallback'); diff --git a/js/views/drawerView.js b/js/views/drawerView.js index f6fcce4f..2db18dc9 100644 --- a/js/views/drawerView.js +++ b/js/views/drawerView.js @@ -1,3 +1,56 @@ +/** + * @file Drawer View - Main controller for sidebar drawer display and content + * @module core/js/views/drawerView + * @description View managing the drawer sidebar with two operational modes: menu list showing + * registered items via {@link DrawerItemView} or custom view content. Handles positioning, + * animations, accessibility, and lifecycle management. + * + * **Operational Modes:** + * 1. **Menu Mode** - Shows list of {@link DrawerItemView} instances from collection + * 2. **Custom Mode** - Shows arbitrary view/HTML with optional back button + * + * **Display Behavior:** + * - Uses `` element for semantic HTML + * - Position: left/right/auto (with RTL support) + * - Animated entrance/exit via CSS transitions + * - Backdrop shadow coordination + * - Keyboard support (Escape to close) + * - Click outside to close + * - Scroll locking while open + * - Auto-focus management + * + * **Configuration** (via `Adapt.config._drawer`): + * - `_position` - 'left', 'right', 'auto' (default: 'auto') + * - `_duration` - Animation duration in ms (default: 400) + * - `_showEasing` - Show easing function (default: 'easeOutQuart') + * - `_hideEasing` - Hide easing function (default: 'easeInQuart') + * + * **Position Logic:** + * - If custom position provided AND global is 'auto' AND RTL: flip left↔right + * - Else if global is not 'auto': use global position + * - Else: use provided position + * + * **Public Events Triggered:** + * - `drawer:opened` - Drawer opened (any mode) + * - `drawer:openedItemView` - Menu mode shown + * - `drawer:openedCustomView` - Custom view shown + * - `drawer:empty` - Drawer content cleared + * - `drawer:closed` - Drawer closed + * - `drawer:noItems` - No menu items in collection + * + * **Known Issues & Improvements:** + * - **Issue:** Single-item workaround (lines 180-188) - Sets `_isCustomViewVisible` to true then immediately false to fix toggle bug, indicates state management problem + * - **Issue:** Force close hack (line 222) - Uses `css('display', 'block')` to prevent HTMLDialogElement.close() from hiding dialog + * - **Issue:** Hardcoded selector `.js-nav-drawer-btn` couples view to navigation implementation + * - **Issue:** Direct DOM manipulation of `$('.js-nav-drawer-btn')` violates separation of concerns + * - **Enhancement:** Refactor state management to eliminate single-item toggle workaround + * - **Enhancement:** Use event system instead of direct DOM manipulation for nav button visibility + * - **Enhancement:** Support configurable drawer button selector + * - **Enhancement:** Add `drawer:beforeOpen` and `drawer:beforeClose` events for cancellation + * + * **Important:** Created internally by {@link module:core/js/drawer}. Use drawer service API, not direct instantiation. + */ + import Adapt from 'core/js/adapt'; import shadow from '../shadow'; import a11y from 'core/js/a11y'; @@ -9,6 +62,12 @@ import { } from '../transitions'; import logging from '../logging'; +/** + * @class DrawerView + * @classdesc Main drawer controller managing display, positioning, and content rendering. + * Lifecycle: Created → Positioned → Animated in → User interaction → Animated out → Removed + * @extends {Backbone.View} + */ class DrawerView extends Backbone.View { tagName() { @@ -109,6 +168,15 @@ class DrawerView extends Backbone.View { this.checkIfDrawerIsAvailable(); } + /** + * Sets drawer position (left/right) with RTL and global config support. + * Complex resolution: custom position + auto mode + RTL = flipped position. + * @param {string} [position] - Desired position ('left'|'right', null uses global config) + * @private + * @example + * this.setDrawerPosition('right'); + * this.setDrawerPosition(null); + */ setDrawerPosition(position) { if (this._useMenuPosition) position = null; const isGlobalPositionAuto = this._globalDrawerPosition === 'auto'; @@ -121,6 +189,19 @@ class DrawerView extends Backbone.View { this.drawerPosition = position; } + /** + * Opens drawer with custom view content. + * Called by {@link module:core/js/drawer#openCustomView}. + * @param {Backbone.View|jQuery|HTMLElement|string} view - View instance or HTML content + * @param {boolean} [hasBackButton=true] - Show back button to return to menu + * @param {string} [position] - Override drawer position ('left'|'right') + * @fires drawer:empty + * @fires drawer:opened + * @fires drawer:openedCustomView + * @example + * this.openCustomView(new ResourcesView(), true, 'right'); + * this.openCustomView($('
Content
'), false); + */ openCustomView(view, hasBackButton = true, position) { this.$('.js-drawer-holder').removeAttr('role'); this._hasBackButton = hasBackButton; @@ -131,6 +212,12 @@ class DrawerView extends Backbone.View { this.$('.drawer__holder').html(view instanceof Backbone.View ? view.$el : view); } + /** + * Checks if drawer has menu items and updates nav button visibility. + * Directly manipulates nav button DOM (couples to navigation implementation). + * @fires drawer:noItems + * @private + */ checkIfDrawerIsAvailable() { const isEmptyDrawer = (this.collection.length === 0); $('.js-nav-drawer-btn').toggleClass('u-display-none', isEmptyDrawer); @@ -149,10 +236,29 @@ class DrawerView extends Backbone.View { this.hideDrawer(); } + /** + * Checks if drawer is open in menu mode. + * Returns false if showing custom view or closed. + * @returns {boolean} True if visible in menu mode (not custom view) + */ get isOpen() { return (this._isVisible && this._isCustomViewVisible === false); } + /** + * Opens and displays the drawer with animation. + * Handles both menu mode and custom view mode. + * @async + * @param {boolean} [emptyDrawer] - True for menu mode, false/null for custom mode + * @param {string} [position=null] - Override drawer position + * @fires drawer:opened + * @fires drawer:openedItemView + * @fires drawer:openedCustomView + * @fires drawer:empty + * @example + * await this.showDrawer(true); + * await this.showDrawer(null, 'right'); + */ async showDrawer(emptyDrawer, position = null) { shadow.show(); this.setDrawerPosition(position); @@ -201,10 +307,21 @@ class DrawerView extends Backbone.View { } + /** + * Clears drawer content container. + * @private + */ emptyDrawer() { this.$('.drawer__holder').empty(); } + /** + * Renders menu items from collection. + * Creates {@link DrawerItemView} for each model. + * Sets ARIA role='list' if multiple items. + * @fires drawer:empty + * @private + */ renderItems() { Adapt.trigger('drawer:empty'); this.emptyDrawer(); @@ -214,6 +331,18 @@ class DrawerView extends Backbone.View { this.collection.forEach(model => new DrawerItemView({ model })); } + /** + * Closes and hides the drawer with animation. + * Can force immediate close (skip animation). + * @async + * @param {jQuery} [$toElement] - Element to focus after closing + * @param {Object} [options] - Close options + * @param {boolean} [options.force=false] - Skip animation, close immediately + * @fires drawer:closed + * @example + * await this.hideDrawer(); + * await this.hideDrawer($('.js-nav-home-btn'), { force: true }); + */ async hideDrawer($toElement, { force = false // close the drawer immediately } = {}) { @@ -248,6 +377,12 @@ class DrawerView extends Backbone.View { this.setDrawerPosition(this._globalDrawerPosition); } + /** + * Cleanup when view is removed. + * Forces drawer closed, removes event listeners, resets collection. + * @fires drawer:empty + * @param {...*} args - Arguments passed to Backbone's remove method + */ remove() { this.hideDrawer(null, { force: true }); super.remove();