jQuery.noConflict();
/**
* File: LeftAndMain.js
*/
(function($) {
window.onresize = function(e) {
// Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event.
$('.cms-container').trigger('windowresize');
};
// setup jquery.entwine
$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
$.entwine('ss', function($) {
/**
* 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
' + 'Your browser is not compatible with the CMS interface. Please use Internet Explorer 7+, Google Chrome 10+ or Mozilla Firefox 3.5+.' + '
' ).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(){ this.handleStateChange(); } }, '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. You can provide any or * all options. The remaining options will not be changed. */ updateLayoutOptions: function(newSpec) { var spec = this.getLayoutOptions(); $.extend(spec, newSpec); this.redraw(); }, /** * Enable the split view - with content on the left and preview on the right. */ splitViewMode: function() { this.updateLayoutOptions({ mode: 'split' }); this.redraw(); }, /** * Content only. */ contentViewMode: function() { this.updateLayoutOptions({ mode: 'content' }); this.redraw(); }, /** * Preview only. */ previewMode: function() { this.updateLayoutOptions({ mode: 'preview' }); this.redraw(); }, redraw: function() { 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(); }, /** * 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) { if(!data) data = {}; if(!title) title = ""; // Check change tracking (can't use events as we need a way to cancel the current state change) var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']); var trackedEls = contentEls.find(':data(changetracker)').add(contentEls.filter(':data(changetracker)')); if(trackedEls.length) { var abort = false; trackedEls.each(function() { if(!$(this).confirmUnsavedChanges()) abort = true; }); if(abort) return; } // Save tab selections so we can restore them later this.saveTabState(); if(window.History.enabled) { // 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. formData.push({name: 'BackURL', value:History.getPageUrl()}); // 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; }, /** * 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() { // Don't allow parallel loading to avoid edge cases if(this.getCurrentXHR()) this.getCurrentXHR().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 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; contentEls.addClass('loading'); var xhr = $.ajax({ headers: headers, url: state.url, complete: function() { // 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.setCurrentXHR(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')) { document.location.href = xhr.getResponseHeader('X-ControllerURL'); return; } // Pseudo-redirects via X-ControllerURL might return empty data, in which // case we'll ignore the response if(!data) return; // Support a full reload if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) { document.location.href = xhr.getResponseHeader('X-ControllerURL'); 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\/json[ \t]*;?/i)) { newFragments = data; } else { // Fall back to replacing the content fragment if HTML is returned $data = $(data); // 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; } // 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'); this.redraw(); this.restoreTabState(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').each(function() { var index, tabset = $(this), tabsetId = tabset.attr('id'), tab, forcedTab = tabset.find('.ss-tabs-force-active'); if(!tabset.data('tabs')) return; // don't act on uninit'ed controls if(forcedTab.length) { index = forcedTab.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, sessionState) { if(tabset.is('#' + sessionState.id)) index = sessionState.selected; }); } if(index !== null) tabset.tabs('select', index); }); }, /** * 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