| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- /** @odoo-module **/
- import publicWidget from "@web/legacy/js/public/public_widget";
- /**
- * M22 Sidebar Widget
- * Handles the collapsible sidebar functionality with smooth transitions
- * and proper state management.
- */
- publicWidget.registry.M22Sidebar = publicWidget.Widget.extend({
- selector: '#m22_sidebar',
- events: {
- 'click .sidebar-collapse-btn': '_onToggleSidebar',
- 'click .nav-link': '_onLinkClick',
- },
- // Transition duration in ms (should match CSS)
- TRANSITION_DURATION: 300,
- /**
- * @override
- */
- start: function () {
- this.backdrop = document.getElementById('m22_sidebar_backdrop');
- this.collapseBtn = this.el.querySelector('.sidebar-collapse-btn');
- this.collapseBtnIcon = this.collapseBtn?.querySelector('i');
-
- // Cache frequently accessed elements
- this.sidebarTexts = this.el.querySelectorAll('.sidebar-text, .sidebar-title, .sidebar-logo-text');
- this.navLinks = this.el.querySelectorAll('.nav-link');
-
- // Setup backdrop listener
- if (this.backdrop) {
- this._boundCloseMobile = this._onCloseMobileSidebar.bind(this);
- this.backdrop.addEventListener('click', this._boundCloseMobile);
- }
- // Restore state from localStorage (without animation on page load)
- this._restoreState(true);
- // Handle window resize
- this._boundResize = this._onWindowResize.bind(this);
- window.addEventListener('resize', this._boundResize);
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- destroy: function () {
- if (this.backdrop && this._boundCloseMobile) {
- this.backdrop.removeEventListener('click', this._boundCloseMobile);
- }
- if (this._boundResize) {
- window.removeEventListener('resize', this._boundResize);
- }
- this._super.apply(this, arguments);
- },
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- /**
- * Restores the sidebar state from localStorage
- * @param {Boolean} skipAnimation - Whether to skip the collapse animation
- * @private
- */
- _restoreState: function (skipAnimation = false) {
- const savedState = localStorage.getItem('m22_sidebar_collapsed');
- const isCollapsed = savedState === 'true';
- const htmlEl = document.documentElement;
- const hasEarlyInit = htmlEl.classList.contains('m22-sidebar-collapsed-init');
-
- // If early init already applied collapsed state (sidebar is hidden)
- if (hasEarlyInit && isCollapsed) {
- // Sync JS state while sidebar is still hidden
- this.el.setAttribute('data-collapsed', 'true');
- document.body.classList.add('sidebar-collapsed');
-
- // Update icon BEFORE showing sidebar
- this._updateCollapseIcon(true);
-
- // Hide texts
- this._hideTexts();
-
- // Now show sidebar by adding initialized class
- // Use double RAF to ensure styles are applied before visibility
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- htmlEl.classList.add('m22-sidebar-initialized');
- });
- });
- return;
- }
-
- // Remove early init class if state is expanded (show sidebar immediately)
- if (hasEarlyInit && !isCollapsed) {
- // First set expanded state
- this.el.setAttribute('data-collapsed', 'false');
- document.body.classList.remove('sidebar-collapsed');
- this._updateCollapseIcon(false);
- this._showTexts();
-
- // Then show sidebar
- htmlEl.classList.remove('m22-sidebar-collapsed-init');
- }
-
- if (skipAnimation && !hasEarlyInit) {
- // Temporarily disable transitions
- this.el.style.transition = 'none';
- document.body.style.transition = 'none';
- }
- if (!hasEarlyInit) {
- this._setCollapsedState(isCollapsed, skipAnimation);
- }
- if (skipAnimation && !hasEarlyInit) {
- // Re-enable transitions after a frame
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- this.el.style.transition = '';
- document.body.style.transition = '';
- });
- });
- }
- },
- /**
- * Sets the collapsed state of the sidebar
- * @param {Boolean} isCollapsed - Whether the sidebar should be collapsed
- * @param {Boolean} skipAnimation - Whether to skip animation
- * @private
- */
- _setCollapsedState: function (isCollapsed, skipAnimation = false) {
- const htmlEl = document.documentElement;
- this.el.setAttribute('data-collapsed', isCollapsed);
- if (isCollapsed) {
- document.body.classList.add('sidebar-collapsed');
- // Ensure init class is present for collapsed state
- htmlEl.classList.add('m22-sidebar-collapsed-init');
- this._updateCollapseIcon(true);
-
- // Hide text immediately when collapsing
- this._hideTexts();
- } else {
- document.body.classList.remove('sidebar-collapsed');
- // Remove init class when expanding to allow normal CSS behavior
- htmlEl.classList.remove('m22-sidebar-collapsed-init');
- this._updateCollapseIcon(false);
-
- // Show text with delay to match sidebar expansion
- if (!skipAnimation) {
- setTimeout(() => {
- this._showTexts();
- }, this.TRANSITION_DURATION / 2);
- } else {
- this._showTexts();
- }
- }
- },
- /**
- * Updates the collapse button icon based on state
- * @param {Boolean} isCollapsed
- * @private
- */
- _updateCollapseIcon: function (isCollapsed) {
- if (this.collapseBtnIcon) {
- this.collapseBtnIcon.classList.remove('fa-chevron-left', 'fa-chevron-right', 'fa-bars');
- this.collapseBtnIcon.classList.add(isCollapsed ? 'fa-chevron-right' : 'fa-chevron-left');
- }
- },
- /**
- * Hides sidebar text elements
- * @private
- */
- _hideTexts: function () {
- this.sidebarTexts.forEach(el => {
- el.style.opacity = '0';
- el.style.visibility = 'hidden';
- });
- },
- /**
- * Shows sidebar text elements
- * @private
- */
- _showTexts: function () {
- this.sidebarTexts.forEach(el => {
- el.style.opacity = '1';
- el.style.visibility = 'visible';
- });
- },
- /**
- * Checks if we're in mobile view
- * @returns {Boolean}
- * @private
- */
- _isMobile: function () {
- return window.innerWidth < 992;
- },
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
- /**
- * Toggles the sidebar collapsed state
- * @param {Event} ev
- * @private
- */
- _onToggleSidebar: function (ev) {
- ev.preventDefault();
- ev.stopPropagation();
- if (this._isMobile()) {
- // On mobile, toggle the overlay sidebar
- const isOpen = this.el.classList.contains('m22-sidebar-open');
- if (isOpen) {
- this._onCloseMobileSidebar();
- } else {
- this._onOpenMobileSidebar();
- }
- } else {
- // On desktop, toggle collapse state
- const isCollapsed = this.el.getAttribute('data-collapsed') === 'true';
- const newState = !isCollapsed;
- this._setCollapsedState(newState);
- localStorage.setItem('m22_sidebar_collapsed', newState);
- }
- },
- /**
- * Closes sidebar on mobile when a link is clicked
- * @private
- */
- _onLinkClick: function () {
- if (this._isMobile() && this.el.classList.contains('m22-sidebar-open')) {
- this._onCloseMobileSidebar();
- }
- },
- /**
- * Opens the mobile sidebar
- * @private
- */
- _onOpenMobileSidebar: function () {
- this.el.classList.add('m22-sidebar-open');
- if (this.backdrop) {
- this.backdrop.classList.add('visible');
- }
- document.body.style.overflow = 'hidden';
- },
- /**
- * Closes the mobile sidebar
- * @private
- */
- _onCloseMobileSidebar: function () {
- this.el.classList.remove('m22-sidebar-open');
- if (this.backdrop) {
- this.backdrop.classList.remove('visible');
- }
- document.body.style.overflow = '';
- },
- /**
- * Handle window resize
- * @private
- */
- _onWindowResize: function () {
- // Close mobile sidebar if resizing to desktop
- if (!this._isMobile() && this.el.classList.contains('m22-sidebar-open')) {
- this._onCloseMobileSidebar();
- }
- }
- });
- /**
- * External trigger widget for opening the sidebar on mobile
- * Use data-action="open-m22-sidebar" on any element
- */
- publicWidget.registry.M22SidebarTrigger = publicWidget.Widget.extend({
- selector: '[data-action="open-m22-sidebar"]',
- events: {
- 'click': '_onTriggerClick',
- },
- _onTriggerClick: function (ev) {
- ev.preventDefault();
- const sidebar = document.getElementById('m22_sidebar');
- const backdrop = document.getElementById('m22_sidebar_backdrop');
-
- if (sidebar) {
- sidebar.classList.add('m22-sidebar-open');
- if (backdrop) {
- backdrop.classList.add('visible');
- }
- document.body.style.overflow = 'hidden';
- }
- }
- });
|