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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions js/drawer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,61 @@
/**
* @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.
Comment on lines +2 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description and terminology aren't quite right throughout.

Replace 'menu items' with 'drawer items' and the word 'menu' for 'sidebar/drawer' as the word menu is used for menu plugins.
Remove 'navigation system' as the word navigation is used for a core module called 'navigation' which is the horizontal navigation bar, not the vertical sidebar.

*
* **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';
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() {
Expand All @@ -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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

   * Adapt.trigger('navigation:toggleDrawer');

Consider as deprecated.

As a general principle for Adapt, any event triggered on a subject as if it were a function call that is not a triggered by the subject as notification of a state change should be replaced with a function API. Where a function alternative to an event trigger already exists, consider the event trigger to be deprecated.

*
* 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('<div>Simple HTML</div>', 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));
Expand All @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions js/views/drawerItemView.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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');
Expand Down
Loading
Loading