604 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			604 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
| /**
 | |
|  * Copyright © Magento, Inc. All rights reserved.
 | |
|  * See COPYING.txt for license details.
 | |
|  */
 | |
| 
 | |
| define([
 | |
|     'jquery',
 | |
|     'jquery-ui-modules/widget',
 | |
|     'jquery-ui-modules/core',
 | |
|     'jquery/jquery-storageapi',
 | |
|     'mage/mage'
 | |
| ], function ($) {
 | |
|     'use strict';
 | |
| 
 | |
|     var hideProps = {},
 | |
|         showProps = {};
 | |
| 
 | |
|     hideProps.height = 'hide';
 | |
|     showProps.height = 'show';
 | |
| 
 | |
|     $.widget('mage.collapsible', {
 | |
|         options: {
 | |
|             active: false,
 | |
|             disabled: false,
 | |
|             collapsible: true,
 | |
|             header: '[data-role=title]',
 | |
|             content: '[data-role=content]',
 | |
|             trigger: '[data-role=trigger]',
 | |
|             closedState: null,
 | |
|             openedState: null,
 | |
|             disabledState: null,
 | |
|             ajaxUrlElement: '[data-ajax=true]',
 | |
|             ajaxContent: false,
 | |
|             loadingClass: null,
 | |
|             saveState: false,
 | |
|             animate: false,
 | |
|             icons: {
 | |
|                 activeHeader: null,
 | |
|                 header: null
 | |
|             },
 | |
|             collateral: {
 | |
|                 element: null,
 | |
|                 openedState: null
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _create: function () {
 | |
|             this.storage = $.localStorage;
 | |
|             this.icons = false;
 | |
| 
 | |
|             if (typeof this.options.icons === 'string') {
 | |
|                 this.options.icons = JSON.parse(this.options.icons);
 | |
|             }
 | |
| 
 | |
|             this._processPanels();
 | |
|             this._processState();
 | |
|             this._refresh();
 | |
| 
 | |
|             if (this.options.icons.header && this.options.icons.activeHeader) {
 | |
|                 this._createIcons();
 | |
|                 this.icons = true;
 | |
|             }
 | |
| 
 | |
|             this.element.on('dimensionsChanged', function (e) {
 | |
|                 if (e.target && e.target.classList.contains('active')) {
 | |
|                     this._scrollToTopIfNotVisible();
 | |
|                 }
 | |
|             }.bind(this));
 | |
| 
 | |
|             this._bind('click');
 | |
|             this._trigger('created');
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _refresh: function () {
 | |
|             this.trigger.attr('tabIndex', 0);
 | |
| 
 | |
|             if (this.options.active && !this.options.disabled) {
 | |
|                 if (this.options.openedState) {
 | |
|                     this.element.addClass(this.options.openedState);
 | |
|                 }
 | |
| 
 | |
|                 if (this.options.collateral.element && this.options.collateral.openedState) {
 | |
|                     $(this.options.collateral.element).addClass(this.options.collateral.openedState);
 | |
|                 }
 | |
| 
 | |
|                 if (this.options.ajaxContent) {
 | |
|                     this._loadContent();
 | |
|                 }
 | |
|                 // ARIA (updates aria attributes)
 | |
|                 this.header.attr({
 | |
|                     'aria-selected': false
 | |
|                 });
 | |
|             } else if (this.options.disabled) {
 | |
|                 this.disable();
 | |
|             } else {
 | |
|                 this.content.hide();
 | |
| 
 | |
|                 if (this.options.closedState) {
 | |
|                     this.element.addClass(this.options.closedState);
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Processing the state:
 | |
|          *     If deep linking is used and the anchor is the id of the content or the content contains this id,
 | |
|          *     and the collapsible element is a nested one having collapsible parents, in order to see the content,
 | |
|          *     all the parents must be expanded.
 | |
|          * @private
 | |
|          */
 | |
|         _processState: function () {
 | |
|             var anchor = window.location.hash,
 | |
|                 isValid = $.mage.isValidSelector(anchor),
 | |
|                 urlPath = window.location.pathname.replace(/\./g, ''),
 | |
|                 state;
 | |
| 
 | |
|             this.stateKey = encodeURIComponent(urlPath + this.element.attr('id'));
 | |
| 
 | |
|             if (isValid &&
 | |
|                 ($(this.content.find(anchor)).length > 0 || this.content.attr('id') === anchor.replace('#', ''))
 | |
|             ) {
 | |
|                 this.element.parents('[data-collapsible=true]').collapsible('forceActivate');
 | |
| 
 | |
|                 if (!this.options.disabled) {
 | |
|                     this.options.active = true;
 | |
| 
 | |
|                     if (this.options.saveState) { //eslint-disable-line max-depth
 | |
|                         this.storage.set(this.stateKey, true);
 | |
|                     }
 | |
|                 }
 | |
|             } else if (this.options.saveState && !this.options.disabled) {
 | |
|                 state = this.storage.get(this.stateKey);
 | |
| 
 | |
|                 if (typeof state === 'undefined' || state === null) {
 | |
|                     this.storage.set(this.stateKey, this.options.active);
 | |
|                 } else if (state === true) {
 | |
|                     this.options.active = true;
 | |
|                 } else if (state === false) {
 | |
|                     this.options.active = false;
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _createIcons: function () {
 | |
|             var icons = this.options.icons;
 | |
| 
 | |
|             if (icons) {
 | |
|                 $('<span>')
 | |
|                     .addClass(icons.header)
 | |
|                     .attr('data-role', 'icons')
 | |
|                     .prependTo(this.header);
 | |
| 
 | |
|                 if (this.options.active && !this.options.disabled) {
 | |
|                     this.header.children('[data-role=icons]')
 | |
|                         .removeClass(icons.header)
 | |
|                         .addClass(icons.activeHeader);
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _destroyIcons: function () {
 | |
|             this.header
 | |
|                 .children('[data-role=icons]')
 | |
|                 .remove();
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _destroy: function () {
 | |
|             var options = this.options;
 | |
| 
 | |
|             this.element.removeAttr('data-collapsible');
 | |
| 
 | |
|             this.trigger.removeAttr('tabIndex');
 | |
| 
 | |
|             if (options.openedState) {
 | |
|                 this.element.removeClass(options.openedState);
 | |
|             }
 | |
| 
 | |
|             if (this.options.collateral.element && this.options.collateral.openedState) {
 | |
|                 $(this.options.collateral.element).removeClass(this.options.collateral.openedState);
 | |
|             }
 | |
| 
 | |
|             if (options.closedState) {
 | |
|                 this.element.removeClass(options.closedState);
 | |
|             }
 | |
| 
 | |
|             if (options.disabledState) {
 | |
|                 this.element.removeClass(options.disabledState);
 | |
|             }
 | |
| 
 | |
|             if (this.icons) {
 | |
|                 this._destroyIcons();
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _processPanels: function () {
 | |
|             var headers, triggers;
 | |
| 
 | |
|             this.element.attr('data-collapsible', 'true');
 | |
| 
 | |
|             if (typeof this.options.header === 'object') {
 | |
|                 this.header = this.options.header;
 | |
|             } else {
 | |
|                 headers = this.element.find(this.options.header);
 | |
| 
 | |
|                 if (headers.length > 0) {
 | |
|                     this.header = headers.eq(0);
 | |
|                 } else {
 | |
|                     this.header = this.element;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (typeof this.options.content === 'object') {
 | |
|                 this.content = this.options.content;
 | |
|             } else {
 | |
|                 this.content = this.header.next(this.options.content).eq(0);
 | |
|             }
 | |
| 
 | |
|             // ARIA (init aria attributes)
 | |
|             if (this.header.attr('id')) {
 | |
|                 this.content.attr('aria-labelledby', this.header.attr('id'));
 | |
|             }
 | |
| 
 | |
|             if (this.content.attr('id')) {
 | |
|                 this.header.attr('aria-controls', this.content.attr('id'));
 | |
|             }
 | |
| 
 | |
|             this.header
 | |
|                 .attr({
 | |
|                     'role': 'tab',
 | |
|                     'aria-selected': this.options.active,
 | |
|                     'aria-expanded': this.options.active
 | |
|                 });
 | |
| 
 | |
|             // For collapsible widget only (not tabs or accordion)
 | |
|             if (this.header.parent().attr('role') !== 'presentation') {
 | |
|                 this.header
 | |
|                     .parent()
 | |
|                     .attr('role', 'tablist');
 | |
|             }
 | |
| 
 | |
|             this.content.attr({
 | |
|                 'role': 'tabpanel',
 | |
|                 'aria-hidden': !this.options.active
 | |
|             });
 | |
| 
 | |
|             if (typeof this.options.trigger === 'object') {
 | |
|                 this.trigger = this.options.trigger;
 | |
|             } else {
 | |
|                 triggers = this.header.find(this.options.trigger);
 | |
| 
 | |
|                 if (triggers.length > 0) {
 | |
|                     this.trigger = triggers.eq(0);
 | |
|                 } else {
 | |
|                     this.trigger = this.header;
 | |
|                 }
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @param {jQuery.Event} event
 | |
|          * @private
 | |
|          */
 | |
|         _keydown: function (event) {
 | |
|             var keyCode;
 | |
| 
 | |
|             if (event.altKey || event.ctrlKey) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             keyCode = $.ui.keyCode;
 | |
| 
 | |
|             switch (event.keyCode) {
 | |
|                 case keyCode.SPACE:
 | |
|                 case keyCode.ENTER:
 | |
|                     this._eventHandler(event);
 | |
|                     break;
 | |
|             }
 | |
| 
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @param {jQuery.Event} event
 | |
|          * @private
 | |
|          */
 | |
|         _bind: function (event) {
 | |
|             var self = this;
 | |
| 
 | |
|             this.events = {
 | |
|                 keydown: '_keydown'
 | |
|             };
 | |
| 
 | |
|             if (event) {
 | |
|                 $.each(event.split(' '), function (index, eventName) {
 | |
|                     self.events[eventName] = '_eventHandler';
 | |
|                 });
 | |
|             }
 | |
|             this._off(this.trigger);
 | |
| 
 | |
|             if (!this.options.disabled) {
 | |
|                 this._on(this.trigger, this.events);
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Disable.
 | |
|          */
 | |
|         disable: function () {
 | |
|             this.options.disabled = true;
 | |
|             this._off(this.trigger);
 | |
|             this.forceDeactivate();
 | |
| 
 | |
|             if (this.options.disabledState) {
 | |
|                 this.element.addClass(this.options.disabledState);
 | |
|             }
 | |
|             this.trigger.attr('tabIndex', -1);
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Enable.
 | |
|          */
 | |
|         enable: function () {
 | |
|             this.options.disabled = false;
 | |
|             this._on(this.trigger, this.events);
 | |
|             this.forceActivate();
 | |
| 
 | |
|             if (this.options.disabledState) {
 | |
|                 this.element.removeClass(this.options.disabledState);
 | |
|             }
 | |
|             this.trigger.attr('tabIndex', 0);
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @param {jQuery.Event} event
 | |
|          * @private
 | |
|          */
 | |
|         _eventHandler: function (event) {
 | |
| 
 | |
|             if (this.options.active && this.options.collapsible) {
 | |
|                 this.deactivate();
 | |
|             } else {
 | |
|                 this.activate();
 | |
| 
 | |
|             }
 | |
|             event.preventDefault();
 | |
| 
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @param {*} prop
 | |
|          * @private
 | |
|          */
 | |
|         _animate: function (prop) {
 | |
|             var duration,
 | |
|                 easing,
 | |
|                 animate = this.options.animate;
 | |
| 
 | |
|             if (typeof animate === 'number') {
 | |
|                 duration = animate;
 | |
|             }
 | |
| 
 | |
|             if (typeof animate === 'string') {
 | |
|                 animate = JSON.parse(animate);
 | |
|             }
 | |
|             duration = duration || animate.duration;
 | |
|             easing = animate.easing;
 | |
|             this.content.animate(prop, duration, easing);
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Deactivate.
 | |
|          */
 | |
|         deactivate: function () {
 | |
|             if (this.options.animate) {
 | |
|                 this._animate(hideProps);
 | |
|             } else {
 | |
|                 this.content.hide();
 | |
|             }
 | |
|             this._close();
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Force deactivate.
 | |
|          */
 | |
|         forceDeactivate: function () {
 | |
|             this.content.hide();
 | |
|             this._close();
 | |
| 
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _close: function () {
 | |
|             this.options.active = false;
 | |
| 
 | |
|             if (this.options.saveState) {
 | |
|                 this.storage.set(this.stateKey, false);
 | |
|             }
 | |
| 
 | |
|             if (this.options.openedState) {
 | |
|                 this.element.removeClass(this.options.openedState);
 | |
|             }
 | |
| 
 | |
|             if (this.options.collateral.element && this.options.collateral.openedState) {
 | |
|                 $(this.options.collateral.element).removeClass(this.options.collateral.openedState);
 | |
|             }
 | |
| 
 | |
|             if (this.options.closedState) {
 | |
|                 this.element.addClass(this.options.closedState);
 | |
|             }
 | |
| 
 | |
|             if (this.icons) {
 | |
|                 this.header.children('[data-role=icons]')
 | |
|                     .removeClass(this.options.icons.activeHeader)
 | |
|                     .addClass(this.options.icons.header);
 | |
|             }
 | |
| 
 | |
|             // ARIA (updates aria attributes)
 | |
|             this.header.attr({
 | |
|                 'aria-selected': 'false',
 | |
|                 'aria-expanded': 'false'
 | |
|             });
 | |
|             this.content.attr({
 | |
|                 'aria-hidden': 'true'
 | |
|             });
 | |
| 
 | |
|             this.element.trigger('dimensionsChanged', {
 | |
|                 opened: false
 | |
|             });
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Activate.
 | |
|          *
 | |
|          * @return void;
 | |
|          */
 | |
|         activate: function () {
 | |
|             if (this.options.disabled) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if (this.options.animate) {
 | |
|                 this._animate(showProps);
 | |
|             } else {
 | |
|                 this.content.show();
 | |
|             }
 | |
|             this._open();
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * Force activate.
 | |
|          */
 | |
|         forceActivate: function () {
 | |
|             if (!this.options.disabled) {
 | |
|                 this.content.show();
 | |
|                 this._open();
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _open: function () {
 | |
|             this.element.trigger('beforeOpen');
 | |
|             this.options.active = true;
 | |
| 
 | |
|             if (this.options.ajaxContent) {
 | |
|                 this._loadContent();
 | |
|             }
 | |
| 
 | |
|             if (this.options.saveState) {
 | |
|                 this.storage.set(this.stateKey, true);
 | |
|             }
 | |
| 
 | |
|             if (this.options.openedState) {
 | |
|                 this.element.addClass(this.options.openedState);
 | |
|             }
 | |
| 
 | |
|             if (this.options.collateral.element && this.options.collateral.openedState) {
 | |
|                 $(this.options.collateral.element).addClass(this.options.collateral.openedState);
 | |
|             }
 | |
| 
 | |
|             if (this.options.closedState) {
 | |
|                 this.element.removeClass(this.options.closedState);
 | |
|             }
 | |
| 
 | |
|             if (this.icons) {
 | |
|                 this.header.children('[data-role=icons]')
 | |
|                     .removeClass(this.options.icons.header)
 | |
|                     .addClass(this.options.icons.activeHeader);
 | |
|             }
 | |
| 
 | |
|             // ARIA (updates aria attributes)
 | |
|             this.header.attr({
 | |
|                 'aria-selected': 'true',
 | |
|                 'aria-expanded': 'true'
 | |
|             });
 | |
|             this.content.attr({
 | |
|                 'aria-hidden': 'false'
 | |
|             });
 | |
| 
 | |
|             this.element.trigger('dimensionsChanged', {
 | |
|                 opened: true
 | |
|             });
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _loadContent: function () {
 | |
|             var url = this.element.find(this.options.ajaxUrlElement).attr('href'),
 | |
|                 that = this;
 | |
| 
 | |
|             if (url) {
 | |
|                 that.xhr = $.get({
 | |
|                     url: url,
 | |
|                     dataType: 'html'
 | |
|                 }, function () {
 | |
|                 });
 | |
|             }
 | |
| 
 | |
|             if (that.xhr && that.xhr.statusText !== 'canceled') {
 | |
|                 if (that.options.loadingClass) {
 | |
|                     that.element.addClass(that.options.loadingClass);
 | |
|                 }
 | |
|                 that.content.attr('aria-busy', 'true');
 | |
|                 that.xhr.done(function (response) {
 | |
|                     setTimeout(function () {
 | |
|                         that.content.html(response);
 | |
|                     }, 1);
 | |
|                 });
 | |
|                 that.xhr.always(function (jqXHR, status) {
 | |
|                     setTimeout(function () {
 | |
|                         if (status === 'abort') {
 | |
|                             that.content.stop(false, true);
 | |
|                         }
 | |
| 
 | |
|                         if (that.options.loadingClass) {
 | |
|                             that.element.removeClass(that.options.loadingClass);
 | |
|                         }
 | |
|                         that.content.removeAttr('aria-busy');
 | |
| 
 | |
|                         if (jqXHR === that.xhr) {
 | |
|                             delete that.xhr;
 | |
|                         }
 | |
|                     }, 1);
 | |
|                 });
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          */
 | |
|         _scrollToTopIfNotVisible: function () {
 | |
|             if (this._isElementOutOfViewport()) {
 | |
|                 this.header[0].scrollIntoView();
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         /**
 | |
|          * @private
 | |
|          * @return {Boolean}
 | |
|          */
 | |
|         _isElementOutOfViewport: function () {
 | |
|             var headerRect = this.header[0].getBoundingClientRect(),
 | |
|                 contentRect = this.content.get().length ? this.content[0].getBoundingClientRect() : false,
 | |
|                 headerOut,
 | |
|                 contentOut;
 | |
| 
 | |
|             headerOut = headerRect.bottom - headerRect.height < 0 ||
 | |
|                 headerRect.right - headerRect.width < 0 ||
 | |
|                 headerRect.left + headerRect.width > window.innerWidth ||
 | |
|                 headerRect.top + headerRect.height > window.innerHeight;
 | |
| 
 | |
|             contentOut = contentRect ? contentRect.bottom - contentRect.height < 0 ||
 | |
|                 contentRect.right - contentRect.width < 0 ||
 | |
|                 contentRect.left + contentRect.width > window.innerWidth ||
 | |
|                 contentRect.top + contentRect.height > window.innerHeight : false;
 | |
| 
 | |
|             return headerOut ? headerOut : contentOut;
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     return $.mage.collapsible;
 | |
| });
 |