mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch 'pjax-multiple'
This commit is contained in:
commit
12f2e1e176
@ -352,7 +352,6 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
if($this->request->isAjax()) {
|
if($this->request->isAjax()) {
|
||||||
$this->response->addHeader('X-ControllerURL', $url);
|
$this->response->addHeader('X-ControllerURL', $url);
|
||||||
if($header = $this->request->getHeader('X-Pjax')) $this->response->addHeader('X-Pjax', $header);
|
if($header = $this->request->getHeader('X-Pjax')) $this->response->addHeader('X-Pjax', $header);
|
||||||
if($header = $this->request->getHeader('X-Pjax-Selector')) $this->response->addHeader('X-Pjax-Selector', $header);
|
|
||||||
return ''; // Actual response will be re-requested by client
|
return ''; // Actual response will be re-requested by client
|
||||||
} else {
|
} else {
|
||||||
parent::redirect($url, $code);
|
parent::redirect($url, $code);
|
||||||
@ -437,6 +436,9 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
'Content' => function() use(&$controller) {
|
'Content' => function() use(&$controller) {
|
||||||
return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
|
return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
|
||||||
},
|
},
|
||||||
|
'Breadcrumbs' => function() use (&$controller) {
|
||||||
|
return $controller->renderWith('CMSBreadcrumbs');
|
||||||
|
},
|
||||||
'default' => function() use(&$controller) {
|
'default' => function() use(&$controller) {
|
||||||
return $controller->renderWith($controller->getViewer('show'));
|
return $controller->renderWith($controller->getViewer('show'));
|
||||||
}
|
}
|
||||||
@ -951,6 +953,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
$form->addExtraClass('cms-edit-form');
|
$form->addExtraClass('cms-edit-form');
|
||||||
$form->loadDataFrom($record);
|
$form->loadDataFrom($record);
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
||||||
|
|
||||||
// Set this if you want to split up tabs into a separate header row
|
// Set this if you want to split up tabs into a separate header row
|
||||||
// if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
// if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
||||||
@ -1015,6 +1018,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
|||||||
$form->addExtraClass('cms-edit-form');
|
$form->addExtraClass('cms-edit-form');
|
||||||
$form->addExtraClass('root-form');
|
$form->addExtraClass('root-form');
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
||||||
|
|
||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
@ -151,6 +151,7 @@ abstract class ModelAdmin extends LeftAndMain {
|
|||||||
$form->addExtraClass('cms-edit-form cms-panel-padded center');
|
$form->addExtraClass('cms-edit-form cms-panel-padded center');
|
||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'EditForm'));
|
$form->setFormAction(Controller::join_links($this->Link($this->modelClass), 'EditForm'));
|
||||||
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
||||||
|
|
||||||
$this->extend('updateEditForm', $form);
|
$this->extend('updateEditForm', $form);
|
||||||
|
|
||||||
|
@ -160,6 +160,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider {
|
|||||||
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
|
||||||
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
||||||
$form->addExtraClass('center ss-tabset cms-tabset ' . $this->BaseCSSClasses());
|
$form->addExtraClass('center ss-tabset cms-tabset ' . $this->BaseCSSClasses());
|
||||||
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
|
||||||
|
|
||||||
$this->extend('updateEditForm', $form);
|
$this->extend('updateEditForm', $form);
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
data.push({name:button.attr('name'),value:button.val()});
|
data.push({name:button.attr('name'),value:button.val()});
|
||||||
|
|
||||||
// TODO Should be set by hiddenfield already
|
// TODO Should be set by hiddenfield already
|
||||||
$('.cms-content').submitForm(
|
$('.cms-container').submitForm(
|
||||||
this,
|
this,
|
||||||
button,
|
button,
|
||||||
function() {
|
function() {
|
||||||
|
@ -29,169 +29,6 @@
|
|||||||
this.add(this.find('.cms-tabset')).redrawTabs();
|
this.add(this.find('.cms-tabset')).redrawTabs();
|
||||||
|
|
||||||
this.layout();
|
this.layout();
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function: ajaxSubmit
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* {DOMElement} button - The pressed button (optional)
|
|
||||||
* {Function} callback - Called in complete() handler of jQuery.ajax()
|
|
||||||
* {Object} ajaxOptions - Object literal to merge into $.ajax() call
|
|
||||||
* {boolean} loadResponse - Render response through _loadResponse() (Default: true)
|
|
||||||
*
|
|
||||||
* Returns:
|
|
||||||
* (boolean)
|
|
||||||
*/
|
|
||||||
submitForm: function(form, button, callback, ajaxOptions, loadResponse) {
|
|
||||||
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('beforesave');
|
|
||||||
this.trigger('submitform', {form: form, button: button});
|
|
||||||
|
|
||||||
// set button to "submitting" state
|
|
||||||
$(button).addClass('loading');
|
|
||||||
|
|
||||||
// validate if required
|
|
||||||
if(!form.validate()) {
|
|
||||||
// TODO Automatically switch to the tab/position of the first error
|
|
||||||
statusMessage("Validation failed.", "bad");
|
|
||||||
|
|
||||||
$(button).removeClass('loading');
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save tab selections in order to reconstruct them later
|
|
||||||
var selectedTabs = [];
|
|
||||||
form.find('.cms-tabset').each(function(i, el) {
|
|
||||||
if($(el).attr('id')) selectedTabs.push({id:$(el).attr('id'), selected:$(el).tabs('option', 'selected')});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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()});
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
'X-Pjax-Selector': '.cms-edit-form'
|
|
||||||
},
|
|
||||||
url: form.attr('action'),
|
|
||||||
data: formData,
|
|
||||||
type: 'POST',
|
|
||||||
complete: function(xmlhttp, status) {
|
|
||||||
$(button).removeClass('loading');
|
|
||||||
|
|
||||||
// TODO This should be using the plugin API
|
|
||||||
form.removeClass('changed');
|
|
||||||
|
|
||||||
if(callback) callback(xmlhttp, status);
|
|
||||||
|
|
||||||
// pass along original form data to enable old/new comparisons
|
|
||||||
if(loadResponse !== false) {
|
|
||||||
self.submitForm_responseHandler(form, xmlhttp.responseText, status, xmlhttp, formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-init tabs (in case the form tag itself is a tabset)
|
|
||||||
if(self.hasClass('cms-tabset')) self.removeClass('cms-tabset').addClass('cms-tabset');
|
|
||||||
|
|
||||||
// re-select previously saved tabs
|
|
||||||
$.each(selectedTabs, function(i, selectedTab) {
|
|
||||||
form.find('#' + selectedTab.id).tabs('select', selectedTab.selected);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redraw the layout
|
|
||||||
$('.cms-container').redraw();
|
|
||||||
},
|
|
||||||
dataType: 'html'
|
|
||||||
}, ajaxOptions));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function: _loadResponse
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* {String} data - Either HTML for straight insertion, or eval'ed JavaScript.
|
|
||||||
* If passed as HTML, it is assumed that everying inside the <form> tag is replaced,
|
|
||||||
* but the old <form> tag itself stays intact.
|
|
||||||
* {String} status
|
|
||||||
* {XMLHTTPRequest} xmlhttp - ..
|
|
||||||
* {Array} origData - The original submitted data, useful to do comparisons of changed
|
|
||||||
* values in new form output, e.g. to detect a URLSegment being changed on the serverside.
|
|
||||||
* Array in jQuery serializeArray() notation.
|
|
||||||
*/
|
|
||||||
submitForm_responseHandler: function(oldForm, data, status, xmlhttp, origData) {
|
|
||||||
if(status == 'success') {
|
|
||||||
if(!data) return;
|
|
||||||
|
|
||||||
var form, newContent = $(data);
|
|
||||||
|
|
||||||
// HACK If response contains toplevel panel rather than a form, replace it instead.
|
|
||||||
// For example, a page view shows tree + edit form. Deleting this page redirects to
|
|
||||||
// the "pages" overview, which doesn't have a separate tree panel.
|
|
||||||
if(newContent.is('.cms-content')) {
|
|
||||||
$('.cms-content').replaceWith(newContent);
|
|
||||||
} else {
|
|
||||||
form = this.replaceForm(oldForm, newContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof(Behaviour) != 'undefined') Behaviour.apply(); // refreshes ComplexTableField
|
|
||||||
|
|
||||||
this.trigger('reloadeditform', {form: form, origData: origData, xmlhttp: xmlhttp});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {jQuery} New form element
|
|
||||||
*/
|
|
||||||
replaceForm: function(form, html) {
|
|
||||||
if(html) {
|
|
||||||
var parent = form.parent(), id = form.attr('id');
|
|
||||||
form.replaceWith(html);
|
|
||||||
// Try to get the new form by ID (assuming they're identical), otherwise fall back to the first form in the parent
|
|
||||||
return id ? $('#' + id) : parent.children('form:first');
|
|
||||||
} else {
|
|
||||||
this.removeForm(form);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function: removeForm
|
|
||||||
*
|
|
||||||
* Remove everying inside the <form> tag
|
|
||||||
* with a custom HTML fragment. Useful e.g. for deleting a page in the CMS.
|
|
||||||
* Checks for unsaved changes before removing the form
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* {String} placeholderHtml - Short note why the form has been removed, displayed in <p> tags.
|
|
||||||
* Falls back to the default RemoveText() option (Optional)
|
|
||||||
*/
|
|
||||||
removeForm: function(form, placeholderHtml) {
|
|
||||||
if(!placeholderHtml) placeholderHtml = this.getPlaceholderHtml();
|
|
||||||
// Alert when unsaved changes are present
|
|
||||||
if(!form.confirmUnsavedChanges()) return;
|
|
||||||
this.trigger('removeform');
|
|
||||||
this.html(placeholderHtml);
|
|
||||||
// TODO This should be using the plugin API
|
|
||||||
this.removeClass('changed');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -245,17 +82,6 @@
|
|||||||
this._super();
|
this._super();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.cms-content.loading,.cms-edit-form.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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
@ -29,7 +29,6 @@
|
|||||||
* Events:
|
* Events:
|
||||||
* ajaxsubmit - Form is about to be submitted through ajax
|
* ajaxsubmit - Form is about to be submitted through ajax
|
||||||
* validate - Contains validation result
|
* validate - Contains validation result
|
||||||
* removeform - A form is about to be removed from the DOM
|
|
||||||
* load - Form is about to be loaded through ajax
|
* load - Form is about to be loaded through ajax
|
||||||
*/
|
*/
|
||||||
$('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{
|
$('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{
|
||||||
@ -158,7 +157,7 @@
|
|||||||
// which means the browser auto-selects the first available form button.
|
// which means the browser auto-selects the first available form button.
|
||||||
// This might be an unrelated button of the form field,
|
// This might be an unrelated button of the form field,
|
||||||
// or a destructive action (if "save" is not available, or not on first position).
|
// or a destructive action (if "save" is not available, or not on first position).
|
||||||
if(button) this.closest('.cms-content').submitForm(this, button);
|
if(button) this.closest('.cms-container').submitForm(this, button);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -40,10 +40,7 @@ jQuery.noConflict();
|
|||||||
// Normalize trailing slashes in URL to work around routing weirdnesses in SS_HTTPRequest.
|
// Normalize trailing slashes in URL to work around routing weirdnesses in SS_HTTPRequest.
|
||||||
var isSame = (url && History.getPageUrl().replace(/\/+$/, '') == url.replace(/\/+$/, ''));
|
var isSame = (url && History.getPageUrl().replace(/\/+$/, '') == url.replace(/\/+$/, ''));
|
||||||
if(url && !isSame) {
|
if(url && !isSame) {
|
||||||
opts = {
|
opts = {pjax: xhr.getResponseHeader('X-Pjax') ? xhr.getResponseHeader('X-Pjax') : settings.headers['X-Pjax']};
|
||||||
pjax: xhr.getResponseHeader('X-Pjax') ? xhr.getResponseHeader('X-Pjax') : settings.headers['X-Pjax'],
|
|
||||||
selector: xhr.getResponseHeader('X-Pjax-Selector') ? xhr.getResponseHeader('X-Pjax-Selector') : settings.headers['X-Pjax-Selector']
|
|
||||||
};
|
|
||||||
window.History.pushState(opts, '', url);
|
window.History.pushState(opts, '', url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,11 +156,10 @@ jQuery.noConflict();
|
|||||||
loadPanel: function(url, title, data) {
|
loadPanel: function(url, title, data) {
|
||||||
if(!data) data = {};
|
if(!data) data = {};
|
||||||
if(!title) title = "";
|
if(!title) title = "";
|
||||||
if(!data.selector) data.selector = '.cms-content';
|
|
||||||
var contentEl = $(data.selector);
|
|
||||||
|
|
||||||
// Check change tracking (can't use events as we need a way to cancel the current state change)
|
// Check change tracking (can't use events as we need a way to cancel the current state change)
|
||||||
var trackedEls = contentEl.find(':data(changetracker)').add(contentEl.filter(':data(changetracker)'));
|
var contentEls = this._findFragments(data.pjax ? data.pjax.split(',') : ['Content']);
|
||||||
|
var trackedEls = contentEls.find(':data(changetracker)').add(contentEls.filter(':data(changetracker)'));
|
||||||
|
|
||||||
if(trackedEls.length) {
|
if(trackedEls.length) {
|
||||||
var abort = false;
|
var abort = false;
|
||||||
@ -183,6 +179,98 @@ jQuery.noConflict();
|
|||||||
window.location = $.path.makeUrlAbsolute(url, $('base').attr('href'));
|
window.location = $.path.makeUrlAbsolute(url, $('base').attr('href'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('beforesave');
|
||||||
|
this.trigger('submitform', {form: form, button: button});
|
||||||
|
|
||||||
|
// set button to "submitting" state
|
||||||
|
$(button).addClass('loading');
|
||||||
|
|
||||||
|
// validate if required
|
||||||
|
if(!form.validate()) {
|
||||||
|
// TODO Automatically switch to the tab/position of the first error
|
||||||
|
statusMessage("Validation failed.", "bad");
|
||||||
|
|
||||||
|
$(button).removeClass('loading');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save tab selections in order to reconstruct them later
|
||||||
|
var selectedTabs = [];
|
||||||
|
form.find('.cms-tabset').each(function(i, el) {
|
||||||
|
if($(el).attr('id')) selectedTabs.push({id:$(el).attr('id'), selected:$(el).tabs('option', 'selected')});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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()});
|
||||||
|
|
||||||
|
// 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(xmlhttp, status);
|
||||||
|
|
||||||
|
var newContentEls = self.handleAjaxResponse(data, status, xhr);
|
||||||
|
if(!newContentEls) return;
|
||||||
|
|
||||||
|
var newForm = newContentEls.filter('form');
|
||||||
|
|
||||||
|
// Re-init tabs (in case the form tag itself is a tabset)
|
||||||
|
if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
|
||||||
|
|
||||||
|
// re-select previously saved tabs
|
||||||
|
$.each(selectedTabs, function(i, selectedTab) {
|
||||||
|
newForm.find('#' + selectedTab.id).tabs('select', selectedTab.selected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redraw the layout
|
||||||
|
$('.cms-container').redraw();
|
||||||
|
|
||||||
|
form.trigger('reloadeditform', {form: newForm, formData: formData, xmlhttp: xhr});
|
||||||
|
},
|
||||||
|
dataType: 'json'
|
||||||
|
}, ajaxOptions));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles ajax loading of new panels through the window.History object.
|
* Handles ajax loading of new panels through the window.History object.
|
||||||
@ -207,91 +295,129 @@ jQuery.noConflict();
|
|||||||
* if the URL is loaded without ajax.
|
* if the URL is loaded without ajax.
|
||||||
*/
|
*/
|
||||||
handleStateChange: function() {
|
handleStateChange: function() {
|
||||||
var self = this, h = window.History, state = h.getState();
|
|
||||||
|
|
||||||
// Don't allow parallel loading to avoid edge cases
|
// Don't allow parallel loading to avoid edge cases
|
||||||
if(this.getCurrentXHR()) this.getCurrentXHR().abort();
|
if(this.getCurrentXHR()) this.getCurrentXHR().abort();
|
||||||
|
|
||||||
|
var self = this, h = window.History, state = h.getState(),
|
||||||
|
fragments = state.data.pjax || 'Content', headers = {},
|
||||||
|
contentEls = this._findFragments(fragments.split(','));
|
||||||
|
|
||||||
var selector = state.data.selector || '.cms-content', contentEl = $(selector);
|
this.trigger('beforestatechange', {state: state, element: contentEls});
|
||||||
|
|
||||||
this.trigger('beforestatechange', {
|
|
||||||
state: state, element: contentEl
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set Pjax headers, which can declare a preference for the returned view.
|
// Set Pjax headers, which can declare a preference for the returned view.
|
||||||
// The actually returned view isn't always decided upon when the request
|
// 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.
|
// is fired, so the server might decide to change it based on its own logic.
|
||||||
var headers = {};
|
headers['X-Pjax'] = fragments;
|
||||||
if(state.data.pjax) {
|
|
||||||
headers['X-Pjax'] = state.data.pjax;
|
|
||||||
} else {
|
|
||||||
// Standard Pjax behaviour is to replace right content area
|
|
||||||
headers["X-Pjax"] = 'Content';
|
|
||||||
}
|
|
||||||
headers['X-Pjax-Selector'] = selector;
|
|
||||||
|
|
||||||
contentEl.addClass('loading');
|
contentEls.addClass('loading');
|
||||||
var xhr = $.ajax({
|
var xhr = $.ajax({
|
||||||
headers: headers,
|
headers: headers,
|
||||||
url: state.url,
|
url: state.url,
|
||||||
success: function(data, status, xhr) {
|
complete: function() {
|
||||||
// Pseudo-redirects via X-ControllerURL might return empty data, in which
|
// Remove loading indication from old content els (regardless of which are replaced)
|
||||||
// case we'll ignore the response
|
contentEls.removeClass('loading');
|
||||||
if(!data) return;
|
|
||||||
|
|
||||||
// Update title
|
|
||||||
var title = xhr.getResponseHeader('X-Title');
|
|
||||||
if(title) document.title = title;
|
|
||||||
|
|
||||||
// Update panels
|
|
||||||
var newContentEl = $(data);
|
|
||||||
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
|
|
||||||
newContentEl.addClass('loading');
|
|
||||||
var origStyle = contentEl.attr('style');
|
|
||||||
var layoutClasses = ['east', 'west', 'center', 'north', 'south'];
|
|
||||||
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);
|
|
||||||
newContentEl.css('visibility', 'hidden');
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Unset loading and restore element state (to avoid breaking existing panel visibility, e.g. with preview expanded)
|
|
||||||
self.redraw();
|
|
||||||
newContentEl.css('visibility', 'visible');
|
|
||||||
newContentEl.removeClass('loading');
|
|
||||||
|
|
||||||
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: newContentEl});
|
|
||||||
},
|
},
|
||||||
error: function(xhr, status, e) {
|
success: function(data, status, xhr) {
|
||||||
contentEl.removeClass('loading');
|
var els = self.handleAjaxResponse(data, status, xhr);
|
||||||
errorMessage(e);
|
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setCurrentXHR(xhr);
|
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.
|
||||||
|
*/
|
||||||
|
handleAjaxResponse: function(data, status, xhr) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// 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 = title;
|
||||||
|
|
||||||
|
var newFragments = {}, newContentEls = $([]);
|
||||||
|
if(xhr.getResponseHeader('Content-Type') == 'text/json') {
|
||||||
|
newFragments = data;
|
||||||
|
} else {
|
||||||
|
// Fall back to replacing the content fragment if HTML is returned
|
||||||
|
newFragments['Content'] = 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
|
||||||
|
newContentEls.add(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 origVisible = contentEl.is(':visible');
|
||||||
|
var layoutClasses = ['east', 'west', 'center', 'north', 'south'];
|
||||||
|
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);
|
||||||
|
newContentEl.css('visibility', 'hidden');
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Unset loading and restore element state (to avoid breaking existing panel visibility, e.g. with preview expanded)
|
||||||
|
if(origVisible) newContentEl.css('visibility', 'visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
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
|
* Function: refresh
|
||||||
*
|
*
|
||||||
@ -314,6 +440,22 @@ jQuery.noConflict();
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Make all buttons "hoverable" with jQuery theming.
|
||||||
* Also sets the clicked button on a form submission, making it available through
|
* Also sets the clicked button on a form submission, making it available through
|
||||||
@ -346,16 +488,13 @@ jQuery.noConflict();
|
|||||||
* as opposed to triggering a full page reload.
|
* as opposed to triggering a full page reload.
|
||||||
* Little helper to avoid repetition, and make it easy to
|
* Little helper to avoid repetition, and make it easy to
|
||||||
* "opt in" to panel loading, while by default links still exhibit their default behaviour.
|
* "opt in" to panel loading, while by default links still exhibit their default behaviour.
|
||||||
* Same goes for breadcrumbs in the CMS.
|
* The PJAX target can be specified via a 'data-pjax-target' attribute.
|
||||||
*/
|
*/
|
||||||
$('.cms .cms-panel-link').entwine({
|
$('.cms .cms-panel-link').entwine({
|
||||||
onclick: function(e) {
|
onclick: function(e) {
|
||||||
var href = this.attr('href'),
|
var href = this.attr('href'),
|
||||||
url = (href && !href.match(/^#/)) ? href : this.data('href'),
|
url = (href && !href.match(/^#/)) ? href : this.data('href'),
|
||||||
data = {
|
data = {pjax: this.data('pjaxTarget')};
|
||||||
selector: this.data('targetPanel'),
|
|
||||||
pjax: this.data('pjax')
|
|
||||||
};
|
|
||||||
|
|
||||||
$('.cms-container').loadPanel(url, null, data);
|
$('.cms-container').loadPanel(url, null, data);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -681,7 +820,7 @@ jQuery.noConflict();
|
|||||||
this.rewriteHashlinks();
|
this.rewriteHashlinks();
|
||||||
|
|
||||||
var id = this.attr('id'), cookieId = 'ui-tabs-' + id,
|
var id = this.attr('id'), cookieId = 'ui-tabs-' + id,
|
||||||
selectedTab = this.find('ul:first .ui-state-selected');
|
selectedTab = this.find('ul:first .ui-tabs-selected');
|
||||||
|
|
||||||
// Fix for wrong cookie storage of deselected tabs
|
// Fix for wrong cookie storage of deselected tabs
|
||||||
if($.cookie && id && $.cookie(cookieId) == -1) $.cookie(cookieId, 0);
|
if($.cookie && id && $.cookie(cookieId) == -1) $.cookie(cookieId, 0);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="breadcrumbs-wrapper">
|
<div class="breadcrumbs-wrapper" data-pjax-fragment="Breadcrumbs">
|
||||||
<% loop Breadcrumbs %>
|
<% loop Breadcrumbs %>
|
||||||
<% if Last %>
|
<% if Last %>
|
||||||
<span class="cms-panel-link crumb">$Title.XML</span>
|
<span class="cms-panel-link crumb">$Title.XML</span>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="cms-content center $BaseCSSClasses" data-layout-type="border">
|
<div class="cms-content center $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
|
||||||
|
|
||||||
$Tools
|
$Tools
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="cms-content center $BaseCSSClasses" data-layout-type="border">
|
<div class="cms-content center $BaseCSSClasses" data-layout-type="border" data-pjax-fragment="Content">
|
||||||
|
|
||||||
<div class="cms-content-header north">
|
<div class="cms-content-header north">
|
||||||
<div>
|
<div>
|
||||||
|
@ -40,23 +40,26 @@ class PjaxResponseNegotiator {
|
|||||||
*/
|
*/
|
||||||
public function respond(SS_HTTPRequest $request, $extraCallbacks = array()) {
|
public function respond(SS_HTTPRequest $request, $extraCallbacks = array()) {
|
||||||
// Prepare the default options and combine with the others
|
// Prepare the default options and combine with the others
|
||||||
$callbacks = array_merge(
|
$callbacks = array_merge($this->callbacks, $extraCallbacks);
|
||||||
array_change_key_case($this->callbacks, CASE_LOWER),
|
$response = new SS_HTTPResponse();
|
||||||
array_change_key_case($extraCallbacks, CASE_LOWER)
|
|
||||||
);
|
|
||||||
|
|
||||||
if($fragment = $request->getHeader('X-Pjax')) {
|
$responseParts = array();
|
||||||
$fragment = strtolower($fragment);
|
if($fragmentStr = $request->getHeader('X-Pjax')) {
|
||||||
if(isset($callbacks[$fragment])) {
|
$fragments = explode(',', $fragmentStr);
|
||||||
return call_user_func($callbacks[$fragment]);
|
foreach($fragments as $fragment) {
|
||||||
} else {
|
if(isset($callbacks[$fragment])) {
|
||||||
throw new SS_HTTPResponse_Exception("X-Pjax = '$fragment' not supported for this URL.", 400);
|
$responseParts[$fragment] = call_user_func($callbacks[$fragment]);
|
||||||
|
} else {
|
||||||
|
throw new SS_HTTPResponse_Exception("X-Pjax = '$fragment' not supported for this URL.", 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
$response->setBody(Convert::raw2json($responseParts));
|
||||||
|
$response->addHeader('Content-Type', 'text/json');
|
||||||
} else {
|
} else {
|
||||||
if($request->isAjax()) throw new SS_HTTPResponse_Exception("Ajax requests to this URL require an X-Pjax header.", 400);
|
if($request->isAjax()) throw new SS_HTTPResponse_Exception("Ajax requests to this URL require an X-Pjax header.", 400);
|
||||||
return call_user_func($callbacks['default']);
|
$response->setBody(call_user_func($callbacks['default']));
|
||||||
}
|
}
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,7 +145,7 @@ Selectors used in these files should mirrow the "scope" set by its filename,
|
|||||||
so don't place a rule applying to all form buttons inside `ModelAdmin.js`.
|
so don't place a rule applying to all form buttons inside `ModelAdmin.js`.
|
||||||
|
|
||||||
The CMS relies heavily on Ajax-loading of interfaces, so each interface and the JavaScript
|
The CMS relies heavily on Ajax-loading of interfaces, so each interface and the JavaScript
|
||||||
driving it have to assume its underlying DOM structure is appended via Ajax callback
|
driving it have to assume its underlying DOM structure is appended via an Ajax callback
|
||||||
rather than being available when the browser window first loads.
|
rather than being available when the browser window first loads.
|
||||||
jQuery.entwine is effectively an advanced version of [jQuery.live](http://api.jquery.com/live/)
|
jQuery.entwine is effectively an advanced version of [jQuery.live](http://api.jquery.com/live/)
|
||||||
and [jQuery.delegate](http://api.jquery.com/delegate/), so takes care of dynamic event binding.
|
and [jQuery.delegate](http://api.jquery.com/delegate/), so takes care of dynamic event binding.
|
||||||
@ -167,9 +167,6 @@ a CMS developer needs to fire a navigation event rather than invoking the Ajax c
|
|||||||
The main point of contact here is `$('.cms-container').loadPanel(<url>, <title>, <data>)`
|
The main point of contact here is `$('.cms-container').loadPanel(<url>, <title>, <data>)`
|
||||||
in `LeftAndMain.js`. The `data` object can contain additional state which is required
|
in `LeftAndMain.js`. The `data` object can contain additional state which is required
|
||||||
in case the same navigation event is fired again (e.g. when the user pressed the back button).
|
in case the same navigation event is fired again (e.g. when the user pressed the back button).
|
||||||
Most commonly, the (optional) `data.selector` property declares which DOM element to replace
|
|
||||||
with the newly loaded HTML (it defaults to `.cms-content`). This is handy to only replace
|
|
||||||
e.g. an edit form, but leave the search panel in the same "content area" unchanged.
|
|
||||||
|
|
||||||
No callbacks are allowed in this style of Ajax loading, as all state needs
|
No callbacks are allowed in this style of Ajax loading, as all state needs
|
||||||
to be "repeatable". Any logic required to be exected after the Ajax call
|
to be "repeatable". Any logic required to be exected after the Ajax call
|
||||||
@ -177,16 +174,79 @@ should be placed in jQuery.entinwe `onmatch()` rules which apply to the newly cr
|
|||||||
See `$('.cms-container').handleStateChange()` in `LeftAndMain.js` for details.
|
See `$('.cms-container').handleStateChange()` in `LeftAndMain.js` for details.
|
||||||
|
|
||||||
Alternatively, form-related Ajax calls can be invoked through their own wrappers,
|
Alternatively, form-related Ajax calls can be invoked through their own wrappers,
|
||||||
which don't cause history events and hence allow callbacks: `$('.cms-content').submitForm()`.
|
which don't cause history events and hence allow callbacks: `$('.cms-container').submitForm()`.
|
||||||
|
|
||||||
|
## PJAX: Partial template replacement through Ajax
|
||||||
|
|
||||||
|
Many user interactions can change more than one area in the CMS.
|
||||||
|
For example, editing a page title in the CMS form changes it in the page tree
|
||||||
|
as well as the breadcrumbs. In order to avoid unnecessary processing,
|
||||||
|
we often want to update these sections independently from their neighbouring content.
|
||||||
|
|
||||||
|
In order for this to work, the CMS templates declare certain sections as "PJAX fragments"
|
||||||
|
through a `data-pjax-fragment` attribute. These names correlate to specific
|
||||||
|
rendering logic in the PHP controllers, through the `[api:PjaxResponseNegotiator]` class.
|
||||||
|
|
||||||
Within the PHP logic, the `[api:PjaxResponseNegotiator]` class determines which view is rendered.
|
|
||||||
Through a custom `X-Pjax` HTTP header, the client can declare which view he's expecting,
|
Through a custom `X-Pjax` HTTP header, the client can declare which view he's expecting,
|
||||||
through identifiers like `CurrentForm` or `Content` (see `[api:LeftAndMain->getResponseNegotiator()]`).
|
through identifiers like `CurrentForm` or `Content` (see `[api:LeftAndMain->getResponseNegotiator()]`).
|
||||||
These identifiers are passed to `loadPanel()` via the `pjax` data option.
|
These identifiers are passed to `loadPanel()` via the `pjax` data option.
|
||||||
|
The HTTP response is a JSON object literal, with template replacements keyed by their Pjax fragment.
|
||||||
|
Through PHP callbacks, we ensure that only the required template parts are actually executed and rendered.
|
||||||
|
When the same URL is loaded without Ajax (and hence without `X-Pjax` headers),
|
||||||
|
it should behave like a normal full page template, but using the same controller logic.
|
||||||
|
|
||||||
|
Example: Create a bare-bones CMS subclass which shows breadcrumbs (a built-in method),
|
||||||
|
as well as info on the current record. A single link updates both sections independently
|
||||||
|
in a single Ajax request.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
// mysite/code/MyAdmin.php
|
||||||
|
class MyAdmin extends LeftAndMain {
|
||||||
|
static $url_segment = 'myadmin';
|
||||||
|
public function getResponseNegotiator() {
|
||||||
|
$negotiator = parent::getResponseNegotiator();
|
||||||
|
$controller = $this;
|
||||||
|
// Register a new callback
|
||||||
|
$negotiator->setCallback('MyRecordInfo', function() use(&$controller) {
|
||||||
|
return $controller->MyRecordInfo();
|
||||||
|
});
|
||||||
|
return $negotiator;
|
||||||
|
}
|
||||||
|
public function MyRecordInfo() {
|
||||||
|
return $this->renderWith('MyRecordInfo');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:::js
|
||||||
|
// MyAdmin.ss
|
||||||
|
<% include CMSBreadcrumbs %>
|
||||||
|
<div>Static content (not affected by update)</div>
|
||||||
|
<% include MyRecordInfo %>
|
||||||
|
<a href="admin/myadmin" class="cms-panel-link" data-pjax-target="MyRecordInfo,Breadcrumbs">
|
||||||
|
Update record info
|
||||||
|
</a>
|
||||||
|
|
||||||
|
:::ss
|
||||||
|
// MyRecordInfo.ss
|
||||||
|
<div data-pjax-fragment="MyRecordInfo">
|
||||||
|
Current Record: $currentPage.Title
|
||||||
|
</div>
|
||||||
|
|
||||||
|
A click on the link will cause the following (abbreviated) ajax HTTP request:
|
||||||
|
|
||||||
|
GET /admin/myadmin HTTP/1.1
|
||||||
|
X-Pjax:Content
|
||||||
|
X-Requested-With:XMLHttpRequest
|
||||||
|
|
||||||
|
... and result in the following response:
|
||||||
|
|
||||||
|
{"MyRecordInfo": "<div...", "CMSBreadcrumbs": "<div..."}
|
||||||
|
|
||||||
Keep in mind that the returned view isn't always decided upon when the Ajax request
|
Keep in mind that the returned view isn't always decided upon when the Ajax request
|
||||||
is fired, so the server might decide to change it based on its own logic,
|
is fired, so the server might decide to change it based on its own logic,
|
||||||
sending back different `X-Pjax` headers and content.
|
sending back different `X-Pjax` headers and content.
|
||||||
|
|
||||||
|
|
||||||
## Ajax Redirects
|
## Ajax Redirects
|
||||||
|
|
||||||
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,
|
Sometimes, a server response represents a new URL state, e.g. when submitting an "add record" form,
|
||||||
@ -226,8 +286,7 @@ Built-in headers are:
|
|||||||
Some links should do more than load a new page in the browser window.
|
Some links should do more than load a new page in the browser window.
|
||||||
To avoid repetition, we've written some helpers for various use cases:
|
To avoid repetition, we've written some helpers for various use cases:
|
||||||
|
|
||||||
* Load into a panel: `<a href="..." class="cms-panel-link" data-target-panel=".cms-content">`
|
* Load into a PJAX panel: `<a href="..." class="cms-panel-link" data-pjax-target="Content">`
|
||||||
* Load via ajax, and show response status message: `<a href="..." class="cms-link-ajax">`
|
|
||||||
* Load URL as an iframe into a popup/dialog: `<a href="..." class="ss-ui-dialog-link">`
|
* Load URL as an iframe into a popup/dialog: `<a href="..." class="ss-ui-dialog-link">`
|
||||||
|
|
||||||
## Buttons
|
## Buttons
|
||||||
|
@ -326,6 +326,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
|||||||
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
|
// TODO Allow customization, e.g. to display an edit form alongside a search form from the CMS controller
|
||||||
$form->setTemplate('LeftAndMain_EditForm');
|
$form->setTemplate('LeftAndMain_EditForm');
|
||||||
$form->addExtraClass('cms-content cms-edit-form center ss-tabset');
|
$form->addExtraClass('cms-content cms-edit-form center ss-tabset');
|
||||||
|
$form->setAttribute('data-pjax-fragment', 'CurrentForm Content');
|
||||||
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
if($form->Fields()->hasTabset()) $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
|
||||||
// TODO Link back to controller action (and edited root record) rather than index,
|
// TODO Link back to controller action (and edited root record) rather than index,
|
||||||
// which requires more URL knowledge than the current link to this field gives us.
|
// which requires more URL knowledge than the current link to this field gives us.
|
||||||
|
@ -33,7 +33,7 @@ class GridFieldLevelup implements GridField_HTMLProvider{
|
|||||||
//$controller = $gridField->getForm()->Controller();
|
//$controller = $gridField->getForm()->Controller();
|
||||||
$forTemplate = new ArrayData(array(
|
$forTemplate = new ArrayData(array(
|
||||||
'UpLink' => sprintf(
|
'UpLink' => sprintf(
|
||||||
'<a class="cms-panel-link list-parent-link" href="?ParentID=%d&view=list" data-target-panel="#Form_ListViewForm" data-pjax="ListViewForm">%s</a>',
|
'<a class="cms-panel-link list-parent-link" href="?ParentID=%d&view=list" data-pjax-target="ListViewForm,Breadcrumbs">%s</a>',
|
||||||
$parentID,
|
$parentID,
|
||||||
_t('GridField.LEVELUP', 'Level up' )
|
_t('GridField.LEVELUP', 'Level up' )
|
||||||
),
|
),
|
||||||
|
@ -19,4 +19,20 @@ class PjaxResponseNegotiatorTest extends SapphireTest {
|
|||||||
$this->assertEquals('myfragment response', $negotiator->respond($request));
|
$this->assertEquals('myfragment response', $negotiator->respond($request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testMultipleFragments() {
|
||||||
|
$negotiator = new PjaxResponseNegotiator(array(
|
||||||
|
'default' => function() {return 'default response';},
|
||||||
|
'myfragment' => function() {return 'myfragment response';},
|
||||||
|
'otherfragment' => function() {return 'otherfragment response';},
|
||||||
|
));
|
||||||
|
$request = new SS_HTTPRequest('GET', '/');
|
||||||
|
$request->addHeader('X-Pjax', 'myfragment,otherfragment');
|
||||||
|
$request->addHeader('Accept', 'text/json');
|
||||||
|
$json = json_decode($negotiator->respond($request));
|
||||||
|
$this->assertObjectHasAttribute('myfragment', $json);
|
||||||
|
$this->assertEquals('myfragment response', $json->myfragment);
|
||||||
|
$this->assertObjectHasAttribute('otherfragment', $json);
|
||||||
|
$this->assertEquals('otherfragment response', $json->otherfragment);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user