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

604 строки
18 KiB

  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. define([
  6. 'jquery',
  7. 'jquery-ui-modules/widget',
  8. 'jquery-ui-modules/core',
  9. 'jquery/jquery-storageapi',
  10. 'mage/mage'
  11. ], function ($) {
  12. 'use strict';
  13. var hideProps = {},
  14. showProps = {};
  15. hideProps.height = 'hide';
  16. showProps.height = 'show';
  17. $.widget('mage.collapsible', {
  18. options: {
  19. active: false,
  20. disabled: false,
  21. collapsible: true,
  22. header: '[data-role=title]',
  23. content: '[data-role=content]',
  24. trigger: '[data-role=trigger]',
  25. closedState: null,
  26. openedState: null,
  27. disabledState: null,
  28. ajaxUrlElement: '[data-ajax=true]',
  29. ajaxContent: false,
  30. loadingClass: null,
  31. saveState: false,
  32. animate: false,
  33. icons: {
  34. activeHeader: null,
  35. header: null
  36. },
  37. collateral: {
  38. element: null,
  39. openedState: null
  40. }
  41. },
  42. /**
  43. * @private
  44. */
  45. _create: function () {
  46. this.storage = $.localStorage;
  47. this.icons = false;
  48. if (typeof this.options.icons === 'string') {
  49. this.options.icons = JSON.parse(this.options.icons);
  50. }
  51. this._processPanels();
  52. this._processState();
  53. this._refresh();
  54. if (this.options.icons.header && this.options.icons.activeHeader) {
  55. this._createIcons();
  56. this.icons = true;
  57. }
  58. this.element.on('dimensionsChanged', function (e) {
  59. if (e.target && e.target.classList.contains('active')) {
  60. this._scrollToTopIfNotVisible();
  61. }
  62. }.bind(this));
  63. this._bind('click');
  64. this._trigger('created');
  65. },
  66. /**
  67. * @private
  68. */
  69. _refresh: function () {
  70. this.trigger.attr('tabIndex', 0);
  71. if (this.options.active && !this.options.disabled) {
  72. if (this.options.openedState) {
  73. this.element.addClass(this.options.openedState);
  74. }
  75. if (this.options.collateral.element && this.options.collateral.openedState) {
  76. $(this.options.collateral.element).addClass(this.options.collateral.openedState);
  77. }
  78. if (this.options.ajaxContent) {
  79. this._loadContent();
  80. }
  81. // ARIA (updates aria attributes)
  82. this.header.attr({
  83. 'aria-selected': false
  84. });
  85. } else if (this.options.disabled) {
  86. this.disable();
  87. } else {
  88. this.content.hide();
  89. if (this.options.closedState) {
  90. this.element.addClass(this.options.closedState);
  91. }
  92. }
  93. },
  94. /**
  95. * Processing the state:
  96. * If deep linking is used and the anchor is the id of the content or the content contains this id,
  97. * and the collapsible element is a nested one having collapsible parents, in order to see the content,
  98. * all the parents must be expanded.
  99. * @private
  100. */
  101. _processState: function () {
  102. var anchor = window.location.hash,
  103. isValid = $.mage.isValidSelector(anchor),
  104. urlPath = window.location.pathname.replace(/\./g, ''),
  105. state;
  106. this.stateKey = encodeURIComponent(urlPath + this.element.attr('id'));
  107. if (isValid &&
  108. ($(this.content.find(anchor)).length > 0 || this.content.attr('id') === anchor.replace('#', ''))
  109. ) {
  110. this.element.parents('[data-collapsible=true]').collapsible('forceActivate');
  111. if (!this.options.disabled) {
  112. this.options.active = true;
  113. if (this.options.saveState) { //eslint-disable-line max-depth
  114. this.storage.set(this.stateKey, true);
  115. }
  116. }
  117. } else if (this.options.saveState && !this.options.disabled) {
  118. state = this.storage.get(this.stateKey);
  119. if (typeof state === 'undefined' || state === null) {
  120. this.storage.set(this.stateKey, this.options.active);
  121. } else if (state === true) {
  122. this.options.active = true;
  123. } else if (state === false) {
  124. this.options.active = false;
  125. }
  126. }
  127. },
  128. /**
  129. * @private
  130. */
  131. _createIcons: function () {
  132. var icons = this.options.icons;
  133. if (icons) {
  134. $('<span>')
  135. .addClass(icons.header)
  136. .attr('data-role', 'icons')
  137. .prependTo(this.header);
  138. if (this.options.active && !this.options.disabled) {
  139. this.header.children('[data-role=icons]')
  140. .removeClass(icons.header)
  141. .addClass(icons.activeHeader);
  142. }
  143. }
  144. },
  145. /**
  146. * @private
  147. */
  148. _destroyIcons: function () {
  149. this.header
  150. .children('[data-role=icons]')
  151. .remove();
  152. },
  153. /**
  154. * @private
  155. */
  156. _destroy: function () {
  157. var options = this.options;
  158. this.element.removeAttr('data-collapsible');
  159. this.trigger.removeAttr('tabIndex');
  160. if (options.openedState) {
  161. this.element.removeClass(options.openedState);
  162. }
  163. if (this.options.collateral.element && this.options.collateral.openedState) {
  164. $(this.options.collateral.element).removeClass(this.options.collateral.openedState);
  165. }
  166. if (options.closedState) {
  167. this.element.removeClass(options.closedState);
  168. }
  169. if (options.disabledState) {
  170. this.element.removeClass(options.disabledState);
  171. }
  172. if (this.icons) {
  173. this._destroyIcons();
  174. }
  175. },
  176. /**
  177. * @private
  178. */
  179. _processPanels: function () {
  180. var headers, triggers;
  181. this.element.attr('data-collapsible', 'true');
  182. if (typeof this.options.header === 'object') {
  183. this.header = this.options.header;
  184. } else {
  185. headers = this.element.find(this.options.header);
  186. if (headers.length > 0) {
  187. this.header = headers.eq(0);
  188. } else {
  189. this.header = this.element;
  190. }
  191. }
  192. if (typeof this.options.content === 'object') {
  193. this.content = this.options.content;
  194. } else {
  195. this.content = this.header.next(this.options.content).eq(0);
  196. }
  197. // ARIA (init aria attributes)
  198. if (this.header.attr('id')) {
  199. this.content.attr('aria-labelledby', this.header.attr('id'));
  200. }
  201. if (this.content.attr('id')) {
  202. this.header.attr('aria-controls', this.content.attr('id'));
  203. }
  204. this.header
  205. .attr({
  206. 'role': 'tab',
  207. 'aria-selected': this.options.active,
  208. 'aria-expanded': this.options.active
  209. });
  210. // For collapsible widget only (not tabs or accordion)
  211. if (this.header.parent().attr('role') !== 'presentation') {
  212. this.header
  213. .parent()
  214. .attr('role', 'tablist');
  215. }
  216. this.content.attr({
  217. 'role': 'tabpanel',
  218. 'aria-hidden': !this.options.active
  219. });
  220. if (typeof this.options.trigger === 'object') {
  221. this.trigger = this.options.trigger;
  222. } else {
  223. triggers = this.header.find(this.options.trigger);
  224. if (triggers.length > 0) {
  225. this.trigger = triggers.eq(0);
  226. } else {
  227. this.trigger = this.header;
  228. }
  229. }
  230. },
  231. /**
  232. * @param {jQuery.Event} event
  233. * @private
  234. */
  235. _keydown: function (event) {
  236. var keyCode;
  237. if (event.altKey || event.ctrlKey) {
  238. return;
  239. }
  240. keyCode = $.ui.keyCode;
  241. switch (event.keyCode) {
  242. case keyCode.SPACE:
  243. case keyCode.ENTER:
  244. this._eventHandler(event);
  245. break;
  246. }
  247. },
  248. /**
  249. * @param {jQuery.Event} event
  250. * @private
  251. */
  252. _bind: function (event) {
  253. var self = this;
  254. this.events = {
  255. keydown: '_keydown'
  256. };
  257. if (event) {
  258. $.each(event.split(' '), function (index, eventName) {
  259. self.events[eventName] = '_eventHandler';
  260. });
  261. }
  262. this._off(this.trigger);
  263. if (!this.options.disabled) {
  264. this._on(this.trigger, this.events);
  265. }
  266. },
  267. /**
  268. * Disable.
  269. */
  270. disable: function () {
  271. this.options.disabled = true;
  272. this._off(this.trigger);
  273. this.forceDeactivate();
  274. if (this.options.disabledState) {
  275. this.element.addClass(this.options.disabledState);
  276. }
  277. this.trigger.attr('tabIndex', -1);
  278. },
  279. /**
  280. * Enable.
  281. */
  282. enable: function () {
  283. this.options.disabled = false;
  284. this._on(this.trigger, this.events);
  285. this.forceActivate();
  286. if (this.options.disabledState) {
  287. this.element.removeClass(this.options.disabledState);
  288. }
  289. this.trigger.attr('tabIndex', 0);
  290. },
  291. /**
  292. * @param {jQuery.Event} event
  293. * @private
  294. */
  295. _eventHandler: function (event) {
  296. if (this.options.active && this.options.collapsible) {
  297. this.deactivate();
  298. } else {
  299. this.activate();
  300. }
  301. event.preventDefault();
  302. },
  303. /**
  304. * @param {*} prop
  305. * @private
  306. */
  307. _animate: function (prop) {
  308. var duration,
  309. easing,
  310. animate = this.options.animate;
  311. if (typeof animate === 'number') {
  312. duration = animate;
  313. }
  314. if (typeof animate === 'string') {
  315. animate = JSON.parse(animate);
  316. }
  317. duration = duration || animate.duration;
  318. easing = animate.easing;
  319. this.content.animate(prop, duration, easing);
  320. },
  321. /**
  322. * Deactivate.
  323. */
  324. deactivate: function () {
  325. if (this.options.animate) {
  326. this._animate(hideProps);
  327. } else {
  328. this.content.hide();
  329. }
  330. this._close();
  331. },
  332. /**
  333. * Force deactivate.
  334. */
  335. forceDeactivate: function () {
  336. this.content.hide();
  337. this._close();
  338. },
  339. /**
  340. * @private
  341. */
  342. _close: function () {
  343. this.options.active = false;
  344. if (this.options.saveState) {
  345. this.storage.set(this.stateKey, false);
  346. }
  347. if (this.options.openedState) {
  348. this.element.removeClass(this.options.openedState);
  349. }
  350. if (this.options.collateral.element && this.options.collateral.openedState) {
  351. $(this.options.collateral.element).removeClass(this.options.collateral.openedState);
  352. }
  353. if (this.options.closedState) {
  354. this.element.addClass(this.options.closedState);
  355. }
  356. if (this.icons) {
  357. this.header.children('[data-role=icons]')
  358. .removeClass(this.options.icons.activeHeader)
  359. .addClass(this.options.icons.header);
  360. }
  361. // ARIA (updates aria attributes)
  362. this.header.attr({
  363. 'aria-selected': 'false',
  364. 'aria-expanded': 'false'
  365. });
  366. this.content.attr({
  367. 'aria-hidden': 'true'
  368. });
  369. this.element.trigger('dimensionsChanged', {
  370. opened: false
  371. });
  372. },
  373. /**
  374. * Activate.
  375. *
  376. * @return void;
  377. */
  378. activate: function () {
  379. if (this.options.disabled) {
  380. return;
  381. }
  382. if (this.options.animate) {
  383. this._animate(showProps);
  384. } else {
  385. this.content.show();
  386. }
  387. this._open();
  388. },
  389. /**
  390. * Force activate.
  391. */
  392. forceActivate: function () {
  393. if (!this.options.disabled) {
  394. this.content.show();
  395. this._open();
  396. }
  397. },
  398. /**
  399. * @private
  400. */
  401. _open: function () {
  402. this.element.trigger('beforeOpen');
  403. this.options.active = true;
  404. if (this.options.ajaxContent) {
  405. this._loadContent();
  406. }
  407. if (this.options.saveState) {
  408. this.storage.set(this.stateKey, true);
  409. }
  410. if (this.options.openedState) {
  411. this.element.addClass(this.options.openedState);
  412. }
  413. if (this.options.collateral.element && this.options.collateral.openedState) {
  414. $(this.options.collateral.element).addClass(this.options.collateral.openedState);
  415. }
  416. if (this.options.closedState) {
  417. this.element.removeClass(this.options.closedState);
  418. }
  419. if (this.icons) {
  420. this.header.children('[data-role=icons]')
  421. .removeClass(this.options.icons.header)
  422. .addClass(this.options.icons.activeHeader);
  423. }
  424. // ARIA (updates aria attributes)
  425. this.header.attr({
  426. 'aria-selected': 'true',
  427. 'aria-expanded': 'true'
  428. });
  429. this.content.attr({
  430. 'aria-hidden': 'false'
  431. });
  432. this.element.trigger('dimensionsChanged', {
  433. opened: true
  434. });
  435. },
  436. /**
  437. * @private
  438. */
  439. _loadContent: function () {
  440. var url = this.element.find(this.options.ajaxUrlElement).attr('href'),
  441. that = this;
  442. if (url) {
  443. that.xhr = $.get({
  444. url: url,
  445. dataType: 'html'
  446. }, function () {
  447. });
  448. }
  449. if (that.xhr && that.xhr.statusText !== 'canceled') {
  450. if (that.options.loadingClass) {
  451. that.element.addClass(that.options.loadingClass);
  452. }
  453. that.content.attr('aria-busy', 'true');
  454. that.xhr.done(function (response) {
  455. setTimeout(function () {
  456. that.content.html(response);
  457. }, 1);
  458. });
  459. that.xhr.always(function (jqXHR, status) {
  460. setTimeout(function () {
  461. if (status === 'abort') {
  462. that.content.stop(false, true);
  463. }
  464. if (that.options.loadingClass) {
  465. that.element.removeClass(that.options.loadingClass);
  466. }
  467. that.content.removeAttr('aria-busy');
  468. if (jqXHR === that.xhr) {
  469. delete that.xhr;
  470. }
  471. }, 1);
  472. });
  473. }
  474. },
  475. /**
  476. * @private
  477. */
  478. _scrollToTopIfNotVisible: function () {
  479. if (this._isElementOutOfViewport()) {
  480. this.header[0].scrollIntoView();
  481. }
  482. },
  483. /**
  484. * @private
  485. * @return {Boolean}
  486. */
  487. _isElementOutOfViewport: function () {
  488. var headerRect = this.header[0].getBoundingClientRect(),
  489. contentRect = this.content.get().length ? this.content[0].getBoundingClientRect() : false,
  490. headerOut,
  491. contentOut;
  492. headerOut = headerRect.bottom - headerRect.height < 0 ||
  493. headerRect.right - headerRect.width < 0 ||
  494. headerRect.left + headerRect.width > window.innerWidth ||
  495. headerRect.top + headerRect.height > window.innerHeight;
  496. contentOut = contentRect ? contentRect.bottom - contentRect.height < 0 ||
  497. contentRect.right - contentRect.width < 0 ||
  498. contentRect.left + contentRect.width > window.innerWidth ||
  499. contentRect.top + contentRect.height > window.innerHeight : false;
  500. return headerOut ? headerOut : contentOut;
  501. }
  502. });
  503. return $.mage.collapsible;
  504. });