/** * File: LeftAndMain.EditForm.js */ (function($) { // Can't bind this through jQuery window.onbeforeunload = function(e) { var form = $('.cms-edit-form'); form.trigger('beforesubmitform'); if(form.is('.changed')) return ss.i18n._t('LeftAndMain.CONFIRMUNSAVEDSHORT'); }; $.entwine('ss', function($){ /** * Class: .cms-edit-form * * Base edit form, provides ajaxified saving * and reloading itself through the ajax return values. * Takes care of resizing tabsets within the layout container. * * Change tracking is enabled on all fields within the form. If you want * to disable change tracking for a specific field, add a "no-change-track" * class to it. * * @name ss.Form_EditForm * @require jquery.changetracker * * Events: * ajaxsubmit - Form is about to be submitted through ajax * validate - Contains validation result * load - Form is about to be loaded through ajax */ $('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{ /** * Variable: PlaceholderHtml * (String_ HTML text to show when no form content is chosen. * Will show inside the
tag. */ PlaceholderHtml: '', /** * Variable: ChangeTrackerOptions * (Object) */ ChangeTrackerOptions: { ignoreFieldSelector: '.no-change-track, .ss-upload :input, .cms-navigator :input' }, /** * Constructor: onmatch */ onadd: function() { var self = this; // Turn off autocomplete to fix the access tab randomly switching radio buttons in Firefox // when refresh the page with an anchor tag in the URL. E.g: /admin#Root_Access. // Autocomplete in the CMS also causes strangeness in other browsers, // filling out sections of the form that the user does not want to be filled out, // so this turns it off for all browsers. // See the following page for demo and explanation of the Firefox bug: // http://www.ryancramer.com/journal/entries/radio_buttons_firefox/ this.attr("autocomplete", "off"); this._setupChangeTracker(); // Catch navigation events before they reach handleStateChange(), // in order to avoid changing the menu state if the action is cancelled by the user // $('.cms-menu') // Optionally get the form attributes from embedded fields, see Form->formHtmlContent() for(var overrideAttr in {'action':true,'method':true,'enctype':true,'name':true}) { var el = this.find(':input[name='+ '_form_' + overrideAttr + ']'); if(el) { this.attr(overrideAttr, el.val()); el.remove(); } } // TODO // // Rewrite # links // html = html.replace(/(]+href *= *")#/g, '$1' + window.location.href.replace(/#.*$/,'') + '#'); // // // Rewrite iframe links (for IE) // html = html.replace(/(]*src=")([^"]+)("[^>]*>)/g, '$1' + $('base').attr('href') + '$2$3'); // Show validation errors if necessary if(this.hasClass('validationerror')) { // Ensure the first validation error is visible var tabError = this.find('.message.validation, .message.required').first().closest('.tab'); $('.cms-container').clearCurrentTabState(); // clear state to avoid override later on tabError.closest('.ss-tabset').tabs('option', 'active', tabError.index('.tab')); } this._super(); }, onremove: function() { this.changetracker('destroy'); this._super(); }, onmatch: function() { this._super(); }, onunmatch: function() { this._super(); }, redraw: function() { if(window.debug) console.log('redraw', this.attr('class'), this.get(0)); // Force initialization of tabsets to avoid layout glitches this.add(this.find('.cms-tabset')).redrawTabs(); this.find('.cms-content-header').redraw(); }, /** * Function: _setupChangeTracker */ _setupChangeTracker: function() { // Don't bind any events here, as we dont replace the // full tag by any ajax updates they won't automatically reapply this.changetracker(this.getChangeTrackerOptions()); }, /** * Function: confirmUnsavedChanges * * Checks the jquery.changetracker plugin status for this form, * and asks the user for confirmation via a browser dialog if changes are detected. * Doesn't cancel any unload or form removal events, you'll need to implement this based on the return * value of this message. * * If changes are confirmed for discard, the 'changed' flag is reset. * * Returns: * (Boolean) FALSE if the user wants to abort with changes present, TRUE if no changes are detected * or the user wants to discard them. */ confirmUnsavedChanges: function() { this.trigger('beforesubmitform'); if(!this.is('.changed')) { return true; } var confirmed = confirm(ss.i18n._t('LeftAndMain.CONFIRMUNSAVED')); if(confirmed) { // confirm discard changes this.removeClass('changed'); } return confirmed; }, /** * Function: onsubmit * * Suppress submission unless it is handled through ajaxSubmit(). */ onsubmit: function(e, button) { // Only submit if a button is present. // This supressed submits from ENTER keys in input fields, // which means the browser auto-selects the first available form button. // This might be an unrelated button of the form field, // or a destructive action (if "save" is not available, or not on first position). if(this.prop("target") != "_blank") { if(button) this.closest('.cms-container').submitForm(this, button); return false; } }, /** * Function: validate * * Hook in (optional) validation routines. * Currently clientside validation is not supported out of the box in the CMS. * * Todo: * Placeholder implementation * * Returns: * {boolean} */ validate: function() { var isValid = true; this.trigger('validate', {isValid: isValid}); return isValid; }, /* * Track focus on htmleditor fields */ 'from .htmleditor': { oneditorinit: function(e){ var self = this, field = $(e.target).closest('.field.htmleditor'), editor = field.find('textarea.htmleditor').getEditor().getInstance(); // TinyMCE 4 will add a focus event, but for now, use click editor.onClick.add(function(e){ self.saveFieldFocus(field.attr('id')); }); } }, /* * Track focus on inputs */ 'from .cms-edit-form :input:not(:submit)': { onclick: function(e){ this.saveFieldFocus($(e.target).attr('id')); }, onfocus: function(e){ this.saveFieldFocus($(e.target).attr('id')); } }, /* * Track focus on treedropdownfields. */ 'from .cms-edit-form .treedropdown *': { onfocusin: function(e){ var field = $(e.target).closest('.field.treedropdown'); this.saveFieldFocus(field.attr('id')); } }, /* * Track focus on chosen selects */ 'from .cms-edit-form .dropdown .chzn-container a': { onfocusin: function(e){ var field = $(e.target).closest('.field.dropdown'); this.saveFieldFocus(field.attr('id')); } }, /* * Restore fields after tabs are restored */ 'from .cms-container': { ontabstaterestored: function(e){ this.restoreFieldFocus(); } }, /* * Saves focus in Window session storage so it that can be restored on page load */ saveFieldFocus: function(selected){ if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return; var id = $(this).attr('id'), focusElements = []; focusElements.push({ id:id, selected:selected }); if(focusElements) { try { window.sessionStorage.setItem(id, JSON.stringify(focusElements)); } 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 focus state. // This is a Safari bug which happens when private browsing is enabled. return; } else { throw err; } } } }, /** * Set focus or window to previously saved fields. * Requires HTML5 sessionStorage support. * * Must follow tab restoration, as reliant on active tab */ restoreFieldFocus: function(){ if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return; var self = this, hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage), sessionData = hasSessionStorage ? window.sessionStorage.getItem(this.attr('id')) : null, sessionStates = sessionData ? JSON.parse(sessionData) : false, elementID, tabbed = (this.find('.ss-tabset').length !== 0), activeTab, elementTab, toggleComposite, scrollY; if(hasSessionStorage && sessionStates.length > 0){ $.each(sessionStates, function(i, sessionState) { if(self.is('#' + sessionState.id)){ elementID = $('#' + sessionState.selected); } }); // If the element IDs saved in session states don't match up to anything in this particular form // that probably means we haven't encountered this form yet, so focus on the first input if($(elementID).length < 1){ this.focusFirstInput(); return; } activeTab = $(elementID).closest('.ss-tabset').find('.ui-tabs-nav .ui-tabs-active .ui-tabs-anchor').attr('id'); elementTab = 'tab-' + $(elementID).closest('.ss-tabset .ui-tabs-panel').attr('id'); // Last focussed element differs to last selected tab, do nothing if(tabbed && elementTab !== activeTab){ return; } toggleComposite = $(elementID).closest('.togglecomposite'); //Reopen toggle fields if(toggleComposite.length > 0){ toggleComposite.accordion('activate', toggleComposite.find('.ui-accordion-header')); } //Calculate position for scroll scrollY = $(elementID).position().top; //Fall back to nearest visible element if hidden (for select type fields) if(!$(elementID).is(':visible')){ elementID = '#' + $(elementID).closest('.field').attr('id'); scrollY = $(elementID).position().top; } //set focus to focus variable if element focusable $(elementID).focus(); // Scroll fallback when element is not focusable // Only scroll if element at least half way down window if(scrollY > $(window).height() / 2){ self.find('.cms-content-fields').scrollTop(scrollY); } } else { // If session storage is not supported or there is nothing stored yet, focus on the first input this.focusFirstInput(); } }, /** * Skip if an element in the form is already focused. Exclude elements which specifically * opt-out of this behaviour via "data-skip-autofocus". This opt-out is useful if the * first visible field is shown far down a scrollable area, for example for the pagination * input field after a long GridField listing. */ focusFirstInput: function() { this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus(); } }); /** * Class: .cms-edit-form .Actions :submit * * All buttons in the right CMS form go through here by default. * We need this onclick overloading because we can't get to the * clicked button from a form.onsubmit event. */ $('.cms-edit-form .Actions input.action[type=submit], .cms-edit-form .Actions button.action').entwine({ /** * Function: onclick */ onclick: function(e) { // Confirmation on delete. if( this.hasClass('gridfield-button-delete') && !confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE')) ) { e.preventDefault(); return false; } if(!this.is(':disabled')) { this.parents('form').trigger('submit', [this]); } e.preventDefault(); return false; } }); /** * If we've a history state to go back to, go back, otherwise fall back to * submitting the form with the 'doCancel' action. */ $('.cms-edit-form .Actions input.action[type=submit].ss-ui-action-cancel, .cms-edit-form .Actions button.action.ss-ui-action-cancel').entwine({ onclick: function(e) { if (History.getStateByIndex(1)) { History.back(); } else { this.parents('form').trigger('submit', [this]); } e.preventDefault(); } }); /** * Hide tabs when only one is available. * Special case is actiontabs - tabs between buttons, where we want to have * extra options hidden within a tab (even if only one) by default. */ $('.cms-edit-form .ss-tabset').entwine({ onmatch: function() { if (!this.hasClass('ss-ui-action-tabset')) { var tabs = this.find("> ul:first"); if(tabs.children("li").length == 1) { tabs.hide().parent().addClass("ss-tabset-tabshidden"); } } this._super(); }, onunmatch: function() { this._super(); } }); }); }(jQuery));