text: //(c) W-Shadow
  
  /*global wsEditorData, defaultMenu, customMenu, _:false */
  
  /**
   * @property wsEditorData
   * @property {boolean} wsEditorData.wsMenuEditorPro
   *
   * @property {object} wsEditorData.blankMenuItem
   * @property {object} wsEditorData.itemTemplates
   * @property {object} wsEditorData.customItemTemplate
   *
   * @property {string} wsEditorData.adminAjaxUrl
   * @property {string} wsEditorData.imagesUrl
   *
   * @property {string} wsEditorData.menuFormatName
   * @property {string} wsEditorData.menuFormatVersion
   *
   * @property {boolean} wsEditorData.hideAdvancedSettings
   * @property {boolean} wsEditorData.showExtraIcons
   * @property {boolean} wsEditorData.dashiconsAvailable
   * @property {string}  wsEditorData.submenuIconsEnabled
   * @property {Object}  wsEditorData.showHints
   *
   * @property {string} wsEditorData.hideAdvancedSettingsNonce
   * @property {string} wsEditorData.getPagesNonce
   * @property {string} wsEditorData.getPageDetailsNonce
   * @property {string} wsEditorData.disableDashboardConfirmationNonce
   *
   * @property {string} wsEditorData.captionShowAdvanced
   * @property {string} wsEditorData.captionHideAdvanced
   *
   * @property {string} wsEditorData.unclickableTemplateId
   * @property {string} wsEditorData.unclickableTemplateClass
   * @property {string} wsEditorData.embeddedPageTemplateId
   *
   * @property {string} wsEditorData.currentUserLogin
   * @property {string|null} wsEditorData.selectedActor
   *
   * @property {object} wsEditorData.actors
   * @property {string[]} wsEditorData.visibleUsers
   *
   * @property {object} wsEditorData.postTypes
   * @property {object} wsEditorData.taxonomies
   *
   * @property {string|null} wsEditorData.selectedMenu
   * @property {string|null} wsEditorData.selectedSubmenu
   *
   * @property {string} wsEditorData.setTestConfigurationNonce
   * @property {string} wsEditorData.testAccessNonce
   *
   * @property {boolean} wsEditorData.isDemoMode
   * @property {boolean} wsEditorData.isMasterMode
   */
  
  wsEditorData.wsMenuEditorPro = !!wsEditorData.wsMenuEditorPro; //Cast to boolean.
  var wsIdCounter = 0;
  
  //A bit of black magic/hack to convince my IDE that wsAmeLodash is an alias for lodash.
  window.wsAmeLodash = (function() {
  	'use strict';
  	if (typeof wsAmeLodash !== 'undefined') {
  		return wsAmeLodash;
  	}
  	return _.noConflict();
  })();
  
  //These two properties must be objects, not arrays.
  jQuery.each(['grant_access', 'hidden_from_actor'], function(unused, key) {
  	'use strict';
  	if (wsEditorData.blankMenuItem.hasOwnProperty(key) && !jQuery.isPlainObject(wsEditorData.blankMenuItem[key])) {
  		wsEditorData.blankMenuItem[key] = {};
  	}
  });
  
  AmeCapabilityManager = AmeActors;
  
  /**
   * A utility for retrieving post and page titles.
   */
  var AmePageTitles = (function($) {
  	'use strict';
  
  	var me = {}, cache = {};
  
  	function getCacheKey(pageId, blogId) {
  		return blogId + '_' + pageId;
  	}
  
  	/**
  	 * Add a page title to the cache.
  	 *
  	 * @param {Number} pageId Post or page ID.
  	 * @param {Number} blogId Blog ID.
  	 * @param {String} title The title of the post or page.
  	 */
  	me.add = function(pageId, blogId, title) {
  		cache[getCacheKey(pageId, blogId)] = title;
  	};
  
  	/**
  	 * Get page title.
  	 *
  	 * Note: This method does not return the title. Instead, it calls the provided callback with the title
  	 * as the first argument. The callback will be executed asynchronously if the title hasn't been cached yet.
  	 *
  	 * @param {Number} pageId
  	 * @param {Number} blogId
  	 * @param {Function} callback
  	 */
  	me.get = function(pageId, blogId, callback) {
  		var key = getCacheKey(pageId, blogId);
  		if (typeof cache[key] !== 'undefined') {
  			callback(cache[key], pageId, blogId);
  			return;
  		}
  
  		$.getJSON(
  			wsEditorData.adminAjaxUrl,
  			{
  				'action' : 'ws_ame_get_page_details',
  				'_ajax_nonce' : wsEditorData.getPageDetailsNonce,
  				'post_id' : pageId,
  				'blog_id' : blogId
  			},
  			function(details) {
  				var title;
  				if (typeof details.error !== 'undefined'){
  					title = details.error;
  				} else if ((typeof details !== 'object') || (typeof details.post_title === 'undefined')) {
  					title = '< Server error >';
  				} else {
  					title = details.post_title;
  				}
  				cache[key] = title;
  
  				callback(cache[key], pageId, blogId);
  			}
  		);
  	};
  
  	return me;
  })(jQuery);
  
  var AmeEditorApi = {};
  window.AmeEditorApi = AmeEditorApi;
  
  
  (function ($, _){
  'use strict';
  
  var actorSelectorWidget = new AmeActorSelector(AmeActors, wsEditorData.wsMenuEditorPro);
  
  var itemTemplates = {
  	templates: wsEditorData.itemTemplates,
  
  	getTemplateById: function(templateId) {
  		if (wsEditorData.itemTemplates.hasOwnProperty(templateId)) {
  			return wsEditorData.itemTemplates[templateId];
  		} else if ((templateId === '') || (templateId === 'custom')) {
  			return wsEditorData.customItemTemplate;
  		}
  		return null;
  	},
  
  	getDefaults: function (templateId) {
  		var template = this.getTemplateById(templateId);
  		if (template) {
  			return template.defaults;
  		} else {
  			return null;
  		}
  	},
  
  	getDefaultValue: function (templateId, fieldName) {
  		if (fieldName === 'template_id') {
  			return null;
  		}
  
  		var defaults = this.getDefaults(templateId);
  		if (defaults && (typeof defaults[fieldName] !== 'undefined')) {
  			return defaults[fieldName];
  		}
  		return null;
  	},
  
  	hasDefaultValue: function(templateId, fieldName) {
  		return (this.getDefaultValue(templateId, fieldName) !== null);
  	}
  };
  
  /**
   * Set an input field to a value. The only difference from jQuery.val() is that
   * setting a checkbox to true/false will check/clear it.
   *
   * @param input
   * @param value
   */
  function setInputValue(input, value) {
  	if (input.attr('type') === 'checkbox'){
  		input.prop('checked', value);
      } else {
          input.val(value);
      }
  }
  
  /**
   * Get the value of an input field. The only difference from jQuery.val() is that
   * checked/unchecked checkboxes will return true/false.
   *
   * @param input
   * @return {*}
   */
  function getInputValue(input) {
  	if (input.attr('type') === 'checkbox'){
  		return input.is(':checked');
  	}
  	return input.val();
  }
  
  
  /*
   * Utility function for generating pseudo-random alphanumeric menu IDs.
   * Rationale: Simpler than atomically auto-incrementing or globally unique IDs.
   */
  function randomMenuId(prefix, size){
  	prefix = (typeof prefix === 'undefined') ? 'custom_item_' : prefix;
  	size = (typeof size === 'undefined') ? 5 : size;
  
      var suffix = "";
      var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  
      for( var i=0; i < size; i++ ) {
          suffix += possible.charAt(Math.floor(Math.random() * possible.length));
      }
  
      return prefix + suffix;
  }
  
  function outputWpMenu(menu){
  	var menuCopy = $.extend(true, {}, menu);
  	var menuBox = $('#ws_menu_box');
  
  	//Remove the current menu data
  	menuBox.empty();
  	$('#ws_submenu_box').empty();
  
  	//Display the new menu
  	var i = 0;
  	for (var filename in menuCopy){
  		if (!menuCopy.hasOwnProperty(filename)){
  			continue;
  		}
  		outputTopMenu(menuCopy[filename]);
  		i++;
  	}
  
  	//Automatically select the first top-level menu
  	menuBox.find('.ws_menu:first').click();
  }
  
  /**
   * Load a menu configuration in the editor.
   * Note: All previous settings will be discarded without warning. Unsaved changes will be lost.
   *
   * @param {Object} adminMenu The menu structure to load.
   */
  function loadMenuConfiguration(adminMenu) {
  	//There are some menu properties that need to be objects, but PHP JSON-encodes empty associative
  	//arrays as numeric arrays. We want them to be empty objects instead.
  	if (adminMenu.hasOwnProperty('color_presets') && !$.isPlainObject(adminMenu.color_presets)) {
  		adminMenu.color_presets = {};
  	}
  
  	var objectProperties = ['grant_access', 'hidden_from_actor'];
  	//noinspection JSUnusedLocalSymbols
  	function fixEmptyObjects(unused, menuItem) {
  		for (var i = 0; i < objectProperties.length; i++) {
  			var key = objectProperties[i];
  			if (menuItem.hasOwnProperty(key) && !$.isPlainObject(menuItem[key])) {
  				menuItem[key] = {};
  			}
  		}
  		if (menuItem.hasOwnProperty('items')) {
  			$.each(menuItem.items, fixEmptyObjects);
  		}
  	}
  	$.each(adminMenu.tree, fixEmptyObjects);
  
  	//Load color presets from the new configuration.
  	if (typeof adminMenu.color_presets === 'object') {
  		colorPresets = $.extend(true, {}, adminMenu.color_presets);
  	} else {
  		colorPresets = {};
  	}
  	wasPresetDropdownPopulated = false;
  
  	//Load capabilities.
  	AmeCapabilityManager.setGrantedCapabilities(_.get(adminMenu, 'granted_capabilities', {}));
  
  	//Load general menu visibility.
  	generalComponentVisibility = _.get(adminMenu, 'component_visibility', {});
  	AmeEditorApi.refreshComponentVisibility();
  
  	//Display the new admin menu.
  	outputWpMenu(adminMenu.tree);
  }
  
  /*
   * Create edit widgets for a top-level menu and its submenus and append them all to the DOM.
   *
   * Inputs :
   *	menu - an object containing menu data
   *	afterNode - if specified, the new menu widget will be inserted after this node. Otherwise,
   *	            it will be added to the end of the list.
   * Outputs :
   *	Object with two fields - 'menu' and 'submenu' - containing the DOM nodes of the created widgets.
   */
  function outputTopMenu(menu, afterNode){
  	//Create the menu widget
  	var menu_obj = buildMenuItem(menu, true);
  
  	if ( (typeof afterNode !== 'undefined') && (afterNode !== null) ){
  		$(afterNode).after(menu_obj);
  	} else {
  		menu_obj.appendTo('#ws_menu_box');
  	}
  
  	//Create a container for menu items, even if there are none
  	var submenu = buildSubmenu(menu.items, menu_obj.attr('id'));
  	submenu.appendTo('#ws_submenu_box');
  	menu_obj.data('submenu_id', submenu.attr('id'));
  
  	//Note: Update the menu only after its children are ready. It needs the submenu items to decide whether to display
  	//the access checkbox as checked or indeterminate.
  	updateItemEditor(menu_obj);
  
  	return {
  		'menu' : menu_obj,
  		'submenu' : submenu
  	};
  }
  
  /*
   * Create and populate a submenu container.
   */
  function buildSubmenu(items, parentMenuId){
  	//Create a container for menu items, even if there are none
  	var submenu = $('<div class="ws_submenu" style="display:none;"></div>');
  	submenu.attr('id', 'ws-submenu-'+(wsIdCounter++));
  
  	if (parentMenuId) {
  		submenu.data('parent_menu_id', parentMenuId);
  	}
  
  	//Only show menus that have items.
  	//Skip arrays (with a length) because filled menus are encoded as custom objects.
  	var entry = null;
  	if (items) {
  		$.each(items, function(index, item) {
  			entry = buildMenuItem(item, false);
  			if ( entry ){
  				submenu.append(entry);
  				updateItemEditor(entry);
  			}
  		});
  	}
  
  	//Make the submenu sortable
  	makeBoxSortable(submenu);
  
  	return submenu;
  }
  
  /**
   * Create an edit widget for a menu item.
   *
   * @param {Object} itemData
   * @param {Boolean} [isTopLevel] Specify if this is a top-level menu or a sub-menu item. Defaults to false (= sub-item).
   * @return {*} The created widget as a jQuery object.
   */
  function buildMenuItem(itemData, isTopLevel) {
  	isTopLevel = (typeof isTopLevel === 'undefined') ? false : isTopLevel;
  
  	//Create the menu HTML
  	var item = $('<div></div>')
  		.attr('class', "ws_container")
  		.attr('id', 'ws-menu-item-' + (wsIdCounter++))
  		.data('menu_item', itemData)
  		.data('field_editors_created', false);
  
  	item.addClass(isTopLevel ? 'ws_menu' : 'ws_item');
  	if ( itemData.separator ) {
  		item.addClass('ws_menu_separator');
  	}
  
  	//Add a header and a container for property editors (to improve performance
  	//the editors themselves are created later, when the user tries to access them
  	//for the first time).
  	var contents = [];
  	var menuTitle = getFieldValue(itemData, 'menu_title', '');
  	if (menuTitle === '') {
  		menuTitle = '&nbsp;';
  	}
  
  	contents.push(
  		'<div class="ws_item_head">',
  			itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>',
  			'<input type="checkbox" class="ws_actor_access_checkbox">',
  			'<span class="ws_item_title">',
  				formatMenuTitle(menuTitle),
  			'&nbsp;</span>',
  
  		'</div>',
  		'<div class="ws_editbox" style="display: none;"></div>'
  	);
  	item.append(contents.join(''));
  
  	//Apply flags based on the item's state
  	var flags = ['hidden', 'unused', 'custom'];
  	for (var i = 0; i < flags.length; i++) {
  		setMenuFlag(item, flags[i], getFieldValue(itemData, flags[i], false));
  	}
  
  	if ( isTopLevel && !itemData.separator ){
  		//Allow the user to drag menu items to top-level menus
  		item.droppable({
  			'hoverClass' : 'ws_menu_drop_hover',
  
  			'accept' : (function(thing){
  				return thing.hasClass('ws_item');
  			}),
  
  			'drop' : (function(event, ui){
  				var droppedItemData = readItemState(ui.draggable);
  				var new_item = buildMenuItem(droppedItemData, false);
  
  				var sourceSubmenu = ui.draggable.parent();
  				var submenu = $('#' + item.data('submenu_id'));
  				submenu.append(new_item);
  
  				if ( !event.ctrlKey ) {
  					ui.draggable.remove();
  				}
  
  				updateItemEditor(new_item);
  
  				//Moving an item can change aggregate menu permissions. Update the UI accordingly.
  				updateParentAccessUi(submenu);
  				updateParentAccessUi(sourceSubmenu);
  			})
  		});
  	}
  
  	return item;
  }
  
  function jsTrim(str){
  	return str.replace(/^\s+|\s+$/g, "");
  }
  
  //Expose this handy tool to our other scripts.
  AmeEditorApi.jsTrim = jsTrim;
  
  function stripAllTags(input) {
  	//Based on: http://phpjs.org/functions/strip_tags/
  	var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi,
  		commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi;
  	return input.replace(commentsAndPhpTags, '').replace(tags, '');
  }
  
  function truncateString(input, maxLength, padding) {
  	if (typeof padding === 'undefined') {
  		padding = '';
  	}
  
  	if (input.length > maxLength) {
  		input = input.substring(0, maxLength - 1) + padding;
  	}
  
  	return input;
  }
  
  /**
   * Format menu title for display in HTML.
   * Strips tags and truncates long titles.
   *
   * @param {String} title
   * @returns {String}
   */
  function formatMenuTitle(title) {
  	title = stripAllTags(title);
  
  	//Compact whitespace.
  	title = title.replace(/[\s\t\r\n]+/g, ' ');
  	title = jsTrim(title);
  
  	//The max. length was chosen empirically.
  	title = truncateString(title, 34, '\u2026');
  	return title;
  }
  
  //Editor field spec template.
  var baseField = {
  	caption : '[No caption]',
      standardCaption : true,
  	advanced : false,
  	type : 'text',
  	defaultValue: '',
  	onlyForTopMenus: false,
  	addDropdown : false,
  	visible: true,
  
  	write: null,
  	display: null,
  
  	tooltip: null
  };
  
  /*
   * List of all menu fields that have an associated editor
   */
  var knownMenuFields = {
  	'menu_title' : $.extend({}, baseField, {
  		caption : 'Menu title',
  		display: function(menuItem, displayValue, input, containerNode) {
  			//Update the header as well.
  			containerNode.find('.ws_item_title').html(formatMenuTitle(displayValue) + '&nbsp;');
  			return displayValue;
  		},
  		write: function(menuItem, value, input, containerNode) {
  			menuItem.menu_title = value;
  			containerNode.find('.ws_item_title').html(stripAllTags(input.val()) + '&nbsp;');
  		}
  	}),
  
  	'template_id' : $.extend({}, baseField, {
  		caption : 'Target page',
  		type : 'select',
  		options : (function(){
  			//Generate name => id mappings for all item templates + the special "Custom" template.
  			var itemTemplateIds = [];
  			itemTemplateIds.push([wsEditorData.customItemTemplate.name, '']);
  
  			for (var template_id in wsEditorData.itemTemplates) {
  				if (wsEditorData.itemTemplates.hasOwnProperty(template_id)) {
  					itemTemplateIds.push([wsEditorData.itemTemplates[template_id].name, template_id]);
  				}
  			}
  
  			itemTemplateIds.sort(function(a, b) {
  				if (a[1] === b[1]) {
  					return 0;
  				}
  
  				//The "Custom" item is always first.
  				if (a[1] === '') {
  					return -1;
  				} else if (b[1] === '') {
  					return 1;
  				}
  
  				//Top-level items go before submenus.
  				var aIsTop = (a[1].charAt(0) === '>') ? 1 : 0;
  				var bIsTop = (b[1].charAt(0) === '>') ? 1 : 0;
  				if (aIsTop !== bIsTop) {
  					return bIsTop - aIsTop;
  				}
  
  				//Everything else is sorted by name, in alphabetical order.
  				if (a[0] > b[0]) {
  					return 1;
  				} else if (a[0] < b[0]) {
  					return -1;
  				}
  				return 0;
  			});
  
  			return itemTemplateIds;
  		})(),
  
  		write: function(menuItem, value, input, containerNode) {
  			var oldTemplateId = menuItem.template_id;
  
  			menuItem.template_id = value;
  			menuItem.defaults = itemTemplates.getDefaults(menuItem.template_id);
  		    menuItem.custom = (menuItem.template_id === '');
  
  		    // The file/URL of non-custom items is read-only and equal to the default
  		    // value. Rationale: simplifies menu generation, prevents some user mistakes.
  		    if (menuItem.template_id !== '') {
  			    menuItem.file = null;
  		    }
  
  		    // The new template might not have default values for some of the fields
  		    // currently set to null (= "default"). In those cases, we need to make
  		    // the current values explicit.
  		    containerNode.find('.ws_edit_field').each(function(index, field){
  			    field = $(field);
  			    var fieldName = field.data('field_name');
  			    var isSetToDefault = (menuItem[fieldName] === null);
  			    var hasDefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName);
  
  			    if (isSetToDefault && !hasDefaultValue) {
  					var oldDefaultValue = itemTemplates.getDefaultValue(oldTemplateId, fieldName);
  					if (oldDefaultValue !== null) {
  						menuItem[fieldName] = oldDefaultValue;
  					}
  			    }
  		    });
  		}
  	}),
  
  	'embedded_page_id' : $.extend({}, baseField, {
  		caption: 'Embedded page ID',
  		defaultValue: 'Select page to display',
  		type: 'text',
  		visible: false, //Displayed on-demand.
  		addDropdown: 'ws_embedded_page_selector',
  
  		display: function(menuItem, displayValue, input) {
  			//Only show this field if the "Embed WP page" template is selected.
  			input.closest('.ws_edit_field').toggle(menuItem.template_id === wsEditorData.embeddedPageTemplateId);
  
  			input.prop('readonly', true);
  			var pageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10),
  				blogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10),
  				formattedId = 'ID: ' + pageId;
  
  			if (pageId <= 0) {
  				return 'Select page =>';
  			}
  
  			if (blogId !== 1) {
  				formattedId = formattedId + ', blog ID: ' + blogId;
  			}
  			displayValue = formattedId;
  
  			AmePageTitles.get(pageId, blogId, function(title) {
  				//If we retrieved the title via AJAX, the user might have selected a different page in the meantime.
  				//Make sure it's still the same page before displaying the title.
  				var currentPageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10),
  					currentBlogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10);
  				if ((currentPageId !== pageId) || (currentBlogId !== blogId)) {
  					return;
  				}
  
  				displayValue = title + ' (' + formattedId + ')';
  				input.val(displayValue);
  			});
  
  			return displayValue;
  		},
  
  		write: function() {
  			//The user cannot directly edit this field. We deliberately ignore writes.
  		}
  	}),
  
  	'file' : $.extend({}, baseField, {
  		caption: 'URL',
  		display: function(menuItem, displayValue, input) {
  			// The URL/file field is read-only for default menus. Also, since the "file"
  			// field is usually set to a page slug or plugin filename for plugin/hook pages,
  			// we display the dynamically generated "url" field here (i.e. the actual URL) instead.
  			if (menuItem.template_id !== '') {
  				input.attr('readonly', 'readonly');
  				displayValue = itemTemplates.getDefaultValue(menuItem.template_id, 'url');
  			} else {
  				input.removeAttr('readonly');
  			}
  			return displayValue;
  		},
  
  		write: function(menuItem, value) {
  			// A menu must always have a non-empty URL. If the user deletes the current value,
  			// reset it to the old value.
  			if (value === '') {
  				value = menuItem.file;
  			}
  			// Default menus always point to the default file/URL.
  			if (menuItem.template_id !== '') {
  				value = null;
  			}
  			menuItem.file = value;
  		}
  	}),
  
  	'access_level' : $.extend({}, baseField, {
  		caption: 'Permissions',
  		defaultValue: 'read',
  		type: 'access_editor',
  		visible: false, //Will be set to visible only in Pro version.
  
  		display: function(menuItem) {
  			//Permissions display is a little complicated and could use improvement.
  			var requiredCap = getFieldValue(menuItem, 'access_level', '');
  			var extraCap = getFieldValue(menuItem, 'extra_capability', '');
  
  			var displayValue = (menuItem.template_id === '') ? '< Custom >' : requiredCap;
  			if (extraCap !== '') {
  				if (menuItem.template_id === '') {
  					displayValue = extraCap;
  				} else {
  					displayValue = displayValue + '+' + extraCap;
  				}
  			}
  
  			return displayValue;
  		},
  
  		write: function(menuItem) {
  			//The required capability can't be directly edited and always equals the default.
  			menuItem.access_level = null;
  		}
  	}),
  
  	'required_capability_read_only' : $.extend({}, baseField, {
  		caption: 'Required capability',
  		defaultValue: 'none',
  		type: 'text',
  		tooltip: "Only users who have this capability can see the menu. "+
  			"The capability can't be changed because it's usually hard-coded in WordPress or the plugin that created the menu."+
  			"<br><br>Use the \"Extra capability\" field to restrict access to this menu.",
  
  		visible: function(menuItem) {
  			//Show only in the free version, on non-custom menus.
  			return !wsEditorData.wsMenuEditorPro && (menuItem.template_id !== '');
  		},
  
  		display: function(menuItem, displayValue, input) {
  			input.prop('readonly', true);
  			return getFieldValue(menuItem, 'access_level', '');
  		},
  
  		write: function(menuItem, value) {
  			//The required capability is read-only. Ignore writes.
  		}
  	}),
  
  	'extra_capability' : $.extend({}, baseField, {
  		caption: 'Extra capability',
  		defaultValue: 'read',
  		type: 'text',
  		addDropdown: 'ws_cap_selector',
  		tooltip: function(menuItem) {
  			if (menuItem.template_id === '') {
  				return 'Only users who have this capability can see the menu.';
  			}
  			return 'An additional capability check that is applied on top of the required capability.';
  		},
  
  		display: function(menuItem) {
  			var requiredCap = getFieldValue(menuItem, 'access_level', '');
  			var extraCap = getFieldValue(menuItem, 'extra_capability', '');
  
  			//On custom menus, show the default required cap when no extra cap is selected.
  			//Otherwise there would be no visible capability requirements at all.
  			var displayValue = extraCap;
  			if ((menuItem.template_id === '') && (extraCap === '')) {
  				displayValue = requiredCap;
  			}
  
  			return displayValue;
  		},
  
  		write: function(menuItem, value) {
  			value = jsTrim(value);
  
  			//Reset to default if the user clears the input.
  			if (value === '') {
  				menuItem.extra_capability = null;
  				return;
  			}
  
  			menuItem.extra_capability = value;
  		}
  	}),
  
  	'appearance_heading' : $.extend({}, baseField, {
  		caption: 'Appearance',
  		advanced : true,
  		onlyForTopMenus: false,
  		type: 'heading',
  		standardCaption: false,
  		visible: false //Only visible in the Pro version.
  	}),
  
  	'icon_url' : $.extend({}, baseField, {
  		caption: 'Icon URL',
  		type : 'icon_selector',
  		advanced : true,
  		defaultValue: 'div',
  		onlyForTopMenus: true,
  
  		display: function(menuItem, displayValue, input, containerNode) {
  			//Display the current icon in the selector.
  			var cssClass = getFieldValue(menuItem, 'css_class', '');
  			var iconUrl = getFieldValue(menuItem, 'icon_url', '', containerNode);
  			displayValue = iconUrl;
  
  			//When submenu icon visibility is set to "only if manually selected",
  			//don't show the default submenu icons.
  			var isDefault = (typeof menuItem.icon_url === 'undefined') || (menuItem.icon_url === null);
  			if (isDefault && (wsEditorData.submenuIconsEnabled === 'if_custom') && containerNode.hasClass('ws_item')) {
  				iconUrl = 'none';
  				cssClass = '';
  			}
  
  			var selectButton = input.closest('.ws_edit_field').find('.ws_select_icon');
  			var cssIcon = selectButton.find('.icon16');
  			var imageIcon = selectButton.find('img');
  
  			var matches = cssClass.match(/\b(ame-)?menu-icon-([^\s]+)\b/);
  			var iconFontMatches = iconUrl && iconUrl.match(/^\s*((dashicons|ame-fa)-[a-z0-9\-]+)/);
  
  			//Icon URL takes precedence over icon class.
  			if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' && !iconFontMatches ) {
  				//Regular image icon.
  				cssIcon.hide();
  				imageIcon.prop('src', iconUrl).show();
  			} else if ( iconFontMatches ) {
  				cssIcon.removeClass().addClass('icon16');
  				if ( iconFontMatches[2] === 'dashicons' ) {
  					//Dashicon.
  					cssIcon.addClass('dashicons ' + iconFontMatches[1]);
  				} else if ( iconFontMatches[2] === 'ame-fa' ) {
  					//FontAwesome icon.
  					cssIcon.addClass('ame-fa ' + iconFontMatches[1]);
  				}
  				imageIcon.hide();
  				cssIcon.show();
  			} else if ( matches ) {
  				//Other CSS-based icon.
  				imageIcon.hide();
  				var iconClass = (matches[1] ? matches[1] : '') + 'icon-' + matches[2];
  				cssIcon.removeClass().addClass('icon16 ' + iconClass).show();
  			} else {
  				//This menu has no icon at all. This is actually a valid state
  				//and WordPress will display a menu like that correctly.
  				imageIcon.hide();
  				cssIcon.removeClass().addClass('icon16').show();
  			}
  
  			return displayValue;
  		}
  	}),
  
  	'colors' : $.extend({}, baseField, {
  		caption: 'Color scheme',
  		defaultValue: 'Default',
  		type: 'color_scheme_editor',
  		onlyForTopMenus: true,
  		visible: false,
  		advanced : true,
  
  		display: function(menuItem, displayValue, input, containerNode) {
  			var colors = getFieldValue(menuItem, 'colors', {}) || {};
  			var colorList = containerNode.find('.ws_color_scheme_display');
  
  			colorList.empty();
  			var count = 0, maxColorsToShow = 7;
  
  			$.each(colors, function(name, value) {
  				if ( !value || (count >= maxColorsToShow) ) {
  					return;
  				}
  
  				colorList.append(
  					$('<span></span>').addClass('ws_color_display_item').css('background-color', value)
  				);
  				count++;
  			});
  
  			if (count === 0) {
  				colorList.append('Default');
  			}
  
  			return 'Placeholder. You should never see this.';
  		},
  
  		write: function(menuItem) {
  			//Menu colors can't be directly edited.
  		}
  	}),
  
  	'html_heading' : $.extend({}, baseField, {
  		caption: 'HTML',
  		advanced : true,
  		onlyForTopMenus: true,
  		type: 'heading',
  		standardCaption: false
  	}),
  
  	'open_in' : $.extend({}, baseField, {
  		caption: 'Open in',
  		advanced : true,
  		type : 'select',
  		options : [
  			['Same window or tab', 'same_window'],
  			['New window', 'new_window'],
  			['Frame', 'iframe']
  		],
  		defaultValue: 'same_window',
  		visible: false
  	}),
  
  	'iframe_height' : $.extend({}, baseField, {
  		caption: 'Frame height (pixels)',
  		advanced : true,
  		visible: function(menuItem) {
  			return wsEditorData.wsMenuEditorPro && (getFieldValue(menuItem, 'open_in') === 'iframe');
  		},
  
  		display: function(menuItem, displayValue, input) {
  			input.prop('placeholder', 'Auto');
  			if (displayValue === 0 || displayValue === '0') {
  				displayValue = '';
  			}
  			return displayValue;
  		},
  
  		write: function(menuItem, value) {
  			value = parseInt(value, 10);
  			if (isNaN(value) || (value < 0)) {
  				value = 0;
  			}
  			value = Math.round(value);
  
  			if (value > 10000) {
  				value = 10000;
  			}
  
  			if (value === 0) {
  				menuItem.iframe_height = null;
  			} else {
  				menuItem.iframe_height = value;
  			}
  
  		}
  	}),
  
  	'css_class' : $.extend({}, baseField, {
  		caption: 'CSS classes',
  		advanced : true,
  		onlyForTopMenus: true
  	}),
  
  	'hookname' : $.extend({}, baseField, {
  		caption: 'ID attribute',
  		advanced : true,
  		onlyForTopMenus: true
  	}),
  
  	'page_properties_heading' : $.extend({}, baseField, {
  		caption: 'Page',
  		advanced : true,
  		onlyForTopMenus: true,
  		type: 'heading',
  		standardCaption: false
  	}),
  
  	'page_heading' : $.extend({}, baseField, {
  		caption: 'Page heading',
  		advanced : true,
  		onlyForTopMenus: false,
  		visible: false
  	}),
  
  	'page_title' : $.extend({}, baseField, {
  		caption: "Window title",
  		standardCaption : true,
  		advanced : true
  	}),
      
      'group_title' : $.extend({}, baseField, {
          caption: "Group title",
          onlyForTopMenus: true,
          standardCaption : true,
          advanced : true
      }),
  
  	'is_always_open' : $.extend({}, baseField, {
  		caption: 'Keep this menu expanded',
  		advanced : true,
  		onlyForTopMenus: true,
  		type: 'checkbox',
  		standardCaption: false
  	})
  };
  
  AmeEditorApi.getItemDisplayUrl = function(menuItem) {
  	var url = getFieldValue(menuItem, 'file', '');
  	if (menuItem.template_id !== '') {
  		//Use the template URL. It's a preset that can't be overridden.
  		var defaultUrl = itemTemplates.getDefaultValue(menuItem.template_id, 'url');
  		if (defaultUrl) {
  			url = defaultUrl;
  		}
  	}
  	return url;
  };
  
  /*
   * Create editors for the visible fields of a menu entry and append them to the specified node.
   */
  function buildEditboxFields(fieldContainer, entry, isTopLevel){
  	isTopLevel = (typeof isTopLevel === 'undefined') ? false : isTopLevel;
  
  	var basicFields = $('<div class="ws_edit_panel ws_basic"></div>').appendTo(fieldContainer);
      var advancedFields = $('<div class="ws_edit_panel ws_advanced"></div>').appendTo(fieldContainer);
  
      if ( wsEditorData.hideAdvancedSettings ){
      	advancedFields.css('display', 'none');
      }
  
  	for (var field_name in knownMenuFields){
  		if (!knownMenuFields.hasOwnProperty(field_name)) {
  			continue;
  		}
  
  		var fieldSpec = knownMenuFields[field_name];
  		if (fieldSpec.onlyForTopMenus && !isTopLevel) {
  			continue;
  		}
  
  		var field = buildEditboxField(entry, field_name, fieldSpec);
  		if (field){
              if (fieldSpec.advanced){
                  advancedFields.append(field);
              } else {
                  basicFields.append(field);
              }
  		}
  	}
  
  	//Add a link that shows/hides advanced fields
  	fieldContainer.append(
  		'<div class="ws_toggle_container"><a href="#" class="ws_toggle_advanced_fields"'+
  		(wsEditorData.hideAdvancedSettings ? '' : ' style="display:none;" ' )+'>'+
  		(wsEditorData.hideAdvancedSettings ? wsEditorData.captionShowAdvanced : wsEditorData.captionHideAdvanced)
  		+'</a></div>'
  	);
  }
  
  /*
   * Create an editor for a specified field.
   */
  //noinspection JSUnusedLocalSymbols
  function buildEditboxField(entry, field_name, field_settings){
  	//Build a form field of the appropriate type
  	var inputBox = null;
  	var basicTextField = '<input type="text" class="ws_field_value">';
  	//noinspection FallthroughInSwitchStatementJS
  	switch(field_settings.type){
  		case 'select':
  			inputBox = $('<select class="ws_field_value">');
  			var option = null;
  			for( var index = 0; index < field_settings.options.length; index++ ){
  				var optionTitle = field_settings.options[index][0];
  				var optionValue = field_settings.options[index][1];
  
  				option = $('<option>')
  					.val(optionValue)
  					.text(optionTitle);
  				option.appendTo(inputBox);
  			}
  			break;
  
          case 'checkbox':
              inputBox = $('<label><input type="checkbox" class="ws_field_value"> <span class="ws_field_label_text">'+
                  field_settings.caption + '</span></label>'
              );
              break;
  
  		case 'access_editor':
  			inputBox = $('<input type="text" class="ws_field_value" readonly="readonly">')
                  .add('<input type="button" class="button ws_launch_access_editor" value="Edit...">');
  			break;
  
  		case 'icon_selector':
  			//noinspection HtmlUnknownTag
  			inputBox = $(basicTextField)
                  .add('<button class="button ws_select_icon" title="Select icon"><div class="icon16 icon-settings"></div><img src="" style="display:none;" alt="Icon"></button>');
  			break;
  
  		case 'color_scheme_editor':
  			inputBox = $('<span class="ws_color_scheme_display">Placeholder</span>')
  				.add('<input type="button" class="button ws_open_color_editor" value="Edit...">');
  			break;
  
  		case 'heading':
  			inputBox = $('<span>' + field_settings.caption + '</span>');
  			break;
  
  		case 'text':
  			/* falls through */
  		default:
  			inputBox = $(basicTextField);
  	}
  
  
  	var className = "ws_edit_field ws_edit_field-"+field_name;
  	if (field_settings.addDropdown){
  		className += ' ws_has_dropdown';
  	}
  	if (!field_settings.standardCaption) {
  		className += ' ws_no_field_caption';
  	}
  	if (field_settings.type === 'heading') {
  		className += ' ws_field_group_heading';
  	}
  
  	var caption = '';
  	if (field_settings.standardCaption) {
  		var tooltip = '';
  		if (field_settings.tooltip !== null) {
  			tooltip = ' <a class="ws_field_tooltip_trigger"><div class="dashicons dashicons-info"></div></a>';
  		}
  		caption = '<span class="ws_field_label_text">' + field_settings.caption + tooltip + '</span><br>';
  	}
  	var editField = $('<div>' + caption + '</div>')
  		.attr('class', className)
  		.append(inputBox);
  
  	if (field_settings.addDropdown) {
  		//Add a dropdown button
  		var dropdownId = field_settings.addDropdown;
  		editField.append(
  			$('<input type="button" value="&#9660;">')
  				.addClass('button ws_dropdown_button ' + dropdownId + '_trigger')
  				.attr('tabindex', '-1')
  				.data('dropdownId', dropdownId)
  		);
  	}
  
  	editField
  		.append(
  			$('<img class="ws_reset_button" title="Reset to default value" src="" alt="Reset">')
  				.attr('src', wsEditorData.imagesUrl + '/transparent16.png')
  		).data('field_name', field_name);
  
  	var visible = true;
  	if (typeof field_settings.visible === 'function') {
  		visible = field_settings.visible(entry, field_name);
  	} else {
  		visible = field_settings.visible;
  	}
  	if (!visible) {
  		editField.css('display', 'none');
  	}
  
  	return editField;
  }
  
  /**
   * Get the parent menu of a menu item.
   *
   * @param containerNode A DOM element as a jQuery object.
   * @return {JQuery} Parent container node, or an empty jQuery set.
   */
  function getParentMenuNode(containerNode) {
  	var submenu = containerNode.closest('.ws_submenu', '#ws_menu_editor'),
  		parentId = submenu.data('parent_menu_id');
  	if (parentId) {
  		return $('#' + parentId);
  	} else {
  		return $([]);
  	}
  }
  
  /**
   * Get all submenu items of a menu item.
   *
   * @param {JQuery} containerNode
   * @return {JQuery} A list of submenu item container nodes, or an empty set.
   */
  function getSubmenuItemNodes(containerNode) {
  	var subMenuId = containerNode.data('submenu_id');
  	if (subMenuId) {
  		return $('#' + subMenuId).find('.ws_container');
  	} else {
  		return $([]);
  	}
  }
  
  /**
   * Apply a callback recursively to a menu item and all of its children, in depth-first order.
   * The callback will be invoked with two arguments: (containerNode, menuItem).
   *
   * @param containerNode
   * @param {Function} callback
   */
  function walkMenuTree(containerNode, callback) {
  	getSubmenuItemNodes(containerNode).each(function() {
  		walkMenuTree($(this), callback);
  	});
  	callback(containerNode, containerNode.data('menu_item'));
  }
  
  /**
   * Update the UI elements that that indicate whether the currently selected
   * actor can access a menu item.
   *
   * @param containerNode
   */
  function updateActorAccessUi(containerNode) {
  	//Update the permissions checkbox & UI
  	var menuItem = containerNode.data('menu_item');
  	if (actorSelectorWidget.selectedActor !== null) {
  		var hasAccess = actorCanAccessMenu(menuItem, actorSelectorWidget.selectedActor);
  		var hasCustomPermissions = actorHasCustomPermissions(menuItem, actorSelectorWidget.selectedActor);
  
  		var isOverrideActive = !hasAccess && getFieldValue(menuItem, 'restrict_access_to_items', false);
  
  		//Check if the parent menu has the "hide all submenus if this is hidden" override in effect.
  		var currentChild = containerNode, parentNode, parentItem;
  		do {
  			parentNode = getParentMenuNode(currentChild);
  			parentItem = parentNode.data('menu_item');
  			if (
  				parentItem
  				&& getFieldValue(parentItem, 'restrict_access_to_items', false)
  				&& !actorCanAccessMenu(parentItem, actorSelectorWidget.selectedActor)
  			) {
  				hasAccess = false;
  				isOverrideActive = true;
  				break;
  			}
  			currentChild = parentNode;
  		} while (parentNode.length > 0);
  
  		var checkbox = containerNode.find('.ws_actor_access_checkbox');
  		checkbox.prop('checked', hasAccess);
  
  		//Display the checkbox in an indeterminate state if the actual menu permissions are unknown
  		//because it uses meta capabilities.
  		var isIndeterminate = (hasAccess === null);
  		//Also show it as indeterminate if some items of this menu are hidden and some are visible,
  		//or if their permissions don't match this menu's permissions.
  		var submenuItems = getSubmenuItemNodes(containerNode);
  		if ((submenuItems.length > 0) && !isOverrideActive)  {
  			var differentPermissions = false;
  			submenuItems.each(function() {
  				var item = $(this).data('menu_item');
  				if ( !item ) { //Skip placeholder items created by drag & drop operations.
  					return true;
  				}
  				var hasSubmenuAccess = actorCanAccessMenu(item, actorSelectorWidget.selectedActor);
  				if (hasSubmenuAccess !== hasAccess) {
  					differentPermissions = true;
  					return false;
  				}
  				return true;
  			});
  
  			if (differentPermissions) {
  				isIndeterminate = true;
  			}
  		}
  		checkbox.prop('indeterminate', isIndeterminate);
  
  		if (isIndeterminate && (hasAccess === null)) {
  			setMenuFlag(
  				containerNode,
  				'uncertain_meta_cap',
  				true,
  				"This item might be visible.\n"
  				+ "The plugin cannot reliably detect if \"" + actorSelectorWidget.selectedDisplayName
  				+ "\" has the \"" + getFieldValue(menuItem, 'access_level', '[No capability]')
  				+ "\" capability. If you need to hide the item, try checking and then unchecking it."
  			);
  		} else {
  			setMenuFlag(containerNode, 'uncertain_meta_cap', false);
  		}
  
  		containerNode.toggleClass('ws_is_hidden_for_actor', !hasAccess);
  		containerNode.toggleClass('ws_has_custom_permissions_for_actor', hasCustomPermissions);
  		setMenuFlag(containerNode, 'custom_actor_permissions', hasCustomPermissions);
  		setMenuFlag(containerNode, 'hidden_from_others', false);
  	} else {
  		containerNode.removeClass('ws_is_hidden_for_actor ws_has_custom_permissions_for_actor');
  		setMenuFlag(containerNode, 'custom_actor_permissions', false);
  		setMenuFlag(containerNode, 'uncertain_meta_cap', false);
  
  		var currentUserActor = 'user:' + wsEditorData.currentUserLogin;
  		var otherActors = _(wsEditorData.actors).keys().without(currentUserActor, 'special:super_admin').value(),
  			hiddenFromCurrentUser = ! actorCanAccessMenu(menuItem, currentUserActor),
  			hasAccessToThisItem = _.curry(actorCanAccessMenu, 2)(menuItem),
  			hiddenFromOthers = _.every(otherActors, function(actorId) {
  				return (hasAccessToThisItem(actorId) === false);
  			}),
  			visibleForSuperAdmin = AmeActors.isMultisite && actorCanAccessMenu(menuItem, 'special:super_admin');
  
  		setMenuFlag(
  			containerNode,
  			'hidden_from_others',
  			hiddenFromOthers,
  			hiddenFromCurrentUser
  				? 'Hidden from everyone'
  				: ('Hidden from everyone except you' + (visibleForSuperAdmin ? ' and Super Admins' : ''))
  		);
  	}
  
  	//Update the "hidden" flag.
  	setMenuFlag(containerNode, 'hidden', itemHasHiddenFlag(menuItem, actorSelectorWidget.selectedActor));
  }
  
  /**
   * Like updateActorAccessUi() except it updates the specified menu's parent, not the menu itself.
   * If the menu has no parent (i.e. it's a top-level menu), this function does nothing.
   *
   * @param containerNode Either a menu item or a submenu container.
   */
  function updateParentAccessUi(containerNode) {
  	var submenu;
  	if ( containerNode.is('.ws_submenu') ) {
  		submenu = containerNode;
  	} else {
  		submenu = containerNode.parent();
  	}
  
  	var parentId = submenu.data('parent_menu_id');
  	if (parentId) {
  		updateActorAccessUi($('#' + parentId));
  	}
  }
  
  /**
   * Update an edit widget with the current menu item settings.
   *
   * @param {JQuery} containerNode
   */
  function updateItemEditor(containerNode) {
  	var menuItem = containerNode.data('menu_item');
  
  	//Apply flags based on the item's state.
  	var flags = ['hidden', 'unused', 'custom'];
  	for (var i = 0; i < flags.length; i++) {
  		setMenuFlag(containerNode, flags[i], getFieldValue(menuItem, flags[i], false));
  	}
  
  	//Update the permissions checkbox & other actor-specific UI
  	updateActorAccessUi(containerNode);
  
  	//Update all input fields with the current values.
  	containerNode.find('.ws_edit_field').each(function(index, field) {
  		field = $(field);
  		var fieldName = field.data('field_name');
  		var input = field.find('.ws_field_value').first();
  
  		var hasADefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName);
  		var defaultValue = getDefaultValue(menuItem, fieldName, null, containerNode);
  		var isDefault = hasADefaultValue && ((typeof menuItem[fieldName] === 'undefined') || (menuItem[fieldName] === null));
  
          if (fieldName === 'access_level') {
              isDefault = (getFieldValue(menuItem, 'extra_capability', '') === '')
  				&& isEmptyObject(menuItem.grant_access)
  				&& (!getFieldValue(menuItem, 'restrict_access_to_items', false));
          } else if (fieldName === 'required_capability_read_only') {
          	isDefault = true;
  	        hasADefaultValue = true;
          }
  
  		field.toggleClass('ws_has_no_default', !hasADefaultValue);
  		field.toggleClass('ws_input_default', isDefault);
  
  		var displayValue = isDefault ? defaultValue : menuItem[fieldName];
  		if (knownMenuFields[fieldName].display !== null) {
  			displayValue = knownMenuFields[fieldName].display(menuItem, displayValue, input, containerNode);
  		}
  
          setInputValue(input, displayValue);
  
  		//Store the value to help with change detection.
  		if (input.length > 0) {
  			$.data(input.get(0), 'ame_last_display_value', displayValue);
  		}
  
  		if (typeof (knownMenuFields[fieldName].visible) === 'function') {
  			var isFieldVisible = knownMenuFields[fieldName].visible(menuItem, fieldName);
  			if (isFieldVisible) {
  				field.css('display', '');
  			} else {
  				field.css('display', 'none');
  			}
  		}
      });
  }
  
  function isEmptyObject(obj) {
      for (var prop in obj) {
          if (obj.hasOwnProperty(prop)) {
              return false;
          }
      }
      return true;
  }
  
  /**
   * Get the current value of a single menu field.
   *
   * If the specified field is not set, this function will attempt to retrieve it
   * from the "defaults" property of the menu object. If *that* fails, it will return
   * the value of the optional third argument defaultValue.
   *
   * @param {Object} entry
   * @param {string} fieldName
   * @param {*} [defaultValue]
   * @param {JQuery} [containerNode]
   * @return {*}
   */
  function getFieldValue(entry, fieldName, defaultValue, containerNode){
  	if ( (typeof entry[fieldName] === 'undefined') || (entry[fieldName] === null) ) {
  		return getDefaultValue(entry, fieldName, defaultValue, containerNode);
  	} else {
  		return entry[fieldName];
  	}
  }
  
  AmeEditorApi.getFieldValue = getFieldValue;
  
  /**
   * Get the default value of a menu field.
   *
   * @param {Object} entry
   * @param {String} fieldName
   * @param {*} [defaultValue]
   * @param {JQuery} [containerNode]
   * @returns {*}
   */
  function getDefaultValue(entry, fieldName, defaultValue, containerNode) {
  	//By default, a submenu item has the same icon as its parent.
  	if ((fieldName === 'icon_url') && containerNode && (wsEditorData.submenuIconsEnabled !== 'never')) {
  		var parentContainerNode = getParentMenuNode(containerNode),
  			parentMenuItem = parentContainerNode.data('menu_item');
  		if (parentMenuItem) {
  			return getFieldValue(parentMenuItem, fieldName, defaultValue, parentContainerNode);
  		}
  	}
  
  	//Use the custom menu title as the page title if the default page title matches the default menu title.
  	//Note that if the page title is an empty string (''), WP automatically uses the menu title. So we do the same.
  	if ((fieldName === 'page_title') && (entry.template_id !== '')) {
  		var defaultPageTitle = itemTemplates.getDefaultValue(entry.template_id, 'page_title'),
  			defaultMenuTitle = itemTemplates.getDefaultValue(entry.template_id, 'menu_title'),
  			customMenuTitle = entry['menu_title'];
  
  		if (
  			(customMenuTitle !== null)
  			&& (customMenuTitle !== '')
  			&& ((defaultPageTitle === '') || (defaultMenuTitle === defaultPageTitle))
  		) {
  			return customMenuTitle;
  		}
  	}
  
  	if (typeof defaultValue === 'undefined') {
  		defaultValue = null;
  	}
  
  	//Known templates take precedence.
  	if ((entry.template_id === '') || (typeof itemTemplates.templates[entry.template_id] !== 'undefined')) {
  		var templateDefault = itemTemplates.getDefaultValue(entry.template_id, fieldName);
  		return (templateDefault !== null) ? templateDefault : defaultValue;
  	}
  
  	if (fieldName === 'template_id') {
  		return null;
  	}
  
  	//Separators can have their own defaults, independent of templates.
  	var hasDefault = (typeof entry.defaults !== 'undefined') && (typeof entry.defaults[fieldName] !== 'undefined');
  	if (hasDefault){
  		return entry.defaults[fieldName];
  	}
  
  	return defaultValue;
  }
  
  /*
   * Make a menu container sortable
   */
  function makeBoxSortable(menuBox){
  	//Make the submenu sortable
  	menuBox.sortable({
  		items: '> .ws_container',
  		cursor: 'move',
  		dropOnEmpty: true,
  		cancel : '.ws_editbox, .ws_edit_link',
  
  		placeholder: 'ws_container ws_sortable_placeholder',
  		forcePlaceholderSize: true,
  
  		stop: function(even, ui) {
  			//Fix incorrect item overlap caused by jQuery.sortable applying the initial z-index as an inline style.
  			ui.item.css('z-index', '');
  
  			//Fix submenu container height. It should be tall enough to reach the selected parent menu.
  			if (ui.item.hasClass('ws_menu') && ui.item.hasClass('ws_active')) {
  				AmeEditorApi.updateSubmenuBoxHeight(ui.item);
  			}
  		}
  	});
  }
  
  /**
   * Iterates over all menu items invoking a callback for each item.
   *
   * The callback will be passed two arguments: the menu item and its UI container node (a jQuery object).
   * You can stop iteration by returning false from the callback.
   *
   * @param {Function} callback
   * @param {boolean} [skipSeparators] Defaults to true. Set to false to include separators in the iteration.
   */
  AmeEditorApi.forEachMenuItem = function(callback, skipSeparators) {
  	if (typeof skipSeparators === 'undefined') {
  		skipSeparators = true;
  	}
  
  	$('#ws_menu_editor').find('.ws_container').each(function() {
  		var containerNode = $(this);
  		if ( !(skipSeparators && containerNode.hasClass('ws_menu_separator')) ) {
  			return callback(containerNode.data('menu_item'), containerNode);
  		}
  	});
  };
  
  /**
   * Select the first menu item that has the specified URL.
   *
   * @param {string} boxSelector
   * @param {string} url
   * @param {boolean|null} [expandProperties]
   * @returns {JQuery}
   */
  AmeEditorApi.selectMenuItemByUrl = function(boxSelector, url, expandProperties) {
  	if (typeof expandProperties === 'undefined') {
  		expandProperties = null;
  	}
  
  	var box = $(boxSelector);
  	if (box.is('#ws_submenu_box')) {
  		box = box.find('.ws_submenu:visible').first();
  	}
  
  	var containerNode =
  		box.find('.ws_container')
  		.filter(function() {
  			var itemUrl = AmeEditorApi.getItemDisplayUrl($(this).data('menu_item'));
  			return (itemUrl === url);
  		})
  		.first();
  
  	if (containerNode.length > 0) {
  		AmeEditorApi.selectItem(containerNode);
  
  		if (expandProperties !== null) {
  			var expandLink = containerNode.find('.ws_edit_link').first();
  			if (expandLink.hasClass('ws_edit_link_expanded') !== expandProperties) {
  				expandLink.click();
  			}
  		}
  	}
  	return containerNode;
  };
  
  /***************************************************************************
                         Parsing & encoding menu inputs
   ***************************************************************************/
  
  /**
   * Encode the current menu structure as JSON
   *
   * @return {String} A JSON-encoded string representing the current menu tree loaded in the editor.
   */
  function encodeMenuAsJSON(tree){
  	if (typeof tree === 'undefined' || !tree) {
  		tree = readMenuTreeState();
  	}
  	tree.format = {
  		name: wsEditorData.menuFormatName,
  		version: wsEditorData.menuFormatVersion
  	};
  
  	//Compress the admin menu.
  	tree = compressMenu(tree);
  
  	return $.toJSON(tree);
  }
  
  function readMenuTreeState(){
  	var tree = {};
  	var menuPosition = 0;
  	var itemsByFilename = {};
  
  	//Gather all menus and their items
  	$('#ws_menu_box').find('.ws_menu').each(function() {
  		var containerNode = this;
  		var menu = readItemState(containerNode, menuPosition++);
  
  		//Attach the current menu to the main structure.
  		var filename = getFieldValue(menu, 'file');
  
  		//Give unclickable items unique keys.
  		if (menu.template_id === wsEditorData.unclickableTemplateId) {
  			ws_paste_count++;
  			filename = '#' + wsEditorData.unclickableTemplateClass + '-' + ws_paste_count;
  		} else if (menu.template_id === wsEditorData.embeddedPageTemplateId) {
  			ws_paste_count++;
  			filename = '#embedded-page-' + ws_paste_count;
  		}
  
  		//Prevent the user from saving top level items with duplicate URLs.
  		//WordPress indexes the submenu array by parent URL and AME uses a {url : menu_data} hashtable internally.
  		//Duplicate URLs would cause problems for both.
  		if (itemsByFilename.hasOwnProperty(filename)) {
  			throw {
  				code: 'duplicate_top_level_url',
  				message: 'Error: Found a duplicate URL! All top level menus must have unique URLs.',
  				duplicates: [itemsByFilename[filename], containerNode]
  			};
  		}
  
  		tree[filename] = menu;
  		itemsByFilename[filename] = containerNode;
  	});
  
  	AmeCapabilityManager.pruneGrantedUserCapabilities();
  
  	return {
  		tree: tree,
  		color_presets: $.extend(true, {}, colorPresets),
  		granted_capabilities: AmeCapabilityManager.getGrantedCapabilities(),
  		component_visibility: $.extend(true, {}, generalComponentVisibility)
  	};
  }
  
  /**
   * Losslessly compress the admin menu configuration.
   * 
   * This is a JS port of the ameMenu::compress() function defined in /includes/menu.php.
   * 
   * @param {Object} adminMenu
   * @returns {Object}
   */
  function compressMenu(adminMenu) {
  	var common = {
  		properties: _.omit(wsEditorData.blankMenuItem, ['defaults']),
  		basic_defaults: _.clone(_.get(wsEditorData.blankMenuItem, 'defaults', {})),
  		custom_item_defaults: _.clone(itemTemplates.getTemplateById('').defaults)
  	};
  
  	adminMenu.format.compressed = true;
  	adminMenu.format.common = common;
  
  	function compressItem(item) {
  		//These empty arrays can be dropped.
  		if ( _.isEmpty(item['grant_access']) ) {
  			delete item['grant_access'];
  		}
  		if ( _.isEmpty(item['items']) ) {
  			delete item['items'];
  		}
  
  		//Normal and custom menu items have different defaults.
  		//Remove defaults that are the same for all items of that type.
  		var defaults = _.get(item, 'custom', false) ? common['custom_item_defaults'] : common['basic_defaults'];
  		if ( _.has(item, 'defaults') ) {
  			_.forEach(defaults, function(value, key) {
  				if (_.has(item['defaults'], key) && (item['defaults'][key] === value)) {
  					delete item['defaults'][key];
  				}
  			});
  		}
  
  		//Remove properties that match the common values.
  		_.forEach(common['properties'], function(value, key) {
  			if (_.has(item, key) && (item[key] === value)) {
  				delete item[key];
  			}
  		});
  
  		return item;
  	}
  
  	adminMenu.tree = _.mapValues(adminMenu.tree, function(topMenu) {
  		topMenu = compressItem(topMenu);
  		if (typeof topMenu.items !== 'undefined') {
  			topMenu.items = _.map(topMenu.items, compressItem);
  		}
  		return topMenu;
  	});
  
  	return adminMenu;
  }
  
  AmeEditorApi.readMenuTreeState = readMenuTreeState;
  AmeEditorApi.encodeMenuAsJson = encodeMenuAsJSON;
  
  /**
   * Extract the current menu item settings from its editor widget.
   *
   * @param itemDiv DOM node containing the editor widget, usually with the .ws_item or .ws_menu class.
   * @param {Number} [position] Menu item position among its sibling menu items. Defaults to zero.
   * @return {Object} A menu object in the tree format.
   */
  function readItemState(itemDiv, position){
  	position = (typeof position === 'undefined') ? 0 : position;
  
  	itemDiv = $(itemDiv);
  	var item = $.extend(true, {}, wsEditorData.blankMenuItem, itemDiv.data('menu_item'), readAllFields(itemDiv));
  
  	item.defaults = itemDiv.data('menu_item').defaults;
  
  	//Save the position data
  	item.position = position;
  	item.defaults.position = position; //The real default value will later overwrite this
  
  	item.separator = itemDiv.hasClass('ws_menu_separator');
  	item.custom = menuHasFlag(itemDiv, 'custom');
  
  	//Gather the menu's sub-items, if any
  	item.items = [];
  	var subMenuId = itemDiv.data('submenu_id');
  	if (subMenuId) {
  		var itemPosition = 0;
  		$('#' + subMenuId).find('.ws_item').each(function () {
  			var sub_item = readItemState(this, itemPosition++);
  			item.items.push(sub_item);
  		});
  	}
  
  	return item;
  }
  
  /*
   * Extract the values of all menu/item fields present in a container node
   *
   * Inputs:
   *	container - a jQuery collection representing the node to read.
   */
  function readAllFields(container){
  	if ( !container.hasClass('ws_container') ){
  		container = container.closest('.ws_container');
  	}
  
  	if ( !container.data('field_editors_created') ){
  		return container.data('menu_item');
  	}
  
  	var state = {};
  
  	//Iterate over all fields of the item
  	container.find('.ws_edit_field').each(function() {
  		var field = $(this);
  
  		//Get the name of this field
  		var field_name = field.data('field_name');
  		//Skip if unnamed
  		if (!field_name) {
  			return true;
  		}
  
  		//Hackety-hack. The "Page" input is for display purposes and contains more than just the ID. Skip it.
  		//Eventually we'll need a better way to handle this.
  		if (field_name === 'embedded_page_id') {
  			return true;
  		}
  		//Headings contain no useful data.
  		if (field.hasClass('ws_field_group_heading')) {
  			return true;
  		}
  
  		//Find the field (usually an input or select element).
  		var input_box = field.find('.ws_field_value');
  
  		//Save null if default used, custom value otherwise
  		if (field.hasClass('ws_input_default')){
  			state[field_name] = null;
  		} else {
  			state[field_name] = getInputValue(input_box);
  		}
  		return true;
  	});
  
      //Permission settings are not stored in the visible access_level field (that's just for show),
      //so do not attempt to read them from there.
      state.access_level = null;
  
  	return state;
  }
  
  
  /***************************************************************************
   Flag manipulation
   ***************************************************************************/
  
  var item_flags = {
  	'custom': 'This is a custom menu item',
  	'unused': 'This item was added since the last time you saved menu settings.',
  	'hidden': 'Cosmetically hidden',
  	'custom_actor_permissions': "The selected role has custom permissions for this item.",
  	'hidden_from_others': 'Hidden from everyone except you.',
  	'uncertain_meta_cap': 'The plugin cannot detect if this item is visible by default.'
  };
  
  function setMenuFlag(item, flag, state, title) {
  	title = title || item_flags[flag];
  	item = $(item);
  
  	var item_class = 'ws_' + flag;
  	var img_class = 'ws_' + flag + '_flag';
  
  	item.toggleClass(item_class, state);
  	if (state) {
  		//Add the flag image.
  		var flag_container = item.find('.ws_flag_container');
  		var image = flag_container.find('.' + img_class);
  		if (image.length === 0) {
  			image = $('<div></div>').addClass('ws_flag').addClass(img_class);
  			flag_container.append(image);
  		}
  		image.attr('title', title);
  	} else {
  		//Remove the flag image.
  		item.find('.' + img_class).remove();
  	}
  }
  
  function menuHasFlag(item, flag){
  	return $(item).hasClass('ws_'+flag);
  }
  
  //The "hidden" flag is special. There's both a global version and one that's actor-specific.
  
  /**
   * Check if a menu item is hidden from an actor.
   * This function only checks the "hidden" and "hidden_from_actor" flags, not permissions.
   *
   * @param {Object} menuItem
   * @param {string|null} actor
   * @returns {boolean}
   */
  function itemHasHiddenFlag(menuItem, actor) {
  	var isHidden = false,
  		userActors,
  		userPrefix = 'user:',
  		userLogin;
  
  	//(Only) A globally hidden item is hidden from everyone.
  	if ((actor === null) || menuItem.hidden) {
  		return menuItem.hidden;
  	}
  
  	if (actor.substr(0, userPrefix.length) === userPrefix) {
  		//You can set an exception for a specific user. It takes precedence.
  		if (menuItem.hidden_from_actor.hasOwnProperty(actor)) {
  			isHidden = menuItem.hidden_from_actor[actor];
  		} else {
  			//Otherwise the item is hidden only if it is hidden from all of the user's roles.
  			userLogin = actorSelectorWidget.selectedActor.substr(userPrefix.length);
  			userActors = AmeCapabilityManager.getGroupActorsFor(userLogin);
  			for (var i = 0; i < userActors.length; i++) {
  				if (menuItem.hidden_from_actor.hasOwnProperty(userActors[i]) && menuItem.hidden_from_actor[userActors[i]]) {
  					isHidden = true;
  				} else {
  					isHidden = false;
  					break;
  				}
  			}
  		}
  	} else {
  		//Roles and the super admin are straightforward.
  		isHidden = menuItem.hidden_from_actor.hasOwnProperty(actor) && menuItem.hidden_from_actor[actor];
  	}
  
  	return isHidden;
  }
  
  /**
   * Toggle menu visibility without changing its permissions.
   *
   * Applies to the selected actor, or all actors if no actor is selected.
   *
   * @param {JQuery} selection A menu container node.
   * @param {boolean} [isHidden] Optional. True = hide the menu, false = show the menu.
   */
  function toggleItemHiddenFlag(selection, isHidden) {
  	var menuItem = selection.data('menu_item');
  
  	//By default, invert the current state.
  	if (typeof isHidden === 'undefined') {
  		isHidden = !itemHasHiddenFlag(menuItem, actorSelectorWidget.selectedActor);
  	}
  
  	//Mark the menu as hidden/visible
  	if (actorSelectorWidget.selectedActor === null) {
  		//For ALL roles and users.
  		menuItem.hidden = isHidden;
  		menuItem.hidden_from_actor = {};
  	} else {
  		//Just for the current role.
  		if (isHidden) {
  			menuItem.hidden_from_actor[actorSelectorWidget.selectedActor] = true;
  		} else {
  			if (actorSelectorWidget.selectedActor.indexOf('user:') === 0) {
  				//User-specific exception. Lets you can hide a menu from all admins but leave it visible to yourself.
  				menuItem.hidden_from_actor[actorSelectorWidget.selectedActor] = false;
  			} else {
  				delete menuItem.hidden_from_actor[actorSelectorWidget.selectedActor];
  			}
  		}
  
  		//When the user un-hides a menu that was globally hidden via the "hidden" flag, we must remove
  		//that flag but also make sure the menu stays hidden from other roles.
  		if (!isHidden && menuItem.hidden) {
  			menuItem.hidden = false;
  			$.each(wsEditorData.actors, function(otherActor) {
  				if (otherActor !== actorSelectorWidget.selectedActor) {
  					menuItem.hidden_from_actor[otherActor] = true;
  				}
  			});
  		}
  	}
  	setMenuFlag(selection, 'hidden', isHidden);
  
  	//Also mark all of it's submenus as hidden/visible
  	var submenuId = selection.data('submenu_id');
  	if (submenuId) {
  		$('#' + submenuId + ' .ws_item').each(function(){
  			toggleItemHiddenFlag($(this), isHidden);
  		});
  	}
  }
  
  /***********************************************************
                    Capability manipulation
   ************************************************************/
  
  function actorCanAccessMenu(menuItem, actor) {
  	if (!$.isPlainObject(menuItem.grant_access)) {
  		menuItem.grant_access = {};
  	}
  
  	//By default, any actor that has the required cap has access to the menu.
  	//Users can override this on a per-menu basis.
  	var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >');
  	var actorHasAccess = false;
  	if (menuItem.grant_access.hasOwnProperty(actor)) {
  		actorHasAccess = menuItem.grant_access[actor];
  	} else {
  		actorHasAccess = AmeCapabilityManager.hasCap(actor, requiredCap, menuItem.grant_access);
  	}
  	return actorHasAccess;
  }
  
  AmeEditorApi.actorCanAccessMenu = actorCanAccessMenu;
  
  function actorHasCustomPermissions(menuItem, actor) {
  	if (menuItem.grant_access && menuItem.grant_access.hasOwnProperty && menuItem.grant_access.hasOwnProperty(actor)) {
  		return (menuItem.grant_access[actor] !== null);
  	}
  	return false;
  }
  
  /**
   * @param containerNode
   * @param {string|Object.<string, boolean>} actor
   * @param {boolean} [allowAccess]
   */
  function setActorAccess(containerNode, actor, allowAccess) {
  	var menuItem = containerNode.data('menu_item');
  
  	//grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays.
  	//However, we want it to be a dictionary.
  	if (!$.isPlainObject(menuItem.grant_access)) {
  		menuItem.grant_access = {};
  	}
  
  	if (typeof actor === 'string') {
  		menuItem.grant_access[actor] = Boolean(allowAccess);
  	} else {
  		_.assign(menuItem.grant_access, actor);
  	}
  }
  
  /**
   * Make a menu item inaccessible to everyone except a particular actor.
   *
   * Will not change access settings for actors that are more specific than the input actor.
   * For example, if the input actor is a "role:", this function will only disable other roles,
   * but will leave "user:" actors untouched.
   *
   * @param {Object} menuItem
   * @param {String} actor
   * @return {Object}
   */
  function denyAccessForAllExcept(menuItem, actor) {
  	//grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays.
  	//However, we want it to be a dictionary.
  	if (!$.isPlainObject(menuItem.grant_access)) {
  		menuItem.grant_access = {};
  	}
  
  	$.each(wsEditorData.actors, function(otherActor) {
  		//If the input actor is more or equally specific...
  		if ((actor === null) || (AmeActorManager.compareActorSpecificity(actor, otherActor) >= 0)) {
  			menuItem.grant_access[otherActor] = false;
  		}
  	});
  
  	if (actor !== null) {
  		menuItem.grant_access[actor] = true;
  	}
  	return menuItem;
  }
  
  /***************************************************************************
   Event handlers
   ***************************************************************************/
  
  //Cut & paste stuff
  var menu_in_clipboard = null;
  var ws_paste_count = 0;
  
  //Color preset stuff.
  var colorPresets = {},
  	wasPresetDropdownPopulated = false;
  
  //General admin menu visibility.
  var generalComponentVisibility = {};
  
  //Combined DOM-ready event handler.
  var isDomReadyDone = false;
  
  function ameOnDomReady() {
  	isDomReadyDone = true;
  
  	//Some editor elements are only available in the Pro version.
  	if (wsEditorData.wsMenuEditorPro) {
  		knownMenuFields.open_in.visible = true;
  		knownMenuFields.access_level.visible = true;
  		knownMenuFields.page_heading.visible = true;
  		knownMenuFields.colors.visible = true;
  		knownMenuFields.appearance_heading.visible = true;
  		knownMenuFields.appearance_heading.onlyForTopMenus = false;
  		knownMenuFields.extra_capability.visible = false; //Superseded by the "access_level" field.
  
  		//The Pro version supports submenu icons, but they can be disabled by the user.
  		knownMenuFields.icon_url.onlyForTopMenus = (wsEditorData.submenuIconsEnabled === 'never');
  
  		$('.ws_hide_if_pro').hide();
  	}
  
  	//Let other plugins filter knownMenuFields.
  	$(document).trigger('filterMenuFields.adminMenuEditor', [knownMenuFields, baseField]);
  
  	//Make the top menu box sortable (we only need to do this once)
      var mainMenuBox = $('#ws_menu_box');
      makeBoxSortable(mainMenuBox);
  
  	/***************************************************************************
  	                  Event handlers for editor widgets
  	 ***************************************************************************/
  	var menuEditorNode = $('#ws_menu_editor'),
  		submenuBox = $('#ws_submenu_box'),
  		submenuDropZone = submenuBox.closest('.ws_main_container').find('.ws_dropzone');
  
  	var currentVisibleSubmenu = null;
  
  	/**
  	 * Select a menu item and show its submenu.
  	 *
  	 * @param {JQuery|HTMLElement} container Menu container node.
  	 */
  	function selectItem(container) {
  		if (container.hasClass('ws_active')) {
  			//The menu item is already selected.
  			return;
  		}
  
  		//Highlight the active item and un-highlight the previous one
  		container.addClass('ws_active');
  		container.siblings('.ws_active').removeClass('ws_active');
  		if (container.hasClass('ws_menu')) {
  			//Show/hide the appropriate submenu
  			if ( currentVisibleSubmenu ){
  				currentVisibleSubmenu.hide();
  			}
  			currentVisibleSubmenu = $('#' + container.data('submenu_id')).show();
  
  			updateSubmenuBoxHeight(container);
  
  			currentVisibleSubmenu.closest('.ws_main_container')
  				.find('.ws_toolbar .ws_delete_menu_button')
  				.toggleClass('ws_button_disabled', !canDeleteItem(getSelectedSubmenuItem()));
  		}
  
  		//Make the "delete" button appear disabled if you can't delete this item.
  		container.closest('.ws_main_container')
  			.find('.ws_toolbar .ws_delete_menu_button')
  			.toggleClass('ws_button_disabled', !canDeleteItem(container));
  	}
  	AmeEditorApi.selectItem = selectItem;
  
  	//Select the clicked menu item and show its submenu
  	menuEditorNode.on('click', '.ws_container', (function () {
  		selectItem($(this));
      }));
  
  	function updateSubmenuBoxHeight(selectedMenu) {
  		//Make the submenu box tall enough to reach the selected item.
  		//This prevents the menu tip (if any) from floating in empty space.
  		if (selectedMenu.hasClass('ws_menu_separator')) {
  			submenuBox.css('min-height', '');
  		} else {
  			var menuTipHeight = 30,
  				empiricalExtraHeight = 4,
  				verticalBoxOffset = (submenuBox.offset().top - mainMenuBox.offset().top),
  				minSubmenuHeight = (selectedMenu.offset().top - mainMenuBox.offset().top)
  					- verticalBoxOffset
  					+ menuTipHeight - submenuDropZone.outerHeight() + empiricalExtraHeight;
  			minSubmenuHeight = Math.max(minSubmenuHeight, 0);
  			submenuBox.css('min-height', minSubmenuHeight);
  		}
  	}
  
  	AmeEditorApi.updateSubmenuBoxHeight = updateSubmenuBoxHeight;
  
  	//Show a notification icon next to the "Permissions" field when the menu item supports extended permissions.
  	function updateExtPermissionsIndicator(container, menuItem) {
  		var extPermissions = AmeItemAccessEditor.detectExtPermissions(AmeEditorApi.getItemDisplayUrl(menuItem)),
  			fieldTitle = container.find('.ws_edit_field-access_level .ws_field_label_text'),
  			indicator = fieldTitle.find('.ws_ext_permissions_indicator');
  
  		if (wsEditorData.wsMenuEditorPro && (extPermissions !== null)) {
  			if (indicator.length < 1) {
  				indicator = $('<div class="dashicons dashicons-info ws_ext_permissions_indicator"></div>');
  				fieldTitle.append(" ").append(indicator);
  			}
  			//Idea: Change the icon based on the kind of permissions available (post type, tags, etc).
  			indicator.show().data('ext_permissions', extPermissions);
  		} else {
  			indicator.hide();
  		}
  	}
  
  	menuEditorNode.on('adminMenuEditor:fieldChange', function(event, menuItem, fieldName) {
  		if ((fieldName === 'template_id') || (fieldName === 'file')) {
  			updateExtPermissionsIndicator($(event.target), menuItem);
  		}
  	});
  
  	//Show/hide a menu's properties
  	menuEditorNode.on('click', '.ws_edit_link', (function (event) {
  		event.preventDefault();
  
  		var container = $(this).parents('.ws_container').first();
  		var box = container.find('.ws_editbox');
  
  		//For performance, the property editors for each menu are only created
  		//when the user tries to access access them for the first time.
  		if ( !container.data('field_editors_created') ){
  			var menuItem = container.data('menu_item');
  			buildEditboxFields(box, menuItem, container.hasClass('ws_menu'));
  			container.data('field_editors_created', true);
  			updateItemEditor(container);
  			updateExtPermissionsIndicator(container, menuItem);
  		}
  
  		$(this).toggleClass('ws_edit_link_expanded');
  		//show/hide the editbox
  		if ($(this).hasClass('ws_edit_link_expanded')){
  			box.show();
  		} else {
  			//Make sure changes are applied before the menu is collapsed
  			box.find('input').change();
  			box.hide();
  		}
      }));
  
      //The "Default" button : Reset to default value when clicked
      menuEditorNode.on('click', '.ws_reset_button', (function () {
          //Find the field div (it holds the field name)
          var field = $(this).parents('.ws_edit_field');
  	    var fieldName = field.data('field_name');
  
  		if ( (field.length > 0) && fieldName ) {
  			//Extract the default value from the menu item.
              var containerNode = field.closest('.ws_container');
  			var menuItem = containerNode.data('menu_item');
  
  			if (fieldName === 'access_level') {
  	            //This is a pretty nasty hack.
  	            menuItem.grant_access = {};
  	            menuItem.extra_capability = null;
  				menuItem.restrict_access_to_items = false;
  				delete menuItem.had_access_before_hiding;
              }
  
  			if (itemTemplates.hasDefaultValue(menuItem.template_id, fieldName)) {
  				menuItem[fieldName] = null;
  				updateItemEditor(containerNode);
  				updateParentAccessUi(containerNode);
  			}
  		}
  	}));
  
  	//When a field is edited, change it's appearance if it's contents don't match the default value.
      function fieldValueChange(){
  	    /* jshint validthis:true */
          var input = $(this);
  		var field = input.parents('.ws_edit_field').first();
  	    var fieldName = field.data('field_name');
  
          if ((fieldName === 'access_level') || (fieldName === 'embedded_page_id')) {
              //These fields are read-only and can never be directly edited by the user.
              //Ignore spurious change events.
              return;
          }
  
  	    var containerNode = field.parents('.ws_container').first();
  	    var menuItem = containerNode.data('menu_item');
  
  	    var oldValue = menuItem[fieldName];
  	    var oldDisplayValue = $.data(this, 'ame_last_display_value');
  	    var value = getInputValue(input);
  	    var defaultValue = getDefaultValue(menuItem, fieldName, null, containerNode);
          var hasADefaultValue = (defaultValue !== null);
  
  	    //Some fields/templates have no default values.
          field.toggleClass('ws_has_no_default', !hasADefaultValue);
          if (!hasADefaultValue) {
              field.removeClass('ws_input_default');
          }
  
          if (field.hasClass('ws_input_default') && (value == defaultValue)) {
              value = null; //null = use default.
          }
  
  	    //Ignore changes where the new value is the same as the old one.
  	    if ((value === oldValue) || (value === oldDisplayValue)) {
  		    return;
  	    }
  
  	    //Update the item.
  	    if (knownMenuFields[fieldName].write !== null) {
  		    knownMenuFields[fieldName].write(menuItem, value, input, containerNode);
  	    } else {
  		    menuItem[fieldName] = value;
  	    }
  
  	    updateItemEditor(containerNode);
  	    updateParentAccessUi(containerNode);
  
  	    containerNode.trigger('adminMenuEditor:fieldChange', [menuItem, fieldName]);
      }
  	menuEditorNode.on('click change', '.ws_field_value', fieldValueChange);
  
  	//Show/hide advanced fields
  	menuEditorNode.on('click', '.ws_toggle_advanced_fields', function(){
  		var self = $(this);
  		var advancedFields = self.parents('.ws_container').first().find('.ws_advanced');
  
  		if ( advancedFields.is(':visible') ){
  			advancedFields.hide();
  			self.text(wsEditorData.captionShowAdvanced);
  		} else {
  			advancedFields.show();
  			self.text(wsEditorData.captionHideAdvanced);
  		}
  
  		return false;
  	});
  
  	//Allow/forbid items in actor-specific views
  	menuEditorNode.on('click', 'input.ws_actor_access_checkbox', function() {
  		if (actorSelectorWidget.selectedActor === null) {
  			return;
  		}
  
  		var checked = $(this).is(':checked');
  		var containerNode = $(this).closest('.ws_container');
  
  		var menu = containerNode.data('menu_item');
  		//Ask for confirmation if the user tries to hide Dashboard -> Home.
  		if ( !checked && ((menu.template_id === 'index.php>index.php') || (menu.template_id === '>index.php')) ) {
  			updateItemEditor(containerNode); //Resets the checkbox back to the old value.
  			confirmDashboardHiding(function(ok) {
  				if (ok) {
  					setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, checked);
  				}
  			});
  		} else {
  			setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, checked);
  		}
  	});
  
  	/**
  	 * This confusingly named function sets actor access for the specified menu item
  	 * and all of its children (if any). It also updates the UI with the new settings.
  	 *
  	 * (And it violates SRP in a particularly egregious manner.)
  	 *
  	 * @param containerNode
  	 * @param {String|Object.<String, Boolean>} actor
  	 * @param {Boolean} [allowAccess]
  	 */
  	function setActorAccessForTreeAndUpdateUi(containerNode, actor, allowAccess) {
  		setActorAccess(containerNode, actor, allowAccess);
  
  		//Apply the same permissions to sub-menus.
  		var subMenuId = containerNode.data('submenu_id');
  		if (subMenuId && containerNode.hasClass('ws_menu')) {
  			$('.ws_item', '#' + subMenuId).each(function() {
  				var node = $(this);
  				setActorAccess(node, actor, allowAccess);
  				updateItemEditor(node);
  			});
  		}
  
  		updateItemEditor(containerNode);
  		updateParentAccessUi(containerNode);
  	}
  
  	/**
  	 * Confirm with the user that they want to hide "Dashboard -> Home".
  	 *
  	 * This particular menu is important because hiding it can cause an "insufficient permissions" error
  	 * to be displayed right when someone logs in, making it look like login failed.
  	 */
  	var permissionConfirmationDialog = $('#ws-ame-dashboard-hide-confirmation').dialog({
  		autoOpen: false,
  		modal: true,
  		closeText: ' ',
  		width: 380,
  		title: 'Warning'
  	});
  	var currentConfirmationCallback = function(ok) {};
  
  	/**
  	 * Confirm hiding "Dashboard -> Home".
  	 *
  	 * @param callback Called when the user selects an option. True = confirmed.
  	 */
  	function confirmDashboardHiding(callback) {
  		//The user can disable the confirmation dialog.
  		if (!wsEditorData.dashboardHidingConfirmationEnabled) {
  			callback(true);
  			return;
  		}
  
  		currentConfirmationCallback = callback;
  		permissionConfirmationDialog.dialog('open');
  	}
  
  	$('#ws_confirm_menu_hiding, #ws_cancel_menu_hiding').click(function() {
  		var confirmed = $(this).is('#ws_confirm_menu_hiding');
  		var dontShowAgain = permissionConfirmationDialog.find('.ws_dont_show_again input[type="checkbox"]').is(':checked');
  
  		currentConfirmationCallback(confirmed);
  		permissionConfirmationDialog.dialog('close');
  
  		if (dontShowAgain) {
  			wsEditorData.dashboardHidingConfirmationEnabled = false;
  			//Run an AJAX request to disable the dialog for this user.
  			$.post(
  				wsEditorData.adminAjaxUrl,
  				{
  					'action' : 'ws_ame_disable_dashboard_hiding_confirmation',
  					'_ajax_nonce' : wsEditorData.disableDashboardConfirmationNonce
  				}
  			);
  		}
  	});
  
  
  	/*************************************************************************
  	                  Access editor dialog
  	 *************************************************************************/
  
  	AmeItemAccessEditor.setup({
  		api: AmeEditorApi,
  		actorSelector: actorSelectorWidget,
  		postTypes: wsEditorData.postTypes,
  		taxonomies: wsEditorData.taxonomies,
  		lodash: _,
  		isPro: wsEditorData.wsMenuEditorPro,
  
  		save: function(menuItem, containerNode, settings) {
  			//Save the new settings.
  			menuItem.extra_capability         = settings.extraCapability;
  			menuItem.grant_access             = settings.grantAccess;
  			menuItem.restrict_access_to_items = settings.restrictAccessToItems;
  
  			//Save granted capabilities.
  			var newlyDisabledCaps = {};
  			_.forEach(settings.grantedCapabilities, function(capabilities, actor) {
  				_.forEach(capabilities, function(grant, capability) {
  					if (!_.isArray(grant)) {
  						grant = [grant, null, null];
  					}
  
  					AmeCapabilityManager.setCap(actor, capability, grant[0], grant[1], grant[2]);
  
  					if (!grant[0]) {
  						if (!newlyDisabledCaps.hasOwnProperty(capability)) {
  							newlyDisabledCaps[capability] = [];
  						}
  						newlyDisabledCaps[capability].push(actor);
  					}
  				});
  			});
  
  			AmeEditorApi.forEachMenuItem(function(menuItem, containerNode) {
  				//When the user unchecks a capability, uncheck ALL menu items associated with that capability.
  				//Anything less won't actually get rid of the capability as enabled menus auto-grant req. caps.
  				var requiredCap = getFieldValue(menuItem, 'access_level');
  				if (newlyDisabledCaps.hasOwnProperty(requiredCap)) {
  					//It's enough to remove custom "allow" settings. The rest happens automatically - items that
  					//have no custom per-role settings use capability checks.
  					_.forEach(newlyDisabledCaps[requiredCap], function(actor) {
  						if (_.get(menuItem.grant_access, actor) === true) {
  							delete menuItem.grant_access[actor];
  						}
  					});
  				}
  
  				//Due to changed caps and cascading submenu overrides, changes to one item's permissions
  				//can affect other items. Lets just update all items.
  				updateActorAccessUi(containerNode);
  			});
  
  			//Refresh the UI.
  			updateItemEditor(containerNode);
  		}
  	});
  
  	menuEditorNode.on('click', '.ws_launch_access_editor', function() {
  		var containerNode = $(this).parents('.ws_container').first();
  		var menuItem = containerNode.data('menu_item');
  
  		AmeItemAccessEditor.open({
  			menuItem: menuItem,
  			containerNode: containerNode,
  			selectedActor: actorSelectorWidget.selectedActor,
  			itemHasSubmenus: (!!(containerNode.data('submenu_id')) &&
  				$('#' + containerNode.data('submenu_id')).find('.ws_item').length > 0)
  		});
  	});
  
  	/***************************************************************************
  		              General dialog handlers
  	 ***************************************************************************/
  
  	$(document).on('click', '.ws_close_dialog', function() {
  		$(this).parents('.ui-dialog-content').dialog('close');
  	});
  
  
  	/***************************************************************************
  	              Drop-down list for combo-box fields
  	 ***************************************************************************/
  
  	var capSelectorDropdown = $('#ws_cap_selector');
  	var currentDropdownOwner = null; //The input element that the dropdown is currently associated with.
  	var currentDropdownOwnerMenu = null; //The menu item that the above input belongs to.
  
  	var isDropdownBeingHidden = false, isSuggestionClick = false;
  
  	//Show/hide the capability drop-down list when the trigger button is clicked
  	$('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked);
  	menuEditorNode.on('mousedown click', '.ws_cap_selector_trigger', onDropdownTriggerClicked);
  
  	function onDropdownTriggerClicked(event){
  		/* jshint validthis:true */
  		var inputBox = null;
  		var button = $(this);
  
  		var isInAccessEditor = false;
  		isSuggestionClick = false;
  
  		//Find the input associated with the button that was clicked.
  		if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) {
  			inputBox = $('#ws_extra_capability');
  			isInAccessEditor = true;
  		} else {
  			inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first();
  		}
  
  		//If the user clicks the same button again while the dropdown is already visible,
  		//ignore the click. The dropdown will be hidden by its "blur" handler.
  		if (event.type === 'mousedown') {
  			if ( capSelectorDropdown.is(':visible') && inputBox.is(currentDropdownOwner) ) {
  				isDropdownBeingHidden = true;
  			}
  			return;
  		} else if (isDropdownBeingHidden) {
  			isDropdownBeingHidden = false; //Ignore the click event.
  			return;
  		}
  
  		//A jQuery UI dialog widget will prevent focus from leaving the dialog. So if we want
  		//the dropdown to be properly focused when displaying it in a dialog, we must make it
  		//a child of the dialog's DOM node (and vice versa when it's not in a dialog).
  		var parentContainer = $(this).closest('.ui-dialog, #ws_menu_editor');
  		if ((parentContainer.length > 0) && (capSelectorDropdown.closest(parentContainer).length === 0)) {
  			var oldHeight = capSelectorDropdown.height(); //Height seems to reset when moving to a new parent.
  			capSelectorDropdown.detach().appendTo(parentContainer).height(oldHeight);
  		}
  
  		//Pre-select the current capability (will clear selection if there's no match).
  		capSelectorDropdown.val(inputBox.val()).show();
  
  		//Move the drop-down near the input box.
  		var inputPos = inputBox.offset();
  		capSelectorDropdown
  			.css({
  				position: 'absolute',
  				zIndex: 1010 //Must be higher than the permissions dialog overlay.
  			})
  			.offset({
  				left: inputPos.left,
  				top : inputPos.top + inputBox.outerHeight()
  			}).
  			width(inputBox.outerWidth());
  
  		currentDropdownOwner = inputBox;
  
  		currentDropdownOwnerMenu = null;
  		if (isInAccessEditor) {
  			currentDropdownOwnerMenu = AmeItemAccessEditor.getCurrentMenuItem();
  		} else {
  			currentDropdownOwnerMenu = currentDropdownOwner.closest('.ws_container').data('menu_item');
  		}
  		
  		capSelectorDropdown.focus();
  
  		capSuggestionFeature.show();
  	}
  
  	//Also show it when the user presses the down arrow in the input field (doesn't work in Opera).
  	$('#ws_extra_capability').bind('keyup', function(event){
  		if ( event.which === 40 ){
  			$('#ws_trigger_capability_dropdown').click();
  		}
  	});
  
  	function hideCapSelector() {
  		capSelectorDropdown.hide();
  		capSuggestionFeature.hide();
  		isSuggestionClick = false;
  	}
  
  	//Event handlers for the drop-down lists themselves
  	var dropdownNodes = $('.ws_dropdown');
  
  	// Hide capability drop-down when it loses focus.
  	dropdownNodes.blur(function(){
  		if (!isSuggestionClick) {
  			hideCapSelector();
  		}
  	});
  
  	dropdownNodes.keydown(function(event){
  
  		//Hide it when the user presses Esc
  		if ( event.which === 27 ){
  			hideCapSelector();
  			if (currentDropdownOwner) {
  				currentDropdownOwner.focus();
  			}
  
  		//Select an item & hide the list when the user presses Enter or Tab
  		} else if ( (event.which === 13) || (event.which === 9) ){
  			hideCapSelector();
  
  			if (currentDropdownOwner) {
  				if ( capSelectorDropdown.val() ){
  					currentDropdownOwner.val(capSelectorDropdown.val()).change();
  				}
  				currentDropdownOwner.focus();
  			}
  
  			event.preventDefault();
  		}
  	});
  
  	//Eat Tab keys to prevent focus theft. Required to make the "select item on Tab" thing work.
  	dropdownNodes.keyup(function(event){
  		if ( event.which === 9 ){
  			event.preventDefault();
  		}
  	});
  
  
  	//Update the input & hide the list when an option is clicked
  	dropdownNodes.click(function(){
  		if (capSelectorDropdown.val()){
  			hideCapSelector();
  			if (currentDropdownOwner) {
  				currentDropdownOwner.val(capSelectorDropdown.val()).change().focus();
  			}
  		}
  	});
  
  	//Highlight an option when the user mouses over it (doesn't work in IE)
  	dropdownNodes.mousemove(function(event){
  		if ( !event.target ){
  			return;
  		}
  
  		var option = event.target;
  		if ( (typeof option.selected !== 'undefined') && !option.selected && option.value ){
  			option.selected = true;
  
  			//Preview which roles have this capability and the required cap.
  			capSuggestionFeature.previewAccessForItem(currentDropdownOwnerMenu, option.value);
  		}
  	});
  
  	/************************************************************************
  	 *                     Capability suggestions
  	 *************************************************************************/
  
  	var capSuggestionFeature = (function() {
  		//This feature is not used in the Pro version because it has a different permission UI.
  		if (wsEditorData.wsMenuEditorPro) {
  			return {
  				previewAccessForItem: function () {},
  				show: function () {},
  				hide: function () {}
  			}
  		}
  
  		var capabilitySuggestions = $('#ws_capability_suggestions'),
  			suggestionBody = capabilitySuggestions.find('table tbody').first().empty(),
  			suggestedCapabilities = AmeActors.getSuggestedCapabilities();
  
  		for (var i = 0; i < suggestedCapabilities.length; i++) {
  			var role = suggestedCapabilities[i].role, capability = suggestedCapabilities[i].capability;
  			$('<tr>')
  				.data('role', role)
  				.data('capability', capability)
  				.append(
  					$('<th>', {text: role.displayName, scope: 'row'}).addClass('ws_ame_role_name')
  				)
  				.append(
  					$('<td>', {text: capability}).addClass('ws_ame_suggested_capability')
  				)
  				.appendTo(suggestionBody);
  		}
  
  		var currentPreviewedCaps = null;
  
  		/**
  		 * Update the access preview.
  		 * @param {string|string[]|null} capabilities
  		 */
  		function previewAccess(capabilities) {
  			if (typeof capabilities === 'string') {
  				capabilities = [capabilities];
  			}
  
  			if (_.isEqual(capabilities, currentPreviewedCaps)) {
  				return;
  			}
  			currentPreviewedCaps = capabilities;
  			capabilitySuggestions.find('#ws_previewed_caps').text(currentPreviewedCaps.join(' + '));
  
  			//Short-circuit the no-caps case.
  			if (capabilities === null || capabilities.length === 0) {
  				suggestionBody.find('tr').removeClass('ws_preview_has_access');
  				return;
  			}
  
  			suggestionBody.find('tr').each(function() {
  				var $row = $(this),
  					role = $row.data('role');
  
  				var hasCaps = true;
  				for (var i = 0; i < capabilities.length; i++) {
  					hasCaps = hasCaps && AmeActors.hasCap(role.id, capabilities[i]);
  				}
  				$row.toggleClass('ws_preview_has_access', hasCaps);
  			});
  		}
  
  		function previewAccessForItem(menuItem, selectedExtraCap) {
  			var requiredCap = '', extraCap = '';
  
  			if (menuItem) {
  				requiredCap = getFieldValue(menuItem, 'access_level', '');
  				extraCap = getFieldValue(menuItem, 'extra_capability', '');
  			}
  			if (typeof selectedExtraCap !== 'undefined') {
  				extraCap = selectedExtraCap;
  			}
  
  			var caps = [];
  			if (menuItem && (menuItem.template_id !== '') || (extraCap === '')) {
  				caps.push(requiredCap);
  			}
  			if (extraCap !== '') {
  				caps.push(extraCap);
  			}
  
  			previewAccess(caps);
  		}
  
  		suggestionBody.on('mouseenter', 'td.ws_ame_suggested_capability', function() {
  			var row = $(this).closest('tr');
  			previewAccessForItem(currentDropdownOwnerMenu, row.data('capability'));
  		});
  
  		capSelectorDropdown.on('keydown keyup', function() {
  			previewAccessForItem(currentDropdownOwnerMenu, capSelectorDropdown.val());
  		});
  
  		suggestionBody.on('mousedown', 'td.ws_ame_suggested_capability', function() {
  			//Don't immediately hide the list when the user tries to click a suggestion.
  			//It would prevent the click from registering.
  			isSuggestionClick = true;
  		});
  
  		suggestionBody.on('click', 'td.ws_ame_suggested_capability', function() {
  			var capability = $(this).closest('tr').data('capability');
  
  			//Change the input to the selected capability.
  			if (currentDropdownOwner) {
  				currentDropdownOwner.val(capability).change();
  			}
  
  			hideCapSelector();
  		});
  
  		//Workaround for pressing LMB on a suggestion, then moving the mouse outside the suggestion box and releasing the button.
  		$(document).on('click', function(event) {
  			if (
  				isSuggestionClick
  				&& capabilitySuggestions.is(':visible')
  				&& ( $(event.target).closest(capabilitySuggestions).length < 1 )
  			) {
  				hideCapSelector();
  			}
  		});
  
  		return {
  			previewAccessForItem: previewAccessForItem,
  			show: function() {
  				//Position the capability suggestion table next to the selector and match heights.
  				capabilitySuggestions
  					.css({
  						position: 'absolute',
  						zIndex: 1009
  					})
  					.show()
  					.position({
  						my: 'left top',
  						at: 'right top',
  						of: capSelectorDropdown,
  						collision: 'none'
  					});
  
  				var selectorHeight = capSelectorDropdown.height(),
  					suggestionsHeight = capabilitySuggestions.height(),
  					desiredHeight = Math.max(selectorHeight, suggestionsHeight);
  				if (selectorHeight < desiredHeight) {
  					capSelectorDropdown.height(desiredHeight);
  				}
  				if (suggestionsHeight < desiredHeight) {
  					capabilitySuggestions.height(desiredHeight);
  				}
  
  				if (currentDropdownOwnerMenu) {
  					previewAccessForItem(currentDropdownOwnerMenu);
  				}
  			},
  			hide: function() {
  				capabilitySuggestions.hide();
  			}
  		};
  	})();
  
  
  	/*************************************************************************
  	                           Icon selector
  	 *************************************************************************/
  	var iconSelector = $('#ws_icon_selector');
  	var currentIconButton = null; //Keep track of the last clicked icon button.
  
  	var iconSelectorTabs = iconSelector.find('#ws_icon_source_tabs');
  	iconSelectorTabs.tabs();
  
  	//When the user clicks one of the available icons, update the menu item.
  	iconSelector.on('click', '.ws_icon_option', function() {
  		var selectedIcon = $(this).addClass('ws_selected_icon');
  		iconSelector.hide();
  
  		//Assign the selected icon to the menu.
  		if (currentIconButton) {
  			var container = currentIconButton.closest('.ws_container');
  			var item = container.data('menu_item');
  
  			//Remove the existing icon class, if any.
  			var cssClass = getFieldValue(item, 'css_class', '');
  			cssClass = jsTrim( cssClass.replace(/\b(ame-)?menu-icon-[^\s]+\b/, '') );
  
  			if (selectedIcon.data('icon-class')) {
  				//Add the new class.
  				cssClass = selectedIcon.data('icon-class') + ' ' + cssClass;
  				//Can't have both a class and an image or we'll get two overlapping icons.
  				item.icon_url = '';
  			} else if (selectedIcon.data('icon-url')) {
  				item.icon_url = selectedIcon.data('icon-url');
  			}
  			item.css_class = cssClass;
  
  			updateItemEditor(container);
  		}
  
  		currentIconButton = null;
  	});
  
  	//Show/hide the icon selector when the user clicks the icon button.
  	menuEditorNode.on('click', '.ws_select_icon', function() {
  		var button = $(this);
  		//Clicking the same button a second time hides the icon list.
  		if ( currentIconButton && button.is(currentIconButton) ) {
  			iconSelector.hide();
  			//noinspection JSUnusedAssignment
  			currentIconButton = null;
  			return;
  		}
  
  		currentIconButton = button;
  
  		var containerNode = currentIconButton.closest('.ws_container');
  		var menuItem = containerNode.data('menu_item');
  		var cssClass = getFieldValue(menuItem, 'css_class', '');
  		var iconUrl = getFieldValue(menuItem, 'icon_url', '', containerNode);
  
  		var customImageOption = iconSelector.find('.ws_custom_image_icon').hide();
  
  		//Highlight the currently selected icon.
  		iconSelector.find('.ws_selected_icon').removeClass('ws_selected_icon');
  
  		var selectedIcon = null;
  		var classMatches = cssClass.match(/\b(ame-)?menu-icon-([^\s]+)\b/);
  		//Dashicons and FontAwesome icons are set via the icon URL field, but they are actually CSS-based.
  		var iconFontMatches = iconUrl && iconUrl.match('^\s*((?:dashicons|ame-fa)-[a-z0-9\-]+)\s*$');
  
  		if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' && !iconFontMatches ) {
  			var currentIcon = iconSelector.find('.ws_icon_option img[src="' + iconUrl + '"]').first().closest('.ws_icon_option');
  			if ( currentIcon.length > 0 ) {
  				selectedIcon = currentIcon.addClass('ws_selected_icon').show();
  			} else {
  				//Display and highlight the custom image.
  				customImageOption.find('img').prop('src', iconUrl);
  				customImageOption.addClass('ws_selected_icon').show().data('icon-url', iconUrl);
  				selectedIcon = customImageOption;
  			}
  		} else if ( classMatches || iconFontMatches ) {
  			//Highlight the icon that corresponds to the current CSS class or Dashicon/FontAwesome icon.
  			var iconClass = iconFontMatches ? iconFontMatches[1] : ((classMatches[1] ? classMatches[1] : '') + 'icon-' + classMatches[2]);
  			selectedIcon = iconSelector.find('.' + iconClass).closest('.ws_icon_option').addClass('ws_selected_icon');
  		}
  
  		//Activate the tab that contains the icon.
  		var activeTabId = ((selectedIcon !== null)
  				? selectedIcon.closest('.ws_tool_tab').prop('id')
  				: 'ws_core_icons_tab'),
  			activeTabItem = iconSelectorTabs.find('a[href="#' + activeTabId + '"]').closest('li');
  		if (activeTabItem.length > 0) {
  			iconSelectorTabs.tabs('option', 'active', activeTabItem.index());
  		}
  
  		iconSelector.show();
  		iconSelector.position({ //Requires jQuery UI.
  			my: 'left top',
  			at: 'left bottom',
  			of: button
  		});
  	});
  
  	//Alternatively, use the WordPress media uploader to select a custom icon.
  	//This code is based on the header selection script in /wp-admin/js/custom-header.js.
  	var mediaFrame = null;
  	$('#ws_choose_icon_from_media').click(function(event) {
  		event.preventDefault();
  
  		//This option is not usable on the demo site since the filesystem is usually read-only.
  		if (wsEditorData.isDemoMode) {
  			alert('Sorry, image upload is disabled in demo mode!');
  			return;
  		}
  
          //If the media frame already exists, reopen it.
          if ( mediaFrame !== null ) {
              mediaFrame.open();
              return;
          }
  
          //Create a custom media frame.
          mediaFrame = wp.media.frames.customAdminMenuIcon = wp.media({
              //Set the title of the modal.
              title: 'Choose a Custom Icon (20x20)',
  
              //Tell it to show only images.
              library: {
                  type: 'image'
              },
  
              //Customize the submit button.
              button: {
                  text: 'Set as icon', //Button text.
                  close: true //Clicking the button closes the frame.
              }
          });
  
          //When an image is selected, set it as the menu icon.
          mediaFrame.on( 'select', function() {
              //Grab the selected attachment.
              var attachment = mediaFrame.state().get('selection').first();
              //TODO: Warn the user if the image exceeds 20x20 pixels.
  
  	        //Set the menu icon to the attachment URL.
              if (currentIconButton) {
                  var container = currentIconButton.closest('.ws_container');
                  var item = container.data('menu_item');
  
                  //Remove the existing icon class, if any.
                  var cssClass = getFieldValue(item, 'css_class', '');
  	            item.css_class = jsTrim( cssClass.replace(/\b(ame-)?menu-icon-[^\s]+\b/, '') );
  
  	            //Set the new icon URL.
  	            item.icon_url = attachment.attributes.url;
  
                  updateItemEditor(container);
              }
  
              currentIconButton = null;
          });
  
  		//If the user closes the frame by via Esc or the "X" button, clear up state.
  		mediaFrame.on('escape', function(){
  			currentIconButton = null;
  		});
  
          mediaFrame.open();
  		iconSelector.hide();
  	});
  
  	//Hide the icon selector if the user clicks outside of it.
  	//Exception: Clicks on "Select icon" buttons are handled above.
  	$(document).on('mouseup', function(event) {
  		if ( !iconSelector.is(':visible') ) {
  			return;
  		}
  
  		if (
  			!iconSelector.is(event.target)
  			&& iconSelector.has(event.target).length === 0
  			&& $(event.target).closest('.ws_select_icon').length === 0
  		) {
  			iconSelector.hide();
  			currentIconButton = null;
  		}
  	});
  
  
  	/*************************************************************************
  	                        Embedded page selector
  	 *************************************************************************/
  
  	var pageSelector = $('#ws_embedded_page_selector'),
  		pageListBox = pageSelector.find('#ws_current_site_pages'),
  		currentPageSelectorButton = null, //The last page dropdown button that was clicked.
  		isPageListPopulated = false,
  		isPageRequestInProgress = false;
  
  	pageSelector.tabs({
  		heightStyle: 'auto',
  		hide: false,
  		show: false
  	});
  	//Hack. The selector needs to be hidden by default, but it can't start out as "display: none" because that makes
  	//jQuery miscalculate tab heights. So we put it in a hidden container, then hide it on load and move it elsewhere.
  	pageSelector.hide().appendTo(menuEditorNode);
  
  	/**
  	 * Update the page selector with the current menu item's settings.
  	 */
  	function updatePageSelector() {
  		var menuItem, selectedPageId = 0, selectedBlogId = 1;
  		if ( currentPageSelectorButton ) {
  			menuItem = currentPageSelectorButton.closest('.ws_container').data('menu_item');
  			selectedPageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10);
  			selectedBlogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10);
  		}
  
  		if (selectedPageId === 0) {
  			pageListBox.val(null);
  		} else {
  			var optionValue = selectedBlogId + '_' + selectedPageId;
  			pageListBox.val(optionValue);
  			if ( pageListBox.val() !== optionValue ) {
  				pageListBox.val('custom');
  			}
  		}
  
  		pageSelector.find('#ws_embedded_page_id').val(selectedPageId);
  		pageSelector.find('#ws_embedded_page_blog_id').val(selectedBlogId);
  	}
  
  	menuEditorNode.on('click', '.ws_embedded_page_selector_trigger', function(event) {
  		var thisButton = $(this),
  			thisInput = thisButton.closest('.ws_edit_field').find('input.ws_field_value:first');
  
  		//Clicking the same button a second time hides the page selector.
  		if (thisButton.is(currentPageSelectorButton) && pageSelector.is(':visible')) {
  			pageSelector.hide();
  			//noinspection JSUnusedAssignment
  			currentPageSelectorButton = null;
  			return;
  		}
  
  		currentPageSelectorButton = thisButton;
  		pageSelector.show();
  		pageSelector.position({
  			my: 'left top',
  			at: 'left bottom',
  			of: thisInput
  		});
  
  		event.stopPropagation();
  
  		if (!isPageListPopulated && !isPageRequestInProgress) {
  			isPageRequestInProgress = true;
  
  			var pageList = pageSelector.find('#ws_current_site_pages');
  			pageList.prop('readonly', true);
  
  			$.getJSON(
  				wsEditorData.adminAjaxUrl,
  				{
  					'action' : 'ws_ame_get_pages',
  					'_ajax_nonce' : wsEditorData.getPagesNonce
  				},
  				function(data){
  					isPageRequestInProgress = false;
  					pageList.prop('readonly', false);
  
  					if (typeof data.error !== 'undefined'){
  						alert(data.error);
  						return;
  					} else if ((typeof data !== 'object') || (typeof data.length === 'undefined')) {
  						alert('Error: Could not retrieve a list of pages. Unexpected response from the server.');
  						return;
  					}
  
  					//An alphabetised list is easier to scan visually.
  					var pages = data.sort(function(a, b) {
  						return a.post_title.localeCompare(b.post_title);
  					});
  
  					//Populate the select box.
  					pageList.empty();
  					$.each(pages, function(index, page) {
  						pageList.append($('<option>', {
  							val: page.blog_id + '_' + page.post_id,
  							text: page.post_title
  						}));
  					});
  
  					//Add a "custom" option. Select it when the current setting doesn't match any of the listed pages.
  					pageList.prepend($('<option>', {
  						val: 'custom',
  						text: '< Custom >'
  					}));
  
  					updatePageSelector();
  					isPageListPopulated = true;
  				},
  				'json'
  			);
  
  		}
  
  		updatePageSelector();
  
  		//Open the "Pages" tab by default, or the "Custom" tab if that's what's selected in the list box.
  		//The updatePageSelector call above sets the pageListBox value.
  		pageSelector.tabs('option', 'active', (pageListBox.val() === 'custom') ? 1 : 0);
  	});
  
  	//Hide the page selector if the user clicks outside of it and outside the current button.
  	$(document).on('mouseup', function(event) {
  		if ( !pageSelector.is(':visible') ) {
  			return;
  		}
  
  		var target = $(event.target);
  		var isOutsideSelector = target.closest(pageSelector).length === 0;
  		var isOutsideButton = currentPageSelectorButton && (target.closest(currentPageSelectorButton).length === 0);
  
  		if (isOutsideSelector && isOutsideButton) {
  			pageSelector.hide();
  			currentPageSelectorButton = null;
  		}
  	});
  
  	function setEmbeddedPageForCurrentItem(newPageId, newBlogId, title) {
  		if ( currentPageSelectorButton ) {
  			var containerNode = currentPageSelectorButton.closest('.ws_container'),
  				menuItem = containerNode.data('menu_item');
  
  			menuItem.embedded_page_id = newPageId;
  			menuItem.embedded_page_blog_id = newBlogId;
  
  			if (typeof title === 'string') {
  				//Store the page title for later. It will be displayed in the text box.
  				AmePageTitles.add(newPageId, newBlogId, title);
  			}
  
  			updateItemEditor(containerNode);
  		}
  	}
  
  	//When the user chooses a page from the list, update the menu item and hide the dropdown.
  	pageListBox.on('change', function() {
  		var selection = pageListBox.val();
  		if (selection === 'custom') { // jshint ignore:line
  			//Do nothing. Presumably, the user will now switch to the "Custom" tab and enter new settings.
  			//If they don't do that and just close the dropdown, we keep the previous settings.
  		} else if ( currentPageSelectorButton ) {
  			//Set the new page and blog IDs. The expected value format is "blogid_postid".
  			var parts = selection.split('_'),
  				newBlogId = parseInt(parts[0], 10),
  				newPageId = parseInt(parts[1], 10);
  
  			pageSelector.hide();
  			setEmbeddedPageForCurrentItem(newPageId, newBlogId, pageListBox.children(':selected').text());
  		}
  	});
  
  	pageSelector.find('#ws_custom_embedded_page_tab form').on('submit', function(event) {
  		event.preventDefault();
  
  		var newPageId = parseInt(pageSelector.find('#ws_embedded_page_id').val(), 10),
  			newBlogId = parseInt(pageSelector.find('#ws_embedded_page_blog_id').val(), 10);
  
  		if (isNaN(newPageId) || (newPageId < 0)) {
  			alert('Error: Invalid post ID');
  		} else if (isNaN(newBlogId) || (newBlogId < 0)) {
  			alert('Error: Invalid blog ID');
  		} else if ( currentPageSelectorButton ) {
  			pageSelector.hide();
  			setEmbeddedPageForCurrentItem(newPageId, newBlogId);
  		}
  	});
  
  
  	/*************************************************************************
  	                             Color picker
  	 *************************************************************************/
  
  	var menuColorDialog = $('#ws-ame-menu-color-settings');
  	if (menuColorDialog.length > 0) {
  		menuColorDialog.dialog({
  			autoOpen: false,
  			closeText: ' ',
  			draggable: false,
  			modal: true,
  			minHeight: 400,
  			minWidth: 520
  		});
  	}
  
  	var colorDialogState = {
  		menuItem: null,
  		editingGlobalColors: false
  	};
  
  	var menuColorVariables = [
  		'base-color',
  		'text-color',
  		'highlight-color',
  		'icon-color',
  
  		'menu-highlight-text',
  		'menu-highlight-icon',
  		'menu-highlight-background',
  
  		'menu-current-text',
  		'menu-current-icon',
  		'menu-current-background',
  
  		'menu-submenu-text',
  		'menu-submenu-background',
  		'menu-submenu-focus-text',
  		'menu-submenu-current-text',
  
  		'menu-bubble-text',
  		'menu-bubble-background',
  		'menu-bubble-current-text',
  		'menu-bubble-current-background'
  	];
  
  	var colorPresetDropdown = $('#ame-menu-color-presets'),
  		colorPresetDeleteButton = $("#ws-ame-delete-color-preset"),
  		areColorChangesIgnored = false;
  
  	//Show only the primary color settings by default.
  	var showAdvancedColors = false;
  	$('#ws-ame-show-advanced-colors').click(function() {
  		showAdvancedColors = !showAdvancedColors;
  		$('#ws-ame-menu-color-settings').find('.ame-advanced-menu-color').toggle(showAdvancedColors);
  		$(this).text(showAdvancedColors ? 'Hide advanced options' : 'Show advanced options');
  	});
  
  	var colorPickersInitialized = false;
  	function setUpColorDialog(dialogTitle) {
  		//Initializing the color pickers takes a while, so we only do it when needed instead of on document ready.
  		if ( !colorPickersInitialized ) {
  			menuColorDialog.find('.ame-color-picker').wpColorPicker({
  				//Deselect the current preset when the user changes any of the color options.
  				change: deselectPresetOnColorChange,
  				clear: deselectPresetOnColorChange
  			});
  			colorPickersInitialized = true;
  		}
  
  		//Populate presets and deselect the previously selected option.
  		colorPresetDropdown.val('');
  		if (!wasPresetDropdownPopulated) {
  			populatePresetDropdown();
  			wasPresetDropdownPopulated = true;
  		}
  
  		//Update the dialog title.
  		menuColorDialog.dialog('option', 'title', dialogTitle);
  	}
  
  	//"Edit.." color schemes.
  	menuEditorNode.on('click', '.ws_open_color_editor, .ws_color_scheme_display', function() {
  		var containerNode = $(this).parents('.ws_container').first();
  		var menuItem = containerNode.data('menu_item');
  
  		colorDialogState.containerNode = containerNode;
  		colorDialogState.menuItem = menuItem;
  		colorDialogState.editingGlobalColors = false;
  
  		//Add menu title to the dialog caption.
  		var title = getFieldValue(menuItem, 'menu_title', null);
  		setUpColorDialog(title ? ('Colors: ' + formatMenuTitle(title)) : 'Colors');
  
  		//Show the [global] preset only if the user has set it up.
  		var globalPresetExists = colorPresets.hasOwnProperty('[global]');
  		menuColorDialog.find('#ame-global-colors-preset').toggle(globalPresetExists);
  
  		var colors = getFieldValue(menuItem, 'colors', {}),
  			colorsToDisplay = colors || {};
  		if (_.isEmpty(colors)) {
  			//Normalization. No custom colors = use default colors, and null is used to indicate default settings.
  			menuItem.colors = null;
  			//If no custom colors, select and display the global preset.
  			if (globalPresetExists) {
  				colorsToDisplay = colorPresets['[global]'];
  				colorPresetDropdown.val('[global]');
  				colorPresetDeleteButton.hide();
  			}
  		}
  		displayColorSettingsInDialog(colorsToDisplay);
  
  		menuColorDialog.dialog('open');
  	});
  
  	//The "Colors" button in the main sidebar.
  	$('#ws_edit_global_colors').click(function() {
  		colorDialogState.editingGlobalColors = true;
  		colorDialogState.menuItem = null;
  		colorDialogState.containerNode = null;
  
  		setUpColorDialog('Default menu colors');
  		displayColorSettingsInDialog(_.get(colorPresets, '[global]', {}));
  
  		//Hide the [global] preset. We'll be editing it.
  		menuColorDialog.find('#ame-global-colors-preset').hide();
  
  		menuColorDialog.dialog('open');
  	});
  
  	function getColorSettingsFromDialog() {
  		var colors = {}, colorCount = 0;
  
  		for (var i = 0; i < menuColorVariables.length; i++) {
  			var name = menuColorVariables[i];
  			var value = $('#ame-color-' + name).val();
  			if (value) {
  				colors[name] = value;
  				colorCount++;
  			}
  		}
  
  		if (colorCount > 0) {
  			return colors;
  		} else {
  			return null;
  		}
  	}
  
  	function displayColorSettingsInDialog(colors) {
  		//noinspection JSUnusedAssignment
  		areColorChangesIgnored = true;
  		var customColorCount = 0;
  
  		for (var i = 0; i < menuColorVariables.length; i++) {
  			var name = menuColorVariables[i];
  			var value = colors.hasOwnProperty(name) ? colors[name] : false;
  
  			if ( value ) {
  				$('#ame-color-' + name).wpColorPicker('color', value);
  				customColorCount++;
  			} else {
  				$('#ame-color-' + name).closest('.wp-picker-container').find('.wp-picker-clear').click();
  			}
  		}
  
  		areColorChangesIgnored = false;
  		return customColorCount;
  	}
  
  	//The "Save Changes" button in the color dialog.
  	$('#ws-ame-save-menu-colors').click(function() {
  		menuColorDialog.dialog('close');
  		var colors = getColorSettingsFromDialog();
  
  		if ( colorDialogState.editingGlobalColors ) {
  			if (colors === null) {
  				delete colorPresets['[global]'];
  			} else {
  				colorPresets['[global]'] = colors;
  			}
  		} else if ( colorDialogState.menuItem ) {
  			var menuItem = colorDialogState.menuItem;
  			//If colors match the global settings, reset them to null. Using the [global] preset is the default.
  			if (_.has(colorPresets, '[global]') && _.isEqual(colors, colorPresets['[global]'])) {
  				menuItem.colors = null;
  			} else {
  				menuItem.colors = colors;
  			}
  			updateItemEditor(colorDialogState.containerNode);
  		}
  
  		colorDialogState.containerNode = null;
  		colorDialogState.menuItem = null;
  		colorDialogState.editingGlobalColors = false;
  	});
  
  	//The "Apply to All" button in the same dialog.
  	$('#ws-ame-apply-colors-to-all').click(function() {
  		if (!confirm('Apply these color settings to ALL top level menus?')) {
  			return;
  		}
  
  		//Set this as the global preset and remove custom settings from all items.
  		var newColors = getColorSettingsFromDialog();
  		if (newColors === null) {
  			delete colorPresets['[global]'];
  		} else {
  			colorPresets['[global]'] = newColors;
  		}
  		$('#ws_menu_box').find('.ws_menu').each(function() {
  			var containerNode = $(this),
  				menuItem = containerNode.data('menu_item');
  			if (!menuItem.separator) {
  				menuItem.colors = null;
  				updateItemEditor(containerNode);
  			}
  		});
  
  		menuColorDialog.dialog('close');
  		colorDialogState.containerNode = null;
  		colorDialogState.menuItem = null;
  	});
  
  	function addColorPreset(name, colors) {
  		colorPresets[name] = colors;
  		populatePresetDropdown();
  		colorPresetDropdown.val(name);
  		colorPresetDeleteButton.removeClass('hidden');
  	}
  
  	function deleteColorPreset(name) {
  		delete colorPresets[name];
  		populatePresetDropdown();
  		colorPresetDropdown.val('');
  		colorPresetDeleteButton.addClass('hidden');
  	}
  
  	function populatePresetDropdown() {
  		var separator = colorPresetDropdown.find('#ame-color-preset-separator');
  
  		//Delete the old options, but keep the "save preset" option and so on.
  		colorPresetDropdown.find('option').not('.ame-meta-option').remove();
  
  		//Sort presets alphabetically.
  		var presetNames = $.map(colorPresets, function(unused, name) {
  			return name;
  		}).sort(function(a, b) {
  			return a.localeCompare(b);
  		});
  
  		//Add them all to the dropdown.
  		var newOptions = jQuery([]);
  		$.each(presetNames, function(unused, name) {
  			if (name === '[global]') {
  				return;
  			}
  
  			newOptions = newOptions.add($('<option>', {
  				val: name,
  				text: name
  			}));
  		});
  		newOptions.insertBefore(separator);
  	}
  
  	function deselectPresetOnColorChange() {
  		//Most jQuery widgets don't trigger change events when you update them via JavaScript,
  		//but apparently wpColorPicker does. We want to ignore those superfluous events.
  		if (!areColorChangesIgnored && (colorPresetDropdown.val() !== '')) {
  			colorPresetDropdown.val('');
  		}
  	}
  
  	colorPresetDropdown.change(function() {
  		var dropdown = $(this),
  			presetName = dropdown.val();
  
  		colorPresetDeleteButton.toggleClass('hidden', _.includes(['[save_preset]', '[global]', '', null], presetName));
  
  		if ((presetName === '[save_preset]') && menuColorDialog.dialog('isOpen')) {
  			//Create a new preset.
  			var colors = getColorSettingsFromDialog();
  			if (colors === null) {
  				dropdown.val('');
  				alert('Error: No colors selected');
  				return;
  			}
  
  			var newPresetName = window.prompt('New preset name:', '');
  			if ((newPresetName === null) || (jsTrim(newPresetName) === '')) {
  				dropdown.val('');
  				return;
  			}
  
  			addColorPreset(newPresetName, colors);
  		} else if (presetName !== '') {
  			//Apply the selected preset.
  			var preset = colorPresets[presetName];
  			displayColorSettingsInDialog(preset);
  		}
  	});
  
  	colorPresetDeleteButton.click(function() {
  		var presetName = $('#ame-menu-color-presets').val();
  		if ( _.includes(['[save_preset]', '[global]', '', null], presetName) ) {
  			return false;
  		}
  		if (!confirm('Are you sure you want to delete the preset "' + presetName + '"?')) {
  			return false;
  		}
  
  		deleteColorPreset(presetName);
  		return false;
  	});
  
      /*************************************************************************
  	                           Menu toolbar buttons
  	 *************************************************************************/
      function getSelectedMenu() {
  	    return $('#ws_menu_box').find('.ws_active');
      }
  
  	//Show/Hide menu
  	$('#ws_hide_menu').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected menu
  		var selection = getSelectedMenu();
  		if (!selection.length) {
  			return;
  		}
  
  		toggleItemHiddenFlag(selection);
  	});
  
  	//Hide a menu and deny access.
  	menuEditorNode.find('.ws_toolbar').on('click', '.ws_hide_and_deny_button', function() {
  		var $box = $(this).closest('.ws_main_container').find('.ws_box'),
  			selection = $box.is('#ws_menu_box') ? getSelectedMenu() : getSelectedSubmenuItem();
  		if (selection.length < 1) {
  			return;
  		}
  
  		function objectFillKeys(keys, value) {
  			var result = {};
  			_.forEach(keys, function(key) {
  				result[key] = value;
  			});
  			return result;
  		}
  
  		if (actorSelectorWidget.selectedActor === null) {
  			//Hide from everyone except Super Admin and the current user.
  			var menuItem = selection.data('menu_item'),
  				validActors = _.keys(wsEditorData.actors),
  				alwaysAllowedActors = _.intersection(
  					['special:super_admin', 'user:' + wsEditorData.currentUserLogin],
  					validActors
  				),
  				victims = _.difference(validActors, alwaysAllowedActors),
  				shouldHide;
  
  			//First, lets check who has access. Maybe this item is already hidden from the victims.
  			shouldHide = _.some(victims, _.curry(actorCanAccessMenu, 2)(menuItem));
  
  			var keepEnabled = objectFillKeys(alwaysAllowedActors, true),
  				hideAllExceptAllowed = _.assign(objectFillKeys(victims, false), keepEnabled);
  
  			walkMenuTree(selection, function(container, item) {
  				var newAccess;
  				if (shouldHide) {
  					//Yay, hide it now!
  					newAccess = hideAllExceptAllowed;
  					//Only update had_access_before_hiding if this item isn't hidden yet or the field is missing.
  					//We don't want to double-hide an item.
  					var actorsWithAccess = _.filter(victims, function(actor) {
  						return actorCanAccessMenu(item, actor);
  					});
  					if ((actorsWithAccess.length) > 0 || _.isEmpty(_.get(item, 'had_access_before_hiding', null))) {
  						item.had_access_before_hiding = actorsWithAccess;
  					}
  				} else {
  					//Give back access to the roles and users who previously had access.
  					//Careful, don't give access to roles that no longer exist.
  					var actorsWhoHadAccess = _.get(item, 'had_access_before_hiding', []) || [];
  					actorsWhoHadAccess = _.intersection(actorsWhoHadAccess, validActors);
  
  					newAccess = _.assign(objectFillKeys(actorsWhoHadAccess, true), keepEnabled);
  					delete item.had_access_before_hiding;
  				}
  
  				setActorAccess(container, newAccess);
  				updateItemEditor(container);
  			});
  
  		} else {
  			//Just toggle the checkbox.
  			selection.find('input.ws_actor_access_checkbox').click();
  		}
  	});
  
  	//Delete error dialog. It shows up when the user tries to delete one of the default menus.
  	var menuDeletionDialog = $('#ws-ame-menu-deletion-error').dialog({
  		autoOpen: false,
  		modal: true,
  		closeText: ' ',
  		title: 'Error',
  		draggable: false
  	});
  	var menuDeletionCallback = function(hide) {
  		menuDeletionDialog.dialog('close');
  		var selection = menuDeletionDialog.data('selected_menu');
  
  		function applyCallbackRecursively(containerNode, callback) {
  			callback(containerNode.data('menu_item'));
  
  			var subMenuId = containerNode.data('submenu_id');
  			if (subMenuId && containerNode.hasClass('ws_menu')) {
  				$('.ws_item', '#' + subMenuId).each(function() {
  					var node = $(this);
  					callback(node.data('menu_item'));
  					updateItemEditor(node);
  				});
  			}
  
  			updateItemEditor(containerNode);
  		}
  
  		function hideRecursively(containerNode, exceptActor) {
  			var otherActors = _(actorSelectorWidget.getVisibleActors())
  				.pluck('id')
  				.without(exceptActor)
  				.value();
  
  			applyCallbackRecursively(containerNode, function(menuItem) {
  				//Remember which actors had access to this item so that it
  				//can be un-hidden by the toolbar button.
  				var actorsWithAccess = _.filter(otherActors, function(actor) {
  					return actorCanAccessMenu(menuItem, actor);
  				});
  				if ((actorsWithAccess.length) > 0) {
  					menuItem.had_access_before_hiding = actorsWithAccess;
  				}
  
  				denyAccessForAllExcept(menuItem, exceptActor);
  			});
  			updateParentAccessUi(containerNode);
  		}
  
  		//TODO: Write had_access_before_hiding so that it can be un-hidden using the toolbar button.
  		if (hide === 'all') {
  			if (wsEditorData.wsMenuEditorPro) {
  				hideRecursively(selection, null);
  			} else {
  				//The free version doesn't have role permissions, so use the global "hidden" flag.
  				applyCallbackRecursively(selection, function(menuItem) {
  					menuItem.hidden = true;
  				});
  			}
  		} else if (hide === 'except_current_user') {
  			hideRecursively(selection, 'user:' + wsEditorData.currentUserLogin);
  		} else if (hide === 'except_administrator' && !wsEditorData.wsMenuEditorPro) {
  			//Set "required capability" to something only the Administrator role would have.
  			var adminOnlyCap = 'manage_options';
  			applyCallbackRecursively(selection, function(menuItem) {
  				menuItem.extra_capability = adminOnlyCap;
  			});
  			alert('The "required capability" field was set to "' + adminOnlyCap + '".');
  		}
  	};
  
  	//Callbacks for each of the dialog buttons.
  	$('#ws_cancel_menu_deletion').click(function() {
  		menuDeletionCallback(false);
  	});
  	$('#ws_hide_menu_from_everyone').click(function() {
  		menuDeletionCallback('all');
  	});
  	$('#ws_hide_menu_except_current_user').click(function() {
  		menuDeletionCallback('except_current_user');
  	});
  	$('#ws_hide_menu_except_administrator').click(function() {
  		menuDeletionCallback('except_administrator');
  	});
  
  	/**
  	 * Check if it's possible to delete a menu item.
  	 *
  	 * @param {JQuery} containerNode
  	 * @returns {boolean}
  	 */
  	function canDeleteItem(containerNode) {
  		if (!containerNode || (containerNode.length < 1)) {
  			return false;
  		}
  
  		var menuItem = containerNode.data('menu_item');
  		var isDefaultItem =
  			( menuItem.template_id !== '')
  			&& ( menuItem.template_id !== wsEditorData.unclickableTemplateId)
  			&& ( menuItem.template_id !== wsEditorData.embeddedPageTemplateId)
  			&& (!menuItem.separator);
  
  		var otherCopiesExist = false;
  		if (isDefaultItem) {
  			//Check if there are any other menus with the same template ID.
  			$('#ws_menu_editor').find('.ws_container').each(function() {
  				var otherItem = $(this).data('menu_item');
  				if ((menuItem !== otherItem) && (menuItem.template_id === otherItem.template_id)) {
  					otherCopiesExist = true;
  					return false;
  				}
  				return true;
  			});
  		}
  
  		return (!isDefaultItem || otherCopiesExist);
  	}
  
  	/**
  	 * Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation.
  	 * UI callback.
  	 *
  	 * @param {JQuery} selection The selected menu item (DOM node).
  	 */
  	function tryDeleteItem(selection) {
  		var menuItem = selection.data('menu_item');
  		var shouldDelete = false;
  
  		if (canDeleteItem(selection)) {
  			//Custom and duplicate items can be deleted normally.
  			shouldDelete = confirm('Delete this menu?');
  		} else {
  			//Non-custom items can not be deleted, but they can be hidden. Ask the user if they want to do that.
  			menuDeletionDialog.find('#ws-ame-menu-type-desc').text(
  				getDefaultValue(menuItem, 'is_plugin_page') ? 'an item added by another plugin' : 'a built-in menu item'
  			);
  			menuDeletionDialog.data('selected_menu', selection);
  
  			//Different versions get slightly different options because only the Pro version has
  			//role-specific permissions.
  			$('#ws_hide_menu_except_current_user').toggleClass('hidden', !wsEditorData.wsMenuEditorPro);
  			$('#ws_hide_menu_except_administrator').toggleClass('hidden', wsEditorData.wsMenuEditorPro);
  
  			menuDeletionDialog.dialog('open');
  
  			//Select "Cancel" as the default button.
  			menuDeletionDialog.find('#ws_cancel_menu_deletion').focus();
  		}
  
  		if (shouldDelete) {
  			//Delete this menu's submenu first, if any.
  			var submenuId = selection.data('submenu_id');
  			if (submenuId) {
  				$('#' + submenuId).remove();
  			}
  			var parentSubmenu = selection.closest('.ws_submenu');
  
  			//Delete the menu.
  			selection.remove();
  
  			if (parentSubmenu) {
  				//Refresh permissions UI for this menu's parent (if any).
  				updateParentAccessUi(parentSubmenu);
  			}
  		}
  	}
  
  	//Delete menu
  	$('#ws_delete_menu').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected menu
  		var selection = getSelectedMenu();
  		if (!selection.length) {
  			return;
  		}
  
  		tryDeleteItem(selection);
  	});
  
  	//Copy menu
  	$('#ws_copy_menu').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected menu
  		var selection = $('#ws_menu_box').find('.ws_active');
  		if (!selection.length) {
  			return;
  		}
  
  		//Store a copy of the current menu state in clipboard
  		menu_in_clipboard = readItemState(selection);
  	});
  
  	//Cut menu
  	$('#ws_cut_menu').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected menu
  		var selection = $('#ws_menu_box').find('.ws_active');
  		if (!selection.length) {
  			return;
  		}
  
  		//Store a copy of the current menu state in clipboard
  		menu_in_clipboard = readItemState(selection);
  
  		//Remove the original menu and submenu
  		$('#'+selection.data('submenu_id')).remove();
  		selection.remove();
  	});
  
  	//Paste menu
  	function pasteMenu(menu, afterMenu) {
  		//The user shouldn't need to worry about giving separators a unique filename.
  		if (menu.separator) {
  			menu.defaults.file = randomMenuId('separator_');
  		}
  
  		//If we're pasting from a sub-menu, we may need to fix some properties
  		//that are blank for sub-menu items but required for top-level menus.
  		if (getFieldValue(menu, 'css_class', '') == '') {
  			menu.css_class = 'menu-top';
  		}
  		if (getFieldValue(menu, 'icon_url', '') == '') {
  			menu.icon_url = 'dashicons-admin-generic';
  		}
  		if (getFieldValue(menu, 'hookname', '') == '') {
  			menu.hookname = randomMenuId();
  		}
  
  		//Paste the menu after the specified one, or at the end of the list.
  		if (afterMenu) {
  			return outputTopMenu(menu, afterMenu);
  		} else {
  			return outputTopMenu(menu);
  		}
  	}
  
  	$('#ws_paste_menu').click(function (event) {
  		event.preventDefault();
  
  		//Check if anything has been copied/cut
  		if (!menu_in_clipboard) {
  			return;
  		}
  
  		var menu = $.extend(true, {}, menu_in_clipboard);
  
  		//Get the selected menu
  		var selection = $('#ws_menu_box').find('.ws_active');
  		//Paste the menu after the selection.
  		pasteMenu(menu, (selection.length > 0) ? selection : null);
  	});
  
  	//New menu
  	$('#ws_new_menu').click(function (event) {
  		event.preventDefault();
  
  		ws_paste_count++;
  
  		//The new menu starts out rather bare
  		var randomId = randomMenuId();
  		var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
  			custom: true, //Important : flag the new menu as custom, or it won't show up after saving.
  			template_id : '',
  			menu_title : 'Custom Menu ' + ws_paste_count,
  			file : randomId,
  			items: []
  		});
  		menu.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
  
  		//Make it accessible only to the current actor if one is selected.
  		if (actorSelectorWidget.selectedActor !== null) {
  			denyAccessForAllExcept(menu, actorSelectorWidget.selectedActor);
  		}
  
  		//Insert the new menu
  		var selection = $('#ws_menu_box').find('.ws_active');
  		var result = outputTopMenu(menu, (selection.length > 0) ? selection : null);
  
  		//The menus's editbox is always open
  		result.menu.find('.ws_edit_link').click();
  	});
  
  	//New separator
  	$('#ws_new_separator, #ws_new_submenu_separator').click(function (event) {
  		event.preventDefault();
  
  		ws_paste_count++;
  
  		//The new menu starts out rather bare
  		var randomId = randomMenuId('separator_');
  		var menu = $.extend(true, {}, wsEditorData.blankMenuItem, {
  			separator: true, //Flag as a separator
  			custom: false,   //Separators don't need to flagged as custom to be retained.
  			items: [],
  			defaults: {
  				separator: true,
  				css_class : 'wp-menu-separator',
  				access_level : 'read',
  				file : randomId,
  				hookname : randomId
  			}
  		});
  
  		if ( $(this).attr('id').indexOf('submenu') === -1 ) {
  			//Insert in the top-level menu.
  			var selection = $('#ws_menu_box').find('.ws_active');
  			outputTopMenu(menu, (selection.length > 0) ? selection : null);
  		} else {
  			//Insert in the currently visible submenu.
  			pasteItem(menu);
  		}
  	});
  
  	//Toggle all menus for the currently selected actor
  	$('#ws_toggle_all_menus').click(function(event) {
  		event.preventDefault();
  
  		if ( actorSelectorWidget.selectedActor === null ) {
  			alert("This button enables/disables all menus for the selected role. To use it, click a role and then click this button again.");
  			return;
  		}
  
  		var topMenuNodes = $('.ws_menu', '#ws_menu_box');
  		//Look at the first menu's permissions and set everything to the opposite.
  		var allow = ! actorCanAccessMenu(topMenuNodes.eq(0).data('menu_item'), actorSelectorWidget.selectedActor);
  
  		topMenuNodes.each(function() {
  			var containerNode = $(this);
  			setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, allow);
  		});
  	});
  
  	//Copy all menu permissions from one role to another.
  	var copyPermissionsDialog = $('#ws-ame-copy-permissions-dialog').dialog({
  		autoOpen: false,
  		modal: true,
  		closeText: ' ',
  		draggable: false
  	});
  
  	var sourceActorList = $('#ame-copy-source-actor'), destinationActorList = $('#ame-copy-destination-actor');
  
  	//The "Copy permissions" toolbar button.
  	$('#ws_copy_role_permissions').click(function(event) {
  		event.preventDefault();
  
  		var previousSource = sourceActorList.val();
  
  		//Populate source/destination lists.
  		sourceActorList.find('option').not('[disabled]').remove();
  		destinationActorList.find('option').not('[disabled]').remove();
  		$.each(actorSelectorWidget.getVisibleActors(), function(index, actor) {
  			var option = $('<option>', {
  				val: actor.id,
  				text: actorSelectorWidget.getNiceName(actor)
  			});
  			sourceActorList.append(option);
  			destinationActorList.append(option.clone());
  		});
  
  		//Pre-select the current actor as the destination.
  		if (actorSelectorWidget.selectedActor !== null) {
  			destinationActorList.val(actorSelectorWidget.selectedActor);
  		}
  
  		//Restore the previous source selection.
  		if (previousSource) {
  			sourceActorList.val(previousSource);
  		}
  		if (!sourceActorList.val()) {
  			sourceActorList.find('option').first().prop('selected', true); //Fallback.
  		}
  
  		copyPermissionsDialog.dialog('open');
  	});
  
  	//Actually copy the permissions when the user click the confirmation button.
  	var copyConfirmationButton = $('#ws-ame-confirm-copy-permissions');
  	copyConfirmationButton.click(function() {
  		var sourceActor = sourceActorList.val();
  		var destinationActor = destinationActorList.val();
  
  		if (sourceActor === null || destinationActor === null) {
  			alert('Select a source and a destination first.');
  			return;
  		}
  
  		//Iterate over all menu items and copy the permissions from one actor to the other.
  		var allMenuNodes = $('.ws_menu', '#ws_menu_box').add('.ws_item', submenuBox);
  		allMenuNodes.each(function() {
  			var node = $(this);
  			var menuItem = node.data('menu_item');
  
  			//Only change permissions when they don't match. This ensures we won't unnecessarily overwrite default
  			//permissions and bloat the configuration with extra grant_access entries.
  			var sourceAccess      = actorCanAccessMenu(menuItem, sourceActor);
  			var destinationAccess = actorCanAccessMenu(menuItem, destinationActor);
  			if (sourceAccess !== destinationAccess) {
  				setActorAccess(node, destinationActor, sourceAccess);
  				//Note: In theory, we could also look at the default permissions for destinationActor and
  				//revert to default instead of overwriting if that would make the two actors' permissions match.
  			}
  		});
  
  		//todo: copy granted permissions like CPTs.
  
  		//If the user is currently looking at the destination actor, force the UI to refresh
  		//so that they can see the new permissions.
  		if (actorSelectorWidget.selectedActor === destinationActor) {
  			//This is a bit of a hack, but right now there's no better way to refresh all items at once.
  			actorSelectorWidget.setSelectedActor(null);
  			actorSelectorWidget.setSelectedActor(destinationActor);
  		}
  
  		//All done.
  		copyPermissionsDialog.dialog('close');
  	});
  
  	//Only enable the copy button when the user selects a valid source and destination.
  	copyConfirmationButton.prop('disabled', true);
  	sourceActorList.add(destinationActorList).click(function() {
  		var sourceActor = sourceActorList.val();
  		var destinationActor = destinationActorList.val();
  
  		var validInputs = (sourceActor !== null) && (destinationActor !== null) && (sourceActor !== destinationActor);
  		copyConfirmationButton.prop('disabled', !validInputs);
  	});
  
  	//Sort menus in ascending or descending order.
  	menuEditorNode.find('.ws_toolbar').on('click', '.ws_sort_menus_button', function(event) {
  		event.preventDefault();
  
  		var button = $(this),
  			direction = button.data('sort-direction') || 'asc',
  			menuBox = $(this).closest('.ws_main_container').find('.ws_box').first();
  
  		if (menuBox.is('#ws_submenu_box')) {
  			menuBox = menuBox.find('.ws_submenu:visible').first();
  		}
  
  		if (menuBox.length > 0) {
  			sortMenuItems(menuBox, direction);
  		}
  
  		//When sorting the top level menu also sort submenus, but leave the first item unmoved.
  		//Moving the first item would change the parent menu URL (WP always links it to the first item),
  		//which can be unexpected and confusing. The user can always move the first item manually.
  		if (menuBox.is('#ws_menu_box')) {
  			$('#ws_submenu_box').find('.ws_submenu').each(function() {
  				sortMenuItems($(this), direction, true);
  			});
  		}
  	});
  
  	/**
  	 * Sort menu items by title.
  	 *
  	 * @param $menuBox A DOM node that contains multiple menu items.
  	 * @param {string} direction 'asc' or 'desc'
  	 * @param {boolean} [leaveFirstItem] Leave the first item in its original position. Defaults to false.
  	 */
  	function sortMenuItems($menuBox, direction, leaveFirstItem) {
  		var multiplier = (direction === 'desc') ? -1 : 1,
  			items = $menuBox.find('.ws_container'),
  			firstItem = items.first();
  
  		//Separators don't have a title, but we don't want them to end up at the top of the list.
  		//Instead, lets keep their position the same relative to the previous item.
  		var prevItemTitle = '';
  		items.each((function(){
  			var item = $(this), sortValue;
  			if (item.is('.ws_menu_separator')) {
  				sortValue = prevItemTitle;
  			} else {
  				sortValue = jsTrim(item.find('.ws_item_title').text());
  				prevItemTitle = sortValue;
  			}
  			item.data('ame-sort-value', sortValue);
  		}));
  
  		function compareMenus(a, b){
  			var aTitle = $(a).data('ame-sort-value'),
  				bTitle = $(b).data('ame-sort-value');
  
  			aTitle = aTitle.toLowerCase();
  			bTitle = bTitle.toLowerCase();
  
  			if (aTitle > bTitle) {
  				return multiplier;
  			} else if (aTitle < bTitle) {
  				return -multiplier;
  			}
  			return 0;
  		}
  
  		items.sort(compareMenus);
  
  		if (leaveFirstItem) {
  			//Move the first item back to the top.
  			firstItem.prependTo($menuBox);
  		}
  	}
  
  	//Toggle the second row of toolbar buttons.
  	$('#ws_toggle_toolbar').click(function() {
  		var visible = menuEditorNode.find('.ws_second_toolbar_row').toggle().is(':visible');
  		$.cookie('ame-show-second-toolbar', visible ? '1' : '0', {expires: 90});
  	});
  
  
  	/*************************************************************************
  	                          Item toolbar buttons
  	 *************************************************************************/
  	function getSelectedSubmenuItem() {
  		return $('#ws_submenu_box').find('.ws_submenu:visible .ws_active');
  	}
  
  	//Show/Hide item
  	$('#ws_hide_item').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected item
  		var selection = getSelectedSubmenuItem();
  		if (!selection.length) {
  			return;
  		}
  
  		//Mark the item as hidden/visible
  		toggleItemHiddenFlag(selection);
  	});
  
  	//Delete item
  	$('#ws_delete_item').click(function (event) {
  		event.preventDefault();
  
  		var selection = getSelectedSubmenuItem();
  		if (!selection.length) {
  			return;
  		}
  
  		tryDeleteItem(selection);
  	});
  
  	//Copy item
  	$('#ws_copy_item').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected item
  		var selection = getSelectedSubmenuItem();
  		if (!selection.length) {
  			return;
  		}
  
  		//Store a copy of item state in the clipboard
  		menu_in_clipboard = readItemState(selection);
  	});
  
  	//Cut item
  	$('#ws_cut_item').click(function (event) {
  		event.preventDefault();
  
  		//Get the selected item
  		var selection = getSelectedSubmenuItem();
  		if (!selection.length) {
  			return;
  		}
  
  		//Store a copy of item state in the clipboard
  		menu_in_clipboard = readItemState(selection);
  
  		var submenu = selection.parent();
  		//Remove the original item
  		selection.remove();
  		updateParentAccessUi(submenu);
  	});
  
  	//Paste item
  	function pasteItem(item, targetSubmenu) {
  		//We're pasting this item into a sub-menu, so it can't have a sub-menu of its own.
  		//Instead, any sub-menu items belonging to this item will be pasted after the item.
  		var newItems = [];
  		for (var file in item.items) {
  			if (item.items.hasOwnProperty(file)) {
  				newItems.push(buildMenuItem(item.items[file], false));
  			}
  		}
  		item.items = [];
  
  		newItems.unshift(buildMenuItem(item, false));
  
  		//Paste into the currently visible submenu by default.
  		targetSubmenu = targetSubmenu || $('#ws_submenu_box').find('.ws_submenu:visible');
  		//Get the selected menu
  		var selection = targetSubmenu.find('.ws_active');
  		for(var i = 0; i < newItems.length; i++) {
  			if (selection.length > 0) {
  				//If an item is selected add the pasted items after it
  				selection.after(newItems[i]);
  			} else {
  				//Otherwise add the pasted items at the end
  				targetSubmenu.append(newItems[i]);
  			}
  
  			updateItemEditor(newItems[i]);
  			newItems[i].show();
  		}
  
  		updateParentAccessUi(targetSubmenu);
  	}
  
  	$('#ws_paste_item').click(function (event) {
  		event.preventDefault();
  
  		//Check if anything has been copied/cut
  		if (!menu_in_clipboard) {
  			return;
  		}
  
  		//You can only add separators to submenus in the Pro version.
  		if ( menu_in_clipboard.separator && !wsEditorData.wsMenuEditorPro ) {
  			return;
  		}
  
  		//Paste it.
  		var item = $.extend(true, {}, menu_in_clipboard);
  		pasteItem(item);
  	});
  
  	//New item
  	$('#ws_new_item').click(function (event) {
  		event.preventDefault();
  
  		if ($('.ws_submenu:visible').length < 1) {
  			return; //Abort if no submenu visible
  		}
  
  		ws_paste_count++;
  
  		var entry = $.extend(true, {}, wsEditorData.blankMenuItem, {
  			custom: true,
  			template_id : '',
  			menu_title : 'Custom Item ' + ws_paste_count,
  			file : randomMenuId(),
  			items: []
  		});
  		entry.defaults = $.extend(true, {}, itemTemplates.getDefaults(''));
  
  		//Make it accessible to only the currently selected actor.
  		if (actorSelectorWidget.selectedActor !== null) {
  			denyAccessForAllExcept(entry, actorSelectorWidget.selectedActor);
  		}
  
  		var menu = buildMenuItem(entry);
  
  		//Insert the item into the currently open submenu.
  		var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
  		var selection = visibleSubmenu.find('.ws_active');
  		if (selection.length > 0) {
  			selection.after(menu);
  		} else {
  			visibleSubmenu.append(menu);
  		}
  		updateItemEditor(menu);
  
  		//The items's editbox is always open
  		menu.find('.ws_edit_link').click();
  
  		updateParentAccessUi(menu);
  	});
  
  	//==============================================
  	//				Main buttons
  	//==============================================
  
  	//Save Changes - encode the current menu as JSON and save
  	$('#ws_save_menu').click(function () {
  		try {
  			var tree = readMenuTreeState();
  		} catch (error) {
  			//Right now the only known error condition is duplicate top level URLs.
  			if (error.hasOwnProperty('code') && (error.code === 'duplicate_top_level_url')) {
  				var message = 'Error: Duplicate menu URLs. The following top level menus have the same URL:\n\n' ;
  				for (var i = 0; i < error.duplicates.length; i++) {
  					var containerNode = $(error.duplicates[i]);
  					message += (i + 1) + '. ' + containerNode.find('.ws_item_title').first().text() + '\n';
  				}
  				message += '\nPlease change the URLs to be unique or delete the duplicates.';
  				alert(message);
  			} else {
  				alert(error.message);
  			}
  			return;
  		}
  
  		function findItemByTemplateId(items, templateId) {
  			var foundItem = null;
  
  			$.each(items, function(index, item) {
  				if (item.template_id === templateId) {
  					foundItem = item;
  					return false;
  				}
  				if (item.hasOwnProperty('items') && (item.items.length > 0)) {
  					foundItem = findItemByTemplateId(item.items, templateId);
  					if (foundItem !== null) {
  						return false;
  					}
  				}
  				return true;
  			});
  
  			return foundItem;
  		}
  
  		//Abort the save if it would make the editor inaccessible.
          if (wsEditorData.wsMenuEditorPro) {
              var myMenuItem = findItemByTemplateId(tree.tree, 'options-general.php>menu_editor');
              if (myMenuItem === null) { // jshint ignore:line
                  //This is OK - the missing menu item will be re-inserted automatically.
              } else if (!actorCanAccessMenu(myMenuItem, 'user:' + wsEditorData.currentUserLogin)) {
                  alert(
  	                "Error: This configuration would make you unable to access the menu editor!\n\n" +
  	                "Please click either your role name or \"Current user (" + wsEditorData.currentUserLogin + ")\" "+
  	                "and enable the \"Menu Editor Pro\" menu item."
                  );
                  return;
              }
          }
  
  		var data = encodeMenuAsJSON(tree);
  		$('#ws_data').val(data);
  		$('#ws_data_length').val(data.length);
  		$('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor);
  
  		var selectedMenu = getSelectedMenu();
  		if (selectedMenu.length > 0) {
  			$('#ws_selected_menu_url').val(AmeEditorApi.getItemDisplayUrl(selectedMenu.data('menu_item')));
  			$('#ws_expand_selected_menu').val(selectedMenu.find('.ws_editbox').is(':visible') ? '1' : '');
  
  			var selectedSubmenu = getSelectedSubmenuItem();
  			if (selectedSubmenu.length > 0) {
  				$('#ws_selected_submenu_url').val(AmeEditorApi.getItemDisplayUrl(selectedSubmenu.data('menu_item')));
  				$('#ws_expand_selected_submenu').val(selectedSubmenu.find('.ws_editbox').is(':visible') ? '1' : '');
  			}
  		}
  
  		$('#ws_main_form').submit();
  	});
  
  	//Load default menu - load the default WordPress menu
  	$('#ws_load_menu').click(function () {
  		if (confirm('Are you sure you want to load the default WordPress menu?')){
  			loadMenuConfiguration(defaultMenu);
  		}
  	});
  
  	//Reset menu - re-load the custom menu. Discards any changes made by user.
  	$('#ws_reset_menu').click(function () {
  		if (confirm('Undo all changes made in the current editing session?')){
  			loadMenuConfiguration(customMenu);
  		}
  	});
  
  	//Enable the "load default menu" and "undo changes" buttons only when "All" is selected.
  	//Otherwise some users incorrectly assume these buttons only affect the currently selected role or user.
  	actorSelectorWidget.onChange(function (newSelectedActor) {
  		$('#ws_load_menu, #ws_reset_menu').prop('disabled', newSelectedActor !== null);
  	});
  	$('#ws_load_menu, #ws_reset_menu').prop('disabled', actorSelectorWidget.selectedActor !== null);
  
  	$('#ws_toggle_editor_layout').click(function () {
  		var isCompactLayoutEnabled = menuEditorNode.toggleClass('ws_compact_layout').hasClass('ws_compact_layout');
  		$.cookie('ame-compact-layout', isCompactLayoutEnabled ? '1' : '0', {expires: 90});
  
  		var button = $(this);
  		if (button.is('input')) {
  			var checkMark = '\u2713';
  			button.val(button.val().replace(checkMark, ''));
  			if (isCompactLayoutEnabled) {
  				button.val(checkMark + ' ' + button.val());
  			}
  		}
  	});
  
  	//Export menu - download the current menu as a file
  	$('#export_dialog').dialog({
  		autoOpen: false,
  		closeText: ' ',
  		modal: true,
  		minHeight: 100
  	});
  
  	$('#ws_export_menu').click(function(){
  		var button = $(this);
  		button.prop('disabled', true);
  		button.val('Exporting...');
  
  		$('#export_complete_notice, #download_menu_button').hide();
  		$('#export_progress_notice').show();
  		var exportDialog = $('#export_dialog');
  		exportDialog.dialog('open');
  
  		//Encode the menu.
  		try {
  			var exportData = encodeMenuAsJSON();
  		} catch (error) {
  			exportDialog.dialog('close');
  			alert(error.message);
  
  			button.val('Export');
  			button.prop('disabled', false);
  			return;
  		}
  
  		//Store the menu for download.
  		$.post(
  			wsEditorData.adminAjaxUrl,
  			{
  				'data' : exportData,
  				'action' : 'export_custom_menu',
  				'_ajax_nonce' : wsEditorData.exportMenuNonce
  			},
  			/**
  			 * @param {Object} data
  			 */
  			function(data){
  				button.val('Export');
  				button.prop('disabled', false);
  
  				if ( typeof data.error !== 'undefined' ){
  					exportDialog.dialog('close');
  					alert(data.error);
  				}
  
  				if ( _.has(data, 'download_url') ){
  					//window.location = data.download_url;
  					$('#download_menu_button').attr('href', _.get(data, 'download_url')).data('filesize', _.get(data, 'filesize'));
  					$('#export_progress_notice').hide();
  					$('#export_complete_notice, #download_menu_button').show();
  				}
  			},
  			'json'
  		);
  	});
  
  	$('#ws_cancel_export').click(function(){
  		$('#export_dialog').dialog('close');
  	});
  
  	$('#download_menu_button').click(function(){
  		$('#export_dialog').dialog('close');
  	});
  
  	//Import menu - upload an exported menu and show it in the editor
  	$('#import_dialog').dialog({
  		autoOpen: false,
  		closeText: ' ',
  		modal: true
  	});
  
  	$('#ws_cancel_import').click(function(){
  		$('#import_dialog').dialog('close');
  	});
  
  	$('#ws_import_menu').click(function(){
  		$('#import_progress_notice, #import_progress_notice2, #import_complete_notice, #ws_import_error').hide();
  		$('#ws_import_panel').show();
  		$('#import_menu_form').resetForm();
  		//The "Upload" button is disabled until the user selects a file
  		$('#ws_start_import').attr('disabled', 'disabled');
  
  		var importDialog = $('#import_dialog');
  		importDialog.find('.hide-when-uploading').show();
  		importDialog.dialog('open');
  	});
  
  	$('#import_file_selector').change(function(){
  		$('#ws_start_import').prop('disabled', ! $(this).val() );
  	});
  
  	//This function displays unhandled server side errors. In theory, our upload handler always returns a well-formed
  	//response even if there's an error. In practice, stuff can go wrong in unexpected ways (e.g. plugin conflicts).
  	function handleUnexpectedImportError(xhr, errorMessage) {
  		//The server-side code didn't catch this error, so it's probably something serious
  		//and retrying won't work.
  		$('#import_menu_form').resetForm();
  		$('#ws_import_panel').hide();
  
  		//Display error information.
  		$('#ws_import_error_message').text(errorMessage);
  		$('#ws_import_error_http_code').text(xhr.status);
  		$('#ws_import_error_response').text((xhr.responseText !== '') ? xhr.responseText : '[Empty response]');
  		$('#ws_import_error').show();
  	}
  
  	//AJAXify the upload form
  	$('#import_menu_form').ajaxForm({
  		dataType : 'json',
  		beforeSubmit: function(formData) {
  
  			//Check if the user has selected a file
  			for(var i = 0; i < formData.length; i++){
  				if ( formData[i].name === 'menu' ){
  					if ( (typeof formData[i].value === 'undefined') || !formData[i].value){
  						alert('Select a file first!');
  						return false;
  					}
  				}
  			}
  
  			$('#import_dialog').find('.hide-when-uploading').hide();
  			$('#import_progress_notice').show();
  
  			$('#ws_start_import').attr('disabled', 'disabled');
  			return true;
  		},
  		success: function(data, status, xhr) {
  			$('#import_progress_notice').hide();
  
  			var importDialog = $('#import_dialog');
  			if ( !importDialog.dialog('isOpen') ){
  				//Whoops, the user closed the dialog while the upload was in progress.
  				//Discard the response silently.
  				return;
  			}
  
  			if ( data === null ) {
  				handleUnexpectedImportError(xhr, 'Invalid response from server. Please check your PHP error log.');
  				return;
  			}
  
  			if ( typeof data.error !== 'undefined' ){
  				alert(data.error);
  				//Let the user try again
  				$('#import_menu_form').resetForm();
  				importDialog.find('.hide-when-uploading').show();
  			}
  
  			if ( (typeof data.tree !== 'undefined') && data.tree ){
  				//Whee, we got back a (seemingly) valid menu. A veritable miracle!
  				//Lets load it into the editor.
  				var progressNotice = $('#import_progress_notice2').show();
  				loadMenuConfiguration(data);
  				progressNotice.hide();
  				//Display a success notice, then automatically close the window after a few moments
  				$('#import_complete_notice').show();
  				setTimeout((function(){
  					//Close the import dialog
  					$('#import_dialog').dialog('close');
  				}), 500);
  			}
  
  		},
  		error: function(xhr, status, errorMessage) {
  			handleUnexpectedImportError(xhr, errorMessage);
  		}
  	});
  
  	/*************************************************************************
  	                 Drag & drop items between menu levels
  	 *************************************************************************/
  
  	if (wsEditorData.wsMenuEditorPro) {
  		//Allow the user to drag sub-menu items to the top level.
  		$('#ws_top_menu_dropzone').droppable({
  			'hoverClass' : 'ws_dropzone_hover',
  			'activeClass' : 'ws_dropzone_active',
  
  			'accept' : (function(thing){
  				return thing.hasClass('ws_item');
  			}),
  
  			'drop' : (function(event, ui){
  				var droppedItemData = readItemState(ui.draggable);
  				var newItemNodes = pasteMenu(droppedItemData);
  
  				//If the item was originally a top level menu, also move its original submenu items.
  				if (getFieldValue(droppedItemData, 'parent') === null) {
  					var droppedItemFile = getFieldValue(droppedItemData, 'file');
  					var nearbyItems = $(ui.draggable).siblings('.ws_item');
  					nearbyItems.each(function() {
  						var containerNode = $(this),
  							submenuItem = containerNode.data('menu_item');
  
  						//Was this item originally a child of the dragged menu?
  						if (getFieldValue(submenuItem, 'parent') === droppedItemFile) {
  							pasteItem(submenuItem, newItemNodes.submenu);
  							if ( !event.ctrlKey ) {
  								containerNode.remove();
  							}
  						}
  					});
  				}
  
  				if ( !event.ctrlKey ) {
  					ui.draggable.remove();
  				}
  			})
  		});
  
  		//...and to drag top level menus to a sub-menu.
  		submenuBox.closest('.ws_main_container').droppable({
  			'hoverClass' : 'ws_top_to_submenu_drop_hover',
  
  			'accept' : (function(thing){
  				var visibleSubmenu = $('#ws_submenu_box').find('.ws_submenu:visible');
  				return (
  					//Accept top-level menus
  					thing.hasClass('ws_menu') &&
  
  					//Prevent users from dropping a menu on its own sub-menu.
  					(visibleSubmenu.attr('id') !== thing.data('submenu_id'))
  				);
  			}),
  
  			'drop' : (function(event, ui){
  				var droppedItemData = readItemState(ui.draggable);
  				pasteItem(droppedItemData);
  				if ( !event.ctrlKey ) {
  					ui.draggable.remove();
  				}
  			})
  		});
  	}
  
  	/******************************************************************
  	                 Component visibility settings
  	 ******************************************************************/
  
  	var $generalVisBox = $('#ws_ame_general_vis_box'),
  		$showAdminMenu = $('#ws_ame_show_admin_menu'),
  		$showWpToolbar = $('#ws_ame_show_toolbar');
  
  	AmeEditorApi.actorCanSeeComponent = function(component, actorId) {
  		if (actorId === null) {
  			return _.some(actorSelectorWidget.getVisibleActors(), function(actor) {
  				return AmeEditorApi.actorCanSeeComponent(component, actor.id);
  			});
  		}
  
  		var actorSpecificSetting = _.get(generalComponentVisibility, [component, actorId], null);
  		if (actorSpecificSetting !== null) {
  			return actorSpecificSetting;
  		}
  
  		//Super Admin can see everything by default.
  		if (actorId === AmeSuperAdmin.permanentActorId) {
  			return _.get(generalComponentVisibility, [component, AmeSuperAdmin.permanentActorId], true);
  		}
  
  		var actor = AmeActors.getActor(actorId);
  		if (actor instanceof AmeUser) {
  			var grants = _.get(generalComponentVisibility, component, {});
  
  			//Super Admin has priority.
  			if (actor.isSuperAdmin) {
  				return AmeEditorApi.actorCanSeeComponent(component, AmeSuperAdmin.permanentActorId);
  			}
  
  			//The user can see the admin menu/Toolbar if at least one of their roles can see it.
  			var result = null;
  			_.forEach(actor.roles, function(roleName) {
  				var allow = _.get(grants, 'role:' + roleName, true);
  				if (result === null) {
  					result = allow;
  				} else {
  					result = result || allow;
  				}
  			});
  
  			if (result !== null) {
  				return result;
  			}
  		}
  
  		//Everyone can see the admin menu and the Toolbar by default.
  		return true;
  	};
  
  	AmeEditorApi.refreshComponentVisibility = function() {
  		if ($generalVisBox.length < 1) {
  			return;
  		}
  
  		var actorId = actorSelectorWidget.selectedActor;
  		$showAdminMenu.prop('checked', AmeEditorApi.actorCanSeeComponent('adminMenu', actorId));
  		$showWpToolbar.prop('checked', AmeEditorApi.actorCanSeeComponent('toolbar', actorId));
  	};
  
  	AmeEditorApi.setComponentVisibility = function(section, actorId, enabled) {
  		if (actorId === null) {
  			_.forEach(actorSelectorWidget.getVisibleActors(), function(actor) {
  				_.set(generalComponentVisibility, [section, actor.id], enabled);
  			});
  		} else {
  			_.set(generalComponentVisibility, [section, actorId], enabled);
  		}
  	};
  
  	if ($generalVisBox.length > 0) {
  		$showAdminMenu.click(function() {
  			AmeEditorApi.setComponentVisibility(
  				'adminMenu',
  				actorSelectorWidget.selectedActor,
  				$(this).is(':checked')
  			);
  		});
  		$showWpToolbar.click(function () {
  			AmeEditorApi.setComponentVisibility(
  				'toolbar',
  				actorSelectorWidget.selectedActor,
  				$(this).is(':checked')
  			);
  		});
  
  		$generalVisBox.find('.handlediv').click(function() {
  			$generalVisBox.toggleClass('closed');
  			$.cookie(
  				'ame_vis_box_open',
  				($generalVisBox.hasClass('closed') ? '0' : '1'),
  				{ expires: 90 }
  			);
  		});
  
  		actorSelectorWidget.onChange(function() {
  			AmeEditorApi.refreshComponentVisibility();
  		});
  	}
  
  	/******************************************************************
  	                      Tooltips and hints
  	 ******************************************************************/
  
  
  	//Set up tooltips
  	$('.ws_tooltip_trigger').qtip({
  		style: {
  			classes: 'qtip qtip-rounded ws_tooltip_node'
  		},
  		hide: {
  			fixed: true,
  			delay: 300
  		}
  	});
  
  	//Set up menu field toltips.
  	menuEditorNode.on('mouseenter click', '.ws_edit_field .ws_field_tooltip_trigger', function(event) {
  		var $trigger = $(this),
  			fieldName = $trigger.closest('.ws_edit_field').data('field_name');
  
  		if (knownMenuFields[fieldName].tooltip === null) {
  			return;
  		}
  
  		var tooltipText = 'Invalid tooltip';
  		if (typeof knownMenuFields[fieldName].tooltip === 'string') {
  			tooltipText = knownMenuFields[fieldName].tooltip;
  		} else if (typeof knownMenuFields[fieldName].tooltip === 'function') {
  			tooltipText = function() {
  				var $theTrigger = $(this),
  					menuItem = $theTrigger.closest('.ws_container').data('menu_item');
  				return knownMenuFields[fieldName].tooltip(menuItem);
  			}
  		}
  
  		$trigger.qtip({
  			overwrite: false,
  			content: {
  				text: tooltipText
  			},
  			show: {
  				event: event.type,
  				ready: true //Show immediately.
  			},
  			style: {
  				classes: 'qtip qtip-rounded ws_tooltip_node'
  			},
  			hide: {
  				fixed: true,
  				delay: 300
  			},
  			position: {
  				my: 'bottom center',
  				at: 'top center'
  			}
  		}, event);
  	});
  
  	//Set up the "additional permissions are available" tooltips.
  	menuEditorNode.on('mouseenter click', '.ws_ext_permissions_indicator', function() {
  		var $indicator = $(this);
  		$indicator.qtip({
  			overwrite: false,
  			content: {
  				text: function() {
  					var indicator = $(this),
  						extPermissions = indicator.data('ext_permissions'),
  						text = 'Additional permission settings are available. Click "Edit..." to change them.',
  						heading = '';
  
  					if (extPermissions && extPermissions.hasOwnProperty('title')) {
  						heading = extPermissions.title;
  						if (extPermissions.hasOwnProperty('type')) {
  							heading = _.capitalize(_.startCase(extPermissions.type).toLowerCase()) + ': ' + heading;
  						}
  						text = '<strong>' + heading + '</strong><br>' + text;
  					}
  
  					return text;
  				}
  			},
  			show: {
  				ready: true //Show immediately.
  			},
  			style: {
  				classes: 'qtip qtip-rounded ws_tooltip_node'
  			},
  			hide: {
  				fixed: true,
  				delay: 300
  			},
  			position: {
  				my: 'bottom center',
  				at: 'top center'
  			}
  		});
  	});
  
  	//Flag closed hints as hidden by sending the appropriate AJAX request to the backend.
  	$('.ws_hint_close').click(function() {
  		var hint = $(this).parents('.ws_hint').first();
  		hint.hide();
  		wsEditorData.showHints[hint.attr('id')] = false;
  		$.post(
  			wsEditorData.adminAjaxUrl,
  			{
  				'action' : 'ws_ame_hide_hint',
  				'hint' : hint.attr('id')
  			}
  		);
  	});
  
  	//Expand/collapse the "How To" box.
  	var $howToBox = $("#ws_ame_how_to_box");
  	$howToBox.find(".handlediv").click(function() {
  		$howToBox.toggleClass('closed');
  		$.cookie(
  			'ame_how_to_box_open',
  			($howToBox.hasClass('closed') ? '0' : '1'),
  			{ expires: 180 }
  		);
  	});
  
  
  	/******************************************************************
  	                           Actor views
  	 ******************************************************************/
  
  	if (wsEditorData.wsMenuEditorPro) {
  		actorSelectorWidget.onChange(function() {
  			//There are some UI elements that can be visible or hidden depending on whether an actor is selected.
  			var editorNode = $('#ws_menu_editor');
  			editorNode.toggleClass('ws_is_actor_view', (actorSelectorWidget.selectedActor !== null));
  
  			//Update the menu item states to indicate whether they're accessible.
  			editorNode.find('.ws_container').each(function() {
  				updateActorAccessUi($(this));
  			});
  		});
  
  		if (wsEditorData.hasOwnProperty('selectedActor') && wsEditorData.selectedActor) {
  			actorSelectorWidget.setSelectedActor(wsEditorData.selectedActor);
  		} else {
  			actorSelectorWidget.setSelectedActor(null);
  		}
  	}
  
  	/******************************************************************
  	                        "Test Access" feature
  	 ******************************************************************/
  	var testAccessDialog = $('#ws_ame_test_access_screen').dialog({
  			autoOpen: false,
  			modal: true,
  			closeText: ' ',
  			title: 'Test access',
  			width: 900
  			//draggable: false
  		}),
  		testMenuItemList = $('#ws_ame_test_menu_item'),
  		testActorList = $('#ws_ame_test_relevant_actor'),
  		testAccessButton = $('#ws_ame_start_access_test'),
  		testAccessFrame = $('#ws_ame_test_access_frame'),
  		testConfig = null,
  
  		testProgress = $('#ws_ame_test_progress'),
  		testProgressText = $('#ws_ame_test_progress_text');
  
  	$('#ws_test_access').click(function () {
  		testConfig = readMenuTreeState();
  
  		var selectedMenuContainer = getSelectedMenu(),
  			selectedItemContainer = getSelectedSubmenuItem(),
  			selectedMenu = null,
  			selectedItem = null,
  			selectedUrl = null;
  		if (selectedMenuContainer.length > 0) {
  			selectedMenu = selectedMenuContainer.data('menu_item');
  			selectedUrl = getFieldValue(selectedMenu, 'url');
  		}
  		if (selectedItemContainer.length > 0) {
  			selectedItem = selectedItemContainer.data('menu_item');
  			selectedUrl = getFieldValue(selectedItem, 'url');
  		}
  
  		function addMenuItems(collection, parentTitle, parentFile) {
  			_.each(collection, function (menuItem) {
  				if (menuItem.separator) {
  					return;
  				}
  
  				var title = formatMenuTitle(getFieldValue(menuItem, 'menu_title', '[Untitled menu]'));
  				if (parentTitle) {
  					title = parentTitle + ' -> ' + title;
  				}
  				var url = getFieldValue(menuItem, 'url', '[no-url]');
  
  				var option = $(
  					'<option>', {
  						val: url,
  						text: title
  					}
  				);
  				option.data('menu_item', menuItem);
  				option.data('parent_file', parentFile || '');
  				option.prop('selected', (url === selectedUrl));
  
  				testMenuItemList.append(option);
  
  				if (menuItem.items) {
  					addMenuItems(menuItem.items, title, getFieldValue(menuItem, 'file', ''));
  				}
  			});
  		}
  
  		//Populate the list of menu items.
  		testMenuItemList.empty();
  		addMenuItems(testConfig.tree);
  
  		//Populate the actor list.
  		testActorList.empty();
  		testActorList.append($('<option>', {text: 'Not selected', val: ''}));
  		_.each(actorSelectorWidget.getVisibleActors(), function (actor) {
  			//TODO: Skip anything that isn't a role
  			var option = $('<option>', {
  				val: actor.id,
  				text: actorSelectorWidget.getNiceName(actor)
  			});
  			testActorList.append(option);
  		});
  
  		//Pre-select the current actor.
  		if (actorSelectorWidget.selectedActor !== null) {
  			testActorList.val(actorSelectorWidget.selectedActor);
  		}
  
  		testAccessDialog.dialog('open');
  	});
  
  	testAccessButton.click(function () {
  		testAccessButton.prop('disabled', true);
  		testProgress.show();
  		testProgressText.text('Sending menu settings...');
  
  		var selectedOption = testMenuItemList.find('option:selected').first(),
  			selectedMenu = selectedOption.data('menu_item'),
  			menuUrl = selectedOption.val();
  
  		$.ajax(
  			wsEditorData.adminAjaxUrl,
  			{
  				data: {
  					'action': 'ws_ame_set_test_configuration',
  					'data': encodeMenuAsJSON(testConfig),
  					'_ajax_nonce': wsEditorData.setTestConfigurationNonce
  				},
  				method: 'post',
  				dataType: 'json',
  				success: function(response) {
  					if (!response) {
  						alert('Error: Could not parse the server response.');
  						testAccessButton.prop('disabled', false);
  						return;
  					}
  					if (response.error) {
  						alert(response.error);
  						testAccessButton.prop('disabled', false);
  						return;
  					}
  					if (!response.success) {
  						alert('Error: The request failed, but there is no error information available.');
  						testAccessButton.prop('disabled', false);
  						return;
  					}
  
  					//Caution: Won't work in IE. Needs compat checks.
  					var testPageUrl = new URL(menuUrl, window.location.href);
  					testPageUrl.searchParams.append('ame-test-menu-access-as', $('#ws_ame_test_access_username').val());
  					testPageUrl.searchParams.append('_wpnonce', wsEditorData.testAccessNonce);
  					testPageUrl.searchParams.append('ame-test-relevant-role', testActorList.val());
  
  					testPageUrl.searchParams.append('ame-test-target-item', getFieldValue(selectedMenu, 'file', ''));
  					testPageUrl.searchParams.append('ame-test-target-parent', selectedOption.data('parent_file'));
  
  					testProgressText.text('Loading the test page....');
  					$('#ws_ame_test_frame_placeholder').hide();
  
  					$(window).on('message', receiveTestAccessResults);
  					testAccessFrame
  						.show()
  						.on('load', onAccessTestLoaded)
  						.prop('src', testPageUrl.href);
  				},
  				error: function(jqXHR, textStatus) {
  					alert('HTTP Error: ' + textStatus);
  					testAccessButton.prop('disabled', false);
  				}
  			}
  		);
  	});
  
  	function onAccessTestLoaded() {
  		testAccessFrame.off('load', onAccessTestLoaded);
  		testProgress.hide();
  
  		testAccessButton.prop('disabled', false);
  	}
  
  	function receiveTestAccessResults(event) {
  		if (event.originalEvent.source !== testAccessFrame.get(0).contentWindow) {
  			if (console && console.warn) {
  				console.warn('AME: Received a message from an unexpected source. Message ignored.');
  			}
  			return;
  		}
  		var message = event.originalEvent.data || event.originalEvent.message;
  		console.log('message received', message);
  
  		$(window).off('message', receiveTestAccessResults);
  	}
  
  
  	//Finally, show the menu
  	loadMenuConfiguration(customMenu);
  
  	//Select the previous selected menu, if any.
  	if (wsEditorData.selectedMenu) {
  		AmeEditorApi.selectMenuItemByUrl(
  			'#ws_menu_box',
  			wsEditorData.selectedMenu,
  			_.get(wsEditorData, 'expandSelectedMenu') === '1'
  		);
  
  		if (wsEditorData.selectedSubmenu) {
  			AmeEditorApi.selectMenuItemByUrl(
  				'#ws_submenu_box',
  				wsEditorData.selectedSubmenu,
  				_.get(wsEditorData, 'expandSelectedSubmenu') === '1'
  			);
  		}
  	}
  
  	//... and make the UI visible now that it's fully rendered.
  	menuEditorNode.css('visibility', 'visible');
  }
  
  $(document).ready(ameOnDomReady);
  
  //Compatibility workaround: If another plugin or theme throws an exception in its jQuery.ready() handler,
  //our callback might never get run. As a backup, set a timer and manually check if the DOM is ready.
  var domCheckAttempts = 0,
  	maxDomCheckAttempts = 30;
  var domCheckIntervalId = window.setInterval(function () {
  	if (isDomReadyDone || (domCheckAttempts >= maxDomCheckAttempts)) {
  		window.clearInterval(domCheckIntervalId);
  		return;
  	}
  	domCheckAttempts++;
  
  	if ($ && $.isReady) {
  		isDomReadyDone = true;
  		ameOnDomReady();
  	}
  }, 1000);
  
  })(jQuery, wsAmeLodash);
  
  //==============================================
  //				Screen options
  //==============================================
  
  jQuery(function($){
  	'use strict';
  
  	var screenOptions = $('#ws-ame-screen-meta-contents');
  	var hideSettingsCheckbox = screenOptions.find('#ws-hide-advanced-settings');
  	hideSettingsCheckbox.prop('checked', wsEditorData.hideAdvancedSettings);
  
  	//Update editor state when settings change
  	$('#ws-hide-advanced-settings').click(function(){
  		wsEditorData.hideAdvancedSettings = hideSettingsCheckbox.prop('checked');
  
  		//Show/hide advanced settings dynamically as the user changes the setting.
  		if ($(this).is(hideSettingsCheckbox)) {
  			var menuEditorNode = $('#ws_menu_editor');
  			if ( wsEditorData.hideAdvancedSettings ){
  				menuEditorNode.find('div.ws_advanced').hide();
  				menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionShowAdvanced).show();
  			} else {
  				menuEditorNode.find('div.ws_advanced').show();
  				menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionHideAdvanced).hide();
  			}
  		}
  
  		$.post(
  			wsEditorData.adminAjaxUrl,
  			{
  				'action' : 'ws_ame_save_screen_options',
  				'hide_advanced_settings' : wsEditorData.hideAdvancedSettings ? 1 : 0,
  				'show_extra_icons' : wsEditorData.showExtraIcons ? 1 : 0,
  				'_ajax_nonce' : wsEditorData.hideAdvancedSettingsNonce
  			}
  		);
  	});
  
  	//Move our options into the screen meta panel
  	var advSettings = $('#adv-settings');
  	if (advSettings.length > 0) {
  		advSettings.empty().append(screenOptions.show());
  	}
  });
  