<?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 (development), Copyright 2015 by Ryan Cramer
 * https://processwire.com
 * 
 * @property bool $listAfterSave
 * 
 */

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' => 112,
			'permanent' => true, 
			'permission' => 'field-admin', // add this permission if you want this Process available for roles other than Superuser
			'icon' => 'cube', 
			'useNavJSON' => true,
			);
	}

	/**
	 * @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() {
		if($this->input->urlSegment1 == 'edit') $this->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')
			);
		
		if($this->input->post->id) $this->id = (int) $this->input->post->id;
			else $this->id = $this->input->get->id ? (int) $this->input->get->id : 0;

		if($this->id) $this->field = $this->fields->get($this->id);
		if(!$this->field) $this->field = new Field();

		return 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
	 *
	 */
	public function ___executeNavJSON(array $options = array()) {
		$fieldsArray = array();
		$showAll = $this->wire('config')->advanced; 
		foreach($this->wire('fields') as $field) {
			if(!$showAll && ($field->flags & Field::flagSystem) && $field->name != 'title') 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() {

		$showAllLabel = $this->_('Show All'); 

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

		$fieldset = $this->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);

		$field = $this->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); 
		}
		$this->session->ProcessFieldListTemplatesID = (int) $this->input->get->templates_id; 
		$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
		$value = (int) $this->session->ProcessFieldListTemplatesID; 
		$field->attr('value', $value); 
		if($value && $template = $this->templates->get($value)) {
			$form->description = sprintf($this->_('Showing fields from template: %s'), $template);
			$this->wire('processHeadline', $this->_('Fields by Template')); // Page headline when filtering by template
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$template = null;
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

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

		$field = $this->modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'fieldtype'); 
		$field->addOption('', $showAllLabel); 
		foreach($this->fieldtypes as $fieldtype) $field->addOption($fieldtype->name, $fieldtype->shortName); 
		$this->session->ProcessFieldListFieldtype = $this->sanitizer->name($this->input->get->fieldtype); 
		$field->label = $this->_('Filter by Field 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
		$value = $this->session->ProcessFieldListFieldtype; 
		$field->attr('value', $value); 
		if($value && $fieldtype = $this->fieldtypes->get($value)) {
			$form->description = sprintf($this->_('Showing fields of type: %s'), $fieldtype->shortName);
			$fieldset->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$fieldset->add($field); 

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

		if(is_null($template) && !$this->session->ProcessFieldListFieldtype) {
			$field = $this->modules->get("InputfieldCheckbox"); 
			$field->attr('id+name', 'show_system'); 
			$field->label = $this->_('Show built-in 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->value = 1; 
			$field->collapsed = Inputfield::collapsedYes; 
			$this->session->ProcessFieldListShowSystem = (int) $this->input->get->show_system; 
			if($this->session->ProcessFieldListShowSystem) {
				$field->attr('checked', 'checked');
				$field->collapsed = Inputfield::collapsedNo;
				$fieldset->collapsed = Inputfield::collapsedNo;
				$form->description = $this->_("Showing all fields, including built-in system and permament fields."); 
			}
			$fieldset->add($field); 
		} else {
			$this->session->ProcessFieldListShowSystem = 1; 
		}
		
		return $form;
	}

	/**
	 * Render a list of current fields
	 * 
	 * @return string
	 *
	 */
	public function ___execute() {
		if($this->wire('config')->ajax) return $this->renderListJSON();

		$out = $this->getListFilterForm()->render() . "\n<div id='ProcessFieldList'>\n";
		$fieldsByTag = array();
		$untaggedLabel = $this->_('Untagged');
		$hasFilters = $this->session->ProcessFieldListTemplatesID || $this->session->ProcessFieldListFieldtype;
		$collapsedTags = array();
		$caseTags = array(); // indexed by lowercase version of tag


		if(!$hasFilters) foreach($this->fields as $field) {
			if($this->session->ProcessFieldListShowSystem) {
				if($field->flags & Field::flagSystem || $field->flags & Field::flagPermanent) 
					$field->tags .= " " . $this->_x('Built-In', 'tag'); // Tag applied to the group of built-in/system fields
			}
			if(empty($field->tags)) {
				$tag = strtolower($untaggedLabel);
				if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
				$fieldsByTag[$tag][$field->name] = $field;
				$caseTags[$tag] = $untaggedLabel;
				continue; 
			}
			$tags = explode(' ', trim($field->tags));
			foreach($tags as $tag) {
				if(empty($tag)) continue; 
				$caseTag = ltrim($tag, '-');
				$tag = strtolower($tag);
				if(substr($tag, 0, 1) == '-') {
					$tag = ltrim($tag, '-');
					$collapsedTags[] = $tag;
				}
				if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array();
				$fieldsByTag[$tag][$field->name] = $field;	
				if(!isset($caseTags[$tag])) $caseTags[$tag] = $caseTag; 
			}
		}

		$tagCnt = count($fieldsByTag);
		if($tagCnt > 1) {
			$form = $this->wire(new InputfieldWrapper());
			ksort($fieldsByTag);
			foreach($fieldsByTag as $tag => $fields) {
				ksort($fields);
				$f = $this->modules->get('InputfieldMarkup');
				$f->entityEncodeLabel = false;
				$f->label = $caseTags[$tag];
				$f->icon = 'tags';
				$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 = $this->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->addClass('head_button_clone');
		$out .= $button->render();
	
		$button = $this->modules->get('InputfieldButton');
		$button->id = 'import_button';
		$button->href = "./import/";
		$button->value = $this->labels['import'];
		$button->icon = 'paste';
		$button->addClass('ui-priority-secondary');
		$out .= $button->render();
		
		$button = $this->modules->get('InputfieldButton');
		$button->id = 'export_button'; 
		$button->href = "./export/";
		$button->value = $this->labels['export'];
		$button->icon = 'copy';
		$button->addClass('ui-priority-secondary');
		$out .= $button->render();

		if($this->input->nosave) {
			$this->session->remove('ProcessFieldListTemplatesID');
			$this->session->remove('ProcessFieldListFieldtype');
			$this->session->remove('ProcessFieldListShowSystem');
		}

		return $out;
	}	

	protected function ___getListTable($fields) {

		$table = $this->modules->get("MarkupAdminDataTable");

		$headerRow = array(
			$this->_x('Name', 'list thead'), 
			$this->_x('Label', 'list thead'), 
			$this->_x('Type', 'list thead'), 
			$this->_x('Templates', 'list thead quantity')
			);

		$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;
	}

	protected function ___getListTableRow(Field $field) {

		$flagDefs = array(
			'Autojoin' => array(
				'icon' => 'fa fa-sign-in',
				'label' => $this->_x('autojoin', 'list notes')
				),
			'Global' => array(
				'icon' => 'fa fa-globe',
				'label' => $this->_x('global', 'list notes')
				),
			'System' => array(
				'icon' => 'fa fa-puzzle-piece',
				'label' => $this->_x('system', 'list notes')
				),
			'Permanent' => array(
				'icon' => 'fa fa-building-o',
				'label' => $this->_x('permanent', 'list notes')
				),
			'Required' => array(
				'icon' => 'fa fa-asterisk', 
				'label' => $this->_x('required', 'list notes')
				),
			'Dependency' => array(
				'icon' => 'fa fa-question-circle', 
				'label' => $this->_x('show if...', 'list notes')
				),
			'Access' => array(
				'icon' => 'fa fa-key', 
				'label' => $this->_x('access', 'list notes')
				),
			);

		$numTemplates = 0; 
		$flags = array();
		$builtIn = false;
		
		$templatesID = $this->session->ProcessFieldListTemplatesID; 
		if($templatesID && $template = $this->templates->get($templatesID)) {
			if(!$template->fieldgroup->has($field)) return array();
		}
		if($fieldtype = $this->session->ProcessFieldListFieldtype) {
			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::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($builtIn && !$templatesID && $field->name != 'title') {
			if(!$this->session->ProcessFieldListShowSystem) return array();
		}

		foreach($field->getFieldgroups() as $fieldgroup) {
			$numTemplates += $fieldgroup->numTemplates();
		}
		$numTemplatesLink = "../template/?filter_field={$field->name}&nosave=1";

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

		$icon = $field->icon ? "<i class='fa fa-" . str_replace(array('icon-', 'fa-'), '', $this->wire('sanitizer')->name($field->icon)) . "'></i> " : '';

		return array(
			$field->name => "edit?id={$field->id}",
			$icon . $this->sanitizer->entities($field->getLabel()), 
			$field->type->shortName,
			"$flagsOut<a 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 {
				$headline .= ' (' . sprintf($this->_('when used with template: %s'), $this->fieldgroup->name) . ')';
			}
		}

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

		if($this->field->id && !$this->fieldgroup && $this->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;
	}
	
	protected function buildEditFormAlert() {

		$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; // confrim there isn't a field with the name
				$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->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;
		}
		
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('class', 'WireTab');
		$form->attr('id', 'alert');
		$form->attr('title', $this->_x('Alert', 'tab'));
		
		if(count($xkeys)) {
			$f = $this->wire('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) {
			$f = $this->wire('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>"; // . $this->_x('None (default)', 'context select') . "</option>";

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

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

		/*
		if($this->fieldgroup) $out .= 
			'<p class="detail">' . 
			sprintf($this->_('*Indicates that field is currently overriding a default value. Settings that you specify on this screen override the default settings only when used with the "%s" template.'), $this->fieldgroup->name) . 
			'</p>';
		*/
	
		return $out; 	
	}

	/**
	 * Build the Field Edit form
	 *
	 */
	protected function ___buildEditForm() {

		// optional context fieldgroup
		if($this->input->post->fieldgroup_id) $fieldgroup_id = (int) $this->input->post->fieldgroup_id; 
			else if($this->input->get->fieldgroup_id && !count($_POST)) $fieldgroup_id = (int) $this->input->get->fieldgroup_id; 
			else $fieldgroup_id = 0;
		
		if($fieldgroup_id) {
			// optional namespace for context fieldgroup
			if($this->input->post('_context_namespace')) $contextNamespace = $this->input->post('_context_namespace');
				else if($this->input->get('context_namespace') && !count($_POST)) $contextNamespace = $this->input->get('context_namespace');
				else $contextNamespace = '';
			if($contextNamespace) {
				$contextNamespace = $this->wire('sanitizer')->fieldName($contextNamespace);
				$this->contextNamespace = $contextNamespace;
			}	
		}
		if($this->input->post('_context_label')) $contextLabel = $this->input->post('_context_label');
			else if($this->input->get('context_label')) $contextLabel = $this->input->get('context_label');
			else $contextLabel = '';
		if(strlen($contextLabel)) {
			$contextLabel = $this->wire('sanitizer')->entities($contextLabel);
			$this->contextLabel = $contextLabel;
		}
		
		if($this->field->id && $this->session->get($this, 'optimize') == $this->field->id) {
			// keep track of what gets retrieved from each field
			foreach($this->wire('fields') as $field) $field->trackGets(true);
		}

		$form = $this->modules->get('InputfieldForm');
		$form->attr('id+name', 'ProcessFieldEdit'); 
		$form->attr('action', 'save'); 
		$form->attr('method', 'post'); 
		$form->addClass('InputfieldFormFocusFirst');
		$this->form = $form;

		if($fieldgroup_id && $this->field->id) {
			$this->fieldgroup = $this->wire('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}'"); 
			$this->field = $this->fieldgroup->getFieldContext($this->field->id, $this->contextNamespace);  // get the field in context of the fieldgroup
		}

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

		if($this->field->id) { 
			$accessTab = $this->buildEditFormAccess();
			if($this->field->type) {
				$this->buildEditFormCustom($form);
				if(!$this->fieldgroup) {
					$form->add($accessTab);
					$form->add($this->buildEditFormAdvanced());
				}
			}
			if(!$this->fieldgroup) {
				$info = $this->buildEditFormInfo();
				if(count($info)) $form->add($info);
				$this->buildEditFormContext($form);
				$form->add($this->buildEditFormDelete());
			} else {
				$form->add($accessTab);
				$this->buildEditFormContext($form);
				// $this->buildEditFormContextFieldgroup($form); 
			}
		}

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

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

		if($this->wire('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
			$field = $this->modules->get('InputfieldButton');
			$field->attr('id+name', 'modal_cancel_button'); 
			$field->attr('value', $this->_x('Cancel', 'button'));
			$field->attr('class', $field->attr('class') . ' ui-priority-secondary');
			$form->append($field);
		
			// contains the asm list item status, populated by JS
			$field = $this->modules->get('InputfieldHidden');
			$field->attr('id+name', 'asmListItemStatus'); 
			$field->attr('class', 'asmListItemStatus');
			$field->attr('data-tpl', "<span class='ui-priority-secondary'>" . str_replace('Fieldtype', '', $this->field->type) . "</span> %"); // % gets replaced with live percent
			$field->attr('value', '');
			$form->append($field); 
		}
		

		return $form; 
	}
	
	protected function ___buildEditFormContext($form) {
		
		$allChanges = array();
		$fieldgroups = $this->fieldgroup ? array($this->fieldgroup) : $this->wire('fieldgroups'); 
		
		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);
		}
		if(!count($allChanges)) return;
		
		$tab = $this->wire(new InputfieldWrapper());
		$tab->attr('title', $this->_('Overrides'));
		$tab->attr('class', 'WireTab');
		$form->add($tab); 
		
		$f = $this->wire('modules')->get('InputfieldMarkup');
		$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 = 'exchange';
		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.');
		}
		$tab->add($f);
	
		/** @var MarkupAdminDataTable $table */
		$table = $this->wire('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('Original', 'context-thead');
		$header[] = $this->_x('Override', 'context-thead');
		$header[] = "<i class='fa fa-trash-o override-select-all'></i>";
		$table->headerRow($header); 
		
		$sanitizer = $this->wire('sanitizer');
		$lastFieldgroupName = '';
		$this->wire('modules')->get('JqueryUI')->use('modal');
		
		foreach($allChanges as $fieldgroupName => $changes) {
			$fieldgroup = $this->wire('fieldgroups')->get($fieldgroupName);
			foreach($changes as $key => $change) {
				$a = $fieldgroupName;
				// $a = "<a class='pw-modal' data-buttons='#Inputfield_submit_save_field' data-autoclose='1' " . 
				//	"href='./edit?id={$this->field->id}&fieldgroup_id=$fieldgroup->id'>$fieldgroupName</a>";
				$fieldgroupLabel = $fieldgroupName == $lastFieldgroupName ? '' : $a;
				$row = array();
				if(!$this->fieldgroup) $row[] = $fieldgroupLabel;
				$row[] = $sanitizer->entities($change['label']);
				$row[] = "<del>" . $sanitizer->entities($change['originalValue']) . "</del>";
				$row[] = $sanitizer->entities($change['value']);
				$row[] = "<input type='checkbox' name='_remove_context[]' value='$fieldgroup->id:$key' />";
				$options = $fieldgroupLabel ? array('separator' => true) : array();
				$table->row($row, $options);
				$lastFieldgroupName = $fieldgroupName;
			}
		}
		
		$f->value = $table->render();
	}

	/*
	protected function ___buildEditFormContextFieldgroup($form) {
		if(!$this->fieldgroup) return;
		$changes = $this->getContextChanges($form, $this->field, $this->fieldgroup);
		
		$tab = new InputfieldWrapper();
		$tab->attr('title', $this->_('Overrides'));
		$tab->attr('class', 'WireTab');
		$form->add($tab); 
		
		$f = $this->wire('modules')->get('InputfieldCheckboxes');
		$f->label = $this->_('Remove context overrides');
		$f->table = true; 
		$f->thead = 
			$this->_x('Remove setting', 'context-thead') . '|' .
			$this->_x('Original', 'context-thead') . '|' . 
			$this->_x('Override', 'context-thead'); 
		$f->description = sprintf($this->_('The following settings are overriding the original field settings when this field is used in the context of the "%s" template.'), $this->fieldgroup->name); // Context description
		$f->description .= ' ' . $this->_('To remove any of the overridden values (and restore the original field value) check the box next to each setting you want to remove/restore.'); // Context description 2
		$tab->add($f);
		
		foreach($changes as $key => $change) {
			$f->addOption($key, "$change[label]|$change[originalValue]|$change[value]");
		}
	}
	*/

	/**
	 * Add Fieldtype and Inputfield custom fields to the form 
	 *
	 */
	protected function ___buildEditFormCustom($form) {
		
		$customFields = $this->field->getConfigInputfields();
		$tab = null;
		
		foreach($customFields as $field) {
			// skip over wrappers if they don't have fields in them 
			if($field instanceof InputfieldWrapper && !count($field->children)) continue; 
			//if(!$this->fieldgroup) $field->attr('class', 'WireTab');
			$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;
		$cls = __NAMESPACE__ . "\\FieldtypeFieldsetOpen";
		if($this->field instanceof $cls) return;
		
		$fieldset = $this->wire('modules')->get('InputfieldFieldset');
		$fieldset->label = $this->_('Front-end editing');
		$fieldset->icon = 'edit';
		$fieldset->collapsed = Inputfield::collapsedYes;
		$tab->add($fieldset);
		
		if(!$this->wire('modules')->isInstalled('PageFrontEdit')) {
			$f = $this->wire('modules')->get('InputfieldMarkup');
			$fieldset->add($f);
			$button = $this->wire('modules')->get('InputfieldButton');
			$button->href = $this->wire('config')->urls->admin . "module/installConfirm?name=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 = $this->wire('config')->paths->PageFrontEdit . 'PageFrontEditConfig.php';
		include_once($file);
		$moduleConfig = $this->wire(new PageFrontEditConfig());
		$moduleConfig->fieldHelpInputfields($fieldset, $this->field); 
	}

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

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

		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'delete');
		$form->attr('class', 'WireTab');
		//$form->head = $deleteLabel;
		$form->attr('title', $this->_x('Delete', 'tab')); 

		$field = $this->modules->get('InputfieldCheckbox');
		$field->label = $deleteLabel;
		$field->icon = 'times-circle';
		$field->attr('id+name', "delete"); 
		$field->attr('value', $this->field->id); 

		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."); 
		}

		$form->add($field);

		return $form;
	}

	/**
	 * Basic field configuration options: name, type, label, description
	 *
	 */
	protected function ___buildEditFormBasics() {

		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'basics');
		$form->attr('class', 'WireTab');
		$form->attr('title', $this->_x('Basics', 'tab')); 


		if($this->fieldgroup) { 	

		} else {
			//$form->head = $this->_('Basic field settings');
			$field = $this->modules->get('InputfieldName');
			$field->attr('value', $this->field->name); 
			$field->description = $this->_("Any combination of ASCII letters [a-z], numbers [0-9], or underscores (no dashes or spaces).");
			$form->add($field); 

			$field = $this->modules->get('InputfieldSelect');
			$field->label = $this->_x('Type', 'select label'); // Label for field type select
			$field->attr('name', 'type'); 
			$field->required = true; 
			if($this->field->type) $field->attr('value', $this->field->type->name); 
				else $field->addOption('', ''); 

			if(!$this->field->id) $field->description = $this->_("After selecting your field type and saving, you may be presented with additional configuration options specific to the field type you selected."); // Note that appears when adding new field

			if($this->field->type) $fieldtypes = $this->field->type->getCompatibleFieldtypes($this->field);
				else $fieldtypes = $this->fieldtypes; 

			if($fieldtypes && count($fieldtypes)) {
				foreach($fieldtypes->sort('name') as $fieldtype) {
					if(!$this->config->advanced && $fieldtype->isAdvanced() && $this->field->name != 'title' && $field->value != $fieldtype->className()) continue; 
					$field->addOption($fieldtype->name, $fieldtype->shortName); 
				}
			} else {
				$field->addOption($this->field->type->name, $this->field->type->shortName); 
			}
			
			if(!$this->field->id) {
				$names = array();
				foreach($this->wire('fields') as $f) {
					if(($f->flags & Field::flagSystem) && !$this->wire('config')->advanced) continue;
					if(strpos($f->name, '_END')) continue;
					$names['_' . $f->name] = $f->name;
				}
				$field->addOption($this->_('Clone existing field'), $names);
			}

			$form->add($field); 
		}

		$languages = $this->wire('languages');
		$languageFields = array();
	
		$field = $this->modules->get('InputfieldText');
		$field->attr('id+name', 'field_label'); 
		$field->label = $this->_x('Label', 'text input'); // Label for 'field label' text input
		$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->description = $this->_("This is the label that appears above the entry field. If left blank, the name will be used instead."); // Description for 'field label'
		$form->add($field);
		$languageFields[] = $field;

		$field = $this->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->description = $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;
		
		$field = $this->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->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;

		if($languages) foreach($languageFields as $field) {
			$field->useLanguages = true; 
			$name = $field->name; 
			if($name == 'field_label') $name = 'label';
			foreach($languages as $language) {
				if($language->isDefault) continue; 
				$field->set("value{$language->id}", $this->field->get("$name{$language->id}")); 
			}
		}

		return $form;
	}

	/**
	 * Build the 'Info' field shown in the Field Edit form
	 *
	 */
	protected function ___buildEditFormInfo() {

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

		$field = $this->modules->get('InputfieldCheckboxes');
		$field->attr('name', 'send_templates'); 	
		$field->label = $this->_('Add or remove field from templates'); 
		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.') . ' ';
		}
		$multi = $this->field->type instanceof FieldtypeMulti; 
		$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';
		$field->thead = 
			$this->_x('Name', 'usage-table-th') . '|' . 
			$this->_x('Label', 'usage-table-th') . '|' . 
			$this->_x('Populated pages', 'usage-table-th') . 
			($multi ? '|' . $this->_x('Rows of data', 'usage-table-th') : ''); 
		$numOmitted = 0;
		foreach($allTemplates as $template) {
			$label = $template->name;
			$longLabel = str_replace('|', ' ', $template->getLabel());
			$numRows = '';
			$numPages = '';
			if(isset($inTemplates[$template->id])) {
				$numPages = $this->wire('fields')->getNumPages($this->field, array('template' => $template));
				$numRows = $multi ? $this->wire('fields')->getNumRows($this->field, array('template' => $template)) : null; 
				$longLabel = "**[$longLabel](../template/edit?id=$template->id)**";
				$label = "**$label**";
			} else if($template->flags & Template::flagSystem) {
				if(!$this->wire('config')->advanced) {
					$numOmitted++;
					continue;
				}
			}
			$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); 
		$form->add($field);
		
		$field = $this->wire('modules')->get('InputfieldHidden');
		$field->attr('id+name', '_send_templates_changed'); 
		$field->attr('value', ''); 
		$form->add($field); 
	
		// --------------------------	
		
		$field = $this->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;
		$form->append($field);
		
		// --------------------------	
		
		$field = $this->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;
		$form->add($field);


		return $form; 
	}
	
	protected function buildEditFormAccess() {

		$config = $this->wire('config');

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

		$f = $this->wire('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.');
		$f->icon = 'key';
		$f->addOption(1, $this->_x('Yes', 'access'));
		$f->addOption(0, $this->_x('No', 'access'));
		$f->attr('value', (int) $this->field->useRoles);
		$f->optionColumns = 1;
		$tab->add($f);

		$f = $this->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.');

		$table = $this->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 = $this->wire('roles')->get($config->guestUserRolePageID);
		$guestHasView = in_array($guestRole->id, $viewRoles);

		foreach($this->wire('roles') as $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' name='viewRoles[]' value='$role->id' " .
					(in_array($role->id, $viewRoles) || $guestHasView ? $checked : '') . " />"),
				("<input type='checkbox' id='editRoles_$role->id' class='editRoles' name='editRoles[]'  value='$role->id' " .
					(in_array($role->id, $editRoles) ? $checked : '') . " " . ($editable ? '' : $disabled) . " />")
			));
		}

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

		$f = $this->wire('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
	 *
	 */
	protected function ___buildEditFormAdvanced() {

		if($this->field->type) {
			$form = $this->field->type->getConfigAdvancedInputfields($this->field); 
		} else {
			$form = $this->wire(new InputfieldWrapper());
		} 

		$form->attr('id', 'advanced');
		$form->attr('class', 'WireTab');
		//$form->head = $this->_('Advanced options'); // Section header for 'Advanced'
		$form->attr('title', $this->_x('Advanced', 'tab')); 

		$field = $this->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->prepend($field); 

		$field = $this->modules->get("InputfieldText"); 
		$field->attr('name', 'tags'); 
		$field->attr('value', $this->field->tags); 
		$field->icon = 'tags';
		$field->label = $this->_('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 (hyphenation is okay).') . " " . 
			$this->_('To make a tag collapsed in the fields list, prepend a hyphen to it, like this: -hello');
		$form->prepend($field); 
		
		return $form;
	}

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

		$this->buildEditForm();

		if(!$this->input->post->submit_save_field) {
			$this->session->redirect("./"); 
		}

		if($this->fieldgroup) return $this->saveContext();
		$isNew = !$this->field->id;

		if($this->input->post->delete && $this->input->post->delete == $this->field->id && $this->field->numFieldgroups() == 0) {
			$this->session->CSRF->validate();
			$this->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); 
			$this->session->redirect("./"); 
			return; 
		}

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

		try {
			$this->form->processInput($this->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();
				$this->session->message($this->_('Added Field') . " - {$this->field->name}"); 
				$this->fieldAdded($this->field); 
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
				if(!$this->field->id) $this->session->redirect("./"); 
			}
			$redirectURL = "edit?id={$this->field->id}";
			if(!$this->wire('input')->get('modal')) $redirectURL .= "#fieldtypeConfig";
			$this->session->redirect($redirectURL);
				
		} else {
			
			$optimized = false; 
			
			$removeKeys = $this->wire('input')->post('_remove_keys');
			if($removeKeys && count($removeKeys)) {
				$_removeKeys = $this->wire('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));
				}
				$optimized = true; 
			}
			
			if($this->input->post('_remove_rows') == $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->database->prepare($sql);
				$query->execute();
				$cnt = $query->rowCount();
				if($cnt) $this->message($this->field->name . ": " . sprintf($this->_('Deleted %d orphaned rows'), $cnt));
				$optimized = true; 
			}

			$this->session->remove($this, 'optimize');
			if($this->input->post('_optimize')) $this->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 = $this->form->get("type"); 
				if($this->field->type->className() != $select->value) {
					$this->session->redirect("changeType?id={$this->field->id}&type={$select->value}"); 
				}	
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
			}
			
			$cloneField = $this->form->get('clone_field');
			$cloneName = $cloneField ? $cloneField->attr('value') : ''; 
			if($cloneName && $this->isAllowedName($cloneName)) {
				$clone = $this->fields->clone($this->field);
				if($clone && $clone->id) {
					$clone->name = $cloneName; 
					if($clone->label) $clone->label = $clone->label . ' ' . $this->_('(copy)'); 
					try {
						$this->fields->save($clone);
						$this->message($this->_('Cloned Field') . " - {$this->field->name} => {$clone->name}");
						if(!count($errors)) {
							$this->wire('session')->message($this->_('You are now editing the field you cloned.'));
							$this->wire('session')->redirect("./edit?id=$clone->id#basics");
						}
					} catch(\Exception $e) {
						$errors[] = $e->getMessage(); 
					}
					// $this->listAfterSave = true;
				} else {
					$errors[] = ($this->_("Error creating clone of this field") . " - {$this->field->name}");
				}
			}
			
			if(count($errors)) {
				foreach($errors as $error) {
					$this->error($error);
				}
				if($isNew) return $this->executeAdd();
			}

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

	/**
	 * Is the given field name allowed to use for a new or cloned field
	 * 
	 * @param string $name
	 * @return bool
	 * 
	 */
	protected function isAllowedName($name) {
		$_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($this->wire('fields')->get($name)) {
			$error = sprintf($this->_('Field name "%s" is already in use'), $name);
		} else if(($this->wire($name) || $this->wire('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($this->wire('languages') && $this->wire('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; 
	}

	/**
	 * Save field in the context of a fieldgroup only
	 *
	 */
	protected function ___saveContext() {
		try { 
			$this->form->processInput($this->input->post); 
			$this->saveInputfields($this->form); 
			$this->fields->saveFieldgroupContext($this->field, $this->fieldgroup, $this->contextNamespace); 
			$this->saveRemoveOverrides();
		} catch(\Exception $e) {
			$this->error($e->getMessage()); 
		}
		$this->session->redirect("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');
	
		foreach($wrapper->children() as $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(!$this->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(!$this->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' && $this->config->advanced) {
				if(!$this->input->post->system) $this->field->flags = $this->field->flags & ~Field::flagSystem; 
					else $this->field->flags = $this->field->flags | Field::flagSystem;
				continue; 
			} else if($name == 'permanent' && $this->config->advanced) {
				if(!$this->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->send_templates) unset($this->field->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; 
			}

			$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
			$this->field->viewRoles = $this->wire('input')->post('viewRoles');
			$this->field->editRoles = $this->wire('input')->post('editRoles');
			$accessFlags = $this->wire('input')->post('_accessFlags');
			if(!is_array($accessFlags)) $accessFlags = array();
			$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;
		}
	}

	/**
	 * 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' />"
			list($fieldgroupID, $property) = explode(':', $value);
			if(isset($fieldgroups[$fieldgroupID])) {
				$fieldgroup = $fieldgroups[$fieldgroupID];
			} else {
				$fieldgroup = $this->wire('fieldgroups')->get((int) $fieldgroupID);
				if(!$fieldgroup) continue;
				$fieldgroups[$fieldgroup->id] = $fieldgroup;
			}
			if(isset($contextArrays[$fieldgroup->id])) {
				$contextArray = $contextArrays[$fieldgroup->id];
			} else {
				$contextArray = $fieldgroup->getFieldContextArray($this->field->id);
			}
			if(strpos($property, 'flagsAdd-') === 0 || strpos($property, 'flagsDel-') === 0) {
				list($flagType, $flag) = explode('-', $property);
				$flag = (int) $flag;
				if(isset($contextArray[$flagType])) $contextArray[$flagType] = $contextArray[$flagType] & ~$flag;
			} else {
				unset($contextArray[$property]);
			}
			$this->message($this->_('Removed context override') . " (template=$fieldgroup->name, property=$property)");
			$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
	 *
	 */
	public function ___executeChangeType() {

		$this->buildEditForm();

		$this->wire('processHeadline', sprintf($this->_('Change type for field: %s'), $this->field->name)); // Page headline when changing type
		$this->wire('breadcrumbs')->add(new Breadcrumb('./', 'Fields'))->add(new Breadcrumb("./edit?id={$this->field->id}", $this->field->name)); 

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

		$form = $this->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->shortName, $newType->shortName);  
		$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

		$f = $this->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);
		
		$f = $this->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); 
	
		$f = $this->modules->get("InputfieldHidden"); 	
		$f->attr('name', 'id'); 
		$f->attr('value', $this->field->id); 
		$form->append($f); 	

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

	/**
	 * Save a changed field type
	 *
	 */
	public function ___executeSaveChangeType() {
		$this->buildEditForm();

		if(!$this->field || !$this->input->post->confirm_type) {
			$this->message($this->_("Field type change aborted")); 
			$this->session->redirect('./'); 
		}

		$type = $this->wire('sanitizer')->name($this->input->post->confirm_type); 
		if($type = $this->fieldtypes->get($type)) {
			$this->session->CSRF->validate();
			$this->message($this->_("Field type changed")); 
			$this->wire('fields')->addHookBefore('changeFieldtype', $this, 'hookFieldsChangeFieldtype'); 
			$this->field->type = $type; 
			$this->field->save();
			$this->fieldChangedType($this->field); 
		}
	
		$this->session->redirect("edit?id={$this->field->id}"); 	
	}
	
	public function hookFieldsChangeFieldtype(HookEvent $event) {
		// FYI: $field = $event->arguments(0); $keepSettings = $event->arguments(1); 
		if($this->input->post('keep_settings')) {
			$event->arguments(1, true); 
		}
	}


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

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

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

		if(!$this->field || !$this->field->id) throw new WireException('No field specified');
		$templateIDs = $this->wire('input')->get('templates');
		if(!strlen($templateIDs)) throw new WireException("No templates specified"); 
		$templateIDs = explode(',', $templateIDs);
		
		$form = $this->wire('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 = $this->wire('templates')->get(abs($templateID)); 
			if(!$template) continue; 
			
			if($templateID < 0) {
				// remove from template
				
				$f = $this->wire('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) {
					
					$f = $this->wire('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); 
				}
			}
		}
		
		$f = $this->wire('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');
		$this->wire('session')->CSRF->validate();
		
		$fields = array($this->field);
		if($this->field->type instanceof FieldtypeFieldsetOpen) {
			$fieldsetClose = $this->wire('fields')->get($this->field->name . '_END');
			if($fieldsetClose) $fields[] = $fieldsetClose;
		}
	
		// first handle additions, since it's possible for removals to take a long time or remote chance of timeout
		foreach($this->wire('templates') as $template) {
	
			foreach($fields as $field) {
				// usually just 1 element in $fields, except when $field is a FieldtypeFieldsetOpen in which case 
				// there is also the corresponding FieldtypeFieldsetClose
				
				$addFieldID = $this->wire('input')->post("add_{$field->id}_template_$template->id");
				if(!$addFieldID) continue;
				if($addFieldID === '-') {
					// add as first field
					$template->fieldgroup->add($field);
					continue;
				}
				
				$before = substr($addFieldID, 0, 1) == '-';
				$addFieldID = (int) ltrim($addFieldID, '-');

				foreach($template->fieldgroup as $f) {
					if($f->id != $addFieldID) 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);
						$this->message(sprintf($this->_('Added field "%1$s" to template "%2$s" after "%3$s"'),
							$field->name, $template->name, $f->name));
					}
				}
			}
			$template->fieldgroup->save();
		}
		
		// next handle removals
		foreach($this->wire('templates') as $template) {
			$removeFieldID = (int) $this->wire('input')->post("remove_{$this->field->id}_template_$template->id");
			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();
					$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
					); 
				}
			}
		}
		
		$this->wire('session')->redirect("./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) {

		if(!$fieldgroup) return array();

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

		$fieldOriginal = $this->wire('fields')->get($field->id);
		$changes = array();

		$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',
		);

		foreach($contextArray as $key => $value) {
			$name = $key;
			$formFieldName = $key;
			if($formFieldName == 'label') $formFieldName = 'field_label';
			$formField = $form->getChildByName($formFieldName);
			$originalValue = $fieldOriginal->$key;
			if($formField) {
				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 = $this->wire('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 = $this->wire('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('|', ' ', $originalValueStr);
			$valueStr = str_replace('|', ' ', $valueStr);
			$change = array(
				"name" => $name, 
				"value" => $valueStr,
				"originalValue" => $originalValueStr
			);

			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($this->wire('languages') && preg_match('/^(label|description|notes)(\d+)$/', $key, $matches)) {
				$label = ucfirst($matches[1]) . ' (' . $this->wire('languages')->get((int) $matches[2])->get('title|name') . ')';
			} else {
				$label = $key;
			}
			$change["label"] = $label;
			$changes[$key] = $change;
		}
		return $changes;
	}


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

		$fields = $this->wire(new InputfieldWrapper());
		$modules = $this->wire('modules');

		$field = $modules->get("InputfieldCheckbox");
		$field->attr('name', 'listAfterSave');
		$field->attr('value', 1); 
		$field->attr('checked', empty($data['listAfterSave']) ? '' : 'checked'); 
		$field->label = $this->_("Return to fields list after saving a field?");
		$field->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.");
		$fields->append($field);

		return $fields;
	}

	/**
	 * 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
	 *
	 */
	public function ___fieldAdded(Field $field) { }

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

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

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

}

