<?php namespace ProcessWire;

/**
 * ProcessWire Template Process
 *
 * List and edit Templates that control page output.
 * 
 * For more details about how Process modules work, please see: 
 * /wire/core/Process.php 
 * 
 * ProcessWire 3.x, Copyright 2022 by Ryan Cramer
 * https://processwire.com
 * 
 * @method InputfieldForm getListFilterForm()
 * @method array|string executeNavJSON(array $options = array())
 * @method MarkupAdminDataTable getListTable($templates)
 * @method array getListTableRow(Template $template, $useLabel = true)
 * @method string executeAdd()
 * @method InputfieldForm buildEditForm(Template $template)
 * @method void fieldAdded(Field $field, Template $template) #pw-hooker
 * @method void fieldRemoved(Field $field, Template $template) #pw-hooker
 * @method void executeSaveFieldgroup($fieldgroup = null)
 *
 */

class ProcessTemplate extends Process implements ConfigurableModule {

	/** @var InputfieldForm */
	protected $form; 
	
	/** @var Template  */
	protected $template;
	
	/** @var int */
	protected $id; 
	
	/** @var int */
	protected $numPages = 0; 
	
	/** @var array */
	protected $moduleInfo; 
	
	/** @var array */
	protected $labels = array();

	/**
	 * Module info
	 * 
	 * @return array
	 * 
	 */
	public static function getModuleInfo() {
		return array(
			'title' => __('Templates', __FILE__),
			'summary' => __('List and edit the templates that control page output', __FILE__),
			'version' => 114,
			'permanent' => true, 
			'icon' => 'cubes', 
			'useNavJSON' => true, 
			'searchable' => 'templates', 
			// add this permission if you want this Process available for roles other than Superuser
			'permission' => 'template-admin', 
		);
	}

	/**
	 * Initialize the template process
	 *
	 */
	public function init() {

		$input = $this->wire()->input;
		$modules = $this->wire()->modules;
		$process = $this->wire()->process;
		
		$this->moduleInfo = self::getModuleInfo();
		
		if("$process" === "$this") {
			$this->headline($this->moduleInfo['title']);
		}

		$this->id = (int) $input->post('id');
		if(!$this->id) $this->id = (int) $input->get('id');

		if($this->id) { 
			$this->template = $this->templates->get($this->id); 
			if(!$this->template) throw new WireException("Unknown template"); 
			$this->numPages = $this->template->getNumPages();
		}
		
		$this->labels = array(
			'Yes' => $this->_('Yes'),
			'No' => $this->_('No'),
			'Either' => $this->_('Either / does not matter'),
			'Are you sure?' => $this->_('Are you sure?'),
			'Export' => $this->_('Export'),
			'Import' => $this->_('Import'),
			'Add New' => $this->_x('Add New Template', 'list-button'),
			'numPages' => sprintf($this->_n(
				'This template is used by %d page', 
				'This template is used by %d pages', 
				$this->numPages), $this->numPages
			),
			'templateNameFormat' => 
				$this->_('Template names may use letters (a-z A-Z), numbers (0-9), hyphens and underscores.') . ' ' .
				$this->_('Lowercase is optional but recommended.') . ' ' . 
				$this->_('Do not include a file extension.') . ' ' . 
				$this->_('Do not use only digits.'),
			'invalidTemplateName' => $this->_('Template name does not match required format.'),
			'nameAlreadyInUse' => $this->_('The name "%s" is already in use'),
			'name' => $this->_('Name'), 
			'label' => $this->_('Label'), 
			'type'  => $this->_('Type'), 
			'tags' => $this->_('Tags'), 
			'manageTags' => $this->_('Manage tags'), 
			'templates' => $this->_('Templates'),
		);

		if($input->urlSegment1) $modules->get('JqueryWireTabs'); 

		/** @var JqueryUI $jQueryUI */
		$jQueryUI = $modules->get('JqueryUI');
		$jQueryUI->use('modal');

		parent::init();
	}

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

	/**
	 * Output JSON list of navigation items for this (intended to for ajax use)
	 *
	 * @param array $options
	 * @return array|string
	 * 
	 */
	public function ___executeNavJSON(array $options = array()) {
		$templates = $this->wire()->templates;
		
		// pull icons out of pageLabelField and populate to an 'icon' property for JSON nav
		$templatesArray = array();
		$advanced = $this->wire()->config->advanced; 
		
		foreach($templates as $t) {
			if(!$advanced && ($t->flags & Template::flagSystem)) continue; 
			$templatesArray[] = $t;
		}
		
		$options['items'] = $templatesArray;
		$options['itemLabel'] = 'name';
		
		return parent::___executeNavJSON($options);
	}

	/**
	 * Execute the template list / default process
	 * 
	 * @return string
	 *
	 */
	public function ___execute() {
		
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;
		
		if($config->ajax) return $this->renderListJSON(); 

		$out = $this->getListFilterForm()->render() . "\n<div id='ProcessTemplateList'>\n";
		$templatesByTag = array();
		$untaggedLabel = $this->_('Untagged'); // Tag applied to untagged fields
		$systemLabel = $this->_x('System', 'tag'); // Tag applied to the group of built-in/system fields
		$hasFilters = $session->getFor($this, 'filterField');
		$showSystem = $session->getFor($this, 'filterSystem');
		$caseTags = array(); // indexed by lowercase version of tag
		$collapsedTags = $this->wire()->modules->getConfig($this, 'collapsedTags'); 
		
		if(!is_array($collapsedTags)) $collapsedTags = array();
		
		foreach($collapsedTags as $key => $tag) {
			$collapsedTags[$key] = strtolower($tag);
		}

		if(!$hasFilters) {
			foreach($this->wire()->templates as $template) {
				if($showSystem && ($template->flags & Template::flagSystem)) {
					$template->tags .= " $systemLabel";
				}
				if(!strlen($template->tags)) {
					$tag = strtolower($untaggedLabel);
					if(!isset($templatesByTag[$tag])) $templatesByTag[$tag] = array();
					$templatesByTag[$tag][$template->name] = $template;
					$caseTags[$tag] = $untaggedLabel;
					continue;
				}
				$tags = $template->getTags();
				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($templatesByTag[$tag])) $templatesByTag[$tag] = array();
					$templatesByTag[$tag][$template->name] = $template;
					if(!isset($caseTags[$tag])) $caseTags[$tag] = $caseTag;
				}
			}
		}
		
		$tagCnt = count($templatesByTag);
		
		if($tagCnt > 1) {
			/** @var InputfieldWrapper $form */
			$form = $this->wire(new InputfieldWrapper());
			ksort($templatesByTag);
			foreach($templatesByTag as $tag => $templates) {
				ksort($templates);
				$table = $this->getListTable($templates);
				if(!count($table->rows)) continue;
				/** @var InputfieldMarkup $f */
				$f = $modules->get('InputfieldMarkup');
				$f->entityEncodeLabel = false;
				$f->label = $caseTags[$tag];
				$f->icon = 'tags';
				$f->value = $table->render();
				if(in_array($tag, $collapsedTags)) $f->collapsed = Inputfield::collapsedYes; 
				$form->add($f);
			}
			$out .= $form->render();
		} else {
			$out .= $this->getListTable($this->templates)->render();
		}

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

		/** @var InputfieldButton $button */
		$button = $modules->get('InputfieldButton');
		$button->href = "./add";
		$button->value = $this->labels['Add New']; 
		$button->icon = 'plus-circle';
		$button->addClass('add_template_button'); 
		$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['manageTags'];
		$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, 'filterSystem');
			$session->removeFor($this, 'filterField');
		}

		return $out;
	}

	/**
	 * @return InputfieldForm
	 * @throws WireException
	 * @throws WirePermissionException
	 * 
	 */
	public function ___getListFilterForm() {
		
		$input = $this->wire()->input; 
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;

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

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

		/** @var InputfieldSelect $field */
		$field = $modules->get("InputfieldSelect"); 
		$field->attr('id+name', 'filter_field'); 
		$field->label = $this->_('Filter by field');
		$field->description = $this->_("Select a field and only templates using that field will be shown."); // Filter by Field, description
		$field->addOption('', $this->_x('Show All', 'filter-select'));
		$field->icon = 'cube';

		foreach($this->fields as $f) {
			$name = $f->name; 
			if(($f->flags & Field::flagSystem) || ($f->flags & Field::flagPermanent)) $name .= "*";
			$field->addOption($f->name, $name);	
		}	
		
		if($input->get('filter_field') !== null) {
			$filterField = $this->sanitizer->name($input->get('filter_field')); 
			$session->setFor($this, 'filterField', $filterField);
		} else {
			$filterField = $session->getFor($this, 'filterField');
		}
		
		$field->attr('value', $filterField); 
		$field->collapsed = $filterField ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
		$fieldset->add($field);
		
		if($filterField) $filterField = $this->wire()->fields->get($filterField);
		if($filterField) $form->description = sprintf($this->_('Templates with field: %s'), $filterField->name);

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

		if(!$filterField) { 
			/** @var InputfieldRadios $field */
			$field = $modules->get("InputfieldRadios"); 
			$field->label = $this->_("Show system templates?");
			$field->description = $this->_("By default, system/internal templates are not shown. Click 'Yes' to have them included in the templates list below."); // Show system templates, description
			$field->attr('id', 'filter_system');
			$field->attr('name', 'system'); 
			$field->addOption(1, $this->labels['Yes']); 
			$field->addOption(0, $this->labels['No']); 
			$field->optionColumns = 1;
			$field->icon = 'gear';
			if($input->get('system') !== null) {
				$filterSystem  = (int) $input->get('system'); 
				$session->setFor($this, 'filterSystem', $filterSystem);
			} else {
				$filterSystem = (int) $session->getFor($this, 'filterSystem');
			}
			$field->attr('value', $filterSystem); 
			$field->collapsed = $filterSystem ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
			$fieldset->add($field); 
		} else {
			$filterSystem = 0;
		}

		if($filterSystem || $filterField) {
			$fieldset->collapsed = Inputfield::collapsedNo;
		}

		return $form;
	}

	/**
	 * Get templates list table
	 * 
	 * @param WireArray|array $templates
	 * @return MarkupAdminDataTable
	 * 
	 */
	public function ___getListTable($templates) {

		/** @var MarkupAdminDataTable $table */
		$table = $this->wire()->modules->get("MarkupAdminDataTable"); 
		$table->setEncodeEntities(false); 
		$useLabels = false;
		$rows = array();
		
		foreach($templates as $template) {
			/** @var Template $template */
			$label = $template->getLabel();
			if($label && $label != $template->name) {
				$useLabels = true;
				break;
			}
		}

		foreach($templates as $template) {
			$row = $this->getListTableRow($template, $useLabels);
			if(!empty($row)) $rows[] = $row;
		}

		$header = array();
		$header[] = $this->_x('Name', 'list-thead');
		if($useLabels) $header[] = $this->_x('Label', 'list-thead');
		$header[] = $this->_x('Fields', 'list-thead');
		$header[] = $this->_x('Pages', 'list-thead');
		$header[] = $this->_x('Modified', 'list-thead');
		$header[] = '&nbsp;';
		
		$table->headerRow($header);
		
		foreach($rows as $row) {
			$table->row($row);
		}

		return $table;
	}

	/**
	 * Get row for templates list table
	 * 
	 * @param Template $template
	 * @param bool $useLabel
	 * @return array
	 * 
	 */
	public function ___getListTableRow(Template $template, $useLabel = true) {
		
		$session = $this->wire()->session;
		$config = $this->wire()->config;

		$editUrl = "edit?id=$template->id";
		$flags = array();
		
		$flagDefs = array(
			'system' => array(
				'label' => $this->_x('system', 'list-note'),
				'icon' => 'puzzle-piece',
				'href' => ($config->advanced ? "$editUrl#find-flagSystem" : $editUrl),
			),
			'access' => array(
				'label' => $this->_x('access', 'list-note'),
				'icon' => 'key',
				'href' => "$editUrl#find-tab_access",
			),
			'no-file' => array(
				'label' => $this->_x('no-file', 'list-note'),
				'icon' => 'ban',
				'href' => "$editUrl#find-tab_files",
			),
			'cache' => array(
				'label' => $this->_x('cache', 'list-note'),
				'icon' => 'clock-o',
				'href' => "$editUrl#find-tab_cache",
			),
			'procache' => array(
				'label' => $this->_x('ProCache', 'list-note'),
				'icon' => 'fighter-jet',
				'href' => "$editUrl#find-tab_cache",
			),
			'url-segments' => array(
				'label' => $this->_x('URL segments', 'list-note'),
				'icon' => 'sitemap',
				'href' => "$editUrl#find-urlSegments",
			),
			'page-numbers' => array(
				'label' => $this->_x('Page numbers', 'list-note'),
				'icon' => 'list-ol',
				'href' => "$editUrl#find-allowPageNum",
			)
		);

		$filterField = $session->getFor($this, 'filterField');
		$filterSystem = $session->getFor($this, 'filterSystem');
		$notes = '';

		if($template->flags & Template::flagSystem) {
			if(!$filterSystem && !$filterField) return array();
			$flags[] = 'system';
		}
		
		if($filterField && !$template->fieldgroup->has($filterField)) return array();
		
		if($template->useRoles) $flags[] = 'access';
		if(!$template->filenameExists()) $flags[] = 'no-file';
		if($template->cache_time > 0) $flags[] = 'cache';
		if($template->cache_time < 0) $flags[] = 'procache';
		if($template->urlSegments) $flags[] = 'url-segments';
		if($template->allowPageNum) $flags[] = 'page-numbers';

		foreach($flags as $flag) {
			$class = 'templateFlag' . ucfirst($flag);
			$flag = $flagDefs[$flag]; 
			$label = str_replace('-', ' ', ucfirst($flag['label']));
			$href = empty($flag['href']) ? '#' : $flag['href'];
			$icon = wireIconMarkup($flag['icon'], 'fw'); 
			$notes .= "<a href='$href' class='tooltip templateFlag $class' title='$label'>$icon</a> ";
		}

		$numPages = $template->getNumPages();
		$numPagesLink = $config->urls->admin . "page/search/?show_options=1&template={$template->name}&sort=title";
		$numFields = count($template->fieldgroup);
		
		if($template->fieldgroup && $template->fieldgroup->name != $template->name) {
			$numFields .= " " . $template->fieldgroup->name;
		}
	
		$row = array();
		$row["{$template->name} "] = $editUrl;
		
		if($useLabel) {
			$icon = $template->getIcon();
			$icon = $icon ? wireIconMarkup($icon, 'fw') : '';
			$row[] = $icon . $this->sanitizer->entities($template->getLabel());
		}
		
		$row[] = "$numFields ";
		$row["{$numPages} "] = "$numPagesLink"; // space is required to make it work
		
		$mod = $template->modified; 
		$row[] = $mod > 0 ? "<span style='display: none;'>$mod </span>" . wireRelativeTimeStr($mod) : '';
		$row[] = $notes;
		
		return $row; 
	}

	/**
	 * Execute the template add process
	 * 
	 * @return string
	 *
	 */
	public function ___executeAdd() {
		
		$config = $this->wire()->config;
		$sanitizer = $this->wire()->sanitizer;
		$templates = $this->wire()->templates;
		$fieldgroups = $this->wire()->fieldgroups;
		$input = $this->wire()->input;
		$session = $this->wire()->session;

		$this->breadcrumb('./', $this->moduleInfo['title']); 
		$this->headline($this->_('Add New Templates'));

		$templateFiles = array();
		$ext = "." . $config->templateExtension; 
		$prependTemplateFile = $config->prependTemplateFile;
		$appendTemplateFile = $config->appendTemplateFile;
		$ignoreRegex = $config->ignoreTemplateFileRegex;
		
		$dir = new \DirectoryIterator($config->paths->templates);

		foreach($dir as $file) {
			if($file->isDir() || $file->isDot()) continue; 
			$filename = $file->getFilename();
			if($filename == $prependTemplateFile || $filename == $appendTemplateFile) continue; // skip over prepend/append files
			if(substr($filename, -1 * strlen($ext)) != $ext) continue; 
			if($ignoreRegex && preg_match($ignoreRegex, $filename)) continue;
			$basename = basename($file->getFilename(), $ext); 
			if(strpos($basename, '.') === 0) continue;
			if($sanitizer->name($basename) !== $basename) continue;
			if(ctype_digit($basename)) continue;
			// if(count($templates->find("name=$basename"))) continue; 
			if($templates->get($basename)) continue;
			$templateFiles[] = $basename; 
		}

		if(!$input->is('post')) {
			// render form
			$form = $this->buildAddForm($templateFiles);
			if(count($input->post)) $form->processInput($input->post);
			return $form->render();
		}
		
		// process submitted form

		$session->CSRF->validate();

		$importFieldgroup = $input->post->name('import_fieldgroup');
		$importFields = array();

		if($importFieldgroup) {
			$importFieldgroup = $fieldgroups->get($importFieldgroup); 	
		} else {
			// find global fields
			$importFieldgroup = null;
			foreach($this->wire()->fields as $field) {
				if($field->flags & Field::flagGlobal) {
					$importFields[] = $field; 
				}
			}
		}

		$postTemplates = $input->post('templates');
		if(!is_array($postTemplates)) $postTemplates = array();
		
		$templateName = trim((string) $input->post('template_name')); 

		// add any templates without files to the postTemplates
		if(strlen($templateName)) {
			if(strpos($templateName, ' ')) {
				$templateNames = explode(' ', $templateName);
			} else {
				$templateNames = array($templateName);
			}
			foreach($templateNames as $name) {
				$name = $sanitizer->name(basename($name)); 
				if(!strlen($name)) continue;
				if(ctype_digit($name)) {
					$this->error(sprintf($this->_('Template name “%s” not added because it contains only digits.'), $name));
					continue;
				}
				if($templates->get($name) || $fieldgroups->get($name)) {	
					$this->error(sprintf($this->labels['nameAlreadyInUse'], $name)); 
				} else {
					$postTemplates[] = $name;
				}
			}
		}
		
		$addedTemplates = array();

		foreach($postTemplates as $basename) {
			
			$basename = $sanitizer->name($basename);
			if(!strlen($basename)) continue; 
			
			try {
				/** @var Template $template */
				$template = $this->wire(new Template());
				$template->name = $basename;
				/** @var Fieldgroup $fieldgroup */
				$fieldgroup = $this->wire(new Fieldgroup());
				$fieldgroup->name = $template->name;
				$fieldgroup->save();
				
				$template->fieldgroup = $fieldgroup;
				$template->roles = array($this->wire()->roles->getGuestRole()->id); 
				$template->save();
				
				$this->message(sprintf($this->_('Added template and fieldgroup: %s'), $template->name)); 
				$addedTemplates[] = $template;
				
			} catch(\Exception $e) {
				$this->error($e->getMessage()); 
				continue; 
			}

			if($importFieldgroup) {
				$this->importFieldgroup($importFieldgroup, $template); 
				$template->fieldgroup->save();

			} else if(count($importFields)) { // global fields
				foreach($importFields as $field) { 
					$template->fieldgroup->add($field);
					$this->fieldAdded($field, $template);	
				}
				$template->fieldgroup->save();
			}
		}
		
		if(count($addedTemplates) === 1) {
			// redirect to edit template when only one has been added
			/** @var Template $template */
			$template = reset($addedTemplates);
			$session->redirect($template->editUrl());
		} else {
			$session->redirect('./');
		}
		
		return ''; // never reached
	}	

	/**
	 * Build the form used for adding templates 
	 * 
	 * @param array $templateFiles
	 * @return InputfieldForm
	 *
	 */ 
	protected function buildAddForm($templateFiles) {
		
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;

		$templateUrl = $config->urls->templates; 
		
		/** @var InputfieldForm $form */
		$form = $modules->get('InputfieldForm');
		$form->attr('id', 'ProcessTemplateAdd'); 
		$form->attr('action', 'add'); 
		$form->attr('method', 'post'); 

		if(count($templateFiles)) { 
			/** @var InputfieldCheckboxes $field */
			$field = $modules->get('InputfieldCheckboxes');
			$field->label = sprintf($this->_('Templates found in: %s'), "$templateUrl*.{$config->templateExtension}"); 
			$field->description = $this->_('The following new templates were found. Check the box next to each template you want to add.'); // Templates found, description
			$field->attr('id+name', 'templates'); 
			$field->icon = 'search-plus';
			foreach($templateFiles as $file) {
				$name = pathinfo($file, PATHINFO_FILENAME); 
				if(ctype_digit($name)) continue;
				$field->addOption($file);
			}
			$form->append($field); 
		} else {
			$this->warning(sprintf(
				$this->_('No new template files were found in: %s'), // Error message when no new templates found
				"$templateUrl*.{$config->templateExtension}"
			)); 
		}

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->label = $this->_('Create a new template without a file');
		$field->description = 
			$this->_('If you want to create a new template even though there is currently no file associated with it, enter the name of the template here.') . ' ' .  // Create template with no file, description
			$this->_('You may enter multiple template names by separating each with a space.'); 
		$field->notes = $this->labels['templateNameFormat'];
		$field->attr('id+name', 'template_name'); 
		$field->set('pattern', '^[-_.a-zA-Z0-9 ]*$');
		$field->icon = 'plus-circle';
		$field->collapsed = count($templateFiles) ? Inputfield::collapsedYes : Inputfield::collapsedNo; 
		$form->append($field); 

		$form->append($this->buildEditFormActionsImportFieldgroup()); 

		/** @var InputfieldSubmit $field */
		$field = $modules->get('InputfieldSubmit');
		$field->attr('value', $this->_n('Add Template', 'Add Templates', count($templateFiles))); 
		$form->append($field); 

		return $form; 
	}

	/**
 	 * Execute the template edit process
	 * 
	 * @return string
	 * @throws WireException
	 *
	 */
	public function ___executeEdit() {
		
		if(!$this->template) throw new WireException("No Template specified");
		
		$config = $this->wire()->config;
		
		if(substr($this->wire()->input->url(), -1) === '/') {
			// we require non-trailing slash for edit requests
			$this->wire()->session->redirect("../edit?id={$this->template->id}");
		}
		
		$min = $config->debug ? '' : '.min';
		$config->scripts->add($config->urls('ProcessTemplate') . "ProcessTemplateFieldCreator$min.js?v=$config->version");
		
		$label = $this->template->getLabel();
		if(!$label) $label = $this->template->name;
		if($label != $this->template->name) $label = "$label ({$this->template->name})";
		
		$headline = sprintf($this->_('Edit Template: %s'), $label); // Headline when editing a template

		$this->breadcrumb('./', $this->moduleInfo['title']); 
		$this->headline($headline);
		$this->browserTitle($headline);
		$this->form = $this->buildEditForm($this->template);
		
		return $this->form->render();
	}	

	/**
	 * Build the main form used for template edits
	 * 
	 * @param Template $template
	 * @return InputfieldForm
	 *
	 */
	protected function ___buildEditForm(Template $template) {
		
		$input = $this->wire()->input;
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;

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

		if(!is_file($template->filename) && !count($input->post) && !$input->get('modal')) {
			$this->message(sprintf(
				$this->_('Pages using this template are not viewable because the template file (%s) does not exist [no-file].'), 
				$template->name . '.' . $config->templateExtension
			)); 
		}

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Basics', 'tab'));
		$t->attr('class', 'WireTab');
		$this->buildEditFormBasics($template, $t);
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Access', 'tab'));
		$t->attr('id', 'tab_access');
		$t->attr('class', 'WireTab'); 
		$t->add($this->buildEditFormAccess($template)); 
		$overrides = $this->buildEditFormAccessOverrides($template);
		if($overrides) $t->add($overrides);
		$t->add($this->buildEditFormAccessFiles($template));
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Family', 'tab'));
		$t->attr('class', 'WireTab'); 
		$t->attr('id', 'tab_family');
		$t->add($this->buildEditFormFamily($template)); 
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('URLs', 'tab'));
		$t->attr('class', 'WireTab'); 
		$t->attr('id', 'tab_urls');
		$t->add($this->buildEditFormURLs($template)); 
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Files', 'tab'));
		$t->attr('class', 'WireTab');
		$t->attr('id', 'tab_files');
		$t->add($this->buildEditFormFile($template));
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Cache', 'tab'));
		$t->attr('class', 'WireTab'); 
		$t->attr('id', 'tab_cache');
		$t->add($this->buildEditFormCache($template)); 
		$form->add($t);

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('title', $this->_x('Advanced', 'tab'));
		$t->attr('class', 'WireTab'); 
		$t->attr('id', 'tab_advanced');
		$t->add($this->buildEditFormAdvanced($template)); 
		$form->add($t); 

		if($config->advanced) {
			/** @var InputfieldWrapper $t */
			$t = $this->wire(new InputfieldWrapper());
			$t->attr('title', $this->_x('System', 'tab'));
			$t->attr('class', 'WireTab'); 
			$t->attr('id', 'tab_system');
			$t->add($this->buildEditFormSystem($template)); 
			$form->add($t); 
		}

		/** @var InputfieldWrapper $t */
		$t = $this->wire(new InputfieldWrapper());
		$t->attr('class', 'WireTab'); 
		$t->attr('title', $this->_x('Actions', 'tab'));
		$t->attr('id', 'tab_actions');
		$t->add($this->buildEditFormActions($template));
		$form->add($t);

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

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

		/** @var InputfieldSubmit $field */
		$field = $modules->get('InputfieldSubmit');
		$field->attr('value', $this->_x('Save', 'button')); 
		$field->showInHeader();
		$form->append($field); 

		return $form; 
	}

	/**
	 * Build the "basics" tab of the edit form
	 * 
	 * @param Template $template
	 * @param InputfieldWrapper $t Form wrapper that gets populated
	 * @throws WireException
	 * 
	 */
	protected function buildEditFormBasics(Template $template, InputfieldWrapper $t) {
		
		$languages = $this->wire()->languages;
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;

		$t->add($this->buildEditFormFields($template));
		
		/** @var InputfieldHidden $f */
		$f = $modules->get('InputfieldHidden');
		$f->attr('id+name', '_fieldgroup_fields_changed');
		$f->attr('value', '');
		$t->add($f);

		$script = 'script';
		$t->appendMarkup = "<$script>$(document).ready(function() { ProcessTemplateInitFields(); });</$script>";
		
		if(in_array($template->id, $config->userTemplateIDs)) {
			/** @var ProcessProfile $profile */
			$profile = $modules->get('ProcessProfile');
			$profileInputs = $profile->getModuleConfigInputfields(array('profileFields' => $profile->profileFields));
			$f = $profileInputs->getChildByName('profileFields');
			$f->attr('id+name', '_user_profile_fields');
			$f->description = $this->_('If you just added fields above, save first and then return here to select them.'); 
			$t->add($f);
		}

		if($template->name != $template->fieldgroup->name || $config->advanced) {
			$t->add($this->buildEditFormFieldgroup($template));
		}
	
		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('name', 'templateLabel');
		$f->attr('value', $template->label);
		$f->icon = 'tag';
		$f->label = $this->_x('Label', 'field-label');
		$f->description = $this->_('An optional label to describe this template.');
		$f->collapsed = Inputfield::collapsedBlank;
		if($languages) {
			$f->useLanguages = true;
			foreach($languages as $language) {
				$f->set('value' . $language->id, $template->get('label' . $language->id));
			}
		}
		$t->add($f);
		
		$icon = $template->getIcon(true);
		/** @var InputfieldIcon  $f */
		$f = $modules->get("InputfieldIcon");
		$f->attr('name', 'pageLabelIcon');
		$f->attr('value', $icon);
		$f->icon = $icon ? $icon : 'puzzle-piece';
		$f->label = $this->_('Icon');
		$f->description = $this->_('Select an icon that will be associated with this template and pages using this template (in the admin).');
		$f->collapsed = Inputfield::collapsedBlank;
		$t->add($f);
		
		if(!$this->numPages || version_compare(PHP_VERSION, '5.3.8', '<')) return;

		/** @var JqueryCore $jquery */
		$jquery = $modules->get('JqueryCore');
		$jquery->use('iframe-resizer');
		
		/** @var InputfieldMarkup $f */
		$f = $modules->get('InputfieldMarkup');
		$f->label = 
			$this->_('Usage') . ' ' . 
			'(' . sprintf($this->_n('%d page', '%d pages', $this->numPages), $this->numPages) . ')';
		$f->set('template', $this->template);
		$f->collapsed = Inputfield::collapsedYesAjax;
		$f->markupFunction = function($inputfield) use($modules) {
			// bookmarks for Lister
			/** @var Inputfield $inputfield */
			$modules->includeModule('ProcessPageLister');
			$windowMode = ProcessPageLister::windowModeBlank;
			$bookmark = array(
				'initSelector' => '', 
				'defaultSelector' => "template=" . $inputfield->get('template') . ", include=all", 
				'columns' => array('title', 'path', 'modified', 'modified_users_id'),
				'toggles' => array('noButtons'),
				'viewMode' => $windowMode,
				'editMode' => $windowMode,
				'editOption' => 0,
			);
			$id = "template_" . $inputfield->get('template') . "_pages";
			$url = ProcessPageLister::addSessionBookmark($id, $bookmark) . '&modal=inline&minimal=1';
			$script = 'script';
			if($url) return "
				<iframe id='ProcessTemplatePageLister' scrolling='no' style='width:100%; border: none;' src='$url'></iframe>
				<$script>$('#ProcessTemplatePageLister').iFrameResize({ });</$script>
			";
			return '';
		};
		$icon = $template->getIcon();
		if(!$icon) $icon = 'search';
		$f->icon = $icon;
		$t->add($f);
	}

	/**
	 * Build the "delete" checkbox of the edit form
	 * 
	 * @param Template $template
	 * @return InputfieldCheckbox
	 * 
	 */
	protected function buildEditFormDelete(Template $template) {
		
		$modules = $this->wire()->modules;
		$fieldgroups = $this->wire()->fieldgroups;
		$templates = $this->wire()->templates;

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

		$fieldgroup = $fieldgroups->get($template->name); 
		$numFieldgroupTemplates = 0;
		
		if($fieldgroup) {
			foreach($templates as $tpl) {
				if($tpl->id == $template->id) continue; 
				if($tpl->fieldgroup && $tpl->fieldgroup->id == $fieldgroup->id) $numFieldgroupTemplates++;
			}
		}

		$nopeLabel = $this->_('This template may not be deleted');
		$naLabel = $this->_('Not available for reason listed below');

		if($template->flags & Template::flagSystem) {
			$field->label2 = $naLabel;
			$field->notes = $nopeLabel . ' - ' . $this->_('System templates cannot be deleted'); 
			$field->attr('disabled', 'disabled'); 

		} else if($this->numPages > 0) {
			$field->label2 = $naLabel; 
			$field->notes = 
				$nopeLabel . ' - ' . 
				sprintf(
					$this->_n('Template is used by 1 page', 'Template is used by %d pages', $this->numPages), // Template can't be deleted because it's in use
					$this->numPages
				); 
			$field->attr('disabled', 'disabled'); 

		} else if($numFieldgroupTemplates > 0) {
			$field->label2 = $naLabel; 
			$field->notes = 
				$nopeLabel . ' - ' . 
				sprintf(
					$this->_n('This template’s fieldgroup is used by 1 other template', 'This template’s fieldgroup is used by %d other templates', $numFieldgroupTemplates), // Template can't be deleted because it's fieldgroup is in use
					$numFieldgroupTemplates
				); 
			$field->attr('disabled', 'disabled'); 

		} else {
			$field->label2 = $this->_('Confirm deletion'); 
			$field->description = $this->_('Note that deleting the template only removes it from the database, it does not delete the template file on disk.'); // Note about template files not being deleted
		}

		return $field;
	}

	/**
	 * Build the fieldgroup box for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldSelect
	 * 
	 */
	protected function buildEditFormFieldgroup(Template $template) {
		
		$modules = $this->wire()->modules;
		$fieldgroups = $this->wire()->fieldgroups;
		$templates = $this->wire()->templates;

		/** @var InputfieldSelect $field */
		$field = $modules->get('InputfieldSelect');
		$field->label = $this->_x("Fieldgroup", 'field-label');
		$field->attr('id+name', 'fieldgroup'); 
		$field->attr('value', $template->fieldgroup->id); 
		$field->required = true; 
		$field->icon = 'cubes';
		$field->description = $this->_("By default, each template manages it's own group of fields. If you want to have this template use the fields from another template, select it here."); // Fieldgroup description
		
		foreach($fieldgroups->getAll()->sort('name') as $fieldgroup) {
			/** @var Fieldgroup $fieldgroup */
			$name = $fieldgroup->name; 
			if($name === $template->name) {
				$name .= ' ' . $this->_('(default)'); // Label appended to default fieldgroup name in select box
			} else {
				$tpl = $templates->get($name); 
				// if the template is not using it's own fieldgroup, don't include it in the list
				if($tpl && $tpl->name && $tpl->fieldgroup && $tpl->fieldgroup->name != $tpl->name) continue; 
			}
			$field->addOption($fieldgroup->id, $name); 
		}

		if($template->fieldgroup->name == $template->name) {
			$field->collapsed = Inputfield::collapsedYes; 
		}

		return $field; 
	}

	/**
	 * Just show the fields that this template's fieldgroup uses (informational)
	 * 
	 * @param Template $template
	 * @return InputfieldMarkup
	 *
	 */
	protected function buildEditFormShowFields(Template $template) {
		$modules = $this->wire()->modules;
		
		/** @var MarkupAdminDataTable $table */
		$table = $modules->get('MarkupAdminDataTable'); 
		
		foreach($template->fieldgroup as $field) {
			$table->row(array($field->name)); 
		}
		
		/** @var InputfieldMarkup $field */
		$field = $modules->get('InputfieldMarkup'); 
		$field->value = $table->render();
		$field->label = $this->_x('Fields', 'field-label (non-default fieldgroup)');
		$field->description = sprintf(
			$this->_('For your reference, this is a list of fields used by this template. This template gets its fields from the “%s” template.'), // Description of where the template's fields come from
			$template->fieldgroup->name
		); 
		
		return $field; 
	}

	/**
	 * Edit the fields that are part of this template
	 * 
	 * @param Template $template
	 * @return InputfieldAsmSelect|InputfieldMarkup
	 *
	 */
	protected function buildEditFormFields(Template $template) {
		
		$config = $this->wire()->config;
		$fields = $this->wire()->fields;

		// if this template isn't defining its fields, then just show what it's using
		if($template->fieldgroup->name != $template->name) {
			return $this->buildEditFormShowFields($template);
		}
	
		if(strpos($template->name, 'field-') === 0) {
			list(,$fieldName) = explode('-', $template->name, 2);
			$forField = $fields->get($fieldName);
		} else {
			$forField = null;
		}

		$editLink = 
			$config->urls->admin . 
			"setup/field/edit?id={value}&fieldgroup_id={$template->fieldgroup->id}&modal=1&process_template=1";

		/** @var InputfieldAsmSelect $select */
		$select = $this->wire()->modules->get('InputfieldAsmSelect'); 
		$select->label = $this->_x('Fields', 'field-label');
	
		if($forField) {
			$select->description = sprintf(
				$this->_('Define the custom fields for “%s”. It is not necessary to configure anything else here.'), 
				$forField->name
			);
		} else {
			$select->description = 
				$this->_('Select the fields that are used by this template and drag/drop to the desired order.') . ' ' . 
				sprintf(
					$this->_('You may also click field name or label to edit in the context of this template, click/drag the percent indicator to adjust width, or [create a new field](%s).'),
					'../field/add'
				);
			$select->notes = $this->_('Save this template to make newly added fields editable here.'); 
		}
		
		$select->icon = 'cube';
		$select->attr('name', 'fieldgroup_fields'); 
		$select->attr('id', 'fieldgroup_fields'); 
		$select->attr('title', $this->_('Add Field')); 
		$select->attr('data-closeAddLabel', $this->_('Close and Add'));
		$select->setAsmSelectOption('sortable', true);
		$select->setAsmSelectOption('fieldset', true); 
		$select->setAsmSelectOption('editLink', $editLink); 
		$select->setAsmSelectOption('hideDeleted', false);
		
		foreach($template->fieldgroup as $field) {
			$field = $template->fieldgroup->getField($field->id, true); // get in context
			$attrs = $this->getAsmListAttrs($field, true); 
			$attrs['selected'] = 'selected';
			$select->addOption($field->id, $field->name, $attrs);
		}
		
		foreach($fields as $field) {
			if($template->fieldgroup->has($field)) continue; 
			if(($field->flags & Field::flagPermanent) && !$config->advanced) continue; 
			$name = $field->name; 
			if($field->flags & Field::flagSystem) $name .= "*";
			$attrs = $this->getAsmListAttrs($field, false); 
			$select->addOption($field->id, $name, $attrs); 
		}
	
		return $select; 
	}

	/**
	 * Get attributes for the asmSelect fields selection
	 * 
	 * @param Field $field
	 * @param bool|null $hasField Does template currently have this field? (or null if column width adjustment not supported)
	 * @return array
	 * 
	 */
	public function getAsmListAttrs(Field $field, $hasField = null) {
		
		$sanitizer = $this->wire()->sanitizer;
		
		$desc = '';
		$fieldType = '';
		
		if(!$field->type instanceof FieldtypeFieldsetClose) {
			$desc = $field->label;
			$maxDesc = 30;
			if(strlen($desc) > $maxDesc) {
				$desc = substr($desc, 0, $maxDesc);
				$pos = strrpos($desc, ' ');
				if($pos) $desc = substr($desc, 0, strrpos($desc, ' '));
				$desc .= '…';
			}

			$fieldType = "<span class='fieldType'>" . str_replace('Fieldtype', '', $field->type) . "</span>";
		}
	
		$icons = '';
		if($field->required) $icons .= wireIconMarkup('asterisk');
		if($field->showIf) $icons .= wireIconMarkup('question-circle');
		$icons = "<span class='fieldInfo'>$icons</span>";

		$width = $field->columnWidth > 0 && $field->columnWidth <= 100 ? $field->columnWidth : 100;
		if($hasField) {
			$widthLabel = $sanitizer->entities1($this->_('Click and drag to adjust field width'));
			$columnWidth = "<span class='columnWidth ui-priority-secondary' data-was='$width' data-field='$field->id' title='$widthLabel'>$width%</span>";
		} else {
			if($hasField === null) {
				$widthLabel = '';
			} else {
				$widthLabel = $sanitizer->entities1($this->_('Save template before adjusting width of new field'));
			}
			$columnWidth = "<span class='columnWidth columnWidthOff ui-priority-secondary' title='$widthLabel'>$width%</span>";
		}

		$attrs = array(
			'data-status' => "$icons $fieldType $columnWidth",
			'data-desc' => $sanitizer->entities($desc),
		);

		if($field->icon) {
			$attrs['data-handle'] = wireIconMarkup($field->icon, 'fw');
		}
		
		return $attrs; 
	}
	
	/**  
	 * Build the "Actions" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormActions(Template $template) {
		
		$modules = $this->wire()->modules;

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		
		if(!($template->flags & Template::flagSystem)) {
			/** @var InputfieldName $field */
			$field = $modules->get('InputfieldName');
			$field->label = $this->_('Rename template');
			$field->description = $this->_('The name used to refer to this template. This is also the default filename of the template file (with .php extension) in /site/templates/.') . ' '; // Rename template, description
			$field->icon = 'code';
			$field->required = false;
			if(basename($template->filename, '.php') == $template->name) {
				if(is_writable($template->filename)) {
					$field->description .= $this->_('The template filename is writable and will be renamed as well.'); // Rename template, filename writable, description
				} else {
					$field->description .= $this->_('The template file is not writable so you will have to rename it manually (instructions will be provided after you save).'); // Rename template, filename not writable, description
				}
			}
			$field->notes = $this->labels['templateNameFormat'];
			$field->attr('id+name', 'rename');
			$field->attr('value', $template->name);
			$field->collapsed = Inputfield::collapsedYes;
			$form->append($field);
		}

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

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText');
		$field->attr('id+name', 'clone_template');
		$field->label = $this->_('Duplicate/clone this template');
		$field->description = $this->_('Enter the name of the new template you want to create. The clone will be created when you save. If your templates directory is writable and the template has a file, it will also be cloned.'); // Clone template, description
		$field->notes = $this->labels['templateNameFormat'];
		$field->collapsed = Inputfield::collapsedYes;
		$field->icon = 'clone';
		$form->append($field);

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

		$form->add($this->buildEditFormActionsImportFieldgroup()); 
		$form->add($this->buildEditFormDelete($template));
		
		return $form;
	}

	/**
	 * @return InputfieldSelect
	 * 
	 */
	protected function buildEditFormActionsImportFieldgroup() {
		
		$config = $this->wire()->config;
		
		/** @var InputfieldSelect $field */
		$field = $this->wire()->modules->get('InputfieldSelect');
		$field->label = $this->_('Copy fields from another template');
		$field->description = $this->_('If you want to copy fields used by another template, select it here. Fields already present in this template will be left alone.'); // Duplicate fields, description
		$field->icon = 'cube';
		$field->attr('id+name', 'import_fieldgroup');
		$field->addOption('');
		$field->attr('value', '');
		$field->collapsed = Inputfield::collapsedYes;

		foreach($this->wire()->fieldgroups->find('sort=name') as $fieldgroup) {
			/** @var Fieldgroup $fieldgroup */
			$template = $this->templates->get($fieldgroup->name);
			if($template && ($template->flags & Template::flagSystem) && !$config->advanced) continue;
			if($this->template && $fieldgroup->name == $this->template->name) continue;
			$field->addOption($fieldgroup->name);
		}
		
		return $field;
	}

	/**
	 * Build the "cache" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormCache(Template $template) {
		
		$modules = $this->wire()->modules;

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
	
		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios');
		$field->attr('name', '_cache_status');
		$field->label = $this->_('Cache Status');
		$field->icon = 'toggle-on';
		$field->addOption(0, $this->_('Disabled'));
		$field->addOption(1, $this->_('Enabled (template cache)'));
		
		if($modules->isInstalled('ProCache')) {
			$field->addOption(2, $this->_('Enabled (ProCache, configured here)'));
			/** @var WireData $procache */
			$procache = $modules->get('ProCache');
			if(in_array($template->id, $procache->get('cacheTemplates')) && $template->cache_time > -1) {
				$field->notes = $this->_('Pages using this template are currently cached in ProCache, but configured directly in ProCache. To configure the ProCache settings for this template here, choose the ProCache option above.'); // notes for ProCache option
			}
		}
		
		if($template->cache_time == 0) {
			$field->attr('value', 0);
		} else if($template->cache_time < 0) {
			$field->attr('value', 2);
		} else {
			$field->attr('value', 1);
		}
		
		$form->append($field);
		
		// --------------------

		/** @var InputfieldInteger $field */
		$field = $modules->get('InputfieldInteger'); 
		$field->attr('name', 'cache_time');
		$field->label = $this->_x('Cache Time', 'field-label');
		$field->icon = 'clock-o';
		$field->description = $this->_('To cache the output of this template, enter the time (in seconds) that the output should be cached. Caching can help significantly with page render time on resource-heavy pages. But caching should not be used on templates that need to process constantly changing data, like from forms or sessions. Also note that URL segments are cachable, but GET and POST vars are not.'); // Cach time description
		$field->notes = $this->_('For example: 60 = 1 minute, 600 = 10 minutes, 3600 = 1 hour, 86400 = 1 day, 604800 = 1 week, 2419200 = 1 month.'); // Cache time notes/examples
		$field->attr('value', abs($template->cache_time)); 
		$field->showIf = '_cache_status!=0';
		$field->requiredIf = '_cache_status!=0';
		$form->append($field); 

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

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('name', 'cacheExpire'); 
		$field->label = $this->_('Page Save / Cache Expiration');
		$field->description = $this->_('When a page using this template is saved, what should happen to the cache?');
		$field->addOption(Template::cacheExpirePage, $this->_('Clear cache for the saved page only (default)')); 
		$field->addOption(Template::cacheExpireSite, $this->_('Clear cache for entire site*')); // Clear cache for entire site // Note you should include the '*' to relate this item to the Cache expiration notes
		$field->addOption(Template::cacheExpireParents, $this->_('Clear cache for the saved page and parents (including homepage)')); 
		$field->addOption(Template::cacheExpireSpecific, $this->_('Clear cache for the saved page and other pages that I specify...'));
		$field->addOption(Template::cacheExpireSelector, $this->_('Clear cache for the saved page and other pages matching selector...'));
		$field->addOption(Template::cacheExpireNone, $this->_('Do nothing'));
		$field->attr('value', (int) $template->cacheExpire); 
		$field->notes = $this->_('*To maximize performance, cache files are all expired as a group rather than cleared individually.'); // Cache expiration notes // This explains the 'Clear cache for entire site*' option
		$field->showIf = "_cache_status!=0";
		$form->append($field); 

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

		/** @var InputfieldPageListSelectMultiple $field */
		$field = $modules->get('InputfieldPageListSelectMultiple'); 
		$field->attr('name', 'cacheExpirePages'); 
		$field->label = $this->_('Specify the other pages that should have their cache cleared'); 
		$field->description = $this->_('When pages using this template are saved, their cache files will be cleared. Select the other pages that should also have their cache files cleared below.'); // Specified cache clear pages, description
		$value = is_array($template->cacheExpirePages) ? $template->cacheExpirePages : array();
		$field->attr('value', $value);
		$field->showIf = '_cache_status!=0, cacheExpire=' . Template::cacheExpireSpecific;
		$form->append($field); 

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

		if($template->cacheExpire == Template::cacheExpireSelector) {
			/** @var InputfieldSelector $field */
			$field = $modules->get('InputfieldSelector');
			if(!$field) $field = $modules->get('InputfieldText'); 
			$field->attr('name', 'cacheExpireSelector');
			$field->description=  $this->_('Selector to find the other pages that should have their cache cleared.');
			$field->attr('value', $template->cacheExpireSelector);
		} else {
			/** @var InputfieldMarkup $field */
			$field = $modules->get('InputfieldMarkup');
			$field->value = $this->_('Save this template and then come back here to configure your selector.');
		}
		
		$field->label = $this->_('Cache expiration selector');
		$field->showIf = '_cache_status!=0, cacheExpire=' . Template::cacheExpireSelector;
		$form->append($field);
		
		// --------------------

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('name', 'useCacheForUsers'); 
		$field->label = $this->_('Cache when rendering pages for these users');
		$field->addOption(0, $this->_('Guests only')); 
		$field->addOption(1, $this->_('Guests and logged-in users'));
		$field->attr('value', (int) $template->useCacheForUsers); 
		$field->notes = $this->_('Note that the cache is always disabled for pages where the user has edit access, regardless of what you select here.'); // Cache for guest/loggin-in user notes
		$field->showIf = '_cache_status=1';
		$form->append($field); 

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

		$labelGET = $this->_('Cache disabling GET variables');
		$labelPOST = $this->_('Cache disabling POST variables');
		$description = $this->_('When your template output is cached, variables of this type are ignored by default. You can optionally specify one or more variable names that will disable the cache for that request, causing the page to be rendered sans cache.'); // Disable cache, GET/POST vars, description
		$notes = $this->_('Optionally enter one or more variable names. If entering more than one, separate each by a space.'); // Disable cache, GET/POST vars, notes
		$notes .= "\n" . $this->_('To disable cache when any variable is present, enter just an asterisk: * (recommended)'); 

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('name', 'noCacheGetVars'); 
		$field->label = $labelGET; 
		$field->description = $description; 
		$field->notes = $notes; 
		$field->attr('value', (string) $template->noCacheGetVars); 
		$field->collapsed = Inputfield::collapsedBlank;
		$field->showIf = '_cache_status=1';
		$form->append($field); 

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

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('name', 'noCachePostVars'); 
		$field->label = $labelPOST; 
		$field->description = $description;
		$field->notes = $notes; 
		$field->attr('value', (string) $template->noCachePostVars); 
		$field->collapsed = Inputfield::collapsedBlank;
		$field->showIf = '_cache_status=1';
		$form->append($field); 

		return $form; 
	}

	/**
	 * Build the "advanced" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormAdvanced(Template $template) {

		$templates = $this->wire()->templates;
		$languages = $this->wire()->languages;
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;
		$advanced = $config->advanced;

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

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

		/** @var InputfieldTextTags $field */
		$field = $modules->get('InputfieldTextTags'); 
		$field->attr('name', 'tags'); 
		$field->attr('value', $template->tags); 
		$field->label = $this->_('Tags');
		$field->icon = 'tags';
		$field->allowUserTags = true;
		$field->setTagsList($templates->getTags());
		$field->description = $this->_('If you want to visually group this template with others in the templates list, enter a one-word tag. Enter the same tag on other templates 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 templates.'); // Description for field tags
		$field->notes = $this->_('Each tag must be one word (hyphenation is okay).') . ' [' . $this->labels['manageTags'] . '](./tags/)';
		$form->add($field); 

		// --------------------
	
		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('name', 'tabContent');
		$f->attr('value', $template->tabContent);
		$f->label = $this->_x('Label for Content tab', 'field-label');
		$f->description = $this->_('Optionally override the default "Content" tab label.');
		$f->columnWidth = 33;
		if($languages) {
			$f->useLanguages = true;
			foreach($languages as $language) {
				$f->set('value' . $language->id, $template->getTabLabel('content', $language));
			}
		}
		$form->add($f);

		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('name', 'tabChildren');
		$f->attr('value', $template->tabChildren);
		$f->label = $this->_x('Label for Children tab', 'field-label');
		$f->description = $this->_('Optionally override the default "Children" tab label.');
		$f->columnWidth = 33;
		if($languages) {
			$f->useLanguages = true;
			foreach($languages as $language) {
				$f->set('value' . $language->id, $template->getTabLabel('children', $language));
			}
		}
		$form->add($f);
	
		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('name', 'nameLabel');
		$f->attr('value', $template->nameLabel);
		$f->label = $this->_x('Label for Page Name property', 'field-label');
		$f->description = $this->_('Optionally override the default "Name" property label.');
		$f->columnWidth = 34;
		if($languages) {
			$f->useLanguages = true;
			foreach($languages as $language) {
				$f->set('value' . $language->id, $template->getNameLabel($language));
			}
		}
		$form->add($f);

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

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios');
		$field->attr('id+name', 'errorAction');
		$field->label = $this->_('Required field action'); 
		$field->description = $this->_('What action should be taken when an already published page is saved with a missing required field?');
		$field->addOption(0, $this->_('Alert user of the error (default)')); 
		$field->addOption(1, $this->_('Restore previous value (when available)'));
		$field->addOption(2, $this->_('Unpublish the page (when allowed)'));
		$field->attr('value', (int) $template->errorAction);
		$field->icon = 'asterisk';
		$form->append($field);

		// --------------------
	
		$pageLabelField = $template->pageLabelField; 
		$icon = $template->getIcon(true);
		if($icon) $pageLabelField = str_replace($icon, '', $pageLabelField); 

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('name', 'pageLabelField');
		$field->label = $this->_('List of fields to display in the admin Page List');
		$field->description = 
			$this->_('Enter one or more field names assigned to this template to display in the admin Page List when listing pages using this template. If left blank, the fields specified on the ProcessPageList module configuration will be used instead. Field names should be separated by a space and/or comma. Blank fields will be ignored.') . ' ' .  // Page list fields, description
			$this->_('You may also use your own format (and any additional punctuation/characters) by specifying field names in brackets, i.e. {name}, {categories.title}, etc.'); // Page list fields, description 2
		$field->icon = 'list';
		$notes = 'name, ';
		foreach($template->fields as $f) {
			if(!$f->type instanceof FieldtypeFieldsetOpen) $notes .= $f->name . ", ";
		}
		$field->notes = 
			$this->_('You may enter one or more of these fields:') . ' ' . rtrim($notes, ', ') . '. ' . 
			$this->_('To specify a property of a field, surround the field in brackets and enter {field_name.property}, for example: {categories.title}.');
		$field->attr('value', $pageLabelField); 
		$field->collapsed = Inputfield::collapsedBlank;
		$form->append($field);
		
		// --------------------
	
		/** @var InputfieldFieldset $fieldset */	
		$fieldset = $modules->get('InputfieldFieldset');
		$fieldset->label = $this->_('Template Toggles'); 
		$fieldset->icon = 'toggle-on';
		$fieldset->description = $this->_('You should generally leave these toggles unchecked unless you have a specific need covered here.'); 
		$form->add($fieldset); 
		
		$label0 = $this->_('specify 0 to disable');
	
		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', 'noChangeTemplate');
		$field->label = $this->_("Don't allow pages to change their template?");
		$field->description = $this->_("When checked, pages using this template will be unable to change to another template."); // noChangeTemplate option, description
		if($advanced) $field->notes = 'API: $template->noChangeTemplate = 1; // ' . $label0; 
		$field->attr('value', 1);
		if($template->noChangeTemplate) {
			$field->attr('checked', 'checked');
			$form->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$fieldset->append($field);

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', 'noUnpublish');
		$field->label = $this->_("Don't allow unpublished pages");
		$field->description = $this->_("When checked, pages using this template may only exist in a published state and may not be unpublished."); // noUnpublish option, description
		if($advanced) $field->notes = 'API: $template->noUnpublish = 1; // ' . $label0; 
		$field->attr('value', 1);
		if($template->noUnpublish) {
			$field->attr('checked', 'checked');
			$form->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$fieldset->append($field);

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', 'allowChangeUser');
		$field->label = $this->_("Allow the 'created user' to be changed on pages?");
		$field->description = $this->_("When checked, pages using this template will have an option to change the 'created by user' (for superusers only). It will also enable the \$page->createdUser or \$page->created_users_id fields to be saved via the API."); // allowChangeUser option, description
		if($advanced) $field->notes = 'API: $template->allowChangeUser = 1; // ' . $label0; 
		$field->attr('value', 1);
		if($template->allowChangeUser) {
			$field->attr('checked', 'checked');
			$form->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$fieldset->append($field);
		
		// --------------------

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', 'noMove');
		$field->label = $this->_("Don't allow pages to be moved?");
		$field->description = $this->_("If you want to prevent pages using this template from being moved (changing parent) then check this box."); // noMove option, description
		if($advanced) $field->notes = 'API: $template->noMove = 1; // ' . $label0;
		$field->attr('value', 1);
		if($template->noMove) {
			$field->attr('checked', 'checked');
			$field->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$fieldset->append($field);

		// --------------------
	
		if($languages) {
			/** @var InputfieldCheckbox $field */
			$field = $modules->get('InputfieldCheckbox');
			$field->attr('name', 'noLang');
			$field->label = $this->_("Disable multi-language support for this template?");
			$field->description = $this->_("When checked, pages using this template will only use the default language.");
			if($advanced) $field->notes = 'API: $template->noLang = 1; // ' . $label0; 

			$field->attr('value', 1);
			if($template->noLang) {
				$field->attr('checked', 'checked');
				$form->collapsed = Inputfield::collapsedNo;
			} else {
				$field->collapsed = Inputfield::collapsedYes;
			}
			$fieldset->append($field);
		}

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

		return $form; 
	}

	/**
	 * Build the "system" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormSystem(Template $template) {
		
		$modules = $this->wire()->modules;
		
		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->id = 'system';
		$form->notes = $this->_('Please note that all of these system settings are intended for ProcessWire system development (not site development). Use them at your own risk.'); // System tab notes

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('name', 'flagSystem');
		$field->label = $this->_('System Flag?');
		$field->description = $this->_("If checked, this template will be defined as for the system and it won't be deleteable. Once the system flag is enabled, it can only be removed via the API (flagSystemOverride)."); // System flag, description
		$field->notes = $this->_('API: $template->flagSystem = 1; // or 0 to disable'); // System flag, API notes
		$field->value = 1; 
		if($template->flags & Template::flagSystem) {
			$field->attr('checked', 'checked'); 
		} else {
			$field->collapsed = Inputfield::collapsedYes; 
		}
		$form->add($field); 

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

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('name', 'pageClass');
		$field->label = $this->_('Page Class Name');
		$field->description = $this->_("The name of the PHP class that will be used to create pages that use this template. By default pages will be created from the Page class. You should leave this blank unless you have another Page-derived class that you want to use."); // Page class, description
		$field->notes = $this->_('API: $template->pageClass = "ClassName";'); // Page class, API notes
		$field->attr('value', $template->pageClass ? $template->pageClass : ''); 
		$field->collapsed = Inputfield::collapsedBlank;
		$form->append($field); 
		if($template->pageClass) $form->collapsed = Inputfield::collapsedNo;

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('name', 'noGlobal');
		$field->label = $this->_('Disregard Global Fields?');
		$field->description = $this->_("By default, when a field is marked 'global' it will be required on all templates (and automatically added to any templates that don't have it). If this template has a special purpose where 'global' fields wouldn't apply, you can check this box to make this template disregard 'global' fields."); // Global flag, description
		$field->notes = $this->_('API: $template->noGlobal = 1; // or 0 to disable'); // Global flag, API notes

		$field->attr('value', 1); 
		if($template->noGlobal) {
			$field->attr('checked', 'checked'); 
			$form->collapsed = Inputfield::collapsedNo;
		} else { 
			$field->collapsed = Inputfield::collapsedYes;
		}
		$form->append($field); 

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('name', 'nameContentTab');
		$field->label = $this->_("Display 'name' field in content tab?");
		$field->description = $this->_("By default, the built-in 'name' field appears in the page editor 'settings' tab. If you would rather have it appear in the 'content' tab check this box."); // Name in content tab, description
		$field->notes = $this->_('API: $template->nameContentTab = 1; // or 0 to disable'); // Name in content tab, API notes

		$field->attr('value', 1); 
		if($template->nameContentTab) {
			$field->attr('checked', 'checked'); 
			$form->collapsed = Inputfield::collapsedNo;
		} else { 
			$field->collapsed = Inputfield::collapsedYes;
		}
		$form->append($field); 

		// --------------------
	
		/** @var InputfieldCheckbox $field */	
		$field = $modules->get('InputfieldCheckbox');
		$field->attr('name', 'noTrash');
		$field->label = $this->_("Disable Trash Option?");
		$field->description = $this->_("When checked, pages using this template will not have the option of being sent to the trash."); // noTrash option, description
		if($this->wire()->config->advanced) $field->notes = $this->_('API: $template->noTrash = 1; // or 0 to disable'); // noTrash option, API notes

		$field->attr('value', 1);
		if($template->noTrash) {
			$field->attr('checked', 'checked');
			$form->collapsed = Inputfield::collapsedNo;
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$form->append($field);
		
		// --------------------

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('name', 'noSettings');
		$field->label = $this->_("Disable Settings Tab?");
		$field->description = $this->_("When checked, pages using this template will not have a 'settings' tab appear in the editor."); // noSettings option, description
		$field->notes = $this->_('API: $template->noSettings = 1; // or 0 to disable'); // noSettings option, API notes

		$field->attr('value', 1); 
		if($template->noSettings) {
			$field->attr('checked', 'checked'); 
			$form->collapsed = Inputfield::collapsedNo;
		} else { 
			$field->collapsed = Inputfield::collapsedYes;
		}
		$form->append($field); 

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

		return $form; 
	}

	/**
	 * Build the "family" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormFamily(Template $template) {

		$modules = $this->wire()->modules;
		$templates = $this->wire()->templates;
		
		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'family'); 	

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

		/** @var InputfieldRadios $field */
		$field = $modules->get("InputfieldRadios"); 
		$field->attr('name', 'noChildren'); 
		$field->label = $this->_('May pages using this template have children?');
		$field->addOption(0, $this->labels['Yes']); 
		$field->addOption(1, $this->labels['No']); 
		$field->attr('value', (int) $template->noChildren); 
		$field->columnWidth = 50; 
		$field->optionColumns = 1; 
		$form->add($field);

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

		/** @var InputfieldRadios $field */
		$field = $modules->get("InputfieldRadios"); 
		$field->attr('name', 'noParents'); 
		$field->label = $this->_('Can this template be used for new pages?');
		// $field->notes = $this->_("An example of a template that you wouldn't want to be used for new pages is your 'homepage' template."); // noParents option, description
		$field->addOption(0, $this->labels['Yes']);
		$field->addOption(1, $this->labels['No']);
		$one = $this->_('One');
		$numPages = $this->numPages;
		if($numPages == 1) {
			// One option shown, but the one page already exists so no more are allowed
			$one .= " (" . $this->_('no more allowed') . ")";
			$field->addOption(-1, $one);
		} else if($numPages > 1) {
			// no "One" option should be shown, since more than one page already exists
			if($template->noParents == -1) $template->noParents = 1; 
		} else {
			// No pages using this template exist yet, so make the One option available
			$field->addOption(-1, $one);
		}
		$field->attr('value', (int) $template->noParents); 
		$field->columnWidth = 50; 
		$field->optionColumns = 1; 
		$form->add($field);

		// ----------------------
		$conflictIndicator = ' ♦';
		$successIndicator = ' ✓';

		/** @var InputfieldAsmSelect $field */
		$field = $modules->get('InputfieldAsmSelect'); 
		$field->attr('name', 'childTemplates'); 
		$field->label = $this->_('Allowed template(s) for children');
		$field->icon = 'indent';
		$field->description = 
			$this->_('Select the template(s) that will be allowed for children of pages using this template. Use this only if you specifically want to restrict placement of pages using this template.') . ' ' .  // childTemplates option, description
			$this->_("If none are selected then any are allowed, within the user's access limits. An example usage could be a 'news-list' template that is only allowed to have children using 'news-item' or 'press-release' templates."); // childTemplates option, notes

		$hasConflicts = false;
		$hasSuccess = false;
		
		foreach($templates as $t) {
			/** @var Template $t */
			$label = $t->name;
			if(count($t->parentTemplates)) { 
				if(in_array($template->id, $t->parentTemplates)) {
					$hasSuccess = true;
					$label .= $successIndicator;
				} else {
					$hasConflicts = true;
					$label .= $conflictIndicator;
				} 
			}
			$tlabel = $t->getLabel();
			if($tlabel == $t->name) $tlabel = '';
			$attrs = array('data-desc' => $tlabel);
			$ticon = $t->getIcon();
			if($ticon) $attrs['data-handle'] = wireIconMarkup($ticon, 'fw');
			$field->addOption($t->id, $label, $attrs);
		}
		
		$field->attr('value', $template->childTemplates); 
		$field->collapsed = count($template->childTemplates) ? Inputfield::collapsedNo : Inputfield::collapsedYes; 
		$notes = array();
		if($hasConflicts) $notes[] = sprintf($this->_('Templates indicated with %s also need to be configured to allow this template as a parent. Click any selected template as a shortcut to configure allowed parents.'), $conflictIndicator); // childTemplates family conflict, notes
		if($hasSuccess) $notes[] = sprintf($this->_('Templates indicated with a %s are already configured to allow this template as a parent.'), $successIndicator); 
		$field->notes = implode("\n", $notes);
		$field->showIf = 'noChildren!=1';
		$field->setAsmSelectOption('editLink', "./edit?id={value}&modal=1&WireTab=tab_family");
		$field->setAsmSelectOption('editLinkOnlySelected', false);
		$form->add($field); 

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

		/** @var InputfieldAsmSelect $field */
		$field = $modules->get('InputfieldAsmSelect'); 
		$field->attr('name', 'parentTemplates'); 
		$field->label = $this->_('Allowed template(s) for parents');
		$field->description = 
			$this->_("Select the template(s) that will be allowed as parents for pages using this template. Use this only if you specifically want to restrict placement of pages using this template.") . ' ' .  // parentTemplates option, description
			$this->_("If none are selected then any parent template is allowed, within the user's access limits. An example usage could be an 'employee' template that is only allowed to have a parent page using a 'company-directory' template."); // parentTemplates option, notes
		$field->icon = 'dedent';

		$hasConflicts = false;
		$hasSuccess = false;
		
		foreach($templates as $t) {
			$label = $t->name;
			if(count($t->childTemplates)) {
				if(in_array($template->id, $t->childTemplates)) {
					$hasSuccess = true;
					$label .= $successIndicator;
				} else {
					$hasConflicts = true;
					$label .= $conflictIndicator;
				}
			}
			$tlabel = $t->getLabel();
			if($tlabel == $t->name) $tlabel = '';
			$attrs = array('data-desc' => $tlabel); 
			$ticon = $t->getIcon();
			if($ticon) $attrs['data-handle'] = wireIconMarkup($ticon, 'fw');
			$field->addOption($t->id, $label, $attrs);
		}
		
		$field->attr('value', $template->parentTemplates); 
		$field->collapsed = count($template->parentTemplates) ? Inputfield::collapsedNo : Inputfield::collapsedYes;
		$notes = array();
		if($hasConflicts) $notes[] = sprintf($this->_('Templates indicated with %s also need to be configured to allow this template for children. Click any selected template as a shortcut to configure allowed children.'), $conflictIndicator); // parentTemplates family conflict, notes
		if($hasSuccess) $notes[] = sprintf($this->_('Templates indicated with a %s are already configured to allow this template as a child.'), $successIndicator); 
		$field->notes = implode("\n", $notes);
		$field->showIf = 'noParents!=1';
		$field->setAsmSelectOption('editLink', "./edit?id={value}&modal=1&WireTab=tab_family"); 
		$field->setAsmSelectOption('editLinkOnlySelected', false);
		$form->add($field); 

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

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('name', 'childNameFormat');
		$field->label = $this->_('Name format for children');
		$field->description = $this->_('Optionally specify a format for page names used when a new child page is added. This enables the "Page Add" step to be skipped when adding a new page. In order to work, a single (1) template must be selected for the "Allowed template(s) for children" field above.'); // Page name format, description
		$field->notes = sprintf(
			$this->_('Leave blank to disable. Enter “title” (without quotes) to auto-generate the name from the page title. [More options](%s) including custom fields and date formats are available as well.'), // Page name format notes
			'https://processwire.com/docs/modules/guides/process-template/'
		) . "\n"; 
		$field->attr('value', $template->childNameFormat); 
		$field->collapsed = Inputfield::collapsedBlank;
		$field->showIf = 'childTemplates.count=1, noChildren!=1'; 
		$form->add($field); 

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

		/** @var InputfieldRadios $field */
		$field = $modules->get("InputfieldRadios"); 
		$field->attr('name', 'noShortcut'); 
		$field->icon = 'question-circle';
		$field->label = $this->_('Show in the add-page shortcut menu?');
		$field->description = 
			$this->_('When checked, this template is eligible to appear on the "add new page" shortcut button/menu that appears on the main Pages screen. This assumes all other conditions are met (see below).') . ' ' . 
			sprintf(
				$this->_('To adjust the order that templates appear in the shortcuts menu, see the [PageAdd module settings](%s).'), 
				'../../module/edit?name=ProcessPageAdd'
			);
		$field->notes =	
			$this->_('**Conditions required for this to work**') .  
			"\n" . 	$this->_('1. You must select a parent template (see: "allowed templates for parents" above). If more than 1 is selected, only the first will be used.') . 
			"\n" . 	$this->_('2. The selected parent template must have defined this template as one allowed for children.');
		$field->addOption(0, $this->labels['Yes']); 
		$field->addOption(1, $this->labels['No']); 
		$field->attr('value', (int) $template->noShortcut); 
		$field->collapsed = Inputfield::collapsedYes; 
		$field->showIf = "parentTemplates.count>0, noParents!=1";
		$field->optionColumns = 1; 
		$form->add($field);

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

		$fieldset = ProcessPageEdit::buildFormSortfield($template->sortfield, $this); 
		$fieldset->attr('name', 'sortfield_fieldset'); 
		$fieldset->label = $this->_('Sort settings for children'); 
		$fieldset->icon = 'sort';
		$fieldset->description = $this->_('If you want children to automatically sort by a specific field, select it here. Otherwise select "None" to let this be set per-page instead. This overrides the page setting, so if you select anything other than "None", the sort option will not appear for pages using this template.'); 
		$fieldset->showIf = 'noChildren!=1';
		$form->add($fieldset); 

		return $form;
	}

	/**
	 * Build the "URLs" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormURLs(Template $template) {
		
		$modules = $this->wire()->modules;

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'urls'); 	

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

		$moreLabel = $this->_('More');

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('id+name', 'allowPageNum');
		$field->label = $this->_('Allow Page Numbers?');
		$field->icon = 'list-ol';
		$field->attr('value', 1); 
		if($template->allowPageNum) {
			$field->attr('checked', 'checked');
		} else {
			$field->collapsed = Inputfield::collapsedYes;
		}
		$field->description = 
			$this->_('If checked, pages using this template will support pagination.') . ' ' . 
			"[$moreLabel](https://processwire.com/docs/admin/setup/templates/#allow-page-numbers)";
		$field->columnWidth = 50;
		$form->append($field); 
		if($template->allowPageNum) $form->collapsed = Inputfield::collapsedNo;

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios');
		$field->attr('id+name', 'slashPageNum');
		$field->label = $this->_('Should page number URLs have a trailing slash?');
		$field->icon = 'list-ol';
		$field->addOption(1, $this->labels['Yes']);
		$field->addOption(-1, $this->labels['No']);
		$field->addOption(0, $this->labels['Either']);
		$field->attr('value', (int) $template->slashPageNum);
		$field->showIf = 'allowPageNum=1';
		$field->columnWidth = 50;
		$form->append($field);

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

		/** @var InputfieldCheckbox $field */
		$field = $modules->get('InputfieldCheckbox'); 
		$field->attr('name', 'urlSegments');
		$field->label = $this->_('Allow URL Segments?');
		$field->icon = 'sitemap';
		$field->columnWidth = 50; 
		$field->description = 
			$this->_('If checked, pages using this template will support custom URL segments after the page URL.') . ' ' . 
			"[$moreLabel](https://processwire.com/docs/admin/setup/templates/#allow-url-segments)";
		
		$field->attr('value', 1); 
		$urlSegments = $template->urlSegments();
		if($urlSegments) {
			$field->attr('checked', 'checked'); 
			$form->collapsed = Inputfield::collapsedNo;
		} else { 
			$field->collapsed = Inputfield::collapsedYes;
		}
		$form->append($field);

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios');
		$field->attr('id+name', 'slashUrlSegments');
		$field->label = $this->_('Should URL segments end with a trailing slash?');
		$field->icon = 'sitemap';
		$field->addOption(1, $this->labels['Yes']);
		$field->addOption(-1, $this->labels['No']);
		$field->addOption(0, $this->labels['Either']);
		$field->attr('value', (int) $template->slashUrlSegments);
		$field->showIf = 'urlSegments=1';
		$field->columnWidth = 50;
		$form->append($field);

		/** @var InputfieldTextarea $field */
		$field = $modules->get('InputfieldTextarea'); 
		$field->attr('name', 'urlSegmentsList');
		$field->label = $this->_('Which URL segments do you want to allow?'); 
		$field->description = 
			$this->_('Enter one allowed segment, segment path or regular expression per line, or leave blank to allow any.') . ' ' .  // Description for allow URL segments
			"[$moreLabel](https://processwire.com/docs/admin/setup/templates/#which-url-segments-do-you-want-to-allow)";
		$field->icon = 'sitemap';
		if(is_array($urlSegments)) $field->attr('value', implode("\n", $urlSegments));
		$field->showIf = 'urlSegments=1';
		$field->collapsed = Inputfield::collapsedBlank;
		$form->append($field); 
		
		// --------------------

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('name', 'slashUrls'); 
		$field->label = $this->_('Should page URLs end with a slash?');
		$field->icon = 'eye-slash';
		$field->addOption(1, $this->labels['Yes']); 
		if($template->name != 'admin') $field->addOption(0, $this->labels['No']); 
		$field->description = $this->_("If 'Yes', pages using this template will always have URLs that end with a trailing slash '/'. And if the page is loaded from a URL without the slash, it will be redirected to it. If you select 'No', the non-slashed version will be enforced instead. Note that this setting does not enforce this behavior on URL segments or page numbers, only actual page URLs. If you don't have a preference, it is recommended that you leave this set to 'Yes'."); // URLs end with slash, description
		$field->attr('value', $template->slashUrls === 0 ? 0 : 1); // force default setting of 1
		$field->collapsed = $template->slashUrls === 0 ? Inputfield::collapsedNo : Inputfield::collapsedYes;
		if($template->slashUrls === 0) $form->collapsed = Inputfield::collapsedNo;
		$form->append($field); 

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

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('name', 'https'); 
		$field->label = $this->_('Scheme/Protocol');
		$field->icon = 'shield';
		$field->addOption(0, $this->_('HTTP or HTTPS')); 
		$field->addOption(1, $this->_('HTTPS only (SSL encrypted)')); 
		$field->addOption(-1, $this->_('HTTP only'));
		$field->collapsed = Inputfield::collapsedBlank;
		$field->description = $this->_("If your site has an SSL certificate and is accessible by HTTPS, you may make pages using this template accessible only via HTTPS or HTTP. For example, if you select 'HTTPS only' below, and a request for a page using this template comes in through HTTP, it will be automatically redirected to the HTTPS version. By default, ProcessWire allows connection from either HTTP or HTTPS."); // Scheme/Protocol, description
		$field->attr('value', (int) $template->https); 
		$form->append($field); 
		if($template->https) $form->collapsed = Inputfield::collapsedNo;
			
		return $form; 
	}

	/**
	 * Build the "files" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormFile(Template $template) {
		
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'files');
		
		// --------------------

		if($config->templateCompile) {
			/** @var InputfieldRadios $field */
			$field = $modules->get('InputfieldRadios');
			$field->attr('name', 'compile');
			$field->label = $this->_('Use Compiled File?');
			$field->icon = 'file-code-o';
			$field->description = 
				$this->_('Enables ProcessWire to update your template file for the ProcessWire namespace and apply any installed FileCompiler modules.') . ' ' .
				$this->_('Recommend at least for template files that do not use a ProcessWire namespace (like those originating from PW 2.x sites).') . ' ' . 
				$this->_('If you are not sure, choose "Auto".');
			$field->addOption(0, $this->labels['No'] . ' ' . $this->_('(disables compiler)'));
			$field->addOption(3, $this->_('Auto (compile when file has no namespace)')); 
			$field->addOption(1, $this->labels['Yes'] . ' ' . $this->_('(template file only)'));
			$field->addOption(2, $this->labels['Yes'] . ' ' . $this->_('(template file and files included from it)'));
			$field->attr('value', $template->compile);
			$template->filename();
			$ns = $template->ns;
			if($ns) {
				$field->notes = sprintf($this->_('Detected template file namespace is "%s".'), $ns);
				if($ns === "\\") {
					$field->notes .= " " . $this->_('Compile recommended.');
				} else {
					$field->notes .= " " . $this->_('Compile not necessary unless needed for modules or includes.');
				}
			}
			$form->append($field);
		}
		
		// --------------------

		/** @var InputfieldSelect $field */
		$field = $modules->get('InputfieldSelect');
		$field->attr('name', 'contentType');
		$field->label = $this->_('Content-Type');
		$field->description = $this->_('Header to send with template file output. If none selected, ProcessWire will not send a content-type header and in most cases text/html is assumed. Selecting a content-type is recommended when output will be something other than html.'); // Description for content-type option
		$field->notes = $this->_('To add more content types see *contentTypes* in /wire/config.php (and add them in /site/config.php).');
		$field->icon = 'html5';
		foreach($config->contentTypes as $ext => $header) {
			$field->addOption($ext, $header);
		}
		if(strpos($template->contentType, '/') !== false) {
			$contentType = array_search($template->contentType, $config->contentTypes);
			if($contentType === false) {
				$this->error("Error: content-type '$template->contentType' not found.");
			} else {
				$field->attr('value', $contentType);
			}
		} else {
			$field->attr('value', $template->contentType);
		}
		$form->append($field);

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

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText');
		$field->attr('name', 'altFilename');
		$field->label = $this->_('Alternate Template Filename');
		$defaultFilename = $template->name . "." . $config->templateExtension;
		$field->description = sprintf($this->_("The template's filename, if different from: %s"), $defaultFilename);
		$field->notes = 
			sprintf($this->_('Template files are assumed to be in directory: %s'), $config->urls->templates) . "\n" .
			sprintf($this->_('Template file extension is assumed to be: %s'), '.' . $config->templateExtension);
		$field->attr('value', $template->altFilename);
		$field->collapsed = Inputfield::collapsedBlank;
		$field->icon = 'exchange';
		$form->append($field);
		
		// --------------------
	
		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('id+name', 'prependFile');
		$f->attr('value', $template->prependFile);
		$f->label = $this->_('Prepend File');
		$f->description = $this->_('File to prepend to template file during page render. Must be relative to /site/templates/.'); // prependFile description
		$f->columnWidth = 50;
		$f->icon = 'backward';
		$form->add($f);

		/** @var InputfieldText $f */
		$f = $modules->get('InputfieldText');
		$f->attr('id+name', 'appendFile');
		$f->attr('value', $template->appendFile);
		$f->label = $this->_('Append File');
		$f->description = $this->_('File to append to template file during page render. Must be relative to /site/templates/.'); // appendFile description
		$f->columnWidth = 50;
		$f->icon = 'forward';
		$form->add($f);
		
		// --------------------

		if($config->prependTemplateFile) {
			/** @var InputfieldCheckbox $field1 */
			$field1 = $modules->get('InputfieldCheckbox');
			$field1->attr('id+name', 'noPrependTemplateFile');
			$field1->label = sprintf($this->_('Disable automatic prepend of file: %s'), $config->prependTemplateFile);
			if($template->noPrependTemplateFile) $field1->attr('checked', 'checked'); 
			$form->add($field1); 
		} else {
			$field1 = null;
		}
		
		if($config->appendTemplateFile) {
			/** @var InputfieldCheckbox $field2 */
			$field2 = $modules->get('InputfieldCheckbox');
			$field2->attr('id+name', 'noAppendTemplateFile');
			$field2->label = sprintf($this->_('Disable automatic append of file: %s'), $config->appendTemplateFile);
			if($template->noAppendTemplateFile) $field2->attr('checked', 'checked');
			$form->add($field2); 
		} else {
			$field2 = null;
		}
		
		if($field1 && $field2) {
			$field1->columnWidth = 50;
			$field2->columnWidth = 50;
		}
		
		// --------------------

		return $form;
	}

	/**
	 * Build the "access" tab for edit form
	 * 
	 * @param Template $template
	 * @return InputfieldWrapper
	 * 
	 */
	protected function buildEditFormAccess(Template $template) {
		
		$modules = $this->wire()->modules;
		$config = $this->wire()->config;
		$pages = $this->wire()->pages;

		/** @var InputfieldWrapper $form */
		$form = $this->wire(new InputfieldWrapper());
		$form->attr('id', 'access'); 	

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

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('id+name', 'useRoles');
		$field->label = $this->_('Do you want to manage view and edit access for pages using this template?'); // Use Roles?
		$field->icon = 'key';
		$field->description = $this->_("If you select 'Yes' you can define what roles have access to pages using this template. If you select 'No' then pages using this template will inherit access from their parent pages."); // Use Roles, description
		$field->addOption(1, $this->labels['Yes']);
		$field->addOption(0, $this->labels['No']); 
		$field->attr('value', (int) $template->useRoles); 
		$field->optionColumns = 1; 
		$field->collapsed = Inputfield::collapsedNo;
		$form->add($field); 

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

		/** @var InputfieldWrapper $fieldset */
		$fieldset = $this->wire(new InputfieldWrapper());
		$fieldset->attr('id', 'useRolesYes'); 
		$form->add($fieldset); 

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

		$fieldset->add($this->buildEditFormAccessRoles($template)); 
		
		// --------------------

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('id+name', 'redirectLogin');
		$field->label = $this->_('What to do when user attempts to view a page and has no access?');
		$field->description = $this->_("If a user attempts to access a page using this template, and doesn't have access to the page, what should it do?"); // What to do when no access, description
		$field->addOption(0, 'A. ' . $this->_('Show a 404 Page')); 
		$field->addOption($config->loginPageID, 'B. ' . sprintf($this->_('Show the Login page: %s'), $pages->get($config->loginPageID)->url));
		$field->addOption(-1, 'C. ' . $this->_('Redirect to another URL or render a page by ID')); 
		if($template->redirectLogin == $config->loginPageID) {
			$field->attr('value', $config->loginPageID);
		} else if($template->redirectLogin) {
			$field->attr('value', -1);
		} else {
			$field->attr('value', 0);
		}
		$field->collapsed = Inputfield::collapsedBlank;
		$fieldset->add($field);

		/** @var InputfieldText $field */
		$field = $modules->get('InputfieldText'); 
		$field->attr('id+name', 'redirectLoginURL'); 
		$field->label = $this->_('Login redirect URL or page ID to render'); 
		$field->description = 
			$this->_('Enter the URL you want to redirect to OR the page ID you want to render when a user does not have access.') . ' ' . 
			$this->_('If specifying a page ID, please note the resulting page will be rendered for the user whether they have view permission or not.') . ' ' . 
			$this->_('This is useful in cases where your login page might itself be access protected.') . ' ' . 
			$this->_('This setting is applicable only if you selected option “C” in the field above.');
		if($template->redirectLogin && $template->redirectLogin != $config->loginPageID) {
			$field->attr('value', $template->redirectLogin);
		}
		$field->collapsed = Inputfield::collapsedNo;
		$field->notes = $this->_("Optional: In your URL, you can include the tag '{id}' (perhaps as a GET variable), and it will be replaced by the requested page's ID number, if you want it."); // Redirect URL, notes
		$fieldset->add($field); 
		
		// --------------------

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios'); 
		$field->attr('id+name', 'guestSearchable');
		$field->label = $this->_('Should pages be searchable when user has no access?');
		$field->description = $this->_("When a user doesn't have access to view a page using this template, what should happen in searches and page lists?"); // Guest searchable, description
		$field->addOption(1, $this->_("Yes - Pages may appear in searches/lists even if the user doesn't have access to view them"));
		$field->addOption(0, $this->_("No - Pages may NOT not appear in searches/lists unless the user has access to view them")); 
		$field->attr('value', (int) $template->guestSearchable); 
		$field->collapsed = Inputfield::collapsedBlank;
		$field->notes = $this->_('API Note: This setting affects the results returned by $pages->find(), $page->children() and other functions that return PageArrays.'); // Guest searchable, notes
		$fieldset->add($field); 

		// noInherit ---------------

		/** @var InputfieldRadios $field */
		$field = $modules->get('InputfieldRadios');
		$field->attr('id+name', 'noInherit');
		$field->label = $this->_('Allow edit-related access to inherit to children?');
		$field->description = 
			$this->_('By default, all access settings on this template inherit through the page tree until a page using another access controlled template overrides it.') . ' ' .  // Description 2 for the noInherit option
			$this->_('Choose the "No" option here if you want to prevent edit-related access from being inherited beyond pages using this template. View access will continue to inherit either way.'); // Description 2 for the noInherit option
		$field->addOption(0, $this->labels['Yes'] . " - " . $this->_('Allow edit-related access to inherit to children that are not access controlled'));
		$field->addOption(1, $this->labels['No'] . " - " . $this->_('Limit edit-related access to pages using this template only'));
		$field->attr('value', $template->noInherit ? 1 : 0);
		$fieldset->add($field);

		return $form; 
	}

	/**
	 * Build the "pagefileSecure" field for the "access" tab
	 * 
	 * @param Template $template
	 * @return InputfieldRadios
	 * 
	 */
	protected function buildEditFormAccessFiles(Template $template) {

		/** @var InputfieldRadios $f */
		$f = $this->wire()->modules->get('InputfieldRadios');
		$f->attr('id+name', 'pagefileSecure');
		$f->label = $this->_('Prevent direct access to file assets owned by pages using this template?');
		$f->icon = 'download';
		$f->description =
			$this->_('When direct access to a file in [u]/site/assets/files/[/u] is blocked, ProcessWire can manage delivery of the file, rather than Apache.') . ' ' .
			$this->_('This enables the file to be access controlled in the same manner as the page that owns it, while still using the original file URL.') . ' ' .
			$this->_('Note that it takes more overhead to deliver a file this way, so only choose the “Yes always” option if you need it.');
		$f->notes =
			$this->_('Always test that the access control is working how you expect by attempting to access the protected file(s) in your browser.') . ' ' .
			$this->_('Do this for when you expect to have access (logged-in) and when you do not (logged-out).');
		$f->addOption(0, $this->_('No') . ' ' . 
			'[span.detail] ' . $this->_('(uses site-wide configuration instead)') . ' [/span]');
		$f->addOption(1, $this->_('Yes when page is unpublished, in the trash, or not publicly accessible'));
		$f->addOption(2, $this->_('Yes always, regardless of page status or access control'));
		$f->val((int) $template->pagefileSecure);
		if(!$template->pagefileSecure) $f->collapsed = Inputfield::collapsedYes;
		
		return $f;
	}

	/**
	 * Build the "roles" field for "access" tab in edit form
	 * 
	 * @param Template|null $template
	 * @return InputfieldMarkup
	 * 
	 */
	protected function buildEditFormAccessRoles(?Template $template = null) {

		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		$adminTheme = $this->wire()->adminTheme;
		
		$checkboxClass = $adminTheme ? $adminTheme->getClass('input-checkbox') : '';
		$roles = $this->wire()->pages->get($config->rolesPageID)->children(); 
		$checked = "checked='checked' ";
		$disabled = "<input class='$checkboxClass' type='checkbox' disabled='disabled' />";

		/** @var InputfieldMarkup $field */
		$field = $modules->get("InputfieldMarkup"); 
		$field->set('thead', "Role");
		$field->attr('id', 'roles_editor'); 
		$field->label = $this->_('What roles can access pages using this template (and those inheriting from it)?');
		$field->notes = $this->_('Note: the edit/create/add options are disabled for roles that do not currently have page-edit permission.');
		$field->icon = 'gears';

		/** @var MarkupAdminDataTable $table */
		$table = $modules->get("MarkupAdminDataTable"); 
		$table->setEncodeEntities(false);
		$table->headerRow(array(
			$this->_x('Role', 'access-thead'), 
			$this->_x('View Pages', 'access-thead'), 
			$this->_x('Edit Pages', 'access-thead'), 
			$this->_x('Create Pages', 'access-thead'), 
			$this->_x('Add Children', 'access-thead'),
		)); 

		foreach($roles as $role) {
			$label = $role->name; 
			$editable = false;
			$details = '';
			foreach($role->permissions as $permission) {
				if(strpos($permission->name, 'page-') !== 0) continue; 
				if($permission->name == 'page-edit') $editable = true; 
				$details .= $permission->title;
				$details .= "\n";
			}
			if($label == 'superuser') continue; 
			if($label == 'guest') $label .= ' ' . $this->_('(everyone)');

			$table->row(array(
				$label,
				("<input type='checkbox' id='roles_$role->id' class='viewRoles $checkboxClass' name='roles[]' value='$role->id' " . 
					($template->roles->has($role) ? $checked : '') . " />"), 
				($editable ? ("<input type='checkbox' class='editRoles $checkboxClass' name='editRoles[]' value='$role->id' " . 
					(in_array($role->id, $template->editRoles) ? $checked : '') . " />") : $disabled), 
				($editable ? ("<input type='checkbox' class='createRoles $checkboxClass' name='createRoles[]' value='$role->id' " . 
					(in_array($role->id, $template->createRoles) ? $checked : '') . " />") : $disabled),
				($editable ? ("<input type='checkbox' class='addRoles $checkboxClass' name='addRoles[]' value='$role->id' " . 
					(in_array($role->id, $template->addRoles) ? $checked : '') . " />") : $disabled)
			));
		}

		$field->value = $table->render();

		return $field; 
	}

	/**
	 * Build the access overrides for "roles" on the "access" tab in edit form
	 * 
	 * @param Template $template
	 * @return InputfieldFieldset|null
	 * 
	 */
	protected function buildEditFormAccessOverrides(Template $template) {
		
		$modules = $this->wire()->modules;
		$roles = $this->wire()->roles;
		$permissions = $this->wire()->permissions;
	
		$isUsable = false;
		$rolesPermissions = $template->rolesPermissions;
		$ignorePermissions = array(
			'page-view',
			'page-lister',
			'page-edit',
		);
	
		/** @var InputfieldFieldset $fieldset */
		$fieldset = $modules->get('InputfieldFieldset');
		$fieldset->attr('id', 'accessOverrides');
		$fieldset->label = $this->_('Additional edit permissions and overrides');
		$fieldset->description = 
			$this->_('Optionally add or revoke permissions assigned to each role when requested for a page using this template (or inheriting access from it).') . ' ' . 
			$this->_('The options available here are determined by your choices for "Edit Pages" above. As a result, you should save this template and come back here after making changes to roles with "Edit Pages" access above.');
		$fieldset->notes = sprintf(
			$this->_('Options are shown in the format "role: permission". For a description of what each permission does, see the [permissions reference](%s).'), 
			'https://processwire.com/docs/user-access/permissions/'
		);
		$fieldset->collapsed = Inputfield::collapsedBlank;
		$fieldset->icon = 'gear';
		
		// add -------------
	
		/** @var InputfieldAsmSelect $f */
		$f = $modules->get('InputfieldAsmSelect');
		$f->attr('name', 'rolesPermissionsAdd');
		$f->label = $this->_('Add permissions by role');
		$f->icon = 'plus-circle';
		$value = array();
		$options = array();
		
		foreach($template->editRoles as $roleID) {
			$role = $roles->get((int) $roleID);
			if(!$role->id) continue;
			foreach($permissions as $permission) {
				if(strpos($permission->name, 'page-') !== 0) continue;
				$ignore = false;
				foreach($ignorePermissions as $name) {
					if($permission->name == $name) $ignore = true; 
					if(strpos($permission->name, 'page-lister') === 0) $ignore = true; 
				}
				if($ignore) continue;
				if($role->hasPermission($permission)) continue;
				$options["$roleID:$permission->id"] = "$role->name\n$permission->name\n$permission->title";
			}
			if(isset($rolesPermissions["$roleID"])) {
				foreach($rolesPermissions["$roleID"] as $permissionID) {
					if(strpos($permissionID, "-") === 0) continue;
					$v = "$roleID:$permissionID";
					if(!isset($options[$v])) continue;
					$value[] = $v;
				}
			}
		}

		if(count($options)) {
			asort($options);
			foreach($options as $k => $v) {
				list($roleName, $permissionName, $permissionTitle) = explode("\n", $v);
				$f->addOption($k, "$roleName: $permissionName", array('data-desc' => $permissionTitle));
			}
			$f->attr('value', $value);
			$fieldset->add($f);
			$isUsable = true;
		}
		
		// revoke -------------

		/** @var InputfieldAsmSelect $f */
		$f = $modules->get('InputfieldAsmSelect');
		$f->attr('name', 'rolesPermissionsRevoke');
		$f->label = $this->_('Revoke permissions by role');
		$f->icon = 'minus-circle';
		$value = array();
		$options = array();

		foreach($template->editRoles as $roleID) {
			$role = $roles->get((int) $roleID);
			if(!$role->id) continue;
			foreach($role->permissions as $permission) {
				if(strpos($permission->name, 'page-') !== 0) continue;
				$ignore = false;
				foreach($ignorePermissions as $name) {
					if(strpos($permission->name, $name) === 0) $ignore = true;
				}
				if($ignore) continue;
				$options["$roleID:-$permission->id"] = "$role->name\n$permission->name\n$permission->title";
			}
			if(isset($rolesPermissions["$roleID"])) {
				foreach($rolesPermissions["$roleID"] as $permissionID) {
					if(strpos($permissionID, "-") !== 0) continue;
					$v = "$roleID:$permissionID";
					if(!isset($options[$v])) continue;
					$value[] = $v;
				}
			}
		}

		if(count($options)) {
			foreach($options as $k => $v) {
				list($roleName, $permissionName, $permissionTitle) = explode("\n", $v);
				$f->addOption($k, "$roleName: $permissionName", array('data-desc' => $permissionTitle));
			}
			$f->attr('value', $value);
			$fieldset->add($f);
			$isUsable = true;
		}
		
		return $isUsable ? $fieldset : null;
	}

	/**
	 * Save template/fieldgroup/field property (ajax)
	 * 
	 * @throws WireException
	 * 
	 */
	public function ___executeSaveProperty() {
		
		$input = $this->wire()->input;
		$fields = $this->wire()->fields;
		
		$result = array('success' => true, 'message' => '', 'value' => '');
		$property = $input->post('property');
		$error = '';
		
		if(!$this->template) {
			$error = 'No template specified';
			
		} else if(!$property) {
			$error = 'No property specified';
			
		} else if($property === 'columnWidth') {
			$fieldgroup = $this->template->fieldgroup;
			$fieldId = (int) $input->post('field');
			$field = $fieldgroup->getFieldContext($fieldId);
			if(!$field) $field = $this->wire()->fields->get($fieldId);
			$columnWidth = (int) $input->post('columnWidth');
			$oldColumnWidth = $field->columnWidth ? $field->columnWidth : 100;

			if($field && $columnWidth != $oldColumnWidth) {
				$data = $fieldgroup->getFieldContextArray($field->id);
				$data['columnWidth'] = $columnWidth;
				$fieldgroup->setFieldContextArray($field->id, $data);
				$fieldgroup->saveContext();
				$result['message'] = "Updated columnWidth from $oldColumnWidth to $columnWidth";
				$result['value'] = $columnWidth;
			} else if($field) {
				$result['message'] = 'No changes';
				$result['value'] = $columnWidth;
			} else {
				$error = 'Field not found';
			}
			
		} else if($property === 'fieldgroup_fields') {
			$ids = $input->post($property);
			$fieldgroup = $this->template->fieldgroup;
		
			if(is_array($ids)) {
				foreach($ids as $id) {
					if(!($id = (int) $id)) continue;
					$id = abs($id); // deleted items have negative values, but we do not delete here
					if(!$field = $fields->get($id)) continue;

					if(!$fieldgroup->has($field)) {
						continue; // field will be added when they save
					} else {
						$field = $fieldgroup->getFieldContext($field->id);
						$fieldgroup->append($field);
					}
				}
				$fieldgroup->save();
				$result['message'] = "Updated fieldgroup field order";
				$result['value'] = $fieldgroup->implode(',', 'id');
			} else {
				$error = "Invalid $property value (array required)";
			}
			
		} else {
			$error = "Unsupported property requested";
		}
		
		if($error) {
			$result['success'] = false;
			$result['message'] = $error;
		}
	
		echo json_encode($result);
		exit;
	}

	/**
	 * Save the template and check to see if a Fieldgroup change was requested
	 *
	 */
	protected function ___executeSave() {

		if(!$this->template) throw new WireException("No template specified");
	
		$input = $this->wire()->input;
		$config = $this->wire()->config;
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;
		$sanitizer = $this->wire()->sanitizer;
		$languages = $this->wire()->languages;
		$templates = $this->wire()->templates;
		$template = $this->template;
		
		$redirectUrl = '';
		$pagefileSecurePrev = (int) $template->pagefileSecure;
		
		$form = $this->buildEditForm($template);
		$form->processInput($input->post);

		/*** DELETE **************************************************************/
		
		$delete = (int) $input->post('delete');
		
		if($delete && $delete === $template->id && $this->numPages == 0) {
			$fieldgroup = $template->fieldgroup;
			$deleteFieldgroup = $fieldgroup->name == $template->name;
			$session->message($this->_('Deleted template') . " - {$template->name}");
			$templates->delete($this->template);
			if($deleteFieldgroup) $this->wire()->fieldgroups->delete($fieldgroup);
			$session->redirect("./");
			return;
		}
		
		/*** USER TEMPLATE *******************************************************/

		if(in_array($template->id, $config->userTemplateIDs)) {
			/** @var Inputfield $f */
			$f = $form->getChildByName('_user_profile_fields');
			$data = $modules->getConfig('ProcessProfile');
			if($f && $data['profileFields'] != $f->val()) {
				$data['profileFields'] = $f->val();
				$modules->saveConfig('ProcessProfile', $data);
				$this->message("Updated user profile fields", Notice::debug); 
			}	
		}
		
		/*** FIELDGROUP **********************************************************/

		$fieldgroupId = (int) $input->post('fieldgroup');
		if($fieldgroupId > 0 && $fieldgroupId != $template->fieldgroup->id) {
			$redirectUrl = "fieldgroup?id={$template->id}&fieldgroup=$fieldgroupId";
		}
		
		/*** URLS ****************************************************************/

		$urlSegments = (int) $form->getChildByName('urlSegments')->val();
		$urlSegmentsList = $form->getChildByName('urlSegmentsList')->val();
		if($urlSegments && strlen($urlSegmentsList)) {
			$template->urlSegments(explode("\n", $urlSegmentsList));
		} else if($urlSegments) {
			$template->urlSegments = 1;
		} else {
			$template->urlSegments = 0;
		}

		/*** GENERAL PROPERTIES **************************************************/
		
		$properties = array(
			'allowPageNum' => 'int',
			'redirectLogin' => 'int',
			'https' => 'int',
			'slashUrls' => 'int',
			'slashUrlSegments' => 'int',
			'slashPageNum' => 'int',
			'guestSearchable' => 'int',
			'noInherit' => 'int',
			'errorAction' => 'int',
			'useCacheForUsers' => 'int',
			'cacheExpire' => 'int',
			'noUnpublish' => 'int',
			'noChangeTemplate' => 'int',
			'allowChangeUser' => 'int',
			'pagefileSecure' => 'int',
			'childNameFormat' => 'text',
			'noPrependTemplateFile' => 'post-int', // field may not be present on all submissions
			'noAppendTemplateFile' => 'post-int', // field may not be present on all submissions
			'compile' => 'post-int',
			'tags' => 'words',
			'contentType' => 'none',
			'noShortcut' => 'post-1',
			'noMove' => 'int',
			'useRoles' => 'int',
		);
		
		foreach($properties as $name => $sanitizerName) {
			if($sanitizerName === 'post-int') {
				$value = (int) $input->post($name);
			} else if($sanitizerName === 'post-1') {
				$value = (int) $input->post($name) ? 1 : 0;
			} else {
				$f = $form->getChildByName($name);
				if(!$f) continue;
				$value = $f->val();
				if($sanitizerName === 'int') {
					$value = (int) $value;
				} else if($sanitizerName === 'text') {
					$value = $sanitizer->text($value);
				} else if($sanitizerName === 'words') {
					$value = $sanitizer->words($value);
				} else {
					// use as-is
				}
			}
			if($template->get($name) != $value) {
				$template->set($name, $value);
			}
		}
		
		/*** PAGE LABEL **********************************************************/
		
		$pageLabelField = $form->getChildByName('pageLabelField')->val();
		if(strpos($pageLabelField, '{') !== false && strpos($pageLabelField, '}')) {
			// {tag} format string, keep as-is
		} else {
			// sanitize to names string
			$pageLabelField = $sanitizer->names($pageLabelField);
		}
		$template->pageLabelField = $pageLabelField;
		

		/*** CACHE ***************************************************************/
		
		$cacheStatus = (int) $form->getChildByName('_cache_status')->val();
		$cacheTime = (int) $form->getChildByName('cache_time')->val();
		if($cacheStatus == 0) {
			$cacheTime = 0;
		} else if($cacheStatus == 2) { // ProCache
			$pwpc = $modules->getModuleConfigData('ProCache');
			if(is_array($pwpc)) {
				if(!$cacheTime) $cacheTime = isset($pwpc['cacheTime']) ? -1 * $pwpc['cacheTime'] : -1;
				$cacheTemplates = isset($pwpc['cacheTemplates']) ? $pwpc['cacheTemplates'] : array();
				if(!in_array($template->id, $cacheTemplates)) {
					$cacheTemplates[] = $template->id;
					$pwpc['cacheTemplates'] = $cacheTemplates;
					$modules->saveModuleConfigData('ProCache', $pwpc);
				}
			}
			if($cacheTime) $cacheTime = $cacheTime * -1; // negative value indicates use of ProCache over template cache
		}
		$template->cache_time = $cacheTime;
		$template->setIcon($form->getChildbyName('pageLabelIcon')->val());
		$template->noCacheGetVars = $sanitizer->names($form->getChildByName('noCacheGetVars')->val(), ' ', array('-', '_', '.', '*'));
		$template->noCachePostVars = $sanitizer->names($form->getChildByName('noCachePostVars')->val(), ' ', array('-', '_', '.', '*'));
		
		if($template->cacheExpire == Template::cacheExpireSpecific) {
			$template->cacheExpirePages = $form->getChildByName('cacheExpirePages')->val();
		} else {
			$template->cacheExpirePages = array();
		}

		if($template->cacheExpire == Template::cacheExpireSelector) {
			$f = $form->getChildByName('cacheExpireSelector');
			if($f) $template->cacheExpireSelector = $f->val();
		} else {
			$template->cacheExpireSelector = '';
		}

		/*** LANGUAGES ***********************************************************/
		
		if($languages) {
			$template->noLang = (int) $form->getChildByName('noLang')->val();
		}
		
		// template label and tab labels, including multi language versions if applicable
		$langProperties = array(
			'label',
			'tabContent',
			'tabChildren',
			'nameLabel',
		);

		foreach($langProperties as $name) {
			$f = $name == 'label' ? $form->getChildByName('templateLabel') : $form->getChildByName($name);
			$template->$name = $sanitizer->text($f->val());
			if($languages) {
				foreach($languages as $language) {
					if($language->isDefault()) continue;
					$template->set($name . $language->id, $sanitizer->text($f->get('value' . $language->id)));
				}
			}
		}
		
		/*** FILES ***************************************************************/

		foreach(array('prependFile', 'appendFile') as $name) {
			$value = $form->getChildByName($name)->val();
			if(empty($value)) {
				$value = '';
			} else {
				$value = trim(trim($sanitizer->path($value), '/'));
				if(strpos($value, '..')) $value = '';
				if($value) {
					if(!is_file($config->paths->templates . $value)) {
						$this->error(
							"$name: " . $config->urls->templates . $value . " - " . 
							$this->_('Warning, file does not exist')
						);
					}
				}
			}
			$template->$name = $value;
		}

		$template->altFilename = basename($form->getChildByName('altFilename')->val(), ".$config->templateExtension");


		/*** FAMILY **************************************************************/

		if($input->post('noChildren')) {
			$template->noChildren = 1;
			$template->childTemplates = array();
		} else {
			$a = array();
			$v = $input->post('childTemplates'); 
			if(is_array($v)) {
				foreach($v as $id) $a[] = (int) $id;
			}
			$template->childTemplates = $a;
			if($template->noChildren) $template->noChildren = 0;
		}

		$a = array();
		$v = $input->post('parentTemplates');
		if(is_array($v)) {
			foreach($v as $id) $a[] = (int) $id;
		}
		$template->parentTemplates = $a;

		if($input->post('noParents') == -1) {
			$template->noParents = -1;
		} else if($input->post('noParents') == 1) {
			$template->noParents = 1;
			$template->parentTemplates = array();
		} else {
			if($template->noParents != 0) $template->noParents = 0;
		}

		$sortfield = $sanitizer->name($input->post('sortfield'));
		$sortfieldReverse = (int) $input->post('sortfield_reverse');
		if($sortfield && $sortfield != 'sort' && $sortfieldReverse) $sortfield = '-' . $sortfield;
		$template->sortfield = $sortfield;

		// remove deprecated property childrenTemplatesID
		// if(!$template->childrenTemplatesID) $template->remove("childrenTemplatesID");

		/*** ADVANCED ***********************************************************/
		
		if($config->advanced) {
			if($form->getChildByName('flagSystem')->val()) {
				$template->flags = $template->flags | Template::flagSystem;
			}
			$template->pageClass = $form->getChildByName('pageClass')->val();
			$intProperties = array(
				'noGlobal',
				'noSettings',
				'noTrash',
				'nameContentTab',
			);
			foreach($intProperties as $name) {
				$template->set($name, (int) $form->getChildByName($name)->val());
			}
		}

		/*** ACCESS **************************************************************/

		if($template->redirectLogin < 0) {
			$template->redirectLogin = $form->getChildByName('redirectLoginURL')->val();
		}

		foreach(array('roles', 'addRoles', 'editRoles', 'createRoles') as $key) {
			$value = $input->post($key);
			if(!is_array($value)) $value = array();
			foreach($value as $k => $v) $value[(int)$k] = (int) $v;
			$template->set($key, $value);
		}

		$rolesPermissions = array();
		foreach(array('rolesPermissionsAdd', 'rolesPermissionsRevoke') as $key) {
			$value = $input->post($key);
			if(!is_array($value)) $value = array();
			foreach($value as $v) {
				list($roleID, $permissionID) = explode(':', $v);
				$roleID = (int) $roleID;
				$permissionID = (int) $permissionID;
				if(!isset($rolesPermissions["$roleID"])) $rolesPermissions["$roleID"] = array();
				$rolesPermissions["$roleID"][] = "$permissionID";
			}
		}
		$template->set('rolesPermissions', $rolesPermissions);
		
		/*************************************************************************/

		if(!$redirectUrl) {
			$redirectUrl = $this->saveFields();
		}

		/*** RENAME **************************************************************/
		
		$rename = $form->getChildByName('rename');
		if($rename) {
			$rename = $rename->val();
			$_rename = $rename;
			$rename = $sanitizer->name($rename);
			if($rename && $template->name != $rename) { 
				if($redirectUrl) {
					$this->error($this->_('Skipped template rename - please complete that after the current action.'));
				} else if($_rename !== $rename) {
					$this->error($this->labels['invalidTemplateName']);
				} else if($templates->get($rename)) {
					$this->error(sprintf($this->labels['nameAlreadyInUse'], $rename)); 
				} else {
					$redirectUrl = "rename?id={$template->id}&name=$rename";
				}
			}
			unset($rename, $_rename);
		}

		/*** SAVE ****************************************************************/
		
		try {
			$changes = $template->getChanges();
			$template->save();
			$this->message(
				sprintf($this->_('Saved template: %s'), $template->name) . ' ' . 
				($config->debug && count($changes) ? '(' . implode(', ', $changes) . ')' : '')
			);
			if(count($changes)) {
				/** @var ProcessPageAdd $ppa */
				$ppa = $modules->get('ProcessPageAdd');
				$ppa->clearSessionCaches();
			}
		} catch(\Exception $e) {
			$this->error($e->getMessage());
		}
		
		/*** CLONE ***************************************************************/

		$cloneTemplateName = $input->post('clone_template');
		if($cloneTemplateName) {
			$_cloneTemplateName = $cloneTemplateName;
			$cloneTemplateName = $sanitizer->name($cloneTemplateName);
			if(!$cloneTemplateName || $_cloneTemplateName !== $cloneTemplateName) {
				$this->error($this->labels['invalidTemplateName']); 
			} else if($templates->get($cloneTemplateName)) {
				$this->error(sprintf($this->labels['nameAlreadyInUse'], $cloneTemplateName)); 
			} else {
				$clone = $templates->clone($this->template, $cloneTemplateName);
				if($clone) {
					$this->message(sprintf($this->_('Created clone of template "%1$s" named "%2$s".'), $template->name, $clone->name));
					if(!$redirectUrl) $redirectUrl = "./edit?id=$clone->id";
				} else {
					$this->error($this->_('Error creating clone of this template'));
				}
			}
			unset($cloneTemplateName, $_cloneTemplateName);
		}

		/*** PAGEFILESECURE ******************************************************/
		
		if($pagefileSecurePrev !== $template->pagefileSecure) {
			// change to the pagefileSecure setting
			$findSelector = "templates_id=$template->id, include=all";
			$qty = $this->wire()->pages->count($findSelector);
			if($qty < 1000) {
				$qty = $template->checkPagefileSecure();
				$this->message(sprintf($this->_('Renamed %d page file path(s) for change to secure files option'), $qty), Notice::noGroup);
			} else {
				$this->warning(
					sprintf($this->_('Your change to the secure files option will be applied as file/image fields on each of the %d affected pages are accessed.'), $qty) . ' ' . 
					$this->_('Note that this may take some time. To apply to all now, execute the following API code from a template file:') . ' ' . 
					"`\$templates->get('$template->name')->checkPagefileSecure();`",
					Notice::noGroup
				); 
			}
		}
		
		/*************************************************************************/

		if(!$redirectUrl) $redirectUrl = "edit?id=$template->id";
		
		$session->redirect($redirectUrl);
	}

	/**
	 * Import the fields from the given fieldgroup to this template's fieldgroup
	 *
	 * This is used by both the add and save functions. 
	 *
	 * @param Fieldgroup $fieldgroup Fieldgroup to import
	 * @param Template $template Template to import to
	 *
	 */
	protected function importFieldgroup(Fieldgroup $fieldgroup, Template $template) {
		$total = 0;
		$hasContext = false;
		$templateFieldgroup = $template->fieldgroup;
		foreach($fieldgroup as $field) {
			/** @var Field $field */
			// if template already has the field, leave it, unless it's a closing fieldset.
			// necessary because the fieldAdded hook may automatically add a closing fieldset, 
			// so this prevents things from getting out of order. 
			if(!$templateFieldgroup->has($field) || $field->type instanceof FieldtypeFieldsetClose) {
				$total++;
				$templateFieldgroup->add($field); 
				$this->fieldAdded($field, $template); 
			}
			$contextData = $fieldgroup->getFieldContextArray($field->id);
			if(!empty($contextData)) {
				$templateFieldgroup->setFieldContextArray($field->id, $contextData);
				$hasContext = true;
			}
		}
		if($hasContext) {
			$templateFieldgroup->save();
			$templateFieldgroup->saveContext();
		}

		$this->message(sprintf($this->_('Duplicated fields from "%1$s" to "%2$s"'), $fieldgroup, $templateFieldgroup)); 
	}

	/**
	 * Save the fields specified for this template/fieldgroup
	 * 
	 * @return string redirect URL, if applicable
	 *
	 */
	protected function saveFields() {

		$sanitizer = $this->wire()->sanitizer;
		$fields = $this->wire()->fields;
		$input = $this->wire()->input;

		/** @var FieldsArray $removedFields */
		$removedFields = $this->wire(new FieldsArray());

		$fieldgroup = $this->template->fieldgroup;
		if($fieldgroup->name != $this->template->name) return '';
		
		$ids = $input->post('fieldgroup_fields'); 
		$saveFieldgroup = false; 
		
		if(is_array($ids) && $input->post('_fieldgroup_fields_changed') == 'changed') {
			$saveFieldgroup = true;
			$badFieldsets = array();
			
			foreach($ids as $id) {
				if(!($id = (int) $id)) continue;
				$isDeleted = $id < 0;
				$id = abs($id);
				if(!$field = $fields->get($id)) continue;
				
				if(!$fieldgroup->has($field)) {
					$fieldgroup->append($field);
					$this->fieldAdded($field, $this->template);
				} else {
					$field = $fieldgroup->getField($field->id, true); // get in context
					$fieldgroup->append($field);
				}

				if($isDeleted) {
					if(!$this->template->getConnectedField() && (($field->flags & Field::flagGlobal) && !$this->template->noGlobal)) {
						$this->error(sprintf($this->_('Field "%s" may not be removed because it is globally required by all fieldgroups'), $field));
					} else {
						$removedFields->add($field);
					}
				}
				
				if($field->type instanceof FieldtypeFieldsetClose) {
					if(isset($badFieldsets[$field->name])) {
						// matches an open fieldset, so can be removed from badFieldsets
						unset($badFieldsets[$field->name]);
					} else {
						// does not match an open fieldset and should not be here
						$badFieldsets[$field->name] = $field;
					}
				} else if($field->type instanceof FieldtypeFieldsetOpen) {
					// queue it for checking that there is a matching fieldset_END 
					$badFieldsets[$field->name . '_END'] = $field;
				}
			}
		
			if(count($badFieldsets)) {
				foreach($badFieldsets as $field) {
					$this->error(sprintf($this->_('Error with placement of fieldset/tab "%s" - please fix and save again'), $field->name));
				}
				// $fieldset = $fieldtypes->get('FieldtypeFieldsetOpen');
				// if($fieldset->checkFieldgroupFieldsets($this->template->fieldgroup)) $saveFieldgroup = true;
			}
		}

		// check if any other fieldgroup should be imported
		if($input->post('import_fieldgroup')) {
			$this->importFieldgroup($this->fieldgroups->get($sanitizer->name($input->post('import_fieldgroup'))), $this->template); 
			$saveFieldgroup = true; 
		}

		if($saveFieldgroup) {
			$fieldgroup->save();
			$this->message(sprintf($this->_('Saved fieldgroup: %s'), $fieldgroup->name), Notice::debug);
		}

		if(count($removedFields)) {
			$url = "removeFields?id={$this->template->id}&fields=";
			foreach($removedFields as $field) {
				/** @var Field $field */
				$url .= $field->id . ',';
			}
			return rtrim($url, ','); 
		}
		
		return '';	
	}

	/**
	 * Confirm the fieldgroup change with another form that shows what will be deleted
	 * 
	 * @return string
	 *
	 */
	public function ___executeFieldgroup() {
		
		$modules = $this->wire()->modules;
		$input = $this->wire()->input;
		$session = $this->wire()->session;

		$fieldgroupID = (int) $input->get('fieldgroup');
		if(!$fieldgroupID) $session->redirect('./'); 
		
		$fieldgroup = $this->wire()->fieldgroups->get($fieldgroupID); 
		if(!$fieldgroup) $session->redirect('./');

		$this->breadcrumb("./", $this->moduleInfo['title']);
		$this->breadcrumb("./edit?id={$this->template->id}", $this->template->name);
		
		/** @var InputfieldForm $form */
		$form = $modules->get('InputfieldForm'); 
		$form->attr('action', 'saveFieldgroup'); 
		$form->attr('method', 'post'); 

		$list = '';
		$icon = wireIconMarkup('times-circle');
		foreach($this->template->fieldgroup as $field) {
			if($fieldgroup->has($field)) continue;
			$list .= "<li class='ui-state-error-text'>$icon $field->name</li>";
		}

		// if nothing will be lost with the fieldgroup change, then just do it now
		if(!$list) $this->executeSaveFieldgroup($fieldgroup);

		/** @var InputfieldMarkup $f */
		$f = $modules->get("InputfieldMarkup"); 
		$f->attr('id', 'changed_fields'); 
		$f->label = $this->_('Fields that will be deleted');
		$f->description = 
			sprintf(
				$this->_('You have requested to change the Fieldgroup from "%1$s" to "%2$s".'), 
				$this->template->fieldgroup->name, $fieldgroup->name
			) . ' ' . 
			$this->labels['numPages'] . ' ' . 
			$this->_('The following fields will be permanently deleted on pages using this template:');
		$f->value = "<ul>$list</ul>";
		$form->append($f); 
	
		/** @var InputfieldCheckbox $f */
		$f = $modules->get("InputfieldCheckbox"); 
		$f->attr('name', 'fieldgroup'); 
		$f->value = $fieldgroup->id; 
		$f->label = $this->labels['Are you sure?']; 
		$f->description = $this->_('Please confirm that you understand the above and that you want to change the fieldgroup by checking the box and submitting this form.'); // Confirm fieldgroup, description
		$form->append($f); 

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

		/** @var InputfieldSubmit $f */
		$f = $modules->get('InputfieldSubmit');
		$f->attr('name', 'submit_change_fieldgroup'); 
		$f->attr('value', $this->_x('Continue', 'submit-fieldgroup')); 
		$form->append($f); 

		$form->description = sprintf(
			$this->_('Please confirm that you want to change the Fieldgroup from "%1$s" to "%2$s"'), 
			$this->template->fieldgroup->name, $fieldgroup->name
		); 	
		
		$this->headline(sprintf($this->_('Change Fieldgroup for Template: %s'), $this->template)); 

		return $form->render();
	}

	/**
	 * Save the fieldgroup
	 *
	 * May be called from a POST action, or from the executeFieldgroup() method directly. 
	 * When called from the executeFieldgroup() method, a fieldgroup param should be provided. 
	 *
	 * @param Fieldgroup $fieldgroup Optional
	 * @throws WireCSRFException
	 *
	 */
	public function ___executeSaveFieldgroup($fieldgroup = null) {
		
		$session = $this->wire()->session;
		$input = $this->wire()->input;

		if(!$this->template) $session->redirect('./'); 	

		if(is_null($fieldgroup)) {
			$session->CSRF->validate();
			$fieldgroupID = (int) $input->post('fieldgroup');
			if(!$fieldgroupID || !$fieldgroup = $this->wire()->fieldgroups->get($fieldgroupID)) {
				$this->message($this->_('Fieldgroup change aborted')); 
				$session->redirect("./"); 
			}
		}

		$this->template->fieldgroup = $fieldgroup; 
		$this->template->save(); 
		$this->message(sprintf($this->_('Changed template fieldgroup to: %s'), $fieldgroup->name)); 
		
		$session->redirect("edit?id={$this->template->id}"); 
	}

	/**
	 * Execute "remove fields" action
	 * 
	 * @throws WireCSRFException
	 * @return string
	 * 
	 */
	public function executeRemoveFields() {
		
		$input = $this->wire()->input;
		$session = $this->wire()->session;

		if(!$input->post('submit_remove_fields')) return $this->renderRemoveFields();
		
		$removeFields = $input->post('remove_fields');
		if(!is_array($removeFields)) $session->redirect("edit?id={$this->template->id}"); 
		
		$session->CSRF->validate();

		foreach($this->template->fieldgroup as $field) {
			if(in_array($field->id, $removeFields)) {
				$this->template->fieldgroup->remove($field); 
				$this->fieldRemoved($field, $this->template); 
			}
		}
		$this->template->fieldgroup->save();
		$session->redirect("edit?id={$this->template->id}"); 
		
		return '';
	}

	/**
	 * Render for "remove fields" action
	 * 
	 * @return string
	 * 
	 */
	public function renderRemoveFields() {
	
		$modules = $this->wire()->modules;
		$session = $this->wire()->session;
		$input = $this->wire()->input;

		$removeIds = $input->get('fields'); 
		if(empty($removeIds)) $session->redirect('./');
		
		$removeIds = explode(',', $removeIds); 
		$removeFields = array();
		$fieldgroup = $this->template->fieldgroup; 
		
		$this->headline(sprintf($this->_('Remove Fields from Template: %s'), $this->template->name)); 
		$this->breadcrumb("./", $this->moduleInfo['title']);
		$this->breadcrumb("edit?id={$this->template->id}", $this->template->name);

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

		/** @var InputfieldCheckboxes $checkboxes */
		$checkboxes = $modules->get("InputfieldCheckboxes"); 
		$checkboxes->label = $this->_('Remove fields from template');
		$checkboxes->icon = 'times-circle';
		$checkboxes->attr('name', 'remove_fields'); 
		$checkboxes->description = $this->_("You have asked to remove one or more fields from the template. This will result in data associated with the fields below being permanently deleted. If the fields that are removed contain a lot of data, it may take time for this operation to complete after you confirm and submit this form. Please confirm that you understand this and want to delete the field(s) by checking the boxes below.");
		
		foreach($fieldgroup as $field) {
			if(!in_array($field->id, $removeIds)) continue;
			$removeFields[$field->id] = $field;
			if($field->type instanceof FieldtypeFieldsetClose) {
				$opener = $field->type->getFieldsetOpenField($field);
				if($opener && $fieldgroup->hasField($opener) && !in_array($opener->id, $removeIds)) {
					unset($removeFields[$field->id]); 
					$removeFields[$opener->id] = $opener;
				}
			}
		}

		foreach($removeFields as $field) {
			$checkboxes->addOption($field->id, sprintf(
				$this->_('Remove field "%1$s" from template "%2$s"'), 
				$field->name, $this->template->name
			)); 
		}

		$form->append($checkboxes); 

		/** @var InputfieldSubmit $submit */
		$submit = $modules->get('InputfieldSubmit');
		$submit->attr('value', $this->_x('Remove Fields', 'submit-remove')); 
		$submit->attr('name', 'submit_remove_fields'); 
		$form->append($submit); 

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

	/**
	 * Rename the template
	 * 
	 * @return string
	 * @throws WireException
	 *
	 */
	public function ___executeRename() {
		
		$input = $this->wire()->input;
		$config = $this->wire()->config;
		$session = $this->wire()->session;
		$modules = $this->wire()->modules;
		$sanitizer = $this->wire()->sanitizer;

		if($this->template->flags & Template::flagSystem) {
			throw new WireException($this->_('This template cannot be renamed because it is a system template'));
		}

		$this->breadcrumb('./', $this->moduleInfo['title']);
		$this->breadcrumb('./?id=' . $this->template->id, $this->template->name);

		$redirectUrl = $this->template ? "edit?id={$this->template->id}" : "../";
		
		if($input->post('confirm_rename')) {
			$name = $input->post('confirm_rename');
		} else if($input->get('name')) {
			$name = $input->get('name');
		} else {
			$name = '';
		}
		
		$_name = $name;
		$name = $sanitizer->name($name); 
		
		if($_name !== $name) {
			$this->error($this->labels['invalidTemplateName']);
			$name = '';
		}
		
		if(!$name) {
			$session->redirect($redirectUrl);
			return '';
		}

		if($this->wire()->templates->get($name) || $this->wire()->fieldgroups->get($name)) {
			$this->error(sprintf($this->labels['nameAlreadyInUse'], $name)); 	
			$session->redirect($redirectUrl); 
			return '';
		}

		$pathname = $this->template->filename; 
		$filename = basename($this->template->filename); 
		$basename = basename($this->template->filename, '.' . $config->templateExtension); 
		$newFilename = "$name." . $config->templateExtension;
		$newPathname = $config->paths->templates . $newFilename; 
		$templateHasFile = is_file($this->template->filename) && $basename == $this->template->name; // template has file that it is also owner of
		$writable = is_writable($this->template->filename); 

		if($input->post('confirm_rename')) {
			$session->CSRF->validate();
			$oldName = $this->template->name; 
			$this->template->name = $name; 
			$this->template->fieldgroup->name = $name; 
			$this->template->save();
			$this->template->fieldgroup->save();
			$this->message(sprintf($this->_('Renamed template "%1$s" to "%2$s"'), $oldName, $name)); 

			if($templateHasFile) { 
				if($writable) {
					if(rename($pathname, $newPathname)) {
						$this->message(sprintf($this->_('Renamed template file "%1$s" to "%2$s"'), $filename, $newFilename)); 
					} else {
						$this->error(sprintf($this->_('Unable to rename "%1$s" to "%2$s" in directory "%3$s". You must rename the file manually on your file system.'), $filename, $newFilename, $this->config->urls->templates)); 
					}

				} else if(!is_file($newPathname)) { 
					$this->error(sprintf($this->_('The template file "%1$s" was not found in directory "%2$s". You must rename the file "%3$s" to "%1$s" manually on your file system.'), $newFilename, $this->config->urls->templates, $filename));
				}
			}

			$session->redirect($redirectUrl); 
			return '';
		}

		/** @var InputfieldForm $form */
		$form = $modules->get("InputfieldForm"); 
		$form->attr('method', 'post'); 
		$form->attr('action', "rename?id={$this->template->id}"); 
		$form->description = sprintf($this->_('Rename template "%1$s" to "%2$s"'), $this->template->name, $name);

		/** @var InputfieldCheckbox $field */
		$field = $modules->get("InputfieldCheckbox"); 
		$field->label = $this->labels['Are you sure?']; 
		$field->attr('name', 'confirm_rename'); 
		$field->attr('value', $name); 
		$field->description = $this->_('Please confirm that you want to rename this template by checking the box below.');

		// check if we have a file to rename
		if($templateHasFile) {
			if($writable) {
				$field->notes = sprintf(
					$this->_('Template file "%s" appears to be writable so we will attempt to rename it when you click submit.'), 
					$filename
				);
			} else {
				$this->error(sprintf(
					$this->_('Template file "%1$s" is not writable. Please rename that file to "%2$s" before clicking submit.'), 
					$config->urls->templates . $filename, $newFilename
				));
			}
		}

		$form->add($field);

		/** @var InputfieldSubmit $field */
		$field = $modules->get("InputfieldSubmit"); 
		$field->attr('id+name', 'submit_confirm_rename'); 
		$form->add($field); 

		return $form->render();
	}

	/**
	 * For hooks to listen to when a field is removed from a template
	 * 
	 * @param Field $field
	 * @param Template $template
	 *
	 */
	public function ___fieldRemoved(Field $field, Template $template) { 
		$this->message(sprintf($this->_('Removed field "%1$s" from template/fieldgroup "%2$s"'), $field, $template)); 
	}

	/**
	 * For hooks to listen to when a field is added to a template
	 * 
	 * @param Field $field
	 * @param Template $template
	 *
	 */
	public function ___fieldAdded(Field $field, Template $template) { 
		$this->message(sprintf($this->_('Added field "%1$s" to template/fieldgroup "%2$s"'), $field, $template)); 
	}


	/**
	 * Execute import
	 *
	 * @return string
	 *
	 */
	public function ___executeImport() {

		$this->headline($this->labels['Import']);
		$this->breadcrumb('../', $this->moduleInfo['title']);

		require(dirname(__FILE__) . '/ProcessTemplateExportImport.php');
		/** @var ProcessTemplateExportImport $o */
		$o = $this->wire(new ProcessTemplateExportImport());
		/** @var InputfieldForm $form */
		$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__) . '/ProcessTemplateExportImport.php');
		/** @var ProcessTemplateExportImport $o */
		$o = $this->wire(new ProcessTemplateExportImport());
		/** @var InputfieldForm $form */
		$form = $o->buildExport();
		
		return $form->render();
	}
	
	/**
	 * Handle the “Manage Tags” actions
	 *
	 * @return string
	 *
	 */
	public function ___executeTags() {

		$input = $this->wire()->input;
		$modules = $this->wire()->modules;
		$templates = $this->wire()->templates;
		$sanitizer = $this->wire()->sanitizer;
		$form = $modules->get('InputfieldForm'); /** @var InputfieldForm $form */

		$out = '';
		$labels = $this->labels;
		$headline = $labels['tags'];
		$this->headline($headline);
		$this->breadcrumb('../', $labels['templates']);

		$templateNamesByTag = $templates->getTags(true);
		$editTag = $sanitizer->words($input->get->text('edit_tag'), array('separator' => '-'));
		$saveTag = $sanitizer->words($input->post->text('save_tag'), array('separator' => '-'));

		$tags = array();
		foreach(array_keys($templateNamesByTag) as $tag) {
			$tags[$tag] = $tag;
		}
		
		$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[tags] - " . (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_templates');
			$f->label = $this->_('Select all templates that should have this tag');
			$f->table = true;
			$f->icon = 'cubes';
			$f->thead = "$labels[name]|$labels[label]|$labels[tags]";
			$value = array();
			foreach($templates as $template) {
				/** @var Field $field */
				if($template->flags & Template::flagSystem) continue;
				$templateTags = $template->getTags();
				$templateLabel = str_replace('|', ' ', $template->label);
				$f->addOption($template->name, "**$template->name**|$templateLabel|" . implode(', ', $templateTags));
				if(isset($templateTags[$editTag])) $value[] = $template->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 templates 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 templates from it.') . "</p>";
			$form->add($f);

		} else if($saveTag) {
			// save tag
			$tagTemplates = $sanitizer->names($input->post('tag_templates'));
			$renameTag = $sanitizer->word($input->post->text('rename_tag'), array('separator' => '-'));
			$isCollapsed = (int) $input->post('tag_collapsed');
			$removeTag = '';
			if($renameTag && $renameTag != $saveTag) {
				$removeTag = $saveTag;
				$saveTag = $renameTag;
			}
			foreach($templates as $template) {
				/** @var Template $template */
				if($removeTag && $template->hasTag($removeTag)) {
					$template->removeTag($removeTag);
				}
				if(is_array($tagTemplates) && in_array($template->name, $tagTemplates)) {
					// template should have the given tag
					if($template->hasTag($saveTag)) continue;
					$template->addTag($saveTag);
					$this->message(sprintf($this->_('Added tag “%1$s” to template: %2$s'), $saveTag, $template->name));
				} else if($template->hasTag($saveTag)) {
					// template should not have the given tag
					$template->removeTag($saveTag);
					$this->message(sprintf($this->_('Removed tag “%1$s” from template: %2$s'), $saveTag, $template->name));
				}
				if($template->isChanged('tags')) $template->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('./');
			return '';

		} else {
			// list defined tags
			$out .= "<p class='description'>" . $this->_('Tags enable you to create collections of templates for listing.') . "</p>";
			/** @var MarkupAdminDataTable $table */
			$table = $modules->get('MarkupAdminDataTable');
			$table->setSortable(false);
			$table->setEncodeEntities(false);
			$table->headerRow(array($labels['name'], $labels['templates']));

			foreach($tags as $tag) {
				$table->row(array(
					$tag => "./?edit_tag=$tag",
					implode(', ', $templateNamesByTag[$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, hyphens, underscores.');
			$f->icon = 'tag';
			$f->addClass('InputfieldIsSecondary', 'wrapClass');
			$form->add($f);
		}

		$f = $modules->get('InputfieldSubmit');
		$form->add($f);
		$out .= $form->render();

		return $out;
	}

	/**
	 * 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();
		$property = isset($options['property']) ? $options['property'] : '';

		$result = array(
			'title' => $page->id ? $page->title : $this->className(),
			'items' => array(),
			'total' => 0, 
			'properties' => array(
				'name',
				'label', 
				'tags',
				'fields',
			)
		);
		
		if(!empty($options['help'])) return $result;
		if(!empty($property) && !in_array($property, $result['properties'])) return $result;

		$looseItems = array();
		$exactItems = array();
		$cnt = 0;

		foreach($this->wire()->templates as $item) {
			/** @var Template $item */
			$search = array(' ');
			if(!$property || $property == 'name' ) $search[] = $item->name;

			if(!$property || $property == 'label') {
				if(!empty($options['multilang']) && $languages) {
					foreach($languages as $lang) {
						$search[] = $item->getLabel($lang);
					}
				} else {
					$search[] = $item->getLabel();
				}
			}

			// when search matches field name exactly (that template has), allow template to be matched
			if($property == 'fields') {
				foreach($item->fieldgroup as $field) {
					if(strtolower($text) == strtolower($field->name)) $search[] = $field->name;
				}
			} else if($property == 'tags') {
				$search[] = $item->tags;
			}

			$search = implode(' ', $search);
			$pos = stripos($search, $text);
			if($pos === false) continue;
			$exact = stripos($search, " $text");
			$numFields = $item->fieldgroup->count();
			$labelFields = sprintf($this->_n('%d field', '%d fields', $numFields), $numFields);
			$label = $item->getLabel();
			$subtitle = $label == $item->name ? $labelFields : "$label ($labelFields)";
			
			$result['total']++;
			if(!empty($options['limit']) && $cnt >= $options['limit']) continue;

			$item = array(
				'id' => $item->id,
				'name' => $item->name,
				'title' => $item->name,
				'subtitle' => trim($subtitle), 
				'summary' => $item->fieldgroup->implode(', ', 'name'),
				'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;
	}
	
	/**
	 * Build a form allowing configuration of this Module
	 *
	 * @param InputfieldWrapper $inputfields
	 *
	 */
	public function getModuleConfigInputfields(InputfieldWrapper $inputfields) { }

}
