;(function($) {

	'use strict';

	var PPOffcanvasContent = function( $scope ) {

		this.node                   = $scope;
		this.wrap                   = $scope.find('.pp-offcanvas-content-wrap');
		this.cart_wrap              = $scope.find('.pp-offcanvas-cart-container');
		this.content                = $scope.find('.pp-offcanvas-content');
		this.button                 = $scope.find('.pp-offcanvas-toggle');
		this.settings               = this.wrap.data('settings');
		this.scopeUniqueId          = this.getOffcanvasUniqueScopeId( $scope );
		this.toggle_source          = this.settings.toggle_source;
		this.id                     = this.scopeUniqueId;
		this.toggle_id              = this.settings.toggle_id;
		this.toggle_class           = this.settings.toggle_class;
		this.transition             = this.settings.transition;
		this.esc_close              = this.settings.esc_close;
		this.body_click_close       = this.settings.body_click_close;
		this.links_click_close      = this.settings.links_click_close;
		this.editor_preview         = this.settings.editor_preview;
		this.direction              = this.settings.direction;
		this.buttons_position       = this.settings.buttons_position;
		this.open_panel_add_to_cart = this.settings.open_panel_add_to_cart;
		this.duration               = 500;

		this.destroy();
		this.init();
	};

	PPOffcanvasContent.prototype = {
		id: '',
		node: '',
		wrap: '',
		content: '',
		button: '',
		settings: {},
		transition: '',
		duration: 400,
		initialized: false,
		animations: [
			'slide',
			'slide-along',
			'reveal',
			'push',
		],

		init: function () {
			if ( ! this.wrap.length ) {
				return;
			}

			$('html').addClass('pp-offcanvas-content-widget');

			if ( $('.pp-offcanvas-container').length === 0 ) {
				var faProJs = $('#font-awesome-pro-js').length > 0 ? $('#font-awesome-pro-js').attr('src') : false,
					faScript;

				if ( faProJs ) {
					$('#font-awesome-pro-js').remove();
				}
				$('body').wrapInner( '<div class="pp-offcanvas-container" />' );
				this.content.insertBefore('.pp-offcanvas-container');
				$('.pp-offcanvas-' + this.id).removeClass('pp-offcanvas-hide');
				$('.pp-offcanvas-' + this.id).removeAttr('style');

				if ( faProJs ) {
					// Built as a node rather than concatenated into a markup
					// string. The src is read back off the page, but a script tag
					// assembled by string concatenation is a code execution sink
					// by construction, and one quote in that URL would break out
					// of the attribute.
					faScript = document.createElement('script');
					faScript.type = 'text/javascript';
					faScript.id = 'font-awesome-pro-js';
					faScript.src = faProJs;
					document.body.appendChild( faScript );
				}
			}

			if ( this.wrap.find('.pp-offcanvas-content').length > 0 ) {
				if ( $('.pp-offcanvas-container > .pp-offcanvas-' + this.id).length > 0 ) {
					$('.pp-offcanvas-container > .pp-offcanvas-' + this.id).remove();
				}
				if ( $('body > .pp-offcanvas-' + this.id ).length > 0 ) {
					$('body > .pp-offcanvas-' + this.id ).remove();
				}
				$('body').prepend( this.wrap.find('.pp-offcanvas-content') );
				$('.pp-offcanvas-' + this.id).removeClass('pp-offcanvas-hide');
				$('.pp-offcanvas-' + this.id).removeAttr('style');
			}

			this.bindEvents();
		},

		getOffcanvasUniqueScopeId: function( $scope ) {
			var scopeId = $scope.data('id'),
				scopeIdCloned,
				$clones = $( '[data-id="' + scopeId + '"]' );

			if ( ! this.hasMultipleScopeId( scopeId ) ) {
				return scopeId;
			}
			scopeIdCloned = scopeId;

			$clones.each( function( index ) {
				$(this).attr( 'data-pp-offcanvas-index', index );
			});

			scopeId = scopeId + '_' + $scope.data('pp-offcanvas-index');

			$( '#pp-offcanvas-' + scopeIdCloned ).each( function() {
				$( this ).attr( 'id', 'pp-offcanvas-' + scopeId );

				if ( $( this ).hasClass( 'pp-offcanvas-' + scopeIdCloned ) ) {
					$( this ).addClass( 'pp-offcanvas-' + scopeId );
				}
			} );

			// The toggle points at the panel by id, so it has to follow the rename
			// or it ends up referencing an element that no longer exists.
			$scope.find( '[aria-controls="pp-offcanvas-' + scopeIdCloned + '"]' )
				.attr( 'aria-controls', 'pp-offcanvas-' + scopeId );

			return scopeId;
		},

		hasMultipleScopeId : function( scopeId ) {
			var $clones = $( '[data-id="' + scopeId + '"]' );

			if ( 1 === $clones.length ) {
				return false;
			}

			return true;
		},

		/**
		 * Event namespace for this widget's bindings.
		 *
		 * Derived from the widget id, so it is stable across re-inits and one
		 * instance can never unbind another's handlers.
		 */
		getNamespace: function() {
			return '.ppOffcanvas' + this.id;
		},

		destroy: function() {
			var ns = this.getNamespace();

			this.close();

			this.animations.forEach(function( animation ) {
				if ( $('html').hasClass( 'pp-offcanvas-content-' + animation ) ) {
					$('html').removeClass( 'pp-offcanvas-content-' + animation );
				}
			});

			// Elementor re-runs element_ready on every editor re-render, and this
			// constructor calls destroy() before init(). Without the unbinding
			// below every re-render stacked another set of handlers: the toggle
			// ended up firing show() and close() in the same click, so the panel
			// stopped opening at all after the first re-render.
			$(window).off( ns );
			$(document).off( ns );
			$(document.body).off( ns );
			this.setTrigger().off( ns );
			this.getPanel().off( ns );

			if ( this._resizeFrame ) {
				cancelAnimationFrame( this._resizeFrame );
				this._resizeFrame = null;
			}
		},

		setTrigger: function() {
			var $trigger = false;

			if ( this.toggle_source == 'element-id' && this.toggle_id != '' ) {
				$trigger = $( '#' + this.toggle_id );
			} else if ( this.toggle_source == 'element-class' && this.toggle_class != '' ) {
				$trigger = $( '.' + this.toggle_class );
			} else {
				$trigger = this.node.find( '.pp-offcanvas-toggle' );
			}
			
			return $trigger;
		},

		bindEvents: function () {
			var self = this,
				ns = this.getNamespace(),
				$trigger = this.setTrigger();

			if ( $trigger ) {
				$trigger.on('click' + ns, $.proxy( this.toggleContent, this ));
			}

			if( 'yes' === this.open_panel_add_to_cart ) {
				// Listen for the cart actually changing rather than for a click on
				// a button collected at init. That fixed set missed every button
				// added later - AJAX pagination, quick view - and the fixed 1000ms
				// timeout that followed it was a guess at when the fragments would
				// land. These events all fire after the new cart contents are in.
				// show() rather than toggleContent(): adding a second item while
				// the panel was open used to close it.
				$( document.body ).on(
					'added_to_cart' + ns + ' pp_added_to_cart' + ns + ' pp_product_actions_added_to_cart' + ns,
					function() {
						self.show();
					}
				);
			}

			$('body').on( 'click' + ns, '.pp-offcanvas-content .pp-offcanvas-close', function() {
				if ( self.editor_preview === 'yes' ) {
					self._suppressShow = true;
				}
				self.close();
			});

			// Nav Menu accordion: indicator-icon click toggles submenu (both trigger modes)
			$('body').on(
				'click' + ns,
				'.pp-offcanvas-' + this.id + ' .pp-offcanvas-menu-accordion .pp-offcanvas-menu-indicator',
				function(e) {
					e.preventDefault();
					e.stopImmediatePropagation();
					$(this).closest('.pp-offcanvas-menu-item-has-children').toggleClass('pp-offcanvas-submenu-open');
				}
			);

			// Nav Menu accordion: indicator keyboard activation (Enter/Space)
			$('body').on(
				'keydown' + ns,
				'.pp-offcanvas-' + this.id + ' .pp-offcanvas-menu-accordion .pp-offcanvas-menu-indicator',
				function(e) {
					if (e.which === 13 || e.which === 32) {
						e.preventDefault();
						e.stopImmediatePropagation();
						$(this).closest('.pp-offcanvas-menu-item-has-children').toggleClass('pp-offcanvas-submenu-open');
					}
				}
			);

			// Nav Menu accordion: parent-item link click toggles submenu (trigger=item)
			$('body').on(
				'click' + ns,
				'.pp-offcanvas-' + this.id + ' .pp-offcanvas-menu-accordion.pp-offcanvas-menu-trigger-item .pp-offcanvas-menu-item-has-children > a',
				function(e) {
					e.preventDefault();
					e.stopImmediatePropagation();
					$(this).parent().toggleClass('pp-offcanvas-submenu-open');
				}
			);

			if ( this.links_click_close === 'yes' ) {
				$('body').on( 'click' + ns, '.pp-offcanvas-content .pp-offcanvas-body a', $.proxy( this.close, this ) );
			}

			if ( this.esc_close === 'yes' && this.editor_preview !== 'yes' ) {
				this.closeESC();
			}
			if ( this.body_click_close === 'yes' && this.editor_preview !== 'yes' ) {
				this.closeClick();
			}
			$(window).on( 'resize' + ns, $.proxy( this._onResize, this ) );

			var prevObserver = this.node.data('pp-offcanvas-preview-observer');
			if ( prevObserver ) {
				prevObserver.disconnect();
				this.node.removeData('pp-offcanvas-preview-observer');
			}

			if ( this.editor_preview === 'yes' ) {
				this.bindEditorPreview();
			}
		},

		bindEditorPreview: function () {
			var self = this;
			var widgetEl = this.node && this.node[0];

			if ( ! widgetEl || typeof MutationObserver === 'undefined' ) {
				return;
			}

			var sync = function () {
				if ( widgetEl.classList.contains('elementor-element-editable') ) {
					if ( ! self._suppressShow ) {
						self.show();
					}
				} else {
					self._suppressShow = false;
					self.close();
				}
			};

			var observer = new MutationObserver(function (mutations) {
				for ( var i = 0; i < mutations.length; i++ ) {
					if ( mutations[i].attributeName === 'class' ) {
						sync();
						return;
					}
				}
			});
			observer.observe( widgetEl, { attributes: true, attributeFilter: ['class'] } );
			this.node.data('pp-offcanvas-preview-observer', observer);

			setTimeout( sync, 0 );
		},

		toggleContent: function(e) {
			e.preventDefault();

			if ( ! $('html').hasClass('pp-offcanvas-open') ) {
				this.show();
			} else {
				this.close();
			}

			this._resize();
		},

		/**
		 * The panel the widget owns, wherever init() has moved it to.
		 */
		getPanel: function() {
			return $('.pp-offcanvas-' + this.id);
		},

		/**
		 * Whether the panel declares itself a modal dialog.
		 *
		 * Only markup that opts in gets the focus handling below, so a widget
		 * whose panel is still a plain container behaves exactly as before.
		 */
		isDialog: function() {
			return 'dialog' === this.getPanel().attr('role');
		},

		show: function() {
			if ( this._closeTimer ) {
				clearTimeout( this._closeTimer );
				this._closeTimer = null;
			}

			this.getPanel().addClass('pp-offcanvas-visible');
			// init animation class.
			$('html').addClass( 'pp-offcanvas-content-' + this.transition );
			$('html').addClass( 'pp-offcanvas-' + this.direction );
			$('html').addClass('pp-offcanvas-open');
			$('html').addClass('pp-offcanvas-' + this.id + '-open');
			$('html').addClass('pp-offcanvas-reset');

			this.button.addClass('pp-is-active');
			this.setExpanded( true );

			this._resize();

			this.trapFocus();
		},

		close: function() {
			var wasOpen = $('html').hasClass('pp-offcanvas-' + this.id + '-open');

			$('html').removeClass('pp-offcanvas-open');
			$('html').removeClass('pp-offcanvas-' + this.id + '-open');

			if ( this._closeTimer ) {
				clearTimeout( this._closeTimer );
			}

			this._closeTimer = setTimeout($.proxy(function () {
				$('html').removeClass('pp-offcanvas-reset');
				$('html').removeClass( 'pp-offcanvas-content-' + this.transition );
				$('html').removeClass( 'pp-offcanvas-' + this.direction );
				this.getPanel().removeClass('pp-offcanvas-visible');
				this._closeTimer = null;
			}, this), 500);

			this.button.removeClass('pp-is-active');
			this.setExpanded( false );

			this.releaseFocus( wasOpen );
		},

		/**
		 * Mirror the open state onto the toggle's disclosure attribute.
		 *
		 * Only touches a toggle that already declares aria-expanded, so a toggle
		 * that is not a real button does not get given a state it cannot own.
		 */
		setExpanded: function( expanded ) {
			this.button.filter('[aria-expanded]').attr( 'aria-expanded', expanded ? 'true' : 'false' );
		},

		/**
		 * Move focus into the dialog and hold it there while it is open.
		 *
		 * Without this the panel opens behind the reading position: init() moves
		 * it to the top of the body, so a keyboard user would have to tab
		 * backwards through the whole page to reach the checkout button.
		 */
		trapFocus: function() {
			var self = this,
				$panel = this.getPanel();

			if ( ! this.isDialog() || ! $panel.length ) {
				return;
			}

			// Not in the editor. There the panel is held open for styling rather
			// than opened by the visitor, and pulling focus into the preview
			// iframe every time the widget is selected would take it away from
			// the control the designer just clicked.
			if ( this.editor_preview === 'yes' ) {
				return;
			}

			// Do not overwrite the return target if the panel is already open -
			// the add to cart handler can call show() on an open panel, and by
			// then the active element is inside the dialog.
			if ( ! this._returnFocusTo ) {
				this._returnFocusTo = document.activeElement;
			}

			// The panel animates in, and focusing something still transformed off
			// screen makes the browser scroll to it. Wait for the frame after the
			// visibility class lands.
			this._focusTimer = setTimeout(function() {
				var $focusable = self.getFocusable();

				if ( $focusable.length ) {
					$focusable.first().trigger('focus');
				} else {
					$panel.attr('tabindex', '-1').trigger('focus');
				}
			}, 50);

			$panel.on('keydown.ppOffcanvasTrap', function(e) {
				var $focusable, first, last;

				if ( 'Tab' !== e.key && 9 !== e.keyCode ) {
					return;
				}

				$focusable = self.getFocusable();

				if ( ! $focusable.length ) {
					e.preventDefault();
					return;
				}

				first = $focusable[0];
				last  = $focusable[ $focusable.length - 1 ];

				if ( e.shiftKey && ( e.target === first || e.target === $panel[0] ) ) {
					e.preventDefault();
					last.focus();
				} else if ( ! e.shiftKey && e.target === last ) {
					e.preventDefault();
					first.focus();
				}
			});
		},

		/**
		 * Release the trap and hand focus back to whatever opened the panel.
		 *
		 * @param {boolean} wasOpen Whether the panel was open when close() ran.
		 */
		releaseFocus: function( wasOpen ) {
			var $panel = this.getPanel(),
				returnTo = this._returnFocusTo;

			if ( ! this.isDialog() ) {
				return;
			}

			clearTimeout( this._focusTimer );
			$panel.off('keydown.ppOffcanvasTrap');

			this._returnFocusTo = null;

			// Only pull focus back when the panel really was open and still holds
			// it. close() also runs on init and on every outside click.
			if ( ! wasOpen || ! returnTo || ! $.contains( $panel[0], document.activeElement ) ) {
				return;
			}

			if ( document.body.contains( returnTo ) ) {
				returnTo.focus();
			} else {
				this.button.first().trigger('focus');
			}
		},

		/**
		 * Tabbable elements inside the panel, in document order.
		 */
		getFocusable: function() {
			return this.getPanel()
				.find('a[href], button, input, select, textarea, [tabindex]')
				.filter(function() {
					return $(this).is(':visible') &&
						! this.disabled &&
						'-1' !== $(this).attr('tabindex');
				});
		},

		closeESC: function() {
			var self = this;

			if ( '' === self.settings.esc_close ) {
				return;
			}

			// menu close on ESC key
			$(document).on('keydown' + this.getNamespace(), function (e) {
				if (e.keyCode === 27) { // ESC
					self.close();
				}
			});
		},

		closeClick: function() {
			var self = this,
				$trigger;

			if ( this.toggle_source == 'element-id' && this.toggle_id != '' ) {
				$trigger = '#' + this.toggle_id;
			} else if ( this.toggle_source == 'element-class' && this.toggle_class != '' ) {
				$trigger = '.' + this.toggle_class;
			} else {
				$trigger = '.pp-offcanvas-toggle';
			}

			$(document).on('click' + this.getNamespace(), function (e) {
				if ( $(e.target).is('.pp-offcanvas-content') ||
					$(e.target).parents('.pp-offcanvas-content').length > 0 || 
					$(e.target).is('.pp-offcanvas-toggle') || 
					$(e.target).parents('.pp-offcanvas-toggle').length > 0 || 
					$(e.target).is($trigger) || 
					$(e.target).parents($trigger).length > 0 || 
					! $(e.target).is('.pp-offcanvas-container') ) {
					return;
				} else {
					self.close();
				}
			});
		},

		/**
		 * Throttled entry point for the window resize event.
		 *
		 * _resize() reads five element heights and then writes, so running it
		 * on every resize event forced a layout per event for the whole drag.
		 * Coalesce them into one run per frame. Callers that need the new
		 * height immediately - show(), toggleContent() - still call _resize()
		 * directly.
		 */
		_onResize: function() {
			var self = this;

			if ( this._resizeFrame ) {
				return;
			}

			this._resizeFrame = requestAnimationFrame(function() {
				self._resizeFrame = null;
				self._resize();
			});
		},

		_resize: function() {
			if ( ! this.cart_wrap.length ) {
				return;
			}

			var off_canvas = $('.pp-offcanvas-' + this.id),
				winHeight,
				headerHeight,
				wrapHeight,
				cartTotalHeight,
				cartButtonsHeight,
				cartMessageHeight,
				itemsHeight,
				finalItemsHeight;

			if ( off_canvas && off_canvas.length > 0 ) {
				if ( this.buttons_position === 'bottom' ) {
					winHeight = window.innerHeight;

					off_canvas.find('.pp-offcanvas-inner').css({
						'height': winHeight + 'px',
						'top': 0
					});

					// outerHeight() on an empty set is undefined, and one undefined
					// poisons the sum. The header is absent when it has no close
					// button and no title, and the total and buttons are absent
					// whenever the cart is empty.
					headerHeight = off_canvas.find('.pp-offcanvas-cart-header').outerHeight(true) || 0;
					wrapHeight = off_canvas.find('.pp-offcanvas-wrap').outerHeight() || 0;
					cartTotalHeight = off_canvas.find('.woocommerce-mini-cart__total').outerHeight() || 0;
					cartButtonsHeight = off_canvas.find('.woocommerce-mini-cart__buttons').outerHeight() || 0;
					cartMessageHeight = off_canvas.find('.pp-woo-menu-cart-message').outerHeight() || 0;

					itemsHeight = winHeight - (headerHeight + cartTotalHeight + cartButtonsHeight + cartMessageHeight);

					finalItemsHeight = itemsHeight - ( winHeight - wrapHeight );
					finalItemsHeight += 'px';
				} else {
					finalItemsHeight = 'auto';
				}

				// Write the height as a custom property on the panel rather than
				// building a <style> element and swapping it into <head>. That
				// swap invalidated style for the whole document on every resize
				// event. The property also has to live on the panel rather than
				// on .cart_list itself: WooCommerce replaces the whole of
				// .widget_shopping_cart_content through cart fragments, which
				// would take an inline style on the list with it.
				off_canvas[0].style.setProperty( '--pp-cart-items-height', finalItemsHeight );
			}
		}
	};

	/**
	 * Handler for the Offcanvas Content widget.
	 */
	var OffCanvasContentHandler = function ($scope) {
		var content_wrap = $scope.find('.pp-offcanvas-content-wrap');

		if ( content_wrap.length > 0 ) {
			new PPOffcanvasContent( $scope );
		}
	};

	/**
	 * Handler for the Woo - Off Canvas Cart widget.
	 */
	var WooOffcanvasCartHandler = function ($scope) {
		var container = $scope.find('.pp-offcanvas-cart-container');

		if ( container.length > 0 ) {
			new PPOffcanvasContent( $scope );
		}
	};

	$(window).on('elementor/frontend/init', function () {
		elementorFrontend.hooks.addAction( 'frontend/element_ready/pp-offcanvas-content.default', OffCanvasContentHandler );
		elementorFrontend.hooks.addAction( 'frontend/element_ready/pp-woo-offcanvas-cart.default', WooOffcanvasCartHandler );
	});

})(jQuery);