silverstripe-framework/admin/javascript/LeftAndMain.Preview.js

741 lines
20 KiB
JavaScript

(function($) {
$.entwine('ss.preview', function($){
/**
* Shows a previewable website state alongside its editable version in backend UI.
*
* 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({
/**
* List of SilverStripeNavigator states (SilverStripeNavigatorItem classes) to search for.
* The order is significant - if the state is not available, preview will start searching the list
* from the beginning.
*/
AllowedStates: ['StageLink', 'LiveLink','ArchiveLink'],
/**
* API
* Name of the current preview state - one of the "AllowedStates".
*/
CurrentStateName: null,
/**
* API
* Current size selection.
*/
CurrentSizeName: 'auto',
/**
* Flags whether the preview is available on this CMS section.
*/
IsPreviewEnabled: false,
/**
* Mode in which the preview will be enabled.
*/
DefaultMode: 'split',
Sizes: {
auto: {
width: '100%',
height: '100%'
},
mobile: {
width: '335px', // add 15px for approx desktop scrollbar
height: '568px'
},
mobileLandscape: {
width: '583px', // add 15px for approx desktop scrollbar
height: '320px'
},
tablet: {
width: '783px', // add 15px for approx desktop scrollbar
height: '1024px'
},
tabletLandscape: {
width: '1039px', // add 15px for approx desktop scrollbar
height: '768px'
},
desktop: {
width: '1024px',
height: '800px'
}
},
/**
* API
* Switch the preview to different state.
* stateName can be one of the "AllowedStates".
*
* @param {String}
* @param {Boolean} Set to FALSE to avoid persisting the state
*/
changeState: function(stateName, save) {
var self = this, states = this._getNavigatorStates();
if(save !== false) {
$.each(states, function(index, state) {
self.saveState('state', stateName);
});
}
this.setCurrentStateName(stateName);
this._loadCurrentState();
this.redraw();
return this;
},
/**
* API
* Change the preview mode.
* modeName can be: split, content, preview.
*/
changeMode: function(modeName, save) {
var container = $('.cms-container');
if (modeName == 'split') {
container.entwine('.ss').splitViewMode();
this.setIsPreviewEnabled(true);
this._loadCurrentState();
} else if (modeName == 'content') {
container.entwine('.ss').contentViewMode();
this.setIsPreviewEnabled(false);
this._loadCurrentState();
} else if (modeName == 'preview') {
container.entwine('.ss').previewMode();
this.setIsPreviewEnabled(true);
} else {
throw 'Invalid mode: ' + modeName;
}
if(save !== false) this.saveState('mode', modeName);
this.redraw();
return this;
},
/**
* API
* Change the preview size.
* sizeName can be: auto, desktop, tablet, mobile.
*/
changeSize: function(sizeName) {
var sizes = this.getSizes();
this.setCurrentSizeName(sizeName);
this.removeClass('auto desktop tablet mobile').addClass(sizeName);
this.find('.preview-device-outer')
.width(sizes[sizeName].width)
.height(sizes[sizeName].height);
this.find('.preview-device-inner')
.width(sizes[sizeName].width);
this.saveState('size', sizeName);
this.redraw();
return this;
},
/**
* API
* Update the visual appearance to match the internal preview state.
*/
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Update preview state selector.
var currentStateName = this.getCurrentStateName();
if (currentStateName) {
this.find('.cms-preview-states').changeVisibleState(currentStateName);
}
// Update preview mode selectors.
var layoutOptions = $('.cms-container').entwine('.ss').getLayoutOptions();
if (layoutOptions) {
// There are two mode selectors that we need to keep in sync. Redraw both.
$('.preview-mode-selector').changeVisibleMode(layoutOptions.mode);
}
// Update preview size selector.
var currentSizeName = this.getCurrentSizeName();
if (currentSizeName) {
this.find('.preview-size-selector').changeVisibleSize(this.getCurrentSizeName());
}
return this;
},
/**
* Store the preview options for this page.
*/
saveState : function(name, value) {
if(!window.localStorage) return;
window.localStorage.setItem('cms-preview-state-' + name, value);
},
/**
* Load previously stored preferences
*/
loadState : function(name) {
if(!window.localStorage) return;
return window.localStorage.getItem('cms-preview-state-' + name);
},
/**
* 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', false);
this.setIsPreviewEnabled(false);
return this;
},
/**
* Enable the area and start updating to reflect the content editing.
*/
enablePreview: function() {
if (!this.getIsPreviewEnabled()) {
this.setIsPreviewEnabled(true);
// Initialise mode.
if ($.browser.msie && $.browser.version.slice(0,3)<=7) {
// We do not support the split mode in IE < 8.
this.changeMode('content');
} else {
this.changeMode(this.getDefaultMode(), false);
}
}
return this;
},
/**
* Initialise the preview element.
*/
onadd: function() {
var self = this, layoutContainer = this.parent();
// Create layout and controls
this.find('iframe').addClass('center');
this.find('iframe').bind('load', function() {
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).removeClass('loading');
});
// 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').hide();
this.disablePreview();
this._super();
},
/**
* Set the preview to unavailable - could be still visible. This is purely visual.
*/
_block: function() {
this.addClass('blocked');
this.find('.cms-preview-overlay').show();
return this;
},
/**
* Set the preview to available (remove the overlay);
*/
_unblock: function() {
this.removeClass('blocked');
this.find('.cms-preview-overlay').hide();
return this;
},
/**
* Update the preview according to browser and CMS section capabilities.
*/
_initialiseFromContent: function() {
var mode, size;
if (!$('.cms-previewable').length) {
this.disablePreview();
} else {
mode = this.loadState('mode');
size = this.loadState('size');
this._moveNavigator();
if(!mode || mode != 'content') {
this.enablePreview();
this._loadCurrentState();
}
this.redraw();
// now check the cookie to see if we have any preview settings that have been
// retained for this page from the last visit
if(mode) this.changeMode(mode);
if(size) this.changeSize(size);
}
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 (if its not already displayed).
*/
_loadUrl: function(url) {
this.find('iframe').addClass('loading').attr('src', url);
return this;
},
/**
* Fetch available states from the current SilverStripeNavigator (SilverStripeNavigatorItems).
* Navigator is supplied by the backend and contains all state options for the current object.
*/
_getNavigatorStates: function() {
// Walk through available states and get the URLs.
var urlMap = $.map(this.getAllowedStates(), function(name) {
var stateLink = $('.cms-preview-states .state-name[data-name=' + name + ']');
if(stateLink.length) {
return {
name: name,
url: stateLink.attr('data-link'),
active: stateLink.is(':radio') ? stateLink.is(':checked') : stateLink.is(':selected')
};
} else {
return null;
}
});
return urlMap;
},
/**
* 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.
*/
_loadCurrentState: function() {
if (!this.getIsPreviewEnabled()) return this;
var states = this._getNavigatorStates();
var currentStateName = this.getCurrentStateName();
var currentState = null;
// Find current state within currently available states.
if (states) {
currentState = $.grep(states, function(state, index) {
return (
currentStateName === state.name ||
(!currentStateName && state.active)
);
});
}
if (currentState[0]) {
// State is available on the newly loaded content. Get it.
this._loadUrl(currentState[0].url);
this._unblock();
} else if (states.length) {
// Fall back to the first available content state.
this.setCurrentStateName(states[0].name);
this._loadUrl(states[0].url);
this._unblock();
} else {
// No state available at all.
this.setCurrentStateName(null);
this._block();
}
return this;
},
/**
* Move the navigator from the content to the preview bar.
*/
_moveNavigator: function() {
var previewEl = $('.cms-preview .cms-preview-controls');
var navigatorEl = $('.cms-edit-form .cms-navigator');
if (navigatorEl.length && previewEl.length) {
// Navigator is available - install the navigator.
previewEl.html($('.cms-edit-form .cms-navigator').detach());
} else {
// Navigator not available.
this._block();
}
},
/**
* Loads the matching edit form for a page viewed in the preview iframe,
* based on metadata sent along with this document.
*/
_loadCurrentPage: function() {
if (!this.getIsPreviewEnabled()) 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');
var editLink = $(doc).find('meta[name=x-cms-edit-link]').attr('content');
var contentPanel = $('.cms-content');
if(id && contentPanel.find(':input[name=ID]').val() != id) {
// Ignore behaviour without history support (as we need ajax loading
// for the new form to load in the background)
if(window.History.enabled)
$('.cms-container').entwine('.ss').loadPanel(editLink);
}
},
/**
* Prepare the iframe content for preview.
*/
_adjustIframeForPreview: function() {
var iframe = this.find('iframe')[0];
if(iframe){
var doc = iframe.contentDocument;
}else{
return;
}
if(!doc) return;
// 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;
if (href.match(/^http:\/\//)) links[i].setAttribute('target', '_blank');
}
// Hide the navigator from the preview iframe and use only the CMS one.
var navi = doc.getElementById('SilverStripeNavigator');
if(navi) navi.style.display = 'none';
var naviMsg = doc.getElementById('SilverStripeNavigatorMessage');
if(naviMsg) naviMsg.style.display = 'none';
// Trigger extensions.
this.trigger('afterIframeAdjustedForPreview', [ doc ]);
}
});
$('.cms-edit-form').entwine({
onadd: function() {
$('.cms-preview')._initialiseFromContent();
}
});
/**
* "Preview state" functions.
* -------------------------------------------------------------------
*/
$('.cms-preview-states').entwine({
/**
* Change the appearance of the state selector.
*/
changeVisibleState: function(state) {
this.find('input[data-name="'+state+'"]').prop('checked', true);
}
});
$('.cms-preview-states .state-name').entwine({
/**
* Reacts to the user changing the state of the preview.
*/
onclick: function(e) {
//Add and remove classes to make switch work ok in old IE
this.parent().find('.active').removeClass('active');
this.next('label').addClass('active');
var targetStateName = $(this).attr('data-name');
// Reload preview with the selected state.
$('.cms-preview').changeState(targetStateName);
}
});
/**
* "Preview mode" functions
* -------------------------------------------------------------------
*/
$('.preview-mode-selector').entwine({
/**
* Change the appearance of the mode selector.
*/
changeVisibleMode: function(mode) {
this.find('select')
.val(mode)
.trigger('liszt:updated')
._addIcon();
}
});
$('.preview-mode-selector select').entwine({
/**
* Reacts to the user changing the preview mode.
*/
onchange: function(e) {
this._super(e);
e.preventDefault();
var targetStateName = $(this).val();
$('.cms-preview').changeMode(targetStateName);
}
});
$('.preview-mode-selector .chzn-results li').entwine({
/**
* IE8 doesn't support programatic access to onchange event
* so react on click
*/
onclick:function(e){
if ($.browser.msie) {
e.preventDefault();
var index = this.index();
var targetStateName = this.closest('.preview-mode-selector').find('select option:eq('+index+')').val();
//var targetStateName = $(this).val();
$('.cms-preview').changeMode(targetStateName);
}
}
});
/**
* Adjust the visibility of the preview-mode selector in the CMS part (hidden if preview is visible).
*/
$('.cms-preview.column-hidden').entwine({
onmatch: function() {
$('#preview-mode-dropdown-in-content').show();
this._super();
},
onunmatch: function() {
$('#preview-mode-dropdown-in-content').hide();
this._super();
}
});
/**
* Initialise the preview-mode selector in the CMS part (could be hidden if preview is visible).
*/
$('#preview-mode-dropdown-in-content').entwine({
onmatch: function() {
if ($('.cms-preview').is('.column-hidden')) {
this.show();
}
else {
this.hide();
}
this._super();
},
onunmatch: function() {
this._super();
}
});
/**
* "Preview size" functions
* -------------------------------------------------------------------
*/
$('.preview-size-selector').entwine({
/**
* Change the appearance of the size selector.
*/
changeVisibleSize: function(size) {
this.find('select')
.val(size)
.trigger('liszt:updated')
._addIcon();
}
});
$('.preview-size-selector select').entwine({
/**
* Trigger change in the preview size.
*/
onchange: function(e) {
e.preventDefault();
var targetSizeName = $(this).val();
$('.cms-preview').changeSize(targetSizeName);
}
});
/**
* "Chosen" plumbing.
* -------------------------------------------------------------------
*/
/*
* Add a class to the chzn select trigger based on the currently
* selected option. Update as this changes
*/
$('.preview-selector select.preview-dropdown').entwine({
'onliszt:showing_dropdown': function() {
this.siblings().find('.chzn-drop').addClass('open')._alignRight();
},
'onliszt:hiding_dropdown': function() {
this.siblings().find('.chzn-drop').removeClass('open')._removeRightAlign();
},
/**
* Trigger additional initial icon update when the control is fully loaded.
* Solves an IE8 timing issue.
*/
'onliszt:ready': function() {
this._super();
this._addIcon();
},
_addIcon: function(){
var selected = this.find(':selected');
var iconClass = selected.attr('data-icon');
var target = this.parent().find('.chzn-container a.chzn-single');
var oldIcon = target.attr('data-icon');
if(typeof oldIcon !== 'undefined'){
target.removeClass(oldIcon);
}
target.addClass(iconClass);
target.attr('data-icon', iconClass);
return this;
}
});
$('.preview-selector .chzn-drop').entwine({
_alignRight: function(){
var that = this;
$(this).hide();
/* Delay so styles applied after chosen applies css
(the line after we find out the dropdown is open)
*/
setTimeout(function(){
$(that).css({left:'auto', right:0});
$(that).show();
}, 100);
},
_removeRightAlign:function(){
$(this).css({right:'auto'});
}
});
/*
* Means of having extra styled data in chzn 'preview-selector' selects
* When chzn ul is ready, grab data-description from original select.
* If it exists, append to option and add description class to list item
*/
/*
Currently buggy (adds dexcription, then re-renders). This may need to
be done inside chosen. Chosen recommends to do this stuff in the css,
but that option is inaccessible and untranslatable
(https://github.com/harvesthq/chosen/issues/399)
$('.preview-selector .chzn-drop ul').entwine({
onmatch: function() {
this.extraData();
this._super();
},
onunmatch: function() {
this._super();
},
extraData: function(){
var that = this;
var options = this.closest('.preview-selector').find('select option');
$.each(options, function(index, option){
var target = $(that).find("li:eq(" + index + ")");
var description = $(option).attr('data-description');
if(description != undefined && !$(target).hasClass('description')){
$(target).append('<span>' + description + '</span>');
$(target).addClass('description');
}
});
}
}); */
/**
* Recalculate the preview space to allow for horizontal scrollbar and the preview actions panel
*/
$('.preview-scroll').entwine({
/**
* Height of the preview actions panel
*/
ToolbarSize: 53,
_redraw: function() {
var toolbarSize = this.getToolbarSize();
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
var previewHeight = (this.height() - toolbarSize);
this.height(previewHeight);
},
onmatch: function() {
this._redraw();
this._super();
},
onunmatch: function() {
this._super();
}
// TODO: Need to recalculate on resize of browser
});
/**
* Rotate preview to landscape
*/
$('.preview-device-outer').click(function() {
if(!$('.preview-device-outer').hasClass('rotate')) {
$('.preview-device-outer').addClass('rotate');
} else {
$('.preview-device-outer').removeClass('rotate');
}
});
});
}(jQuery));