(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); 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(); }); // 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.getIsPreviewEnabled() ? 'show' : '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 the 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').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) { 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); } }); /** * 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('' + 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));