API Support disabling/enabling of previews.

This fixes the problem of panels flipping back to the Pages section if
loading a non-previewable section, and also initialisation problems.
This commit is contained in:
Mateusz Uzdowski 2012-12-04 10:16:58 +13:00 committed by Ingo Schommer
parent 8ce272861c
commit 4fa2b0f3ff
3 changed files with 161 additions and 98 deletions

View File

@ -1081,7 +1081,12 @@ class LeftAndMain extends Controller implements PermissionProvider {
$form->loadDataFrom($record);
$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
$form->setAttribute('data-pjax-fragment', 'CurrentForm');
// Announce the capability so the frontend can decide whether to allow preview or not.
if(in_array('CMSPreviewable', class_implements($record))) {
$form->addExtraClass('cms-previewable');
}
// Set this if you want to split up tabs into a separate header row
// if($form->Fields()->hasTabset()) {
// $form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');

View File

@ -2,17 +2,13 @@
$.entwine('ss.preview', function($){
/**
* Shows a previewable website state alongside its editable version in backend UI,
* typically a page. This allows CMS users to seamlessly switch between preview and
* edit mode in the same browser window. The preview panel is embedded in the layout
* of the backend UI, and loads its content via an iframe.
* Shows a previewable website state alongside its editable version in backend UI.
*
* Relies on the server responses to indicate if a preview URL is available for the
* currently loaded admin interface. If no preview is available, the panel is "blocked"
* automatically.
*
* Internal links within the preview iframe trigger a refresh of the admin panel as well,
* while all external links are disabled (via JavaScript).
* Relies on the server responses to indicate if a preview is available for the
* currently loaded admin interface - signified by class ".cms-previewable" being present.
*
* The preview options at the bottom are constructured by grabbing a SilverStripeNavigator
* structure also provided by the backend.
*/
$('.cms-preview').entwine({
@ -35,6 +31,11 @@
*/
CurrentSizeName: 'auto',
/**
* Flags whether the preview is available on this CMS section.
*/
IsPreviewEnabled: false,
/**
* API
* Switch the preview to different state.
@ -42,7 +43,7 @@
*/
changeState: function(stateName) {
this.setCurrentStateName(stateName);
this._updatePreview();
this._loadCurrentState();
this.redraw();
return this;
@ -112,45 +113,113 @@
if (currentSizeName) {
this.find('.preview-size-selector').changeVisibleSize(this.getCurrentSizeName());
}
return this;
},
/**
* Disable the area - it will not appear in the GUI.
* Caveat: the preview will be automatically enabled when ".cms-previewable" class is detected.
*/
disablePreview: function() {
this._loadUrl('about:blank');
this._block();
this.changeMode('content');
this.setIsPreviewEnabled(false);
return this;
},
/**
* Enable the area and start updating to reflect the content editing.
*/
enablePreview: function() {
this.setIsPreviewEnabled(true);
this.changeMode('split');
this._loadCurrentState();
return this;
},
/**
* Initialise the preview element.
*/
onadd: function() {
var self = this, layoutContainer = this.parent();
// this.resizable({
// handles: 'w',
// stop: function(e, ui) {
// $('.cms-container').layout({resize: false});
// }
// });
// Create layout and controls
this.find('iframe').addClass('center');
this.find('iframe').bind('load', function() {
self._fixIframeLinks();
self._adjustIframeForPreview();
// Load edit view for new page, but only if the preview is activated at the moment.
// This avoids e.g. force-redirections of the edit view on RedirectorPage instances.
self._loadCurrentPage();
});
this.data('cms-preview-initialized', true);
// Preview might not be available in all admin interfaces - block/disable when necessary
this.append('<div class="cms-preview-overlay ui-widget-overlay-light"></div>');
this.find('.cms-preview-overlay-light').hide();
$('.cms-preview-toggle-link')[this._canPreview() ? 'show' : 'hide']();
$('.cms-preview-toggle-link')[this.getIsPreviewEnabled() ? 'show' : 'hide']();
self._fixIframeLinks();
this._updatePreview();
this.disablePreview();
this._super();
},
/**
* Load the URL into the preview iframe.
* Set the preview to unavailable - could be still visible. This is purely visual.
*/
_block: function() {
this.addClass('blocked');
return this;
},
/**
* Set the preview to available (remove the overlay);
*/
_unblock: function() {
this.removeClass('blocked');
return this;
},
/**
* Update the preview according to the CMS section capabilities.
*/
_initialiseFromContent: function() {
if (!$('.cms-previewable').length) {
this.disablePreview();
} else {
this.enablePreview();
this._moveNavigator();
}
return this;
},
/**
* Update preview whenever any panels are reloaded.
*/
'from .cms-container': {
onafterstatechange: function(){
this._initialiseFromContent();
}
},
/**
* Update preview whenever a form is submitted.
* This is an alternative to the LeftAndmMain::loadPanel functionality which we already
* cover in the onafterstatechange handler.
*/
'from .cms-container .cms-edit-form': {
onaftersubmitform: function(){
this._initialiseFromContent();
}
},
/**
* Change the URL of the preview iframe.
*/
_loadUrl: function(url) {
this.find('iframe').attr('src', url);
return this;
},
/**
@ -168,10 +237,15 @@
},
/**
* Reload the preview while keeping current state.
* Fall back to first preferred state if state is no longer available.
* Load current state into the preview (e.g. StageLink or LiveLink).
* We try to reuse the state we have been previously in. Otherwise we fall back
* to the first state available on the "AllowedStates" list.
*
* @returns New state name.
*/
_updatePreview: function() {
_loadCurrentState: function() {
if (!this.getIsPreviewEnabled()) return this;
var states = this._getNavigatorStates();
var currentStateName = this.getCurrentStateName();
var currentState = null;
@ -184,42 +258,37 @@
}
if (currentState[0]) {
// State is available.
// State is available on the newly loaded content. Get it.
this._loadUrl(currentState[0].url);
this._unblock();
} else if (states.length) {
// Fall back to first preferred state.
// Fall back to the first available content state.
this.setCurrentStateName(states[0].name);
this._loadUrl(states[0].url);
this._unblock();
} else {
// No state available.
// No state available at all.
this.setCurrentStateName(null);
this._block();
}
return this;
},
_updateAfterXhr: function(){
$('.cms-preview-toggle-link')[this._canPreview() ? 'show' : 'hide']();
this._updatePreview();
},
/**
* Update preview whenever any panels are reloaded.
* Move the navigator from the content to the preview bar.
*/
'from .cms-container': {
onafterstatechange: function(){
this._updateAfterXhr();
}
},
_moveNavigator: function() {
var previewEl = $('.cms-preview .cms-preview-controls');
var navigatorEl = $('.cms-edit-form .cms-navigator');
/**
* Update preview whenever form is submitted. This does not use the usual LeftAndmMain::loadPanel
* functionality which is already covered in onafterstatechange above.
*/
'from .cms-container .cms-edit-form': {
onaftersubmitform: function(){
this._updateAfterXhr();
if (navigatorEl.length && previewEl.length) {
// Navigator is available - install the navigator.
previewEl.html($('.cms-edit-form .cms-navigator').detach());
$('.cms-preview')._loadCurrentState();
} else {
// Navigator not available.
this._block();
}
},
@ -228,9 +297,10 @@
* based on metadata sent along with this document.
*/
_loadCurrentPage: function() {
var doc = this.find('iframe')[0].contentDocument, containerEl = $('.cms-container');
if (!this.getIsPreviewEnabled()) return;
if(!this._canPreview()) return;
var doc = this.find('iframe')[0].contentDocument,
containerEl = $('.cms-container');
// Load this page in the admin interface if appropriate
var id = $(doc).find('meta[name=x-page-id]').attr('content');
@ -246,18 +316,9 @@
},
/**
* Determines if the current interface is capable of previewing its managed record.
*
* Returns: {boolean}
* Prepare the iframe content for preview.
*/
_canPreview: function() {
var contentEl = $('.cms-container .cms-content');
// Only load if we're in the "edit page" view
var blockedClasses = ['CMSPagesController', 'CMSPageHistoryController'];
return !(contentEl.is('.' + blockedClasses.join(',.')));
},
_fixIframeLinks: function() {
_adjustIframeForPreview: function() {
var iframe = this.find('iframe')[0];
if(iframe){
var doc = iframe.contentDocument;
@ -267,15 +328,13 @@
if(!doc) return;
// Block outside links from going anywhere
// Open external links in new window to avoid "escaping" the internal page context in the preview
// iframe, which is important to stay in for the CMS logic.
var links = doc.getElementsByTagName('A');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href');
if(!href) continue;
// Open external links in new window to avoid "escaping" the
// internal page context in the preview iframe,
// which is important to stay in for the CMS logic.
if (href.match(/^http:\/\//)) links[i].setAttribute('target', '_blank');
}
@ -284,36 +343,18 @@
if(navi) navi.style.display = 'none';
var naviMsg = doc.getElementById('SilverStripeNavigatorMessage');
if(naviMsg) naviMsg.style.display = 'none';
},
_block: function() {
this.addClass('blocked');
},
_unblock: function() {
this.removeClass('blocked');
}
});
$('.cms-edit-form').entwine({
/**
* Initialise the navigator - move it from the EditForm to the preview.
*/
onadd: function() {
var previewEl = $('.cms-preview .cms-preview-controls');
var navigatorEl = $('.cms-edit-form .cms-navigator');
if (navigatorEl.length && previewEl.length) {
// Preview is available - install the navigator.
previewEl.html($('.cms-edit-form .cms-navigator').detach());
$('.cms-preview').changeMode('split');
} else {
// Preview not available.
$('.cms-preview').changeMode('content');
}
$('.cms-preview')._initialiseFromContent();
}
});
/**
* Update the "preview unavailable" overlay according to the class.
*/
$('.cms-preview.blocked').entwine({
onmatch: function() {
this.find('.cms-preview-overlay').show();
@ -331,7 +372,7 @@
*/
$('.cms-preview-states').entwine({
/**
* Change the displayed state.
* Change the appearance of the state selector.
*/
changeVisibleState: function(state) {
// Arbitrary mapping from checkbox state to the preview state.
@ -361,7 +402,7 @@
*/
$('.preview-mode-selector').entwine({
/**
* Change the displayed mode.
* Change the appearance of the mode selector.
*/
changeVisibleMode: function(mode) {
this.find('select')
@ -421,7 +462,7 @@
*/
$('.preview-size-selector').entwine({
/**
* Change the displayed size.
* Change the appearance of the size selector.
*/
changeVisibleSize: function(size) {
this.find('select')
@ -444,7 +485,7 @@
});
/**
* Chosen plumbing.
* "Chosen" plumbing.
* -------------------------------------------------------------------
*/

View File

@ -6,23 +6,36 @@ With the addition of side-by-side editing, the preview has the ability to appear
content in the _Pages_ section of the CMS. The site is rendered into an iframe. It will update itself whenever the
content is saved, and relevant pages will be loaded for editing when the user navigates around in the preview.
The root element for preview is `.cms-preview` which maintains the internal states neccessary for rendering. It provides
function calls for transitioning between these states and has the ability to redraw the area.
The root element for preview is `.cms-preview` which maintains the internal states neccessary for rendering within the
entwine properties. It provides function calls for transitioning between these states and has the ability to update the
appearance of the option selectors.
In terms of backend support, it relies on `SilverStripeNavigator` to be rendered into the `.cms-edit-form`.
_LeftAndMain_ will automatically take care of generating it as long as the `*_SilverStripeNavigator` template is found -
first segment has to match current _LeftAndMain_-derived class (e.g. `LeftAndMain_SilverStripeNavigator`).
We use `ss.preview` entwine namespace for all preview-related entwines.
<div class="notice" markdown='1'>
Caveat: `SilverStripeNavigator` and `CMSPreviewable` interface currently only support SiteTree objects that are
_Versioned_. They are not general enough for using on any other DataObject. That pretty much limits the extendability
of the feature.
</div>
If the `SilverStripeNavigator` structure is found, it is detached and installed in the `.cms-preview-control` panel at
the bottom of the preview, and the preview is enabled into _split_ mode.
## Enabling preview
We use `ss.preview` entwine namespace for all preview-related entwines.
The frontend decides on the preview being enabled or disabled based on the presnce of the `.cms-previewable` class. If
this class is not found the preview will remain hidden, and the layout will stay in the _content_ mode.
If the class is found, frontend looks for the `SilverStripeNavigator` structure and moves it to the
`.cms-preview-control` panel at the bottom of the preview. This structure supplies preview options such as state
selector.
If the navigator is not found, the preview appears in the GUI, but is shown as "blocked" - i.e. displaying the "preview
unavailable" overlay.
The preview can be affected by calling `enablePreview` and `disablePreview`. You can check if the preview is active by
inspecting the `IsPreviewEnabled` entwine property.
## Preview states
@ -95,9 +108,13 @@ Namespace `ss.preview`, selector `.cms-preview`:
* **getCurrentStateName**: get the name of the current state (e.g. _LiveLink_ or _StageLink_).
* **getCurrentSizeName**: get the name of the current device size.
* **getIsPreviewEnabled**: check if the preview is enabled.
* **changeState**: one of the `AllowedStates`.
* **changeSize**: one of _auto_, _desktop_, _tablet_, _mobile_.
* **changeMode**: maps to _threeColumnLayout_ modes - _split_, _preview_, _content_.
* **enablePreview**: activate the preview and switch to the _split_ mode. Try to load the relevant URL from the content.
* **disablePreview**: deactivate the preview and switch to the _content_ mode. Preview will re-enable itself when new
previewable content is loaded.
## Related