navigation-menu.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. /**
  2. * Copyright © 2015 Magento. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /*jshint jquery:true*/
  6. define([
  7. "jquery",
  8. "matchMedia",
  9. "mage/template",
  10. "mage/dropdowns",
  11. "mage/terms"
  12. ],function($, mediaCheck, mageTemplate) {
  13. 'use strict';
  14. $.widget('mage.navigationMenu', {
  15. options: {
  16. itemsContainer: '> ul',
  17. topLevel: 'li.level0',
  18. topLevelSubmenu: '> .submenu',
  19. topLevelHoverClass: 'hover',
  20. expandedTopLevel: '.more',
  21. hoverInTimeout: 300,
  22. hoverOutTimeout: 500,
  23. submenuAnimationSpeed: 200,
  24. collapsable: true,
  25. collapsableDropdownTemplate:
  26. '<script type="text/x-magento-template">' +
  27. '<li class="level0 level-top more parent">' +
  28. '<div class="submenu">' +
  29. '<ul><%= elems %></ul>' +
  30. '</div>' +
  31. '</li>' +
  32. '</script>'
  33. },
  34. _create: function() {
  35. this.itemsContainer = $(this.options.itemsContainer, this.element);
  36. this.topLevel = $(this.options.topLevel, this.element);
  37. this.topLevelSubmenu = $(this.options.topLevelSubmenu, this.topLevel);
  38. this._bind();
  39. },
  40. _init: function() {
  41. if (this.options.collapsable) {
  42. setTimeout($.proxy(function() {
  43. this._checkToCollapseOrExpand();
  44. }, this), 100);
  45. }
  46. },
  47. _bind: function() {
  48. this._on({
  49. 'mouseenter > ul > li.level0': function(e) {
  50. if (!this.entered) { // fix IE bug with 'mouseenter' event
  51. this.timeoutId && clearTimeout(this.timeoutId);
  52. this.timeoutId = setTimeout($.proxy(function() {
  53. this._openSubmenu(e);
  54. }, this), this.options.hoverInTimeout);
  55. this.entered = true;
  56. }
  57. },
  58. 'mouseleave > ul > li.level0': function(e) {
  59. this.entered = null;
  60. this.timeoutId && clearTimeout(this.timeoutId);
  61. this.timeoutId = setTimeout($.proxy(function() {
  62. this._closeSubmenu(e.currentTarget);
  63. }, this), this.options.hoverOutTimeout);
  64. },
  65. 'click': function(e) {
  66. e.stopPropagation();
  67. }
  68. });
  69. $(document)
  70. .on('click.hideMenu', $.proxy(function(e) {
  71. var isOpened = this.topLevel.filter(function() {
  72. return $(this).data('opened');
  73. });
  74. if (isOpened) {
  75. this._closeSubmenu(null, false);
  76. }
  77. }, this));
  78. $(window)
  79. .on('resize', $.proxy(function() {
  80. this.timeoutOnResize && clearTimeout(this.timeoutOnResize);
  81. this.timeoutOnResize = setTimeout($.proxy(function() {
  82. if (this.options.collapsable) {
  83. if ($(this.options.expandedTopLevel, this.element).length) {
  84. this._expandMenu();
  85. }
  86. this._checkToCollapseOrExpand();
  87. }
  88. }, this), 300);
  89. }, this));
  90. },
  91. _openSubmenu: function(e) {
  92. var menuItem = e.currentTarget;
  93. if (!$(menuItem).data('opened')) {
  94. this._closeSubmenu(menuItem, true, true);
  95. $(this.options.topLevelSubmenu, menuItem)
  96. .slideDown(this.options.submenuAnimationSpeed, $.proxy(function() {
  97. $(menuItem).addClass(this.options.topLevelHoverClass);
  98. $(menuItem).data('opened', true);
  99. }, this));
  100. } else if ($(e.target).closest(this.options.topLevel)) {
  101. $(e.target)
  102. .addClass(this.options.topLevelHoverClass)
  103. .siblings(this.options.topLevel)
  104. .removeClass(this.options.topLevelHoverClass);
  105. }
  106. },
  107. _closeSubmenu: function(menuItem, excludeCurrent, fast) {
  108. var topLevel = $(this.options.topLevel, this.element),
  109. activeSubmenu = $(this.options.topLevelSubmenu, menuItem || null);
  110. $(this.options.topLevelSubmenu, topLevel)
  111. .filter(function() {
  112. return excludeCurrent ? $(this).not(activeSubmenu) : true;
  113. })
  114. .slideUp(fast ? 0 : this.options.submenuAnimationSpeed);
  115. topLevel
  116. .removeClass(this.options.topLevelHoverClass)
  117. .data('opened', false);
  118. },
  119. _checkToCollapseOrExpand: function() {
  120. if ($("html").hasClass("lt-640") || $("html").hasClass("w-640")) {
  121. return;
  122. }
  123. var navWidth = this.itemsContainer.width(),
  124. totalWidth = 0,
  125. startCollapseIndex = 0;
  126. $.each($(this.options.topLevel, this.element), function(index, item) {
  127. totalWidth = totalWidth + $(item).outerWidth(true);
  128. if (totalWidth > navWidth && !startCollapseIndex) {
  129. startCollapseIndex = index - 2;
  130. }
  131. });
  132. this[startCollapseIndex ? '_collapseMenu' : '_expandMenu'](startCollapseIndex);
  133. },
  134. _collapseMenu: function(startCollapseIndex) {
  135. this.elemsToCollapse = this.topLevel.filter(function(index) {
  136. return index > startCollapseIndex;
  137. });
  138. this.elemsToCollapseClone = $('<div></div>').append(this.elemsToCollapse.clone()).html();
  139. this.collapsableDropdown = $(
  140. mageTemplate(
  141. this.options.collapsableDropdownTemplate,
  142. {elems: this.elemsToCollapseClone}
  143. )
  144. );
  145. this.itemsContainer.append(this.collapsableDropdown);
  146. this.elemsToCollapse.detach();
  147. },
  148. _expandMenu: function() {
  149. this.elemsToCollapse && this.elemsToCollapse.appendTo(this.itemsContainer);
  150. this.collapsableDropdown && this.collapsableDropdown.remove();
  151. },
  152. _destroy: function() {
  153. this._expandMenu();
  154. }
  155. });
  156. /*
  157. * Provides "Continium" effect for submenu
  158. * */
  159. $.widget('mage.navigationMenu', $.mage.navigationMenu, {
  160. options: {
  161. parentLevel: '> ul > li.level0',
  162. submenuAnimationSpeed: 150,
  163. submenuContiniumEffect: false
  164. },
  165. _init: function() {
  166. this._super();
  167. this._applySubmenuStyles();
  168. },
  169. _applySubmenuStyles: function() {
  170. $(this.options.topLevelSubmenu, $(this.options.topLevel, this.element))
  171. .removeAttr('style');
  172. $(this.options.topLevelSubmenu, $(this.options.parentLevel, this.element))
  173. .css({
  174. display: 'block',
  175. height: 0,
  176. overflow: 'hidden'
  177. });
  178. },
  179. _openSubmenu: function(e) {
  180. var menuItem = e.currentTarget,
  181. submenu = $(this.options.topLevelSubmenu, menuItem),
  182. openedItems = $(this.options.topLevel, this.element).filter(function() {
  183. return $(this).data('opened');
  184. });
  185. if (submenu.length) {
  186. this.heightToAnimate = $(this.options.itemsContainer, submenu).outerHeight(true);
  187. if (openedItems.length) {
  188. this._closeSubmenu(menuItem, true, this.heightToAnimate, $.proxy(function() {
  189. submenu.css({
  190. height: 'auto'
  191. });
  192. $(menuItem)
  193. .addClass(this.options.topLevelHoverClass);
  194. }, this), e);
  195. } else {
  196. submenu.animate({
  197. height: this.heightToAnimate
  198. }, this.options.submenuAnimationSpeed, $.proxy(function() {
  199. $(menuItem)
  200. .addClass(this.options.topLevelHoverClass);
  201. }, this));
  202. }
  203. $(menuItem)
  204. .data('opened', true);
  205. } else {
  206. this._closeSubmenu(menuItem);
  207. }
  208. },
  209. _closeSubmenu: function(menuItem, excludeCurrent, heightToAnimate, callback, e) {
  210. var topLevel = $(this.options.topLevel, this.itemsContainer),
  211. expandedTopLevel = e && $(e.target).closest(this.options.expandedTopLevel);
  212. if (!excludeCurrent) {
  213. $(this.options.topLevelSubmenu, $(this.options.parentLevel, this.element))
  214. .animate({
  215. height: 0
  216. });
  217. topLevel
  218. .data('opened', false)
  219. .removeClass(this.options.topLevelHoverClass);
  220. } else {
  221. var prevOpenedItem = topLevel.filter(function() {
  222. return $(this).data('opened');
  223. }),
  224. prevOpenedSubmenu = $(this.options.topLevelSubmenu, prevOpenedItem);
  225. prevOpenedSubmenu.animate({
  226. height: heightToAnimate
  227. }, this.options.submenuAnimationSpeed, 'linear', function() {
  228. $(this).css({
  229. height: 0
  230. });
  231. callback && callback();
  232. });
  233. prevOpenedItem
  234. .data('opened', false)
  235. .removeClass(this.options.topLevelHoverClass);
  236. }
  237. },
  238. _collapseMenu: function() {
  239. this._superApply(arguments);
  240. this._applySubmenuStyles();
  241. }
  242. });
  243. // Responsive menu
  244. $.widget('mage.navigationMenu', $.mage.navigationMenu, {
  245. options: {
  246. responsive: false,
  247. origNavPlaceholder: '.page-header',
  248. mainContainer: 'body',
  249. pageWrapper: '.page-wrapper',
  250. openedMenuClass: 'opened',
  251. toggleActionPlaceholder: '.block-search',
  252. itemWithSubmenu: 'li.parent',
  253. titleWithSubmenu: 'li.parent > a',
  254. submenu: 'li.parent > .submenu',
  255. toggleActionTemplate:
  256. '<script type="text/x-magento-template">' +
  257. '<span data-action="toggle-nav" class="action toggle nav">Toggle Nav</span>' +
  258. '</script>',
  259. submenuActionsTemplate:
  260. '<script type="text/x-magento-template">' +
  261. '<li class="action all">' +
  262. '<a href="<%= categoryURL %>"><span>All <%= category %></span></a>' +
  263. '</li>' +
  264. '</script>',
  265. navigationSectionsWrapperTemplate:
  266. '<script type="text/x-magento-template">' +
  267. '<dl class="navigation-tabs" data-sections="tabs">' +
  268. '</dl>' +
  269. '</script>',
  270. navigationItemWrapperTemplate:
  271. '<script type="text/x-magento-template">' +
  272. '<dt class="item title <% if (active) { %>active<% } %>" data-section="title">' +
  273. '<a class="switch" data-toggle="switch" href="#TODO"><%= title %></a>' +
  274. '</dt>' +
  275. '<dd class="item content <% if (active) { %>active<%}%>" data-section="content">' +
  276. '</dd>' +
  277. '</script>'
  278. },
  279. _init: function() {
  280. this._super();
  281. this.mainContainer = $(this.options.mainContainer);
  282. this.pageWrapper = $(this.options.pageWrapper);
  283. this.toggleAction = $(mageTemplate(this.options.toggleActionTemplate, {}));
  284. if (this.options.responsive) {
  285. mediaCheck({
  286. media: '(min-width: 768px)',
  287. entry: $.proxy(function() {
  288. this._toggleDesktopMode();
  289. }, this),
  290. exit: $.proxy(function() {
  291. this._toggleMobileMode();
  292. }, this)
  293. });
  294. }
  295. },
  296. _bind: function() {
  297. this._super();
  298. this._bindDocumentEvents();
  299. },
  300. _bindDocumentEvents: function() {
  301. if (!this.eventsBound) {
  302. $(document)
  303. .on('click.toggleMenu', '.action.toggle.nav', $.proxy(function(e) {
  304. if ($(this.element).data('opened')) {
  305. this._hideMenu();
  306. } else {
  307. this._showMenu();
  308. }
  309. e.stopPropagation();
  310. this.mobileNav.scrollTop(0);
  311. this._fixedBackLink();
  312. }, this))
  313. .on('click.hideMenu', this.options.pageWrapper, $.proxy(function() {
  314. if ($(this.element).data('opened')) {
  315. this._hideMenu();
  316. this.mobileNav.scrollTop(0);
  317. this._fixedBackLink();
  318. }
  319. }, this))
  320. .on('click.showSubmenu', this.options.titleWithSubmenu, $.proxy(function(e) {
  321. this._showSubmenu(e);
  322. e.preventDefault();
  323. this.mobileNav.scrollTop(0);
  324. this._fixedBackLink();
  325. }, this))
  326. .on('click.hideSubmenu', '.action.back', $.proxy(function(e) {
  327. this._hideSubmenu(e);
  328. this.mobileNav.scrollTop(0);
  329. this._fixedBackLink();
  330. }, this));
  331. this.eventsBound = true;
  332. }
  333. },
  334. _showMenu: function() {
  335. $(this.element).data('opened', true);
  336. this.mainContainer.add( "html" ).addClass(this.options.openedMenuClass);
  337. },
  338. _hideMenu: function() {
  339. $(this.element).data('opened', false);
  340. this.mainContainer.add( "html" ).removeClass(this.options.openedMenuClass);
  341. },
  342. _showSubmenu: function(e) {
  343. $(e.currentTarget).addClass('action back');
  344. var submenu = $(e.currentTarget).siblings('.submenu');
  345. submenu.addClass('opened');
  346. },
  347. _hideSubmenu: function(e) {
  348. var submenuSelector = '.submenu',
  349. submenu = $(e.currentTarget).next(submenuSelector);
  350. $(e.currentTarget).removeClass('action back');
  351. submenu.removeClass('opened');
  352. },
  353. _renderSubmenuActions: function() {
  354. $.each(
  355. $(this.options.itemWithSubmenu),
  356. $.proxy(
  357. function(index, item) {
  358. var actions = $(
  359. mageTemplate(
  360. this.options.submenuActionsTemplate,
  361. {
  362. category: $('> a > span', item).text(),
  363. categoryURL: $('> a', item).attr('href')
  364. }
  365. )
  366. ),
  367. submenu = $('> .submenu', item),
  368. items = $('> ul', submenu);
  369. items.prepend(actions);
  370. },
  371. this
  372. )
  373. );
  374. },
  375. _toggleMobileMode: function() {
  376. this._expandMenu();
  377. $(this.options.topLevelSubmenu, $(this.options.topLevel, this.element))
  378. .removeAttr('style');
  379. this.toggleAction.insertBefore(this.options.toggleActionPlaceholder);
  380. this.mobileNav = $(this.element).detach().clone();
  381. this.mainContainer.prepend(this.mobileNav);
  382. this.mobileNav.find('> ul').addClass('nav');
  383. this._insertExtraItems();
  384. this._wrapItemsInSections();
  385. this.mobileNav.scroll($.proxy(
  386. function() {
  387. this._fixedBackLink();
  388. }, this
  389. ));
  390. this._renderSubmenuActions();
  391. this._bindDocumentEvents();
  392. },
  393. _toggleDesktopMode: function() {
  394. this.mobileNav && this.mobileNav.remove();
  395. this.toggleAction.detach();
  396. $(this.element).insertAfter(this.options.origNavPlaceholder);
  397. $(document)
  398. .off('click.toggleMenu', '.action.toggle.nav')
  399. .off('click.hideMenu', this.options.pageWrapper)
  400. .off('click.showSubmenu', this.options.titleWithSubmenu)
  401. .off('click.hideSubmenu', '.action.back');
  402. this.eventsBound = false;
  403. this._applySubmenuStyles();
  404. },
  405. _insertExtraItems: function() {
  406. if ($('.header.panel .switcher').length) {
  407. var settings = $('.header.panel .switcher')
  408. .clone()
  409. .addClass('settings');
  410. this.mobileNav.prepend(settings);
  411. }
  412. if ($('.footer .switcher').length) {
  413. var footerSettings = $('.footer .switcher')
  414. .clone()
  415. .addClass('settings');
  416. this.mobileNav.prepend(footerSettings);
  417. }
  418. if ($('.header.panel .header.links li').length) {
  419. var account = $('.header.panel > .header.links')
  420. .clone()
  421. .addClass('account');
  422. this.mobileNav.prepend(account);
  423. }
  424. },
  425. _wrapItemsInSections: function() {
  426. var account = $('> .account', this.mobileNav),
  427. settings = $('> .settings', this.mobileNav),
  428. nav = $('> .nav', this.mobileNav),
  429. navigationSectionsWrapper = $(mageTemplate(this.options.navigationSectionsWrapperTemplate, {})),
  430. navigationItemWrapper;
  431. this.mobileNav.append(navigationSectionsWrapper);
  432. if (nav.length) {
  433. navigationItemWrapper = $(mageTemplate(this.options.navigationItemWrapperTemplate, {title: 'Menu'}));
  434. navigationSectionsWrapper.append(navigationItemWrapper);
  435. navigationItemWrapper.eq(1).append(nav);
  436. }
  437. if (account.length) {
  438. navigationItemWrapper = $(mageTemplate(this.options.navigationItemWrapperTemplate, {title: 'Account'}));
  439. navigationSectionsWrapper.append(navigationItemWrapper);
  440. navigationItemWrapper.eq(1).append(account);
  441. }
  442. if (settings.length) {
  443. navigationItemWrapper = $(
  444. mageTemplate(this.options.navigationItemWrapperTemplate, {title: 'Settings'})
  445. );
  446. navigationSectionsWrapper.append(navigationItemWrapper);
  447. navigationItemWrapper.eq(1).append(settings);
  448. }
  449. navigationSectionsWrapper.addClass("navigation-tabs-" + navigationSectionsWrapper.find('[data-section="title"]').length);
  450. navigationSectionsWrapper.terms();
  451. },
  452. _fixedBackLink: function() {
  453. var linksBack = this.mobileNav.find('.submenu .action.back');
  454. var linkBack = this.mobileNav.find('.submenu.opened > ul > .action.back').last();
  455. linksBack.removeClass('fixed');
  456. if(linkBack.length) {
  457. var subMenu = linkBack.parent(),
  458. navOffset = this.mobileNav.find('.nav').position().top,
  459. linkBackHeight = linkBack.height();
  460. if (navOffset <= 0) {
  461. linkBack.addClass('fixed');
  462. subMenu.css({
  463. paddingTop: linkBackHeight
  464. })
  465. } else {
  466. linkBack.removeClass('fixed');
  467. subMenu.css({
  468. paddingTop: 0
  469. })
  470. }
  471. }
  472. }
  473. });
  474. return $.mage.navigationMenu;
  475. });