'Selector', 'version' => 22, 'summary' => 'Build a page finding selector visually.', 'author' => 'Avoine + ProcessWire', 'autoload' => "template=admin", ); } const debug = false; /** * Contains all possible operators, indexed by operator with values as labels describing the operator * * @var array */ protected $operators = array(); /** * Array indexed by input types to operator arrays where values are the operators (no labels) * * @var array * */ protected $operatorsByType = array(); /** * Characters that should be automatically trimmed from any operators * * @var string * */ protected $operatorTrimChars = ''; /** * Array of predefined system fields indexed by field name and Fieldtype::getSelectorInfo style arrays as the value * * @var array * */ protected $systemFields = array(); /** * Same as $systemFields except contains only system fields applicable to page references * * @var array * */ protected $systemPageFields = array(); /** * Fields that modify behaviors of selectors but don't refer to any data: sort, limit, include * * @var array * */ protected $modifierFields = array(); /** * Instance of Selectors, when in the process of rendering * * @var Selectors * */ protected $selectors = null; /** * Variable names that should be picked up through the session each time * */ protected $sessionVarNames = array( 'allowSystemCustomFields', 'allowSystemNativeFields', 'allowSystemTemplates', 'allowSubselectors', 'allowSubfields', 'allowSubfieldGroups', 'allowModifiers', 'dateFormat', 'datePlaceholder', 'timeFormat', 'timePlaceholder', ); /** * Default values of each setting, for outside configuration * * @var array * */ protected $defaultSettings = array(); /** * Initialize the default values used in Selector * * See the comments a the top of this file for descriptions of all settings initialized here. * */ public function __construct() { $this->attr('name', 'selector'); // default name, which presumably will be changed $this->setting('preview', 1); // whether to show a live preview in notes section $this->setting('counter', 1); // whether to show the live ajax number-of-page-matches counter $this->setting('initValue', ''); // initial selector value: not changeable by user $this->setting('addIcon', 'plus-circle'); $this->setting('addLabel', $this->_('Add Field')); $this->set('subfieldIdentifier', ' …'); $this->set('groupIdentifier', ' ' . $this->_('(1)')); // group identifier // what is allowed $this->setting('allowSystemCustomFields', false); $this->setting('allowSystemNativeFields', true); $this->setting('allowSystemTemplates', false); $this->setting('allowSubselectors', true); $this->setting('allowSubfields', true); $this->setting('allowSubfieldGroups', true); $this->setting('allowModifiers', true); $this->setting('allowBlankValues', false); // blank values are allowed in selector or if not present, blank values allowed and represented by ="" $this->setting('showInitValue', false); // whether or not inputs for the initValue should be shown (they won't be modifyable if shown) $this->setting('showFieldLabels', false); // makes it use field labels, rather than names (when true) $this->setting('showOptgroups', true); // show option groups to separate system, field, subfield, etc. $this->setting('optgroupsOrder', 'system,field,subfield,group,modifier,adjust'); // field selection option groups and order to render them in (applicable only if showOptgroups=true) $this->setting('exclude', ''); // CSV fields to disallow $this->setting('dateFormat', $this->_('Y-m-d')); // date format $this->setting('datePlaceholder', $this->_('yyyy-mm-dd')); // date format placeholder (what users see) $this->setting('timeFormat', $this->_('H:i')); // time format $this->setting('timePlaceholder', $this->_('hh:mm')); // time format placeholder (what users see) parent::__construct(); } /** * Same as set() except that this remembers the default value populated to it for later retrieval from getDefaultSettings() * * @param $key * @param $value * */ protected function setting($key, $value) { $this->set($key, $value); $this->defaultSettings[$key] = $value; } /** * Return the default settings * * @return array of key=>value * */ public function getDefaultSettings() { return $this->defaultSettings; } /** * Return the configured settings * * @return array of key=>value * */ public function getSettings() { $settings = $this->defaultSettings; foreach($this->getArray() as $key => $value) { if(array_key_exists($key, $settings)) $settings[$key] = $value; } return $settings; } /** * Monitor and respond to AJAX events * */ public function ready() { $input = $this->wire('input'); $name = $input->get('name'); $action = $input->get($this->className()); if(!$action || !$name) return; if(!self::debug && !$this->wire('config')->ajax) return; $this->attr('name', $this->wire('sanitizer')->fieldName($name)); // for session validity if(!$this->sessionGet('valid')) return; if(!$this->wire('user')->isLoggedin()) return; $this->set('initValue', $this->sessionGet('initValue')); $this->setup(); $sanitizer = $this->wire('sanitizer'); foreach($this->sessionVarNames as $key) { $this->set($key, $this->sessionGet($key)); } if($action == 'field') { $out = $this->renderSelectField(); } else if($action == 'subfield' && ($fieldName = $input->get->field)) { $fieldName = $sanitizer->name($fieldName); if(strpos($fieldName, '.')) list($fieldName, $subfieldName) = explode('.', $fieldName); $out = $this->renderSelectSubfield($fieldName); } else if($action == 'opval' && ($fieldName = $input->get->field)) { $fieldName = $sanitizer->name($fieldName); //$subfield = $input->get->subfield ? $sanitizer->name($input->get->subfield) : ''; //if($subfield) $fieldName = "$fieldName.$subfield"; $type = $sanitizer->name($input->get->type); $out = $this->renderOpval($fieldName, $type); } else if($action == 'test' && ($selector = $input->post->selector)) { $out = $this->renderTestSelector($selector); } else if($action == 'autocomplete' && ($fieldName = $input->get('field')) && ($q = $input->get('q'))) { $out = $this->renderAutocompleteJSON($sanitizer->name($fieldName), $sanitizer->text($q)); } else { $out = "Ajax request missing required info"; } echo $out; exit; } /** * Setup the shared structures and data used by Selector * */ public function setup() { $this->operators = array( '=' => $this->_('Equals'), '!=' => $this->_('Not Equals'), '>' => $this->_('Greater Than'), '<' => $this->_('Less Than'), '>=' => $this->_('Greater Than or Equal'), '<=' => $this->_('Less Than or Equal'), '%=' => $this->_('Contains Text'), '*=' => $this->_('Contains Phrase'), '~=' => $this->_('Contains Words'), '^=' => $this->_('Starts With'), '$=' => $this->_('Ends With'), '.=' => $this->_('Ascending By'), '.=-' => $this->_('Descending By'), '@=' => $this->_('Has'), '@!=' => $this->_('Does Not Have'), '#=' => $this->_('Matches'), '#!=' => $this->_('Does Not Match'), '=""' => $this->_('Is Empty'), '!=""' => $this->_('Is Not Empty'), ); // operators by input type // this is a backup and/or for system fields, as these may also be specified // with fieldtype's getSelectorInfo() method, which takes precedence $this->operatorsByType = array( 'name' => array('=', '!=', '%='), 'text' => array('%=', '*=', '~=', '^=', '$=', '=', '!=', '=""', '!=""'), 'autocomplete' => array('=', '!='), 'number' => array('=', '!=', '<', '>', '<=', '>='), 'datetime' => array('=', '!=', '<', '>', '<=', '>='), 'page' => array('@=', '@!='), 'checkbox' => array('=', '!='), 'sort' => array('.=', '.=-'), 'status' => array('@=', '@!='), //'selector' => array('#=', '#!='), 'selector' => array('=', '!=', '<', '>', '<=', '>='), ); // chars that are trimmed off operators before being used // enables different contexts for the same operator $this->operatorTrimChars = '.@#'; $templates = array(); foreach($this->wire('templates') as $template) { if(($template->flags & Template::flagSystem) && !$this->allowSystemTemplates) { continue; } $templates[$template->id] = $template->label ? "$template->label ($template->name)" : $template->name; } // make users selectable if there are under 100 of them // otherwise utilize the user ID property $users = array(); $numUsers = $this->wire('pages')->count("template=user, include=all"); if($numUsers < 100) { foreach($this->wire('users') as $user) { $users[$user->id] = $user->name; } } $titleField = $this->wire('fields')->get('title'); // system fields definitions $this->systemFields = array( 'template' => array( 'input' => 'select', 'label' => $this->_('Template'), 'options' => $templates, 'sanitizer' => 'integer', 'operators' => array('=', '!='), ), 'title' => array( 'input' => 'text', 'label' => ($titleField ? $titleField->getLabel() : 'Title') ), 'id' => array( 'input' => 'number', 'label' => $this->_('ID'), 'sanitizer' => 'integer', ), 'name' => array( 'input' => 'text', 'label' => $this->_('Name'), 'sanitizer' => 'pageName', 'operators' => array('=', '!=', '%='), ), 'status' => array( 'input' => 'select', 'label' => $this->_('Status'), 'options' => array( 'hidden' => $this->_('Hidden'), 'unpublished' => $this->_('Unpublished'), 'locked' => $this->_('Locked'), 'trash' => $this->_('Trash'), ), 'sanitizer' => 'integer', 'operators' => array('@=', '@!='), ), 'modified' => array( 'input' => 'datetime', 'label' => $this->_('Modified date'), 'operators' => $this->operatorsByType['datetime'], ), 'created' => array( 'input' => 'datetime', 'label' => $this->_('Created date'), 'operators' => $this->operatorsByType['datetime'], ), 'modified_users_id' => array( 'input' => 'select', 'label' => $this->_('Modified by user'), 'options' => $users, 'operators' => array('=', '!='), ), 'created_users_id' => array( 'input' => 'select', 'label' => $this->_('Created by user'), 'options' => $users, 'operators' => array('=', '!='), ), 'num_children' => array( 'input' => 'number', 'label' => $this->_('Number of children'), 'sanitizer' => 'integer', ), 'count' => array( 'input' => 'number', 'label' => $this->_('Count'), 'sanitizer' => 'integer', ), 'path' => array( 'input' => 'text', 'label' => $this->_('Path/URL'), 'operators' => $this->operatorsByType['text'], ), 'parent' => array( 'input' => 'number', 'label' => $this->_x('Parent', 'parent-only'), 'operators' => array('=', '!='), ), 'parent.' => array( 'input' => 'subfields', 'label' => $this->_x('Parent',' parent-with-subfield'), ), 'has_parent' => array( 'input' => 'text', 'label' => $this->_('Has parent/ancestor'), 'operators' => array('=', '!='), ), '_custom' => array( 'input' => 'text', 'label' => $this->_('Custom (field=value)'), 'operators' => array(), 'placeholder' => $this->_('field=value'), ), //'parent' => $this->_('parent'), ); if(!count($users)) { $this->systemFields['modified_users_id']['type'] = 'number'; unset($this->systemFields['modified_users_id']['options']); $this->systemFields['created_users_id']['type'] = 'number'; unset($this->systemFields['created_users_id']['options']); } // system fields for page references $this->systemPageFields = array( 'id' => $this->systemFields['id'], 'name' => $this->systemFields['name'], 'status' => $this->systemFields['status'], 'modified' => $this->systemFields['modified'], 'created' => $this->systemFields['created'], ); $this->modifierFields = array( 'sort' => array( 'input' => 'select', 'label' => $this->_('Sort'), 'sanitizer' => 'fieldName', 'operators' => array('.=', '.=-'), 'options' => array() // populated below ), 'limit' => array( 'input' => 'integer', 'label' => $this->_('Limit'), 'operators' => array('=') ), 'include' => array( 'input' => 'select', 'label' => $this->_('Include'), 'options' => array( 'hidden' => $this->_('Hidden'), 'unpublished' => $this->_('Hidden + Unpublished'), 'trash' => $this->_('Hidden + Unpublished + Trash'), 'all' => $this->_('All'), ), 'operators' => array('=') ) ); // populate the sort options $options = array(); foreach($this->systemFields as $name => $f) { $options[$name] = $f['label']; } foreach($this->wire('fields') as $f) { if(strpos($f->type, 'FieldtypeFieldset') === 0) continue; $options[$f->name] = $f->name; } ksort($options); $this->modifierFields['sort']['options'] = $options; } /** * Set a session variable specific to this Inputfield instance * * @param string $key * @param mixed $value * @return this * */ protected function sessionSet($key, $value) { $s = $this->wire('session')->get($this->className()); if(!is_array($s)) $s = array(); if(count($s) > 30) $s = array_slice($s, -9); // prevent from growing too large $id = 'id' . $this->wire('page')->id . "_" . $this->wire('sanitizer')->fieldName($this->attr('name')); if(!isset($s[$id])) $s[$id] = array(); $s[$id][$key] = $value; $this->wire('session')->set($this->className(), $s); return $this; } /** * Retrieve a session variable specific to this Inputfield instance * * @param string $key * @return mixed * */ protected function sessionGet($key) { $s = $this->wire('session')->get($this->className()); if(!$s) return null; $id = 'id' . $this->wire('page')->id . "_" . $this->wire('sanitizer')->fieldName($this->attr('name')); if(empty($s[$id])) return null; if(empty($s[$id][$key])) return null; return $s[$id][$key]; } /** * Returns an array of selector information for the given Field or field name * * Front-end to Fieldtype::getSelectorInfo * * @param string|Field $field * @return array Blank array if information not available * */ public function getSelectorInfo($field) { if(is_string($field)) { if(isset($this->systemFields[$field])) return $this->systemFields[$field]; if(isset($this->modifierFields[$field])) return $this->modifierFields[$field]; $field = $this->wire('fields')->get($field); } if(!$field || !$field instanceof Field || !$field->type) return array(); $info = $field->type->getSelectorInfo($field); if($info['input'] == 'page') { $info['subfields'] = array_merge($info['subfields'], $this->systemPageFields); } if(!empty($info['subfields'])) ksort($info['subfields']); return $info; } /** * Does the given $field have subfields? * * @param Field $field * @return bool * */ protected function hasSubfields(Field $field) { return count($info['subfields']) > 0; } /** * Render the results of a selector test: how many pages match * * @param $selector * @return string * */ protected function renderTestSelector($selector) { try { $selector = $this->sanitizeSelectorString($selector); $cnt = $this->wire('pages')->count($selector); $out = ''; // take into account a limit=n if(strpos($selector, 'limit=') !== false && preg_match('/\blimit=(\d+)/', $selector, $matches)) { $out = ' ' . sprintf($this->_('(%d without limit)'), $cnt); if($cnt > $matches[1]) $cnt = $matches[1]; } $out = sprintf($this->_n('matches %d page', 'matches %d pages', $cnt), $cnt) . $out; if(self::debug) $out .= " (" . $selector . ")"; if($cnt > 0) { // bookmarks for Lister $bookmark = array( 'defaultSelector' => $selector, 'initSelector' => $this->initValue, ); $this->wire('modules')->includeModule('ProcessPageLister'); $id = ((int) $this->wire('input')->get('id')) . '_' . $this->attr('name'); $url = ProcessPageLister::addSessionBookmark($id, $bookmark); if($url) { $title = $this->_('Pages that match your selector'); $out .= " (" . $this->_('show') . ")"; } } } catch(Exception $e) { $out = $e->getMessage(); } return $out; } /** * Render the primary field "; if(!$this->showOptgroups) { ksort($outAll); foreach($outAll as $o) $out .= $o; $this->optgroupsOrder = 'modifier,adjust'; } foreach(explode(',', $this->optgroupsOrder) as $name) { $name = strtolower(trim($name)); if(empty($outSections[$name])) continue; $optgroupLabel = $outLabels[$name]; $out .= "$outSections[$name]"; } $out .= ""; return $out; } /** * Given a field, return a "|" separated string of template IDs using that field * * @param Field $field * @return string * */ protected function getTemplateIdsUsingField(Field $field) { static $fieldsToTemplates = array(); if(isset($fieldsToTemplates[$field->name])) return $fieldsToTemplates[$field->name]; $templateIDs = array(); foreach($this->wire('templates') as $template) { if($template->fieldgroup->hasField($field)) $templateIDs[] = $template->id; } $idStr = implode('|', $templateIDs); $fieldsToTemplates[$field->name] = $idStr; return $idStr; } /** * Render a single "; } $out .= ""; return $out; } /** * Render the operator or box for the value $out .= ""; } else if($type == 'autocomplete') { // render autocomplete input $placeholder = $this->_('Start typing...'); $selectedValueTitle = $this->wire('pages')->get((int) $selectedValueEntities)->get('title|name'); $out .= ""; $out .= ""; } else if($type == 'datetime' || $type == 'date') { // render date/datetime input $out .= $this->renderDateInput($inputName, $selectedValue, $type == 'datetime'); } else { // render other input that uses an whether text, number or selector $inputType = $type; $inputClass = "input-value input-value-$type"; if($type == 'number' || $type == 'selector' || $fieldName == 'id' || $subfield == 'id') { // adding this class tells InputfieldSelector.js that selector strings are allowed for this input $inputClass .= " input-value-subselect"; $inputType = 'text'; } $out .= ""; } // end the opval row by rendering a checkbox for the OR option $orLabel = $this->_('Check box to make this row OR rather than AND'); $orChecked = $orChecked ? ' checked' : ''; $out .= ""; return $out; } /** * Render a datepicker input * * @param $name * @param $value * @param bool $useTime * @return mixed * */ protected function renderDateInput($name, $value, $useTime = false) { $inputfield = $this->wire('modules')->get('InputfieldDatetime'); $inputfield->attr('name', $name); $inputfield->attr('value', $value); $inputfield->datepicker = InputfieldDatetime::datepickerFocus; $inputfield->placeholder = $this->datePlaceholder; $inputfield->dateInputFormat = $this->dateFormat; $inputfield->addClass('input-value'); if($useTime) { $inputfield->timeInputFormat = $this->timeFormat; $inputfield->placeholder .= ' ' . $this->timePlaceholder; } return $inputfield->render(); } /** * Render a subfield "; $out .= ""; // determine if there is a current value string and if it contains a selector string $selectorValue = is_null($selector) ? '' : $selector->value; if(is_array($selectorValue)) $selectorValue = reset($selectorValue); $valueHasSelectorString = strlen($selectorValue) > 0 && Selectors::stringHasSelector($selectorValue); // render all the subfield options foreach($selectorInfo['subfields'] as $name => $info) { if(isset($this->systemFields[$name])) { $label = isset($this->systemFields[$name]['label']) ? $this->systemFields[$name]['label'] : $name; } else if(!empty($info['label'])) { $label = $info['label']; } else { $f = $this->wire('fields')->get($name); $label = $f ? $f->getLabel() : $name; } $label = $this->wire('sanitizer')->entities($label); // render primary subfield selection (unless selector info says not to) if($info['input'] != 'none') { $selected = $selectedValue == $name && (!$valueHasSelectorString || empty($info['subfields'])) ? ' selected' : ''; $out .= "$label"; } // render 'id' option if there are subfields: this enables one to specify a sub-selector string // since selectors don't allow things like field.subfield.tertiaryfield if(!empty($info['subfields']) && $this->allowSubselectors) { $selected = $selectedValue == $name && $valueHasSelectorString ? ' selected' : ''; $outSub .= "$label"; } } if($outSub) { $label = $this->_('Match by ID (subselector)'); $out .= "$outSub"; } $out .= ""; return $out; } /** * Whether or not to use autocomplete * * If no, blank string is returned. * If yes, then the selector string to find pages is returned. * * @param Field $field * @param int $threshold If determined selectable quantity is <= this number, function will return blank. * @param bool $checkQuantity * @return string Selector string. Blank string means don't use autocomplete. * */ protected function useAutocomplete(Field $field, $threshold = 100, $checkQuantity = true) { if(!$field->type instanceof FieldtypePage) return ''; $selector = ''; // determine autocomplete state based on field settings and quantity of pages involved if($field->findPagesSelector) { // user-specified selector determines which pages match $selector = trim($field->findPagesSelector, ', '); if($field->parent_id) $selector .= ",has_parent=" . (int) $field->parent_id; } else { if($field->parent_id) $selector = "parent_id=" . (int) $field->parent_id; } if($field->template_id) { $selector .= ",templates_id="; if(is_array($field->template_id)) { if(count($field->template_id)) $selector .= implode('|', $field->template_id); } else { $selector .= (int) $field->template_id; } } if(empty($selector)) { // if it's using a runtime code to determine, then we can't use autocomplete if($field->findPagesCode) return ''; // otherwise just populate a selector that can match anything $selector = "id>0"; } if(!$checkQuantity) return $selector; $quantity = $this->wire('pages')->count($selector); return $quantity > $threshold ? $selector : ''; } /** * Render autocomplete results and return JSON string * * @param string $fieldName * @param string $q Query string * @return string JSON * */ protected function renderAutocompleteJSON($fieldName, $q) { header("Content-Type: application/json"); // format for our returned JSON $data = array( 'field' => "$fieldName", 'status' => 0, // 0=error, 1=success 'selector' => '', 'items' => array() ); if(strpos($fieldName, '.') !== false) { list($ignored, $fieldName) = explode('.', $fieldName); } $field = $this->wire('fields')->get($fieldName); if(!$field) { $data['error'] = 'Field does not exist'; return json_encode($data); } $selector = $this->useAutocomplete($field, 100, false); if(!$selector) { $data['error'] = "Field '$field->name' does not require autocomplete"; return json_encode($data); } $searchFields = $field->searchFields; // used by InputfieldPageAutocomplete $labelFieldName = $field->labelFieldName; $labelField = $this->wire('fields')->get($labelFieldName); $template_id = (int) (is_array($field->template_id) ? reset($field->template_id) : $field->template_id); $template = $template_id ? $this->wire('templates')->get($template_id) : null; if($searchFields) { $searchFields = str_replace(' ', '|', trim($searchFields)); } else if($labelField && $labelField->type instanceof FieldtypeText) { $searchFields = $labelFieldName; } else if($template && $template->fieldgroup->hasField('title')) { $searchFields = 'title'; $labelFieldName = 'title'; } else { $searchFields = 'name'; $labelFieldName = 'name'; } $selector .= ", $searchFields%=" . $this->wire('sanitizer')->selectorValue($q); $selector .= ", limit=50, include=hidden"; foreach($this->wire('pages')->find($selector) as $item) { $data['items'][] = array( 'value' => $item->id, 'label' => $item->get("$labelFieldName|name") ); } $data['status'] = 1; $data['selector'] = $selector; return json_encode($data); } /** * Render a selector row
  • * * @param string $select Rendered output for the if applicable * @param string $opval Rendered output for the operator * @param string $class Optional class for the row * @return string * */ public function renderRow($select, $subfield, $opval, $class = '') { $out = "
  • $select $subfield $opval  
  • "; return $out; } /** * Set an attribute to this Inputfield, overridden from Inputfield class * * @param array|string $key * @param int|string $value * @return $this * */ public function setAttribute($key, $value) { if($key == 'value') { if($this->initValue && strpos($value, $this->initValue) === 0) { // remove initValue from value so that inputs aren't drawn for it $value = trim(substr($value, strlen($this->initValue)+1), ', '); } } return parent::setAttribute($key, $value); } /** * Primary Inputfield render method * * @return string * */ public function ___render() { // tell jQuery UI we want it to load the modal component which makes a.modal open modal windows $this->wire('modules')->get('JqueryUI')->use('modal'); if(self::debug) $this->counter = true; // force load the CSS/JS files used for dates, since we don't know if they will be needed or not $this->renderDateInput('tmp', '', true); // build the structures and default values $this->setup(); $this->wrapClass .= ' InputfieldSelector_' . ($this->showFieldLabels ? 'labels' : 'names'); // convert the value attribute to a Selectors object try { $value = trim($this->attr('value')); $this->selectors = new Selectors($value); } catch(Exception $e) { $this->error($e->getMessage()); } // set a session variable so that ajax request know there has been a valid request $this->sessionSet('valid', true); $this->sessionSet('initValue', $this->initValue); // all other session variables tha tneed to be remembered foreach($this->sessionVarNames as $key) { $this->sessionSet($key, $this->$key); } // determine if there are any initValue templates in play, so that we can pre-limit what fields are available $templates = array(); $renderSelectOptions = array(); if($this->initValue) foreach(new Selectors($this->initValue) as $selector) { if($selector->field == 'template') { $templateValue = $selector->value; if(!is_array($templateValue)) $templateValue = array($templateValue); foreach($templateValue as $t) { $template = $this->wire('templates')->get($t); if($template) $templates[] = $template->name; } } if(count($templates)) $renderSelectOptions['templates'] = $templates; } $select = $this->renderSelectField($renderSelectOptions); $previewClass = $this->preview ? '' : ' selector-preview-disabled'; $counterClass = $this->counter ? '' : ' selector-counter-disabled'; // render the template row to start: this is duplicated with JS when a field added $rows = $this->renderRow($select, '', '', 'selector-template-row'); // render all the rows for existing selector values already in this Inputfield's value foreach($this->selectors as $selector) { $rowClass = ''; $orChecked = false; $fields = $selector->field; $quote = $selector->quote; if(!is_array($fields)) $fields = array($fields); // render a row for each field in the $selector (usually 1) foreach($fields as $fieldNum => $field) { $field1 = $field; $field2 = ''; $group = is_null($selector->group) ? '' : '@'; $dot = strpos($field, '.'); if($dot && !isset($this->systemFields[$field])) { $field1 = substr($field, 0, $dot); $field2 = substr($field, $dot+1); } $select = $this->renderSelectField(array(), $group . $field); $select2 = $dot ? $this->renderSelectSubfield($field1, $field2, $selector) : ''; if($select2) $rowClass .= " has-subfield"; if($fieldNum > 0) $rowClass .= " has-or-field"; $values = $selector->value; if(!is_array($values)) $values = array($values); // render a row for each value in the selector (usually 1) foreach($values as $valueNum => $value) { if($valueNum > 0) $rowClass .= " has-or-value"; if($fieldNum > 0 || $valueNum > 0) $orChecked = true; if(!strlen($value) && $quote) $value = "$quote{$value}$quote"; $opval = $this->renderOpval(($field2 ? $field2 : $field1), '', $selector->operator, $value, $orChecked); $rows .= $this->renderRow($select, $select2, $opval, $rowClass); } } } $notes = $this->_('Each selector row above says: field must match value. These are called AND conditions. In cases where the same field or value appears in more than one row, an OR condition is possible. The presence of a checkbox at the end of the row indicates this. Check this box to make the row an OR condition rather than an AND condition.'); // Description of OR checkbox // attributes for our hidden input, populated by javascript as filters are added/changed/removed $attr = array( 'type' => 'hidden', 'id' => $this->attr('id'), 'name' => $this->attr('name'), 'value' => $this->attr('value'), 'class' => 'selector-value', 'data-template-ids' => implode(',', $this->getTemplatesFromInitValue($this->initValue)), ); if($this->allowBlankValues) $attr['class'] .= ' allow-blank'; $attrStr = $this->getAttributesString($attr); $attr['value'] = $this->wire('sanitizer')->entities($attr['value']); $initValue = $this->wire('sanitizer')->entities($this->initValue); // starting output $out = "
      $rows
    $this->addLabel

    $attr[value]

    $notes

    "; return $out; } /** * Sanitize a selector string and return it * * @param $selectorString * @return string * */ public function sanitizeSelectorString($selectorString) { $initSelectors = new Selectors($this->initValue); $userSelectors = new Selectors($selectorString); foreach($userSelectors as $s) { if($s->quote == '[' && !$this->allowSubselectors) { $this->error("Subselectors are disabled"); $userSelectors->remove($s); $userSelectors->add(new SelectorLessThan('id', 0)); // forced non match } } $selector = (string) $initSelectors . ", "; $selector .= (string) $userSelectors; $selector = trim($selector, ", "); return $selector; } /** * Returns array of template IDs that correspond with any templates specified in the initValue * * @param string $initValue * @return array of template IDs * */ protected function getTemplatesFromInitValue($initValue) { // determine if a template is enforced and populate allowedTemplates $templates = array(); if(!$initValue || strpos($initValue, 'template=') === false) return array(); foreach(new Selectors($initValue) as $selector) { if($selector->field == 'template') { $value = is_array($selector->value) ? $selector->value : array($selector->value); foreach($value as $name) { $t = $this->wire('templates')->get($name); if($t) $templates[] = $t->id; } } } return $templates; } /** * Process input submitted to this Inputfield * * @param WireInputData $input * @return $this * */ public function ___processInput(WireInputData $input) { parent::___processInput($input); $value = $this->attr('value'); $this->attr('value', $this->sanitizeSelectorString($value)); return $this; } /** * Module settings: provide a sandbox area for playing with the Inputfield * * @param array $data * @return InputfieldsWrapper|InputfieldWrapper * */ public static function getModuleConfigInputfields(array $data) { $form = new InputfieldWrapper(); $f = wire('modules')->get('InputfieldSelector'); $f->name = 'test'; $f->label = 'Selector Sandbox'; $f->description = 'This is here just in case you want to test out the functionality of this Inputfield.'; $form->add($f); return $form; } }