Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

818 строки
28 KiB

  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. define([
  6. 'jquery',
  7. 'matchMedia',
  8. 'jquery-ui-modules/menu',
  9. 'mage/translate'
  10. ], function ($, mediaCheck) {
  11. 'use strict';
  12. /**
  13. * Menu Widget - this widget is a wrapper for the jQuery UI Menu
  14. */
  15. $.widget('mage.menu', $.ui.menu, {
  16. options: {
  17. responsive: false,
  18. expanded: false,
  19. showDelay: 42,
  20. hideDelay: 300,
  21. delay: 0,
  22. mediaBreakpoint: '(max-width: 768px)'
  23. },
  24. /**
  25. * @private
  26. */
  27. _create: function () {
  28. var self = this;
  29. this.delay = this.options.delay;
  30. this._super();
  31. $(window).on('resize', function () {
  32. self.element.find('.submenu-reverse').removeClass('submenu-reverse');
  33. });
  34. },
  35. /**
  36. * @private
  37. */
  38. _init: function () {
  39. this._super();
  40. if (this.options.expanded === true) {
  41. this.isExpanded();
  42. }
  43. if (this.options.responsive === true) {
  44. mediaCheck({
  45. media: this.options.mediaBreakpoint,
  46. entry: $.proxy(function () {
  47. this._toggleMobileMode();
  48. }, this),
  49. exit: $.proxy(function () {
  50. this._toggleDesktopMode();
  51. }, this)
  52. });
  53. }
  54. this._assignControls()._listen();
  55. this._setActiveMenu();
  56. },
  57. /**
  58. * @return {Object}
  59. * @private
  60. */
  61. _assignControls: function () {
  62. this.controls = {
  63. toggleBtn: $('[data-action="toggle-nav"]')
  64. };
  65. return this;
  66. },
  67. /**
  68. * @private
  69. */
  70. _listen: function () {
  71. var controls = this.controls,
  72. toggle = this.toggle;
  73. controls.toggleBtn.off('click');
  74. controls.toggleBtn.on('click', toggle.bind(this));
  75. },
  76. /**
  77. * Toggle.
  78. */
  79. toggle: function () {
  80. var html = $('html');
  81. if (html.hasClass('nav-open')) {
  82. html.removeClass('nav-open');
  83. setTimeout(function () {
  84. html.removeClass('nav-before-open');
  85. }, this.options.hideDelay);
  86. } else {
  87. html.addClass('nav-before-open');
  88. setTimeout(function () {
  89. html.addClass('nav-open');
  90. }, this.options.showDelay);
  91. }
  92. },
  93. /**
  94. * Tries to figure out the active category for current page and add appropriate classes:
  95. * - 'active' class for active category
  96. * - 'has-active' class for all parents of active category
  97. *
  98. * First, checks whether current URL is URL of category page,
  99. * otherwise tries to retrieve category URL in case of current URL is product view page URL
  100. * which has category tree path in it.
  101. *
  102. * @return void
  103. * @private
  104. */
  105. _setActiveMenu: function () {
  106. var currentUrl = window.location.href.split('?')[0];
  107. if (!this._setActiveMenuForCategory(currentUrl)) {
  108. this._setActiveMenuForProduct(currentUrl);
  109. }
  110. },
  111. /**
  112. * Looks for category with provided URL and adds 'active' CSS class to it if it was not set before.
  113. * If menu item has parent categories, sets 'has-active' class to all af them.
  114. *
  115. * @param {String} url - possible category URL
  116. * @returns {Boolean} - true if active category was founded by provided URL, otherwise return false
  117. * @private
  118. */
  119. _setActiveMenuForCategory: function (url) {
  120. var activeCategoryLink = this.element.find('a[href="' + url + '"]'),
  121. classes,
  122. classNav;
  123. if (!activeCategoryLink || !activeCategoryLink.hasClass('ui-menu-item-wrapper')) {
  124. //category was not found by provided URL
  125. return false;
  126. } else if (!activeCategoryLink.parent().hasClass('active')) {
  127. activeCategoryLink.parent().addClass('active');
  128. classes = activeCategoryLink.parent().attr('class');
  129. classNav = classes.match(/(nav\-)[0-9]+(\-[0-9]+)+/gi);
  130. if (classNav) {
  131. this._setActiveParent(classNav[0]);
  132. }
  133. }
  134. return true;
  135. },
  136. /**
  137. * Sets 'has-active' CSS class to all parent categories which have part of provided class in childClassName
  138. *
  139. * @example
  140. * childClassName - 'nav-1-2-3'
  141. * CSS class 'has-active' will be added to categories have 'nav-1-2' and 'nav-1' classes
  142. *
  143. * @param {String} childClassName - Class name of active category <li> element
  144. * @return void
  145. * @private
  146. */
  147. _setActiveParent: function (childClassName) {
  148. var parentElement,
  149. parentClass = childClassName.substr(0, childClassName.lastIndexOf('-'));
  150. if (parentClass.lastIndexOf('-') !== -1) {
  151. parentElement = this.element.find('.' + parentClass);
  152. if (parentElement) {
  153. parentElement.addClass('has-active');
  154. }
  155. this._setActiveParent(parentClass);
  156. }
  157. },
  158. /**
  159. * Tries to retrieve category URL from current URL and mark this category as active
  160. * @see _setActiveMenuForCategory(url)
  161. *
  162. * @example
  163. * currentUrl - http://magento.com/category1/category12/product.html,
  164. * category URLs has extensions .phtml - http://magento.com/category1.phtml
  165. * method sets active category which has URL http://magento.com/category1/category12.phtml
  166. *
  167. * @param {String} currentUrl - current page URL without parameters
  168. * @return void
  169. * @private
  170. */
  171. _setActiveMenuForProduct: function (currentUrl) {
  172. var categoryUrlExtension,
  173. lastUrlSection,
  174. possibleCategoryUrl,
  175. //retrieve first category URL to know what extension is used for category URLs
  176. firstCategoryUrl = this.element.find('> li a').attr('href');
  177. if (firstCategoryUrl) {
  178. lastUrlSection = firstCategoryUrl.substr(firstCategoryUrl.lastIndexOf('/'));
  179. categoryUrlExtension = lastUrlSection.lastIndexOf('.') !== -1 ?
  180. lastUrlSection.substr(lastUrlSection.lastIndexOf('.')) : '';
  181. possibleCategoryUrl = currentUrl.substr(0, currentUrl.lastIndexOf('/')) + categoryUrlExtension;
  182. this._setActiveMenuForCategory(possibleCategoryUrl);
  183. }
  184. },
  185. /**
  186. * Add class for expanded option.
  187. */
  188. isExpanded: function () {
  189. var subMenus = this.element.find(this.options.menus),
  190. expandedMenus = subMenus.find(this.options.menus);
  191. expandedMenus.addClass('expanded');
  192. },
  193. /**
  194. * @param {jQuery.Event} event
  195. * @private
  196. */
  197. _activate: function (event) {
  198. window.location.href = this.active.find('> a').attr('href');
  199. this.collapseAll(event);
  200. },
  201. /**
  202. * @param {jQuery.Event} event
  203. * @private
  204. */
  205. _keydown: function (event) {
  206. var match, prev, character, skip, regex,
  207. preventDefault = true;
  208. /* eslint-disable max-depth */
  209. /**
  210. * @param {String} value
  211. */
  212. function escape(value) {
  213. return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
  214. }
  215. if (this.active.closest(this.options.menus).attr('aria-expanded') != 'true') { //eslint-disable-line eqeqeq
  216. switch (event.keyCode) {
  217. case $.ui.keyCode.PAGE_UP:
  218. this.previousPage(event);
  219. break;
  220. case $.ui.keyCode.PAGE_DOWN:
  221. this.nextPage(event);
  222. break;
  223. case $.ui.keyCode.HOME:
  224. this._move('first', 'first', event);
  225. break;
  226. case $.ui.keyCode.END:
  227. this._move('last', 'last', event);
  228. break;
  229. case $.ui.keyCode.UP:
  230. this.previous(event);
  231. break;
  232. case $.ui.keyCode.DOWN:
  233. if (this.active && !this.active.is('.ui-state-disabled')) {
  234. this.expand(event);
  235. }
  236. break;
  237. case $.ui.keyCode.LEFT:
  238. this.previous(event);
  239. break;
  240. case $.ui.keyCode.RIGHT:
  241. this.next(event);
  242. break;
  243. case $.ui.keyCode.ENTER:
  244. case $.ui.keyCode.SPACE:
  245. this._activate(event);
  246. break;
  247. case $.ui.keyCode.ESCAPE:
  248. this.collapse(event);
  249. break;
  250. default:
  251. preventDefault = false;
  252. prev = this.previousFilter || '';
  253. character = String.fromCharCode(event.keyCode);
  254. skip = false;
  255. clearTimeout(this.filterTimer);
  256. if (character === prev) {
  257. skip = true;
  258. } else {
  259. character = prev + character;
  260. }
  261. regex = new RegExp('^' + escape(character), 'i');
  262. match = this.activeMenu.children('.ui-menu-item').filter(function () {
  263. return regex.test($(this).children('a').text());
  264. });
  265. match = skip && match.index(this.active.next()) !== -1 ?
  266. this.active.nextAll('.ui-menu-item') :
  267. match;
  268. // If no matches on the current filter, reset to the last character pressed
  269. // to move down the menu to the first item that starts with that character
  270. if (!match.length) {
  271. character = String.fromCharCode(event.keyCode);
  272. regex = new RegExp('^' + escape(character), 'i');
  273. match = this.activeMenu.children('.ui-menu-item').filter(function () {
  274. return regex.test($(this).children('a').text());
  275. });
  276. }
  277. if (match.length) {
  278. this.focus(event, match);
  279. if (match.length > 1) {
  280. this.previousFilter = character;
  281. this.filterTimer = this._delay(function () {
  282. delete this.previousFilter;
  283. }, 1000);
  284. } else {
  285. delete this.previousFilter;
  286. }
  287. } else {
  288. delete this.previousFilter;
  289. }
  290. }
  291. } else {
  292. switch (event.keyCode) {
  293. case $.ui.keyCode.DOWN:
  294. this.next(event);
  295. break;
  296. case $.ui.keyCode.UP:
  297. this.previous(event);
  298. break;
  299. case $.ui.keyCode.RIGHT:
  300. if (this.active && !this.active.is('.ui-state-disabled')) {
  301. this.expand(event);
  302. }
  303. break;
  304. case $.ui.keyCode.ENTER:
  305. case $.ui.keyCode.SPACE:
  306. this._activate(event);
  307. break;
  308. case $.ui.keyCode.LEFT:
  309. case $.ui.keyCode.ESCAPE:
  310. this.collapse(event);
  311. break;
  312. default:
  313. preventDefault = false;
  314. prev = this.previousFilter || '';
  315. character = String.fromCharCode(event.keyCode);
  316. skip = false;
  317. clearTimeout(this.filterTimer);
  318. if (character === prev) {
  319. skip = true;
  320. } else {
  321. character = prev + character;
  322. }
  323. regex = new RegExp('^' + escape(character), 'i');
  324. match = this.activeMenu.children('.ui-menu-item').filter(function () {
  325. return regex.test($(this).children('a').text());
  326. });
  327. match = skip && match.index(this.active.next()) !== -1 ?
  328. this.active.nextAll('.ui-menu-item') :
  329. match;
  330. // If no matches on the current filter, reset to the last character pressed
  331. // to move down the menu to the first item that starts with that character
  332. if (!match.length) {
  333. character = String.fromCharCode(event.keyCode);
  334. regex = new RegExp('^' + escape(character), 'i');
  335. match = this.activeMenu.children('.ui-menu-item').filter(function () {
  336. return regex.test($(this).children('a').text());
  337. });
  338. }
  339. if (match.length) {
  340. this.focus(event, match);
  341. if (match.length > 1) {
  342. this.previousFilter = character;
  343. this.filterTimer = this._delay(function () {
  344. delete this.previousFilter;
  345. }, 1000);
  346. } else {
  347. delete this.previousFilter;
  348. }
  349. } else {
  350. delete this.previousFilter;
  351. }
  352. }
  353. }
  354. /* eslint-enable max-depth */
  355. if (preventDefault) {
  356. event.preventDefault();
  357. }
  358. },
  359. /**
  360. * @private
  361. */
  362. _toggleMobileMode: function () {
  363. var subMenus;
  364. $(this.element).off('mouseenter mouseleave');
  365. this._on({
  366. /**
  367. * @param {jQuery.Event} event
  368. */
  369. 'click .ui-menu-item:has(a)': function (event) {
  370. var target;
  371. event.preventDefault();
  372. target = $(event.target).closest('.ui-menu-item');
  373. target.get(0).scrollIntoView();
  374. if (!target.hasClass('level-top') || !target.has('.ui-menu').length) {
  375. window.location.href = target.find('> a').attr('href');
  376. }
  377. },
  378. /**
  379. * @param {jQuery.Event} event
  380. */
  381. 'click .ui-menu-item:has(.ui-state-active)': function (event) {
  382. this.collapseAll(event, true);
  383. }
  384. });
  385. subMenus = this.element.find('.level-top');
  386. $.each(subMenus, $.proxy(function (index, item) {
  387. var category = $(item).find('> a span').not('.ui-menu-icon').text(),
  388. categoryUrl = $(item).find('> a').attr('href'),
  389. menu = $(item).find('> .ui-menu');
  390. this.categoryLink = $('<a>')
  391. .attr('href', categoryUrl)
  392. .text($.mage.__('All %1').replace('%1', category));
  393. this.categoryParent = $('<li>')
  394. .addClass('ui-menu-item all-category')
  395. .html(this.categoryLink);
  396. if (menu.find('.all-category').length === 0) {
  397. menu.prepend(this.categoryParent);
  398. }
  399. }, this));
  400. },
  401. /**
  402. * @private
  403. */
  404. _toggleDesktopMode: function () {
  405. var categoryParent, html;
  406. $(this.element).off('click mousedown mouseenter mouseleave');
  407. this._on({
  408. /**
  409. * Prevent focus from sticking to links inside menu after clicking
  410. * them (focus should always stay on UL during navigation).
  411. */
  412. 'mousedown .ui-menu-item > a': function (event) {
  413. event.preventDefault();
  414. },
  415. /**
  416. * Prevent focus from sticking to links inside menu after clicking
  417. * them (focus should always stay on UL during navigation).
  418. */
  419. 'click .ui-state-disabled > a': function (event) {
  420. event.preventDefault();
  421. },
  422. /**
  423. * @param {jQuer.Event} event
  424. */
  425. 'click .ui-menu-item:has(a)': function (event) {
  426. var target = $(event.target).closest('.ui-menu-item');
  427. if (!this.mouseHandled && target.not('.ui-state-disabled').length) {
  428. this.select(event);
  429. // Only set the mouseHandled flag if the event will bubble, see #9469.
  430. if (!event.isPropagationStopped()) {
  431. this.mouseHandled = true;
  432. }
  433. // Open submenu on click
  434. if (target.has('.ui-menu').length) {
  435. this.expand(event);
  436. } else if (!this.element.is(':focus') &&
  437. $(this.document[0].activeElement).closest('.ui-menu').length
  438. ) {
  439. // Redirect focus to the menu
  440. this.element.trigger('focus', [true]);
  441. // If the active item is on the top level, let it stay active.
  442. // Otherwise, blur the active item since it is no longer visible.
  443. if (this.active && this.active.parents('.ui-menu').length === 1) { //eslint-disable-line
  444. clearTimeout(this.timer);
  445. }
  446. }
  447. }
  448. },
  449. /**
  450. * @param {jQuery.Event} event
  451. */
  452. 'mouseenter .ui-menu-item': function (event) {
  453. var target = $(event.currentTarget),
  454. submenu = this.options.menus,
  455. ulElement,
  456. ulElementWidth,
  457. width,
  458. targetPageX,
  459. rightBound;
  460. if (target.has(submenu)) {
  461. ulElement = target.find(submenu);
  462. ulElementWidth = ulElement.outerWidth(true);
  463. width = target.outerWidth() * 2;
  464. targetPageX = target.offset().left;
  465. rightBound = $(window).width();
  466. if (ulElementWidth + width + targetPageX > rightBound) {
  467. ulElement.addClass('submenu-reverse');
  468. }
  469. if (targetPageX - ulElementWidth < 0) {
  470. ulElement.removeClass('submenu-reverse');
  471. }
  472. }
  473. // Remove ui-state-active class from siblings of the newly focused menu item
  474. // to avoid a jump caused by adjacent elements both having a class with a border
  475. target.siblings().children('.ui-state-active').removeClass('ui-state-active');
  476. this.focus(event, target);
  477. },
  478. /**
  479. * @param {jQuery.Event} event
  480. */
  481. 'mouseleave': function (event) {
  482. this.collapseAll(event, true);
  483. },
  484. /**
  485. * Mouse leave.
  486. */
  487. 'mouseleave .ui-menu': 'collapseAll'
  488. });
  489. categoryParent = this.element.find('.all-category');
  490. html = $('html');
  491. categoryParent.remove();
  492. if (html.hasClass('nav-open')) {
  493. html.removeClass('nav-open');
  494. setTimeout(function () {
  495. html.removeClass('nav-before-open');
  496. }, this.options.hideDelay);
  497. }
  498. },
  499. /**
  500. * @param {*} handler
  501. * @param {Number} delay
  502. * @return {Number}
  503. * @private
  504. */
  505. _delay: function (handler, delay) {
  506. var instance = this,
  507. /**
  508. * @return {*}
  509. */
  510. handlerProxy = function () {
  511. return (typeof handler === 'string' ? instance[handler] : handler).apply(instance, arguments);
  512. };
  513. return setTimeout(handlerProxy, delay || 0);
  514. },
  515. /**
  516. * @param {jQuery.Event} event
  517. */
  518. expand: function (event) {
  519. var newItem = this.active &&
  520. this.active
  521. .children('.ui-menu')
  522. .children('.ui-menu-item')
  523. .first();
  524. if (newItem && newItem.length) {
  525. if (newItem.closest('.ui-menu').is(':visible') &&
  526. newItem.closest('.ui-menu').has('.all-categories')
  527. ) {
  528. return;
  529. }
  530. // remove the active state class from the siblings
  531. this.active.siblings().children('.ui-state-active').removeClass('ui-state-active');
  532. this._open(newItem.parent());
  533. // Delay so Firefox will not hide activedescendant change in expanding submenu from AT
  534. this._delay(function () {
  535. this.focus(event, newItem);
  536. });
  537. }
  538. },
  539. /**
  540. * @param {jQuery.Event} event
  541. */
  542. select: function (event) {
  543. var ui;
  544. this.active = this.active || $(event.target).closest('.ui-menu-item');
  545. if (this.active.is('.all-category')) {
  546. this.active = $(event.target).closest('.ui-menu-item');
  547. }
  548. ui = {
  549. item: this.active
  550. };
  551. if (!this.active.has('.ui-menu').length) {
  552. this.collapseAll(event, true);
  553. }
  554. this._trigger('select', event, ui);
  555. }
  556. });
  557. $.widget('mage.navigation', $.mage.menu, {
  558. options: {
  559. responsiveAction: 'wrap', //option for responsive handling
  560. maxItems: null, //option to set max number of menu items
  561. container: '#menu', //container to check against navigation length
  562. moreText: $.mage.__('more'),
  563. breakpoint: 768
  564. },
  565. /**
  566. * @private
  567. */
  568. _init: function () {
  569. var that, responsive;
  570. this._super();
  571. that = this;
  572. responsive = this.options.responsiveAction;
  573. this.element
  574. .addClass('ui-menu-responsive')
  575. .attr('responsive', 'main');
  576. this.setupMoreMenu();
  577. this.setMaxItems();
  578. //check responsive option
  579. if (responsive == 'onResize') { //eslint-disable-line eqeqeq
  580. $(window).on('resize', function () {
  581. if ($(window).width() > that.options.breakpoint) {
  582. that._responsive();
  583. $('[responsive=more]').show();
  584. } else {
  585. that.element.children().show();
  586. $('[responsive=more]').hide();
  587. }
  588. });
  589. } else if (responsive == 'onReload') { //eslint-disable-line eqeqeq
  590. this._responsive();
  591. }
  592. },
  593. /**
  594. * Setup more menu.
  595. */
  596. setupMoreMenu: function () {
  597. var moreListItems = this.element.children().clone(),
  598. moreLink = $('<a>' + this.options.moreText + '</a>');
  599. moreListItems.hide();
  600. moreLink.attr('href', '#');
  601. this.moreItemsList = $('<ul>')
  602. .append(moreListItems);
  603. this.moreListContainer = $('<li>')
  604. .append(moreLink)
  605. .append(this.moreItemsList);
  606. this.responsiveMenu = $('<ul>')
  607. .addClass('ui-menu-more')
  608. .attr('responsive', 'more')
  609. .append(this.moreListContainer)
  610. .menu({
  611. position: {
  612. my: 'right top',
  613. at: 'right bottom'
  614. }
  615. })
  616. .insertAfter(this.element);
  617. },
  618. /**
  619. * @private
  620. */
  621. _responsive: function () {
  622. var container = $(this.options.container),
  623. containerSize = container.width(),
  624. width = 0,
  625. items = this.element.children('li'),
  626. more = $('.ui-menu-more > li > ul > li a');
  627. items = items.map(function () {
  628. var item = {};
  629. item.item = $(this);
  630. item.itemSize = $(this).outerWidth();
  631. return item;
  632. });
  633. $.each(items, function (index) {
  634. var itemText = items[index].item
  635. .find('a:first')
  636. .text();
  637. width += parseInt(items[index].itemSize, null); //eslint-disable-line radix
  638. if (width < containerSize) {
  639. items[index].item.show();
  640. more.each(function () {
  641. var text = $(this).text();
  642. if (text === itemText) {
  643. $(this).parent().hide();
  644. }
  645. });
  646. } else if (width > containerSize) {
  647. items[index].item.hide();
  648. more.each(function () {
  649. var text = $(this).text();
  650. if (text === itemText) {
  651. $(this).parent().show();
  652. }
  653. });
  654. }
  655. });
  656. },
  657. /**
  658. * Set max items.
  659. */
  660. setMaxItems: function () {
  661. var items = this.element.children('li'),
  662. itemsCount = items.length,
  663. maxItems = this.options.maxItems,
  664. overflow = itemsCount - maxItems,
  665. overflowItems = items.slice(overflow);
  666. overflowItems.hide();
  667. overflowItems.each(function () {
  668. var itemText = $(this).find('a:first').text();
  669. $(this).hide();
  670. $('.ui-menu-more > li > ul > li a').each(function () {
  671. var text = $(this).text();
  672. if (text === itemText) {
  673. $(this).parent().show();
  674. }
  675. });
  676. });
  677. }
  678. });
  679. return {
  680. menu: $.mage.menu,
  681. navigation: $.mage.navigation
  682. };
  683. });