'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