Source: side_navigation.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. import EventEmitter from './events/EventEmitter';
  15. /**
  16. * Options
  17. *
  18. * @property {String|Number} breakpoint The window width that defines the desktop size.
  19. * @property {String} content The class or ID of the content container.
  20. * @property {String} container The class or ID of the sidenav container.
  21. * @property {String|Number} gutter The space between the sidenav-slider and the sidenav-content.
  22. * @property {String} navigation The class or ID of the navigation container.
  23. * @property {String} position The position of the sidenav-slider. Possible values: left, right
  24. * @property {String} type The type of sidenav in desktop. Possible values: relative, fixed, fixed-push
  25. * @property {String} typeMobile The type of sidenav in mobile. Possible values: relative, fixed, fixed-push
  26. * @property {String|Object} url The URL to fetch the content to inject into .sidebar-body
  27. * @property {String|Number} width The width of the side navigation.
  28. */
  29. const DEFAULTS = {
  30. breakpoint: 768,
  31. content: '.sidenav-content',
  32. gutter: '0px',
  33. loadingIndicatorTPL:
  34. '<div class="loading-animation loading-animation-md"></div>',
  35. navigation: '.sidenav-menu-slider',
  36. position: 'left',
  37. type: 'relative',
  38. typeMobile: 'relative',
  39. url: null,
  40. width: '225px',
  41. };
  42. /**
  43. * Map from toggler DOM nodes to sidenav instances.
  44. */
  45. const INSTANCE_MAP = new WeakMap();
  46. /**
  47. * Utility function that strips off a possible jQuery and Metal
  48. * component wrappers from a DOM element.
  49. */
  50. function getElement(element) {
  51. // Remove jQuery wrapper, if any.
  52. if (element && element.jquery) {
  53. if (element.length > 1) {
  54. throw new Error(
  55. `getElement(): Expected at most one element, got ${element.length}`
  56. );
  57. }
  58. element = element.get(0);
  59. }
  60. // Remove Metal wrapper, if any.
  61. if (element && !(element instanceof HTMLElement)) {
  62. element = element.element;
  63. }
  64. return element;
  65. }
  66. function getInstance(element) {
  67. element = getElement(element);
  68. const instance = INSTANCE_MAP.get(element);
  69. return instance;
  70. }
  71. /**
  72. * A list of attributes that can contribute to the "identity" of an element, for
  73. * the purposes of delegated event handling.
  74. */
  75. const IDENTITY_ATTRIBUTES = [/^aria-/, /^data-/, /^type$/];
  76. /**
  77. * Returns a unique selector for the supplied element. Ideally we'd just use
  78. * the "id" attribute (assigning one if necessary), but the use of Metal
  79. * components means that any "id" we assign will be blown away on the next state
  80. * change.
  81. */
  82. function getUniqueSelector(element) {
  83. element = getElement(element);
  84. if (element.id) {
  85. return `#${element.id}`;
  86. }
  87. let ancestorWithId = element.parentNode;
  88. while (ancestorWithId) {
  89. if (ancestorWithId.id) {
  90. break;
  91. }
  92. ancestorWithId = ancestorWithId.parentNode;
  93. }
  94. const attributes = Array.from(element.attributes)
  95. .map(({name, value}) => {
  96. const isIdentifying = IDENTITY_ATTRIBUTES.some((regExp) => {
  97. return regExp.test(name);
  98. });
  99. return isIdentifying ? `[${name}=${JSON.stringify(value)}]` : null;
  100. })
  101. .filter(Boolean)
  102. .sort();
  103. return [
  104. ancestorWithId ? `#${ancestorWithId.id} ` : '',
  105. element.tagName.toLowerCase(),
  106. ...attributes,
  107. ].join('');
  108. }
  109. function addClass(element, className) {
  110. setClasses(element, {
  111. [className]: true,
  112. });
  113. }
  114. function removeClass(element, className) {
  115. setClasses(element, {
  116. [className]: false,
  117. });
  118. }
  119. function setClasses(element, classes) {
  120. element = getElement(element);
  121. if (element) {
  122. // One at a time because IE 11: https://caniuse.com/#feat=classlist
  123. Object.entries(classes).forEach(([className, present]) => {
  124. // Some callers use multiple space-separated classNames for
  125. // `openClass`/`data-open-class`. (Looking at you,
  126. // product-navigation-simulation-web...)
  127. className.split(/\s+/).forEach((name) => {
  128. if (present) {
  129. element.classList.add(name);
  130. }
  131. else {
  132. element.classList.remove(name);
  133. }
  134. });
  135. });
  136. }
  137. }
  138. function hasClass(element, className) {
  139. element = getElement(element);
  140. // Again, product-navigation-simulation-web passes multiple classNames.
  141. return className.split(/\s+/).every((name) => {
  142. return element.classList.contains(name);
  143. });
  144. }
  145. function setStyles(element, styles) {
  146. element = getElement(element);
  147. if (element) {
  148. Object.entries(styles).forEach(([property, value]) => {
  149. element.style[property] = value;
  150. });
  151. }
  152. }
  153. /**
  154. * For compatibility with jQuery, which will treat "100" as "100px".
  155. */
  156. function px(dimension) {
  157. if (typeof dimension === 'number') {
  158. return dimension + 'px';
  159. }
  160. else if (
  161. typeof dimension === 'string' &&
  162. dimension.match(/^\s*\d+\s*$/)
  163. ) {
  164. return dimension.trim() + 'px';
  165. }
  166. else {
  167. return dimension;
  168. }
  169. }
  170. /**
  171. * Replacement for jQuery's `offset().left`.
  172. *
  173. * @see: https://github.com/jquery/jquery/blob/438b1a3e8a52/src/offset.js#L94-L100
  174. */
  175. function offsetLeft(element) {
  176. const elementLeft = element.getBoundingClientRect().left;
  177. const documentOffset = element.ownerDocument.defaultView.pageOffsetX || 0;
  178. return elementLeft + documentOffset;
  179. }
  180. /**
  181. * Keys are event names (eg. "click").
  182. * Values are objects mapping selectors to EventEmitters.
  183. */
  184. const eventNamesToSelectors = {};
  185. function handleEvent(eventName, event) {
  186. Object.keys(eventNamesToSelectors[eventName]).forEach((selector) => {
  187. let matches = false;
  188. let target = event.target;
  189. while (target) {
  190. // In IE11 SVG elements have no `parentElement`, only a
  191. // `parentNode`, so we have to search up the DOM using
  192. // the latter. This in turn requires us to check for the
  193. // existence of `target.matches` before using it.
  194. //
  195. // See: https://stackoverflow.com/a/36270354/2103996
  196. matches = target.matches && target.matches(selector);
  197. if (matches) {
  198. break;
  199. }
  200. target = target.parentNode;
  201. }
  202. if (matches) {
  203. const emitter = eventNamesToSelectors[eventName][selector];
  204. emitter.emit('click', event);
  205. }
  206. });
  207. }
  208. /**
  209. * Creates a delegated event listener for `eventName` events on
  210. * `elementOrSelector`.
  211. */
  212. function subscribe(elementOrSelector, eventName, handler) {
  213. if (elementOrSelector) {
  214. // Add only one listener per `eventName`.
  215. if (!eventNamesToSelectors[eventName]) {
  216. eventNamesToSelectors[eventName] = {};
  217. document.body.addEventListener(eventName, (event) =>
  218. handleEvent(eventName, event)
  219. );
  220. }
  221. const emitters = eventNamesToSelectors[eventName];
  222. const selector =
  223. typeof elementOrSelector === 'string'
  224. ? elementOrSelector
  225. : getUniqueSelector(elementOrSelector);
  226. if (!emitters[selector]) {
  227. emitters[selector] = new EventEmitter();
  228. }
  229. const emitter = emitters[selector];
  230. const subscription = emitter.on(eventName, (event) => {
  231. if (!event.defaultPrevented) {
  232. handler(event);
  233. }
  234. });
  235. return {
  236. dispose() {
  237. subscription.dispose();
  238. },
  239. };
  240. }
  241. return null;
  242. }
  243. function toInt(str) {
  244. return parseInt(str, 10) || 0;
  245. }
  246. function SideNavigation(toggler, options) {
  247. toggler = getElement(toggler);
  248. this.init(toggler, options);
  249. }
  250. SideNavigation.TRANSITION_DURATION = 500;
  251. SideNavigation.prototype = {
  252. _bindUI() {
  253. const instance = this;
  254. instance._subscribeClickTrigger();
  255. instance._subscribeClickSidenavClose();
  256. },
  257. _emit(event) {
  258. this._emitter.emit(event, this);
  259. },
  260. _getSidenavWidth() {
  261. const instance = this;
  262. const options = instance.options;
  263. const widthOriginal = options.widthOriginal;
  264. let width = widthOriginal;
  265. const winWidth = window.innerWidth;
  266. if (winWidth < widthOriginal + 40) {
  267. width = winWidth - 40;
  268. }
  269. return width;
  270. },
  271. _getSimpleSidenavType() {
  272. const instance = this;
  273. const options = instance.options;
  274. const desktop = instance._isDesktop();
  275. const type = options.type;
  276. const typeMobile = options.typeMobile;
  277. if (desktop && type === 'fixed-push') {
  278. return 'desktop-fixed-push';
  279. }
  280. else if (!desktop && typeMobile === 'fixed-push') {
  281. return 'mobile-fixed-push';
  282. }
  283. return 'fixed';
  284. },
  285. _isDesktop() {
  286. return window.innerWidth >= this.options.breakpoint;
  287. },
  288. _isSidenavRight() {
  289. const instance = this;
  290. const options = instance.options;
  291. const container = document.querySelector(options.container);
  292. const isSidenavRight = hasClass(container, 'sidenav-right');
  293. return isSidenavRight;
  294. },
  295. _isSimpleSidenavClosed() {
  296. const instance = this;
  297. const options = instance.options;
  298. const openClass = options.openClass;
  299. const container = document.querySelector(options.container);
  300. return !hasClass(container, openClass);
  301. },
  302. _loadUrl(element, url) {
  303. const instance = this;
  304. const sidebar = element.querySelector('.sidebar-body');
  305. if (!instance._fetchPromise && sidebar) {
  306. while (sidebar.firstChild) {
  307. sidebar.removeChild(sidebar.firstChild);
  308. }
  309. const loading = document.createElement('div');
  310. addClass(loading, 'sidenav-loading');
  311. loading.innerHTML = instance.options.loadingIndicatorTPL;
  312. sidebar.appendChild(loading);
  313. instance._fetchPromise = Liferay.Util.fetch(url);
  314. instance._fetchPromise
  315. .then((response) => {
  316. if (!response.ok) {
  317. throw new Error(`Failed to fetch ${url}`);
  318. }
  319. return response.text();
  320. })
  321. .then((text) => {
  322. const range = document.createRange();
  323. range.selectNode(sidebar);
  324. // Unlike `.innerHTML`, this will eval scripts.
  325. const fragment = range.createContextualFragment(text);
  326. sidebar.removeChild(loading);
  327. sidebar.appendChild(fragment);
  328. instance.setHeight();
  329. })
  330. .catch((err) => {
  331. console.error(err);
  332. });
  333. }
  334. },
  335. _renderNav() {
  336. const instance = this;
  337. const options = instance.options;
  338. const container = document.querySelector(options.container);
  339. const navigation = container.querySelector(options.navigation);
  340. const menu = navigation.querySelector('.sidenav-menu');
  341. const closed = hasClass(container, 'closed');
  342. const sidenavRight = instance._isSidenavRight();
  343. const width = instance._getSidenavWidth();
  344. if (closed) {
  345. setStyles(menu, {
  346. width: px(width),
  347. });
  348. if (sidenavRight) {
  349. const positionDirection = options.rtl ? 'left' : 'right';
  350. setStyles(menu, {
  351. [positionDirection]: px(width),
  352. });
  353. }
  354. }
  355. else {
  356. instance.showSidenav();
  357. instance.setHeight();
  358. }
  359. },
  360. _renderUI() {
  361. const instance = this;
  362. const options = instance.options;
  363. const container = document.querySelector(options.container);
  364. const toggler = instance.toggler;
  365. const mobile = instance.mobile;
  366. const type = mobile ? options.typeMobile : options.type;
  367. if (!instance.useDataAttribute) {
  368. if (mobile) {
  369. setClasses(container, {
  370. closed: true,
  371. open: false,
  372. });
  373. setClasses(toggler, {
  374. active: false,
  375. open: false,
  376. });
  377. }
  378. if (options.position === 'right') {
  379. addClass(container, 'sidenav-right');
  380. }
  381. if (type !== 'relative') {
  382. addClass(container, 'sidenav-fixed');
  383. }
  384. instance._renderNav();
  385. }
  386. // Force Reflow for IE11 Browser Bug
  387. setStyles(container, {
  388. display: '',
  389. });
  390. },
  391. _subscribeClickSidenavClose() {
  392. const instance = this;
  393. const options = instance.options;
  394. const containerSelector = options.container;
  395. if (!instance._sidenavCloseSubscription) {
  396. const closeButtonSelector = `${containerSelector} .sidenav-close`;
  397. instance._sidenavCloseSubscription = subscribe(
  398. closeButtonSelector,
  399. 'click',
  400. function handleSidenavClose(event) {
  401. event.preventDefault();
  402. instance.toggle();
  403. }
  404. );
  405. }
  406. },
  407. _subscribeClickTrigger() {
  408. const instance = this;
  409. if (!instance._togglerSubscription) {
  410. const toggler = instance.toggler;
  411. instance._togglerSubscription = subscribe(
  412. toggler,
  413. 'click',
  414. function handleTogglerClick(event) {
  415. instance.toggle();
  416. event.preventDefault();
  417. }
  418. );
  419. }
  420. },
  421. _subscribeSidenavTransitionEnd(element, fn) {
  422. setTimeout(() => {
  423. removeClass(element, 'sidenav-transition');
  424. fn();
  425. }, SideNavigation.TRANSITION_DURATION);
  426. },
  427. clearHeight() {
  428. const instance = this;
  429. const options = instance.options;
  430. const container = document.querySelector(options.container);
  431. if (container) {
  432. const content = container.querySelector(options.content);
  433. const navigation = container.querySelector(options.navigation);
  434. const menu = container.querySelector('.sidenav-menu');
  435. [content, navigation, menu].forEach((element) => {
  436. setStyles(element, {
  437. height: '',
  438. 'min-height': '',
  439. });
  440. });
  441. }
  442. },
  443. destroy() {
  444. const instance = this;
  445. if (instance._sidenavCloseSubscription) {
  446. instance._sidenavCloseSubscription.dispose();
  447. instance._sidenavCloseSubscription = null;
  448. }
  449. if (instance._togglerSubscription) {
  450. instance._togglerSubscription.dispose();
  451. instance._togglerSubscription = null;
  452. }
  453. INSTANCE_MAP.delete(instance.toggler);
  454. },
  455. hide() {
  456. const instance = this;
  457. if (instance.useDataAttribute) {
  458. instance.hideSimpleSidenav();
  459. }
  460. else {
  461. instance.toggleNavigation(false);
  462. }
  463. },
  464. hideSidenav() {
  465. const instance = this;
  466. const options = instance.options;
  467. const container = document.querySelector(options.container);
  468. if (container) {
  469. const content = container.querySelector(options.content);
  470. const navigation = container.querySelector(options.navigation);
  471. const menu = navigation.querySelector('.sidenav-menu');
  472. const sidenavRight = instance._isSidenavRight();
  473. let positionDirection = options.rtl ? 'right' : 'left';
  474. if (sidenavRight) {
  475. positionDirection = options.rtl ? 'left' : 'right';
  476. }
  477. const paddingDirection = 'padding-' + positionDirection;
  478. setStyles(content, {
  479. [paddingDirection]: '',
  480. [positionDirection]: '',
  481. });
  482. setStyles(navigation, {
  483. width: '',
  484. });
  485. if (sidenavRight) {
  486. setStyles(menu, {
  487. [positionDirection]: px(instance._getSidenavWidth()),
  488. });
  489. }
  490. }
  491. },
  492. hideSimpleSidenav() {
  493. const instance = this;
  494. const options = instance.options;
  495. const simpleSidenavClosed = instance._isSimpleSidenavClosed();
  496. if (!simpleSidenavClosed) {
  497. const content = document.querySelector(options.content);
  498. const container = document.querySelector(options.container);
  499. const closedClass = options.closedClass;
  500. const openClass = options.openClass;
  501. const toggler = instance.toggler;
  502. const target =
  503. toggler.dataset.target || toggler.getAttribute('href');
  504. instance._emit('closedStart.lexicon.sidenav');
  505. instance._subscribeSidenavTransitionEnd(content, () => {
  506. removeClass(container, 'sidenav-transition');
  507. removeClass(toggler, 'sidenav-transition');
  508. instance._emit('closed.lexicon.sidenav');
  509. });
  510. if (hasClass(content, openClass)) {
  511. setClasses(content, {
  512. [closedClass]: true,
  513. [openClass]: false,
  514. 'sidenav-transition': true,
  515. });
  516. }
  517. addClass(container, 'sidenav-transition');
  518. addClass(toggler, 'sidenav-transition');
  519. setClasses(container, {
  520. [closedClass]: true,
  521. [openClass]: false,
  522. });
  523. const nodes = document.querySelectorAll(
  524. `[data-target="${target}"], [href="${target}"]`
  525. );
  526. Array.from(nodes).forEach((node) => {
  527. setClasses(node, {
  528. active: false,
  529. [openClass]: false,
  530. });
  531. setClasses(node, {
  532. active: false,
  533. [openClass]: false,
  534. });
  535. });
  536. }
  537. },
  538. init(toggler, options) {
  539. const instance = this;
  540. /**
  541. * For compatibility, we use a data-toggle attribute of
  542. * "liferay-sidenav" to distinguish our internal uses from
  543. * possible external uses of the old jQuery plugin (which used
  544. * "sidenav').
  545. */
  546. const useDataAttribute = toggler.dataset.toggle === 'liferay-sidenav';
  547. options = {...DEFAULTS, ...options};
  548. options.breakpoint = toInt(options.breakpoint);
  549. options.container =
  550. options.container ||
  551. toggler.dataset.target ||
  552. toggler.getAttribute('href');
  553. options.gutter = toInt(options.gutter);
  554. options.rtl = document.dir === 'rtl';
  555. options.width = toInt(options.width);
  556. options.widthOriginal = options.width;
  557. // instantiate using data attribute
  558. if (useDataAttribute) {
  559. options.closedClass = toggler.dataset.closedClass || 'closed';
  560. options.content = toggler.dataset.content;
  561. options.loadingIndicatorTPL =
  562. toggler.dataset.loadingIndicatorTpl ||
  563. options.loadingIndicatorTPL;
  564. options.openClass = toggler.dataset.openClass || 'open';
  565. options.type = toggler.dataset.type;
  566. options.typeMobile = toggler.dataset.typeMobile;
  567. options.url = toggler.dataset.url;
  568. options.width = '';
  569. }
  570. instance.toggler = toggler;
  571. instance.options = options;
  572. instance.useDataAttribute = useDataAttribute;
  573. instance._emitter = new EventEmitter();
  574. instance._bindUI();
  575. instance._renderUI();
  576. },
  577. on(event, listener) {
  578. return this._emitter.on(event, listener);
  579. },
  580. setHeight() {
  581. const instance = this;
  582. const options = instance.options;
  583. const container = document.querySelector(options.container);
  584. const type = instance.mobile ? options.typeMobile : options.type;
  585. if (type !== 'fixed' && type !== 'fixed-push') {
  586. const content = container.querySelector(options.content);
  587. const navigation = container.querySelector(options.navigation);
  588. const menu = container.querySelector('.sidenav-menu');
  589. const contentHeight = content.getBoundingClientRect().height;
  590. const navigationHeight = navigation.getBoundingClientRect().height;
  591. const tallest = px(Math.max(contentHeight, navigationHeight));
  592. setStyles(content, {
  593. 'min-height': tallest,
  594. });
  595. setStyles(navigation, {
  596. height: '100%',
  597. 'min-height': tallest,
  598. });
  599. setStyles(menu, {
  600. height: '100%',
  601. 'min-height': tallest,
  602. });
  603. }
  604. },
  605. show() {
  606. const instance = this;
  607. if (instance.useDataAttribute) {
  608. instance.showSimpleSidenav();
  609. }
  610. else {
  611. instance.toggleNavigation(true);
  612. }
  613. },
  614. showSidenav() {
  615. const instance = this;
  616. const mobile = instance.mobile;
  617. const options = instance.options;
  618. const container = document.querySelector(options.container);
  619. const content = container.querySelector(options.content);
  620. const navigation = container.querySelector(options.navigation);
  621. const menu = navigation.querySelector('.sidenav-menu');
  622. const sidenavRight = instance._isSidenavRight();
  623. const width = instance._getSidenavWidth();
  624. const offset = width + options.gutter;
  625. const url = options.url;
  626. if (url) {
  627. instance._loadUrl(menu, url);
  628. }
  629. setStyles(navigation, {
  630. width: px(width),
  631. });
  632. setStyles(menu, {
  633. width: px(width),
  634. });
  635. let positionDirection = options.rtl ? 'right' : 'left';
  636. if (sidenavRight) {
  637. positionDirection = options.rtl ? 'left' : 'right';
  638. }
  639. const paddingDirection = 'padding-' + positionDirection;
  640. const pushContentCssProperty = mobile
  641. ? positionDirection
  642. : paddingDirection;
  643. const type = mobile ? options.typeMobile : options.type;
  644. if (type !== 'fixed') {
  645. let navigationStartX = hasClass(container, 'open')
  646. ? offsetLeft(navigation) - options.gutter
  647. : offsetLeft(navigation) - offset;
  648. const contentStartX = offsetLeft(content);
  649. const contentWidth = toInt(getComputedStyle(content).width);
  650. let padding = '';
  651. if (
  652. (options.rtl && sidenavRight) ||
  653. (!options.rtl && options.position === 'left')
  654. ) {
  655. navigationStartX = offsetLeft(navigation) + offset;
  656. if (navigationStartX > contentStartX) {
  657. padding = navigationStartX - contentStartX;
  658. }
  659. }
  660. else if (
  661. (options.rtl && options.position === 'left') ||
  662. (!options.rtl && sidenavRight)
  663. ) {
  664. if (navigationStartX < contentStartX + contentWidth) {
  665. padding = contentStartX + contentWidth - navigationStartX;
  666. if (padding >= offset) {
  667. padding = offset;
  668. }
  669. }
  670. }
  671. setStyles(content, {
  672. [pushContentCssProperty]: px(padding),
  673. });
  674. }
  675. },
  676. showSimpleSidenav() {
  677. const instance = this;
  678. const options = instance.options;
  679. const simpleSidenavClosed = instance._isSimpleSidenavClosed();
  680. if (simpleSidenavClosed) {
  681. const content = document.querySelector(options.content);
  682. const container = document.querySelector(options.container);
  683. const closedClass = options.closedClass;
  684. const openClass = options.openClass;
  685. const toggler = instance.toggler;
  686. const url = toggler.dataset.url;
  687. if (url) {
  688. instance._loadUrl(container, url);
  689. }
  690. instance._emit('openStart.lexicon.sidenav');
  691. instance._subscribeSidenavTransitionEnd(content, () => {
  692. removeClass(container, 'sidenav-transition');
  693. removeClass(toggler, 'sidenav-transition');
  694. instance._emit('open.lexicon.sidenav');
  695. });
  696. setClasses(content, {
  697. [closedClass]: false,
  698. [openClass]: true,
  699. 'sidenav-transition': true,
  700. });
  701. setClasses(container, {
  702. [closedClass]: false,
  703. [openClass]: true,
  704. 'sidenav-transition': true,
  705. });
  706. setClasses(toggler, {
  707. active: true,
  708. [openClass]: true,
  709. 'sidenav-transition': true,
  710. });
  711. }
  712. },
  713. toggle() {
  714. const instance = this;
  715. if (instance.useDataAttribute) {
  716. instance.toggleSimpleSidenav();
  717. }
  718. else {
  719. instance.toggleNavigation();
  720. }
  721. },
  722. toggleNavigation(force) {
  723. const instance = this;
  724. const options = instance.options;
  725. const container = document.querySelector(options.container);
  726. const menu = container.querySelector('.sidenav-menu');
  727. const toggler = instance.toggler;
  728. const width = options.width;
  729. const closed =
  730. typeof force === 'boolean' ? force : hasClass(container, 'closed');
  731. const sidenavRight = instance._isSidenavRight();
  732. if (closed) {
  733. instance._emit('openStart.lexicon.sidenav');
  734. }
  735. else {
  736. instance._emit('closedStart.lexicon.sidenav');
  737. }
  738. instance._subscribeSidenavTransitionEnd(container, () => {
  739. const menu = container.querySelector('.sidenav-menu');
  740. if (hasClass(container, 'closed')) {
  741. instance.clearHeight();
  742. setClasses(toggler, {
  743. open: false,
  744. 'sidenav-transition': false,
  745. });
  746. instance._emit('closed.lexicon.sidenav');
  747. }
  748. else {
  749. setClasses(toggler, {
  750. open: true,
  751. 'sidenav-transition': false,
  752. });
  753. instance._emit('open.lexicon.sidenav');
  754. }
  755. if (instance.mobile) {
  756. // ios 8 fixed element disappears when trying to scroll
  757. menu.focus();
  758. }
  759. });
  760. if (closed) {
  761. instance.setHeight();
  762. setStyles(menu, {
  763. width: px(width),
  764. });
  765. const positionDirection = options.rtl ? 'left' : 'right';
  766. if (sidenavRight) {
  767. setStyles(menu, {
  768. [positionDirection]: '',
  769. });
  770. }
  771. }
  772. addClass(container, 'sidenav-transition');
  773. addClass(toggler, 'sidenav-transition');
  774. if (closed) {
  775. instance.showSidenav();
  776. }
  777. else {
  778. instance.hideSidenav();
  779. }
  780. setClasses(container, {
  781. closed: !closed,
  782. open: closed,
  783. });
  784. setClasses(toggler, {
  785. active: closed,
  786. open: closed,
  787. });
  788. },
  789. toggleSimpleSidenav() {
  790. const instance = this;
  791. const simpleSidenavClosed = instance._isSimpleSidenavClosed();
  792. if (simpleSidenavClosed) {
  793. instance.showSimpleSidenav();
  794. }
  795. else {
  796. instance.hideSimpleSidenav();
  797. }
  798. },
  799. visible() {
  800. const instance = this;
  801. let closed;
  802. if (instance.useDataAttribute) {
  803. closed = instance._isSimpleSidenavClosed();
  804. }
  805. else {
  806. const container = document.querySelector(
  807. instance.options.container
  808. );
  809. closed = hasClass(container, 'sidenav-transition')
  810. ? !hasClass(container, 'closed')
  811. : hasClass(container, 'closed');
  812. }
  813. return !closed;
  814. },
  815. };
  816. SideNavigation.destroy = function destroy(element) {
  817. const instance = getInstance(element);
  818. if (instance) {
  819. instance.destroy();
  820. }
  821. };
  822. SideNavigation.hide = function hide(element) {
  823. const instance = getInstance(element);
  824. if (instance) {
  825. instance.hide();
  826. }
  827. };
  828. SideNavigation.initialize = function initialize(toggler, options = {}) {
  829. toggler = getElement(toggler);
  830. let instance = INSTANCE_MAP.get(toggler);
  831. if (!instance) {
  832. instance = new SideNavigation(toggler, options);
  833. INSTANCE_MAP.set(toggler, instance);
  834. }
  835. return instance;
  836. };
  837. SideNavigation.instance = getInstance;
  838. function onReady() {
  839. const togglers = document.querySelectorAll(
  840. '[data-toggle="liferay-sidenav"]'
  841. );
  842. Array.from(togglers).forEach(SideNavigation.initialize);
  843. }
  844. if (document.readyState !== 'loading') {
  845. // readyState is "interactive" or "complete".
  846. onReady();
  847. }
  848. else {
  849. document.addEventListener('DOMContentLoaded', () => {
  850. onReady();
  851. });
  852. }
  853. export default SideNavigation;