(function($) { $.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. * * 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). */ $('.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', /** * API * Switch the preview to different state. * stateName can be one of the "AllowedStates". */ changeState: function(stateName) { this.setCurrentStateName(stateName); this._updatePreview(); 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()); } }, 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(); // 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('
'); this.find('.cms-preview-overlay-light').hide(); $('.cms-preview-toggle-link')[this._canPreview() ? 'show' : 'hide'](); self._fixIframeLinks(); this._updatePreview(); this._super(); }, /** * Load the URL into the preview iframe. */ _loadUrl: function(url) { this.find('iframe').attr('src', url); }, /** * 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 .switch-options a[name=' + name + ']'); return stateLink.length ? {name: name, url: stateLink.attr('href')} : null; }); return urlMap; }, /** * Reload the preview while keeping current state. * Fall back to first preferred state if state is no longer available. */ _updatePreview: function() { 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. this._loadUrl(currentState[0].url); this._unblock(); } else if (states.length) { // Fall back to first preferred state. this.setCurrentStateName(states[0].name); this._loadUrl(states[0].url); this._unblock(); } else { // No state available. this._block(); } return this; }, _updateAfterXhr: function(){ $('.cms-preview-toggle-link')[this._canPreview() ? 'show' : 'hide'](); this._updatePreview(); }, /** * Update preview whenever any panels are reloaded. */ 'from .cms-container': { onafterstatechange: function(){ this._updateAfterXhr(); } }, /** * 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(); } }, /** * Loads the matching edit form for a page viewed in the preview iframe, * based on metadata sent along with this document. */ _loadCurrentPage: function() { var doc = this.find('iframe')[0].contentDocument, containerEl = $('.cms-container'); if(!this._canPreview()) return; // 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); } }, /** * Determines if the current interface is capable of previewing its managed record. * * Returns: {boolean} */ _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() { var iframe = this.find('iframe')[0]; if(iframe){ var doc = iframe.contentDocument; }else{ return; } if(!doc) return; // Block outside links from going anywhere 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'); } // 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'; }, _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.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 displayed state. */ changeVisibleState: function(state) { // Arbitrary mapping from checkbox state to the preview state. if (state==='LiveLink') { this.find('.cms-preview-checkbox').prop('checked', false); } else { this.find('.cms-preview-checkbox').prop('checked', true); } } }); $('.cms-preview-states .switch-options a').entwine({ /** * Reacts to the user changing the state of the preview. * TODO Rewrite this function to ensure we can handle 1,2,3+ states. */ onclick: function(e) { var targetStateName = $(this).siblings('a').attr('name'); // Reload preview with the selected state. $('.cms-preview').changeState(targetStateName); return false; } }); /** * "Preview mode" functions * ------------------------------------------------------------------- */ $('.preview-mode-selector').entwine({ /** * Change the displayed mode. */ 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); } }); /** * 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 displayed size. */ 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('' + description + ''); $(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 }); }); }(jQuery));