jQuery.entwine('ss', ($) => {
  $('.add-to-campaign-action, #add-to-campaign__action').entwine({
    onclick() {
      let dialog = $('#add-to-campaign__dialog');

      if (dialog.length) {
        dialog.open();
      } else {
        dialog = $('<div id="add-to-campaign__dialog" class="add-to-campaign__dialog" />');
        $('body').append(dialog);
      }

      if (dialog.children().length === 0) dialog.addClass('loading');

      const form = this.closest('form');
      const button = this;

      const formData = form.serializeArray();
      formData.push({
        name: button.attr('name'),
        value: '1',
      });

      $.ajax({
        url: form.attr('action'),
        data: formData,
        type: 'POST',
        global: false,
        complete() {
          dialog.removeClass('loading');
        },
        success(data, status, xhr) {
          if (xhr.getResponseHeader('Content-Type').indexOf('text/plain') === 0) {
            const container = $(
              '<div class="add-to-campaign__response add-to-campaign__response--good">' +
              '<span></span></div>'
            );
            container.find('span').text(data);
            dialog.append(container);
          } else {
            dialog.html(data);
          }
        },
        error(xhr) {
          const error = xhr.responseText
            || 'Something went wrong. Please try again in a few minutes.';
          const container = $(
            '<div class="add-to-campaign__response add-to-campaign__response--error">' +
            '<span></span></div>'
          );
          container.find('span').text(error);
          dialog.append(container);
        },
      });

      return false;
    },
  });

  $('#add-to-campaign__dialog').entwine({
    onadd() {
      // Create jQuery dialog
      if (!this.is('.ui-dialog-content')) {
        this.ssdialog({
          autoOpen: true,
          minHeight: 200,
          maxHeight: 200,
          minWidth: 200,
          maxWidth: 500,
        });
      }

      this._super();
    },

    open() {
      this.ssdialog('open');
    },

    close() {
      this.ssdialog('close');
    },

    onssdialogclose() {
      this.empty();
    },

    'onchosen:showing_dropdown': function () {  // eslint-disable-line
      this.css({
        overflow: 'visible',
      });
    },

    'onchosen:hiding_dropdown': function () {  // eslint-disable-line
      this.css({
        overflow: '',
      });
    },
  });
});
$.entwine('ss', function($){

  /**
   * The "content" area contains all of the section specific UI (excluding the menu).
   * This area can be a form itself, as well as contain one or more forms.
   * For example, a page edit form might fill the whole area,
   * while a ModelAdmin layout shows a search form on the left, and edit form on the right.
   */
  $('.cms-content').entwine({

    onadd: function() {
      var self = this;

      // Force initialization of certain UI elements to avoid layout glitches
      this.find('.cms-tabset').redrawTabs();
      this._super();

    },

    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

      // Force initialization of certain UI elements to avoid layout glitches
      this.add(this.find('.cms-tabset')).redrawTabs();
      this.find('.cms-content-header').redraw();
      this.find('.cms-content-actions').redraw();
    }
  });

  /**
   * Load edit form for the selected node when its clicked.
   */
  $('.cms-content .cms-tree').entwine({
    onadd: function() {
      var self = this;

      this._super();

      this.bind('select_node.jstree', function(e, data) {
        var node = data.rslt.obj, loadedNodeID = self.find(':input[name=ID]').val(), origEvent = data.args[2], container = $('.cms-container');

        // Don't trigger unless coming from a click event.
        // Avoids problems with automated section switches from tree to detail view
        // when JSTree auto-selects elements on first load.
        if(!origEvent) {
          return false;
        }

        // Don't allow checking disabled nodes
        if($(node).hasClass('disabled')) return false;

        // Don't allow reloading of currently selected node,
        // mainly to avoid doing an ajax request on initial page load
        if($(node).data('id') == loadedNodeID) return;

        var url = $(node).find('a:first').attr('href');
        if(url && url != '#') {
          // strip possible querystrings from the url to avoid duplicateing document.location.search
          url = url.split('?')[0];

          // Deselect all nodes (will be reselected after load according to form state)
          self.jstree('deselect_all');
          self.jstree('uncheck_all');

          // Ensure URL is absolute (important for IE)
          if($.path.isExternal($(node).find('a:first'))) url = url = $.path.makeUrlAbsolute(url, $('base').attr('href'));
          // Retain search parameters
          if(document.location.search) url = $.path.addSearchParams(url, document.location.search.replace(/^\?/, ''));
          // Load new page
          container.loadPanel(url);
        } else {
          self.removeForm();
        }
      });
    }
  });

  $('.cms-content .cms-content-fields').entwine({
    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
    }
  });

  $('.cms-content .cms-content-header, .cms-content .cms-content-actions').entwine({
    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

      // Fix dimensions to actual extents, in preparation for a relayout via jslayout.
      this.height('auto');
      this.height(this.innerHeight()-this.css('padding-top')-this.css('padding-bottom'));
    }
  });


});
$.entwine('ss', function($) {
	/**
	 * Generic rules for all ss-ui-action-tabsets
	 * * ActionMenus
	 * * SiteTree ActionTabs
	 */
	$('.ss-tabset.ss-ui-action-tabset').entwine({
		// Ignore tab state so it will not be reopened on form submission.
		IgnoreTabState: true,

		onadd: function() {
			// Make sure the .ss-tabset is already initialised to apply our modifications on top.
			this._super();
			//Set actionTabs to allow closing and be closed by default
			this.tabs({'collapsible': true, 'active': false});
		},

		onremove: function() {
			// Remove all bound events.
			// This guards against an edge case where the click handlers are not unbound
			// because the panel is still open when the ajax edit form reloads.
			var frame = $('.cms-container').find('iframe');
			frame.each(function(index, iframe){
				try {
					$(iframe).contents().off('click.ss-ui-action-tabset');
				} catch (e) {
					console.warn('Unable to access iframe, possible https mis-match');
				}
			});
			$(document).off('click.ss-ui-action-tabset');

			this._super();
		},

		/**
		 * Deal with available vertical space
		 */
		'ontabsbeforeactivate': function(event, ui) {
			this.riseUp(event, ui);
		},

		/**
		 * Handle opening and closing tabs
		 */
		onclick: function(event, ui) {
			this.attachCloseHandler(event, ui);
		},

		/**
		 * Generic function to close open tabs. Stores event in a handler,
		 * and removes the bound event once activated.
		 *
		 * Note: Should be called by a click event attached to 'this'
		 */
		attachCloseHandler: function(event, ui) {
			var that = this, frame = $('.cms-container').find('iframe'), closeHandler;

			// Create a handler for the click event so we can close tabs
			// and easily remove the event once done
			closeHandler = function(event) {
				var panel, frame;
				panel = $(event.target).closest('.ss-ui-action-tabset .ui-tabs-panel');

				// If anything except the ui-nav button or panel is clicked,
				// close panel and remove handler. We can't close if click was
				// within panel, as it might've caused a button action,
				// and we need to show its loading indicator.
				if (!$(event.target).closest(that).length && !panel.length) {
					that.tabs('option', 'active', false); // close tabs

					// remove click event from objects it is bound to (iframe's and document)
					frame = $('.cms-container').find('iframe');
					frame.each(function(index, iframe){
						$(iframe).contents().off('click.ss-ui-action-tabset', closeHandler);
					});
					$(document).off('click.ss-ui-action-tabset', closeHandler);
				}
			};

			// Bind click event to document, and use closeHandler to handle the event
			$(document).on('click.ss-ui-action-tabset', closeHandler);
			// Make sure iframe click also closes tab
			// iframe needs a special case, else the click event will not register here
			if(frame.length > 0){
				frame.each(function(index, iframe) {
					$(iframe).contents().on('click.ss-ui-action-tabset', closeHandler);
				});
			}
		},
		/**
		 * Function riseUp checks to see if a tab should be opened upwards
		 * (based on space concerns). If true, the rise-up class is applied
		 * and a new position is calculated and applied to the element.
		 *
		 * Note: Should be called by a tabsbeforeactivate event
		 */
		riseUp: function(event, ui) {
			var elHeight, trigger, endOfWindow, elPos, activePanel, activeTab, topPosition, containerSouth, padding;

			// Get the numbers needed to calculate positions
			elHeight = $(this).find('.ui-tabs-panel').outerHeight();
			trigger = $(this).find('.ui-tabs-nav').outerHeight();
			endOfWindow = ($(window).height() + $(document).scrollTop()) - trigger;
			elPos = $(this).find('.ui-tabs-nav').offset().top;

			activePanel = ui.newPanel;
			activeTab = ui.newTab;

			if (elPos + elHeight >= endOfWindow && elPos - elHeight > 0){
				this.addClass('rise-up');

				if (activeTab.position() !== null){
					topPosition = -activePanel.outerHeight();
					containerSouth = activePanel.parents('.south');
					if (containerSouth){
						// If container is the southern panel, make tab appear from the top of the container
						padding = activeTab.offset().top - containerSouth.offset().top;
						topPosition = topPosition-padding;
					}
					$(activePanel).css('top',topPosition+"px");
				}
			} else {
				// else remove the rise-up class and set top to 0
				this.removeClass('rise-up');
				if (activeTab.position() !== null){
					$(activePanel).css('top','0px');
				}
			}
			return false;
		}
	});


	/**
	 * ActionMenus
	 * * Specific rules for ActionMenus, used for edit page actions
	 */
	$('.cms-content-actions .ss-tabset.ss-ui-action-tabset').entwine({
		/**
		 * Make necessary adjustments before tab is activated
		 */
		'ontabsbeforeactivate': function(event, ui) {
			this._super(event, ui);
			//Set the position of the opening tab (if it exists)
			if($(ui.newPanel).length > 0){
				$(ui.newPanel).css('left', ui.newTab.position().left+"px");
			}
		}
	});

	/**
	 * SiteTree ActionTabs
	 * Specific rules for site tree action tabs. Applies to tabs
	 * within the expanded content area, and within the sidebar
	 */
	$('.cms-actions-row.ss-tabset.ss-ui-action-tabset').entwine({
		/**
		 * Make necessary adjustments before tab is activated
		 */
		'ontabsbeforeactivate': function(event, ui) {
			this._super(event, ui);
			// Remove tabset open classes (Last gets a unique class
			// in the bigger sitetree. Remove this if we have it)
			$(this).closest('.ss-ui-action-tabset')
					.removeClass('tabset-open tabset-open-last');
		}
	});

	/**
	 * SiteTree ActionTabs: expanded
	 * * Specific rules for siteTree actions within the expanded content area.
	 */
	$('.cms-content-fields .ss-tabset.ss-ui-action-tabset').entwine({
		/**
		 * Make necessary adjustments before tab is activated
		 */
		'ontabsbeforeactivate': function(event, ui) {
			this._super(event, ui);
			if($( ui.newPanel).length > 0){
				if($(ui.newTab).hasClass("last")){
					// Align open tab to the right (because opened tab is last)
					$(ui.newPanel).css({'left': 'auto', 'right': '0px'});

					// Last needs to be styled differently when open, so apply a unique class
					$(ui.newPanel).parent().addClass('tabset-open-last');
				}else{
					// Assign position to tabpanel based on position of relivent active tab item
					$(ui.newPanel).css('left', ui.newTab.position().left+"px");

					// If this is the first tab, make sure the position doesn't include border
					// (hard set position to 0 ), and add the tab-set open class
					if($(ui.newTab).hasClass("first")){
						$(ui.newPanel).css('left',"0px");
						$(ui.newPanel).parent().addClass('tabset-open');
					}
				}
			}
		}
	});

	/**
	 * SiteTree ActionTabs: sidebar
	 * * Specific rules for when the site tree actions panel appears in
	 * * the side-bar
	 */
	$('.cms-tree-view-sidebar .cms-actions-row.ss-tabset.ss-ui-action-tabset').entwine({

		// If actions panel is within the sidebar, apply active class
		// to help animate open/close on hover
		'from .ui-tabs-nav li': {
			onhover: function(e) {
				$(e.target).parent().find('li .active').removeClass('active');
				$(e.target).find('a').addClass('active');
			}
		},

		/**
		 * Make necessary adjustments before tab is activated
		 */
		'ontabsbeforeactivate': function(event, ui) {
			this._super(event, ui);
			// Reset position of tabs, else anyone going between the large
			// and the small sitetree will see broken tabs
			// Apply styles with .css, to avoid overriding currently applied styles
			$(ui.newPanel).css({'left': 'auto', 'right': 'auto'});

			if($(ui.newPanel).length > 0){
				$(ui.newPanel).parent().addClass('tabset-open');
			}
		}
	});

});
Пример #4
0
$.entwine('ss', function($) {
	$('.ss-gridfield').entwine({
		/**
		 * @param {Object} Additional options for jQuery.ajax() call
		 * @param {successCallback} callback to call after reloading succeeded.
		 */

		reload: function(ajaxOpts, successCallback) {
			var self = this, form = this.closest('form'),
				focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh
				data = form.find(':input').serializeArray();

			if(!ajaxOpts) ajaxOpts = {};
			if(!ajaxOpts.data) ajaxOpts.data = [];
			ajaxOpts.data = ajaxOpts.data.concat(data);


			// Include any GET parameters from the current URL, as the view state might depend on it.
			// For example, a list prefiltered through external search criteria might be passed to GridField.
			if(window.location.search) {
				ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data);
			}

			form.addClass('loading');

			$.ajax($.extend({}, {
				headers: {"X-Pjax" : 'CurrentField'},
				type: "POST",
				url: this.data('url'),
				dataType: 'html',
				success: function(data) {
					// Replace the grid field with response, not the form.
					// TODO Only replaces all its children, to avoid replacing the current scope
					// of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
					self.empty().append($(data).children());

					// Refocus previously focused element. Useful e.g. for finding+adding
					// multiple relationships via keyboard.
					if(focusedElName) self.find(':input[name="' + focusedElName + '"]').focus();

					// Update filter
					if(self.find('.filter-header').length) {
						var content;
						if(ajaxOpts.data[0].filter=="show") {
							content = '<span class="non-sortable"></span>';
							self.addClass('show-filter').find('.filter-header').show();
						} else {
							content = '<button type="button" name="showFilter" class="ss-gridfield-button-filter trigger"></button>';
							self.removeClass('show-filter').find('.filter-header').hide();
						}

						self.find('.sortable-header th:last').html(content);
					}

					form.removeClass('loading');
					if(successCallback) successCallback.apply(this, arguments);
					self.trigger('reload', self);
				},
				error: function(e) {
					alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION'));
					form.removeClass('loading');
				}
			}, ajaxOpts));
		},
		showDetailView: function(url) {
			window.location.href = url;
		},
		getItems: function() {
			return this.find('.ss-gridfield-item');
		},
		/**
		 * @param {String}
		 * @param {Mixed}
		 */
		setState: function(k, v) {
			var state = this.getState();
			state[k] = v;
			this.find(':input[name="' + this.data('name') + '[GridState]"]').val(JSON.stringify(state));
		},
		/**
		 * @return {Object}
		 */
		getState: function() {
			return JSON.parse(this.find(':input[name="' + this.data('name') + '[GridState]"]').val());
		}
	});

	$('.ss-gridfield *').entwine({
		getGridField: function() {
			return this.closest('.ss-gridfield');
		}
	});



	$('.ss-gridfield :button[name=showFilter]').entwine({
		onclick: function(e) {
			$('.filter-header')
				.show('slow') // animate visibility
				.find(':input:first').focus(); // focus first search field
			this.closest('.ss-gridfield').addClass('show-filter');
			this.parent().html('<span class="non-sortable"></span>');
			e.preventDefault();
		}
	});


	$('.ss-gridfield .ss-gridfield-item').entwine({
		onclick: function(e) {
			if($(e.target).closest('.action').length) {
				this._super(e);
				return false;
			}

			var editLink = this.find('.edit-link');
			if(editLink.length) this.getGridField().showDetailView(editLink.prop('href'));
		},
		onmouseover: function() {
			if(this.find('.edit-link').length) this.css('cursor', 'pointer');
		},
		onmouseout: function() {
			this.css('cursor', 'default');
		}
	});

	$('.ss-gridfield .action').entwine({
		onclick: function(e){
			var filterState='show'; //filterstate should equal current state.

			// If the button is disabled, do nothing.
			if (this.button('option', 'disabled')) {
				e.preventDefault();
				return;
			}

			if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.ss-gridfield').hasClass('show-filter'))){
				filterState='hidden';
			}

			this.getGridField().reload({data: [{name: this.attr('name'), value: this.val(), filter: filterState}]});
			e.preventDefault();
		},
		/**
		 * Get the url this action should submit to
		 */
		actionurl: function() {
			var btn = this.closest(':button'), grid = this.getGridField(),
				form = this.closest('form'), data = form.find(':input.gridstate').serialize(),
				csrf = form.find('input[name="SecurityID"]').val();

			// Add current button
			data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());

			// Add csrf
			if(csrf) {
				data += "&SecurityID=" + encodeURIComponent(csrf);
			}

			// Include any GET parameters from the current URL, as the view
			// state might depend on it. For example, a list pre-filtered
			// through external search criteria might be passed to GridField.
			if(window.location.search) {
				data = window.location.search.replace(/^\?/, '') + '&' + data;
			}

			// decide whether we should use ? or & to connect the URL
			var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&';

			return $.path.makeUrlAbsolute(
				grid.data('url') + connector + data,
				$('base').attr('href')
			);
		}

	});

	/**
	 * Don't allow users to submit empty values in grid field auto complete inputs.
	 */
	$('.ss-gridfield .add-existing-autocompleter').entwine({
		onbuttoncreate: function () {
			var self = this;

			this.toggleDisabled();

			this.find('input[type="text"]').on('keyup', function () {
				self.toggleDisabled();
			});
		},
		onunmatch: function () {
			this.find('input[type="text"]').off('keyup');
		},
		toggleDisabled: function () {
			var $button = this.find('.ss-ui-button'),
				$input = this.find('input[type="text"]'),
				inputHasValue = $input.val() !== '',
				buttonDisabled = $button.is(':disabled');

			if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
				$button.button("option", "disabled", !buttonDisabled);
			}
		}
	});

	// Covers both tabular delete button, and the button on the detail form
	$('.ss-gridfield .col-buttons .action.gridfield-button-delete, .cms-edit-form .btn-toolbar button.action.action-delete').entwine({
		onclick: function(e){
			if(!confirm(i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
				e.preventDefault();
				return false;
			} else {
				this._super(e);
			}
		}
	});

	$('.ss-gridfield .action.gridfield-button-print').entwine({
		UUID: null,
		onmatch: function() {
			this._super();
			this.setUUID(new Date().getTime());
		},
		onunmatch: function() {
			this._super();
		},
		onclick: function(e){
			var url = this.actionurl();
			window.open(url);
			e.preventDefault();
			return false;
		}
	});

	$('.ss-gridfield-print-iframe').entwine({
		onmatch: function(){
			this._super();

			this.hide().bind('load', function() {
				this.focus();
				var ifWin = this.contentWindow || this;
				ifWin.print();
			});
		},
		onunmatch: function() {
			this._super();
		}
	});

	/**
	 * Prevents actions from causing an ajax reload of the field.
	 *
	 * Useful e.g. for actions which rely on HTTP response headers being
	 * interpreted natively by the browser, like file download triggers.
	 */
	$('.ss-gridfield .action.no-ajax').entwine({
		onclick: function(e){
			window.location.href = this.actionurl();
			e.preventDefault();
			return false;
		}
	});

	$('.ss-gridfield .action-detail').entwine({
		onclick: function() {
			this.getGridField().showDetailView($(this).prop('href'));
			return false;
		}
	});

	/**
	 * Allows selection of one or more rows in the grid field.
	 * Purely clientside at the moment.
	 */
	$('.ss-gridfield[data-selectable]').entwine({
		/**
		 * @return {jQuery} Collection
		 */
		getSelectedItems: function() {
			return this.find('.ss-gridfield-item.ui-selected');
		},
		/**
		 * @return {Array} Of record IDs
		 */
		getSelectedIDs: function() {
			return $.map(this.getSelectedItems(), function(el) {return $(el).data('id');});
		}
	});
	$('.ss-gridfield[data-selectable] .ss-gridfield-items').entwine({
		onadd: function() {
			this._super();

			// TODO Limit to single selection
			this.selectable();
		},
		onremove: function() {
			this._super();
			if (this.data('selectable')) this.selectable('destroy');
		}
	});

	/**
	 * Catch submission event in filter input fields, and submit the correct button
	 * rather than the whole form.
	 */
	$('.ss-gridfield .filter-header :input').entwine({
		onmatch: function() {
			var filterbtn = this.closest('.form__fieldgroup').find('.ss-gridfield-button-filter'),
				resetbtn = this.closest('.form__fieldgroup').find('.ss-gridfield-button-reset');

			if(this.val()) {
				filterbtn.addClass('filtered');
				resetbtn.addClass('filtered');
			}
			this._super();
		},
		onunmatch: function() {
			this._super();
		},
		onkeydown: function(e) {
			// Skip reset button events, they should trigger default submission
			if(this.closest('.ss-gridfield-button-reset').length) return;

			var filterbtn = this.closest('.form__fieldgroup').find('.ss-gridfield-button-filter'),
				resetbtn = this.closest('.form__fieldgroup').find('.ss-gridfield-button-reset');

			if(e.keyCode == '13') {
				var btns = this.closest('.filter-header').find('.ss-gridfield-button-filter');
				var filterState='show'; //filterstate should equal current state.
				if(this.hasClass('ss-gridfield-button-close')||!(this.closest('.ss-gridfield').hasClass('show-filter'))){
					filterState='hidden';
				}

				this.getGridField().reload({data: [{name: btns.attr('name'), value: btns.val(), filter: filterState}]});
				return false;
			}else{
				filterbtn.addClass('hover-alike');
				resetbtn.addClass('hover-alike');
			}
		}
	});

	$(".ss-gridfield .relation-search").entwine({
		onfocusin: function (event) {
			this.autocomplete({
				source: function(request, response){
					var searchField = $(this.element);
					var form = $(this.element).closest("form");
					$.ajax({
						headers: {
							"X-Pjax" : 'Partial'
						},
						dataType: 'json',
						type: "GET",
						url: $(searchField).data('searchUrl'),
						data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()),
						success: response,
						error: function(e) {
							alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.'));
						}
					});
				},
				select: function(event, ui) {
					var hiddenField = $('<input type="hidden" name="relationID" class="action_gridfield_relationfind" />');
						hiddenField.val(ui.item.id);
						$(this)
							.closest(".ss-gridfield")
							.find(".action_gridfield_relationfind")
							.replaceWith(hiddenField);
					var addbutton = $(this).closest(".ss-gridfield").find(".action_gridfield_relationadd");
					if(addbutton.data('button')){
						addbutton.button('enable');
					} else {
						addbutton.removeAttr('disabled');
					}
				}
			});
		}
	});

	$(".ss-gridfield .pagination-page-number input").entwine({
		onkeydown: function(event) {
			if(event.keyCode == 13) {
				var newpage = parseInt($(this).val(), 10);

				var gridfield = $(this).getGridField();
				gridfield.setState('GridFieldPaginator', {currentPage: newpage});
				gridfield.reload();

				return false;
			}
		}
	});
});
Пример #5
0
$.entwine('ss', function($){
	/**
	 * Delete selected folders through "batch actions" tab.
	 */
	/* assets don't currently have batch actions; disabling for now
	$(document).ready(function() {
		$('#Form_BatchActionsForm').entwine('.ss.tree').register(
			// TODO Hardcoding of base URL
			'admin/assets/batchactions/delete',
			function(ids) {
				var confirmed = confirm(
					i18n.sprintf(
						i18n._t('AssetAdmin.BATCHACTIONSDELETECONFIRM'),
						ids.length
					)
				);
				return (confirmed) ? ids : false;
			}
		);
	});
	*/

	/**
	 * Load folder detail view via controller methods
	 * rather than built-in GridField view (which is only geared towards showing files).
	 */
	$('.AssetAdmin.cms-edit-form .ss-gridfield-item').entwine({
		onclick: function(e) {
			// Let actions do their own thing
			if($(e.target).closest('.action').length) {
				this._super(e);
				return;
			}

			var grid = this.closest('.ss-gridfield');
			if(this.data('class') == 'Folder') {
				var url = grid.data('urlFolderTemplate').replace('%s', this.data('id'));
				$('.cms-container').loadPanel(url);
				return false;
			}

			this._super(e);
		}
	});

	$('.AssetAdmin.cms-edit-form .ss-gridfield .col-buttons .action.gridfield-button-delete, .AssetAdmin.cms-edit-form .Actions button.action.action-delete').entwine({
		onclick: function(e) {
			var msg;
			if(this.closest('.ss-gridfield-item').data('class') == 'Folder') {
				msg = i18n._t('AssetAdmin.ConfirmDelete');
			} else {
				msg = i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE');
			}
			if(!confirm(msg)) return false;	
			
			this.getGridField().reload({data: [{name: this.attr('name'), value: this.val()}]});
			e.preventDefault();
			return false;
		}
	});

	$('.AssetAdmin.cms-edit-form :submit[name=action_delete]').entwine({
		onclick: function(e) {
			if(!confirm(i18n._t('AssetAdmin.ConfirmDelete'))) return false;	
			else this._super(e);
		}
	});

	/**
	 * Prompt for a new foldername, rather than using dedicated form.
	 * Better usability, but less flexibility in terms of inputs and validation.
	 * Mainly necessary because AssetAdmin->AddForm() returns don't play nicely
	 * with the nested AssetAdmin->EditForm() DOM structures.
	 */
	$('.AssetAdmin .cms-add-folder-link').entwine({
		onclick: function(e) {
			var name = prompt(i18n._t('Folder.Name'));
			if(!name) return false;

			this.closest('.cms-container').loadPanel(this.data('url') + '&Name=' + name);
			return false;
		}
	});

	/**
	 * Class: #Form_SyncForm
	 */
	$('#Form_SyncForm').entwine({
		
		/**
		 * Function: onsubmit
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onsubmit: function(e) {
			var button = jQuery(this).find(':submit:first');
			button.addClass('loading');
			$.ajax({
				url: jQuery(this).attr('action'),
				data: this.serializeArray(),
				success: function() {
					button.removeClass('loading');
					// reload current form and tree
					var currNode = $('.cms-tree')[0].firstSelected();
					if(currNode) {
					  var url = $(currNode).find('a').attr('href');
						$('.cms-content').loadPanel(url);
					}
					$('.cms-tree')[0].setCustomURL('admin/assets/getsubtree');
					$('.cms-tree')[0].reload({onSuccess: function() {
						// TODO Reset current tree node
					}});
				}
			});
			
			return false;
		}
	});

	/**
	 * Reload the gridfield to show the user the file has been added
	 */
	$('.AssetAdmin.cms-edit-form .ss-uploadfield-item-progress').entwine({
		onunmatch: function () {
			$('.AssetAdmin.cms-edit-form .ss-gridfield').reload();
		}
	});
	
	$('.AssetAdmin .grid-levelup').entwine({
		onmatch: function () {
			this.closest('.ui-tabs-panel').find('.cms-actions-row').prepend(this);
		}
	})
});
$.entwine('ss', function($) {
  /**
   * Converts an inline field description into a tooltip
   * which is shown on hover over any part of the field container,
   * as well as when focusing into an input element within the field container.
   *
   * Note that some fields don't have distinct focusable
   * input fields (e.g. GridField), and aren't compatible
   * with showing tooltips.
   */
  $(".cms .field.cms-description-tooltip").entwine({
    onmatch: function() {
      this._super();

      var descriptionEl = this.find('.description'), inputEl, tooltipEl;
      if(descriptionEl.length) {
        this
          // TODO Remove title setting, shouldn't be necessary
          .attr('title', descriptionEl.text())
          .tooltip({content: descriptionEl.html()});
        descriptionEl.remove();
      }
    },
  });

  $(".cms .field.cms-description-tooltip :input").entwine({
    onfocusin: function(e) {
      this.closest('.field').tooltip('open');
    },
    onfocusout: function(e) {
      this.closest('.field').tooltip('close');
      }
  });

});
Пример #7
0
$.entwine('ss', function($){
	/**
	 * Lightweight wrapper around jQuery UI tabs for generic tab set-up
	 */
	$('.ss-tabset').entwine({
		IgnoreTabState: false,

		onadd: function() {
			var hash = window.location.hash;

			// Can't name redraw() as it clashes with other CMS entwine classes
			this.redrawTabs();

			if (hash !== '') {
				this.openTabFromURL(hash);
			}

			this._super();
		},

		onremove: function() {
			if(this.data('tabs')) this.tabs('destroy');
			this._super();
		},

		redrawTabs: function() {
			this.rewriteHashlinks();
			this.tabs();
		},

		/**
		 * @func openTabFromURL
		 * @param {string} hash
		 * @desc Allows linking to a specific tab.
		 */
		openTabFromURL: function (hash) {
			var $trigger;

			// Make sure the hash relates to a valid tab.
			$.each(this.find('.ui-tabs-anchor'), function () {
				// The hash in in the button's href and there is exactly one tab with that id.
				if (this.href.indexOf(hash) !== -1 && $(hash).length === 1) {
					$trigger = $(this);
					return false; // break the loop
				}
			});

			// If there's no tab, it means the hash is invalid, so do nothing.
			if ($trigger === void 0) {
				return;
			}

			// Switch to the correct tab when AJAX loading completes.
			$(document).ready('ajaxComplete', function () {
				$trigger.click();
			});
		},

		/**
		 * @func rewriteHashlinks
		 * @desc Ensure hash links are prefixed with the current page URL, otherwise jQuery interprets them as being external.
		 */
		rewriteHashlinks: function() {
			$(this).find('ul a').each(function() {
				if (!$(this).attr('href')) return;

				var matches = $(this).attr('href').match(/#.*/);
				if(!matches) return;
				$(this).attr('href', document.location.href.replace(/#.*/, '') + matches[0]);
			});
		}
	});

  // adding bootstrap theme classes to corresponding jQueryUI elements
  $('.ui-tabs-active .ui-tabs-anchor').entwine({
    onmatch: function() {
      this.addClass('nav-link active');
    },
    onunmatch: function() {
      this.removeClass('active');
    }
  });
});
import jQuery from 'jquery';

jQuery.entwine('ss', ($) => {
  const recipient = {
    // Some fields are only visible when HTML email are being sent.
    updateFormatSpecificFields: () => {
      const sendPlainChecked = $('input[name="SendPlain"]').is(':checked');

      $('.field.toggle-html-only')[sendPlainChecked ? 'hide' : 'show']();
      $('.field.toggle-plain-only')[sendPlainChecked ? 'show' : 'hide']();
    },
  };

  $('#Form_ItemEditForm .EmailRecipientForm').entwine({
    onmatch: () => {
      recipient.updateFormatSpecificFields();
    },

    onunmatch: () => {
      this._super();
    },
  });

  $('#Form_ItemEditForm .EmailRecipientForm input[name="SendPlain"]').entwine({
    onchange: () => {
      recipient.updateFormatSpecificFields();
    },
  });
});
$.entwine('ss', function($){
  $('.cms-content-tools #Form_SearchForm').entwine({
    onsubmit: function(e) {
      //We need to trigger handleStateChange() explicitly, otherwise handleStageChange()
      //doesn't called if landing from another section of cms
      this.trigger('beforeSubmit');
    }
  });

  /**
   * Class: .importSpec
   * 
   * Toggle import specifications
   */
  $('.importSpec').entwine({
    onmatch: function() {
      this.find('div.details').hide();
      this.find('a.detailsLink').click(function() {
        $('#' + $(this).attr('href').replace(/.*#/,'')).slideToggle();
        return false;
      });
      
      this._super();
    },
    onunmatch: function() {
      this._super();
    }
  });
});
/* global window */
import jQuery from 'jquery';

jQuery.entwine('ss', ($) => {
    $.entwine('ss', function($) {
        // Covers both tabular delete button, and the button on the detail form
        $('.grid-field.hasonebutton .add-existing-autocompleter .action_gridfield_relationadd').entwine({
            onclick: function(e) {
                this._super(e);

                grid_field = this.getGridField();
                grid_field.reload();
            }
        });
    });
});
$.entwine('ss.tree', function($){

	$('.cms-tree').entwine({

		Hints: null,

		IsUpdatingTree: false,

		IsLoaded: false,

		onadd: function(){
			this._super();

			// Don't reapply (expensive) tree behaviour if already present
			if($.isNumeric(this.data('jstree_instance_id'))) return;

			var hints = this.attr('data-hints');
			if(hints) this.setHints($.parseJSON(hints));

			/**
			 * @todo Icon and page type hover support
			 * @todo Sorting of sub nodes (originally placed in context menu)
			 * @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0)
			 *  to avoid doing partial selection with "hidden nodes" (unloaded markup)
			 * @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints)
			 * @todo Disallow moving of pages marked as deleted
			 *  most likely by server response codes rather than clientside
			 * @todo "defaultChild" when creating a page (sitetreeHints)
			 * @todo Duplicate page (originally located in context menu)
			 * @todo Update tree node title information and modified state after reordering (response is a JSON array)
			 *
			 * Tasks most likely not required after moving to a standalone tree:
			 *
			 * @todo Context menu - to be replaced by a bezel UI
			 * @todo Refresh form for selected tree node if affected by reordering (new parent relationship)
			 * @todo Cancel current form load via ajax when new load is requested (synchronous loading)
			 */
			var self = this;
				this
					.jstree(this.getTreeConfig())
					.bind('loaded.jstree', function(e, data) {
						self.setIsLoaded(true);

						// Add ajax settings after init period to avoid unnecessary initial ajax load
						// of existing tree in DOM - see load_node_html()
						data.inst._set_settings({'html_data': {'ajax': {
							'url': self.data('urlTree'),
							'data': function(node) {
								var params = self.data('searchparams') || [];
								// Avoid duplication of parameters
								params = $.grep(params, function(n, i) {return (n.name != 'ID' && n.name != 'value');});
								params.push({name: 'ID', value: $(node).data("id") ? $(node).data("id") : 0});
								params.push({name: 'ajax', value: 1});
								return params;
							}
						}}});

						self.updateFromEditForm();
						self.css('visibility', 'visible');

						// Only show checkboxes with .multiple class
						data.inst.hide_checkboxes();
					})
					.bind('before.jstree', function(e, data) {
						if(data.func == 'start_drag') {
							// Don't allow drag'n'drop if multi-select is enabled'
							if(!self.hasClass('draggable') || self.hasClass('multiselect')) {
								e.stopImmediatePropagation();
								return false;
							}
						}

						if($.inArray(data.func, ['check_node', 'uncheck_node'])) {
							// don't allow check and uncheck if parent is disabled
							var node = $(data.args[0]).parents('li:first');
							var allowedChildren = node.find('li:not(.disabled)');

							// if there are child nodes that aren't disabled, allow expanding the tree
							if(node.hasClass('disabled') && allowedChildren == 0) {
								e.stopImmediatePropagation();
								return false;
							}
						}
					})
					.bind('move_node.jstree', function(e, data) {
						if(self.getIsUpdatingTree()) return;

						var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode), newParentID = $(newParentNode).data('id') || 0, nodeID = $(movedNode).data('id');
						var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
							return $(el).data('id');
						});

						$.ajax({
							'url': $.path.addSearchParams(
								self.data('urlSavetreenode'),
								self.data('extraParams')
							),
							'type': 'POST',
							'data': {
								ID: nodeID,
								ParentID: newParentID,
								SiblingIDs: siblingIDs
							},
							success: function() {
								// We only need to update the ParentID if the current page we're on is the page being moved
								if ($('.cms-edit-form :input[name=ID]').val() == nodeID) {
									$('.cms-edit-form :input[name=ParentID]').val(newParentID);
								}
								self.updateNodesFromServer([nodeID]);
							},
							statusCode: {
								403: function() {
									$.jstree.rollback(data.rlbk);
								}
							}
						});
					})
					// Make some jstree events delegatable
					.bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) {
						$(document).triggerHandler(e, data);
					});
		},
		onremove: function(){
			this.jstree('destroy');
			this._super();
		},

		'from .cms-container': {
			onafterstatechange: function(e){
				this.updateFromEditForm();
				// No need to refresh tree nodes, we assume only form submits cause state changes
			}
		},

		'from .cms-container form': {
			onaftersubmitform: function(e){
				var id = $('.cms-edit-form :input[name=ID]').val();
				// TODO Trigger by implementing and inspecting "changed records" metadata
				// sent by form submission response (as HTTP response headers)
				this.updateNodesFromServer([id]);
			}
		},

		getTreeConfig: function() {
			var self = this;
			return {
				'core': {
					'initially_open': ['record-0'],
					'animation': 0,
					'html_titles': true
				},
				'html_data': {
					// 'ajax' will be set on 'loaded.jstree' event
				},
				'ui': {
					"select_limit" : 1,
					'initially_select': [this.find('.current').attr('id')]
				},
				"crrm": {
					'move': {
						// Check if a node is allowed to be moved.
						// Caution: Runs on every drag over a new node
						'check_move': function(data) {
							var movedNode = $(data.o), newParent = $(data.np),
								isMovedOntoContainer = data.ot.get_container()[0] == data.np[0],
								movedNodeClass = movedNode.getClassname(),
								newParentClass = newParent.getClassname(),
								// Check allowedChildren of newParent or against root node rules
								hints = self.getHints(),
								disallowedChildren = [],
								hintKey = newParentClass ? newParentClass : 'Root',
								hint = (hints && typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null;

							// Special case for VirtualPage: Check that original page type is an allowed child
							if(hint && movedNode.attr('class').match(/VirtualPage-([^\s]*)/)) movedNodeClass = RegExp.$1;

							if(hint) disallowedChildren = (typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [];
							var isAllowed = (
								// Don't allow moving the root node
								movedNode.data('id') !== 0
								// Archived pages can't be moved
								&& !movedNode.hasClass('status-archived')
								// Only allow moving node inside the root container, not before/after it
								&& (!isMovedOntoContainer || data.p == 'inside')
								// Children are generally allowed on parent
								&& !newParent.hasClass('nochildren')
								// movedNode is allowed as a child
								&& (!disallowedChildren.length || $.inArray(movedNodeClass, disallowedChildren) == -1)
							);

							return isAllowed;
						}
					}
				},
				'dnd': {
					"drop_target" : false,
					"drag_target" : false
				},
				'checkbox': {
					'two_state': true
				},
				'themes': {
					'theme': 'apple',
					'url': $('body').data('frameworkpath') + '/thirdparty/jstree/themes/apple/style.css'
				},
				// Caution: SilverStripe has disabled $.vakata.css.add_sheet() for performance reasons,
				// which means you need to add any CSS manually to framework/admin/scss/_tree.css
				'plugins': [
					'html_data', 'ui', 'dnd', 'crrm', 'themes',
					'checkbox' // checkboxes are hidden unless .multiple is set
				]
			};
		},

		/**
		 * Function:
		 *  search
		 *
		 * Parameters:
		 *  (Object) data Pass empty data to cancel search
		 *  (Function) callback Success callback
		 */
		search: function(params, callback) {
			if(params) this.data('searchparams', params);
			else this.removeData('searchparams');
			this.jstree('refresh', -1, callback);
		},

		/**
		 * Function: getNodeByID
		 *
		 * Parameters:
		 *  (Int) id
		 *
		 * Returns
		 *  DOMElement
		 */
		getNodeByID: function(id) {
			return this.find('*[data-id='+id+']');
		},

		/**
		 * Creates a new node from the given HTML.
		 * Wrapping around jstree API because we want the flexibility to define
		 * the node's <li> ourselves. Places the node in the tree
		 * according to data.ParentID.
		 *
		 * Parameters:
		 *  (String) HTML New node content (<li>)
		 *  (Object) Map of additional data, e.g. ParentID
		 *  (Function) Success callback
		 */
		createNode: function(html, data, callback) {
			var self = this,
				parentNode = data.ParentID !== void 0 ? self.getNodeByID(data.ParentID) : false, // Explicitly check for undefined as 0 is a valid ParentID
				newNode = $(html);

			// Extract the state for the new node from the properties taken from the provided HTML template.
			// This will correctly initialise the behaviour of the node for ajax loading of children.
			var properties = {data: ''};
			if(newNode.hasClass('jstree-open')) {
				properties.state = 'open';
			} else if(newNode.hasClass('jstree-closed')) {
				properties.state = 'closed';
			}
			this.jstree(
				'create_node',
				parentNode.length ? parentNode : -1,
				'last',
				properties,
				function(node) {
					var origClasses = node.attr('class');
					// Copy attributes
					for(var i=0; i<newNode[0].attributes.length; i++){
						var attr = newNode[0].attributes[i];
						node.attr(attr.name, attr.value);
					}
					// Substitute html from request for that generated by jstree
					node.addClass(origClasses).html(newNode.html());
					callback(node);
				}
			);
		},

		/**
		 * Updates a node's state in the tree,
		 * including all of its HTML, as well as its position.
		 *
		 * Parameters:
		 *  (DOMElement) Existing node
		 *  (String) HTML New node content (<li>)
		 *  (Object) Map of additional data, e.g. ParentID
		 */
		updateNode: function(node, html, data) {
			var self = this, newNode = $(html);

			var nextNode = data.NextID ? this.getNodeByID(data.NextID) : false;
			var prevNode = data.PrevID ? this.getNodeByID(data.PrevID) : false;
			var parentNode = data.ParentID ? this.getNodeByID(data.ParentID) : false;

			// Copy attributes. We can't replace the node completely
			// without removing or detaching its children nodes.
			$.each(['id', 'style', 'class', 'data-pagetype'], function(i, attrName) {
				node.attr(attrName, newNode.attr(attrName));
			});

			// Replace inner content
			var origChildren = node.children('ul').detach();
			node.html(newNode.html()).append(origChildren);

			if (nextNode && nextNode.length) {
				this.jstree('move_node', node, nextNode, 'before');
			}
			else if (prevNode && prevNode.length) {
				this.jstree('move_node', node, prevNode, 'after');
			}
			else {
				this.jstree('move_node', node, parentNode.length ? parentNode : -1);
			}
		},

		/**
		 * Sets the current state based on the form the tree is managing.
		 */
		updateFromEditForm: function() {
			var node, id = $('.cms-edit-form :input[name=ID]').val();
			if(id) {
				node = this.getNodeByID(id);
				if(node.length) {
					this.jstree('deselect_all');
					this.jstree('select_node', node);
				} else {
					// If form is showing an ID that doesn't exist in the tree,
					// get it from the server
					this.updateNodesFromServer([id]);
				}
			} else {
				// If no ID exists in a form view, we're displaying the tree on its own,
				// hence to page should show as active
				this.jstree('deselect_all');
			}
		},

		/**
		 * Reloads the view of one or more tree nodes
		 * from the server, ensuring that their state is up to date
		 * (icon, title, hierarchy, badges, etc).
		 * This is easier, more consistent and more extensible
		 * than trying to correct all aspects via DOM modifications,
		 * based on the sparse data available in the current edit form.
		 *
		 * Parameters:
		 *  (Array) List of IDs to retrieve
		 */
		updateNodesFromServer: function(ids) {
			if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;

			var self = this, i, includesNewNode = false;
			this.setIsUpdatingTree(true);
			self.jstree('save_selected');

			var correctStateFn = function(node) {
				// Duplicates can be caused by the subtree reloading through
				// a tree "open"/"select" event, while at the same time creating a new node
				self.getNodeByID(node.data('id')).not(node).remove();

				// Select this node
				self.jstree('deselect_all');
				self.jstree('select_node', node);
			};

			// TODO 'initially_opened' config doesn't apply here
			self.jstree('open_node', this.getNodeByID(0));
			self.jstree('save_opened');
			self.jstree('save_selected');

			$.ajax({
				url: $.path.addSearchParams(this.data('urlUpdatetreenodes'), 'ids=' + ids.join(',')),
				dataType: 'json',
				success: function(data, xhr) {
					$.each(data, function(nodeId, nodeData) {
						var node = self.getNodeByID(nodeId);

						// If no node data is given, assume the node has been removed
						if(!nodeData) {
							self.jstree('delete_node', node);
							return;
						}

						// Check if node exists, create if necessary
						if(node.length) {
							self.updateNode(node, nodeData.html, nodeData);
							setTimeout(function() {
								correctStateFn(node);
							}, 500);
						} else {
							includesNewNode = true;

							// If the parent node can't be found, it might have not been loaded yet.
							// This can happen for deep trees which require ajax loading.
							// Assumes that the new node has been submitted to the server already.
							if(nodeData.ParentID && !self.find('li[data-id='+nodeData.ParentID+']').length) {
								self.jstree('load_node', -1, function() {
									newNode = self.find('li[data-id='+nodeId+']');
									correctStateFn(newNode);
								});
							} else {
								self.createNode(nodeData.html, nodeData, function(newNode) {
									correctStateFn(newNode);
								});
							}
						}
					});

					if(!includesNewNode) {
						self.jstree('deselect_all');
						self.jstree('reselect');
						self.jstree('reopen');
					}
				},
				complete: function() {
					self.setIsUpdatingTree(false);
				}
			});
		}

	});

	$('.cms-tree.multiple').entwine({
		onmatch: function() {
			this._super();
			this.jstree('show_checkboxes');
		},
		onunmatch: function() {
			this._super();
			this.jstree('uncheck_all');
			this.jstree('hide_checkboxes');
		},
		/**
		 * Function: getSelectedIDs
		 *
		 * Returns:
		 * 	(Array)
		 */
		getSelectedIDs: function() {
			return $(this)
				.jstree('get_checked')
				.not('.disabled')
				.map(function() {
					return $(this).data('id');
				})
				.get();
		}
	});

	$('.cms-tree li').entwine({

		/**
		 * Function: setEnabled
		 *
		 * Parameters:
		 * 	(bool)
		 */
		setEnabled: function(bool) {
			this.toggleClass('disabled', !(bool));
		},

		/**
		 * Function: getClassname
		 *
		 * Returns PHP class for this element. Useful to check business rules like valid drag'n'drop targets.
		 */
		getClassname: function() {
			var matches = this.attr('class').match(/class-([^\s]*)/i);
			return matches ? matches[1] : '';
		},

		/**
		 * Function: getID
		 *
		 * Returns:
		 * 	(Number)
		 */
		getID: function() {
			return this.data('id');
		}
	});
});
Пример #12
0
jQuery.entwine('ss', ($) => {
  $('.insert-link__dialog-wrapper').entwine({
    Element: null,

    Data: {},

    onunmatch() {
      // solves errors given by ReactDOM "no matched root found" error.
      this._clearModal();
    },

    _clearModal() {
      ReactDOM.unmountComponentAtNode(this[0]);
      // this.empty();
    },

    open() {
      this.renderModal(true);
    },

    close() {
      this.setData({});
      this.renderModal(false);
    },

    renderModal() {
      /* noop */
    },

    /**
     * Default behaviour, recommended to overload this and sanitise where needed
     *
     * @param data
     * @private
     */
    handleInsert(data) {
      const attributes = this.buildAttributes(data);

      this.insertLinkInEditor(attributes);
      this.close();

      return Promise.resolve();
    },

    buildAttributes(data) {
      const anchor = data.Anchor && data.Anchor.length ? `#${data.Anchor}` : '';
      const href = `${data.Link}${anchor}`;

      return {
        href,
        target: data.TargetBlank ? '_blank' : '',
        title: data.Description,
      };
    },

    insertLinkInEditor(attributes) {
      const editor = this.getElement().getEditor();
      editor.insertLink(attributes);
      editor.addUndo();
      editor.repaint();
    },

    getOriginalAttributes() {
      const editor = this.getElement().getEditor();
      const node = $(editor.getSelectedNode());

      const hrefParts = (node.attr('href') || '').split('#');

      return {
        Link: hrefParts[0] || '',
        Anchor: hrefParts[1] || '',
        Description: node.attr('title'),
        TargetBlank: !!node.attr('target'),
      };
    },
  });
});
jQuery.entwine('ss', ($) => {
  /**
   * Class: .cms-edit-form #Form_EditForm_ContentReviewType_Holder
   *
   * Toggle display of group dropdown in "access" tab,
   * based on selection of radiobuttons.
   */
  $('.cms-edit-form #Form_EditForm_ContentReviewType_Holder').entwine({
    // Constructor: onmatch
    onmatch() {
      const self = this;
      this.find('.optionset :input').bind('change', (e) => {
        self.show_option(e.target.value);
      });

      // initial state
      const currentVal = this.find('input[name=ContentReviewType]:checked').val();
      this.show_option(currentVal);
      this._super();
    },

    onunmatch() {
      return this._super();
    },

    show_option(value) {
      if (value === 'Custom') {
        this._custom();
      } else if (value === 'Inherit') {
        this._inherited();
      } else {
        this._disabled();
      }
    },

    _custom() {
      $('.review-settings').show();
      $('.field.custom-setting').show();
    },

    _inherited() {
      $('.review-settings').show();
      $('.field.custom-setting').hide();
    },

    _disabled() {
      $('.review-settings').hide();
    },
  });
});
Пример #14
0
$.entwine('ss.tree', function($){
	$('.cms-tree').entwine({
		fromDocument: {
			'oncontext_show.vakata': function(e){
				this.adjustContextClass();
			}
		},
		/*
		 * Add and remove classes from context menus to allow for
		 * adjusting the display
		 */
		adjustContextClass: function(){
			var menus = $('#vakata-contextmenu').find("ul ul");

			menus.each(function(i){
				var col = "1",
					count = $(menus[i]).find('li').length;

				//Assign columns to menus over 10 items long
				if(count > 20){
					col = "3";
				}else if(count > 10){
					col = "2";
				}

				$(menus[i]).addClass('col-' + col).removeClass('right');

				//Remove "right" class that jstree adds on mouseenter
				$(menus[i]).find('li').on("mouseenter", function (e) {
					$(this).parent('ul').removeClass("right");
				});
			});
		},
		getTreeConfig: function() {
			var self = this, config = this._super(), hints = this.getHints();
			config.plugins.push('contextmenu');
			config.contextmenu = {
				'items': function(node) {

			var menuitems = {
						edit: {
							'label': (node.hasClass('edit-disabled')) ?
								 i18n._t('Tree.EditPage', 'Edit page', 100, 'Used in the context menu when right-clicking on a page node in the CMS tree')
								 : i18n._t('Tree.ViewPage', 'View page', 100, 'Used in the context menu when right-clicking on a page node in the CMS tree'),
							'action': function(obj) {
								$('.cms-container').entwine('.ss').loadPanel(i18n.sprintf(
									self.data('urlEditpage'), obj.data('id')
								));
							}
						}
					};

					// Add "show as list"
					if(!node.hasClass('nochildren')) {
						menuitems['showaslist'] = {
							'label': i18n._t('Tree.ShowAsList'),
							'action': function(obj) {
								$('.cms-container').entwine('.ss').loadPanel(
									self.data('urlListview') + '&ParentID=' + obj.data('id'),
									null,
									// Default to list view tab
									{tabState: {'pages-controller-cms-content': {'tabSelector': '.content-listview'}}}
								);
							}
						};
					}

					// Build a list for allowed children as submenu entries
					var pagetype = node.data('pagetype'),
						id = node.data('id'),
						allowedChildren = node.find('>a .item').data('allowedchildren'),
						menuAllowedChildren = {},
						hasAllowedChildren = false;

					// Convert to menu entries
					$.each(allowedChildren, function(klass, title){
						hasAllowedChildren = true;
						menuAllowedChildren["allowedchildren-" + klass ] = {
							'label': '<span class="jstree-pageicon"></span>' + title,
							'_class': 'class-' + klass,
							'action': function(obj) {
								$('.cms-container').entwine('.ss').loadPanel(
									$.path.addSearchParams(
										i18n.sprintf(self.data('urlAddpage'), id, klass),
										self.data('extraParams')
									)
								);
							}
						};
					});

					if(hasAllowedChildren) {
						menuitems['addsubpage'] = {
								'label': i18n._t('Tree.AddSubPage', 'Add page under this page', 100, 'Used in the context menu when right-clicking on a page node in the CMS tree'),
								'submenu': menuAllowedChildren
							};
					}

					if (!node.hasClass('edit-disabled')) {
						menuitems['duplicate'] = {
							'label':   i18n._t('Tree.Duplicate'),
							'submenu': [
								{
									'label':  i18n._t('Tree.ThisPageOnly'),
									'action': function (obj) {
										$('.cms-container').entwine('.ss').loadPanel(
											$.path.addSearchParams(
												i18n.sprintf(self.data('urlDuplicate'), obj.data('id')),
												self.data('extraParams')
											)
										);
									}
								}, {
									'label':  i18n._t('Tree.ThisPageAndSubpages'),
									'action': function (obj) {
										$('.cms-container').entwine('.ss').loadPanel(
											$.path.addSearchParams(
												i18n.sprintf(self.data('urlDuplicatewithchildren'), obj.data('id')),
												self.data('extraParams')
											)
										);
									}
								}
							]
						};
					}

					return menuitems;
				}
			};
			return config;
		}
	});

	// Scroll tree down to context of the current page, if it isn't
	// already visible
	$('.cms-tree a.jstree-clicked').entwine({
		onmatch: function(){
			var self = this,
				panel = self.parents('.cms-panel-content'),
				scrollTo;

			if(self.offset().top < 0 ||
				self.offset().top > panel.height() - self.height()) {
				// Current scroll top + our current offset top is our
				// position in the panel
				scrollTo = panel.scrollTop() + self.offset().top
							+ (panel.height() / 2);

				panel.animate({
					scrollTop: scrollTo
				}, 'slow');
			}
		}
	});

	// Clear filters button
	$('.cms-tree-filtered .clear-filter').entwine({
		onclick: function () {
			window.location = location.protocol + '//' + location.host + location.pathname;
		}
	});
});
import $ from 'jQuery';

$.entwine('ss', function($){

	$('.memberdatetimeoptionset').entwine({
		onmatch: function() {
			this.find('.description .toggle-content').hide();
			this._super();
		}
	});

	$('.memberdatetimeoptionset .toggle').entwine({
		onclick: function(e) {
			jQuery(this).closest('.description').find('.toggle-content').toggle();
			return false;
		}
	});

});
$.entwine('ss', function($){
	/**
	 * Class: .cms-edit-form :input[name=ClassName]
	 * Alert the user on change of page-type. This might have implications
	 * on the available form fields etc.
	 */
	$('.cms-edit-form :input[name=ClassName]').entwine({
		// Function: onchange
		onchange: function() {
			alert(i18n._t('CMSMAIN.ALERTCLASSNAME'));
		}
	});

	/**
	 * Class: .cms-edit-form input[name=Title]
	 *
	 * Input validation on the Title field
	 */
	$('.cms-edit-form input[name=Title]').entwine({
		// Constructor: onmatch
		onmatch : function() {
			var self = this;

			self.data('OrigVal', self.val());

			var form = self.closest('form');
			var urlSegmentInput = $('input:text[name=URLSegment]', form);
			var liveLinkInput = $('input[name=LiveLink]', form);

			if (urlSegmentInput.length > 0) {
				self._addActions();
				this.bind('change', function(e) {
					var origTitle = self.data('OrigVal');
					var title = self.val();
					self.data('OrigVal', title);

					// Criteria for defining a "new" page
					if (
						urlSegmentInput.val().indexOf(urlSegmentInput.data('defaultUrl')) === 0
						&& liveLinkInput.val() == ''
					) {
						self.updateURLSegment(title);
					} else {
						$('.update', self.parent()).show();
					}

					self.updateRelatedFields(title, origTitle);
					self.updateBreadcrumbLabel(title);
				});
			}

			this._super();
		},
		onunmatch: function() {
			this._super();
		},

		/**
		 * Function: updateRelatedFields
		 *
		 * Update the related fields if appropriate
		 * (String) title The new title
		 * (Stirng) origTitle The original title
		 */
		updateRelatedFields: function(title, origTitle) {
			// Update these fields only if their value was originally the same as the title
			this.parents('form').find('input[name=MetaTitle], input[name=MenuTitle]').each(function() {
				var $this = $(this);
				if($this.val() == origTitle) {
					$this.val(title);
					// Onchange bubbling didn't work in IE8, so .trigger('change') couldn't be used
					if($this.updatedRelatedFields) $this.updatedRelatedFields();
				}
			});
		},

		/**
		 * Function: updateURLSegment
		 *
		 * Update the URLSegment
		 * (String) title
		 */
		updateURLSegment: function(title) {
			var urlSegmentInput = $('input:text[name=URLSegment]', this.closest('form'));
			var urlSegmentField = urlSegmentInput.closest('.field.urlsegment');
			var updateURLFromTitle = $('.update', this.parent());
			urlSegmentField.update(title);
			if (updateURLFromTitle.is(':visible')) {
				updateURLFromTitle.hide();
			}
		},

		/**
		 * Function: updateBreadcrumbLabel
		 *
		 * Update the breadcrumb
		 * (String) title
		 */
		updateBreadcrumbLabel: function(title) {
			var pageID = $('.cms-edit-form input[name=ID]').val();
			var panelCrumb = $('span.cms-panel-link.crumb');
			if (title && title != "") {
				panelCrumb.text(title);
			}
		},

		/**
		 * Function: _addActions
		 *
		 * Utility to add update from title action
		 *
		 */
		_addActions: function() {
			var self = this;
			var	updateURLFromTitle;

			// update button
			updateURLFromTitle = $('<button />', {
				'class': 'update ss-ui-button-small',
				'text': i18n._t('URLSEGMENT.UpdateURL'),
				'type': 'button',
				'click': function(e) {
					e.preventDefault();
					self.updateURLSegment(self.val());
				}
			});

			// insert elements
			updateURLFromTitle.insertAfter(self);
			updateURLFromTitle.hide();
		}
	});

	/**
	 * Class: .cms-edit-form .parentTypeSelector
	 *
	 * ParentID field combination - mostly toggling between
	 * the two radiobuttons and setting the hidden "ParentID" field
	 */
	$('.cms-edit-form .parentTypeSelector').entwine({
		// Constructor: onmatch
		onmatch : function() {
			var self = this;
			this.find(':input[name=ParentType]').bind('click', function(e) {self._toggleSelection(e);});
			this.find('.TreeDropdownField').bind('change', function(e) {self._changeParentId(e);});

			this._changeParentId();
			this._toggleSelection();

			this._super();
		},
		onunmatch: function() {
			this._super();
		},

		/**
		 * Function: _toggleSelection
		 *
		 * Parameters:
		 *  (Event) e
		 */
		_toggleSelection: function(e) {
			var selected = this.find(':input[name=ParentType]:checked').val(),
				holder = this.find('#Form_EditForm_ParentID_Holder');
			// reset parent id if 'root' radiobutton is selected
			if(selected == 'root') this.find(':input[name=ParentID]').val(0);
			// otherwise use the old value
			else this.find(':input[name=ParentID]').val(this.find('#Form_EditForm_ParentType_subpage').data('parentIdValue'));
			// toggle tree dropdown based on selection
			if(selected != 'root') {
				holder.slideDown(400, function() {
					$(this).css('overflow', 'visible');
				});
			} else {
				holder.slideUp();
			}
		},

		/**
		 * Function: _changeParentId
		 *
		 * Parameters:
		 *  (Event) e
		 */
		_changeParentId: function(e) {
			var value = this.find(':input[name=ParentID]').val();
			// set a data attribute so we know what to use in _toggleSelection
			this.find('#Form_EditForm_ParentType_subpage').data('parentIdValue', value);
		}
	});

	/**
	 * Class: .cms-edit-form #CanViewType, .cms-edit-form #CanEditType
	 *
	 * Toggle display of group dropdown in "access" tab,
	 * based on selection of radiobuttons.
	 */
	$('.cms-edit-form #CanViewType, .cms-edit-form #CanEditType, .cms-edit-form #CanCreateTopLevelType').entwine({
		// Constructor: onmatch
		onmatch: function() {
			// TODO Decouple
			var dropdown;
			if(this.attr('id') == 'CanViewType') dropdown = $('#Form_EditForm_ViewerGroups_Holder');
			else if(this.attr('id') == 'CanEditType') dropdown = $('#Form_EditForm_EditorGroups_Holder');
			else if(this.attr('id') == 'CanCreateTopLevelType') dropdown = $('#Form_EditForm_CreateTopLevelGroups_Holder');

			this.find('.optionset :input').bind('change', function(e) {
				var wrapper = $(this).closest('.middleColumn').parent('div');
				if(e.target.value == 'OnlyTheseUsers') {
					wrapper.addClass('remove-splitter');
					dropdown['show']();
				}
				else {
					wrapper.removeClass('remove-splitter');
					dropdown['hide']();
				}
			});

			// initial state
			var currentVal = this.find('input[name=' + this.attr('id') + ']:checked').val();
			dropdown[currentVal == 'OnlyTheseUsers' ? 'show' : 'hide']();

			this._super();
		},
		onunmatch: function() {
			this._super();
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_print
	 *
	 * Open a printable representation of the form in a new window.
	 * Used for readonly older versions of a specific page.
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_print').entwine({
		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var printURL = $(this[0].form).attr('action').replace(/\?.*$/,'')
				+ '/printable/'
				+ $(':input[name=ID]',this[0].form).val();
			if(printURL.substr(0,7) != 'http://') printURL = $('base').attr('href') + printURL;

			window.open(printURL, 'printable');

			return false;
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_rollback
	 *
	 * A "rollback" to a specific version needs user confirmation.
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_rollback').entwine({

		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
			if(version) {
				message = i18n.sprintf(
					i18n._t('CMSMain.RollbackToVersion'),
					version
				);
			} else {
				message = i18n._t('CMSMain.ConfirmRestoreFromLive');
			}
			if(confirm(message)) {
				return this._super(e);
			} else {
				return false;
			}
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_archive
	 *
	 * Informing the user about the archive action while requiring confirmation
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_archive').entwine({

		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
			message = i18n.sprintf(
				i18n._t('CMSMain.Archive'),
				version
			);
			if(confirm(message)) {
				return this._super(e);
			} else {
				return false;
			}
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_restore
	 *
	 * Informing the user about the archive action while requiring confirmation
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_restore').entwine({

		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var form = this.parents('form:first'),
				version = form.find(':input[name=Version]').val(),
				message = '',
				toRoot = this.data('toRoot');
			message = i18n.sprintf(
				i18n._t(toRoot ? 'CMSMain.RestoreToRoot' : 'CMSMain.Restore'),
				version
			);
			if(confirm(message)) {
				return this._super(e);
			} else {
				return false;
			}
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_delete
	 *
	 * Informing the user about the delete from draft action while requiring confirmation
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_delete').entwine({

		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
			message = i18n.sprintf(
				i18n._t('CMSMain.DeleteFromDraft'),
				version
			);
			if(confirm(message)) {
				return this._super(e);
			} else {
				return false;
			}
		}
	});

	/**
	 * Class: .cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish
	 * Informing the user about the unpublish action while requiring confirmation
	 */
	$('.cms-edit-form .btn-toolbar #Form_EditForm_action_unpublish').entwine({

		/**
		 * Function: onclick
		 *
		 * Parameters:
		 *  (Event) e
		 */
		onclick: function(e) {
			var form = this.parents('form:first'), version = form.find(':input[name=Version]').val(), message = '';
			message = i18n.sprintf(
				i18n._t('CMSMain.Unpublish'),
				version
			);
			if(confirm(message)) {
				return this._super(e);
			} else {
				return false;
			}
		}
	});

	/**
	 * Enable save buttons upon detecting changes to content.
	 * "changed" class is added by jQuery.changetracker.
	 */
	$('.cms-edit-form.changed').entwine({
		onmatch: function(e) {
			this.find('button[name=action_save]').button('option', 'showingAlternate', true);
			this.find('button[name=action_publish]').button('option', 'showingAlternate', true);
			this._super(e);
		},
		onunmatch: function(e) {
			var saveButton = this.find('button[name=action_save]');
			if(saveButton.data('button')) saveButton.button('option', 'showingAlternate', false);
			var publishButton = this.find('button[name=action_publish]');
			if(publishButton.data('button')) publishButton.button('option', 'showingAlternate', false);
			this._super(e);
		}
	});

	$('.cms-edit-form .btn-toolbar button[name=action_publish]').entwine({
		/**
		 * Bind to ssui.button event to trigger stylistic changes.
		 */
		onbuttonafterrefreshalternate: function() {
			if (this.button('option', 'showingAlternate')) {
				this.addClass('ss-ui-action-constructive');
			}
			else {
				this.removeClass('ss-ui-action-constructive');
			}
		}
	});

	$('.cms-edit-form .btn-toolbar button[name=action_save]').entwine({
		/**
		 * Bind to ssui.button event to trigger stylistic changes.
		 */
		onbuttonafterrefreshalternate: function() {
			if (this.button('option', 'showingAlternate')) {
				this.addClass('ss-ui-action-constructive');
			}
			else {
				this.removeClass('ss-ui-action-constructive');
			}
		}
	});

	/**
	 * Class: .cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked
	 *
	 * Showing the "Page location" "Parent page" chooser only when the "Sub-page underneath a parent page"
	 * radio button is selected
	 */
	$('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').entwine({
		onmatch: function() {
			this.redraw();
			this._super();
		},
		onunmatch: function() {
			this._super();
		},
		redraw: function() {
			var treeField = $('.cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder');
			if ($(this).attr('id') == 'Form_EditForm_ParentType_root') treeField.slideUp();
			else treeField.slideDown();
		},
		onclick: function() {
			this.redraw();
		}
	});

	//trigger an initial change event to do the initial hiding of the element, if necessary
	if ($('.cms-edit-form.CMSPageSettingsController input[name="ParentType"]:checked').attr('id') == 'Form_EditForm_ParentType_root') {
		$('.cms-edit-form.CMSPageSettingsController #Form_EditForm_ParentID_Holder').hide(); //quick hide on first run
	}
});
Пример #17
0
$.entwine('ss', (jQuery) => {
  jQuery('input[type=date]').entwine({
    onadd() {
      // Browser supports type=date natively
      if (modernizr.inputtypes.date) {
        return;
      }

      // disabled, readonly or already applied
      if (this.prop('disabled') || this.prop('readonly') || this.hasClass('hasDatepicker')) {
        return;
      }

      // Duplicate input field to store ISO value
      const hiddenInput = jQuery(
        '<input/>',
        { type: 'hidden', name: this.attr('name'), value: this.val() }
      );
      this.parent().append(hiddenInput);

      // Avoid original field being saved
      this.removeAttr('name');

      // Set localised value in original field
      moment.locale(this.attr('lang'));
      const isoDate = this.val();
      let localDate = '';
      if (isoDate) {
        const dateObject = moment(isoDate);
        if (dateObject.isValid()) {
          localDate = dateObject.format('L');
        }
      }
      this.val(localDate);

      // Set useful localised placeholder
      const placeholder = i18n.inject(
        i18n._t('Admin.FormatExample', 'Example: {format}'),
        { format: moment().endOf('month').format('L') }
      );
      this.attr('placeholder', placeholder);

      this.updateValue();
    },
    onchange() {
      // TODO Validation
      this.updateValue();
    },
    updateValue() {
      const localDate = this.val();
      let isoDate = '';
      if (localDate) {
        for (const format of ['L', 'YYYY-MM-DD']) {
          const dateObject = moment(localDate, format);
          if (dateObject.isValid()) {
            isoDate = dateObject.format('YYYY-MM-DD');
            break;
          }
        }
      }
      this.parent().find('input[type=hidden]').val(isoDate);
    },
  });
});
jQuery.entwine('ss', ($) => {
  let stickyHeaderInterval = null;

  $('.uf-field-editor .ss-gridfield-items').entwine({
    onmatch() {
      let thisLevel = 0;
      let depth = 0;
      const $buttonrow = $('.uf-field-editor .ss-gridfield-buttonrow').addClass('sticky-buttons');
      const navHeight = $('.cms-content-header.north').first().height()
        + parseInt($('.sticky-buttons').css('padding-top'), 10);
      const fieldEditor = $('.uf-field-editor');

      this._super();

      // Loop through all rows and set necessary styles
      this.find('.ss-gridfield-item').each((index, el) => {
        switch ($(el).data('class')) {
          case 'SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFormStep': {
            depth = 0;
            return;
          }
          case 'SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroup': {
            thisLevel = ++depth;
            break;
          }
          case 'SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd': {
            thisLevel = depth--;
            break;
          }
          default: {
            thisLevel = depth;
          }
        }

        $(el).toggleClass('infieldgroup', thisLevel > 0);
        for (let i = 1; i <= 5; i++) {
          $(el).toggleClass(`infieldgroup-level-${i}`, thisLevel >= i);
        }
      });

      // Make sure gridfield buttons stick to top of page when user scrolls down
      stickyHeaderInterval = setInterval(() => {
        const offsetTop = fieldEditor.offset().top;
        $buttonrow.width('100%');
        if (offsetTop > navHeight || offsetTop === 0) {
          $buttonrow.removeClass('sticky-buttons');
        } else {
          $buttonrow.addClass('sticky-buttons');
        }
      }, 300);
    },
    onunmatch() {
      this._super();

      clearInterval(stickyHeaderInterval);
    },
  });

  // When new fields are added.
  $('.uf-field-editor .ss-gridfield-buttonrow .action').entwine({
    onclick(e) {
      this._super(e);

      this.trigger('addnewinline');
    },
  });

  $('.uf-field-editor').entwine({
    onmatch() {
      this._super();

      // When the 'Add field' button is clicked set a one time listener.
      // When the GridField is reloaded focus on the newly added field.
      this.on('addnewinline', () => {
        this.one('reload', () => {
          // If fieldgroup, focus on the start marker
          let $newField = this.find('.ss-gridfield-item').last();
          let $groupEnd = null;
          const fqcn = 'SilverStripe\\UserForms\\Model\\EditableFormField\\EditableFieldGroupEnd';
          if ($newField.attr('data-class') === fqcn) {
            $groupEnd = $newField;
            $groupEnd.prev().find('.col-Title input').focus();
            $newField = $groupEnd.add($groupEnd.prev());
            $groupEnd.css('visibility', 'hidden');
          } else {
            $newField.find('.col-Title input').focus();
          }

          $newField.addClass('flashBackground');
          $('.cms-content-fields').scrollTop($('.cms-content-fields')[0].scrollHeight);
          if ($groupEnd) {
            $groupEnd.css('visibility', 'visible');
          }
        });
      });
    },
    onummatch() {
      this._super();
    },
  });
});
jQuery.entwine('ss', ($) => {
  $('.advancedWorkflowTransition').entwine({
    onclick(e) {
      e.preventDefault();

      // get the stuff for it and show a dialog
      const comments = prompt('Comments');
      const instanceId = this.parents('ul').attr('data-instance-id');
      const transitionId = this.attr('data-transition-id');
      const securityId = $('[name=SecurityID]').val();
      if (!securityId) {
        alert('Invalid SecurityID field!');
        return false;
      }

      $.post(
        'AdvancedWorkflowActionController/transition',
        {
          SecurityID: securityId,
          comments,
          transition: transitionId,
          id: instanceId,
        },
        (data) => {
          if (data) {
            const parsedData = $.parseJSON(data);

            if (parsedData.success) {
              location.href = parsedData.link;
            } else {
              alert(i18n._t('Workflow.ProcessError'));
            }
          }
        }
      );

      return false;
    },
  });
});
$.entwine('ss', function($){

  $('.js-react-boot').entwine({

    onmatch: function() {
      const menuToggleWrapper = $('.cms-mobile-menu-toggle-wrapper');
      if (menuToggleWrapper.length > 0) {
        ReactDOM.render(
          <MobileMenuToggleContainer store={window.ss.store} controls="cms-menu" />,
          menuToggleWrapper[0]
        );
      }

      const store = window.ss.store;
      const menu = $('.cms-menu');
      const menuOverlay = $('.cms-menu-mobile-overlay');
      store.subscribe(() => {
        const state = store.getState();
        const isOpen = !!(state.mobileMenu && state.mobileMenu.isOpen);
        menu
          .toggleClass('cms-menu--open', isOpen)
          .attr('aria-expanded', isOpen);
        menuOverlay.attr('aria-expanded', isOpen);
      });
    }
  });

  $('.cms-menu-mobile-overlay').entwine({
    onclick: function() {
      window.ss &&
      window.ss.store &&
      window.ss.store.dispatch(closeMobileMenu());
    }
  });



});
Пример #21
0
jQuery.entwine('ss', function($) {

  /**
   * Class: textarea.htmleditor
   *
   * Add tinymce to HtmlEditorFields within the CMS. Works in combination
   * with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings,
   * and included in the page as an inline <script> tag.
   */
  $('textarea.htmleditor').entwine({

    Editor: null,

    /**
     * Constructor: onmatch
     */
    onadd: function() {
      var edClass = this.data('editor') || 'default',
        ed = ss.editorWrappers[edClass]();
      this.setEditor(ed);

      ed.init(this.attr('id'));

      this._super();
    },

    /**
     * Destructor: onunmatch
     */
    onremove: function() {
      this.getEditor().destroy();
      this._super();
    },

    /**
     * Make sure the editor has flushed all it's buffers before the form is submitted.
     */
    'from .cms-edit-form': {
      onbeforesubmitform: function() {
        this.getEditor().save();
        this._super();
      }
    },

    /**
     * Triggers insert-link dialog
     * See editor_plugin_src.js
     */
    openLinkDialog: function() {
      this.openDialog('link');
    },

    /**
     * Triggers insert-media dialog
     * See editor_plugin_src.js
     */
    openMediaDialog: function() {
      this.openDialog('media');
    },

    /**
     * Triggers insert-embed dialog
     * See editor_plugin_src.js
     */
    openEmbedDialog: function() {
      this.openDialog('embed');
    },

    openDialog: function(type) {
      // Note: This requires asset-admin module
      if (type === 'media' && window.InsertMediaModal) {
        let dialog = $('#insert-media-react__dialog-wrapper');

        if (!dialog.length) {
          dialog = $('<div id="insert-media-react__dialog-wrapper" />');
          $('body').append(dialog);
        }

        dialog.setElement(this);
        dialog.open();
        return;
      }

      // Note: This requires asset-admin module
      if (type === 'embed' && window.InsertEmbedModal) {
        let dialog = $('#insert-embed-react__dialog-wrapper');

        if (!dialog.length) {
          dialog = $('<div id="insert-embed-react__dialog-wrapper" />');
          $('body').append(dialog);
        }

        dialog.setElement(this);
        dialog.open();
        return;
      }

      throw new Error(`Dialog named ${type} is not available.`);
    }
  });
});
$.entwine('ss', function($){

	/**
	 * Class: .cms-edit-form
	 *
	 * Base edit form, provides ajaxified saving
	 * and reloading itself through the ajax return values.
	 * Takes care of resizing tabsets within the layout container.
	 *
	 * Change tracking is enabled on all fields within the form. If you want
	 * to disable change tracking for a specific field, add a "no-change-track"
	 * class to it.
	 *
	 * @name ss.Form_EditForm
	 * @require jquery.changetracker
	 *
	 * Events:
	 *  ajaxsubmit - Form is about to be submitted through ajax
	 *  validate - Contains validation result
	 *  load - Form is about to be loaded through ajax
	 */
	$('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{
		/**
		 * Variable: PlaceholderHtml
		 * (String_ HTML text to show when no form content is chosen.
		 * Will show inside the <form> tag.
		 */
		PlaceholderHtml: '',

		/**
		 * Variable: ChangeTrackerOptions
		 * (Object)
		 */
		ChangeTrackerOptions: {
			ignoreFieldSelector: '.no-change-track, .ss-upload :input, .cms-navigator :input'
		},


    /**
     * Variable: ValidationErrorShown
     * Boolean for tracking whether a validation error has been already been shown. Used because tabs can
     * sometimes be inadvertently initialised multiple times, but we don't want duplicate messages
     * (Boolean)
     */
    ValidationErrorShown: false,

		/**
		 * Constructor: onmatch
		 */
		onadd: function() {
			var self = this;

			// Turn off autocomplete to fix the access tab randomly switching radio buttons in Firefox
			// when refresh the page with an anchor tag in the URL. E.g: /admin#Root_Access.
			// Autocomplete in the CMS also causes strangeness in other browsers,
			// filling out sections of the form that the user does not want to be filled out,
			// so this turns it off for all browsers.
			// See the following page for demo and explanation of the Firefox bug:
			//  http://www.ryancramer.com/journal/entries/radio_buttons_firefox/
			this.attr("autocomplete", "off");

			this._setupChangeTracker();

			// Catch navigation events before they reach handleStateChange(),
			// in order to avoid changing the menu state if the action is cancelled by the user
			// $('.cms-menu')

			// Optionally get the form attributes from embedded fields, see Form->formHtmlContent()
			for(var overrideAttr in {'action':true,'method':true,'enctype':true,'name':true}) {
				var el = this.find(':input[name='+ '_form_' + overrideAttr + ']');
				if(el) {
					this.attr(overrideAttr, el.val());
					el.remove();
				}
			}

      // Reset error display
      this.setValidationErrorShown(false);

			// TODO
			// // Rewrite # links
			// html = html.replace(/(<a[^>]+href *= *")#/g, '$1' + window.location.href.replace(/#.*$/,'') + '#');
			//
			// // Rewrite iframe links (for IE)
			// html = html.replace(/(<iframe[^>]*src=")([^"]+)("[^>]*>)/g, '$1' + $('base').attr('href') + '$2$3');

			this._super();
		},
    'from .cms-tabset': {
      onafterredrawtabs: function () {
        // Show validation errors if necessary
        if(this.hasClass('validationerror')) {
          // Ensure the first validation error is visible
          var tabError = this.find('.message.validation, .message.required').first().closest('.tab');
          $('.cms-container').clearCurrentTabState(); // clear state to avoid override later on

          // Attempt #1: Look for nearest .ss-tabset (usually nested deeper underneath a .cms-tabset).
          var $tabSet = tabError.closest('.ss-tabset');

          // Attempt #2: Next level in tab-ception, try to select the tab within this higher level .cms-tabset if possible
          if (!$tabSet.length) {
            $tabSet = tabError.closest('.cms-tabset');
          }

          if ($tabSet.length) {
            $tabSet.tabs('option', 'active', tabError.index('.tab'));
          } else if (!this.getValidationErrorShown()) {
            // Ensure that this error message popup won't be added more than once
            this.setValidationErrorShown(true);
            errorMessage(ss.i18n._t('ModelAdmin.VALIDATIONERROR', 'Validation Error'));
          }
        }
      }
    },
		onremove: function() {
			this.changetracker('destroy');
			this._super();
		},
		onmatch: function() {
			this._super();
		},
		onunmatch: function() {
			this._super();
		},
		redraw: function() {
			if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

			// Force initialization of tabsets to avoid layout glitches
			this.add(this.find('.cms-tabset')).redrawTabs();
			this.find('.cms-content-header').redraw();
		},

		/**
		 * Function: _setupChangeTracker
		 */
		_setupChangeTracker: function() {
			// Don't bind any events here, as we dont replace the
			// full <form> tag by any ajax updates they won't automatically reapply
			this.changetracker(this.getChangeTrackerOptions());
		},

		/**
		 * Function: confirmUnsavedChanges
		 *
		 * Checks the jquery.changetracker plugin status for this form,
		 * and asks the user for confirmation via a browser dialog if changes are detected.
		 * Doesn't cancel any unload or form removal events, you'll need to implement this based on the return
		 * value of this message.
		 *
		 * If changes are confirmed for discard, the 'changed' flag is reset.
		 *
		 * Returns:
		 *  (Boolean) FALSE if the user wants to abort with changes present, TRUE if no changes are detected
		 *  or the user wants to discard them.
		 */
		confirmUnsavedChanges: function() {
			this.trigger('beforesubmitform');
			if(!this.is('.changed') || this.is('.discardchanges')) {
				return true;
			}
			var confirmed = confirm(i18n._t('LeftAndMain.CONFIRMUNSAVED'));
			if(confirmed) {
				// Ensures that once a form is confirmed, subsequent
				// changes to the underlying form don't trigger
				// additional change confirmation requests
				this.addClass('discardchanges');
			}
			return confirmed;
		},

		/**
		 * Function: onsubmit
		 *
		 * Suppress submission unless it is handled through ajaxSubmit().
		 */
		onsubmit: function(e, button) {
			// Only submit if a button is present.
			// This supressed submits from ENTER keys in input fields,
			// which means the browser auto-selects the first available form button.
			// This might be an unrelated button of the form field,
			// or a destructive action (if "save" is not available, or not on first position).
			if(this.prop("target") != "_blank") {
				if(button) this.closest('.cms-container').submitForm(this, button);
				return false;
			}
		},

		/**
		 * Function: validate
		 *
		 * Hook in (optional) validation routines.
		 * Currently clientside validation is not supported out of the box in the CMS.
		 *
		 * Todo:
		 *  Placeholder implementation
		 *
		 * Returns:
		 *  {boolean}
		 */
		validate: function() {
			var isValid = true;
			this.trigger('validate', {isValid: isValid});

			return isValid;
		},
		/*
		 * Track focus on htmleditor fields
		 */
		'from .htmleditor': {
			oneditorinit: function(e){
				var self = this,
					field = $(e.target).closest('.field.htmleditor'),
					editor = field.find('textarea.htmleditor').getEditor().getInstance();

				// TinyMCE 4 will add a focus event, but for now, use click
				editor.onClick.add(function(e){
					self.saveFieldFocus(field.attr('id'));
				});
			}
		},
		/*
		 * Track focus on inputs
		 */
		'from .cms-edit-form :input:not(:submit)': {
			onclick: function(e){
				this.saveFieldFocus($(e.target).attr('id'));
			},
			onfocus: function(e){
				this.saveFieldFocus($(e.target).attr('id'));
			}
		},
		/*
		 * Track focus on treedropdownfields.
		 */
		'from .cms-edit-form .treedropdown *': {
			onfocusin: function(e){
				var field = $(e.target).closest('.field.treedropdown');
				this.saveFieldFocus(field.attr('id'));
			}
		},
		/*
		 * Track focus on chosen selects
		 */
		'from .cms-edit-form .dropdown .chosen-container a': {
			onfocusin: function(e){
				var field = $(e.target).closest('.field.dropdown');
				this.saveFieldFocus(field.attr('id'));
			}
		},
		/*
		 * Restore fields after tabs are restored
		 */
		'from .cms-container': {
			ontabstaterestored: function(e){
				this.restoreFieldFocus();
			}
		},
		/*
		 * Saves focus in Window session storage so it that can be restored on page load
		 */
		saveFieldFocus: function(selected){
			if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;

			var id = $(this).attr('id'),
				focusElements = [];

			focusElements.push({
				id:id,
				selected:selected
			});

			if(focusElements) {
				try {
					window.sessionStorage.setItem(id, JSON.stringify(focusElements));
				} catch(err) {
					if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
						// If this fails we ignore the error as the only issue is that it
						// does not remember the focus state.
						// This is a Safari bug which happens when private browsing is enabled.
						return;
					} else {
						throw err;
					}
				}
			}
		},
		/**
		 * Set focus or window to previously saved fields.
		 * Requires HTML5 sessionStorage support.
		 *
		 * Must follow tab restoration, as reliant on active tab
		 */
		restoreFieldFocus: function(){
			if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;

			var self = this,
				hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
				sessionData = hasSessionStorage ? window.sessionStorage.getItem(this.attr('id')) : null,
				sessionStates = sessionData ? JSON.parse(sessionData) : false,
				elementID,
				tabbed = (this.find('.ss-tabset').length !== 0),
				activeTab,
				elementTab,
				toggleComposite,
				scrollY;

			if(hasSessionStorage && sessionStates.length > 0){
				$.each(sessionStates, function(i, sessionState) {
					if(self.is('#' + sessionState.id)){
						elementID = $('#' + sessionState.selected);
					}
				});

				// If the element IDs saved in session states don't match up to anything in this particular form
				// that probably means we haven't encountered this form yet, so focus on the first input
				if($(elementID).length < 1){
					this.focusFirstInput();
					return;
				}

				activeTab = $(elementID).closest('.ss-tabset').find('.ui-tabs-nav .ui-tabs-active .ui-tabs-anchor').attr('id');
				elementTab  = 'tab-' + $(elementID).closest('.ss-tabset .ui-tabs-panel').attr('id');

				// Last focussed element differs to last selected tab, do nothing
				if(tabbed && elementTab !== activeTab){
					return;
				}

				toggleComposite = $(elementID).closest('.togglecomposite');

				//Reopen toggle fields
				if(toggleComposite.length > 0){
					toggleComposite.accordion('activate', toggleComposite.find('.ui-accordion-header'));
				}

				//Calculate position for scroll
				scrollY = $(elementID).position().top;

				//Fall back to nearest visible element if hidden (for select type fields)
				if(!$(elementID).is(':visible')){
					elementID = '#' + $(elementID).closest('.field').attr('id');
					scrollY = $(elementID).position().top;
				}

				//set focus to focus variable if element focusable
				$(elementID).focus();

				// Scroll fallback when element is not focusable
				// Only scroll if element at least half way down window
				if(scrollY > $(window).height() / 2){
					self.find('.cms-content-fields').scrollTop(scrollY);
				}

			} else {
				// If session storage is not supported or there is nothing stored yet, focus on the first input
				this.focusFirstInput();
			}
		},
		/**
		 * Skip if an element in the form is already focused. Exclude elements which specifically
		 * opt-out of this behaviour via "data-skip-autofocus". This opt-out is useful if the
		 * first visible field is shown far down a scrollable area, for example for the pagination
		 * input field after a long GridField listing.
		 */
		focusFirstInput: function() {
			this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
		}
	});

	/**
	 * Class: .cms-edit-form .Actions :submit
	 *
	 * All buttons in the right CMS form go through here by default.
	 * We need this onclick overloading because we can't get to the
	 * clicked button from a form.onsubmit event.
	 */
	$('.cms-edit-form .Actions input.action[type=submit], .cms-edit-form .Actions button.action').entwine({
		/**
		 * Function: onclick
		 */
		onclick: function(e) {
			// Confirmation on delete.
			if(
				this.hasClass('gridfield-button-delete')
				&& !confirm(i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))
			) {
				e.preventDefault();
				return false;
			}

			if(!this.is(':disabled')) {
				this.parents('form').trigger('submit', [this]);
			}
			e.preventDefault();
			return false;
		}
	});

	/**
	 * If we've a history state to go back to, go back, otherwise fall back to
	 * submitting the form with the 'doCancel' action.
	 */
	$('.cms-edit-form .Actions input.action[type=submit].ss-ui-action-cancel, .cms-edit-form .Actions button.action.ss-ui-action-cancel').entwine({
		onclick: function(e) {
			if (window.history.length > 1) {
				window.history.back();
			} else {
				this.parents('form').trigger('submit', [this]);
			}
			e.preventDefault();
		}
	});

	/**
	 * Hide tabs when only one is available.
	 * Special case is actiontabs - tabs between buttons, where we want to have
	 * extra options hidden within a tab (even if only one) by default.
	 */
	$('.cms-edit-form .ss-tabset').entwine({
		onmatch: function() {
			if (!this.hasClass('ss-ui-action-tabset')) {
				var tabs = this.find("> ul:first");

				if(tabs.children("li").length == 1) {
					tabs.hide().parent().addClass("ss-tabset-tabshidden");
				}
			}

			this._super();
		},
		onunmatch: function() {
			this._super();
		}
	});

});
jQuery.entwine('ss', ($) => {
  $('textarea.htmleditor').entwine({
    openLinkInternalDialog() {
      let dialog = $(`#${modalId}`);

      if (!dialog.length) {
        dialog = $(`<div id="${modalId}" />`);
        $('body').append(dialog);
      }
      dialog.addClass('insert-link__dialog-wrapper');

      dialog.setElement(this);
      dialog.open();
    },
  });

  /**
   * Assumes that $('.insert-link__dialog-wrapper').entwine({}); is defined for shared functions
   */
  $(`#${modalId}`).entwine({
    renderModal(show) {
      const store = ss.store;
      const client = ss.apolloClient;
      const handleHide = () => this.close();
      const handleInsert = (...args) => this.handleInsert(...args);
      const attrs = this.getOriginalAttributes();

      // create/update the react component
      ReactDOM.render(
        <ApolloProvider store={store} client={client}>
          <InsertLinkInternalModal
            show={show}
            onInsert={handleInsert}
            onHide={handleHide}
            title={i18n._t('CMS.LINK_PAGE', 'Link to a page')}
            bodyClassName="modal__dialog"
            className="insert-link__dialog-wrapper--internal"
            fileAttributes={attrs}
          />
        </ApolloProvider>,
        this[0]
      );
    },

    /**
     * @param {Object} data - Posted data
     * @return {Object}
     */
    buildAttributes(data) {
      const shortcode = ShortcodeSerialiser.serialise({
        name: 'sitetree_link',
        properties: { id: data.PageID },
      }, true);

      // Add anchor
      const anchor = data.Anchor && data.Anchor.length ? `#${data.Anchor}` : '';
      const href = `${shortcode}${anchor}`;

      return {
        href,
        target: data.TargetBlank ? '_blank' : '',
        title: data.Description,
      };
    },

    getOriginalAttributes() {
      const editor = this.getElement().getEditor();
      const node = $(editor.getSelectedNode());

      // Get href
      const hrefParts = (node.attr('href') || '').split('#');
      if (!hrefParts[0]) {
        return {};
      }

      // check if page is safe
      const shortcode = ShortcodeSerialiser.match('sitetree_link', false, hrefParts[0]);
      if (!shortcode) {
        return {};
      }

      return {
        PageID: shortcode.properties.id ? parseInt(shortcode.properties.id, 10) : 0,
        Anchor: hrefParts[1] || '',
        Description: node.attr('title'),
        TargetBlank: !!node.attr('target'),
      };
    },
  });
});
$.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);
        // Do not load content as the preview is not visible.
      } else if (modeName == 'preview') {
        container.entwine('.ss').previewMode();
        this.setIsPreviewEnabled(true);
        this._loadCurrentState();
      } 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(this._supportsLocalStorage()) window.localStorage.setItem('cms-preview-state-' + name, value);
    },

    /**
     * Load previously stored preferences
     */
    loadState : function(name) {
      if(this._supportsLocalStorage()) 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.setPendingURL(null);
      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;
    },

    /**
     * Return a style element we can use in IE8 to fix fonts (see readystatechange binding in onadd below)
     */
    getOrAppendFontFixStyleElement: function() {
      var style = $('#FontFixStyleElement');
      if (!style.length) {
        style = $(
          '<style type="text/css" id="FontFixStyleElement" disabled="disabled">'+
            ':before,:after{content:none !important}'+
          '</style>'
        ).appendTo('head');
      }

      return style;
    },

    /**
     * Initialise the preview element.
     */
    onadd: function() {
      var self = this, layoutContainer = this.parent(), iframe = this.find('iframe');

      // Create layout and controls
      iframe.addClass('center');
      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');
      });

      // If there's any webfonts in the preview, IE8 will start glitching. This fixes that.
      if ($.browser.msie && 8 === parseInt($.browser.version, 10)) {
        iframe.bind('readystatechange', function(e) {
          if(iframe[0].readyState == 'interactive') {
            self.getOrAppendFontFixStyleElement().removeAttr('disabled');
            setTimeout(function(){ self.getOrAppendFontFixStyleElement().attr('disabled', 'disabled'); }, 0);
          }
        });
      }

      // 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();
    },

    /**
    * Detect and use localStorage if available. In IE11 windows 8.1 call to window.localStorage was throwing out an access denied error in some cases which was causing the preview window not to display correctly in the CMS admin area.
    */
    _supportsLocalStorage: function() {
      var uid = new Date;
      var storage;
      var result;
      try {
        (storage = window.localStorage).setItem(uid, uid);
        result = storage.getItem(uid) == uid;
        storage.removeItem(uid);
        return result && storage;
      } catch (exception) {
        console.warn('localStorge is not available due to current browser / system settings.');
      }
    },

    onenable: function () {
      var $viewModeSelector = $('.preview-mode-selector');

      $viewModeSelector.removeClass('split-disabled');
      $viewModeSelector.find('.disabled-tooltip').hide();
    },

    ondisable: function () {
      var $viewModeSelector = $('.preview-mode-selector');

      $viewModeSelector.addClass('split-disabled');
      $viewModeSelector.find('.disabled-tooltip').show();
    },

    /**
     * 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(e, data) {
        // Don't update preview if we're dealing with a custom redirect
        if(data.xhr.getResponseHeader('X-ControllerURL')) return;

        this._initialiseFromContent();
      }
    },

    /** @var string A URL that should be displayed in this preview panel once it becomes visible */
    PendingURL: null,

    oncolumnvisibilitychanged: function() {
      var url = this.getPendingURL();
      if (url && !this.is('.column-hidden')) {
        this.setPendingURL(null);
        this._loadUrl(url);
        this._unblock();
      }
    },

    /**
     * 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('href'),
            active: stateLink.hasClass('active')
          };
        } 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)
          );
        });
      }

      var url = null;

      if (currentState[0]) {
        // State is available on the newly loaded content. Get it.
        url = currentState[0].url;
      } else if (states.length) {
        // Fall back to the first available content state.
        this.setCurrentStateName(states[0].name);
        url = states[0].url;
      } else {
        // No state available at all.
        this.setCurrentStateName(null);
      }

      // Mark url as a preview url so it can get special treatment
       if (url) {
          url += ((url.indexOf('?') === -1) ? '?' : '&') + 'CMSPreview=1';
      }

      // If this preview panel isn't visible at the moment, delay loading the URL until it (maybe) is later
      if (this.is('.column-hidden')) {
        this.setPendingURL(url);
        this._loadUrl('about:blank');
        this._block();
      }
      else {
        this.setPendingURL(null);

        if (url) {
          this._loadUrl(url);
          this._unblock();
        }
        else {
          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,
        containerEl = $('.cms-container');
      try {
        doc = this.find('iframe')[0].contentDocument;
      } catch (e) {
        // iframe can't be accessed - might be secure?
        console.warn('Unable to access iframe, possible https mis-match');
      }
      if (!doc) {
        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)
        $('.cms-container').entwine('.ss').loadPanel(editLink);
      }
    },

    /**
     * Prepare the iframe content for preview.
     */
    _adjustIframeForPreview: function() {
      var iframe = this.find('iframe')[0],
        doc;
      if(!iframe){
        return;
      }

      try {
        doc = iframe.contentDocument;
      } catch (e) {
        // iframe can't be accessed - might be secure?
        console.warn('Unable to access iframe, possible https mis-match');
      }
      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() {
      this._super();
      $('.cms-preview')._initialiseFromContent();
    }
  });

  /**
   * "Preview state" functions.
   * -------------------------------------------------------------------
   */
  $('.cms-preview-states').entwine({
    /**
     * Change the appearance of the state selector.
     */
    changeVisibleState: function(state) {
      this.find('[data-name="'+state+'"]').addClass('active').siblings().removeClass('active');
    }
  });

  $('.cms-preview-states .state-name').entwine({
    /**
     * Reacts to the user changing the state of the preview.
     */
    onclick: function(e) {
      //only intercept left click, so middle click can open new windows
      if (e.which == 1) {
        var targetStateName = $(this).attr('data-name');

        //Add and remove classes to make switch work ok in old IE
        this.addClass('active').siblings().removeClass('active');

        // Reload preview with the selected state.
        $('.cms-preview').changeState(targetStateName);

        e.preventDefault();
      }
    }
  });

  /**
   * "Preview mode" functions
   * -------------------------------------------------------------------
   */
  $('.preview-mode-selector').entwine({
    /**
     * Change the appearance of the mode selector.
     */
    changeVisibleMode: function(mode) {
      this.find('select')
        .val(mode)
        .trigger('chosen: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);
    }
  });

  /**
   * 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();
      // Alert the user as to why the preview is hidden
      if ($('.cms-preview .result-selected').hasClass('font-icon-columns')) {
        statusMessage(i18n._t(
          'LeftAndMain.DISABLESPLITVIEW',
          "Screen too small to show site preview in split mode"),
        "error");
      }
      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('chosen: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 chosen select trigger based on the currently
  *  selected option. Update as this changes
  */
  $('.preview-selector select.preview-dropdown').entwine({
    /**
     * Trigger additional initial icon update when the control is fully loaded.
     * Solves an IE8 timing issue.
     */
    'onchosen:ready': function() {
      this._super();
      this._addIcon();
    },

    _addIcon: function(){
      var selected = this.find(':selected');
      var iconClass = selected.attr('data-icon');

      var target = this.parent().find('.chosen-container a.chosen-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-mode-selector .chosen-drop li:last-child').entwine({
    onmatch: function () {
      if ($('.preview-mode-selector').hasClass('split-disabled')) {
        this.parent().append('<div class="disabled-tooltip"></div>');
      } else {
        this.parent().append('<div class="disabled-tooltip" style="display: none;"></div>');
      }
    }
  });

  

  /**
   * Rotate preview to landscape
   */
  $('.preview-device-outer').entwine({
    onclick: function () {
      this.toggleClass('rotate');
    }
  });
});
$.entwine('ss', function($) {

  /*
   * Handle messages sent via nested iframes
   * Messages should be raised via postMessage with an object with the 'type' parameter given.
   * An optional 'target' and 'data' parameter can also be specified. If no target is specified
   * events will be sent to the window instead.
   * type should be one of:
   *  - 'event' - Will trigger the given event (specified by 'event') on the target
   *  - 'callback' - Will call the given method (specified by 'callback') on the target
   */
  $(window).on("message", function(e) {
    var target,
      event = e.originalEvent,
      data = typeof event.data === 'object' ? event.data : JSON.parse(event.data);

    // Reject messages outside of the same origin
    if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return;

    // Get target of this action
    target = typeof(data.target) === 'undefined'
      ? $(window)
      : $(data.target);

    // Determine action
    switch(data.type) {
      case 'event':
        target.trigger(data.event, data.data);
        break;
      case 'callback':
        target[data.callback].call(target, data.data);
        break;
    }
  });

  /**
   * Position the loading spinner animation below the ss logo
   */
  var positionLoadingSpinner = function() {
    var offset = 120; // offset from the ss logo
    var spinner = $('.ss-loading-screen .loading-animation');
    var top = ($(window).height() - spinner.height()) / 2;
    spinner.css('top', top + offset);
    spinner.show();
  };

  // apply an select element only when it is ready, ie. when it is rendered into a template
  // with css applied and got a width value.
  var applyChosen = function(el) {
    if(el.is(':visible')) {
      el.addClass('has-chosen').chosen({
        allow_single_deselect: true,
        disable_search_threshold: 20,
        display_disabled_options: true
      });
    } else {
      setTimeout(function() {
        // Make sure it's visible before applying the ui
        el.show();
        applyChosen(el);
      }, 500);
    }
  };

  /**
   * Compare URLs, but normalize trailing slashes in
   * URL to work around routing weirdnesses in SS_HTTPRequest.
   * Also normalizes relative URLs by prefixing them with the <base>.
   */
  var isSameUrl = function(url1, url2) {
    var baseUrl = $('base').attr('href');
    url1 = $.path.isAbsoluteUrl(url1) ? url1 : $.path.makeUrlAbsolute(url1, baseUrl),
    url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl);
    var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2);
    return (
      url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
      url1parts.search == url2parts.search
    );
  };

  var ajaxCompleteEvent = window.ss.debounce(function () {
    $(window).trigger('ajaxComplete');
  }, 1000, true);

  $(window).bind('resize', positionLoadingSpinner).trigger('resize');

  // global ajax handlers
  $(document).ajaxComplete(function(e, xhr, settings) {
    // Simulates a redirect on an ajax response.
    var origUrl,
      url = xhr.getResponseHeader('X-ControllerURL'),
      destUrl = settings.url,
      msg = xhr.getResponseHeader('X-Status') !== null ? xhr.getResponseHeader('X-Status') : xhr.statusText, // Handle custom status message headers
      msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good',
      ignoredMessages = ['OK', 'success'];
    if(window.history.state) {
      origUrl = window.history.state.path;
    } else {
      origUrl = document.URL;
    }

    // Only redirect if controller url differs to the requested or current one
    if (url !== null && (!isSameUrl(origUrl, url) || !isSameUrl(destUrl, url))) {
      router.show(url, {
        id: (new Date()).getTime() + String(Math.random()).replace(/\D/g,''), // Ensure that redirections are followed through by history API by handing it a unique ID
        pjax: xhr.getResponseHeader('X-Pjax') ? xhr.getResponseHeader('X-Pjax') : settings.headers['X-Pjax']
      });
    }

    // Enable reauthenticate dialog if requested
    if (xhr.getResponseHeader('X-Reauthenticate')) {
      $('.cms-container').showLoginDialog();
      return;
    }

    // Show message (but ignore aborted requests)
    if (xhr.status !== 0 && msg && $.inArray(msg, ignoredMessages) === -1) {
      // Decode into UTF-8, HTTP headers don't allow multibyte
      statusMessage(decodeURIComponent(msg), msgType);
    }

    ajaxCompleteEvent(this);
  });

  /**
   * Main LeftAndMain interface with some control panel and an edit form.
   *
   * Events:
   *  ajaxsubmit - ...
   *  validate - ...
   *  aftersubmitform - ...
   */
  $('.cms-container').entwine({

    /**
     * Tracks current panel request.
     */
    StateChangeXHR: null,

    /**
     * Tracks current fragment-only parallel PJAX requests.
     */
    FragmentXHR: {},

    StateChangeCount: 0,

    /**
     * Options for the threeColumnCompressor layout algorithm.
     *
     * See LeftAndMain.Layout.js for description of these options.
     */
    LayoutOptions: {
      minContentWidth: 940,
      minPreviewWidth: 400,
      mode: 'content'
    },

    /**
     * Constructor: onmatch
     */
    onadd: function() {
      var self = this,
        basePath = getUrlPath($('base')[0].href);

        // Cleanup baseurl
        basePath = basePath.replace(/\/$/, ''); // No trailing slash
        if(basePath.match(/^[^\/]/)) {
          basePath = '/' + basePath; // Mandatory leading slash
        }
        router.base(basePath);

        // Register all top level routes.
        Config.getTopLevelRoutes().forEach((route) => {
          router(`/${route}(/*?)?`, (ctx, next) => {

          // If the page isn't ready.
          if (document.readyState !== 'complete') {
            return next();
          }

          // Load the panel then call the next route.
          self.handleStateChange(null, ctx.state)
            .done(next);
        });
      });

      router.start();

      // Browser detection
      if($.browser.msie && parseInt($.browser.version, 10) < 8) {
        $('.ss-loading-screen').append(
          '<p class="ss-loading-incompat-warning"><span class="notice">' +
          'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' +
          '</span></p>'
        ).css('z-index', $('.ss-loading-screen').css('z-index')+1);
        $('.loading-animation').remove();

        this._super();
        return;
      }

      // Initialize layouts
      this.redraw();

      // Remove loading screen
      $('.ss-loading-screen').hide();
      $('body').removeClass('loading');
      $(window).unbind('resize', positionLoadingSpinner);
      this.restoreTabState();
      this._super();
    },

    fromWindow: {
      onstatechange: function(event, historyState){
        this.handleStateChange(event, historyState);
      }
    },

    'onwindowresize': function() {
      this.redraw();
    },

    'from .cms-panel': {
      ontoggle: function(){ this.redraw(); }
    },

    'from .cms-container': {
      onaftersubmitform: function(){ this.redraw(); }
    },

    /**
     * Ensure the user can see the requested section - restore the default view.
     */
    'from .cms-menu-list li a': {
      onclick: function(e) {
        var href = $(e.target).attr('href');
        if(e.which > 1 || href == this._tabStateUrl()) return;
        this.splitViewMode();
      }
    },

    /**
     * Change the options of the threeColumnCompressor layout, and trigger layouting if needed.
     * You can provide any or all options. The remaining options will not be changed.
     */
    updateLayoutOptions: function(newSpec) {
      var spec = this.getLayoutOptions();

      var dirty = false;

      for (var k in newSpec) {
        if (spec[k] !== newSpec[k]) {
          spec[k] = newSpec[k];
          dirty = true;
        }
      }

      if (dirty) this.redraw();
    },

    /**
     * Enable the split view - with content on the left and preview on the right.
     */
    splitViewMode: function() {
      this.updateLayoutOptions({
        mode: 'split'
      });
    },

    /**
     * Content only.
     */
    contentViewMode: function() {
      this.updateLayoutOptions({
        mode: 'content'
      });
    },

    /**
     * Preview only.
     */
    previewMode: function() {
      this.updateLayoutOptions({
        mode: 'preview'
      });
    },

    RedrawSuppression: false,

    redraw: function() {
      if (this.getRedrawSuppression()) return;

      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

      // Reset the algorithm.
      this.data('jlayout', jLayout.threeColumnCompressor(
        {
          menu: this.children('.cms-menu'),
          content: this.children('.cms-content'),
          preview: this.children('.cms-preview')
        },
        this.getLayoutOptions()
      ));

      // Trigger layout algorithm once at the top. This also lays out children - we move from outside to
      // inside, resizing to fit the parent.
      this.layout();

      // Redraw on all the children that need it
      this.find('.cms-panel-layout').redraw();
      this.find('.cms-content-fields[data-layout-type]').redraw();
      this.find('.cms-edit-form[data-layout-type]').redraw();
      this.find('.cms-preview').redraw();
      this.find('.cms-content').redraw();
    },

    /**
     * Confirm whether the current user can navigate away from this page
     *
     * @param {array} selectors Optional list of selectors
     * @returns {boolean} True if the navigation can proceed
     */
    checkCanNavigate: function(selectors) {
      // Check change tracking (can't use events as we need a way to cancel the current state change)
      var contentEls = this._findFragments(selectors || ['Content']),
        trackedEls = contentEls
          .find(':data(changetracker)')
          .add(contentEls.filter(':data(changetracker)')),
        safe = true;

      if(!trackedEls.length) {
        return true;
      }

      trackedEls.each(function() {
        // See LeftAndMain.EditForm.js
        if(!$(this).confirmUnsavedChanges()) {
          safe = false;
        }
      });

      return safe;
    },

    /**
     * @param string url
     * @param string title - New window title.
     * @param object data - Any additional data passed through to `window.history.state`.
     * @param boolean forceReload - Forces the replacement of the current history state, even if the URL is the same, i.e. allows reloading.
     */
    loadPanel: function (url, title = '', data = {}, forceReload, forceReferer = window.history.state.path) {
      // Check for unsaved changes
      if (!this.checkCanNavigate(data.pjax ? data.pjax.split(',') : ['Content'])) {
        return;
      }

      this.saveTabState();

      data.__forceReferer = forceReferer;

      if (forceReload) {
        data.__forceReload = Math.random(); // Make sure the page reloads even if the URL is the same.
      }

      router.show(url, data);
    },

    /**
     * Nice wrapper for reloading current history state.
     */
    reloadCurrentPanel: function() {
      this.loadPanel(window.history.state.path, null, null, true);
    },

    /**
     * Function: submitForm
     *
     * Parameters:
     *  {DOMElement} form - The form to be submitted. Needs to be passed
     *   in to avoid entwine methods/context being removed through replacing the node itself.
     *  {DOMElement} button - The pressed button (optional)
     *  {Function} callback - Called in complete() handler of jQuery.ajax()
     *  {Object} ajaxOptions - Object literal to merge into $.ajax() call
     *
     * Returns:
     *  (boolean)
     */
    submitForm: function(form, button, callback, ajaxOptions) {
      var self = this;

      // look for save button
      if(!button) button = this.find('.Actions :submit[name=action_save]');
      // default to first button if none given - simulates browser behaviour
      if(!button) button = this.find('.Actions :submit:first');

      form.trigger('beforesubmitform');
      this.trigger('submitform', {form: form, button: button});

      // set button to "submitting" state
      $(button).addClass('loading');

      // validate if required
      var validationResult = form.validate();
      if(typeof validationResult!=='undefined' && !validationResult) {
        // TODO Automatically switch to the tab/position of the first error
        statusMessage("Validation failed.", "bad");

        $(button).removeClass('loading');

        return false;
      }

      // get all data from the form
      var formData = form.serializeArray();
      // add button action
      formData.push({name: $(button).attr('name'), value:'1'});
      // Artificial HTTP referer, IE doesn't submit them via ajax.
      // Also rewrites anchors to their page counterparts, which is important
      // as automatic browser ajax response redirects seem to discard the hash/fragment.
      // TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
      formData.push({ name: 'BackURL', value: window.history.state.path.replace(/\/$/, '') });

      // Save tab selections so we can restore them later
      this.saveTabState();

      // Standard Pjax behaviour is to replace the submitted form with new content.
      // The returned view isn't always decided upon when the request
      // is fired, so the server might decide to change it based on its own logic,
      // sending back different `X-Pjax` headers and content
      jQuery.ajax(jQuery.extend({
        headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"},
        url: form.attr('action'),
        data: formData,
        type: 'POST',
        complete: function() {
          $(button).removeClass('loading');
        },
        success: function(data, status, xhr) {
          form.removeClass('changed'); // TODO This should be using the plugin API
          if(callback) callback(data, status, xhr);

          var newContentEls = self.handleAjaxResponse(data, status, xhr);
          if(!newContentEls) return;

          newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
        }
      }, ajaxOptions));

      return false;
    },

    /**
     * Last html5 history state
     */
    LastState: null,

    /**
     * Flag to pause handleStateChange
     */
    PauseState: false,

    /**
     * Handles ajax loading of new panels through the window.history object.
     * To trigger loading, pass a new URL to router.show().
     * Use loadPanel() as a router.show() wrapper as it provides some additional functionality
     * like global changetracking and user aborts.
     *
     * Due to the nature of history management, no callbacks are allowed.
     * Use the 'beforestatechange' and 'afterstatechange' events instead,
     * or overwrite the beforeLoad() and afterLoad() methods on the
     * DOM element you're loading the new content into.
     * Although you can pass data into router.show(url, data), it shouldn't contain
     * DOM elements or callback closures.
     *
     * The passed URL should allow reconstructing important interface state
     * without additional parameters, in the following use cases:
     * - Explicit loading through router.show()
     * - Implicit loading through browser navigation event triggered by the user (forward or back)
     * - Full window refresh without ajax
     * For example, a ModelAdmin search event should contain the search terms
     * as URL parameters, and the result display should automatically appear
     * if the URL is loaded without ajax.
     */
    handleStateChange: function (event, historyState = window.history.state) {
      if (this.getPauseState()) {
        return;
      }

      // Don't allow parallel loading to avoid edge cases
      if (this.getStateChangeXHR()) {
        this.getStateChangeXHR().abort();
      }

      var self = this,
        fragments = historyState.pjax || 'Content',
        headers = {},
        fragmentsArr = fragments.split(','),
        contentEls = this._findFragments(fragmentsArr);

      this.setStateChangeCount(this.getStateChangeCount() + 1);

      if (!this.checkCanNavigate()) {
        var lastState = this.getLastState();

        // Suppress panel loading while resetting state
        this.setPauseState(true);

        // Restore best last state
        if (lastState !== null) {
          router.show(lastState.url);
        } else {
          router.back();
        }

        this.setPauseState(false);

        // Abort loading of this panel
        return;
      }

      this.setLastState(historyState);

      // If any of the requested Pjax fragments don't exist in the current view,
      // fetch the "Content" view instead, which is the "outermost" fragment
      // that can be reloaded without reloading the whole window.
      if (contentEls.length < fragmentsArr.length) {
        fragments = 'Content', fragmentsArr = ['Content'];
        contentEls = this._findFragments(fragmentsArr);
      }

      this.trigger('beforestatechange', { state: historyState, element: contentEls });

      // Set Pjax headers, which can declare a preference for the returned view.
      // The actually returned view isn't always decided upon when the request
      // is fired, so the server might decide to change it based on its own logic.
      headers['X-Pjax'] = fragments;

      if (typeof historyState.__forceReferer !== 'undefined') {
        // Ensure query string is properly encoded if present
        let url = historyState.__forceReferer;

        try {
          // Prevent double-encoding by attempting to decode
          url = decodeURI(url);
        } catch(e) {
          // URL not encoded, or was encoded incorrectly, so do nothing
        } finally {
          // Set our referer header to the encoded URL
          headers['X-Backurl'] = encodeURI(url);
        }
      }

      contentEls.addClass('loading');

      let promise = $.ajax({
        headers: headers,
        url: historyState.path
      })
      .done((data, status, xhr) => {
        var els = self.handleAjaxResponse(data, status, xhr, historyState);
        self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: historyState});
      })
      .always(() => {
        self.setStateChangeXHR(null);
        // Remove loading indication from old content els (regardless of which are replaced)
        contentEls.removeClass('loading');
      });

      this.setStateChangeXHR(promise);

      return promise;
    },

    /**
     * ALternative to loadPanel/submitForm.
     *
     * Triggers a parallel-fetch of a PJAX fragment, which is a separate request to the
     * state change requests. There could be any amount of these fetches going on in the background,
     * and they don't register as a HTML5 history states.
     *
     * This is meant for updating a PJAX areas that are not complete panel/form reloads. These you'd
     * normally do via submitForm or loadPanel which have a lot of automation built in.
     *
     * On receiving successful response, the framework will update the element tagged with appropriate
     * data-pjax-fragment attribute (e.g. data-pjax-fragment="<pjax-fragment-name>"). Make sure this element
     * is available.
     *
     * Example usage:
     * $('.cms-container').loadFragment('admin/foobar/', 'FragmentName');
     *
     * @param url string Relative or absolute url of the controller.
     * @param pjaxFragments string PJAX fragment(s), comma separated.
     */
    loadFragment: function(url, pjaxFragments) {

      var self = this,
        xhr,
        headers = {},
        baseUrl = $('base').attr('href'),
        fragmentXHR = this.getFragmentXHR();

      // Make sure only one XHR for a specific fragment is currently in progress.
      if(
        typeof fragmentXHR[pjaxFragments]!=='undefined' &&
        fragmentXHR[pjaxFragments]!==null
      ) {
        fragmentXHR[pjaxFragments].abort();
        fragmentXHR[pjaxFragments] = null;
      }

      url = $.path.isAbsoluteUrl(url) ? url : $.path.makeUrlAbsolute(url, baseUrl);
      headers['X-Pjax'] = pjaxFragments;

      xhr = $.ajax({
        headers: headers,
        url: url,
        success: function(data, status, xhr) {
          var elements = self.handleAjaxResponse(data, status, xhr, null);

          // We are fully done now, make it possible for others to hook in here.
          self.trigger('afterloadfragment', { data: data, status: status, xhr: xhr, elements: elements });
        },
        error: function(xhr, status, error) {
          self.trigger('loadfragmenterror', { xhr: xhr, status: status, error: error });
        },
        complete: function() {
          // Reset the current XHR in tracking object.
          var fragmentXHR = self.getFragmentXHR();
          if(
            typeof fragmentXHR[pjaxFragments]!=='undefined' &&
            fragmentXHR[pjaxFragments]!==null
          ) {
            fragmentXHR[pjaxFragments] = null;
          }
        }
      });

      // Store the fragment request so we can abort later, should we get a duplicate request.
      fragmentXHR[pjaxFragments] = xhr;

      return xhr;
    },

    /**
     * Handles ajax responses containing plain HTML, or mulitple
     * PJAX fragments wrapped in JSON (see PjaxResponseNegotiator PHP class).
     * Can be hooked into an ajax 'success' callback.
     *
     * Parameters:
     *   (Object) data
     *   (String) status
     *   (XMLHTTPRequest) xhr
     *   (Object) state The original history state which the request was initiated with
     */
    handleAjaxResponse: function(data, status, xhr, state) {
      var self = this, url, selectedTabs, guessFragment, fragment, $data;

      // Support a full reload
      if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) {
        var baseUrl = $('base').attr('href'),
          rawURL = xhr.getResponseHeader('X-ControllerURL'),
          url = $.path.isAbsoluteUrl(rawURL) ? rawURL : $.path.makeUrlAbsolute(rawURL, baseUrl);

        document.location.href = url;
        return;
      }

      // Pseudo-redirects via X-ControllerURL might return empty data, in which
      // case we'll ignore the response
      if(!data) return;

      // Update title
      var title = xhr.getResponseHeader('X-Title');
      if(title) document.title = decodeURIComponent(title.replace(/\+/g, ' '));

      var newFragments = {}, newContentEls;
      // If content type is text/json (ignoring charset and other parameters)
      if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) {
        newFragments = data;
      } else {

        // Fall back to replacing the content fragment if HTML is returned
        fragment = document.createDocumentFragment();

        jQuery.clean( [ data ], document, fragment, [] );
        $data = $(jQuery.merge( [], fragment.childNodes ));

        // Try and guess the fragment if none is provided
        // TODO: data-pjax-fragment might actually give us the fragment. For now we just check most common case
        guessFragment = 'Content';
        if ($data.is('form') && !$data.is('[data-pjax-fragment~=Content]')) guessFragment = 'CurrentForm';

        newFragments[guessFragment] = $data;
      }

      this.setRedrawSuppression(true);
      try {
        // Replace each fragment individually
        $.each(newFragments, function(newFragment, html) {
          var contentEl = $('[data-pjax-fragment]').filter(function() {
            return $.inArray(newFragment, $(this).data('pjaxFragment').split(' ')) != -1;
          }), newContentEl = $(html);

          // Add to result collection
          if(newContentEls) newContentEls.add(newContentEl);
          else newContentEls = newContentEl;

          // Update panels
          if(newContentEl.find('.cms-container').length) {
            throw 'Content loaded via ajax is not allowed to contain tags matching the ".cms-container" selector to avoid infinite loops';
          }

          // Set loading state and store element state
          var origStyle = contentEl.attr('style');
          var origParent = contentEl.parent();
          var origParentLayoutApplied = (typeof origParent.data('jlayout')!=='undefined');
          var layoutClasses = ['east', 'west', 'center', 'north', 'south', 'column-hidden'];
          var elemClasses = contentEl.attr('class');
          var origLayoutClasses = [];
          if(elemClasses) {
            origLayoutClasses = $.grep(
              elemClasses.split(' '),
              function(val) { return ($.inArray(val, layoutClasses) >= 0);}
            );
          }

          newContentEl
            .removeClass(layoutClasses.join(' '))
            .addClass(origLayoutClasses.join(' '));
          if(origStyle) newContentEl.attr('style', origStyle);

          // Allow injection of inline styles, as they're not allowed in the document body.
          // Not handling this through jQuery.ondemand to avoid parsing the DOM twice.
          var styles = newContentEl.find('style').detach();
          if(styles.length) $(document).find('head').append(styles);

          // Replace panel completely (we need to override the "layout" attribute, so can't replace the child instead)
          contentEl.replaceWith(newContentEl);

          // Force jlayout to rebuild internal hierarchy to point to the new elements.
          // This is only necessary for elements that are at least 3 levels deep. 2nd level elements will
          // be taken care of when we lay out the top level element (.cms-container).
          if (!origParent.is('.cms-container') && origParentLayoutApplied) {
            origParent.layout();
          }
        });

        // Re-init tabs (in case the form tag itself is a tabset)
        var newForm = newContentEls.filter('form');
        if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
      }
      finally {
        this.setRedrawSuppression(false);
      }

      this.redraw();
      this.restoreTabState((state && typeof state.tabState !== 'undefined') ? state.tabState : null);

      return newContentEls;
    },

    /**
     *
     *
     * Parameters:
     * - fragments {Array}
     * Returns: jQuery collection
     */
    _findFragments: function(fragments) {
      return $('[data-pjax-fragment]').filter(function() {
        // Allows for more than one fragment per node
        var i, nodeFragments = $(this).data('pjaxFragment').split(' ');
        for(i in fragments) {
          if($.inArray(fragments[i], nodeFragments) != -1) return true;
        }
        return false;
      });
    },

    /**
     * Function: refresh
     *
     * Updates the container based on the current url
     *
     * Returns: void
     */
    refresh: function() {
      $(window).trigger('statechange');

      $(this).redraw();
    },

    /**
     * Save tab selections in order to reconstruct them later.
     * Requires HTML5 sessionStorage support.
     */
    saveTabState: function() {
      if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;

      var selectedTabs = [], url = this._tabStateUrl();
      this.find('.cms-tabset,.ss-tabset').each(function(i, el) {
        var id = $(el).attr('id');
        if(!id) return; // we need a unique reference
        if(!$(el).data('tabs')) return; // don't act on uninit'ed controls

        // Allow opt-out via data element or entwine property.
        if($(el).data('ignoreTabState') || $(el).getIgnoreTabState()) return;

        selectedTabs.push({id:id, selected:$(el).tabs('option', 'selected')});
      });

      if(selectedTabs) {
        var tabsUrl = 'tabs-' + url;
        try {
          window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs));
        } catch(err) {
          if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
            // If this fails we ignore the error as the only issue is that it
            // does not remember the tab state.
            // This is a Safari bug which happens when private browsing is enabled.
            return;
          } else {
            throw err;
          }
        }
      }
    },

    /**
     * Re-select previously saved tabs.
     * Requires HTML5 sessionStorage support.
     *
     * Parameters:
     *   (Object) Map of tab container selectors to tab selectors.
     *   Used to mark a specific tab as active regardless of the previously saved options.
     */
    restoreTabState: function(overrideStates) {
      var self = this, url = this._tabStateUrl(),
        hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
        sessionData = hasSessionStorage ? window.sessionStorage.getItem('tabs-' + url) : null,
        sessionStates = sessionData ? JSON.parse(sessionData) : false;

      this.find('.cms-tabset, .ss-tabset').each(function() {
        var index, tab,
          tabset = $(this),
          tabsetId = tabset.attr('id'),
          forcedTab = tabset.children('ul').children('li.ss-tabs-force-active');

        if(!tabset.data('tabs')){
          return; // don't act on uninit'ed controls
        }

        // The tabs may have changed, notify the widget that it should update its internal state.
        tabset.tabs('refresh');

        // Make sure the intended tab is selected. Only force the tab on the correct tabset though
        if(forcedTab.length) {
          index = forcedTab.first().index();
        } else if(overrideStates && overrideStates[tabsetId]) {
          tab = tabset.find(overrideStates[tabsetId].tabSelector);
          if(tab.length){
            index = tab.index();
          }
        } else if(sessionStates) {
          $.each(sessionStates, function(i, state) {
            if(tabsetId == state.id) {
              index = state.selected;
            }
          });
        }
        if(index !== null){
          tabset.tabs('option', 'active', index);
          self.trigger('tabstaterestored');
        }
      });
    },

    /**
     * Remove any previously saved state.
     *
     * Parameters:
     *  (String) url Optional (sanitized) URL to clear a specific state.
     */
    clearTabState: function(url) {
      if(typeof(window.sessionStorage)=="undefined") return;

      var s = window.sessionStorage;
      if(url) {
        s.removeItem('tabs-' + url);
      } else {
        for(var i=0;i<s.length;i++) {
          if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i));
      }
      }
    },

    /**
     * Remove tab state for the current URL.
     */
    clearCurrentTabState: function() {
      this.clearTabState(this._tabStateUrl());
    },

    _tabStateUrl: function() {
      return window.history.state.path
        .replace(/\?.*/, '')
        .replace(/#.*/, '')
        .replace($('base').attr('href'), '');
    },

    showLoginDialog: function() {
      var tempid = $('body').data('member-tempid'),
        dialog = $('.leftandmain-logindialog'),
        url = 'CMSSecurity/login';

      // Force regeneration of any existing dialog
      if(dialog.length) dialog.remove();

      // Join url params
      url = $.path.addSearchParams(url, {
        'tempid': tempid,
        'BackURL': window.location.href
      });

      // Show a placeholder for instant feedback. Will be replaced with actual
      // form dialog once its loaded.
      dialog = $('<div class="leftandmain-logindialog"></div>');
      dialog.attr('id', new Date().getTime());
      dialog.data('url', url);
      $('body').append(dialog);
    }
  });

  // Login dialog page
  $('.leftandmain-logindialog').entwine({
    onmatch: function() {
      this._super();

      // Create jQuery dialog
      this.ssdialog({
        iframeUrl: this.data('url'),
        dialogClass: "leftandmain-logindialog-dialog",
        autoOpen: true,
        minWidth: 500,
        maxWidth: 500,
        minHeight: 370,
        maxHeight: 400,
        closeOnEscape: false,
        open: function() {
          $('.ui-widget-overlay').addClass('leftandmain-logindialog-overlay');
        },
        close: function() {
          $('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay');
        }
      });
    },
    onunmatch: function() {
      this._super();
    },
    open: function() {
      this.ssdialog('open');
    },
    close: function() {
      this.ssdialog('close');
    },
    toggle: function(bool) {
      if(this.is(':visible')) this.close();
      else this.open();
    },
    /**
     * Callback activated by CMSSecurity_success.ss
     */
    reauthenticate: function(data) {
      // Replace all SecurityID fields with the given value
      if(typeof(data.SecurityID) !== 'undefined') {
        $(':input[name=SecurityID]').val(data.SecurityID);
      }
      // Update TempID for current user
      if(typeof(data.TempID) !== 'undefined') {
        $('body').data('member-tempid', data.TempID);
      }
      this.close();
    }
  });

  /**
   * Add loading overlay to selected regions in the CMS automatically.
   * Not applied to all "*.loading" elements to avoid secondary regions
   * like the breadcrumbs showing unnecessary loading status.
   */
  $('form.loading,.cms-content.loading,.cms-content-fields.loading,.cms-content-view.loading').entwine({
    onmatch: function() {
      this.append('<div class="cms-content-loading-overlay ui-widget-overlay-light"></div><div class="cms-content-loading-spinner"></div>');
      this._super();
    },
    onunmatch: function() {
      this.find('.cms-content-loading-overlay,.cms-content-loading-spinner').remove();
      this._super();
    }
  });

  /** Make all buttons "hoverable" with jQuery theming. */
  $('.cms input[type="submit"], .cms button, .cms input[type="reset"], .cms .ss-ui-button').entwine({
    onadd: function() {
      this.addClass('ss-ui-button');
      if(!this.data('button')) this.button();
      this._super();
    },
    onremove: function() {
      if(this.data('button')) this.button('destroy');
      this._super();
    }
  });

  /**
   * Loads the link's 'href' attribute into a panel via ajax,
   * as opposed to triggering a full page reload.
   * Little helper to avoid repetition, and make it easy to
   * "opt in" to panel loading, while by default links still exhibit their default behaviour.
   * The PJAX target can be specified via a 'data-pjax-target' attribute.
   */
  $('.cms .cms-panel-link').entwine({
    onclick: function(e) {
      if($(this).hasClass('external-link')) {
        e.stopPropagation();

        return;
      }

      var href = this.attr('href'),
        url = (href && !href.match(/^#/)) ? href : this.data('href'),
        data = {pjax: this.data('pjaxTarget')};

      $('.cms-container').loadPanel(url, null, data);
      e.preventDefault();
    }
  });

  /**
   * Does an ajax loads of the link's 'href' attribute via ajax and displays any FormResponse messages from the CMS.
   * Little helper to avoid repetition, and make it easy to trigger actions via a link,
   * without reloading the page, changing the URL, or loading in any new panel content.
   */
  $('.cms .ss-ui-button-ajax').entwine({
    onclick: function(e) {
      $(this).removeClass('ui-button-text-only');
      $(this).addClass('ss-ui-button-loading ui-button-text-icons');

      var loading = $(this).find(".ss-ui-loading-icon");

      if(loading.length < 1) {
        loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon');

        $(this).prepend(loading);
      }

      loading.show();

      var href = this.attr('href'), url = href ? href : this.data('href');

      jQuery.ajax({
        url: url,
        // Ensure that form view is loaded (rather than whole "Content" template)
        complete: function(xmlhttp, status) {
          var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText;

          try {
            if (typeof msg != "undefined" && msg !== null) eval(msg);
          }
          catch(e) {}

          loading.hide();

          $(".cms-container").refresh();

          $(this).removeClass('ss-ui-button-loading ui-button-text-icons');
          $(this).addClass('ui-button-text-only');
        },
        dataType: 'html'
      });
      e.preventDefault();
    }
  });

  /**
   * Trigger dialogs with iframe based on the links href attribute (see ssui-core.js).
   */
  $('.cms .ss-ui-dialog-link').entwine({
    UUID: null,
    onmatch: function() {
      this._super();
      this.setUUID(new Date().getTime());
    },
    onunmatch: function() {
      this._super();
    },
    onclick: function() {
      this._super();

      var self = this, id = 'ss-ui-dialog-' + this.getUUID();
      var dialog = $('#' + id);
      if(!dialog.length) {
        dialog = $('<div class="ss-ui-dialog" id="' + id + '" />');
        $('body').append(dialog);
      }

      var extraClass = this.data('popupclass')?this.data('popupclass'):'';

      dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass});
      return false;
    }
  });

  /**
   * Add styling to all contained buttons, and create buttonsets if required.
   */
  $('.cms-content .Actions').entwine({
    onmatch: function() {
      this.find('.ss-ui-button').click(function() {
          var form = this.form;

          // forms don't natively store the button they've been triggered with
          if(form) {
            form.clickedButton = this;
            // Reset the clicked button shortly after the onsubmit handlers
            // have fired on the form
          setTimeout(function() {
            form.clickedButton = null;
          }, 10);
        }
      });

      this.redraw();
      this._super();
    },
    onunmatch: function() {
      this._super();
    },
    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

      // Remove whitespace to avoid gaps with inline elements
      this.contents().filter(function() {
        return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
      }).remove();

      // Init buttons if required
      this.find('.ss-ui-button').each(function() {
        if(!$(this).data('button')) $(this).button();
      });

      // Mark up buttonsets
      this.find('.ss-ui-buttonset').buttonset();
    }
  });

  /**
   * Duplicates functionality in DateField.js, but due to using entwine we can match
   * the DOM element on creation, rather than onclick - which allows us to decorate
   * the field with a calendar icon
   */
  $('.cms .field.date input.text').entwine({
    onmatch: function() {
      var holder = $(this).parents('.field.date:first'), config = holder.data();
      if(!config.showcalendar) {
        this._super();
        return;
      }

      config.showOn = 'button';
      if(config.locale && $.datepicker.regional[config.locale]) {
        config = $.extend(config, $.datepicker.regional[config.locale], {});
      }

      $(this).datepicker(config);
      // // Unfortunately jQuery UI only allows configuration of icon images, not sprites
      // this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'});

      this._super();
    },
    onunmatch: function() {
      this._super();
    }
  });

  /**
   * Styled dropdown select fields via chosen. Allows things like search and optgroup
   * selection support. Rather than manually adding classes to selects we want
   * styled, we style everything but the ones we tell it not to.
   *
   * For the CMS we also need to tell the parent div that it has a select so
   * we can fix the height cropping.
   */
  $('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({
    onmatch: function() {
      if(this.is('.no-chosen')) {
        this._super();
        return;
      }

      // Explicitly disable default placeholder if no custom one is defined
      if(!this.data('placeholder')) this.data('placeholder', ' ');

      // We could've gotten stale classes and DOM elements from deferred cache.
      this.removeClass('has-chosen').chosen("destroy");
      this.siblings('.chosen-container').remove();

      // Apply Chosen
      applyChosen(this);

      this._super();
    },
    onunmatch: function() {
      this._super();
    }
  });

  $(".cms-panel-layout").entwine({
    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
    }
  });

  /**
   * Overload the default GridField behaviour (open a new URL in the browser)
   * with the CMS-specific ajax loading.
   */
  $('.cms .ss-gridfield').entwine({
    showDetailView: function(url) {
      // Include any GET parameters from the current URL, as the view state might depend on it.
      // For example, a list prefiltered through external search criteria might be passed to GridField.
      var params = window.location.search.replace(/^\?/, '');
      if(params) url = $.path.addSearchParams(url, params);
      $('.cms-container').loadPanel(url);
    }
  });


  /**
   * Generic search form in the CMS, often hooked up to a GridField results display.
   */
  $('.cms-search-form').entwine({
    onsubmit: function(e) {
      // Remove empty elements and make the URL prettier
      var nonEmptyInputs,
        url;

      nonEmptyInputs = this.find(':input:not(:submit)').filter(function() {
        // Use fieldValue() from jQuery.form plugin rather than jQuery.val(),
        // as it handles checkbox values more consistently
        var vals = $.grep($(this).fieldValue(), function(val) { return (val);});
        return (vals.length);
      });

      url = this.attr('action');

      if(nonEmptyInputs.length) {
        url = $.path.addSearchParams(
          url,
          // Undo jQuery's non-standard serialisation
          // See https://github.com/jquery/jquery/blob/1.7.2/src/ajax.js#L797
          nonEmptyInputs.serialize().replace('+', '%20')
        );
      }

      var container = this.closest('.cms-container');
      container.find('.cms-edit-form').tabs('select',0);  //always switch to the first tab (list view) when searching
      container.loadPanel(url, "", {}, true);

      return false;
    }
  });

  /**
   * Reset button handler. IE8 does not bubble reset events to
   */
  $(".cms-search-form button[type=reset], .cms-search-form input[type=reset]").entwine({
    onclick: function(e) {
      e.preventDefault();

      var form = $(this).parents('form');

      form.clearForm();
      form.find(".dropdown select").prop('selectedIndex', 0).trigger("chosen:updated"); // Reset chosen.js
      form.submit();
    }
  });

  /**
   * Allows to lazy load a panel, by leaving it empty
   * and declaring a URL to load its content via a 'url' HTML5 data attribute.
   * The loaded HTML is cached, with cache key being the 'url' attribute.
   * In order for this to work consistently, we assume that the responses are stateless.
   * To avoid caching, add a 'deferred-no-cache' to the node.
   */
  window._panelDeferredCache = {};
  $('.cms-panel-deferred').entwine({
    onadd: function() {
      this._super();
      this.redraw();
    },
    onremove: function() {
      if(window.debug) console.log('saving', this.data('url'), this);

      // Save the HTML state at the last possible moment.
      // Don't store the DOM to avoid memory leaks.
      if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
      this._super();
    },
    redraw: function() {
      if(window.debug) console.log('redraw', this.attr('class'), this.get(0));

      var self = this, url = this.data('url');
      if(!url) throw 'Elements of class .cms-panel-deferred need a "data-url" attribute';

      this._super();

      // If the node is empty, try to either load it from cache or via ajax.
      if(!this.children().length) {
        if(!this.data('deferredNoCache') && typeof window._panelDeferredCache[url] !== 'undefined') {
          this.html(window._panelDeferredCache[url]);
        } else {
          this.addClass('loading');
          $.ajax({
            url: url,
            complete: function() {
              self.removeClass('loading');
            },
            success: function(data, status, xhr) {
              self.html(data);
            }
          });
        }
      }
    }
  });

  /**
   * Lightweight wrapper around jQuery UI tabs.
   * Ensures that anchor links are set properly,
   * and any nested tabs are scrolled if they have
   * their height explicitly set. This is important
   * for forms inside the CMS layout.
   */
  $('.cms-tabset').entwine({
    onadd: function() {
      // Can't name redraw() as it clashes with other CMS entwine classes
      this.redrawTabs();
      this._super();
    },
    onremove: function() {
      if (this.data('tabs')) this.tabs('destroy');
      this._super();
    },
    redrawTabs: function() {
      this.rewriteHashlinks();

      var id = this.attr('id'), activeTab = this.find('ul:first .ui-tabs-active');

      if(!this.data('uiTabs')) this.tabs({
        active: (activeTab.index() != -1) ? activeTab.index() : 0,
        beforeLoad: function(e, ui) {
          // Disable automatic ajax loading of tabs without matching DOM elements,
          // determining if the current URL differs from the tab URL is too error prone.
          return false;
        },
        activate: function(e, ui) {
          // Usability: Hide actions for "readonly" tabs (which don't contain any editable fields)
          var actions = $(this).closest('form').find('.Actions');
          if($(ui.newTab).closest('li').hasClass('readonly')) {
            actions.fadeOut();
          } else {
            actions.show();
          }
        }
      });
    },

    /**
     * Ensure hash links are prefixed with the current page URL,
     * otherwise jQuery interprets them as being external.
     */
    rewriteHashlinks: function() {
      $(this).find('ul a').each(function() {
        if (!$(this).attr('href')) return;
        var matches = $(this).attr('href').match(/#.*/);
        if(!matches) return;
        $(this).attr('href', document.location.href.replace(/#.*/, '') + matches[0]);
      });
    }
  });

  /**
   * CMS content filters
   */
  $('#filters-button').entwine({
    onmatch: function () {
      this._super();

      this.data('collapsed', true); // The current collapsed state of the element.
      this.data('animating', false); // True if the element is currently animating.
    },
    onunmatch: function () {
      this._super();
    },
    showHide: function () {
      var self = this,
        $filters = $('.cms-content-filters').first(),
        collapsed = this.data('collapsed');

      // Prevent the user from spamming the UI with animation requests.
      if (this.data('animating')) {
        return;
      }

      this.toggleClass('active');
      this.data('animating', true);

      // Slide the element down / up based on it's current collapsed state.
      $filters[collapsed ? 'slideDown' : 'slideUp']({
        complete: function () {
          // Update the element's state.
          self.data('collapsed', !collapsed);
          self.data('animating', false);
        }
      });
    },
    onclick: function () {
      this.showHide();
    }
  });
});
jQuery.entwine('ss', ($) => {


    MarkdownEditorField.addCustomAction('ssEmbed', function(editor){
        if(window.InsertMediaModal) {
            let dialog = $('#insert-md-embed-react__dialog-wrapper');
            if (!dialog.length) {
                dialog = $('<div id="insert-md-embed-react__dialog-wrapper" />');
                $('body').append(dialog);
            }
            dialog.setElement(editor);
            dialog.open();
        }
        else {
            alert('Media embed is not supported');
        }
    });

    MarkdownEditorField.addCustomAction('ssImage', function(editor){
        if(window.InsertMediaModal) {
            let dialog = $('#insert-md-media-react__dialog-wrapper');
            if (!dialog.length) {
                dialog = $('<div id="insert-md-media-react__dialog-wrapper" />');
                $('body').append(dialog);
            }
            dialog.setElement(editor);
            dialog.open();
        }
        else {
            SimpleMDE.drawImage(editor);
        }
    });




    $('.js-markdown-container:visible').entwine({
        onunmatch() {
            this._super();
            ReactDOM.unmountComponentAtNode(this[0]);
        },
        onmatch() {
            this._super();
            this.refresh();
        },
        refresh() {
            let textArea = $(this).parent().find('textarea')[0];
            let data = JSON.parse(textArea.dataset.config);
            let toolbar = ss.markdownConfigs.readToolbarConfigs(data.toolbar);

            ReactDOM.render(
                <MarkdownEditorField textarea={textArea} toolbar={toolbar} identifier={data.identifier}></MarkdownEditorField>,
                this[0]
            );
        }
    });
});
jQuery.entwine('ss', ($) => {
  $('input.slider').entwine({
    getMin() {
      return this.data('min');
    },
    getMax() {
      return this.data('max');
    },
    getOrientation() {
      return this.data('orientation');
    },
    limitValue() {
      let val = parseInt(this.val(), 10);
      if (isNaN(val)) val = 0;
      val = Math.max(this.getMin(), Math.min(this.getMax(), val));
      this.val(val);
      return val;
    },
    onmatch() {
      const _that = this;
      const val = _that.limitValue();
        // setup slider controller
      $("<div class='slide-controller'></div>")
        .insertAfter(this)
        .slider({
          orientation: _that.getOrientation(),
          range: 'min',
          value: val,
          min: _that.getMin(),
          max: _that.getMax(),
          slide: (event, ui) => {
            _that.val(ui.value);
          }
        });
    },
    onchange() {
      this
        .siblings('.slide-controller')
        .slider('value', this.limitValue());
    }
  });
});
$.entwine('ss', function($){

	/**
	 * Class: #Form_VersionsForm
	 *
	 * The left hand side version selection form is the main interface for
	 * users to select a version to view, or to compare two versions
	 */
	$('#Form_VersionsForm').entwine({
		/**
		 * Constructor
		 */
		onmatch: function() {
			this._super();
		},
		onunmatch: function() {
			this._super();
		},
		/**
		 * Function: submit.
		 *
		 * Submits either the compare versions form or the view single form
		 * display based on whether we have two or 1 option selected
		 *
		 * Todo:
		 *		Handle coupling to admin url
		 */
		onsubmit: function(e, d) {
			e.preventDefault();
			
			var id, self = this;
			
			id = this.find(':input[name=ID]').val();

			if(!id) return false;

			var button, url, selected, to, from, compare, data;
			
			compare = (this.find(":input[name=CompareMode]").is(":checked"));
			selected = this.find("table input[type=checkbox]").filter(":checked");
			
			if(compare) {
				if(selected.length != 2) return false;
				
				to = selected.eq(0).val();
				from = selected.eq(1).val();
				button = this.find(':submit[name=action_doCompare]');
				url = i18n.sprintf(this.data('linkTmplCompare'), id,from,to);
			}
			else {
				to = selected.eq(0).val();
				button = this.find(':submit[name=action_doShowVersion]');
				url = i18n.sprintf(this.data('linkTmplShow'), id,to);
			}
			
			$('.cms-container').loadPanel(url, '', {pjax: 'CurrentForm'});
		}
	});

	/**
	 * Class: :input[name=ShowUnpublished]
	 *
	 * Used for toggling whether to show or hide unpublished versions.
	 */
	$('#Form_VersionsForm input[name=ShowUnpublished]').entwine({
		onmatch: function() {
			this.toggle();
			this._super();
		},
		onunmatch: function() {
			this._super();
		},
		/**
		 * Event: :input[name=ShowUnpublished] change
		 *
		 * Changing the show unpublished checkbox toggles whether to show
		 * or hide the unpublished versions. Because those rows may be being
		 * compared this also ensures those rows are unselected.
		 */
		onchange: function() {
			this.toggle();
		},
		toggle: function() {
			var self = $(this);
			var form = self.parents('form');

			if(self.attr('checked')) {
				form.find('tr[data-published=false]').css('display', '');
			} else {
				form.find("tr[data-published=false]").css('display', 'none')._unselect();
			}
		}
	});

	/**
	 * Class: #Form_VersionsForm tr
	 *
	 * An individual row in the versions form. Selecting the row updates
	 * the edit form depending on whether we're showing individual version
	 * information or displaying comparsion.
	 */
	$("#Form_VersionsForm tbody tr").entwine({
		
		/**
		 * Function: onclick
		 *
		 * Selects or deselects the row (if in compare mode). Will trigger
		 * an update of the edit form if either selected (in single mode)
		 * or if this is the second row selected (in compare mode)
		 */
		onclick: function(e) {
			var compare, selected;
			
			// compare mode
			compare = this.parents("form").find(':input[name=CompareMode]').attr("checked");
			selected = this.siblings(".active");
			
			if(compare && this.hasClass('active')) {
				this._unselect();
				
				return;
			}
			else if(compare) {
				// check if we have already selected more than two.
				if(selected.length > 1) {
					return alert(i18n._t('CMS.ONLYSELECTTWO', 'You can only compare two versions at this time.'));
				}
			
				this._select();

				// if this is the second selected then we can compare.
				if(selected.length == 1) {
					this.parents('form').submit();
				}
					
				return;
			}
			else {
				this._select();
				selected._unselect();	
				
				this.parents("form").submit();
			}
		},
		
		/**
		 * Function: _unselect()
		 *
		 * Unselects the row from the form selection.
		 */
		_unselect: function() {
			this.removeClass('active');
			this.find(":input[type=checkbox]").attr("checked", false);
		},
		
		/**
		 * Function: _select()
		 *
		 * Selects the currently matched row in the form selection
		 */
		_select: function() {
			this.addClass('active');
			this.find(":input[type=checkbox]").attr("checked", true);
		}
		
	});
});
Пример #29
0
$.entwine('ss', () => {
  $('.asset-gallery-component-wrapper').entwine({
    onadd() {
      const store = configureStore();

      // Build API callers from the URLs provided to us in the div
      // In time, something like a GraphQL endpoint might be a better way to run

      const filesByParentApi = backend.createEndpointFetcher({
        method: 'get',
        responseFormat: 'json',
        url: this.data('asset-gallery-files-by-parent-url'),
      });

      const deleteApi = backend.createEndpointFetcher({
        method: 'delete',
        payloadFormat: 'urlencoded',
        url: this.data('asset-gallery-delete-url'),
      });

      const addFolderApi = backend.createEndpointFetcher({
        method: 'post',
        payloadFormat: 'urlencoded',
        url: this.data('asset-gallery-add-folder-url'),
      });

      const updateApi = backend.createEndpointFetcher({
        method: 'put',
        payloadFormat: 'urlencoded',
        url: this.data('asset-gallery-update-url'),
      });

      const limit = this.data('asset-gallery-limit');
      const bulkActions = this.data('asset-gallery-bulk-actions');

      const name = $('.asset-gallery').data('asset-gallery-name');
      const section = 'SilverStripe\\AssetAdmin\\Controller\\AssetAdmin';

      // TODO
      // filesBySiblingApi={filesBySiblingApi}
      // searchApi={searchApi}
      // updateApi={updateApi}

      ReactDOM.render(
        <Provider store={store}>
          <AssetAdminContainer
            name={name}
            limit={limit}
            bulkActions={!!bulkActions}

            filesByParentApi={filesByParentApi}
            addFolderApi={addFolderApi}
            deleteApi={deleteApi}
            updateApi={updateApi}
            sectionConfigKey={section}
          />
        </Provider>,
        this[0]
      );
    },
    onremove() {
      ReactDOM.unmountComponentAtNode(this[0]);
    },
  });
});
jQuery.entwine('ss', ($) => {
	/**
   * Kick off an "add to campaign" dialog from the CMS actions.
   */
  $(
    '.cms-content-actions .add-to-campaign-action,' +
    '#add-to-campaign__action'
  ).entwine({
    onclick() {
      let dialog = $('#add-to-campaign__dialog-wrapper');

      if (!dialog.length) {
        dialog = $('<div id="add-to-campaign__dialog-wrapper" />');
        $('body').append(dialog);
      }

      dialog.open();

      return false;
    },
  });

	/**
   * Uses React-Bootstrap in order to replicate the bootstrap styling and JavaScript behaviour.
   * The "add to campaign" dialog is used in a similar fashion in AssetAdmin.
   */
  $('#add-to-campaign__dialog-wrapper').entwine({

    onunmatch() {
      // solves errors given by ReactDOM "no matched root found" error.
      this._clearModal();
    },

    open() {
      this._renderModal(true);
    },

    close() {
      this._renderModal(false);
    },

    _renderModal(show) {
      const handleHide = () => this.close();
      const handleSubmit = (...args) => this._handleSubmitModal(...args);
      const id = $('form.cms-edit-form :input[name=ID]').val();
      const store = window.ss.store;
      const sectionKey = 'SilverStripe\\CMS\\Controllers\\CMSPageEditController';
      const sectionConfig = store.getState().config.sections[sectionKey];
      const modalSchemaUrl = `${sectionConfig.form.AddToCampaignForm.schemaUrl}/${id}`;

      ReactDOM.render(
        <Provider store={store}>
          <FormBuilderModal
            show={show}
            handleSubmit={handleSubmit}
            handleHide={handleHide}
            schemaUrl={modalSchemaUrl}
            bodyClassName="modal__dialog"
            responseClassBad="modal__response modal__response--error"
            responseClassGood="modal__response modal__response--good"
          />
        </Provider>,
        this[0]
      );
    },

    _clearModal() {
      ReactDOM.unmountComponentAtNode(this[0]);
      // this.empty();
    },

    _handleSubmitModal(data, action, submitFn) {
      return submitFn();
    },

  });
});