mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-27 21:15:40 +02:00
652 lines
17 KiB
JavaScript
652 lines
17 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'],
|
|
|
|
/**
|
|
* 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,
|
|
|
|
/**
|
|
* API
|
|
* Switch the preview to different state.
|
|
* stateName can be one of the "AllowedStates".
|
|
*/
|
|
changeState: function(stateName) {
|
|
this.setCurrentStateName(stateName);
|
|
this._loadCurrentState();
|
|
this.redraw();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* API
|
|
* Change the preview mode.
|
|
* modeName can be: split, content, preview.
|
|
*/
|
|
changeMode: function(modeName) {
|
|
var container = $('.cms-container');
|
|
|
|
if (modeName === 'split') {
|
|
container.entwine('.ss').splitViewMode();
|
|
} else if (modeName === 'content') {
|
|
container.entwine('.ss').contentViewMode();
|
|
} else {
|
|
container.entwine('.ss').previewMode();
|
|
}
|
|
|
|
this.redraw();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* API
|
|
* Change the preview size.
|
|
* sizeName can be: auto, desktop, tablet, mobile.
|
|
*/
|
|
changeSize: function(sizeName) {
|
|
this.setCurrentSizeName(sizeName);
|
|
|
|
this.removeClass('auto desktop tablet mobile')
|
|
.addClass(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;
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
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('split');
|
|
}
|
|
}
|
|
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-light').hide();
|
|
|
|
this.disablePreview();
|
|
|
|
this._super();
|
|
},
|
|
|
|
/**
|
|
* 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 browser and CMS section capabilities.
|
|
*/
|
|
_initialiseFromContent: function() {
|
|
if (!$('.cms-previewable').length) {
|
|
this.disablePreview();
|
|
} else {
|
|
this.enablePreview();
|
|
this._moveNavigator();
|
|
this._loadCurrentState();
|
|
this.redraw();
|
|
}
|
|
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').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 + ']');
|
|
return stateLink.length ? {name: name, url: stateLink.attr('data-link')} : 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;
|
|
});
|
|
}
|
|
|
|
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';
|
|
}
|
|
});
|
|
|
|
$('.cms-edit-form').entwine({
|
|
onadd: function() {
|
|
$('.cms-preview')._initialiseFromContent();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Update the "preview unavailable" overlay according to the class.
|
|
*/
|
|
$('.cms-preview.blocked').entwine({
|
|
onmatch: function() {
|
|
this.find('.cms-preview-overlay').show();
|
|
this._super();
|
|
},
|
|
onunmatch: function() {
|
|
this.find('.cms-preview-overlay').hide();
|
|
this._super();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* "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) {
|
|
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();
|
|
},
|
|
_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(oldIcon != undefined){
|
|
target.removeClass(oldIcon);
|
|
}
|
|
target.addClass(iconClass);
|
|
target.attr('data-icon', iconClass);
|
|
}
|
|
});
|
|
|
|
/*
|
|
* When chzn initiated run select addIcon
|
|
* Apply description text if applicable
|
|
*/
|
|
$('.preview-selector a.chzn-single').entwine({
|
|
onmatch: function() {
|
|
this.closest('.preview-selector').find('select')._addIcon();
|
|
this._super();
|
|
},
|
|
onunmatch: function() {
|
|
this._super();
|
|
}
|
|
});
|
|
|
|
|
|
$('.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));
|