<?php namespace ProcessWire;

/**
 * ProcessWire Field Editing Process
 *
 * Add, Edit, and Remove Fields
 * 
 * For more details about how Process modules work, please see: 
 * /wire/core/Process.php 
 * 
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 * 
 * @property bool $listAfterSave
 *
 * @method string execute()
 * @method string executeAdd()
 * @method string executeEdit()
 * @method void executeSave()
 * @method string executeChangeType()
 * @method void executeSaveChangeType()
 * @method void saveContext()
 * @method bool allowFieldInTemplate(Field $field, Template $template)
 * @method InputfieldForm getListFilterForm()
 * @method MarkupAdminDataTable getListTable(array|Fields $fields)
 * @method array getListTableRow(Field $field)
 * @method InputfieldForm buildEditForm()
 * @method buildEditFormContext(InputfieldForm $form)
 * @method buildEditFormCustom(InputfieldForm $form)
 * @method InputfieldWrapper buildEditFormDelete()
 * @method InputfieldWrapper buildEditFormBasics()
 * @method InputfieldWrapper buildEditFormInfo($form) Deprecated, hook buildEditFormActions() instead.
 * @method InputfieldWrapper buildEditFormActions(InputfieldForm $form)
 * @method InputfieldWrapper buildEditFormAdvanced()
 * 
 * @method void fieldAdded(Field $field)
 * @method void fieldSaved(Field $field)
 * @method void fieldDeleted(Field $field)
 * @method void fieldChangedType(Field $field)
 * @method void fieldChangedContext(Field $field, Fieldgroup $fieldgroup, array $diffContextArray);
 * 
 */

class ProcessField extends Process implements ConfigurableModule {

	public static function getModuleInfo() {
		return array(
			'title' => __('Fields', __FILE__),
			'summary' => __('Edit individual fields that hold page data', __FILE__),
			'version' => 114,
			'permanent' => true, 
			'permission' => 'field-admin', // add this permission if you want this Process available for roles other than Superuser
			'icon' => 'cube', 
			'useNavJSON' => true,
			'searchable' => 'fields', 
			'addFlag' => Modules::flagsNoUserConfig
		);
	}

	/**
	 * @var InputfieldForm|null $form
	 * 
	 */
	protected $form = null;

	/**
	 * @var Field|null $field
	 * 
	 */
	protected $field;

	/**
	 * @var int $id
	 * 
	 */
	protected $id;

	/**
	 * @var array $moduleInfo
	 * 
	 */
	protected $moduleInfo = array();

	/**
	 * @var array $labels
	 * 
	 */
	protected $labels = array();

	/**
	 * Optional context fieldgroup
	 * 
	 * When populated, we are editing the field only in the context of this fieldgroup/template. 
	 * 
	 * @var Fieldgroup|null $fieldgroup
	 *
	 */
	protected $fieldgroup = null;

	/**
	 * Optional namespace for context fieldgroup
	 * 
	 * @var string
	 * 
	 */
	protected $contextNamespace = '';

	/**
	 * Label that indicates what the context is (whether fieldgroup or namespace)
	 * 
	 * Provided via get/post var $context_label
	 * 
	 * @var string
	 * 
	 */
	protected $contextLabel = '';

	/**
	 * Data thats been overridden by all fieldgroups, indexed by fieldgroup ID with array value indexed by overridden field keys
	 * 
	 * @var array $fieldgroupData
	 *
	 */
	protected $fieldgroupData = array();

	/**
	 * Init the module
	 *
	 */
	public function init() {
	
		$input = $this->wire()->input;
		
		if($input->urlSegment1 === 'edit') $this->wire()->modules->get('JqueryWireTabs');
		
		$this->moduleInfo = self::getModuleInfo();
		$this->headline($this->moduleInfo['title']); 
		
		$this->labels = array(
			'save' => $this->_('Save'), // Save button label
			'import' => $this->_('Import'), 
			'export' => $this->_('Export'),
			'ok' => $this->_('Ok'),
			'name' => $this->_x('Name', 'list thead'), 
			'label' => $this->_x('Label', 'list thead'),
			'type' => $this->_x('Type', 'list thead'), 
			'tag' => $this->_('Tag'),
			'tags' => $this->_('Tags'),
			'manage-tags' => $this->_('Manage Tags'), 
			'fields' => $this->_('Fields'),
			'templates' => $this->_x('Templates', 'list thead quantity'), 
			'yes' => $this->_x('Yes', 'access'), // General purpose "Yes" label
			'no' => $this->_x('No', 'access') // General purpose "No" label
		);
		
		$this->id = (int) $input->post('id');
		if(!$this->id) $this->id = (int) $input->get('id'); 
		if($this->id < 1) $this->id = 0;
		if($this->id) $this->field = $this->wire()->fields->get($this->id);
		if(!$this->field) $this->field = $this->wire(new Field());

		parent::init();
	}

	/**
	 * Render JSON map of all fields (old method, but should be kept for backwards compatibility)
	 * 
	 * @return string JSON
	 *
	 */
	public function renderListJSON() {
		$a = array();
		$showAll = $this->wire()->input->get('all');
		foreach($this->wire()->fields as $field) {
			if(!$showAll && ($field->flags & Field::flagSystem) && $field->name != 'title') continue; 
			$a[] = array(
				'id' => $field->id,
				'name' => $field->name,
				'flags' => $field->flags, 
			);
		}
		header("Content-Type: application/json"); 
		return json_encode($a); 
	}

	/**
	 * Output JSON list of navigation items for this (intended to for ajax use)
	 *
	 * For 2.5+ admin theme navigation
	 * 
	 * @param array $options
	 * @return string|array
	 *
	 */
	public function ___executeNavJSON(array $options = array()) {
		$fieldsArray = array();
		$showAll = $this->wire()->config->advanced; 
		foreach($this->wire()->fields as $field) {
			if(!$showAll) {
				if(($field->flags & Field::flagSystem) && $field->name != 'title') continue;
				if($field->type instanceof FieldtypeFieldsetClose) continue;
			}
			$fieldsArray[] = $field; 
		}
		$options['items'] = $fieldsArray;
		$options['itemLabel'] = 'name';
		return parent::___executeNavJSON($options);
	}

	/**
	 * Renders filtering options when viewing a list of Fields
	 *
	 * @return InputfieldForm
	 * 
	 */
	public function ___getListFilterForm() {

		$fieldtypes = $this->wire()->fieldtypes;
		$sanitizer = $this->wire()->sanitizer;
		$session = $this->wire()->session;
		$modules = $this->wire()->modules;
		$input = $this->wire()->input;
		
		$showAllLabel = $this->_('Show All'); 

		/** @var InputfieldForm $form */
		$form = $modules->get("InputfieldForm"); 
		$form->attr('id', 'field_filter_form'); 
		$form->attr('method', 'get'); 
		$form->attr('action', './'); 

		/** @var InputfieldFieldset $fieldset */
		$fieldset = $modules->get("InputfieldFieldset"); 
		$fieldset->attr('id', 'template_filters');
		$fieldset->entityEncodeLabel = false; 
		$fieldset->label = $this->_("Filters"); // Field list filters fieldset label
		$fieldset->icon = 'filter';
		$fieldset->collapsed = Inputfield::collapsedYes;
		$form->add($fieldset);

		/** @var InputfieldSelect $field */
		$field = $modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'templates_id'); 
		$field->addOption('', $showAllLabel); 
		foreach($this->templates as $template) {
			$name = $template->name; 
			if($template->flags & Template::flagSystem) $name .= "*";
			$field->addOption($template->id, $name); 
		}
		$inputTemplatesID = $input->get('templates_id');
		if($inputTemplatesID !== null) {
			$session->setFor($this, 'filterTemplate', (int) $inputTemplatesID);
			$inputTemplatesID = null;
		}
		$field->label = $this->_('Filter by template');
		$field->description = $this->_("When selected, only the fields from a specific template will be shown. Built-in fields are also shown when filtering by template. Asterisk (*) indicates system templates."); // Filter by template description
		$field->icon = 'cubes';
		$filterTemplateID = (int) $session->getFor($this, 'filterTemplate'); // $this->session->ProcessFieldListTemplatesID; 
		$field->attr('value', $filterTemplateID); 
		if($filterTemplateID && $template = $this->wire()->templates->get($filterTemplateID)) {
			$form->description = sprintf($this->_('Showing fields from template: %s'), $template);
			$this->headline($this->_('Fields by Template')); // Page headline when filtering by template
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$template = null;
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

		// ----------------------------------------------------------------

		/** @var InputfieldSelect $field */
		$field = $modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'fieldtype'); 
		$field->addOption('', $showAllLabel); 
		foreach($fieldtypes as $fieldtype) {
			$field->addOption($fieldtype->name, $fieldtype->longName);
		}
		$inputFieldtype = $input->get('fieldtype'); 
		if($inputFieldtype !== null) {
			$session->setFor($this, 'filterFieldtype', $sanitizer->name($inputFieldtype));
			$inputFieldtype = null;
		}
		$field->label = $this->_('Filter by type');
		$field->description = $this->_('When specified, only fields of the selected type will be shown. Built-in fields are also shown when filtering by field type.'); // Filter by fieldtype description
		$field->icon = 'plug';
		$filterFieldtype = $session->getFor($this, 'filterFieldtype'); 
		$field->attr('value', $filterFieldtype); 
		if($filterFieldtype && $fieldtype = $fieldtypes->get($filterFieldtype)) {
			$form->description = sprintf($this->_('Showing fields of type: %s'), $fieldtype->longName);
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

		// ----------------------------------------------------------------

		if(is_null($template) && !$session->getFor($this, 'filterFieldtype')) {
			/** @var InputfieldRadios $field */
			$field = $modules->get("InputfieldRadios"); 
			$field->attr('id+name', 'show_system'); 
			$field->label = $this->_('Show system fields?');
			$field->description = $this->_("When checked, built-in fields will also be shown. These include system fields and permanent fields. System fields are required by the system and cannot be deleted or have their name changed. Permanent fields are those that cannot be removed from a template. These fields are used internally by ProcessWire."); // Show built-in fields description
			$field->addOption(1, $this->labels['yes']);
			$field->addOption(0, $this->labels['no']);
			$field->optionColumns = 1;
			$field->icon = 'gear';
			$field->collapsed = Inputfield::collapsedYes; 
			$inputShowSystem = $input->get('show_system');
			if($inputShowSystem !== null) {
				$session->setFor($this, 'filterShowSystem', (int) $inputShowSystem); 
				$inputShowSystem = null;
			}
			$field->value = (int) $session->getFor($this, 'filterShowSystem'); 
			if($session->getFor($this, 'filterShowSystem')) {
				$field->attr('checked', 'checked');
				$field->collapsed = Inputfield::collapsedNo;
				$fieldset->collapsed = Inputfield::collapsedNo;
				$form->description = $this->_('Showing all fields, including built-in system and permanent fields.'); 
			}
			$fieldset->add($field); 
		}
		
		return $form;
	}

	/**
	 * Render a list of current fields
	 * 
	 * @return string
	 *
	 */
	public function ___execute() {
		
		if($this->wire()->config->ajax) return $this->renderListJSON();
	
		$session = $this->wire()->session;
		$modules = $this->wire()->modules;
		$textTools = $this->wire()->sanitizer->getTextTools();
		
		$out = $this->getListFilterForm()->render() . "\n<div id='ProcessFieldList'>\n";
		$fieldsByTag = array();
		$untaggedLabel = $this->_('Untagged');
		$hasFilters = $session->getFor($this, 'filterTemplate') || $session->getFor($this, 'filterFieldtype');
		$showSystem = $session->getFor($this, 'filterShowSystem');
		$collapsedTags = $modules->getConfig($this, 'collapsedTags');
		$caseTags = array(); // indexed by lowercase version of tag
		if(!is_array($collapsedTags)) $collapsedTags = array();
		$systemTag = $this->_x('System', 'tag'); // Tag applied to the group of built-in/system fields

		if(!$hasFilters) {
			foreach($this->fields as $field) {
				$tags = $field->getTags();
				if($showSystem) {
					if($field->flags & Field::flagSystem || $field->flags & Field::flagPermanent) {
						$tags['system'] = $systemTag;
						$caseTags[$systemTag] = $systemTag;
					}
				}
				if(empty($tags)) {
					$tag = $textTools->strtolower($untaggedLabel);
					if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
					$fieldsByTag[$tag][$field->name] = $field;
					$caseTags[$tag] = $untaggedLabel;
					continue;
				}
				foreach($tags as /* $name => */ $tag) {
					if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
					$fieldsByTag[$tag][$field->name] = $field;
					if(!isset($caseTags[$tag])) {
						$caseTags[$tag] = sprintf($this->_('Tag: %s'), trim($tag, '_-'));
					}
				}
			}
		}

		$tagCnt = count($fieldsByTag);
		if($tagCnt > 1) {
			$form = $this->wire(new InputfieldWrapper());
			ksort($fieldsByTag);
			foreach($fieldsByTag as $tag => $fields) {
				ksort($fields);
				/** @var InputfieldMarkup $f */
				$f = $modules->get('InputfieldMarkup');
				$f->entityEncodeLabel = false;
				$f->label = $caseTags[$tag];
				$f->icon = 'tags';
				if($tag == $systemTag) $f->icon = 'gear';
				$f->value = $this->getListTable($fields)->render();
				if(in_array($tag, $collapsedTags)) $f->collapsed = Inputfield::collapsedYes; 
				$form->add($f);
			}
			$out .= $form->render();
		} else {
			$out .= $this->getListTable($this->fields)->render();
		}

		$out .= "\n</div><!--/#ProcessFieldList-->";

		/** @var InputfieldButton $button */
		$button = $modules->get('InputfieldButton');
		$button->id = 'add_field_button'; 
		$button->href = "./add"; 
		$button->value = $this->_x('Add New Field', 'list button'); 
		$button->icon = 'plus-circle';
		$button->showInHeader();
		$out .= $button->render();

		/** @var InputfieldButton $button */
		$button = $modules->get('InputfieldButton');
		$button->id = 'tags_button';
		$button->href = './tags/';
		$button->icon = 'tags';
		$button->value = $this->labels['manage-tags'];
		$out .= $button->render();

		/** @var InputfieldButton $button */
		$button = $modules->get('InputfieldButton');
		$button->id = 'import_button';
		$button->href = "./import/";
		$button->value = $this->labels['import'];
		$button->icon = 'paste';
		$button->setSecondary();
		$out .= $button->render();

		/** @var InputfieldButton $button */
		$button = $modules->get('InputfieldButton');
		$button->id = 'export_button'; 
		$button->href = "./export/";
		$button->value = $this->labels['export'];
		$button->icon = 'copy';
		$button->setSecondary();
		$out .= $button->render();

		if($this->wire()->input->get('nosave')) {
			$session->removeFor($this, 'filterTemplate');
			$session->removeFor($this, 'filterFieldtype');
			$session->removeFor($this, 'filterShowSystem');
		}

		return $out;
	}

	/**
	 * Handle the “Manage Tags” actions
	 * 
	 * @return string
	 * 
	 */
	public function executeTags() {
		
		$input = $this->wire()->input; 
		$modules = $this->wire()->modules;
		$fields = $this->wire()->fields;
		$sanitizer = $this->wire()->sanitizer;
		
		/** @var InputfieldForm $form */
		$form = $modules->get('InputfieldForm');

		$out = '';
		$labels = $this->labels;
		$headline = $labels['tags'];
		$this->headline($headline);
		$this->breadcrumb('../', $labels['fields']);
		
		$tags = $fields->getTags();
		$editTag = $sanitizer->words($input->get->text('edit_tag'), array('separator' => '-'));
		$saveTag = $sanitizer->words($input->post->text('save_tag'), array('separator' => '-'));
		
		$collapsedTags = $modules->getConfig($this, 'collapsedTags');
		if(!is_array($collapsedTags)) $collapsedTags = array();
	
		if($editTag) {
			// edit which fields are assigned to tag
			$this->breadcrumb('./', $headline);
			$this->headline("$labels[tag] - " . (isset($tags[$editTag]) ? $tags[$editTag] : $editTag));
			
			/** @var InputfieldName $f */
			$f = $modules->get('InputfieldText');
			$f->attr('name', 'rename_tag');
			$f->label = $this->_('Tag name');
			$f->attr('value', isset($tags[$editTag]) ? $tags[$editTag] : $editTag);
			$f->collapsed = Inputfield::collapsedYes;
			$f->addClass('InputfieldIsSecondary', 'wrapClass');
			$f->icon = 'tag';
			$form->add($f);

			/** @var InputfieldCheckboxes $f */
			$f = $modules->get('InputfieldCheckboxes');
			$f->attr('name', 'tag_fields');
			$f->label = $this->_('Select all fields that should have this tag');
			$f->table = true;
			$f->icon = 'cube';
			$f->thead = "$labels[name]|$labels[label]|$labels[type]|$labels[tags]";
			$value = array();
			foreach($fields as $field) {
				/** @var Field $field */
				/** @var Fieldtype $fieldtype */
				$fieldtype = $field->type;
				if($field->flags & Field::flagSystem && !in_array($field->name, array('title', 'email'))) continue;
				$f->addOption($field->name, "**$field->name**|$field->label|{$fieldtype->shortName}|" . $field->getTags(', '));
				if($field->hasTag($editTag)) $value[] = $field->name;
			}
			$f->attr('value', $value);
			$form->add($f);
			
			/** @var InputfieldCheckbox */
			$f = $modules->get('InputfieldCheckbox');
			$f->attr('name', 'tag_collapsed'); 
			$f->label = $this->_('Display as collapsed in fields list?');
			if(in_array($editTag, $collapsedTags)) $f->attr('checked', 'checked');
			$form->add($f);
		
			/** @var InputfieldHidden $f */
			$f = $modules->get('InputfieldHidden');
			$f->attr('name', 'save_tag');
			$f->attr('value', $editTag); 
			$form->appendMarkup = "<p class='detail'>" . wireIconMarkup('trash-o') . ' ' . 
				$this->_('To delete this tag, remove all fields from it.') . "</p>";
			$form->add($f);
			
		} else if($saveTag) {
			// save tag
			
			$tagFields = $sanitizer->names($input->post('tag_fields'));
			if(!is_array($tagFields)) $tagFields = array();
			$renameTag = $sanitizer->words($input->post->text('rename_tag'), array('separator' => '-')); 
			$isCollapsed = (int) $input->post('tag_collapsed');
			$removeTag = '';
			if($renameTag && $renameTag != $saveTag) {
				$removeTag = $saveTag;	
				$saveTag = $renameTag;
			}
			foreach($fields as $field) {
				/** @var Field $field */
				if($removeTag && $field->hasTag($removeTag)) {
					$field->removeTag($removeTag);
				}
				if(in_array($field->name, $tagFields)) { 
					// field should have the given tag
					if($field->hasTag($saveTag)) continue;
					$field->addTag($saveTag);
					$this->message(sprintf($this->_('Added tag “%1$s” to field: %2$s'), $saveTag, $field->name));
				} else if($field->hasTag($saveTag)) {
					// field should not have the given tag
					$field->removeTag($saveTag);
					$this->message(sprintf($this->_('Removed tag “%1$s” from field: %2$s'), $saveTag, $field->name));
				}
				if($field->isChanged('tags')) $field->save();
			}
			$_collapsedTags = $collapsedTags;
			if($isCollapsed) {
				if(!in_array($saveTag, $collapsedTags)) $collapsedTags[] = $saveTag;
			} else {
				$key = array_search($saveTag, $collapsedTags); 
				if($key !== false) unset($collapsedTags[$key]); 
			}
			if($collapsedTags !== $_collapsedTags) {
				$modules->saveConfig($this, 'collapsedTags', $collapsedTags);
			}
			$this->wire()->session->redirect('./'); 
			
		} else {
			// list defined tags
			$out .= "<p class='description'>" . 
				$this->_('Tags enable you to create collections of fields for listing or searching.') . "</p>";
			/** @var MarkupAdminDataTable $table */
			$table = $modules->get('MarkupAdminDataTable');
			$table->setSortable(false);
			$table->setEncodeEntities(false);
			$table->headerRow(array($labels['name'], $labels['fields']));
			
			foreach($tags as $key => $tag) {
				$tagFields = $fields->findByTag($tag, true);
				$table->row(array(
					$tag => "./?edit_tag=$tag",
					implode(', ', $tagFields)
				));
				if($fields->get($key)) {
					$this->warning(sprintf($this->_('Warning: tag “%s” has the same name as a Field.'), $tag));
				}
			}
			
			if(count($tags)) $out .= $table->render();

			$form->attr('method', 'get');
			/** @var InputfieldName $f */
			$f = $modules->get('InputfieldText');
			$f->attr('name', 'edit_tag');
			$f->label = $this->_('Add new tag');
			$f->description = $this->_('You may use letters, digits or underscore.'); 
			$f->icon = 'tag';
			$f->addClass('InputfieldIsSecondary', 'wrapClass');
			$form->add($f);
		}
		
		$f = $modules->get('InputfieldSubmit');
		$form->add($f);
		$out .= $form->render();
	
		return $out;
	}
	

	/**
	 * Get the table that lists fields
	 * 
	 * @param array|Fields $fields
	 * @return MarkupAdminDataTable
	 * 
	 */
	protected function ___getListTable($fields) {

		/** @var MarkupAdminDataTable $table */
		$table = $this->wire()->modules->get("MarkupAdminDataTable");
		$labels = $this->labels; 

		$headerRow = array(
			$labels['name'], 
			$labels['label'], 
			$labels['type'], 
			$labels['templates']
		); 

		$table->headerRow($headerRow);
		$table->setEncodeEntities(false);
		$numRows = 0;

		foreach($fields as $field) {
			$row = $this->getListTableRow($field);
			if(!empty($row)) {
				$table->row($row); 
				$numRows++;
			}
		}

		if(!$numRows) $table->row(array($this->_("No fields matched your filter"))); 

		return $table;
	}

	/**
	 * Get a table row (array) for placement in getListTable()
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	protected function ___getListTableRow(Field $field) {
		
		$sanitizer = $this->wire()->sanitizer; 
		$session = $this->wire()->session;
		$editUrl = "edit?id=$field->id";
		
		if($field->type instanceof FieldtypeFieldsetClose) {
			if(!$session->getFor($this, 'filterShowSystem')) return array();
		}

		$flagDefs = array(
			'Autojoin' => array(
				'icon' => 'sign-in',
				'label' => $this->_x('autojoin', 'list notes'),
				'href' => "$editUrl#find-autojoin",
			),
			'Global' => array(
				'icon' => 'globe',
				'label' => $this->_x('global', 'list notes'),
				'href' => "$editUrl#find-global",
			),
			'System' => array(
				'icon' => 'puzzle-piece',
				'label' => $this->_x('system', 'list notes'),
				'href' => ($this->wire()->config->advanced ? "$editUrl#find-system" : ''),
			),
			'Permanent' => array(
				'icon' => 'building-o',
				'label' => $this->_x('permanent', 'list notes')
			),
			'Required' => array(
				'icon' => 'asterisk', 
				'label' => $this->_x('required', 'list notes'),
				'href' => "$editUrl#find-required",
			),
			'Dependency' => array(
				'icon' => 'question-circle', 
				'label' => $this->_x('show if...', 'list notes'),
				'href' => "$editUrl#find-showIf",
			),
			'Access' => array(
				'icon' => 'key', 
				'label' => $this->_x('access', 'list notes'),
				'href' => "$editUrl#find-useRoles",
			),
			'Unique' => array(
				'icon' => 'snowflake-o',
				'label' => $this->_x('unique', 'list notes'),
				'href' => "$editUrl#find-flagUnique",
			),
			'Context' => array(
				'icon' => 'shield',
				'label' => $this->_x('overrides/contexts', 'list notes'),
				'href' => "$editUrl#find-tab-overrides",
			),
		);

		$numTemplates = $field->getTemplates(true);
		$numTemplatesLink = "$editUrl#find-send_templates";
		$numTemplatesLabel = sprintf($this->_n('%d template', '%d templates', $numTemplates), $numTemplates);
		$flags = array();
		$fieldName = $field->name;
		$builtIn = false;
		
		$templatesID = (int) $session->getFor($this, 'filterTemplate'); 
		if($templatesID && $template = $this->templates->get($templatesID)) {
			if(!$template->fieldgroup->has($field)) return array();
		}
		if($fieldtype = $session->getFor($this, 'filterFieldtype')) {
			if($field->type != $fieldtype) return array();
		}

		if($field->flags & Field::flagAutojoin) $flags[] = 'Autojoin';
		if($field->flags & Field::flagGlobal) $flags[] = 'Global';
		if($field->flags & Field::flagAccess) $flags[] = 'Access';
		if($field->flags & Field::flagUnique) $flags[] = 'Unique';

		if($field->flags & Field::flagSystem) {
			$flags[] = 'System';
			$builtIn = true;
		}

		if($field->flags & Field::flagPermanent) {
			$flags[] = 'Permanent';
			$builtIn = true;
		}
		
		if($field->showIf) $flags[] = 'Dependency';
		if($field->required) $flags[] = 'Required';
		if(count($field->getContexts())) $flags[] = 'Context';

		if($builtIn && !$templatesID && $field->name != 'title' && $field->name != 'email') {
			if(!$session->getFor($this, 'filterShowSystem')) return array();
		}


		$flagsOut = '';
		foreach($flags as $flagName) {
			if(!isset($flagDefs[$flagName])) continue;
			$flag = $flagDefs[$flagName];
			$icon = wireIconMarkup($flag['icon']);
			$href = isset($flag['href']) ? $flag['href'] : '#';
			$flagsOut .= "<a href='$href' class='fieldFlag fieldFlag$flagName tooltip' title='$flag[label]'>$icon</span></a>";
		}

		$icon = $field->getIcon();
		$icon = $icon ? wireIconMarkup($icon) : ''; 
		$label = $sanitizer->entities($field->getLabel());
		$typeName = $sanitizer->entities($field->type->shortName);
		$inputName = $typeName === 'Page' ? $field->get('inputfield') : $field->get('inputfieldClass|fieldtypeClass');
		
		if($inputName) {
			$inputName = $sanitizer->entities(str_replace(array('Inputfield', 'Fieldtype'), '', $inputName));
			if(strpos($inputName, $typeName) === 0) list(,$inputName) = explode($typeName, $inputName, 2);
			$inputName = $inputName === $typeName || !$inputName ? '' : "/$inputName";
		} else {
			$inputName = '';
		}
		
		if(!$numTemplates) {
			$label .= " <span class='notes'>" . $this->_('(not used)') . "</span>";
		}
		
		return array(
			"$fieldName" => $editUrl,
			"$icon $label",
			"$typeName$inputName",
			"$flagsOut<a class='pw-tooltip' title='$numTemplatesLabel' href='$numTemplatesLink'>$numTemplates</a>"
		);
	}

	/**
	 * Add a new field
	 *
	 */
	public function ___executeAdd() {

		// unrelated shortcut feature: double check that all field tables actually exist
		// and re-create them in instances where they don't (like if a bunch of fields
	 	// were migrated over in an SQL dump of the "fields" table or something). 
		$this->wire()->fields->checkFieldTables(); 

		return $this->executeEdit(); 
	}


	/**
	 * Edit an existing Field
	 *
	 */
	public function ___executeEdit() {
		
		if(is_null($this->form)) $this->buildEditForm();
		$this->breadcrumb('./', $this->moduleInfo['title']);
		
		if($this->field->id) {
			$headline = sprintf($this->_x('Edit Field: %s', 'edit headline'), $this->field->name); // Headline when editing a field
		} else {
			$headline = $this->_x('Add New Field', 'add headline'); // Headline when adding a field
		}
		
		if($this->fieldgroup) {
			$this->breadcrumb("./edit?id=" . $this->field->id, $this->field->name);
			if($this->contextLabel) {
				$headline .= ' (' . sprintf($this->_('when used with: %s'), $this->contextLabel) . ')';
			} else if($this->contextNamespace) {
				$headline .= ' (' . sprintf($this->_('when used with template “%1$s” in context “%2$s”'), $this->fieldgroup->name, $this->contextNamespace) . ')';
			} else {
				$headline .= ' (' . sprintf($this->_('when used with template: %s'), $this->fieldgroup->name) . ')';
			}
		}

		$this->headline($headline);
		$this->browserTitle($headline);

		if($this->field->id && !$this->fieldgroup && $this->wire()->session->get($this, 'optimize') == $this->field->id) {
			// this is outside of buildEditForm intentionally, in case anything has hooked buildEditForm and adding properties to it
			$alert = $this->buildEditFormAlert();	
			if(!is_null($alert)) {
				$this->form->action .= '#alert';
				$f = $this->form->getChildByName('submit_save_field');
				if($f) {
					$this->form->insertBefore($alert, $f);
				} else {
					$this->form->add($alert);
				}
			}
		}
		
		$out = $this->form->render(); 
		$out .= $this->renderContextSelect();
		
		return $out;
	}

	/**
	 * Get an array of all template IDs (integers) that don't have the given field
	 * 
	 * @param Field $field
	 * @return array
	 * 
	 */
	protected function getTemplatesWithoutField(Field $field) {
		$templatesWithoutField = array();
		foreach($this->wire()->templates as $template) {
			if($template->fieldgroup->hasField($field)) continue;
			$templatesWithoutField[] = (int) $template->id;
		}
		return $templatesWithoutField;
	}

	/**
	 * Build alert section for unknown field properties
	 * 
	 * @return InputfieldWrapper|null
	 * 
	 */
	protected function buildEditFormAlert() {
		
		$modules = $this->wire()->modules;

		$this->field->type->getDatabaseSchema($this->field); // may add to trackGets, so we include it (i.e. FieldtypeFile and fileSchema)
		$gets = $this->field->trackGets();
		$xkeys = array();
		$numRows = 0; 
		$checkLabel = $this->_('Check field reported:') . ' ';
		
		if(is_array($gets)) {
			foreach($this->field->data as $key => $value) {
				if(in_array($key, $gets)) continue;
				if($this->form->getChildByName($key)) continue; // confirm there isn't a field with the name
				if($key === '_lazy') continue;
				$xkeys[] = $key;
			}
		}

		foreach($this->wire()->fields as $field) {
			
			$table = $field->getTable();
			
			$sql = 	"SELECT * FROM $table " .
					"LEFT JOIN pages ON $table.pages_id=pages.id " .
					"WHERE pages.id IS NULL ";
			
			$templatesWithoutField = $this->getTemplatesWithoutField($field);
			if(count($templatesWithoutField)) {
				$sql .= "OR pages.templates_id IN(" . implode(',', $templatesWithoutField) . ")";
			}

			$query = $this->database->prepare($sql);
			
			try {
				$query->execute();
				$cnt = $query->rowCount();
				if($cnt > 0) {
					if($field->name == $this->field->name) {
						$message = $checkLabel . sprintf($this->_('%d orphaned table rows found'), $cnt); 
						$numRows = $cnt; 
						$n = 0; 
						while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
							$message .= "\n" . 
								"<small>pages_id: $row[pages_id], data: " . 
								$this->wire()->sanitizer->entities($row['data']) . "</small>";
							if(++$n >= 100) break;
						}
						if($n >= 100) $message .= "\n" . $this->_('...and so on...');
					} else {
						$message = sprintf(
							$this->_('Please run "Actions > Check field data" on field %s'), 
							"<a href='./edit?id=$field->id#info'>$field->name</a>"
						);
					}
					$this->error(nl2br($message), Notice::allowMarkup); 
				}
			} catch(\Exception $e) {
			}
		}
		
		if(!count($xkeys) && !$numRows) {
			$this->message($checkLabel . sprintf($this->_('No issues found for field %s'), $this->field->name)); 
			$this->session->remove($this, 'optimize'); 
			return null;
		}
	
		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('class', 'WireTab');
		$form->attr('id', 'alert');
		$form->attr('title', $this->_x('Alert', 'tab'));
		
		if(count($xkeys)) {
			/** @var InputfieldCheckboxes $f */
			$f = $modules->get('InputfieldCheckboxes'); 
			$f->attr('name', '_remove_keys'); 
			$f->label = $this->_('Unknown Properties'); 
			$f->description = $this->_('The following properties were found with this field with zero accesses during configuration. Sometimes this can indicate that the properties are no longer in use. Check the box next to each property you want to remove. If you are not sure, there is no harm in just leaving them there.'); 
			$f->icon = 'exclamation-triangle';
			foreach($xkeys as $key) $f->addOption($key); 
			$this->wire()->session->set($this, '_remove_keys', $xkeys); 
			$form->add($f); 
			$this->error($checkLabel . $this->_('Potential unknown properties found in this field. Please see the "Alert" tab.')); 
		}
	
		if($numRows) {
			/** @var InputfieldCheckbox $f */
			$f = $modules->get('InputfieldCheckbox'); 
			$f->attr('name', '_remove_rows');
			$f->label = sprintf($this->_('Remove %d orphaned table rows?'), $numRows);
			$f->description = $this->_('We found rows of data in the table for this field that do not match up with any page, or match pages that do not have this field.');
			$f->icon = 'exclamation-triangle';
			$f->attr('value', $this->field->id); 
			$f->autocheck = false;
			$form->add($f); 
		}
		
		return $form; 
	}

	/**
	 * Add a '*' symbol to the labels of any fields that are overriding the defaults
	 * 
	 * @deprecated
	 *
	 */
	protected function identifyContextChanges() {

		if(!$this->fieldgroup) return;

		$fieldOriginal = $this->wire()->fields->get($this->field->id);
		$context = $this->fieldgroup->getFieldContextArray($this->field->id, $this->contextNamespace); 
		$languages = $this->wire()->languages;

		foreach($context as $key => $value) {

			$languageID = 0;
			if($languages) foreach($languages as $language) {
				if(strpos($key, (string) $language->id) && preg_match('/(.+?)' . $language->id . '$/', $key, $matches)) {
					$key = $matches[1];					
					$languageID = $language->id; 
					break;
				}
			} 

			if($key == 'label') $key = 'field_label'; // for retrieving inputfield

			$inputfield = $this->form->getChildByName($key);
			if(!$inputfield) continue; 

			if($key == 'field_label') $key = 'label'; // convert back
			if($languageID) $key .= $languageID; 

			if($value == $fieldOriginal->$key) continue; 
			$inputfield->label .= ' *';
		}
	}

	/**
	 * Render a select box where the user can choose another fieldgroup context
	 *
	 */
	protected function renderContextSelect() {

		if(!$this->field->id) return '';
		
		if($this->fieldgroup && $this->wire()->input->get('modal')) {
			return "<input type='hidden' name='fieldgroup_id' value='{$this->fieldgroup->id}' />";
		}
		
		$fieldgroups = $this->field->getFieldgroups();
		if(!count($fieldgroups)) return '';
		$contextLabel = $this->_('Override by template');

		$out = 	
			"<div id='fieldgroupContext'>" . 
			"<select id='fieldgroupContextSelect' name='fieldgroup_id'>" . 
			"<option value=''>$contextLabel</option>"; 

		foreach($fieldgroups->sort('name') as $fieldgroup) {
			/** @var Fieldgroup $fieldgroup */
			$selected = $this->fieldgroup && $this->fieldgroup->id == $fieldgroup->id ? " selected='selected'" : '';
			$out .= "<option$selected value='$fieldgroup->id'>$fieldgroup->name</option>";
		}

		$out .= 
			"</select>" . 
			"</div>";

		return $out; 	
	}

	/**
	 * Build the Field Edit form
	 * 
	 * @return InputfieldForm
	 * @throws WireException
	 *
	 */
	protected function ___buildEditForm() {
	
		$sanitizer = $this->wire()->sanitizer;
		$input = $this->wire()->input;
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;
		$fields = $this->wire()->fields;
		$fieldgroups = $this->wire()->fieldgroups;
		
		$isPost = $input->requestMethod('POST');

		// optional context fieldgroup
		$fieldgroup_id = (int) $input->post('fieldgroup_id');
		if(!$fieldgroup_id && !$isPost) $fieldgroup_id = (int) $input->get('fieldgroup_id');
	
		if($fieldgroup_id) {
			// optional namespace for context fieldgroup
			$contextNamespace = '';
			if($input->post('_context_namespace')) {
				$contextNamespace = $input->post('_context_namespace');
			} else if($input->get('context_namespace') && !$isPost) {
				$contextNamespace = $input->get('context_namespace');
			}
			if($contextNamespace) {
				$contextNamespace = $sanitizer->fieldName($contextNamespace);
				$this->contextNamespace = $contextNamespace;
			}	
		}

		$contextLabel = '';
		if($input->post('_context_label')) {
			$contextLabel = $input->post('_context_label');
		} else if($input->get('context_label')) {
			$contextLabel = $input->get('context_label');
		}
		if(strlen($contextLabel)) {
			$contextLabel = $sanitizer->entities($sanitizer->text($contextLabel));
			$this->contextLabel = $contextLabel;
		}
		
		if($this->field->id && $session->get($this, 'optimize') == $this->field->id) {
			// keep track of what gets retrieved from each field
			foreach($fields as $field) $field->trackGets(true);
		}

		/** @var InputfieldForm $form */
		$form = $modules->get('InputfieldForm');
		$form->attr('id+name', 'ProcessFieldEdit'); 
		$form->attr('action', 'save?id=' . $this->field->id); 
		$form->attr('method', 'post'); 
		$form->addClass('InputfieldFormFocusFirst');
		$this->form = $form;

		if($fieldgroup_id && $this->field->id) {
			$this->fieldgroup = $fieldgroups->get($fieldgroup_id); 
			if(!$this->fieldgroup) {
				throw new WireException("Invalid fieldgroup");
			}	
			if(!$this->fieldgroup->has($this->field)) {
				throw new WireException("Fieldgroup '{$this->fieldgroup->name}' does not have field '{$this->field->name}'");
			}
			// get the field in context of the fieldgroup
			$this->field = $this->fieldgroup->getFieldContext($this->field->id, $this->contextNamespace);
		}

		$form->add($this->buildEditFormBasics());

		if($this->field->id) { 
			$accessTab = $this->buildEditFormAccess();
			if($this->field->type) {
				$this->buildEditFormCustom($form);
				if(!$this->fieldgroup) {
					if($accessTab) $form->add($accessTab);
					$form->add($this->buildEditFormAdvanced());
				}
			}
			if(!$this->fieldgroup) {
				if($this->hasHook('buildEditFormInfo()')) {
					$actions = $this->buildEditFormInfo($form); // for legacy hooks
				} else {
					$actions = $this->buildEditFormActions($form);
				}
				if(count($actions)) $form->add($actions);
			} else {
				if($accessTab) $form->add($accessTab);
			}
			$this->buildEditFormContext($form);
		}

		/** @var InputfieldHidden $field */
		$field = $modules->get('InputfieldHidden');
		$field->attr('name', 'id'); 
		$field->attr('value', $this->field->id); 
		$form->add($field); 

		if($this->fieldgroup) {
			/** @var InputfieldHidden $field */
			$field = $modules->get('InputfieldHidden');
			$field->attr('name', 'fieldgroup_id'); 
			$field->attr('value', $this->fieldgroup->id); 
			$form->add($field);
			if($this->contextNamespace) {
				/** @var InputfieldHidden $field */
				$field = $modules->get('InputfieldHidden');
				$field->attr('name', '_context_namespace');
				$field->attr('value', $this->contextNamespace);
				$form->add($field);
			}
			if($this->contextLabel) {
				/** @var InputfieldHidden $field */
				$field = $modules->get('InputfieldHidden');
				$field->attr('name', '_context_label');
				$field->attr('value', $this->contextLabel);
				$form->add($field);
			}
		}
	
		/** @var InputfieldSubmit $field */
		$field = $modules->get('InputfieldSubmit');
		$field->attr('value', $this->labels['save']); 
		$field->attr('name', 'submit_save_field'); 
		$field->showInHeader();
		$form->add($field); 

		if($input->get('process_template')) {
			// ProcessTemplate has loaded the field editor in a modal window
			// so we add a cancel button that asmSelect will recognize for it's modal
			/** @var InputfieldButton $field */
			$field = $modules->get('InputfieldButton');
			$field->attr('id+name', 'modal_cancel_button'); 
			$field->attr('value', $this->_x('Cancel', 'button'));
			$field->setSecondary();
			$form->append($field);
		
			// contains the asm list item status, populated by JS
			/** @var InputfieldHidden $field */
			$field = $modules->get('InputfieldHidden');
			$field->attr('id+name', 'asmListItemStatus'); 
			$field->attr('class', 'asmListItemStatus');
			$width = $this->field->columnWidth;
			$field->attr('data-tpl',
				// % gets replaced with live percent
				"<span>" .  $this->field->type->shortName . "</span> " . 
				"<span class='columnWidth ui-priority-secondary' data-was='$width' data-field='{$this->field->id}'>%</span>"
			); 
			$field->attr('value', '');
			$form->append($field); 
		}
	
		// move the 'tabLabel' input right below the 'collapsed' input
		$f1 = $form->getChildByName('tabLabel');
		$fs = $form->getChildByName('visibility');
		$f2 = $fs ? $fs->getChildByName('collapsed') : null;
		if($f1 && $f2) {
			$f1->getParent()->remove($f1);
			$fs->insertAfter($f1, $f2);
		}
		
		$focus = $input->get('focus');
		if($focus) {
			$focus = $sanitizer->fieldName($focus);
			if($focus === 'label') $focus = 'field_label';
			$field = $focus ? $form->getChildByName($focus) : null;
			if($field) {
				$form->appendMarkup .=
					"<script>" . 
						"jQuery(document).ready(function() { " . 
							"Inputfields.find('#$field->id'); " . 
						"});" . 
					"</script>";
			}
		}
		
		return $form; 
	}

	/**
	 * Build field/template context edit
	 * 
	 * @param InputfieldForm $form
	 * 
	 */
	protected function ___buildEditFormContext($form) {
	
		$modules = $this->wire()->modules;
		
		$allChanges = array();
		$fieldgroups = $this->fieldgroup ? array($this->fieldgroup) : $this->wire()->fieldgroups; 
		$allowAddSettings = !$this->fieldgroup && !$this->contextNamespace; // whether settings can be added here
		
		foreach($fieldgroups as $fieldgroup) {
			/** @var Fieldgroup $fieldgroup */
			/** @var Field $field */
			$field = $fieldgroup->getField($this->field->name); 
			if(!$field) continue; 
			if(!$fieldgroup->hasFieldContext($field->name, $this->contextNamespace)) continue; 	
			$allChanges[$fieldgroup->name] = $this->getContextChanges($form, $field, $fieldgroup);
		}
	
		// exit now if there are no context change and none are allowed to be added
		if(!count($allChanges) && !$allowAddSettings) return;
	
		/** @var InputfieldWrapper $tab */
		$tab = $this->wire(new InputfieldWrapper());
		$tab->attr('title', $this->_('Overrides'));
		$tab->attr('id', 'tab-overrides');
		$tab->attr('class', 'WireTab');
		$form->add($tab); 
	
		/** @var InputfieldMarkup $f */
		$f = $modules->get('InputfieldMarkup');
		$f->attr('name', 'overrides_table');
		$f->description = $this->_('The following settings are overridden for this field by the indicated template(s). Check the box to the right of any row to remove the setting override (restoring the original value).');
		$f->icon = 'shield';
		if($this->fieldgroup) {
			if($this->contextNamespace) {
				$f->label = sprintf($this->_('Setting overrides for field namespace: %s'), $this->contextNamespace);
			} else {
				$f->label = sprintf($this->_('Setting overrides for template: %s'), $this->fieldgroup->name);
			}
		} else {
			$f->label = $this->_('Setting overrides by template'); 
			$f->notes = $this->_('To edit an override setting or override other settings, edit any template and click the field name in the fields list.') . ' ';
			$f->notes .= $this->_('To adjust what settings are allowed for overrides, see the field below.');
		}
		$tab->add($f);

		if(count($allChanges)) {
			$table = $this->buildEditFormContextTable($allChanges);
			$f->value = $table->render();
		} else {
			$f->value = '<p>' . $this->_('There are currently no settings being overridden by template.') . '</p>';
			$f->collapsed = Inputfield::collapsedYes;
		}
	
		if($allowAddSettings) $this->buildEditFormAllowedContexts($form, $tab); 
	}

	/**
	 * Build the overrides list table
	 * 
	 * @param array $allChanges
	 * @return MarkupAdminDataTable
	 * 
	 */
	protected function buildEditFormContextTable(array $allChanges) {

		$fieldgroups = $this->wire()->fieldgroups;
		$sanitizer = $this->wire()->sanitizer;
		$modules = $this->wire()->modules;
		
		/** @var InputfieldCheckbox $checkbox */
		$checkbox = $modules->get('InputfieldCheckbox');
		$checkbox->attr('name', '_remove_context[]');
		$checkbox->attr('id', '_remove_context');
		$checkbox->checkboxOnly = true;
		
		/** @var MarkupAdminDataTable $table */
		$table = $modules->get('MarkupAdminDataTable');
		$table->setEncodeEntities(false);
		$table->setSortable(false);
		$header = array();
		if(!$this->fieldgroup) $header[] = $this->_x('Template', 'context-thead');
		$header[] = $this->_x('Setting', 'context-thead');
		$header[] = $this->_x('Changes', 'context-thead');
		$header[] = "<i class='fa fa-lg fa-trash override-select-all'></i>";
		$table->headerRow($header);

		/** @var JqueryUI $jQueryUI */
		$jQueryUI = $modules->get('JqueryUI');
		$jQueryUI->use('modal');
		
		$textTools = $sanitizer->getTextTools();
		$allowModalEdit = !$this->fieldgroup && !$this->contextNamespace && !$this->wire()->input->get('modal');

		foreach($allChanges as $fieldgroupName => $changes) {
			$fieldgroup = $fieldgroups->get($fieldgroupName);
			$n = 0;

			foreach($changes as $key => $change) {
				$ns = empty($change['ns']) ? '' : substr($change['ns'], 3);
				if($allowModalEdit) {
					$url = "./edit?id={$this->field->id}&fieldgroup_id=$fieldgroup->id&focus=$change[name]";
					if($ns) $url .= "&context_namespace=$ns";
					$fieldgroupLabel = "<a class='pw-modal' data-buttons='#Inputfield_submit_save_field' data-autoclose='1' href='$url'>$fieldgroupName</a>";
				} else {
					$fieldgroupLabel = $fieldgroupName;
				}
				$row = array();
				$settingLabel = $change['label'];
				$newValue = $change['value'];
				$originalValue = $change['originalValue'];

				if($ns) {
					// use of $change[ns] rather than $ns is intentional so that $key is NS_foo.bar
					if(strpos($key, $change['ns']) !== 0) $key = $change['ns'] . '.' . $key; 
					$fieldgroupLabel .= " <span class='detail'>($ns)</span>";
				}

				$checkbox->attr('value', "$fieldgroup->id:$key");
				$checkbox->attr('id', '_remove_context_' . $fieldgroup->id . '_' . ($n++));
			
				if(!$this->fieldgroup) $row[] = $fieldgroupLabel;
				
				$row[] = $sanitizer->entities($settingLabel);
				$row[] = "<span class='pw-diff'>" . $textTools->diffMarkup($originalValue, $newValue, array('split' => '[^\d\w]+')) . "</span>";
				$row[] = $checkbox->render();
				
				$options = $fieldgroupLabel ? array('separator' => true) : array();
				$table->row($row, $options);
			}
		}
		
		return $table;
	}

	/**
	 * Build the "allow contexts" Inputfield
	 * 
	 * @param InputfieldForm $form
	 * @param InputfieldWrapper $tab
	 * 
	 */
	protected function buildEditFormAllowedContexts(InputfieldForm $form, InputfieldWrapper $tab) {

		$labels = $this->labels;

		/** @var InputfieldCheckboxes $field */
		$field = $this->wire()->modules->get('InputfieldCheckboxes');
		$field->attr('name', 'allowContexts');
		$field->label = $this->_('Settings allowed to override by template');
		$field->icon = 'sliders';
		$field->description =
			$this->_('Checked settings will appear as configuration options when editing this field within the context of a particular template.') . ' ' .
			$this->_('**WARNING:** enabling settings beyond those specified by the Fieldtype/Inputfield module may not always work, or may cause problems.') . ' ' .
			$this->_('As a result, we recommend testing any modifications to these settings in a non-production (development) environment first.');
		$field->notes =
			$this->_('Please Note:') . ' ' .
			$this->_('Settings in **bold** are those that the Fieldtype/Inputfield module has designated as always allowed for override.') . ' ' .
			$this->_('Other settings may or may not work for context overrides.') . ' ' . 
			$this->_('You should un-check any settings you find do not work, or otherwise cause problems.');
		$field->table = true;
		$field->thead = "$labels[label]|$labels[name]|$labels[type]";

		$tabNames = array(
			'fieldtypeConfig' => $this->_('Details:'),
			'inputfieldConfig' => $this->_('Input:')
		);
		
		$exclusions = array(
			'collapsed',
			'showIf',
			'required',
			'requiredIf',
			'columnWidth',
			'visibility',
		);

		$fieldtypeNames = $this->field->type->getConfigAllowContext($this->field);
		if(!is_array($fieldtypeNames)) $fieldtypeNames = array();
		$dummyPage = $this->wire()->pages->get("/"); // only using this to satisfy param requirement 
		$inputfield = $this->field->getInputfield($dummyPage);
		$inputfieldNames = $inputfield ? $inputfield->getConfigAllowContext($this->field) : array();
		if(!is_array($inputfieldNames)) $inputfieldNames = array();
		$alwaysSelected = array_merge($fieldtypeNames, $inputfieldNames);
		$allowContexts = $this->field->get('allowContexts');
		$alwaysSelected = array_diff($alwaysSelected, $allowContexts);
		$qty = 0;

		foreach($tabNames as $tabName => $tabLabel) {
			/** @var InputfieldWrapper $tabInputfield */
			$tabInputfield = $form->getChildByName($tabName);
			if(!$tabInputfield) continue;
			foreach($tabInputfield->getAll() as $f) {
				/** @var Inputfield $f */
				$name = $f->name;
				if(strpos($name, '_') === 0) continue;
				if(in_array($name, $exclusions) || strpos($name, 'theme') === 0) continue;
				if($f instanceof InputfieldMarkup) continue;
				if($f instanceof InputfieldWrapper) continue;
				if($f instanceof InputfieldHidden) continue;
				$typeName = str_replace('Inputfield', '', $f->className());
				$settingLabel = str_replace('|', ' ', "$tabLabel $f->label");
				$label = $settingLabel .
					"| [span.detail] $name [/span] " .
					"| [span.detail] $typeName [/span]";
				if(in_array($name, $alwaysSelected)) {
					$label = str_replace($settingLabel, "[strong]" . $settingLabel . "[/strong]", $label);
					$field->addOption($name, $label, array('checked' => 'checked', 'disabled' => 'disabled'));
					$allowContexts[] = $name;
				} else {
					$field->addOption($name, $label);
					$qty++;
				}
			}
		}

		$field->attr('value', $allowContexts);
		if($qty) $tab->append($field);
	}

	/**
	 * Add Fieldtype and Inputfield custom fields to the form 
	 * 
	 * @param InputfieldForm $form
	 *
	 */
	protected function ___buildEditFormCustom($form) {
		
		$customFields = $this->field->getConfigInputfields();
		$tab = null;
		
		foreach($customFields as $field) {
			/** @var Inputfield|InputfieldWrapper $field */
			// skip over wrappers if they don't have fields in them 
			if($field instanceof InputfieldWrapper && !count($field->children)) continue; 
			$field->attr('class', 'WireTab');
			$field->head = '';
			if($field->name == 'inputfieldConfig') $tab = $field;
			$form->add($field); 
		}
		
		if($tab) $this->buildEditFormFrontEdit($tab);
	}

	/**
	 * Build the front-end editor settings
	 * 
	 * @param InputfieldWrapper $tab
	 * 
	 */
	protected function buildEditFormFrontEdit($tab) {
		
		if(!$this->field) return;
		
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;
		
		$cls = __NAMESPACE__ . "\\FieldtypeFieldsetOpen";
		if($this->field->type instanceof $cls) return;
	
		/** @var InputfieldFieldset $fieldset */
		$fieldset = $modules->get('InputfieldFieldset');
		$fieldset->label = $this->_('Front-end editing');
		$fieldset->icon = 'edit';
		$fieldset->attr('id', 'front-end-editing');
		$fieldset->collapsed = Inputfield::collapsedYes;
		$tab->add($fieldset);
	
		if(!$modules->isInstalled('PageFrontEdit')) {
			/** @var InputfieldMarkup $f */
			$f = $modules->get('InputfieldMarkup');
			$fieldset->add($f);
			/** @var InputfieldButton $button */
			$button = $modules->get('InputfieldButton');
			$button->href = $modules->getModuleInstallUrl('PageFrontEdit');
			$button->value = $this->_('Install');
			$button->icon = 'sign-in';
			$button->target = '_blank';
			$f->description = $this->_('Please install the front-end page editor module to enable front-end editing for fields. After installing, reload and return here for more options.');
			$f->value = $button->render();
			return;
		}
		
		$file = $config->paths('PageFrontEdit') . 'PageFrontEditConfig.php';
		/** @noinspection PhpIncludeInspection */
		include_once($file);
		/** @var PageFrontEditConfig $moduleConfig */
		$moduleConfig = $this->wire(new PageFrontEditConfig());
		$moduleConfig->fieldHelpInputfields($fieldset, $this->field); 
	}

	/**
	 * Add a delete tab to the form
	 * 
	 * @return InputfieldCheckbox
	 *
	 */
	protected function ___buildEditFormDelete() {

		$deleteLabel = $this->_('Delete field');

		/** @var InputfieldCheckbox $field */
		$field = $this->wire()->modules->get('InputfieldCheckbox');
		$field->label = $deleteLabel;
		$field->icon = 'trash-o';
		$field->attr('id+name', "delete"); 
		$field->attr('value', $this->field->id); 
		$field->collapsed = Inputfield::collapsedYes;

		if($this->field->id && $this->field->numFieldgroups() == 0) {
			$field->description = $this->_("This field is not in use and is safe to delete.");
		} else { 
			$field->attr('disabled', 'disabled'); 
			$field->description = $this->_("This field may not be deleted because it is in use by one or more templates."); 
		}
		
		return $field;
	}

	/**
	 * Basic field configuration options: name, type, label, description
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormBasics() {
	
		$modules = $this->wire()->modules;
		$languages = $this->wire()->languages;
		
		$languageFields = array();
		$adminTheme = $this->wire()->adminTheme;
		$legacyTheme = !$adminTheme || in_array($adminTheme->className(), array('AdminThemeDefault', 'AdminThemeReno'));

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'basics');
		$form->attr('class', 'WireTab');
		$form->attr('title', $this->_x('Basics', 'tab'));
		
		/** @var InputfieldPageTitle $field */
		$field = $modules->get('InputfieldPageTitle');
		$field->attr('id+name', 'field_label');
		$field->label = $this->labels['label'];
		$field->attr('value', $this->field->label);
		$field->class .= ' asmListItemDesc'; // for modal to populate parent asmSelect desc field with this value (recognized by jquery.asmselect.js)
		$field->columnWidth = $languages ? 100 : 33;
		$field->icon = 'tag';
		$field->nameField = 'name';
		$field->nameDelimiter = '_';
		$labelField = $field;
		$form->add($field);
		$languageFields[] = $field;

		if($this->fieldgroup) { 	
			// ok
			$field->columnWidth = 100;

		} else {
			/** @var InputfieldName $field */
			$field = $modules->get('InputfieldName');
			$field->attr('name', 'name');
			$field->attr('value', $this->field->name); 
			$field->description = '';
			$field->columnWidth = $languages ? 50 : 33;
			$field->pattern = '^[_a-zA-Z][_a-zA-Z0-9]*$';
			$field->placeholder = 'a-z A-Z 0-9 _';
			$field->icon = 'code';
			$field->attr('required', 'required');
			$nameField = $field;
			$form->add($field); 
			
			$typeField = $this->buildEditFormTypeSelect();
			$form->add($typeField); 
			
			if($legacyTheme) {
				$labelField->columnWidth = 100;
				$nameField->columnWidth = 100;
				$typeField->columnWidth = 100;
			}
		}

		/** @var InputfieldTextarea $field */
		$field = $modules->get('InputfieldTextarea');
		$field->label = $this->_x('Description', 'textarea input'); // Label for the 'field description' textarea input
		$field->attr('name', 'description'); 
		$field->attr('value', $this->field->description); 
		$field->attr('rows', 3); 
		$field->icon = 'align-left';
		$field->description = $this->_('Description appears here and looks like this.'); 
		$field->detail = $this->_("Additional information describing this field and/or instructions on how to enter the content."); // Description for 'field description'
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);
		$languageFields[] = $field;
	
		/** @var InputfieldTextarea $field */
		$field = $modules->get('InputfieldTextarea');
		$field->label = $this->_x('Notes', 'textarea input'); // Label for the 'field description' textarea input
		$field->attr('name', 'notes');
		$field->attr('value', $this->field->notes);
		$field->attr('rows', 3);
		$field->icon = 'sticky-note';
		$field->description = $this->_("Usage notes or additional information that appears beneath the input."); // Description for 'field notes'
		$field->notes = $this->_('Notes appear here and look like this.');
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);
		$languageFields[] = $field;
	
		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText');
		$field->label = $this->_('Label for tab');
		$field->attr('name', 'tabLabel');
		$field->attr('value', (string) $this->field->get('tabLabel'));
		$field->icon = 'tag';
		$field->description = $this->_('If field is displayed in its own tab, optionally specify an alternate tab label if different from the field label.');
		$field->notes = $this->_('The tab label should ideally be very short, like just one word.');
		$field->collapsed = Inputfield::collapsedBlank;
		$field->showIf = 'collapsed=' . Inputfield::collapsedTab . '|' . Inputfield::collapsedTabAjax . '|' . Inputfield::collapsedTabLocked;
		$form->add($field);
		$languageFields[] = $field;

		if($languages) {
			foreach($languageFields as $field) {
				$field->useLanguages = true;
				$name = $field->name;
				if($name == 'field_label') $name = 'label';
				foreach($languages as $language) {
					/** @var Language $language */
					if($language->isDefault()) continue;
					$field->set("value{$language->id}", $this->field->get("$name{$language->id}"));
				}
			}
		}
		
		/** @var InputfieldIcon $field */
		$field = $modules->get("InputfieldIcon");
		$field->attr('name', 'icon');
		$field->attr('value', $this->field->icon);
		$field->icon = 'puzzle-piece';
		$field->label = $this->_('Icon');
		$field->description = $this->_('If you want to associate an icon with the field, select an icon below. Click the "Show all icons" link for visual selection.'); // Description for field tags
		$field->collapsed = Inputfield::collapsedBlank;
		$form->add($field);

		if($this->field->id) {
			/** @var InputfieldMarkup $field */
			$field = $modules->get('InputfieldMarkup');
			$field->attr('name', '_usage_table');
			$field->label = $this->_('Usage');
			$field->icon = 'search';
			$field->collapsed = Inputfield::collapsedYes;
			$field->value = 'value';
			$form->add($field);
		}

		return $form;
	}

	/**
	 * Build the Fieldtype selection for the editor form
	 * 
	 * @return InputfieldSelect
	 * 
	 */
	protected function buildEditFormTypeSelect() {
		
		$modules = $this->wire()->modules;
		$languages = $this->wire()->langauges;
		$fields = $this->wire()->fields;
		$config = $this->wire()->config;
		$isNew = !$this->field || !$this->field->id;
		
		/** @var InputfieldSelect $field */
		$field = $modules->get('InputfieldSelect');
		$field->label = $this->labels['type'];
		$field->attr('name', 'type');
		$field->required = true;
		$field->attr('required', 'required');
		$field->columnWidth = $languages ? 50 : 34;
		$field->icon = 'database';
		if($this->field->type) {
			$field->attr('value', $this->field->type->name);
		} else {
			$field->addOption('', '');
		}

		$fieldtypes = $fields->getCompatibleFieldtypes($this->field);
		
		if(!count($fieldtypes)) {
			$field->addOption($this->field->type->name, $this->field->type->longName);
			return $field;
		}

		$fieldtypeLabels = array();
		foreach($fieldtypes as $fieldtype) {
			/** @var Fieldtype $fieldtype */
			$label = $fieldtype->longName;
			if(isset($fieldtypeLabels[$label])) $label .= " ($fieldtype->name)";
			$fieldtypeLabels[$label] = $fieldtype;
		}
		ksort($fieldtypeLabels);

		$advanced = $config->advanced;
		$fieldsetOptions = array();
		$textOptions = array();
		$numberOptions = array();
		$numberTypes = array('FieldtypeInteger', 'FieldtypeFloat', 'FieldtypeDecimal');
		$typeOptions = array();
		$coreOptions = array();
		$xtraGroups = array();
		$optgroups = array();

		foreach($fieldtypeLabels as $label => $fieldtype) {
			
			if(!$advanced && $fieldtype->isAdvanced() && $this->field->name != 'title') {
				if($field->value != $fieldtype->className()) continue;
			}
			
			if(!$isNew) {
				$typeOptions[$fieldtype->name] = $label;
				continue;
			}
			
			$setups = $fieldtype->getFieldSetups();
			$fieldtypeClass = wireClassName($fieldtype);
			$inOptgroup = '';
			
			if(count($setups)) {
				$setupOptions = array();
				foreach($fieldtype->getFieldSetups() as $setupName => $setupInfo) {
					$setupTitle = isset($setupInfo['title']) ? $setupInfo['title'] : $setupName;
					$setupOptions["$fieldtype->name.$setupName"] = "$setupTitle";
				}
				while(isset($optgroups[$label])) $label .= ' …';
				$optgroups[$label] = $setupOptions;
				$inOptgroup = $label;
			}
				
			if(wireInstanceOf($fieldtype, $numberTypes)) {
				$numberOptions[$fieldtype->name] = $label;
				
			} else if(strpos($fieldtypeClass, 'FieldtypeFieldset') === 0 || wireInstanceOf($fieldtype, 'FieldtypeFieldsetOpen')) {
				$fieldsetOptions[$fieldtype->name] = $label;
				
			} else if($fieldtype instanceof FieldtypeText && !$fieldtype instanceof FieldtypeTextarea) {
				$textOptions[$fieldtype->name] = $label;

			} else if($inOptgroup) {
				// ok, does not need to be dupicated in core or 3rd party options
				
			} else if($modules->getModuleInfoProperty($fieldtype, 'core')) {
				$coreOptions[$fieldtype->name] = $label;
				
			} else {
				$typeOptions[$fieldtype->name] = $label;
			}
			
			if(!$inOptgroup && strpos($label, ':') && preg_match('/^(.+):\s+(.+)$/', $label, $matches)) {
				// look for 'GroupName: FieldtypeLabel'
				$groupTitle = $matches[1];
				$label = $matches[2];
				if(!isset($xtraGroups[$groupTitle])) $xtraGroups[$groupTitle] = array();
				$xtraGroups[$groupTitle][$fieldtype->name] = $label;
			}
		}
		
		if(!$isNew) {
			$field->addOptions($typeOptions);
			return $field;
		}

		// groups indicated by "GroupName: FieldtypeLabel" in module title
		foreach($xtraGroups as $groupLabel => $groupOptions) {
			if(count($groupOptions) < 2) continue;
			foreach($groupOptions as $fieldtypeName => $label) {
				unset($fieldsetOptions[$fieldtypeName]);
				unset($textOptions[$fieldtypeName]);
				unset($typeOptions[$fieldtypeName]);
				unset($coreOptions[$fieldtypeName]);
				unset($numberOptions[$fieldtypeName]);
			}
			$optgroups[$groupLabel] = $groupOptions;
		}

		$groupLabel = $this->_('Fieldset');
		while(isset($optgroups[$groupLabel])) $groupLabel .= ' …';
		$optgroups[$groupLabel] = $fieldsetOptions;

		$groupLabel = $this->_('Text');
		while(isset($optgroups[$groupLabel])) $groupLabel .= ' …';
		$optgroups[$groupLabel] = $textOptions;

		$groupLabel = $this->_('Number');
		while(isset($optgroups[$groupLabel])) $groupLabel .= ' …';
		$optgroups[$groupLabel] = $numberOptions;

		ksort($optgroups);

		if(count($coreOptions)) {
			$groupLabel = $this->_('Other core types');
			while(isset($optgroups[$groupLabel])) $groupLabel .= ' …';
			asort($coreOptions);
			$optgroups[$groupLabel] = $coreOptions;
		}

		if(count($typeOptions)) {
			$groupLabel = $this->_('Other non-core types');
			while(isset($optgroups[$groupLabel])) $groupLabel .= ' …';
			asort($typeOptions);
			$optgroups[$groupLabel] = $typeOptions;
		}

		foreach($optgroups as $label => $optgroupOptions) {
			$field->addOption($label, $optgroupOptions);
		}

		// indicate fieldtypes that are not yet installed
		$uninstalledOptions = array();
		foreach($modules->getInstallable() as $moduleName => $filename) {
			if(strpos($moduleName, 'Fieldtype') !== 0) continue;
			$title = $modules->getModuleInfoProperty($moduleName, 'title');
			$uninstalledOptions[$moduleName] = $title;
			$field->addOption($this->_('Not yet installed'), $uninstalledOptions);
			foreach(array_keys($uninstalledOptions) as $moduleName) {
				$field->setOptionAttributes($moduleName, array('disabled' => 'disabled'));
			}
		}

		// add clone options
		$names = array();
		foreach($fields as $f) {
			/** @var Field $f */
			if(($f->flags & Field::flagSystem) && !$config->advanced) continue;
			if(strpos($f->name, '_END')) continue;
			$names['_' . $f->name] = $f->name;
		}
		$field->addOption($this->_('Clone existing field'), $names);
		
		return $field;
	}

	protected function ___buildEditFormInfo($form) { // legacy name (left for hooks)
		return $this->buildEditFormActions($form);
	}

	/**
	 * Build the 'Info' field shown in the Field Edit form
	 * 
	 * @param InputfieldForm $form Argument added 3.0.202
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormActions(InputfieldForm $form) {
		
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		$fields = $this->wire()->fields;
		$sanitizer = $this->wire()->sanitizer;
		$allTemplates = $this->wire()->templates;
		
		$inTemplates = array();
		$multi = $this->field->type instanceof FieldtypeMulti; 

		/** @var InputfieldWrapper $fieldset */
		$fieldset = $this->wire(new InputfieldWrapper());
		$fieldset->attr('class', 'WireTab');
		$fieldset->attr('id', 'info');
		$fieldset->attr('title', $this->_x('Actions', 'tab'));
	
		foreach($allTemplates as $template) {
			if($template->fieldgroup->hasField($this->field)) {
				$inTemplates[$template->id] = $template;
			}
		}

		/** @var MarkupAdminDataTable $table For display in Basics > Usage */
		$table = $modules->get('MarkupAdminDataTable');
		$table->setEncodeEntities(false);
		
		/** @var InputfieldCheckboxes $field */
		$field = $modules->get('InputfieldCheckboxes');
		$field->attr('name', 'send_templates'); 	
		$field->label = $this->_('Add or remove field from templates'); 
		$field->collapsed = Inputfield::collapsedYes;
		
		if(count($inTemplates)) {
			$field->description = $this->_('This field is in use on the checked templates below.') . ' ';
		} else {
			$field->description = $this->_('This field is not currently in use on any templates.') . ' ';
		}
		
		$field->description .= $this->_('You may quickly add or remove this field from templates by checking or unchecking the relevant boxes and clicking save.'); // Description for template usage
		$field->notes .= $this->_('You will be asked to confirm additions and removals on the next screen after you save.'); 
		$field->table = true;
		$field->icon = 'check-square';
		
		$populatedPagesLabel = $this->_x('Populated pages', 'usage-table-th');
		$rowsOfDataLabel = $this->_x('Rows of data', 'usage-table-th');
		
		$field->thead = 
			$this->labels['name'] . '|' . 
			$this->labels['label'] . '|' . 
			$populatedPagesLabel . 
			($multi ? '|' . $rowsOfDataLabel : ''); 
		
		$numOmitted = 0;
		
		foreach($allTemplates as $template) {
			/** @var Template $template */
			
			$label = $template->name;
			$templateLabel = $template->getLabel();
			$longLabel = str_replace('|', ' ', $templateLabel);
			$numRows = '';
			$numPages = '';
			
			if(isset($inTemplates[$template->id])) {
				// field uses this template
				$numPages = $fields->getNumPages($this->field, array('template' => $template));
				$numRows = $multi ? $fields->getNumRows($this->field, array('template' => $template)) : null;
				$editUrl = "../template/edit?id=$template->id";
			
				$icon = $template->getIcon();
				$tableRowName = $sanitizer->entities($label);
				if($templateLabel != $label) {
					$tableRowLabel = $sanitizer->entities($templateLabel);
				} else {
					$tableRowLabel = "<span class='barely-visible'>$label</span>";	
				}
				if($icon) $tableRowName = wireIconMarkup($icon, 'fw') . ' ' . $tableRowName;
				$row = array("<a href='$editUrl' target='_blank'>$tableRowName</a>", $tableRowLabel, $numPages);
				if($multi) $row[] = $numRows;
				$table->row($row);
				
				$longLabel = "**[$longLabel]($editUrl)**";
				$label = "**$label**";
				
			} else if($template->flags & Template::flagSystem && !$config->advanced) {
				// field does not use template and it is a system template
				$numOmitted++;
				continue;
				
			} else {
				// field does not use template
				if($longLabel === $label) $longLabel = "[span.barely-visible] $longLabel [/span]";
		 	}
			
			$label = "$label|$longLabel|$numPages" . ($multi ? "|$numRows" : "");
			$field->addOption($template->id, $label); 
		}
		
		$field->attr('value', array_keys($inTemplates)); 
		$field->set('_valuePrevious', $field->attr('value')); 
		if($numOmitted) $field->notes .= ' ' . sprintf($this->_('%d system templates are not shown because advanced mode is off.'), $numOmitted); 
		$fieldset->add($field);
	
		// populate usage table from Basics tab
		$tableHeaderRow = array(
			$this->_('Template'),
			$this->_('Label'),
			$populatedPagesLabel
		);
		if($multi) $tableHeaderRow[] = $rowsOfDataLabel;
		$table->headerRow($tableHeaderRow);

		$usage = $form->getChildByName('_usage_table');
		if($usage) {
			if(count($inTemplates)) {
				$usage->value = $table->render();
				$usage->detail = $this->_('See the Actions tab to add/remove from templates.');
			} else {
				$usage->value = '<p>' . $this->_('This field is not currently in use.') . '</p>';
			}
		}
	
		/** @var InputfieldHidden $field */
		$field = $modules->get('InputfieldHidden');
		$field->attr('id+name', '_send_templates_changed'); 
		$field->attr('value', ''); 
		$fieldset->add($field); 
	
		// --------------------------	
	
		/** @var InputfieldText $field */
		$field = $modules->get("InputfieldText");
		$field->attr('id+name', '_clone_field');
		$field->attr('value', '');
		$field->label = $this->_('Duplicate/clone this field?');
		$field->icon = 'copy';
		$field->description = $this->_('To clone this field, enter the name of the new field you wish to create.'); // Description for clone field
		$field->notes = $this->_('Note that you will be editing your cloned copy after submitting this form.'); 
		$field->collapsed = Inputfield::collapsedYes;
		$fieldset->append($field);
		
		// --------------------------	
	
		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', '_optimize');
		$field->label = $this->_('Check field data');
		$field->icon = 'medkit';
		$field->description = $this->_('Check the field for unused data or other possible optimizations. If any issues are found, you will have the opportunity to correct them from the Alert tab after saving.');
		$field->collapsed = Inputfield::collapsedBlank;
		$fieldset->add($field);
		
		$fieldset->add($this->buildEditFormDelete());

		return $fieldset; 
	}

	/**
	 * Build the "Access" tab for the field edit form, or null if not supported for Field
	 * 
	 * @return InputfieldWrapper|null
	 * 
	 */
	protected function buildEditFormAccess() {
		
		if(!$this->allowAccessTab()) return null;	

		$roles = $this->wire()->roles;
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		$adminTheme = $this->wire()->adminTheme;
		$checkboxClass = $adminTheme ? $adminTheme->getClass('input-checkbox') : '';

		/** @var InputfieldWrapper $tab */
		$tab = $this->wire(new InputfieldWrapper());
		$tab->attr('class', 'WireTab');
		$tab->attr('id', 'access');
		$tab->attr('title', $this->_x('Access', 'tab'));

		/** @var InputfieldRadios $f */
		$f = $modules->get('InputfieldRadios');
		$f->attr('name', 'useRoles');
		$f->label = $this->_('Do you want to manage access control for this field?');
		$f->description = $this->_('When enabled, you can limit view and edit access to this field by user role.');
		if($this->field->hasFlag(Field::flagSystem)) {
			$f->notes = $this->_('This is a system field, enabling access control is NOT recommend.');
		}
		$f->icon = 'key';
		$f->addOption(1, $this->labels['yes']);
		$f->addOption(0, $this->labels['no']);
		$f->attr('value', (int) $this->field->useRoles);
		$f->optionColumns = 1;
		$tab->add($f);

		/** @var InputfieldMarkup $f */
		$f = $modules->get("InputfieldMarkup");
		$f->attr('id', '_roles_editor');
		$f->showIf = 'useRoles=1';
		$f->label = $this->_('What roles can view and/or edit this field?');
		$f->description = $this->_('Users must also have page view or edit access on the page where the field exists before these permissions are applicable.'); // Access control description
		$f->notes = $this->_('When no boxes are checked, only superuser can view/edit the contents of the field.');

		/** @var MarkupAdminDataTable $table */
		$table = $modules->get("MarkupAdminDataTable");
		$table->setEncodeEntities(false);
		$table->headerRow(array(
			$this->_x('Role', 'access-thead'),
			$this->_x('View', 'access-thead'),
			$this->_x('Edit', 'access-thead'),
		));
		$checked = " checked='checked'";
		$disabled = " disabled='disabled'";
		$viewRoles = $this->field->viewRoles;
		$editRoles = $this->field->editRoles;
		$guestRole = $roles->get($config->guestUserRolePageID);
		$guestHasView = in_array($guestRole->id, $viewRoles);
	
		foreach($roles as $role) {
			/** @var Role $role */
			if($role->id == $config->superUserRolePageID) continue;
			$label = $role->name;
			$editable = $role->hasPermission('page-edit');
			if($role->id == $guestRole->id) $label .= ' <span class="detail">' . $this->_('(everyone)') . "</span>";
			$table->row(array(
				$label,
				("<input type='checkbox' id='viewRoles_$role->id' class='viewRoles $checkboxClass' name='viewRoles[]' value='$role->id' " .
					(in_array($role->id, $viewRoles) || $guestHasView ? $checked : '') . " />"),
				("<input type='checkbox' id='editRoles_$role->id' class='editRoles $checkboxClass' name='editRoles[]'  value='$role->id' " .
					(in_array($role->id, $editRoles) ? $checked : '') . " " . ($editable ? '' : $disabled) . " />")
			));
		}

		$f->value = $table->render();
		$tab->add($f);

		/** @var InputfieldCheckboxes $f */
		$f = $modules->get('InputfieldCheckboxes');
		$f->attr('name', '_accessFlags');
		$f->label = $this->_('Access toggles');
		$f->addOption(Field::flagAccessEditor, $this->_('Show field in page editor if viewable but not editable (user can see but not change)'));
		$f->addOption(Field::flagAccessAPI, $this->_('Make field value accessible from API even if not viewable (see below)*'));
		$f->showIf = "useRoles=1";
		$value = array();
		if($this->field->flags & Field::flagAccessAPI) $value[] = Field::flagAccessAPI;
		if($this->field->flags & Field::flagAccessEditor) $value[] = Field::flagAccessEditor;
		$f->attr('value', $value);
		$f->notes = '*' . sprintf($this->_('When a field is not viewable (on the front-end with output formatting on), the $page->%s value will be a blank version of the expected type, which prevents the value from being shown.'), $this->field->name) .
			' ' . $this->_('Check the box above to bypass this behavior, making the value always API accessible. This will leave you to do any access checking.') .
			' ' . $this->_('You can check view access from the API for this field like this:') .
			sprintf("\n\n`if(\$page->viewable('%s')) { ... }`", $this->field->name);
		$f->collapsed = Inputfield::collapsedBlank;
		$tab->add($f);

		return $tab;
	}


	/**
	 * Build the 'Advanced' field shown in the Field Edit form
	 * 
	 * @return InputfieldWrapper
	 *
	 */
	protected function ___buildEditFormAdvanced() {

		if($this->field->type) {
			$form = $this->field->type->getConfigAdvancedInputfields($this->field); 
		} else {
			$form = $this->wire(new InputfieldWrapper());
		} 
		
		/** @var InputfieldWrapper $form */
		$form->attr('id', 'advanced');
		$form->attr('class', 'WireTab');
		$form->attr('title', $this->_x('Advanced', 'tab'));

		/** @var InputfieldTextTags $field */
		$field = $this->wire()->modules->getInstall('InputfieldTextTags');
		$field->attr('name', 'tags');
		$field->allowUserTags = true;
		$field->setTagsList($this->wire()->fields->getTags());
		$field->attr('value', $this->field->getTags(true));
		$field->icon = 'tags';
		$field->label = $this->labels['tags'];
		$field->description = 
			$this->_('If you want to visually group this field with others in the fields list, enter a one-word tag. Enter the same tag on other fields you want to group with. To specify multiple tags, separate each with a space. Use of tags may be worthwhile if your site has a large number of fields.'); // Description for field tags
		$field->notes = 
			$this->_('Each tag must be one word (underscores are okay).') . ' ' .
			'[' . $this->labels['manage-tags'] . '](./tags/)';
		$field->collapsed = Inputfield::collapsedBlank;
		$form->prepend($field);

		return $form;
	}

	/**
	 * Save the results of a Field Edit
	 *
	 */
	public function ___executeSave() {

		$input = $this->wire()->input;
		$session = $this->wire()->session; 
		$sanitizer = $this->wire()->sanitizer;
		$form = $this->buildEditForm();
	
		if($this->input->get('reloadInputfieldAjax') === 'Inputfield_overrides_table') {
			return $form->render();
		}
	
		if(!$form->isSubmitted('submit_save_field')) {
			$session->redirect("./"); 
		}

		if($this->fieldgroup) {
			$this->saveContext();
			return '';
		}
		
		$isNew = !$this->field->id;
		$delete = (int) $input->post('delete');

		if($delete && $delete === (int) $this->field->id && $this->field->numFieldgroups() == 0) {
			$session->CSRF->validate();
			$session->message($this->_('Deleted field') . " - {$this->field->name}"); // Message after deleting a field, followed by field name
			$this->fields->delete($this->field); 
			$this->fieldDeleted($this->field); 
			$session->location('./'); 
			return ''; 
		}

		if($isNew) {
			// when adding new field, make sure name is valid
			$name = $input->post('name');
			$name = $sanitizer->fieldName($name);
			if(!$this->isAllowedName($name)) $session->location('./add');
			$type = (string) $input->post('type');
			// check for clone option on create
			if(strpos($type, '_') === 0) {
				// clone existing field option is existing field name with "_" prepended
				$cloneFieldName = $sanitizer->fieldName(substr($type, 1));
				$cloneField = $this->wire()->fields->get($cloneFieldName);
				if($cloneField) {
					$this->field = $this->cloneField($cloneField, $name); 
					if(!$this->field) throw new WireException("Error cloning $cloneFieldName");
				}
			}
		}

		try {
			$form->processInput($input->post); 
			$this->saveInputfields($this->form); 
		} catch(\Exception $e) {
			$this->error($e->getMessage()); 
		}
		
		$errors = array();

		if(!$this->field->name) {
			$errors[] = $this->_("Field name is required"); 
			
		} else if(!$this->field->type) {
			$errors[] = $this->_("Field type is required"); 
			
		} else if($isNew) {
			try { 
				$this->field->save();
				$session->message($this->_('Added Field') . " - {$this->field->name}"); 
				$this->fieldAdded($this->field); 
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
				if(!$this->field->id) $session->location('./'); 
			}
			$redirectURL = "edit?id={$this->field->id}";
			if(!$input->get('modal')) $redirectURL .= "#fieldtypeConfig";
			$session->location($redirectURL);
				
		} else {
			
			$removeKeys = $input->post('_remove_keys');
			if($removeKeys && count($removeKeys)) {
				$_removeKeys = $session->get($this, '_remove_keys');
				foreach($removeKeys as $xkey) {
					if(!in_array($xkey, $_removeKeys)) continue; // validate via session
					$this->field->remove($xkey);
					$this->message(sprintf($this->_('Removed unused property: %s'), $xkey));
				}
			}
			
			if(((int) $input->post('_remove_rows')) === (int) $this->field->id) {
				$table = $this->field->getTable();
				$sql = "DELETE $table FROM $table LEFT JOIN pages ON $table.pages_id=pages.id WHERE pages.id IS NULL ";
				$templatesWithoutField = $this->getTemplatesWithoutField($this->field);
				if(count($templatesWithoutField)) {
					$sql .= "OR pages.templates_id IN(" . implode(',', $templatesWithoutField) . ")";
				}
				$query = $this->wire()->database->prepare($sql);
				$query->execute();
				$cnt = $query->rowCount();
				if($cnt) $this->message($this->field->name . ": " . sprintf($this->_('Deleted %d orphaned rows'), $cnt));
			}

			$session->remove($this, 'optimize');
			if($input->post('_optimize')) $session->set($this, 'optimize', $this->field->id); 
		
			try { 
				$this->field->save();
				$this->message($this->_('Saved Field') . " - {$this->field->name}"); 
				$this->saveRemoveOverrides();
				$this->fieldSaved($this->field); 
				$select = $form->getChildByName('type'); 
				if($this->field->type->className() != $select->value) {
					$session->location("changeType?id={$this->field->id}&type={$select->value}"); 
				}	
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
			}
			
			$cloneField = $form->getChildByName('_clone_field');
			$cloneName = $cloneField ? $cloneField->attr('value') : ''; 
			if($cloneName) {
				$clone = $this->cloneField($this->field, $cloneName);
				if($clone && $clone->id) {
					try {
						$this->wire()->fields->save($clone);
						$this->fieldAdded($clone);
						$session->message($this->_('You are now editing the field you cloned.'));
						$session->location("./edit?id=$clone->id#basics");
					} catch(\Exception $e) {
						$errors[] = $e->getMessage();
					}
				} else {
					$errors[] = ($this->_("Error creating clone of this field") . " - {$this->field->name}");
				}
			}
			
			if(count($errors)) {
				foreach($errors as $error) {
					$this->error($error);
				}
			}

			if($input->post('_send_templates_changed') === 'changed') {
				$sendTemplates = $form->getChildByName('send_templates');
				$value = $sendTemplates->attr('value');
				$valuePrevious = $sendTemplates->get('_valuePrevious');
				if($value != $valuePrevious) {
					$templateIDs = array();
					foreach($value as $templateID) {
						if(in_array($templateID, $valuePrevious)) continue;
						$templateIDs[] = (int) $templateID; // add
					}
					foreach($valuePrevious as $templateID) {
						if(in_array($templateID, $value)) continue;
						$templateIDs[] = $templateID * -1;  // remove
					}
					if(count($templateIDs)) {
						$session->location("./send-templates?id={$this->field->id}&templates=" . implode(',', $templateIDs));
					}
				}
			}
		}
	
		if($this->listAfterSave) {
			$session->location("./");
		} else {
			$url = "edit?id={$this->field->id}";
			if($input->post("_optimize")) $url .= '#alert';
			$session->location($url);
		}
		
		return '';
	}

	/**
	 * Clone $cloneField to create a new field with given name
	 * 
	 * @param Field $cloneField
	 * @param string $name Name of new field
	 * @return bool|Field
	 * 
	 */
	protected function cloneField(Field $cloneField, $name) {
		if(!$this->isAllowedName($name)) return false;
		
		$fields = $this->wire()->fields;
		if($fields->get($name)) return false;
		
		$field = $fields->clone($cloneField, $name);
		if(!$field) return false;

		$field->label = $field->label . ' ' . $this->_('(copy)');
		$this->message($this->_('Cloned Field') . " - $cloneField->name => $field->name");

		if($field->type instanceof FieldtypeFieldsetOpen && strpos($cloneField->name, '_END') === false) {
			// handle a cloned fieldset by also cloning its closer
			$closeFieldset = $fields->get($cloneField->name . '_END'); 
			$closeFieldsetCopy = null;
			if($closeFieldset) {
				$closeName = $name . '_END';
				$closeFieldsetCopy = $fields->get($closeName);
				if(!$closeFieldsetCopy) $closeFieldsetCopy = $fields->clone($closeFieldset, $closeName);
				if($closeFieldsetCopy) $field->set('closeFieldID', $closeFieldsetCopy->id);
			}
			if(!$closeFieldsetCopy) {
				$field->set('closeFieldID', null);
			}
		}
		
		return $field;
	}

	/**
	 * Is the given field name allowed to use for a new or cloned field
	 * 
	 * @param string $name
	 * @return bool
	 * 
	 */
	protected function isAllowedName($name) {
		$fields = $this->wire()->fields;
		$languages = $this->wire()->languages;
		$_name = $name;
		$name = $this->wire()->sanitizer->fieldName($name);
		$allowed = false;
		$okay = array('files', 'datetime'); // field names that are okay, even if they collide with something else
		$error = '';
		
		if(empty($name)) {
			$error = $this->_('Field name is empty'); 
		} else if($name !== $_name) {
			$error = $this->_('Field names may only contain ASCII letters, digits or underscore.');
		} else if($fields->get($name)) {
			$error = sprintf($this->_('Field name "%s" is already in use'), $name);
		} else if(($this->wire($name) || $fields->isNative($name)) && !in_array($name, $okay)) {
			$error = sprintf($this->_('Field name "%s" is a reserved word'), $name);
		} else if(preg_match('/^(_|\d)/', $name)) {
			$error = $this->_('Field names may not begin with "_" or digits');
		} else if($languages && $languages->get($name)->id) {
			$error = $this->_('Field name may not be the same as a Language name');
		} else {
			$allowed = true; 
		}
		
		if($error) {
			if($this->form && $field = $this->form->getChildByName('name')) {
				$field->error($error); 
			} else {
				$this->error($error); 
			}
		}
		
		return $allowed; 
	}

	/**
	 * Compare two diff context arrays and return the differences
	 * 
	 * @param array $a1
	 * @param array $a2
	 * @return array
	 * 
	 */
	protected function diffContextArray(array $a1, array $a2) {
		$diff = array();
		foreach($a1 as $k => $v) {
			if(is_array($v)) {
				if(!isset($a2[$k]) || !is_array($a2[$k])) {
					$diff[$k] = $v;
				} else {
					$_diff = $this->diffContextArray($v, $a2[$k]);
					if(count($_diff)) $diff[$k] = $_diff;
				}
			} else if(!array_key_exists($k, $a2)) {
				$diff[$k] = $v;
			} else if($a2[$k] !== $v) {
				$diff[$k] = $v;
			}
		}
		return $diff;
	}

	/**
	 * Save field in the context of a fieldgroup only
	 *
	 */
	protected function ___saveContext() {
		try {
			$oldContextArray = $this->fieldgroup->getFieldContextArray($this->field->id); 
			$this->form->processInput($this->input->post); 
			$this->saveInputfields($this->form); 
			$this->wire()->fields->saveFieldgroupContext($this->field, $this->fieldgroup, $this->contextNamespace); 
			$this->saveRemoveOverrides();
			$newContextArray = $this->fieldgroup->getFieldContextArray($this->field->id); 
			$diffContextArray = $this->diffContextArray($newContextArray, $oldContextArray); 
			if(count($diffContextArray)) {
				$this->fieldChangedContext($this->field, $this->fieldgroup, $diffContextArray); 
			}
		} catch(\Exception $e) {
			$this->error($e->getMessage()); 
		}
		$this->wire()->session->location("edit" . 
			"?id={$this->field->id}" . 
			"&fieldgroup_id={$this->fieldgroup->id}" . 
			"&context_namespace=$this->contextNamespace" . 
			"&context_label=$this->contextLabel"
		); 
	}

	/**
	 * Save the results of a Field Edit, field by field
	 * 
	 * @param InputfieldWrapper $wrapper
	 *
	 */
	protected function saveInputfields(InputfieldWrapper $wrapper) {

		$languages = $this->wire()->languages;
		$sanitizer = $this->wire()->sanitizer;
		$input = $this->wire()->input;
		$config = $this->wire()->config;
	
		foreach($wrapper->children() as $inputfield) {
			/** @var Inputfield|InputfieldWrapper $inputfield */

			if($inputfield instanceof InputfieldWrapper && count($inputfield->children())) {
				$this->saveInputfields($inputfield); 
				continue; 
			}

			$name = $inputfield->name; 
			$value = $inputfield->value; 

			if(!$name || $inputfield instanceof InputfieldSubmit || $inputfield instanceof InputfieldMarkup) continue; 

			// see /core/Fieldtype.php for the inputfields that initiate the autojoin and global flags
			if($name === 'autojoin') {
				if(!$input->post('autojoin')) {
					$this->field->flags = $this->field->flags & ~Field::flagAutojoin;
				} else {
					$this->field->flags = $this->field->flags | Field::flagAutojoin;
				}
				continue; 

			} else if($name === 'global') {
				if(!$input->post('global')) {
					$this->field->flags = $this->field->flags & ~Field::flagGlobal;
				} else {
					$this->field->flags = $this->field->flags | Field::flagGlobal;
				}
				continue; 
			} else if($name === 'system' && $config->advanced) {
				if(!$input->post('system')) {
					$this->field->flags = $this->field->flags | Field::flagSystemOverride;
					$this->field->flags = $this->field->flags & ~Field::flagSystem;
					$this->field->flags = $this->field->flags & ~Field::flagSystemOverride;
				} else {
					$this->field->flags = $this->field->flags | Field::flagSystem;
				}
				continue; 
			} else if($name === 'permanent' && $config->advanced) {
				if(!$input->post('permanent')) {
					$this->field->flags = $this->field->flags & ~Field::flagPermanent;
				} else {
					$this->field->flags = $this->field->flags | Field::flagPermanent;
				}
				continue; 
			}

			if($name === 'type' && $this->field->id) continue; // skip this change for existing fields
			if($name === 'type' && strpos($value, '_') === 0) continue; // skip clone type
			if($name === 'delete') continue; 
			if($name === 'fieldgroup_id') continue; 
			if($name === 'field_label') $name = 'label';
			if($name === 'id' && $this->field->id) continue;
			if($name === 'send_templates') continue;
			if($this->field->get('send_templates')) $this->field->__unset('send_templates'); // value was previously getting stored
			if($name === 'useRoles') $value = (bool) ((int) $value);
		
			// if adding new field or existing name has changed, check that its an allowed name
			if($name === 'name' && (!$this->field->id || $value !== $this->field->name)) {
				if(!$this->isAllowedName($value)) continue; 
			}
			
			if(($name === 'showIf' || $name === 'requiredIf') && strlen($value)) {
				$this->checkInputfieldDependencySetting($inputfield);
			}
			
			if($name === 'tags') {
				$value = $sanitizer->words($value);
			}

			$this->field->set($name, $value); 

			// account for languages, if used
			if($languages && $inputfield->getSetting('useLanguages')) {
				foreach($languages as $language) {
					$value = $inputfield->get("value$language->id");
					$this->field->set($name . $language->id, $value); 
				}
			}
		}
		
		if($this->field->useRoles) {
			// access control active for this field
			$viewRoles = $input->post('viewRoles');
			$viewRoles = empty($viewRoles) ? array() : $sanitizer->intArray($viewRoles);
			$this->field->viewRoles = $viewRoles;
			
			$editRoles = $input->post('editRoles');
			$editRoles = empty($editRoles) ? array() : $sanitizer->intArray($editRoles);
			$this->field->editRoles = $editRoles;
			
			$accessFlags = $input->post('_accessFlags');
			if(!is_array($accessFlags)) $accessFlags = array();
			$accessFlags = empty($accessFlags) ? array() : $sanitizer->intArray($accessFlags);
			
			$flags = $this->field->flags;
			if(in_array(Field::flagAccessAPI, $accessFlags)) {
				$flags = $flags | Field::flagAccessAPI;
			} else {
				$flags = $flags & ~Field::flagAccessAPI;
			}
			if(in_array(Field::flagAccessEditor, $accessFlags)) {
				$flags = $flags | Field::flagAccessEditor;
			}  else {
				$flags = $flags & ~Field::flagAccessEditor;
			}
			if($flags != $this->field->flags) $this->field->flags = $flags;
		}
	}

	/**
	 * Check a showIf or requiredIf setting for potential problems
	 * 
	 * @param Inputfield $inputfield Inputfield containing the setting (showIf/requiredIf)
	 * 
	 */
	protected function checkInputfieldDependencySetting(Inputfield $inputfield) {
		$fields = $this->wire()->fields;
		
		$label = sprintf($this->_('Error in setting “%s”'), $inputfield->getSetting('label'));
		$value = $inputfield->attr('value');
		$valueLabel = ' ' . sprintf($this->_('(you specified “%s”)'), $value);
		
		if(empty($value)) return;
		
		try {
			$selectors = new Selectors($value);
			$this->wire($selectors);
		} catch(\Exception $e) {
			$this->error("$label - " . $this->_('Unable to validate') . $valueLabel); 
			return;
		}
		
		foreach($selectors as $selector) {
			foreach($selector->fields() as $f) {
				if(strpos($f, '.')) continue;
				if(!strlen($selector->value())) continue;
				$f = $fields->get($f); /** @var Field $f */
				if(!$f || !$f->type instanceof FieldtypePage) continue;
				$v = implode('', $selector->values());
				if(ctype_digit("$v")) continue; // validates
				$this->error("$label - " . $this->_('This will not work because values must be page IDs') . $valueLabel);
			}
		}
	}

	/**
	 * Saves the submitted checkboxes from the "Overrides" tab
	 *
	 */
	protected function saveRemoveOverrides() {

		$removeContext = $this->wire()->input->post('_remove_context');
		if(empty($removeContext)) return;

		$contextArrays = array();
		$fieldgroups = array();

		foreach($removeContext as $value) {
			// FYI: "<input type='checkbox' name='_remove_context[]' value='$fieldgroup->id:$key' />"

			$ns = '';
			list($fieldgroupID, $property) = explode(':', $value);
			$fieldgroupID = (int) $fieldgroupID;

			// check for namespace (when there is no $this->contextNamespace)
			if(strpos($property, 'NS_') === 0 && strpos($property, '.')) {
				list($ns, $property) = explode('.', $property, 2);
				if(strpos($property, '.') !== false) list($property,) = explode('.', $property, 2);
			}

			if(isset($fieldgroups[$fieldgroupID])) {
				$fieldgroup = $fieldgroups[$fieldgroupID];
			} else {
				$fieldgroup = $this->wire()->fieldgroups->get((int) $fieldgroupID);
				if(!$fieldgroup) continue;
				$fieldgroups[$fieldgroup->id] = $fieldgroup;
			}
			
			/** @var Fieldgroup $fieldgroup */

			if(isset($contextArrays[$fieldgroup->id])) {
				// use previously loaded version
				$contextArray = $contextArrays[$fieldgroup->id];
			} else {
				$contextArray = $fieldgroup->getFieldContextArray($this->field->id);
			}

			if($ns && isset($contextArray[$ns])) {
				// narrrow in on namespace portion
				$context = &$contextArray[$ns];
			} else {
				$context = &$contextArray;
			}

			if(strpos($property, 'flagsAdd-') === 0 || strpos($property, 'flagsDel-') === 0) {
				// special handling of flags bitmask removals
				list($flagType, $flag) = explode('-', $property);
				$flag = (int) $flag;
				if(isset($context[$flagType])) $context[$flagType] = $context[$flagType] & ~$flag;
			} else {
				// remove property
				unset($context[$property]);
			}

			$this->message($this->_('Removed context override') . " (template=$fieldgroup->name, property=$property)");

			// cache for if this comes up in another iteration
			$contextArrays[$fieldgroup->id] = $contextArray;
		}

		foreach($contextArrays as $fieldgroupID => $contextArray) {
			$fieldgroup = $fieldgroups[$fieldgroupID];
			$fieldgroup->setFieldContextArray($this->field->id, $contextArray);
			$fieldgroup->saveContext();
		}
	}

	/**	
	 * Executed when a field type change is requested and provides an informative confirmation form
	 * 
	 * @return string
	 *
	 */
	public function ___executeChangeType() {
		
		$input = $this->wire()->input;
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;
		$sanitizer = $this->wire()->sanitizer;

		$this->buildEditForm();

		$this->headline(sprintf($this->_('Change type for field: %s'), $this->field->name)); // Page headline when changing type
		$this->breadcrumb('./', $this->labels['fields']);
		$this->breadcrumb("./edit?id={$this->field->id}", $this->field->name); 

		if(!$input->get('type')) $session->location('./'); 
		$newType = $sanitizer->name($input->get('type')); 
		$newType = $this->wire()->fieldtypes->get($newType); 
		if(!$newType) $session->location('./'); 

		/** @var InputfieldForm $form */
		$form = $modules->get("InputfieldForm"); 
		$form->attr('method', 'post');
		$form->attr('action', 'saveChangeType'); 
		$form->head = sprintf($this->_('Change field type from "%1$s" to "%2$s"'), $this->field->type->longName, $newType->longName);  
		$form->description = $this->_("Please note that changing the field type alters the database schema. If the new fieldtype is not compatible with the old, or if it contains a significantly different schema, it is possible for data loss to occur. As a result, you are advised to backup the database before completing a field type change."); // Change field type description

		/** @var InputfieldCheckbox $f */
		$f = $modules->get("InputfieldCheckbox"); 
		$f->attr('name', 'confirm_type'); 
		$f->attr('value', $newType->className()); 
		$f->label = $this->_("Confirm field type change");
		$f->description = $this->_("If you are sure you want to change the field type, check the box below and submit this form."); // Confirm change description
		$form->append($f);
	
		/** @var InputfieldCheckbox $f */
		$f = $modules->get("InputfieldCheckbox");
		$f->attr('name', 'keep_settings');
		$f->label = $this->_('Keep field settings?'); 
		$f->description = $this->_('Check this box to retain all the custom settings for this field (from the Details and Input tabs). This is desirable if the new field type has the same or similar configuration properties to the old field type. However, it can also result in unnecessary or redundant configuration data taking up space in the field. You can always analyze this later from: Advanced > Check field data.'); // Keep field settings description
		$f->attr('checked', 'checked'); 
		$f->showIf = 'confirm_type=' . $newType->className();
		$form->append($f); 

		/** @var InputfieldHidden $f */
		$f = $modules->get("InputfieldHidden"); 	
		$f->attr('name', 'id'); 
		$f->attr('value', $this->field->id); 
		$form->append($f); 	

		/** @var InputfieldSubmit $field */
		$field = $modules->get('InputfieldSubmit');
		$field->attr('name', 'submit_change_field_type'); 
		$form->append($field); 
	
		return $form->render();	
	}

	/**
	 * Save a changed field type
	 *
	 */
	public function ___executeSaveChangeType() {
		
		$input = $this->wire()->input;
		$session = $this->wire()->session;
		
		$this->buildEditForm();
		
		$confirmType = $input->post->name('confirm_type');

		if(!$this->field || !$confirmType) {
			$this->message($this->_('Field type change aborted')); 
			$session->location('./'); 
		}

		$fieldtype = $this->wire()->fieldtypes->get($confirmType);
		
		if($fieldtype) {
			$session->CSRF->validate();
			$this->message($this->_('Field type changed')); 
			$this->wire()->fields->addHookBefore('changeFieldtype', $this, 'hookFieldsChangeFieldtype'); 
			$this->field->type = $fieldtype; 
			$this->field->save();
			$this->fieldChangedType($this->field); 
		}
	
		$session->location("edit?id={$this->field->id}"); 	
	}

	/**
	 * Hook to before(Fields::changeFieldtype)
	 * 
	 * @param HookEvent $event
	 * 
	 */
	public function hookFieldsChangeFieldtype(HookEvent $event) {
		// FYI: $field = $event->arguments(0); $keepSettings = $event->arguments(1); 
		if($this->wire()->input->post('keep_settings')) {
			$event->arguments(1, true); 
		}
	}

	/**
	 * Execute import 
	 * 
	 * @return string
	 * @throws WireException if given invalid import data
	 *
	 */
	public function ___executeImport() {
		
		$this->headline($this->labels['import']);
		$this->breadcrumb('../', $this->moduleInfo['title']);
		
		require(dirname(__FILE__) . '/ProcessFieldExportImport.php');
		
		/** @var ProcessFieldExportImport $o */
		$o = $this->wire(new ProcessFieldExportImport());
		$form = $o->buildImport();
		
		return $form->render();
	}

	/**
	 * Execute export
	 * 
	 * @return string
	 *
	 */
	public function ___executeExport() {
		
		$this->headline($this->labels['export']);
		$this->breadcrumb('../', $this->moduleInfo['title']); 
		
		require(dirname(__FILE__) . '/ProcessFieldExportImport.php'); 
		/** @var ProcessFieldExportImport $o */
		$o = $this->wire(new ProcessFieldExportImport());
		$form = $o->buildExport();
		
		return $form->render();
	}

	/**
	 * Execute send field to template(s)
	 *
	 * @return string
	 * @throws WireException
	 *
	 */
	public function ___executeSendTemplates() {

		$templates = $this->wire()->templates;
		$modules = $this->wire()->modules;
		$input = $this->wire()->input;

		if(!$this->field || !$this->field->id) throw new WireException('No field specified');
		$templateIDs = $input->get('templates');
		if(!strlen($templateIDs)) throw new WireException("No templates specified"); 
		$templateIDs = explode(',', $templateIDs);
	
		/** @var InputfieldForm $form */
		$form = $modules->get('InputfieldForm'); 
		$form->attr('action', "./send-templates-save?id={$this->field->id}");
		
		$fields = array($this->field); 
		if($this->field->type instanceof FieldtypeFieldsetOpen) {
			$fieldsetClose = $this->wire()->fields->get($this->field->name . '_END'); 
			if($fieldsetClose) $fields[] = $fieldsetClose;
		}

		foreach($templateIDs as $templateID) {
			
			$templateID = (int) $templateID; 
			if(!$templateID) continue; 
			
			$template = $templates->get(abs($templateID)); 
			if(!$template) continue; 
			
			if($templateID < 0) {
				// remove from template
			
				/** @var InputfieldCheckbox $f */	
				$f = $modules->get('InputfieldCheckbox'); 
				$f->attr('name', "remove_{$this->field->id}_template_$template->id"); 
				$f->attr('value', $this->field->id); 
				$f->icon = 'minus-circle';
				$f->label = sprintf($this->_('Remove field "%1$s" from template "%2$s"'), $this->field->name, $template->name);
				$f->label2 = sprintf($this->_('Confirm removal of field "%1$s" from template "%2$s"'), $this->field->name, $template->name); 
				$numRows = $this->wire()->fields->getNumRows($this->field, array('template' => $template));
				if($numRows) {
					$f->description = $this->_('WARNING: This will result in data associated with the field being permanently deleted.') . ' '; // Warning for field deletion
					$f->notes = sprintf($this->_n('This will also delete %d row of data.', 'This will also delete %d rows of data.', $numRows), $numRows);  // Notes for field deletion
					if($numRows > 100) $f->notes .= ' ' . $this->_('After you submit, be patient as it may take some time for this operation to complete.'); // Additional notes for large quantity field deletion
				} else {
					$f->description = '';
					$f->notes = $this->_('There do not appear to be any rows of data associated with this field/template combination');
				}
				$f->description .= $this->_('Please check the box to confirm.');
				$form->add($f); 

			} else {
				// add to template

				foreach($fields as $field) {
				
					/** @var InputfieldSelect $f */
					$f = $modules->get('InputfieldSelect');
					$f->attr('name', "add_{$field->id}_template_$templateID");
					$f->label = sprintf($this->_('Template: %s'), $template->name);
					$f->label = sprintf($this->_('Add field "%1$s" to template "%2$s"'), $field->name, $template->name);
					$f->description = sprintf($this->_('Where do you want to add "%s?"'), $field->name);
					$f->icon = 'plus-circle';
					$beforeLabel = $this->_('Before %s');
					$afterLabel = $this->_('After %s');
					$value = 0;
					if(!count($template->fieldgroup)) {
						$f->addOption('-', $this->_('Add as first field'));
						$value = '-';
					}
					$f->addOption(0, $this->_('Do not add'));

					foreach($template->fieldgroup as $_field) { // overwrite of $field is OK
						if(strpos((string) $_field->type, 'FieldtypeFieldset') === 0) continue;
						$f->addOption($_field->name, array(
							"-$_field->id" => sprintf($beforeLabel, $_field->name),
							"$_field->id"  => sprintf($afterLabel, $_field->name)
						));
						$value = "$_field->id";
					}

					$f->attr('value', $value);
					$form->add($f); 
				}
			}
		}
	
		/** @var InputfieldSubmit $f */
		$f = $modules->get('InputfieldSubmit');
		$form->add($f); 
		
		$this->headline($this->_('Add or remove from template(s)')); 
		$this->breadcrumb("./edit?id={$this->field->id}", $this->field->name); 
		$out = "<h2>" . sprintf($this->_('Please review and adjust or confirm your changes'), $this->field->name) . "</h2>";
		$out .= $form->render();
		
		return $out; 
	}

	/**
	 * Process the form from executeSendTemplates and redirect back to editor
	 *
	 */
	public function ___executeSendTemplatesSave() {
		
		if(!$this->field || !$this->field->id) throw new WireException('No field specified');
		
		$session = $this->wire()->session;
		$session->CSRF->validate();
		
		$input = $this->wire()->input;	
		$templates = $this->wire()->templates;
		
		$changedTemplates = array();
		$isFieldset = false;
		$fields = array($this->field);
		if($this->field->type instanceof FieldtypeFieldsetOpen) {
			$fieldsetClose = $this->wire()->fields->get($this->field->name . '_END');
			if($fieldsetClose) {
				$fields[] = $fieldsetClose;
				$isFieldset = true; 
			}
		}
		
		// first handle additions, since it's possible for removals to take a long time or remote chance of timeout
		foreach($templates as $template) {
			$templateChanged = false;
	
			foreach($fields as $key => $field) {
					
				// usually just 1 element in $fields, except when $field is a FieldtypeFieldsetOpen in which case 
				// there is also the corresponding FieldtypeFieldsetClose
				$isFieldsetEnd = $isFieldset && $key > 0;
				
				$addFieldID = $input->post("add_{$field->id}_template_$template->id");
				if(!$addFieldID) continue;
				if(!$this->allowFieldInTemplate($field, $template)) continue;
				if($isFieldsetEnd && $template->fieldgroup->hasField($field)) continue;
				if($addFieldID === '-' && !$isFieldsetEnd) {
					// add as first field
					$template->fieldgroup->add($field);
					continue;
				}
				
				$before = substr($addFieldID, 0, 1) == '-';
				$addFieldID = (int) ltrim($addFieldID, '-');
				$fieldNames = array();

				foreach($template->fieldgroup as $f) {
					$fieldName = $f->name;
					$fieldNames[$fieldName] = $fieldName;
					
					if($f->id != $addFieldID) continue;
					
					if($isFieldsetEnd && !isset($fieldNames[$fields[0]->name])) {
						// force to insert after opening fieldset
						$before = false;
						$addFieldID = $fields[0]->id;
						continue;
					}

					if($before) {
						$template->fieldgroup->insertBefore($field, $f);
						$this->message(sprintf(
							$this->_('Added field "%1$s" to template "%2$s" before "%3$s"'),
							$field->name, $template->name, $f->name
						));
					} else {
						$template->fieldgroup->insertAfter($field, $f);
						$changedTemplates[$template->id] = $template;
						$this->message(sprintf(
							$this->_('Added field "%1$s" to template "%2$s" after "%3$s"'),
							$field->name, $template->name, $f->name
						));
					}
					$templateChanged = true;
				}
			}
			
			if($templateChanged) {
				$template->fieldgroup->save();
				$template->save();
			}
		}
		
		// next handle removals
		foreach($templates as $template) {
			$removeFieldID = (int) $input->post("remove_{$this->field->id}_template_$template->id");
			$templateChanged = false;
			if($removeFieldID && $removeFieldID === (int) $this->field->id) {
				$template->fieldgroup->remove($this->field);
				if($this->field->type instanceof FieldtypeFieldsetOpen) {
					$fieldsetClose = $this->wire()->fields->get($this->field->name . '_END'); 
					if($fieldsetClose) $template->fieldgroup->remove($fieldsetClose); 
				}
				try {
					$template->fieldgroup->save();
					$templateChanged = true;
					$this->message(sprintf(
						$this->_('Removed field "%1$s" from template "%2$s"'), 
						$this->field->name, $template->name
					));
				} catch(\Exception $e) {
					$this->error(
						sprintf(
							$this->_('Error removing "%1$s" from template "%2$s"'), 
							$this->field->name, $template->name
						) . ' - ' . $e->getMessage(), 
						Notice::log
					); 
				}
			}
			if($templateChanged) $template->save();
		}
		
		$session->location("./edit?id={$this->field->id}"); 
	}

	/**
	 * Return array of human readable changes as a result of field context
	 *
	 * @param InputfieldWrapper $form
	 * @param Field $field
	 * @param Fieldgroup $fieldgroup
	 * @return array
	 *
	 */
	protected function getContextChanges(InputfieldWrapper $form, Field $field, Fieldgroup $fieldgroup) {

		$contextArray = $fieldgroup->getFieldContextArray($field->id, $this->contextNamespace);
		if(!count($contextArray)) return array();

		$fieldOriginal = $this->wire()->fields->get($field->id);
		$changes = array();
		// $isInContext = $this->fieldgroup || $this->contextNamespace; 
		
		$roles = $this->wire()->roles;
		$languages = $this->wire()->languages;

		$labels = array(
			'on' => $this->_('On'),
			'off' => $this->_('Off'),
			'all' => $this->_('All'), 
			'blank' => $this->_('[blank]'),
			'collapsed' => $this->_('Field presentation/visibility'),
			'viewRoles' => $this->_('Roles allowed to view this field'),
			'editRoles' => $this->_('Roles allowed to edit this field'),
			'flagsAddAccess' => $this->_('Enable access control'),
			'flagsDelAccess' => $this->_('Remove access control'),
			'flagsAddAccessAPI' => $this->_('Make field value API accessible even when not viewable'),
			'flagsDelAccessAPI' => $this->_('Make field value NOT API accessible when not viewable'),
			'flagsAddAccessEditor' => $this->_('Show in page editor even if not editable'),
			'flagsDelAccessEditor' => $this->_('Hide in page editor when not editable'),
		);

		$flagsAddLookup = array(
			Field::flagAccess => 'flagsAddAccess',
			Field::flagAccessAPI =>'flagsAddAccessAPI',
			Field::flagAccessEditor =>'flagsAddAccessEditor',
		);
		$flagsDelLookup = array(
			Field::flagAccess => 'flagsDelAccess',
			Field::flagAccessAPI => 'flagsDelAccessAPI',
			Field::flagAccessEditor =>'flagsDelAccessEditor',
		);
	
		// flatten namespaced to follow same formats others, but with key/name as NS_foo.bar
		foreach($contextArray as $key => $value) {
			if(strpos($key, 'NS_') === 0 && is_array($value)) {
				foreach($value as $k => $v) {
					$contextArray["$key.$k"] = $v;
					unset($contextArray[$key]); 
				}
			}
		}

		foreach($contextArray as $key => $value) {
			$ns = '';
			$name = $key;
			if(strpos($key, '.')) list($ns, $name) = explode('.', $key, 2);
			$formFieldName = $name;
			if($formFieldName == 'label') $formFieldName = 'field_label';
			$formField = $form->getChildByName($formFieldName);
			$originalValue = $fieldOriginal->$name;
			
			if($formField) {
				// if($isInContext) $formField->set('themeColor', 'primary');
				if($formField instanceof InputfieldSelect) {
					$options = $formField->getOptions();
					if(is_array($value)) foreach($value as $k => $v) {
						if(isset($options[$v])) $value[$k] = $options[$v];
					} else if(isset($options[$value])) {
						$value = $options[$value];
					}
					if(is_array($originalValue)) foreach($originalValue as $k => $v) {
						if(isset($options[$v])) $originalValue[$k] = $options[$v];
					} else if(isset($options[$originalValue])) {
						$originalValue = $options[$originalValue];
					}
				} else if($formField instanceof InputfieldCheckbox) {
					$value = $value ? $labels['on'] : $labels['off'];
					$originalValue = $originalValue ? $labels['on'] : $labels['off'];
				}
			}
			
			if(is_array($value) && ($key == 'viewRoles' || $key == 'editRoles')) {
				foreach($value as $k => $v) {
					$v = $roles->get((int) $v);
					$roleName = $v->name === 'guest' ? $labels['all'] : $v->name;
					if($v) $value[$k] = $roleName;
				}
			}
			
			if(is_array($originalValue) && ($key == 'viewRoles' || $key == 'editRoles')) {
				foreach($originalValue as $k => $v) {
					$v = $roles->get((int) $v);
					$roleName = $v->name === 'guest' ? $labels['all'] : $v->name;
					if($v) $originalValue[$k] = $roleName;
				}
			}
			
			$valueStr = $value;
			$originalValueStr = $originalValue;
			if(is_array($originalValueStr)) $originalValueStr = implode(', ', $originalValueStr);
			if(is_array($valueStr)) $valueStr = implode(', ', $valueStr);
			if($key === 'flagsAdd' || $key === 'flagsDel') {
				$originalValueStr = ((int) $originalValue) ? $labels['on'] : $labels['off'];
				$valueStr = ((int) $value) ? $labels['on'] : $labels['off'];
			}
			
			$originalValueStr = str_replace('|', ' ', (string) $originalValueStr);
			$valueStr = str_replace('|', ' ', (string) $valueStr);
			$change = array(
				"name" => $name, 
				"value" => $valueStr,
				"originalValue" => $originalValueStr
			);
			if($ns) {
				$change['ns'] = $ns;
			} else if($this->contextNamespace) {
				$change['ns'] = 'NS_' . $this->contextNamespace;
			}

			if(isset($labels[$key])) {
				$label = $labels[$key];
			} else if($formField) {
				$label = $formField->label;
			} else if($key == 'flagsAdd') {
				foreach($flagsAddLookup as $flag => $lookupKey) {
					if($value & $flag) {
						$change["label"] = $labels[$lookupKey];
						$changes["flagsAdd-$flag"] = $change;
					}
				}
				continue;
			} else if($key == 'flagsDel') {
				foreach($flagsDelLookup as $flag => $lookupKey) {
					if($value & $flag) {
						$change["label"] = $labels[$lookupKey];
						$changes["flagsDel-$flag"] = $change;
					}
				}
				continue;
			} else if($languages && preg_match('/^(label|description|notes)(\d+)$/', $key, $matches)) {
				$label = ucfirst($matches[1]) . ' (' . $languages->get((int) $matches[2])->get('title|name') . ')';
			} else {
				$label = $key;
			}
			$change["label"] = $label;
			$changes[$key] = $change;
		}
		
		return $changes;
	}

	/**
	 * Search for items containing $text and return an array representation of them
	 *
	 * Implementation for SearchableModule interface
	 *
	 * @param string $text Text to search for
	 * @param array $options Options to modify behavior:
	 *  - `edit` (bool): True if any 'url' returned should be to edit items rather than view them
	 *  - `multilang` (bool): If true, search all languages rather than just current (default=true).
	 *  - `start` (int): Start index (0-based), if pagination active (default=0).
	 *  - `limit` (int): Limit to this many items, if pagination active (default=0, disabled).
	 * @return array
	 *
	 */
	public function search($text, array $options = array()) {

		$languages = $this->wire()->langauges;
		$page = $this->getProcessPage();

		$result = array(
			'title' => $page->id ? $page->title : $this->className(),
			'items' => array(),
			'total' => 0, 
			'properties' => array(
				'name',
				'type',
				'label',
				'description',
				'notes',
				'settings',
				'tags',
				'icon',
			)
		);
		
		if(!empty($options['help'])) return $result;

		$looseItems = array();
		$exactItems = array();
		$property = isset($options['property']) ? $options['property'] : '';
		$cnt = 0;

		foreach($this->wire()->fields as $item) {
			/** @var Field $item */
			if(!$item->type) continue;

			$search = array(' ');
			if(empty($property) || $property == 'name' || $property == 'all') $search[] = $item->name; 
			if(empty($property) || $property == 'type' || $property == 'all') $search[] = $item->type->shortName;

			if(!empty($options['multilang']) && $languages) {
				foreach($languages as $lang) {
					if(empty($property) || $property == 'label' || $property == 'all') $search[] = $item->getLabel($lang);
					if($property == 'description' || $property == 'all') $search[] = $item->getDescription($lang);
					if($property == 'notes' || $property == 'all') $search[] = $item->getNotes($lang);
				}
			} else {
				if(empty($property) || $property == 'label' || $property == 'all') $search[] = $item->label;
				if($property == 'description' || $property == 'all') $search[] = $item->description;
				if($property == 'notes' || $property == 'all') $search[] = $item->notes;
			}
			
			if($property == 'settings' || $property == 'data') {
				foreach($item->getArray() as $v) {
					$search[] = (string) $v;
				}
			} else if(!in_array($property, $result['properties'])) {
				$val = $item->get($property);
				if($val !== null) $search[] = $val;
			}

			$search = implode(' ', $search);
			$pos = stripos($search, $text);
			if($pos === false) continue;
			$exact = stripos($search, " $text");

			$result['total']++;
			if(!empty($options['limit']) && $cnt >= $options['limit']) break;
			
			$item = array(
				'id' => $item->id,
				'name' => $item->name,
				'title' => $item->name, 
				'subtitle' => $item->type->shortName,
				'summary' => $item->getLabel(),
				'icon' => $item->getIcon(),
				'url' => empty($options['edit']) ? '' : $page->url() . "edit?id=$item->id",
			);

			if($exact) {
				$exactItems[] = $item;
			} else {
				$looseItems[] = $item;
			}
			
			$cnt++;
		}

		$result['items'] = array_merge($exactItems, $looseItems);
		
		return $result;
	}

	/**
	 * URL to redirect to after non-authenticated user is logged-in, or false if module does not support
	 *
	 * @param Page $page
	 * @return string
	 * @sine 3.0.167
	 *
	 */
	public static function getAfterLoginUrl(Page $page) {
		
		$sanitizer = $page->wire()->sanitizer;
		$input = $page->wire()->input;
		$segment = $input->urlSegment1;
		$data = array();
		
		if(empty($segment)) {
			$data = array(
				'templates_id' => (int) $input->get('templates_id'),
				'fieldtype' => $input->get->name('fieldtype'),
				'show_system' => (int) $input->get('show_system'),
				'nosave' => (int) $input->get('nosave'),
				'fieldgroup_id' => (int) $input->get('fieldgroup_id'),
			);
		} else if($segment === 'tags') {
			$data = array(
				'edit_tag' => $sanitizer->word($input->get->text('edit_tag')), 
			);
		} else if($segment === 'edit') {
			$data = array(
				'id' => (int) $input->get('id'),
				'context_namespace' => $input->get->fieldName('context_namespace'), 
				'context_label' => $input->get->text('context_label'),
				'process_template' => (int) $input->get('process_template'),
				'focus' => $input->get->fieldName('focus'),
				'modal' => (int) $input->get('modal'),
				'fieldgroup_id' => (int) $input->get('fieldgroup_id'),
			);
			if(!empty($data['context_label'])) $data['context_label'] = urlencode($data['context_label']);
		} else if($segment === 'changeType' || $segment === 'change-type') {
			$data = array(
				'id' => (int) $input->get('id'),
				'type' => $input->get->name('type'),
			);
		} else if($segment === 'sendTemplates' || $segment === 'send-templates') {
			$templates = array();
			foreach(explode(',', (string) $input->get('templates')) as $value) {
				if(!empty($value)) $templates[] = $sanitizer->templateName($value);
			}
			$data = array(
				'id' => (int) $input->get('id'),
				'templates' => implode(',', $templates),
			);
		} else if($segment === 'import' || $segment === 'export') {
			// ok, no query string
		} else {
			$segment = '';
		}
		
		foreach($data as $key => $value) $data[$key] = urlencode($value);
		
		return $page->url() . $segment . (count($data) ? '?' . implode('&', $data) : '');
	}

	/**
	 * Allow use of the “Access” tab?
	 * 
	 * @return bool
	 * @since 3.0.197
	 * 
	 */
	protected function allowAccessTab() {
		if(!$this->field) return false;
		if($this->field->type instanceof FieldtypeFieldsetOpen) return false;
		if(!$this->wire()->user->isSuperuser()) return false;
		if($this->field->useRoles) return true; // access control already enabled
		if(!$this->field->hasFlag(Field::flagSystem)) return true;
		if($this->wire()->config->advanced) return true;
		return false;
	}

	/**
	 * Build a form allowing configuration of this Module
	 * 
	 * @param array $data
	 * @return InputfieldWrapper
	 *
	 */
	public function getModuleConfigInputfields(array $data) {

		/** @var InputfieldWrapper $inputfields */
		$inputfields = $this->wire(new InputfieldWrapper());

		$f = $inputfields->InputfieldCheckbox;
		$f->attr('name', 'listAfterSave');
		$f->attr('value', 1); 
		$f->attr('checked', empty($data['listAfterSave']) ? '' : 'checked'); 
		$f->label = $this->_("Return to fields list after saving a field?");
		$f->description = $this->_("By default, you will remain in the fields editor after saving a field. If you want to instead return to the fields list, check this box.");
		$inputfields->add($f);

		return $inputfields;
	}

	/**
	 * Return the current field being edited, or NULL if it hasn't been set
	 *
	 * $this->field is set by buildEditForm
	 *
	 * @return Field|null
	 *
	 */
	public function getField() {
		return $this->field; 
	}

	/**
	 * For hooks to listen to when a new field is added
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldAdded(Field $field) { }

	/**
	 * For hooks to listen to when any field is saved
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldSaved(Field $field) { }

	/**
	 * For hooks to listen to when a field is deleted
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldDeleted(Field $field) { }

	/**
	 * For hooks to listen to when a field type changes
	 * 
	 * @param Field $field
	 *
	 */
	public function ___fieldChangedType(Field $field) { }

	/**
	 * Hook called when field context is changed and saved
	 * 
	 * @param Field $field
	 * @param Fieldgroup $fieldgroup
	 * @param array $changes Indexed by property name
	 * 
	 */
	public function ___fieldChangedContext(Field $field, Fieldgroup $fieldgroup, array $changes) { }

	/**
	 * For hooks to modify if they want to specific prevent a field from being added to a template from here
	 * 
	 * @param Field $field
	 * @param Template $template
	 * @return bool
	 *
	 */
	public function ___allowFieldInTemplate(Field $field, Template $template) {
		return true; 
	}

	/**
	 * Upgrade module from one version to another
	 * 
	 * @param int|string $fromVersion
	 * @param int|string $toVersion
	 * 
	 */
	public function ___upgrade($fromVersion, $toVersion) {
		$collapsedTags = $this->wire()->modules->getConfig($this, 'collapsedTags');
		if(!is_array($collapsedTags)) {
			$collapsedTags = array();
			foreach($this->wire()->fields->getTags() as $tag) {
				$c = substr($tag, 0, 1);
				if($c === '-' || $c === '_') $collapsedTags[] = $tag;
			}
			$this->wire()->modules->saveConfig($this, 'collapsedTags', $collapsedTags);
		}
	}
}
