jQuery.noConflict();

/**
 * File: LeftAndMain.js
 */
(function($) {

	window.ss = window.ss || {};

	var windowWidth, windowHeight;

	/**
	 * @func debounce
	 * @param func {function} - The callback to invoke after `wait` milliseconds.
	 * @param wait {number} - Milliseconds to wait.
	 * @param immediate {boolean} - If true the callback will be invoked at the start rather than the end.
	 * @return {function}
	 * @desc Returns a function that will not be called until it hasn't been invoked for `wait` seconds.
	 */
	window.ss.debounce = function (func, wait, immediate) {
		var timeout, context, args;

		var later = function() {
			timeout = null;
			if (!immediate) func.apply(context, args);
		};

		return function() {
			var callNow = immediate && !timeout;

			context = this;
			args = arguments;

			clearTimeout(timeout);
			timeout = setTimeout(later, wait);

			if (callNow) {
				func.apply(context, args);
			}
		};
	};

	$(window).bind('resize.leftandmain', function(e) {
		// Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event.
		var cb = function() {$('.cms-container').trigger('windowresize');};

		// Workaround to avoid IE8 infinite loops when elements are resized as a result of this event
		if($.browser.msie && parseInt($.browser.version, 10) < 9) {
			var newWindowWidth = $(window).width(), newWindowHeight = $(window).height();
			if(newWindowWidth != windowWidth || newWindowHeight != windowHeight) {
				windowWidth = newWindowWidth;
				windowHeight = newWindowHeight;
				cb();
			}
		} else {
			cb();
		}
	});

	// setup jquery.entwine
	$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
	$.entwine('ss', function($) {

		/*
		 * Handle messages sent via nested iframes
		 * Messages should be raised via postMessage with an object with the 'type' parameter given.
		 * An optional 'target' and 'data' parameter can also be specified. If no target is specified
		 * events will be sent to the window instead.
		 * type should be one of:
		 *  - 'event' - Will trigger the given event (specified by 'event') on the target
		 *  - 'callback' - Will call the given method (specified by 'callback') on the target
		 */
		$(window).on("message", function(e) {
			var target,
				event = e.originalEvent,
				data = JSON.parse(event.data);

			// Reject messages outside of the same origin
			if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return;

			// Get target of this action
			target = typeof(data.target) === 'undefined'
				? $(window)
				: $(data.target);

			// Determine action
			switch(data.type) {
				case 'event':
					target.trigger(data.event, data.data);
					break;
				case 'callback':
					target[data.callback].call(target, data.data);
					break;
			}
		});

		/**
		 * Position the loading spinner animation below the ss logo
		 */
		var positionLoadingSpinner = function() {
			var offset = 120; // offset from the ss logo
			var spinner = $('.ss-loading-screen .loading-animation');
			var top = ($(window).height() - spinner.height()) / 2;
			spinner.css('top', top + offset);
			spinner.show();
		};

		// apply an select element only when it is ready, ie. when it is rendered into a template
		// with css applied and got a width value.
		var applyChosen = function(el) {
			if(el.is(':visible')) {
				el.addClass('has-chzn').chosen({
					allow_single_deselect: true,
					disable_search_threshold: 20
				});

				var title = el.prop('title');

				if(title) {
					el.siblings('.chzn-container').prop('title', title);
				}
			} else {
				setTimeout(function() {
					// Make sure it's visible before applying the ui
					el.show();
					applyChosen(el); },
				500);
			}
		};

		/**
		 * Compare URLs, but normalize trailing slashes in
		 * URL to work around routing weirdnesses in SS_HTTPRequest.
		 * Also normalizes relative URLs by prefixing them with the <base>.
		 */
		var isSameUrl = function(url1, url2) {
			var baseUrl = $('base').attr('href');
			url1 = $.path.isAbsoluteUrl(url1) ? url1 : $.path.makeUrlAbsolute(url1, baseUrl),
			url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl);
			var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2);
			return (
				url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
				url1parts.search == url2parts.search
			);
		};

		var ajaxCompleteEvent = window.ss.debounce(function () {
			$(window).trigger('ajaxComplete');
		}, 1000, true);

		$(window).bind('resize', positionLoadingSpinner).trigger('resize');

		// global ajax handlers
		$(document).ajaxComplete(function(e, xhr, settings) {
			// Simulates a redirect on an ajax response.
			if(window.History.enabled) {
				var url = xhr.getResponseHeader('X-ControllerURL'),
					// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
					origUrl = History.getPageUrl().replace(/\/$/, ''),
					destUrl = settings.url,
					opts;

				// Only redirect if controller url differs to the requested or current one
				if(url !== null &&
					(!isSameUrl(origUrl, url) || !isSameUrl(destUrl, url))
				) {
					opts = {
						// Ensure that redirections are followed through by history API by handing it a unique ID
						id: (new Date()).getTime() + String(Math.random()).replace(/\D/g,''),
						pjax: xhr.getResponseHeader('X-Pjax')
							? xhr.getResponseHeader('X-Pjax')
							: settings.headers['X-Pjax']
					};
					window.History.pushState(opts, '', url);
				}
			}

			// Handle custom status message headers
			var msg = (xhr.getResponseHeader('X-Status')) ? xhr.getResponseHeader('X-Status') : xhr.statusText,
				reathenticate = xhr.getResponseHeader('X-Reauthenticate'),
				msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good',
				ignoredMessages = ['OK', 'success', 'HTTP/2.0 200'];

			// Enable reauthenticate dialog if requested
			if(reathenticate) {
				$('.cms-container').showLoginDialog();
				return;
			}

			// Show message (but ignore aborted requests)
			if(xhr.status !== 0 && msg && $.inArray(msg, ignoredMessages) === -1) {
				// Decode into UTF-8, HTTP headers don't allow multibyte
				statusMessage(decodeURIComponent(msg), msgType);
			}

			ajaxCompleteEvent(this);
		});

		/**
		 * Main LeftAndMain interface with some control panel and an edit form.
		 *
		 * Events:
		 *  ajaxsubmit - ...
		 *  validate - ...
		 *  aftersubmitform - ...
		 */
		$('.cms-container').entwine({

			/**
			 * Tracks current panel request.
			 */
			StateChangeXHR: null,

			/**
			 * Tracks current fragment-only parallel PJAX requests.
			 */
			FragmentXHR: {},

			StateChangeCount: 0,

			/**
			 * Options for the threeColumnCompressor layout algorithm.
			 *
			 * See LeftAndMain.Layout.js for description of these options.
			 */
			LayoutOptions: {
				minContentWidth: 940,
				minPreviewWidth: 400,
				mode: 'content'
			},

			/**
			 * Constructor: onmatch
			 */
			onadd: function() {
				var self = this;

				// Browser detection
				if($.browser.msie && parseInt($.browser.version, 10) < 8) {
					$('.ss-loading-screen').append(
						'<p class="ss-loading-incompat-warning"><span class="notice">' +
						'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' +
						'</span></p>'
					).css('z-index', $('.ss-loading-screen').css('z-index')+1);
					$('.loading-animation').remove();

					this._super();
					return;
				}

				// Initialize layouts
				this.redraw();

				// Remove loading screen
				$('.ss-loading-screen').hide();
				$('body').removeClass('loading');
				$(window).unbind('resize', positionLoadingSpinner);
				this.restoreTabState();
				this._super();
			},

			fromWindow: {
				onstatechange: function(e){
					this.handleStateChange(e); 
				}
			},

			'onwindowresize': function() {
				this.redraw();
			},

			'from .cms-panel': {
				ontoggle: function(){ this.redraw(); }
			},

			'from .cms-container': {
				onaftersubmitform: function(){ this.redraw(); }
			},

			/**
			 * Ensure the user can see the requested section - restore the default view.
			 */
			'from .cms-menu-list li a': {
				onclick: function(e) {
					var href = $(e.target).attr('href');
					if(e.which > 1 || href == this._tabStateUrl()) return;
					this.splitViewMode();
				}
			},

			/**
			 * Change the options of the threeColumnCompressor layout, and trigger layouting if needed.
			 * You can provide any or all options. The remaining options will not be changed.
			 */
			updateLayoutOptions: function(newSpec) {
				var spec = this.getLayoutOptions();

				var dirty = false;

				for (var k in newSpec) {
					if (spec[k] !== newSpec[k]) {
						spec[k] = newSpec[k];
						dirty = true;
					}
				}

				if (dirty) this.redraw();
			},

			/**
			 * Enable the split view - with content on the left and preview on the right.
			 */
			splitViewMode: function() {
				this.updateLayoutOptions({
					mode: 'split'
				});
			},

			/**
			 * Content only.
			 */
			contentViewMode: function() {
				this.updateLayoutOptions({
					mode: 'content'
				});
			},

			/**
			 * Preview only.
			 */
			previewMode: function() {
				this.updateLayoutOptions({
					mode: 'preview'
				});
			},

			RedrawSuppression: false,

			redraw: function() {
				if (this.getRedrawSuppression()) return;

				if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

				// Reset the algorithm.
				this.data('jlayout', jLayout.threeColumnCompressor(
					{
						menu: this.children('.cms-menu'),
						content: this.children('.cms-content'),
						preview: this.children('.cms-preview')
					},
					this.getLayoutOptions()
				));

				// Trigger layout algorithm once at the top. This also lays out children - we move from outside to
				// inside, resizing to fit the parent.
				this.layout();

				// Redraw on all the children that need it
				this.find('.cms-panel-layout').redraw();
				this.find('.cms-content-fields[data-layout-type]').redraw();
				this.find('.cms-edit-form[data-layout-type]').redraw();
				this.find('.cms-preview').redraw();
				this.find('.cms-content').redraw();
			},

			/**
			 * Confirm whether the current user can navigate away from this page
			 * 
			 * @param {array} selectors Optional list of selectors
			 * @returns {boolean} True if the navigation can proceed
			 */
			checkCanNavigate: function(selectors) {
				// Check change tracking (can't use events as we need a way to cancel the current state change)
				var contentEls = this._findFragments(selectors || ['Content']),
					trackedEls = contentEls
						.find(':data(changetracker)')
						.add(contentEls.filter(':data(changetracker)')),
					safe = true;

				if(!trackedEls.length) {
					return true;
				}

				trackedEls.each(function() {
					// See LeftAndMain.EditForm.js
					if(!$(this).confirmUnsavedChanges()) {
						safe = false;
					}
				});

				return safe;
			},

			/**
			 * Proxy around History.pushState() which handles non-HTML5 fallbacks,
			 * as well as global change tracking. Change tracking needs to be synchronous rather than event/callback
			 * based because the user needs to be able to abort the action completely.
			 *
			 * See handleStateChange() for more details.
			 *
			 * Parameters:
			 *  - {String} url
			 *  - {String} title New window title
			 *  - {Object} data Any additional data passed through to History.pushState()
			 *  - {boolean} forceReload Forces the replacement of the current history state, even if the URL is the same, i.e. allows reloading.
			 */
			loadPanel: function(url, title, data, forceReload, forceReferer) {
				if(!data) data = {};
				if(!title) title = "";
				if (!forceReferer) forceReferer = History.getState().url;

				// Check for unsaved changes
				if(!this.checkCanNavigate(data.pjax ? data.pjax.split(',') : ['Content'])) {
					return;
				}

				// Save tab selections so we can restore them later
				this.saveTabState();

				if(window.History.enabled) {
					$.extend(data, {__forceReferer: forceReferer});
					// Active menu item is set based on X-Controller ajax header,
					// which matches one class on the menu
					if(forceReload) {
						// Add a parameter to make sure the page gets reloaded even if the URL is the same.
						$.extend(data, {__forceReload: Math.random()});
						window.History.replaceState(data, title, url);
					} else {
						window.History.pushState(data, title, url);
					}
				} else {
					window.location = $.path.makeUrlAbsolute(url, $('base').attr('href'));
				}
			},

			/**
			 * Nice wrapper for reloading current history state.
			 */
			reloadCurrentPanel: function() {
				this.loadPanel(window.History.getState().url, null, null, true);
			},

			/**
			 * Function: submitForm
			 *
			 * Parameters:
			 *  {DOMElement} form - The form to be submitted. Needs to be passed
			 *   in to avoid entwine methods/context being removed through replacing the node itself.
			 *  {DOMElement} button - The pressed button (optional)
			 *  {Function} callback - Called in complete() handler of jQuery.ajax()
			 *  {Object} ajaxOptions - Object literal to merge into $.ajax() call
			 *
			 * Returns:
			 *  (boolean)
			 */
			submitForm: function(form, button, callback, ajaxOptions) {
				var self = this;

				// look for save button
				if(!button) button = this.find('.Actions :submit[name=action_save]');
				// default to first button if none given - simulates browser behaviour
				if(!button) button = this.find('.Actions :submit:first');

				form.trigger('beforesubmitform');
				this.trigger('submitform', {form: form, button: button});

				// set button to "submitting" state
				$(button).addClass('loading');

				// validate if required
				var validationResult = form.validate();
				if(typeof validationResult!=='undefined' && !validationResult) {
					// TODO Automatically switch to the tab/position of the first error
					statusMessage("Validation failed.", "bad");

					$(button).removeClass('loading');

					return false;
				}

				// get all data from the form
				var formData = form.serializeArray();
				// add button action
				formData.push({name: $(button).attr('name'), value:'1'});
				// Artificial HTTP referer, IE doesn't submit them via ajax.
				// Also rewrites anchors to their page counterparts, which is important
				// as automatic browser ajax response redirects seem to discard the hash/fragment.
				// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
				formData.push({name: 'BackURL', value:History.getPageUrl().replace(/\/$/, '')});

				// Save tab selections so we can restore them later
				this.saveTabState();

				// Standard Pjax behaviour is to replace the submitted form with new content.
				// The returned view isn't always decided upon when the request
				// is fired, so the server might decide to change it based on its own logic,
				// sending back different `X-Pjax` headers and content
				jQuery.ajax(jQuery.extend({
					headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"},
					url: form.attr('action'),
					data: formData,
					type: 'POST',
					complete: function() {
						$(button).removeClass('loading');
					},
					success: function(data, status, xhr) {
						form.removeClass('changed'); // TODO This should be using the plugin API
						if(callback) callback(data, status, xhr);

						var newContentEls = self.handleAjaxResponse(data, status, xhr);
						if(!newContentEls) return;

						newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
					}
				}, ajaxOptions));

				return false;
			},

			/**
			 * Last html5 history state
			 */
			LastState: null,
			
			/**
			 * Flag to pause handleStateChange
			 */
			PauseState: false,

			/**
			 * Handles ajax loading of new panels through the window.History object.
			 * To trigger loading, pass a new URL to window.History.pushState().
			 * Use loadPanel() as a pushState() wrapper as it provides some additional functionality
			 * like global changetracking and user aborts.
			 *
			 * Due to the nature of history management, no callbacks are allowed.
			 * Use the 'beforestatechange' and 'afterstatechange' events instead,
			 * or overwrite the beforeLoad() and afterLoad() methods on the
			 * DOM element you're loading the new content into.
			 * Although you can pass data into pushState(), it shouldn't contain
			 * DOM elements or callback closures.
			 *
			 * The passed URL should allow reconstructing important interface state
			 * without additional parameters, in the following use cases:
			 * - Explicit loading through History.pushState()
			 * - Implicit loading through browser navigation event triggered by the user (forward or back)
			 * - Full window refresh without ajax
			 * For example, a ModelAdmin search event should contain the search terms
			 * as URL parameters, and the result display should automatically appear
			 * if the URL is loaded without ajax.
			 */
			handleStateChange: function() {
				if(this.getPauseState()) {
					return;
				}
				
				// Don't allow parallel loading to avoid edge cases
				if(this.getStateChangeXHR()) this.getStateChangeXHR().abort();

				var self = this, h = window.History, state = h.getState(),
					fragments = state.data.pjax || 'Content', headers = {},
					fragmentsArr = fragments.split(','),
					contentEls = this._findFragments(fragmentsArr);

				// For legacy IE versions (IE7 and IE8), reload without ajax
				// as a crude way to fix memory leaks through whole window refreshes.
				this.setStateChangeCount(this.getStateChangeCount() + 1);
				var isLegacyIE = ($.browser.msie && parseInt($.browser.version, 10) < 9);
				if(isLegacyIE && this.getStateChangeCount() > 20) {
					document.location.href = state.url;
					return;
				}

				if(!this.checkCanNavigate()) {
					// If history is emulated (ie8 or below) disable attempting to restore
					if(h.emulated.pushState) {
						return;
					}
					
					var lastState = this.getLastState();
					
					// Suppress panel loading while resetting state
					this.setPauseState(true);
					
					// Restore best last state
					if(lastState) {
						h.pushState(lastState.id, lastState.title, lastState.url);
					} else {
						h.back();
					}
					this.setPauseState(false);
					
					// Abort loading of this panel
					return;
				}
				this.setLastState(state);

				// If any of the requested Pjax fragments don't exist in the current view,
				// fetch the "Content" view instead, which is the "outermost" fragment
				// that can be reloaded without reloading the whole window.
				if(contentEls.length < fragmentsArr.length) {
					fragments = 'Content', fragmentsArr = ['Content'];
					contentEls = this._findFragments(fragmentsArr);
				}

				this.trigger('beforestatechange', {state: state, element: contentEls});

				// Set Pjax headers, which can declare a preference for the returned view.
				// The actually returned view isn't always decided upon when the request
				// is fired, so the server might decide to change it based on its own logic.
				headers['X-Pjax'] = fragments;

				// Set 'fake' referer - we call pushState() before making the AJAX request, so we have to
				// set our own referer here
				if (typeof state.data.__forceReferer !== 'undefined') {
					// Ensure query string is properly encoded if present
					var url = state.data.__forceReferer;

					try {
						// Prevent double-encoding by attempting to decode
						url = decodeURI(url);
					} catch(e) {
						// URL not encoded, or was encoded incorrectly, so do nothing
					} finally {
						// Set our referer header to the encoded URL
						headers['X-Backurl'] = encodeURI(url);
					}
				}

				contentEls.addClass('loading');
				var xhr = $.ajax({
					headers: headers,
					url: state.url,
					complete: function() {
						self.setStateChangeXHR(null);
						// Remove loading indication from old content els (regardless of which are replaced)
						contentEls.removeClass('loading');
					},
					success: function(data, status, xhr) {
						var els = self.handleAjaxResponse(data, status, xhr, state);
						self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state});
					}
				});

				this.setStateChangeXHR(xhr);
			},

			/**
			 * ALternative to loadPanel/submitForm.
			 *
			 * Triggers a parallel-fetch of a PJAX fragment, which is a separate request to the
			 * state change requests. There could be any amount of these fetches going on in the background,
			 * and they don't register as a HTML5 history states.
			 *
			 * This is meant for updating a PJAX areas that are not complete panel/form reloads. These you'd
			 * normally do via submitForm or loadPanel which have a lot of automation built in.
			 *
			 * On receiving successful response, the framework will update the element tagged with appropriate
			 * data-pjax-fragment attribute (e.g. data-pjax-fragment="<pjax-fragment-name>"). Make sure this element
			 * is available.
			 *
			 * Example usage:
			 * $('.cms-container').loadFragment('admin/foobar/', 'FragmentName');
			 *
			 * @param url string Relative or absolute url of the controller.
			 * @param pjaxFragments string PJAX fragment(s), comma separated.
			 */
			loadFragment: function(url, pjaxFragments) {

				var self = this,
					xhr,
					headers = {},
					baseUrl = $('base').attr('href'),
					fragmentXHR = this.getFragmentXHR();

				// Make sure only one XHR for a specific fragment is currently in progress.
				if(
					typeof fragmentXHR[pjaxFragments]!=='undefined' &&
					fragmentXHR[pjaxFragments]!==null
				) {
					fragmentXHR[pjaxFragments].abort();
					fragmentXHR[pjaxFragments] = null;
				}

				url = $.path.isAbsoluteUrl(url) ? url : $.path.makeUrlAbsolute(url, baseUrl);
				headers['X-Pjax'] = pjaxFragments;

				xhr = $.ajax({
					headers: headers,
					url: url,
					success: function(data, status, xhr) {
						var elements = self.handleAjaxResponse(data, status, xhr, null);

						// We are fully done now, make it possible for others to hook in here.
						self.trigger('afterloadfragment', { data: data, status: status, xhr: xhr, elements: elements });
					},
					error: function(xhr, status, error) {
						self.trigger('loadfragmenterror', { xhr: xhr, status: status, error: error });
					},
					complete: function() {
						// Reset the current XHR in tracking object.
						var fragmentXHR = self.getFragmentXHR();
						if(
							typeof fragmentXHR[pjaxFragments]!=='undefined' &&
							fragmentXHR[pjaxFragments]!==null
						) {
							fragmentXHR[pjaxFragments] = null;
						}
					}
				});

				// Store the fragment request so we can abort later, should we get a duplicate request.
				fragmentXHR[pjaxFragments] = xhr;

				return xhr;
			},

			/**
			 * Handles ajax responses containing plain HTML, or mulitple
			 * PJAX fragments wrapped in JSON (see PjaxResponseNegotiator PHP class).
			 * Can be hooked into an ajax 'success' callback.
			 *
			 * Parameters:
			 * 	(Object) data
			 * 	(String) status
			 * 	(XMLHTTPRequest) xhr
			 * 	(Object) state The original history state which the request was initiated with
			 */
			handleAjaxResponse: function(data, status, xhr, state) {
				var self = this, url, selectedTabs, guessFragment;

				// Support a full reload
				if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) {
					var baseUrl = $('base').attr('href'),
						rawURL = xhr.getResponseHeader('X-ControllerURL'),
						url = $.path.isAbsoluteUrl(rawURL) ? rawURL : $.path.makeUrlAbsolute(rawURL, baseUrl);

					document.location.href = url;
					return;
				}

				// Pseudo-redirects via X-ControllerURL might return empty data, in which
				// case we'll ignore the response
				if(!data) return;

				// Update title
				var title = xhr.getResponseHeader('X-Title');
				if(title) document.title = decodeURIComponent(title.replace(/\+/g, ' '));

				var newFragments = {}, newContentEls;
				// If content type is text/json (ignoring charset and other parameters)
				if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) {
					newFragments = data;
				} else {

					// Fall back to replacing the content fragment if HTML is returned
					var fragment = document.createDocumentFragment();
					jQuery.clean( [ data ], document, fragment, [] );
					$data = $(jQuery.merge( [], fragment.childNodes ));

					// Try and guess the fragment if none is provided
					// TODO: data-pjax-fragment might actually give us the fragment. For now we just check most common case
					guessFragment = 'Content';
					if ($data.is('form') && !$data.is('[data-pjax-fragment~=Content]')) guessFragment = 'CurrentForm';

					newFragments[guessFragment] = $data;
				}

				this.setRedrawSuppression(true);
				try {
					// Replace each fragment individually
					$.each(newFragments, function(newFragment, html) {
						var contentEl = $('[data-pjax-fragment]').filter(function() {
							return $.inArray(newFragment, $(this).data('pjaxFragment').split(' ')) != -1;
						}), newContentEl = $(html);

						// Add to result collection
						if(newContentEls) newContentEls.add(newContentEl);
						else newContentEls = newContentEl;

						// Update panels
						if(newContentEl.find('.cms-container').length) {
							throw 'Content loaded via ajax is not allowed to contain tags matching the ".cms-container" selector to avoid infinite loops';
						}

						// Set loading state and store element state
						var origStyle = contentEl.attr('style');
						var origParent = contentEl.parent();
						var origParentLayoutApplied = (typeof origParent.data('jlayout')!=='undefined');
						var layoutClasses = ['east', 'west', 'center', 'north', 'south', 'column-hidden'];
						var elemClasses = contentEl.attr('class');
						var origLayoutClasses = [];
						if(elemClasses) {
							origLayoutClasses = $.grep(
								elemClasses.split(' '),
								function(val) { return ($.inArray(val, layoutClasses) >= 0);}
							);
						}

						newContentEl
							.removeClass(layoutClasses.join(' '))
							.addClass(origLayoutClasses.join(' '));
						if(origStyle) newContentEl.attr('style', origStyle);

						// Allow injection of inline styles, as they're not allowed in the document body.
						// Not handling this through jQuery.ondemand to avoid parsing the DOM twice.
						var styles = newContentEl.find('style').detach();
						if(styles.length) $(document).find('head').append(styles);

						// Replace panel completely (we need to override the "layout" attribute, so can't replace the child instead)
						contentEl.replaceWith(newContentEl);

						// Force jlayout to rebuild internal hierarchy to point to the new elements.
						// This is only necessary for elements that are at least 3 levels deep. 2nd level elements will
						// be taken care of when we lay out the top level element (.cms-container).
						if (!origParent.is('.cms-container') && origParentLayoutApplied) {
							origParent.layout();
						}
					});

					// Re-init tabs (in case the form tag itself is a tabset)
					var newForm = newContentEls.filter('form');
					if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
				}
				finally {
					this.setRedrawSuppression(false);
				}

				this.redraw();
				this.restoreTabState((state && typeof state.data.tabState !== 'undefined') ? state.data.tabState : null);

				return newContentEls;
			},

			/**
			 *
			 *
			 * Parameters:
			 * - fragments {Array}
			 * Returns: jQuery collection
			 */
			_findFragments: function(fragments) {
				return $('[data-pjax-fragment]').filter(function() {
					// Allows for more than one fragment per node
					var i, nodeFragments = $(this).data('pjaxFragment').split(' ');
					for(i in fragments) {
						if($.inArray(fragments[i], nodeFragments) != -1) return true;
					}
					return false;
				});
			},

			/**
			 * Function: refresh
			 *
			 * Updates the container based on the current url
			 *
			 * Returns: void
			 */
			refresh: function() {
				$(window).trigger('statechange');

				$(this).redraw();
			},

			/**
			 * Save tab selections in order to reconstruct them later.
			 * Requires HTML5 sessionStorage support.
			 */
			saveTabState: function() {
				if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;

				var selectedTabs = [], url = this._tabStateUrl();
				this.find('.cms-tabset,.ss-tabset').each(function(i, el) {
					var id = $(el).attr('id');
					if(!id) return; // we need a unique reference
					if(!$(el).data('tabs')) return; // don't act on uninit'ed controls

					// Allow opt-out via data element or entwine property.
					if($(el).data('ignoreTabState') || $(el).getIgnoreTabState()) return;

					selectedTabs.push({id:id, selected:$(el).tabs('option', 'selected')});
				});

				if(selectedTabs) {
					var tabsUrl = 'tabs-' + url;
					try {
						window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs));
					} catch(err) {
						if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
							// If this fails we ignore the error as the only issue is that it
							// does not remember the tab state.
							// This is a Safari bug which happens when private browsing is enabled.
							return;
						} else {
							throw err;
						}
					}
				}
			},

			/**
			 * Re-select previously saved tabs.
			 * Requires HTML5 sessionStorage support.
			 *
			 * Parameters:
			 * 	(Object) Map of tab container selectors to tab selectors.
			 * 	Used to mark a specific tab as active regardless of the previously saved options.
			 */
			restoreTabState: function(overrideStates) {
				var self = this, url = this._tabStateUrl(),
					hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
					sessionData = hasSessionStorage ? window.sessionStorage.getItem('tabs-' + url) : null,
					sessionStates = sessionData ? JSON.parse(sessionData) : false;

				this.find('.cms-tabset, .ss-tabset').each(function() {
					var index, 
						tabset = $(this), 
						tabsetId = tabset.attr('id'), 
						tab,
						forcedTab = tabset.children('ul').children('li.ss-tabs-force-active');

					if(!tabset.data('tabs')){
						return; // don't act on uninit'ed controls
					}

					// The tabs may have changed, notify the widget that it should update its internal state.
					tabset.tabs('refresh');

					// Make sure the intended tab is selected. Only force the tab on the correct tabset though
					if(forcedTab.length) {
						index = forcedTab.first().index();
					} else if(overrideStates && overrideStates[tabsetId]) {
						tab = tabset.find(overrideStates[tabsetId].tabSelector);
						if(tab.length){
							index = tab.index();
						}
					} else if(sessionStates) {
						$.each(sessionStates, function(i, state) {
							if(tabsetId == state.id){
								index = state.selected;
							}
						});
					}
					if(index !== null){
						tabset.tabs('option', 'active', index);
						self.trigger('tabstaterestored');
					}
				});
			},

			/**
			 * Remove any previously saved state.
			 *
			 * Parameters:
			 *  (String) url Optional (sanitized) URL to clear a specific state.
			 */
			clearTabState: function(url) {
				if(typeof(window.sessionStorage)=="undefined") return;

				var s = window.sessionStorage;
				if(url) {
					s.removeItem('tabs-' + url);
				} else {
					for(var i=0;i<s.length;i++) {
						if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i));
				}
				}
			},

			/**
			 * Remove tab state for the current URL.
			 */
			clearCurrentTabState: function() {
				this.clearTabState(this._tabStateUrl());
			},

			_tabStateUrl: function() {
				return History.getState().url
					.replace(/\?.*/, '')
					.replace(/#.*/, '')
					.replace($('base').attr('href'), '');
			},

			showLoginDialog: function() {
				var tempid = $('body').data('member-tempid'),
					dialog = $('.leftandmain-logindialog'),
					url = 'CMSSecurity/login';

				// Force regeneration of any existing dialog
				if(dialog.length) dialog.remove();

				// Join url params
				url = $.path.addSearchParams(url, {
					'tempid': tempid,
					'BackURL': window.location.href
				});

				// Show a placeholder for instant feedback. Will be replaced with actual
				// form dialog once its loaded.
				dialog = $('<div class="leftandmain-logindialog"></div>');
				dialog.attr('id', new Date().getTime());
				dialog.data('url', url);
				$('body').append(dialog);
			}
		});

		// Login dialog page
		$('.leftandmain-logindialog').entwine({
			onmatch: function() {
				this._super();

				// Create jQuery dialog
				this.ssdialog({
					iframeUrl: this.data('url'),
					dialogClass: "leftandmain-logindialog-dialog",
					autoOpen: true,
					minWidth: 500,
					maxWidth: 500,
					minHeight: 370,
					maxHeight: 400,
					closeOnEscape: false,
					open: function() {
						$('.ui-widget-overlay').addClass('leftandmain-logindialog-overlay');
					},
					close: function() {
						$('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay');
					}
				});
			},
			onunmatch: function() {
				this._super();
			},
			open: function() {
				this.ssdialog('open');
			},
			close: function() {
				this.ssdialog('close');
			},
			toggle: function(bool) {
				if(this.is(':visible')) this.close();
				else this.open();
			},
			/**
			 * Callback activated by CMSSecurity_success.ss
			 */
			reauthenticate: function(data) {
				// Replace all SecurityID fields with the given value
				if(typeof(data.SecurityID) !== 'undefined') {
					$(':input[name=SecurityID]').val(data.SecurityID);
				}
				// Update TempID for current user
				if(typeof(data.TempID) !== 'undefined') {
					$('body').data('member-tempid', data.TempID);
				}
				this.close();
			}
		});

		/**
		 * Add loading overlay to selected regions in the CMS automatically.
		 * Not applied to all "*.loading" elements to avoid secondary regions
		 * like the breadcrumbs showing unnecessary loading status.
		 */
		$('form.loading,.cms-content.loading,.cms-content-fields.loading,.cms-content-view.loading').entwine({
			onmatch: function() {
				this.append('<div class="cms-content-loading-overlay ui-widget-overlay-light"></div><div class="cms-content-loading-spinner"></div>');
				this._super();
			},
			onunmatch: function() {
				this.find('.cms-content-loading-overlay,.cms-content-loading-spinner').remove();
				this._super();
			}
		});

		/** Make all buttons "hoverable" with jQuery theming. */
		$('.cms input[type="submit"], .cms button, .cms input[type="reset"], .cms .ss-ui-button').entwine({
			onadd: function() {
				this.addClass('ss-ui-button');
				if(!this.data('button')) this.button();
				this._super();
			},
			onremove: function() {
				if(this.data('button')) this.button('destroy');
				this._super();
			}
		});

		/**
		 * Loads the link's 'href' attribute into a panel via ajax,
		 * as opposed to triggering a full page reload.
		 * Little helper to avoid repetition, and make it easy to
		 * "opt in" to panel loading, while by default links still exhibit their default behaviour.
		 * The PJAX target can be specified via a 'data-pjax-target' attribute.
		 */
		$('.cms .cms-panel-link').entwine({
			onclick: function(e) {
				if($(this).hasClass('external-link')) {
					e.stopPropagation();

					return;
				}

				var href = this.attr('href'),
					url = (href && !href.match(/^#/)) ? href : this.data('href'),
					data = {pjax: this.data('pjaxTarget')};

				$('.cms-container').loadPanel(url, null, data);
				e.preventDefault();
			}
		});

		/**
		 * Does an ajax loads of the link's 'href' attribute via ajax and displays any FormResponse messages from the CMS.
		 * Little helper to avoid repetition, and make it easy to trigger actions via a link,
		 * without reloading the page, changing the URL, or loading in any new panel content.
		 */
		$('.cms .ss-ui-button-ajax').entwine({
			onclick: function(e) {
				$(this).removeClass('ui-button-text-only');
				$(this).addClass('ss-ui-button-loading ui-button-text-icons');

				var loading = $(this).find(".ss-ui-loading-icon");

				if(loading.length < 1) {
					loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon');

					$(this).prepend(loading);
				}

				loading.show();

				var href = this.attr('href'), url = href ? href : this.data('href');

				jQuery.ajax({
					url: url,
					// Ensure that form view is loaded (rather than whole "Content" template)
					complete: function(xmlhttp, status) {
						var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText;

						try {
							if (typeof msg != "undefined" && msg !== null) eval(msg);
						}
						catch(e) {}

						loading.hide();

						$(".cms-container").refresh();

						$(this).removeClass('ss-ui-button-loading ui-button-text-icons');
						$(this).addClass('ui-button-text-only');
					},
					dataType: 'html'
				});
				e.preventDefault();
			}
		});

		/**
		 * Trigger dialogs with iframe based on the links href attribute (see ssui-core.js).
		 */
		$('.cms .ss-ui-dialog-link').entwine({
			UUID: null,
			onmatch: function() {
				this._super();
				this.setUUID(new Date().getTime());
			},
			onunmatch: function() {
				this._super();
			},
			onclick: function() {
				this._super();

				var self = this, id = 'ss-ui-dialog-' + this.getUUID();
				var dialog = $('#' + id);
				if(!dialog.length) {
					dialog = $('<div class="ss-ui-dialog" id="' + id + '" />');
					$('body').append(dialog);
				}

				var extraClass = this.data('popupclass')?this.data('popupclass'):'';

				dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass});
				return false;
			}
		});

		/**
		 * Add styling to all contained buttons, and create buttonsets if required.
		 */
		$('.cms-content .Actions').entwine({
			onmatch: function() {
				this.find('.ss-ui-button').click(function() {
						var form = this.form;

						// forms don't natively store the button they've been triggered with
						if(form) {
							form.clickedButton = this;
							// Reset the clicked button shortly after the onsubmit handlers
							// have fired on the form
						setTimeout(function() {
							form.clickedButton = null;
						}, 10);
					}
				});

				this.redraw();
				this._super();
			},
			onunmatch: function() {
				this._super();
			},
			redraw: function() {
				if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

				// Remove whitespace to avoid gaps with inline elements
				this.contents().filter(function() {
					return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
				}).remove();

				// Init buttons if required
				this.find('.ss-ui-button').each(function() {
					if(!$(this).data('button')) $(this).button();
				});

				// Mark up buttonsets
				this.find('.ss-ui-buttonset').buttonset();
			}
		});

		/**
		 * Duplicates functionality in DateField.js, but due to using entwine we can match
		 * the DOM element on creation, rather than onclick - which allows us to decorate
		 * the field with a calendar icon
		 */
		$('.cms .field.date input.text').entwine({
			onmatch: function() {
				var holder = $(this).parents('.field.date:first'), config = holder.data();
				if(!config.showcalendar) {
					this._super();
					return;
				}

				config.showOn = 'button';
				if(config.locale && $.datepicker.regional[config.locale]) {
					config = $.extend(config, $.datepicker.regional[config.locale], {});
				}

				$(this).datepicker(config);
				// // Unfortunately jQuery UI only allows configuration of icon images, not sprites
				// this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'});

				this._super();
			},
			onunmatch: function() {
				this._super();
			}
		});

		/**
		 * Styled dropdown select fields via chosen. Allows things like search and optgroup
		 * selection support. Rather than manually adding classes to selects we want
		 * styled, we style everything but the ones we tell it not to.
		 *
		 * For the CMS we also need to tell the parent div that it has a select so
		 * we can fix the height cropping.
		 */

		$('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({
			onmatch: function() {
				if(this.is('.no-chzn')) {
					this._super();
					return;
				}

				// Explicitly disable default placeholder if no custom one is defined
				if(!this.data('placeholder')) this.data('placeholder', ' ');

				// We could've gotten stale classes and DOM elements from deferred cache.
				this.removeClass('has-chzn chzn-done');
				this.siblings('.chzn-container').remove();

				// Apply Chosen
				applyChosen(this);

				this._super();
			},
			onunmatch: function() {
				this._super();
			}
		});

		$(".cms-panel-layout").entwine({
			redraw: function() {
				if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
			}
		});

		/**
		 * Overload the default GridField behaviour (open a new URL in the browser)
		 * with the CMS-specific ajax loading.
		 */
		$('.cms .ss-gridfield').entwine({
			showDetailView: function(url) {
				// Include any GET parameters from the current URL, as the view state might depend on it.
				// For example, a list prefiltered through external search criteria might be passed to GridField.
				var params = window.location.search.replace(/^\?/, '');
				if(params) url = $.path.addSearchParams(url, params);
				$('.cms-container').loadPanel(url);
			}
		});


		/**
		 * Generic search form in the CMS, often hooked up to a GridField results display.
		 */
		$('.cms-search-form').entwine({
			onsubmit: function(e) {
				// Remove empty elements and make the URL prettier
				var nonEmptyInputs,
					url;

				nonEmptyInputs = this.find(':input:not(:submit)').filter(function() {
					// Use fieldValue() from jQuery.form plugin rather than jQuery.val(),
					// as it handles checkbox values more consistently
					var vals = $.grep($(this).fieldValue(), function(val) { return (val);});
					return (vals.length);
				});

				url = this.attr('action');

				if(nonEmptyInputs.length) {
					url = $.path.addSearchParams(url, nonEmptyInputs.serialize());
				}

				var container = this.closest('.cms-container');
				container.find('.cms-edit-form').tabs('select',0);  //always switch to the first tab (list view) when searching
				container.loadPanel(url, "", {}, true);

				return false;
			}
		});

		/**
		 * Reset button handler. IE8 does not bubble reset events to
		 */
		$(".cms-search-form button[type=reset], .cms-search-form input[type=reset]").entwine({
			onclick: function(e) {
				e.preventDefault();

				var form = $(this).parents('form');

				form.clearForm();
				form.find(".dropdown select").prop('selectedIndex', 0).trigger("liszt:updated"); // Reset chosen.js
				form.submit();
				}
		});

		/**
		 * Allows to lazy load a panel, by leaving it empty
		 * and declaring a URL to load its content via a 'url' HTML5 data attribute.
		 * The loaded HTML is cached, with cache key being the 'url' attribute.
		 * In order for this to work consistently, we assume that the responses are stateless.
		 * To avoid caching, add a 'deferred-no-cache' to the node.
		 */
		window._panelDeferredCache = {};
		$('.cms-panel-deferred').entwine({
			onadd: function() {
				this._super();
				this.redraw();
			},
			onremove: function() {
				if(window.debug) console.log('saving', this.data('url'), this);

				// Save the HTML state at the last possible moment.
				// Don't store the DOM to avoid memory leaks.
				if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
				this._super();
			},
			redraw: function() {
				if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

				var self = this, url = this.data('url');
				if(!url) throw 'Elements of class .cms-panel-deferred need a "data-url" attribute';

				this._super();

				// If the node is empty, try to either load it from cache or via ajax.
				if(!this.children().length) {
					if(!this.data('deferredNoCache') && typeof window._panelDeferredCache[url] !== 'undefined') {
						this.html(window._panelDeferredCache[url]);
					} else {
						this.addClass('loading');
						$.ajax({
							url: url,
							complete: function() {
								self.removeClass('loading');
							},
							success: function(data, status, xhr) {
								self.html(data);
							}
						});
					}
				}
			}
		});

		/**
		 * Lightweight wrapper around jQuery UI tabs.
		 * Ensures that anchor links are set properly,
		 * and any nested tabs are scrolled if they have
		 * their height explicitly set. This is important
		 * for forms inside the CMS layout.
		 */
		$('.cms-tabset').entwine({
			onadd: function() {
				// Can't name redraw() as it clashes with other CMS entwine classes
				this.redrawTabs();
				this._super();
			},
			onremove: function() {
				if (this.data('tabs')) this.tabs('destroy');
				this._super();
			},
			redrawTabs: function() {
				this.rewriteHashlinks();

				var id = this.attr('id'), activeTab = this.find('ul:first .ui-tabs-active');

				if(!this.data('uiTabs')) this.tabs({
					active: (activeTab.index() != -1) ? activeTab.index() : 0,
					beforeLoad: function(e, ui) {
						// Disable automatic ajax loading of tabs without matching DOM elements,
						// determining if the current URL differs from the tab URL is too error prone.
						return false;
					},
					activate: function(e, ui) {
						// Accessibility: Simulate click to trigger panel load when tab is focused
						// by a keyboard navigation event rather than a click
						if(ui.newTab) {
							ui.newTab.find('.cms-panel-link').click();
						}

						// Usability: Hide actions for "readonly" tabs (which don't contain any editable fields)
						var actions = $(this).closest('form').find('.Actions');
						if($(ui.newTab).closest('li').hasClass('readonly')) {
							actions.fadeOut();
						} else {
							actions.show();
						}
					}
				});

				this.trigger('afterredrawtabs');
			},

			/**
			 * Ensure hash links are prefixed with the current page URL,
			 * otherwise jQuery interprets them as being external.
			 */
			rewriteHashlinks: function() {
				$(this).find('ul a').each(function() {
					if (!$(this).attr('href')) return;
					var matches = $(this).attr('href').match(/#.*/);
					if(!matches) return;
					$(this).attr('href', document.location.href.replace(/#.*/, '') + matches[0]);
				});
			}
		});

		/**
		 * CMS content filters
		 */
		$('#filters-button').entwine({
			onmatch: function () {
				this._super();

				this.data('collapsed', true); // The current collapsed state of the element.
				this.data('animating', false); // True if the element is currently animating.
			},
			onunmatch: function () {
				this._super();
			},
			showHide: function () {
				var self = this,
					$filters = $('.cms-content-filters').first(),
					collapsed = this.data('collapsed');

				// Prevent the user from spamming the UI with animation requests.
				if (this.data('animating')) {
					return;
				}

				this.toggleClass('active');
				this.data('animating', true);

				// Slide the element down / up based on it's current collapsed state.
				$filters[collapsed ? 'slideDown' : 'slideUp']({
					complete: function () {
						// Update the element's state.
						self.data('collapsed', !collapsed);
						self.data('animating', false);
					}
				});
			},
			onclick: function () {
				this.showHide();
			}
		});
	});

}(jQuery));

var statusMessage = function(text, type) {
	text = jQuery('<div/>').text(text).html(); // Escape HTML entities in text
	jQuery.noticeAdd({text: text, type: type, stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});
};

var errorMessage = function(text) {
	jQuery.noticeAdd({text: text, type: 'error', stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});
};