From ffde966920b2f50ab8b575e7ba90d31a6aef94ad Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 16 Dec 2016 10:45:32 -0500 Subject: [PATCH] Upgrades to configuration of FieldtypePage and InputfieldPage (as seen in field editor), Upgrades to Page and WireArray __invoke() support, Rewrite Selectors::stringHasSelector() method to make it more accurate, Upgrades to Sanitizer::minArray(), add support for load-time filtering to FieldtypePage (Page fields), plus some other small tweaks. --- wire/core/Functions.php | 1 + wire/core/Interfaces.php | 7 + wire/core/Page.php | 19 +- wire/core/Sanitizer.php | 49 ++- wire/core/Selectors.php | 75 +++-- wire/core/WireArray.php | 4 +- wire/modules/Fieldtype/FieldtypePage.module | 190 ++++++++++-- .../InputfieldAsmSelect.module | 2 +- .../InputfieldFile/InputfieldFile.module | 6 +- .../InputfieldImage/InputfieldImage.module | 2 +- .../InputfieldPage/InputfieldPage.module | 283 +++++++++++++----- .../InputfieldPageAutocomplete.module | 27 +- .../InputfieldPageListSelect.module | 4 + .../InputfieldPageListSelectMultiple.module | 3 +- 14 files changed, 505 insertions(+), 167 deletions(-) diff --git a/wire/core/Functions.php b/wire/core/Functions.php index f96dcb3f..2738b30e 100644 --- a/wire/core/Functions.php +++ b/wire/core/Functions.php @@ -77,6 +77,7 @@ endif; * - Specify false to exclude all empty values (this is the default if not specified). * - Specify true to allow all empty values to be retained. * - Specify an array of keys (from data) that should be retained if you want some retained and not others. + * - Specify array of literal empty value types to retain, i.e. [ 0, '0', array(), false, null ]. * - Specify the digit 0 to retain values that are 0, but not other types of empty values. * @param bool $beautify Beautify the encoded data when possible for better human readability? (requires PHP 5.4+) * @return string String of JSON data diff --git a/wire/core/Interfaces.php b/wire/core/Interfaces.php index e4c0a6b7..a2729fca 100644 --- a/wire/core/Interfaces.php +++ b/wire/core/Interfaces.php @@ -548,3 +548,10 @@ interface WireProfilerInterface { */ interface InputfieldHasArrayValue { } +/** + * Inputfield that has a sortable value (usually in addition to InputfieldHasArrayValue) + * + */ +interface InputfieldHasSortableValue { } + + diff --git a/wire/core/Page.php b/wire/core/Page.php index 5384f172..6141e9c1 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -1250,7 +1250,8 @@ class Page extends WireData implements \Countable, WireMatchable { * Get the value for a non-native page field, and call upon Fieldtype to join it if not autojoined * * @param string $key Name of field to get - * @param string $selector Optional selector to filter load by + * @param string $selector Optional selector to filter load by... + * ...or, if not in selector format, it becomes an __invoke() argument for object values . * @return null|mixed * */ @@ -1260,6 +1261,7 @@ class Page extends WireData implements \Countable, WireMatchable { $field = $this->getField($key); $value = parent::get($key); if(!$field) return $value; // likely a runtime field, not part of our data + $invokeArgument = ''; if($field->useRoles && $this->outputFormatting) { // API access may be limited when output formatting is ON @@ -1285,6 +1287,13 @@ class Page extends WireData implements \Countable, WireMatchable { $track = $this->trackChanges(); $this->setTrackChanges(false); if(!$field->type) return null; + + if($selector && !Selectors::stringHasSelector($selector)) { + // if selector argument provdied, but isn't valid, we assume it + // to instead be an argument for the value's __invoke() method + $invokeArgument = $selector; + $selector = ''; + } if($selector) { $value = $field->type->loadPageFieldFilter($this, $field, $selector); @@ -1311,7 +1320,13 @@ class Page extends WireData implements \Countable, WireMatchable { if(is_object($value) && $value instanceof Wire) $value->resetTrackChanges(true); if($track) $this->setTrackChanges(true); - return $this->formatFieldValue($field, $value); + $value = $this->formatFieldValue($field, $value); + + if($invokeArgument && is_object($value) && method_exists($value, '__invoke')) { + $value = $value->__invoke($invokeArgument); + } + + return $value; } /** diff --git a/wire/core/Sanitizer.php b/wire/core/Sanitizer.php index 4280e904..a7d87821 100644 --- a/wire/core/Sanitizer.php +++ b/wire/core/Sanitizer.php @@ -2180,10 +2180,11 @@ class Sanitizer extends Wire { * #pw-group-arrays * * @param array $data Array to reduce - * @param bool|array $allowEmpty Should empty values be allowed in the encoded data? + * @param bool|array $allowEmpty Should empty values be allowed in the encoded data? Specify any of the following: * - `false` (bool): to exclude all empty values (this is the default if not specified). * - `true` (bool): to allow all empty values to be retained (thus no point in calling this function). * - Specify array of keys (from data) that should be retained if you want some retained and not others. + * - Specify array of literal empty value types to retain, i.e. [ 0, '0', array(), false, null ] * - Specify the digit `0` to retain values that are 0, but not other types of empty values. * @param bool $convert Perform type conversions where appropriate? i.e. convert digit-only string to integer (default=false). * @return array @@ -2194,6 +2195,14 @@ class Sanitizer extends Wire { if(!is_array($data)) { $data = $this->___array($data, null); } + + $allowEmptyTypes = array(); + if(is_array($allowEmpty)) { + foreach($allowEmpty as $emptyType) { + if(!empty($emptyType)) continue; + $allowEmptyTypes[] = $emptyType; + } + } foreach($data as $key => $value) { @@ -2210,21 +2219,33 @@ class Sanitizer extends Wire { $data[$key] = $value; - // skip empty values whether blank, 0, empty array, etc. - if(empty($value)) { - - if($allowEmpty === 0 && $value === 0) { - // keep it because $allowEmpty === 0 means to keep 0 values only - - } else if(is_array($allowEmpty) && !in_array($key, $allowEmpty)) { - // remove it because it's not specifically allowed in allowEmpty - unset($data[$key]); - - } else if(!$allowEmpty) { - // remove the empty value - unset($data[$key]); + // if value is not empty, no need to continue further checks + if(!empty($value)) continue; + + $typeMatched = false; + if(count($allowEmptyTypes)) { + foreach($allowEmptyTypes as $emptyType) { + if($value === $emptyType) { + $typeMatched = true; + break; + } } } + + if($typeMatched) { + // keep it because type matched an allowEmptyTypes + + } else if($allowEmpty === 0 && $value === 0) { + // keep it because $allowEmpty === 0 means to keep 0 values only + + } else if(is_array($allowEmpty) && !in_array($key, $allowEmpty)) { + // remove it because it's not specifically allowed in allowEmpty + unset($data[$key]); + + } else if(!$allowEmpty) { + // remove the empty value + unset($data[$key]); + } } return $data; diff --git a/wire/core/Selectors.php b/wire/core/Selectors.php index e19af959..a9061260 100644 --- a/wire/core/Selectors.php +++ b/wire/core/Selectors.php @@ -292,39 +292,68 @@ class Selectors extends WireArray { } /** - * Does the given string start with a selector? + * Is the give string a Selector string? * - * Meaning string starts with [field][operator] like "field=" - * * #pw-group-static-helpers * - * @param string $str + * @param string $str String to check for selector(s) * @return bool * */ static public function stringHasSelector($str) { + if(!self::stringHasOperator($str)) return false; + $has = false; - - if(!self::stringHasOperator($str)) { - - // default: has=false - - } else if(preg_match('/^!?([-._a-zA-Z0-9|]+)([' . implode('', self::getOperatorChars()) . ']+)/', $str, $matches)) { - - $field = $matches[1]; - $operator = $matches[2]; - - if(in_array($field[0], array('-', '.', '|'))) { - // fields can't start with a dash or a period or a pipe - $has = false; - } else if(!isset(self::$selectorTypes[$operator])) { - // if it's not an operator we recognize then abort - $has = false; - } else { - // if we made it here, then we've found a selector - $has = true; + $alphabet = 'abcdefghijklmnopqrstuvwxyz'; + + // replace characters that are allowed but aren't useful here + $str = str_replace(array('!', '(', ')', '@', '.', '|', '_'), '', trim(strtolower($str))); + + // first character must match alphabet + if(strpos($alphabet, substr($str, 0, 1)) === false) return false; + + $operatorChars = implode('', self::getOperatorChars()); + + if(strpos($str, ',')) { + // split the string into all key=value components and check each individually + $inQuote = ''; + $cLast = ''; + // replace comments in quoted values so that they aren't considered selector boundaries + for($n = 0; $n < strlen($str); $n++) { + $c = $str[$n]; + if($c === ',') { + // commas in quoted values are replaced with semicolons + if($inQuote) $str[$n] = ';'; + } else if(($c === '"' || $c === "'") && $cLast != "\\") { + if($inQuote && $inQuote === $c) { + $inQuote = ''; // end quote + } else if(!$inQuote) { + $inQuote = $c; // start quote + } + } + $cLast = $c; } + $parts = explode(',', $str); + } else { + // outside of verbose mode, only the first apparent selector is checked + $parts = array($str); + } + + // check each key=value component + foreach($parts as $part) { + $has = preg_match('/^[a-z][a-z0-9]*([' . $operatorChars . ']+)(.*)$/', trim($part), $matches); + if($has) { + $operator = $matches[1]; + $value = $matches[2]; + if(!isset(self::$selectorTypes[$operator])) { + $has = false; + } else if(self::stringHasOperator($value) && $value[0] != '"' && $value[0] != "'") { + // operators not allowed in values unless quoted + $has = false; + } + } + if(!$has) break; } return $has; diff --git a/wire/core/WireArray.php b/wire/core/WireArray.php index 9d455b30..7bc17820 100644 --- a/wire/core/WireArray.php +++ b/wire/core/WireArray.php @@ -537,7 +537,7 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count if(isset($this->data[$key])) return $this->data[$key]; // check if key contains a selector - if(Selectors::stringHasOperator($key)) { + if(Selectors::stringHasSelector($key)) { $item = $this->findOne($key); if($item === false) $item = null; return $item; @@ -2101,6 +2101,8 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count // if keys are not numeric, we delegete numbers to eq(n) return $this->eq((int) $key); } + } else if(is_callable($key) || (is_string($key) && strpos($key, '{') !== false && strpos($key, '}'))) { + return $this->each($key); } return $this->get($key); } diff --git a/wire/modules/Fieldtype/FieldtypePage.module b/wire/modules/Fieldtype/FieldtypePage.module index f7513a20..1f16cc81 100644 --- a/wire/modules/Fieldtype/FieldtypePage.module +++ b/wire/modules/Fieldtype/FieldtypePage.module @@ -19,7 +19,7 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule public static function getModuleInfo() { return array( 'title' => 'Page Reference', - 'version' => 103, + 'version' => 104, 'summary' => 'Field that stores one or more references to ProcessWire pages', 'permanent' => true, ); @@ -180,6 +180,14 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule if(!is_array($value)) $value = array($value); // $value = $this->validatePageIDs($page, $value); + if(isset($value['_filters'])) { + $filters = $value['_filters']; + unset($value['_filters']); + if(!count($filters)) $filters = null; + } else { + $filters = null; + } + if($derefAsPage > 0) { // we're going to return a single page, NullPage or false $pg = false; @@ -197,6 +205,11 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule } else { // we're going to return a PageArray if(!count($value)) return $this->getBlankValue($page, $field); + if($filters) { + $filters->add(new SelectorEqual('id', $value)); + $finder = $this->pages->getPageFinder(); + $value = $finder->findIDs($filters); + } $pageArray = $this->wire('pages')->getById($value, $template); foreach($pageArray as $pg) { // remove any pages that have an unpublished status @@ -495,6 +508,7 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule } else if(is_string($value) || is_int($value)) { $value = $this->sanitizeValueString($page, $field, $value); if($value instanceof PageArray) $value = $value->first(); + /** @var Page $value */ if(is_object($value) && $value->get('_FieldtypePage_remove') === $value->id) { $value->__unset('_FieldtypePage_remove'); $value = null; // remove item @@ -608,6 +622,23 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule return $pageArray; } + /** + * Apply a where condition to a load query (used by getLoadQuery method) + * + * @param Field $field + * @param DatabaseQuerySelect $query + * @param string $col The column name + * @param string $operator The comparison operator + * @param mixed $value The value to find + * @return DatabaseQuery $query + * @throws WireException if given invalid or unrecognized arguments + * + */ + protected function getLoadQueryWhere(Field $field, DatabaseQuerySelect $query, $col, $operator, $value) { + // cancel the default behavior since Page fields filter from the wakeupValue method instead + return $query; + } + /** * Update a DatabaseQuerySelect object to match a Page * @@ -791,6 +822,7 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule $pageFinderOptions = array('getTotal' => false); $value = $this->wire('sanitizer')->selectorValue($value); $findPagesSelector = $field->get('findPagesSelector'); + if(empty($findPagesSelector)) $findPagesSelector = $field->get('findPagesSelect'); $parent_id = $field->get('parent_id'); $template_id = $field->get('template_ids'); if(empty($template_id)) $template_id = $field->get('template_id'); @@ -974,44 +1006,144 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule public function ___getConfigInputfields(Field $field) { $inputfields = parent::___getConfigInputfields($field); - + + $labels = array( + 'PageArray' => $this->_('Multiple pages (PageArray)'), + 'PageOrFalse' => $this->_('Single page (Page) or boolean false when none selected'), + 'PageOrNullPage' => $this->_('Single page (Page) or empty page (NullPage) when none selected'), + ); + + $_labels = $labels; + $url = "https://processwire.com/api/ref"; + $aPageArray = "[PageArray]($url/page-array/)"; + $aPage = "[Page]($url/page/)"; + $aNullPage = "[NullPage]($url/null-page/)"; + + foreach($labels as $key => $value) { + $labels[$key] = str_replace( + array('(PageArray)', '(Page)', '(NullPage)'), + array("$aPageArray", "$aPage", "$aNullPage"), + $value + ); + } + /** @var InputfieldRadios $select */ $select = $this->modules->get("InputfieldRadios"); $select->attr('name', 'derefAsPage'); - $select->label = $this->_('Dereference in API as'); + $select->label = $this->_('Page field value type'); $select->description = $this->_('If your field will contain multiple pages, then you should select the first option (PageArray). If your field only needs to contain a single page, then select one of the single Page options (if you are not sure which, select the last option).'); // Long description for: dereference in API - $select->addOption(FieldtypePage::derefAsPageArray, $this->_('Multiple pages (PageArray)')); - $select->addOption(FieldtypePage::derefAsPageOrFalse, $this->_('Single page (Page) or boolean false when none selected')); - $select->addOption(FieldtypePage::derefAsPageOrNullPage, $this->_('Single page (Page) or empty page (NullPage) when none selected')); - + $select->addOption(FieldtypePage::derefAsPageArray, $labels['PageArray']); + $select->addOption(FieldtypePage::derefAsPageOrFalse, $labels['PageOrFalse']); + $select->addOption(FieldtypePage::derefAsPageOrNullPage, $labels['PageOrNullPage']); $select->attr('value', (int) $field->get('derefAsPage')); + $select->icon = 'tasks'; $inputfields->append($select); + + /** @var InputfieldMarkup $f */ + $exampleFieldset = $this->wire('modules')->get('InputfieldFieldset'); + $exampleFieldset->attr('name', '_examplesFieldset'); + $exampleFieldset->label = $this->_('API usage examples'); + $exampleFieldset->icon = 'code'; + $inputfields->add($exampleFieldset); + + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageArray'); + $f->label = $_labels['PageArray']; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageArray; + $f->icon = 'scissors'; + $f->value = + "
" . $this->wire('sanitizer')->entities(
+			"foreach(\$page->{$field->name} as \$item) {" .
+			"\n  echo \"
  • \$item->title
  • \";". + "\n}" + ) . "
    "; + $exampleFieldset->add($f); + + $alternateLabel = $this->_('Same as above with alternate syntax'); + + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageArray2'); + $f->label = $alternateLabel; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageArray; + $f->icon = 'scissors'; + $f->value = + "
    " . $this->wire('sanitizer')->entities(
    +			"echo \$page->{$field->name}->each(\n  \"
  • {title}
  • \"\n);" + ) . "
    "; + $f->notes = sprintf($this->_('More about the %s method.'), "[each()]($url/page-array/each/)"); + $exampleFieldset->add($f); + + /** @var InputfieldMarkup $f */ + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageOrFalse'); + $f->label = $_labels['PageOrFalse']; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageOrFalse; + $f->icon = 'scissors'; + $f->value = + "
    " . $this->wire('sanitizer')->entities(
    +			"if(\$page->{$field->name}) {" .
    +			"\n  echo \"{\$page->{$field->name}->title}\";" .
    +			"\n}" 
    +			) . "
    "; + $exampleFieldset->add($f); + + /** @var InputfieldMarkup $f */ + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageOrFalse2'); + $f->label = $alternateLabel; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageOrFalse; + $f->icon = 'scissors'; + $f->value = + "
    " . $this->wire('sanitizer')->entities(
    +			"if(\$page->{$field->name}) {" .
    +			"\n  echo \$page->{$field->name}(\"{title}\");" .
    +			"\n}"
    +			) . "
    "; + $exampleFieldset->add($f); + + /** @var InputfieldMarkup $f */ + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageOrNullPage'); + $f->label = $_labels['PageOrNullPage']; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageOrNullPage; + $f->icon = 'scissors'; + $f->value = + "
    " . $this->wire('sanitizer')->entities(
    +			"if(\$page->{$field->name}->id) {" .
    +			"\n  echo \"{\$page->{$field->name}->title}\";" .
    +			"\n}" 
    +			) . "
    "; + $exampleFieldset->add($f); + + /** @var InputfieldMarkup $f */ + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->attr('name', '_examplePageOrNullPage2'); + $f->label = $alternateLabel; + $f->showIf = 'derefAsPage=' . FieldtypePage::derefAsPageOrNullPage; + $f->icon = 'scissors'; + $f->value = + "
    " . $this->wire('sanitizer')->entities(
    +				"if(\$page->{$field->name}->id) {" .
    +				"\n  echo \$page->{$field->name}(\"{title}\");" .
    +				"\n}"
    +			) . "
    "; + $exampleFieldset->add($f); + + /** @var InputfieldCheckbox $checkbox */ + $value = (int) $field->get('allowUnpub'); + $checkbox = $this->modules->get('InputfieldCheckbox'); + $checkbox->attr('name', 'allowUnpub'); + $checkbox->label = $this->_('Allow unpublished pages?'); + $checkbox->description = $this->_('When checked, unpublished pages are allowed in the field value. Unpublished pages will not appear on the front-end, except to those with edit access.'); // Description for allowUnpub option + $checkbox->attr('value', 1); + $checkbox->attr('checked', $value ? 'checked' : ''); + $checkbox->icon = 'eye-slash'; + if(!$value) $checkbox->collapsed = Inputfield::collapsedYes; + $inputfields->append($checkbox); return $inputfields; } - /** - * Return advanced configuration fields definable for each FieldtypePage - * - * @param Field $field - * @return InputfieldWrapper - * - */ - public function ___getConfigAdvancedInputfields(Field $field) { - $inputfields = parent::___getConfigAdvancedInputfields($field); - - /** @var InputfieldCheckbox $checkbox */ - $checkbox = $this->modules->get('InputfieldCheckbox'); - $checkbox->attr('name', 'allowUnpub'); - $checkbox->label = $this->_('Allow unpublished pages?'); - $checkbox->description = $this->_('When checked, unpublished pages are allowed in the field value. Unpublished pages will not appear on the front-end, except to those with edit access.'); // Description for allowUnpub option - $checkbox->attr('value', 1); - $checkbox->attr('checked', $field->get('allowUnpub') ? 'checked' : ''); - $inputfields->prepend($checkbox); - - return $inputfields; - } - /** * Export configuration values for external consumption * diff --git a/wire/modules/Inputfield/InputfieldAsmSelect/InputfieldAsmSelect.module b/wire/modules/Inputfield/InputfieldAsmSelect/InputfieldAsmSelect.module index 12e2e4ac..4bfb3d57 100644 --- a/wire/modules/Inputfield/InputfieldAsmSelect/InputfieldAsmSelect.module +++ b/wire/modules/Inputfield/InputfieldAsmSelect/InputfieldAsmSelect.module @@ -6,7 +6,7 @@ * @property int|bool $usePageEdit * */ -class InputfieldAsmSelect extends InputfieldSelectMultiple implements InputfieldHasArrayValue { +class InputfieldAsmSelect extends InputfieldSelectMultiple implements InputfieldHasArrayValue, InputfieldHasSortableValue { protected $asmOptions = array(); diff --git a/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module b/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module index dfd4f7a9..52677666 100644 --- a/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module +++ b/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module @@ -2,7 +2,7 @@ /** * An Inputfield for handling file uploads - * + * * @property string $extensions Allowed file extensions, space separated * @property int $maxFiles Maximum number of files allowed * @property int $maxFilesize Maximum file size @@ -19,7 +19,7 @@ * @property bool|int $noCollapseItem Set to true to disable collapsed items (like for LanguageTranslator tool or other things that add tools to files) * @property bool|int $noShortName Set to true to disable shortened filenames in output * @property bool|int $noCustomButton Set to true to disable use of the styled - * + * * @method string renderItem($pagefile, $id, $n) * @method string renderList($value) * @method string renderUpload($value) @@ -30,7 +30,7 @@ * @method bool processInputFile(WireInputData $input, Pagefile $pagefile, $n) * */ -class InputfieldFile extends Inputfield implements InputfieldItemList { +class InputfieldFile extends Inputfield implements InputfieldItemList, InputfieldHasSortableValue { public static function getModuleInfo() { return array( diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module index 1ea01b73..1eb3ca10 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module @@ -41,7 +41,7 @@ * */ -class InputfieldImage extends InputfieldFile implements InputfieldItemList { +class InputfieldImage extends InputfieldFile implements InputfieldItemList, InputfieldHasSortableValue { public static function getModuleInfo() { return array( diff --git a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module index 1cde9f0c..4ac34047 100644 --- a/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module +++ b/wire/modules/Inputfield/InputfieldPage/InputfieldPage.module @@ -18,6 +18,7 @@ * @property string $labelFieldFormat Formatting string for $page->getMarkup() as alternative to $labelFieldName * @property string $findPagesCode * @property string $findPagesSelector + * @property string $findPagesSelect Same as findPageSelector, but configured interactively with InputfieldSelector . * @property int|bool $addable * @property int|bool $allowUnpub * @property int $derefAsPage @@ -34,7 +35,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'Page', - 'version' => 106, + 'version' => 107, 'summary' => 'Select one or more pages', 'permanent' => true, ); @@ -76,6 +77,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { 'labelFieldName' => '', 'labelFieldFormat' => '', 'findPagesCode' => '', + 'findPagesSelect' => '', 'findPagesSelector' => '', 'derefAsPage' => 0, 'addable' => 0, @@ -228,6 +230,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $valid = true; $findPagesSelector = $field->get('findPagesSelector'); + if(empty($findPagesSelector)) $findPagesSelector = $field->get('findPagesSelect'); $parent_id = $field->get('parent_id'); $template_id = $field->get('template_id'); $template_ids = $field->get('template_ids'); @@ -252,8 +255,10 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { // if($field->findPagesCode) { } // we don't currently validate these if($parent_id && $parent_id != $page->parent_id) { + $inputfieldClass = ltrim($field->get('inputfield'), '_'); + if(empty($inputfieldClass)) $inputfieldClass = 'InputfieldSelect'; if(version_compare(PHP_VERSION, '5.3.8') >= 0) { - $interfaces = wireClassImplements($field->get('inputfield')); + $interfaces = wireClassImplements($inputfieldClass); if(in_array('InputfieldPageListSelection', $interfaces)) { // parent_id represents a root parent $rootParent = $page->wire('pages')->get($parent_id); @@ -268,7 +273,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { } else { // PHP version prior to 5.3.8 // @deprecated - $reflector = new \ReflectionClass($field->get('inputfield')); + $reflector = new \ReflectionClass($inputfieldClass); $valid = $reflector->implementsInterface('InputfieldPageListSelection'); } } @@ -298,6 +303,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { * * @param Page $page The page being edited * @return PageArray (hopefully) + * @deprecated Use hook to InputfieldPage::getSelectablePages() instead * */ protected function ___findPagesCode(Page $page) { @@ -313,9 +319,10 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { } public function getSetting($key) { + if($key === 'template_ids') return $this->getTemplateIDs(); $value = parent::getSetting($key); - if(empty($value) && $key == 'template_id') { - $templateIDs = parent::getSetting('template_ids'); + if($key === 'template_id' && empty($value)) { + $templateIDs = $this->getTemplateIDs(); if(!empty($templateIDs)) $value = reset($templateIDs); } return $value; @@ -334,6 +341,8 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $statusUnder = $this->allowUnpub ? Page::statusTrash : Page::statusUnpublished; $children = null; $templateIDs = $this->getTemplateIDs(true); + $findPagesSelector = $this->getSetting('findPagesSelector'); + if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); if($this->configMode) { $children = $this->wire('pages')->newPageArray(); @@ -347,10 +356,10 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $children = $this->wire('pages')->newPageArray(); } - } else if($this->findPagesSelector) { + } else if($findPagesSelector) { // a find() selector $instance = $this->processInputMode ? $this : null; - $selector = self::getFindPagesSelector($page, $this->findPagesSelector, $instance); + $selector = self::getFindPagesSelector($page, $findPagesSelector, $instance); $children = $this->pages->find($selector); } else if($this->findPagesCode) { @@ -390,13 +399,13 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { * */ public function getTemplateIDs($getString = false) { - $templateIDs = $this->getSetting('template_ids'); - $templateID = $this->getSetting('template_id'); + $templateIDs = parent::getSetting('template_ids'); + $templateID = parent::getSetting('template_id'); + if(empty($templateID)) return $getString ? '' : array(); if(empty($templateIDs)) { - if(empty($templateID)) return $getString ? '' : array(); $templateIDs = array($templateID); - } else { - if(!in_array($templateID, $templateIDs)) array_unshift($templateIDs, $templateID); + } else if($templateID && !in_array($templateID, $templateIDs)) { + array_unshift($templateIDs, $templateID); } return $getString ? implode('|', $templateIDs) : $templateIDs; } @@ -471,6 +480,16 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { return $label; } + /** + * Get the selected Inputfield class for input (adjuted version of $this->inputfield) + * + * @return string + * + */ + protected function getInputfieldClass() { + return ltrim($this->getSetting('inputfield'), '_'); + } + /** * Get delegate Inputfield for page selection * @@ -480,9 +499,11 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { */ public function getInputfield() { - if($this->inputfieldWidget && ((string) $this->inputfieldWidget) == $this->inputfield) return $this->inputfieldWidget; + if($this->inputfieldWidget && ((string) $this->inputfieldWidget) == $this->getInputfieldClass()) { + return $this->inputfieldWidget; + } - $inputfield = $this->wire('modules')->get($this->inputfield); + $inputfield = $this->wire('modules')->get($this->getInputfieldClass()); if(!$inputfield) return null; if($this->derefAsPage) $inputfield->set('maxSelectedItems', 1); @@ -532,6 +553,8 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $labelFieldName = $this->getSetting('labelFieldName'); $labelFieldFormat = $this->getSetting('labelFieldFormat'); + if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); + if($parent_id) { $inputfield->parent_id = $parent_id; } else if($findPagesCode) { @@ -626,8 +649,9 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $out = "
    "; $out .= $inputfield->render(); $out .= $this->renderAddable(); - + $findPagesSelector = $this->getSetting('findPagesSelector'); + if(empty($findPagesSelector)) $findPagesSelector = $this->getSetting('findPagesSelect'); $labelFieldFormat = $this->getSetting('labelFieldFormat'); $labelFieldName = $this->getSetting('labelFieldName'); @@ -687,7 +711,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { if(!$parent->addable($test)) return ''; if(!$test->publishable()) return ''; - $inputfield = $this->wire('modules')->get($this->getSetting('inputfield')); + $inputfield = $this->wire('modules')->get($this->getInputfieldClass()); if(!$inputfield) return ''; $key = "_{$this->name}_add_items"; @@ -937,40 +961,51 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $exampleLabel = $this->_('Example:') . ' '; $defaultLabel = ' ' . $this->_('(default)'); - $inputfields = parent::___getConfigInputfields(); + $inputfields = new InputfieldWrapper(); + $this->wire($inputfields); $fieldset = $this->wire('modules')->get('InputfieldFieldset'); - $fieldset->label = $this->_('Selectable Pages'); + $fieldset->label = $this->_('Selectable pages'); $fieldset->attr('name', '_selectable_pages'); - + $fieldset->description = $this->_('Use at least one of the options below to determine which pages will be selectable with this field.'); + $fieldset->icon = 'files-o'; + $selectablePagesFieldset = $fieldset; + /** @var InputfieldPageListSelect $field */ $field = $this->modules->get('InputfieldPageListSelect'); $field->setAttribute('name', 'parent_id'); - $field->label = $this->_('Parent of selectable page(s)'); + $field->label = $this->_('Parent'); $field->attr('value', (int) $this->parent_id); $field->description = $this->_('Select the parent of the pages that are selectable.'); $field->required = false; + $field->icon = 'folder-open-o'; + $field->collapsed = Inputfield::collapsedBlank; $fieldset->append($field); /** @var InputfieldSelect $field */ $field = $this->modules->get('InputfieldSelect'); $field->setAttribute('name', 'template_id'); - $field->label = $this->_('Template of selectable page(s)'); - $field->description = $this->_('Select the template of the pages that are selectable. May be used instead of, or in addition to, the parent above. NOTE: Not compatible with PageListSelect input field types.'); // Description for Template of selectable pages + $field->label = $this->_('Template'); + $field->description = $this->_('Select the template of the pages that are selectable. May be used instead of, or in addition to, the parent above.'); // Description for Template of selectable pages foreach($this->templates as $template) { $field->addOption($template->id, $template->name); } - $field->attr('value', $this->getSetting('template_id')); + $template_id = parent::getSetting('template_id'); + $field->attr('value', $template_id); $field->collapsed = Inputfield::collapsedBlank; + $field->icon = 'cube'; $fieldset->append($field); - + $templateIDs = $this->getTemplateIDs(); + $key = array_search($template_id, $templateIDs); + if(is_int($key)) unset($templateIDs[$key]); /** @var InputfieldAsmSelect $field */ $field = $this->modules->get('InputfieldAsmSelect'); $field->attr('name', 'template_ids'); - $field->label = $this->_('Need more templates? Select them here'); - $field->description = $this->_('Optionally select multiple templates to use for page selections.'); - $field->description .= ' ' . $this->_('This may not be supported by all input types.'); + $field->label = $this->_('Additional templates'); + $field->description = $this->_('If you need additional templates for selectable pages, select them here.'); + // $field->description .= ' ' . $this->_('This may not be supported by all input types.'); + $field->icon = 'cubes'; foreach($this->templates as $template) { $field->addOption($template->id, $template->name); } @@ -982,31 +1017,72 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { } $fieldset->append($field); + $extra = $this->_('While this overrides parent and template selections above, those selections (if present) are still used for validation.'); // Additional notes + + /** @var InputfieldSelector $field */ + $field = $this->modules->get('InputfieldSelector'); + $field->description = $this->_('Add one or more fields below to create a query that finds the pages you want to make selectable.'); + $field->description .= ' ' . $this->_('This creates a selector that finds pages at runtime. If you prefer to enter this manually, use the “Selector string” option below instead.'); + $field->description .= ' ' . $extra; + $field->attr('name', 'findPagesSelect'); + $field->label = $this->_('Custom find'); + $field->attr('value', $this->get('findPagesSelect')); + //$field->collapsed = Inputfield::collapsedBlank; + $field->icon = 'search-plus'; + $field->addLabel = $this->_('Add field to query'); + $field->allowSystemCustomFields = true; + $field->allowSystemTemplates = true; + $field->showFieldLabels = 1; + $field->collapsed = Inputfield::collapsedBlank; + $fieldset->append($field); + + /** @var InputfieldText $field */ $field = $this->modules->get('InputfieldText'); $field->attr('name', 'findPagesSelector'); - $field->label = $this->_('Custom selector to find selectable pages'); + $field->label = $this->_('Selector string'); $field->attr('value', $this->findPagesSelector); - $field->description = $this->_('If you want to find selectable pages using a ProcessWire selector rather than selecting a parent page or template (above) then enter the selector to find the selectable pages. This selector will be passed to a $pages->find("your selector"); statement. NOTE: Not currently compatible with PageListSelect input field types.'); // Description for Custom selector to find selectable pages - $extra = $this->_('While this overrides parent and template selections above, those selections are still used for validation (de-select them if you do not want that behavior).'); // Additional notes + $field->description = $this->_('If you want to find selectable pages using a ProcessWire selector, enter the selector string to find the selectable pages. This selector will be passed to a `$pages->find("your selector");` call.'); // Description for Custom selector to find selectable pages $field->description .= ' ' . $extra; $field->notes = $exampleLabel . $this->_('parent=/products/, template=product, sort=name'); // Example of Custom selector to find selectable pages $field->collapsed = Inputfield::collapsedBlank; - $fieldset->append($field); + $field->icon = 'search'; + $fieldset->append($field); - /** @var InputfieldTextarea $field */ - $field = $this->modules->get('InputfieldTextarea'); - $field->attr('name', 'findPagesCode'); - $field->label = $this->_('Custom PHP code to find selectable pages'); - $field->attr('value', $this->findPagesCode); - $field->attr('rows', 4); - $field->description = $this->_('If you want to find selectable pages using a PHP code snippet rather than selecting a parent page or template (above) then enter the code to find the selectable pages. This statement has access to the $page and $pages API variables, where $page refers to the page being edited.'); // Description for Custom PHP to find selectable pages 1 - $field->description .= ' ' . $this->_('The snippet should return either a PageArray, Page or NULL. If it returns a Page, children of that Page are used as selectable pages. Using this is optional, and if used, it overrides the parent/template/selector fields above.'); // Description for Custom PHP to find selectable pages 2 - $field->description .= ' ' . $extra; - $field->description .= ' ' . $this->_('NOTE: Not compatible with PageListSelect or Autocomplete input field types.'); // Description for Custom PHP to find selectable pages 3 - $field->notes = $exampleLabel . $this->_('return $page->parent->parent->children("name=locations")->first()->children();'); // Example of Custom PHP code to find selectable pages - $field->collapsed = Inputfield::collapsedBlank; - $fieldset->append($field); + if($this->findPagesCode) { + // allow only if already present, as this option is deprecated + /** @var InputfieldTextarea $field */ + $field = $this->modules->get('InputfieldTextarea'); + $field->attr('name', 'findPagesCode'); + $field->attr('value', $this->findPagesCode); + $field->attr('rows', 4); + $field->description = $this->_('If you want to find selectable pages using a PHP code snippet rather than selecting a parent page or template (above) then enter the code to find the selectable pages. This statement has access to the $page and $pages API variables, where $page refers to the page being edited.'); // Description for Custom PHP to find selectable pages 1 + $field->description .= ' ' . $this->_('The snippet should return either a PageArray, Page or NULL. If it returns a Page, children of that Page are used as selectable pages. Using this is optional, and if used, it overrides the parent/template/selector fields above.'); // Description for Custom PHP to find selectable pages 2 + $field->description .= ' ' . $extra; + $field->description .= ' ' . $this->_('NOTE: Not compatible with PageListSelect or Autocomplete input field types.'); // Description for Custom PHP to find selectable pages 3 + $field->notes = $exampleLabel . $this->_('return $page->parent->parent->children("name=locations")->first()->children();'); // Example of Custom PHP code to find selectable pages + $field->collapsed = Inputfield::collapsedBlank; + } else { + $field = $this->modules->get('InputfieldMarkup'); + $field->attr('name', '_findPagesCode'); + $field->collapsed = Inputfield::collapsedYes; + $field->value = '

    ' . + sprintf($this->_('Add the following hook to a %s file and modify per your needs. The hook should find and return selectable pages in a PageArray.'), '/site/ready.php') . + "

    " . 
    +				"\$wire->addHookAfter('InputfieldPage::getSelectablePages', function(\$event) {" .
    +				"\n  if(\$event->object->name == '$this->name') {" . 
    +				"\n    \$event->return = \$event->pages->find('your selector here');" . 
    +				"\n  }" . 
    +				"\n});" . 
    +				"
    " . + "

    " . + sprintf($this->_('If you need to know the page being edited, it is accessible from: %s'), + "\$event->arguments('page');") . + "

    "; + } + $field->label = $this->_('Custom PHP code'); + $field->icon = 'code'; + $fieldset->append($field); $inputfields->append($fieldset); @@ -1015,6 +1091,7 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $field->attr('name', 'labelFieldName'); $field->label = $this->_('Label field'); $field->required = true; + $field->icon = 'thumb-tack'; $field->description = $this->_('Select the page field that you want to be used in generating the labels for each selectable page.'); // Description for Label Field $field->notes = $this->_('Select "Custom format" if you want to specify multiple fields, or other fields you do not see above.'); $field->addOption('.', $this->_('Custom format (multiple fields)' . ' ...')); @@ -1069,50 +1146,102 @@ class InputfieldPage extends Inputfield implements ConfigurableModule { $field->setAttribute('name', 'inputfield'); $field->setAttribute('value', $this->inputfield); $field->label = $this->_('Input field type'); - $field->description = $this->_('The type of field that will be used to select a page. Select one that is consistent with the single page vs. multi-page needs you chose in the "details" tab of this field.'); // Description for Inputfield field type - $field->description .= ' ' . $this->_('Some input types may provide additional configuration options. After selecting an input field type, and then saving your changes, additional configuration options may appear below this section.'); // Description 2 for Inputfield field type + $field->description = $this->_('The type of input field (Inputfield module) that will be used to select page(s) for this field.'); + $field->description .= ' ' . $this->_('Select one that is consistent with your “Value type” selection on the “Details” tab for single or multiple-page selection.'); + $field->notes = $this->_('After selecting an input field type and saving changes, please note that additional configuration options specific to your selection may appear directly below this.'); $field->required = true; - $field->notes = '* ' . $this->_('Types indicated with an asterisk are for multiple page selection.') . "\n" . - '+ ' . $this->_('Types indicated with a plus assume a "parent" to be the root of a tree, rather than an immediate parent.'); + $field->icon = 'plug'; + $inputfieldSelection = $field; + + $singles = array(); + $multiples = array(); + $sortables = array(); + $pageListTypes = array(); foreach($this->inputfieldClasses as $class) { - $module = $this->modules->get($class); - $label = str_replace("Inputfield", '', $class); - if($module instanceof InputfieldHasArrayValue) $label .= "*"; - if($module instanceof InputfieldPageListSelection) $label .= "+"; - // if(is_subclass_of($module, 'InputfieldPageListSelection')) $label .= "+"; - $field->addOption($class, $label); + $module = $this->modules->getModule($class, array('noInit' => true)); + $info = $this->modules->getModuleInfo($module); + $label = ucfirst($info['title']); + if($module instanceof InputfieldPageListSelection) { + $pageListTypes[] = $class; + } + if($module instanceof InputfieldHasSortableValue) { + $sortables[$class] = $label; + } else if($module instanceof InputfieldHasArrayValue) { + $multiples[$class] = $label; + } else { + $singles[$class] = $label; + } + if($class == 'InputfieldPageAutocomplete') $singles["_$class"] = $label; } - $inputfields->append($field); + $multiLabel = $this->_('Multiple page selection'); + $field->addOption($this->_('Single page selection'), $singles); + $field->addOption($multiLabel, $multiples); + $field->addOption($multiLabel . ' (' . $this->_('sortable') . ')', $sortables); + $inputfields->insertBefore($field, $selectablePagesFieldset); + + $f = $this->modules->get('InputfieldMarkup'); + $f->label = $this->_('Regarding “Page List” input types'); + $f->icon = 'warning'; + $f->showIf = 'inputfield=' . implode('|', $pageListTypes); + $f->value = '

    ' . + $this->_('You have selected an input type that has specific requirements.') . ' ' . + $this->_('Specify only the “Parent” option below when configuring “Selectable pages”.') . ' ' . + $this->_('Note that the parent you specify implies the root of the tree of selectable pages.') . + $this->_('If you want to make everything selectable, then specify nothing.') . + '

    '; + $inputfields->insertAfter($f, $field); + + if($this->hasFieldtype !== false) { + $field = $this->modules->get('InputfieldCheckbox'); + $field->attr('name', 'addable'); + $field->attr('value', 1); + $field->icon = 'lightbulb-o'; + $field->label = $this->_('Allow new pages to be created from field?'); + $field->description = $this->_('If checked, an option to add new page(s) will also be present if the indicated requirements are met.'); + $field->notes = + $this->_('1. Both a parent and template must be specified in the “Selectable pages” section above.') . "\n" . + $this->_('2. The editing user must have access to create/publish these pages.') . "\n" . + $this->_('3. The “label field” must be set to “title (default)”.'); + + if($this->addable) { + $field->attr('checked', 'checked'); + } else { + $field->collapsed = Inputfield::collapsedYes; + } + $inputfields->append($field); + } + + foreach(parent::___getConfigInputfields() as $inputfield) { + $inputfields->add($inputfield); + } $inputfield = $this->getInputfield(); if($inputfield) { - // then call again, knowing the module has it's config populated - $inputfield->hasFieldtype = true; // tell it it's under control of a parent, regardless of whether this one is hasFieldtype true or not. + // tell it it's under control of a parent, regardless of whether this one is hasFieldtype true or not. + $info = $this->modules->getModuleInfo($inputfield); + $inputfield->hasFieldtype = true; + /** @var InputfieldFieldset $fieldset */ + $fieldset = $this->modules->get('InputfieldFieldset'); + $n = 0; foreach($inputfield->___getConfigInputfields() as $f) { - if(in_array($f->name, array('required', 'collapsed', 'columnWidth')) || array_key_exists($f->name, self::$defaultConfig)) continue; - if($f->name && $inputfields->getChildByName($f->name)) continue; // if we already have the given field, skip over it to avoid duplication - $inputfields->add($f); + if(in_array($f->name, array('required', 'requiredIf', 'showIf', 'collapsed', 'columnWidth'))) continue; + if(array_key_exists($f->name, self::$defaultConfig)) continue; + // if we already have the given field, skip over it to avoid duplication + if($f->name && $inputfields->getChildByName($f->name)) continue; + $fieldset->add($f); + $n++; + } + if($n) { + $fieldset->label = sprintf($this->_('Settings specific to “%s”'), $info['title']); + $fieldset->icon = 'gear'; + $fieldset->collapsed = Inputfield::collapsedYes; + $fieldset->showIf = 'inputfield=' . $inputfield->className(); + $inputfields->insertAfter($fieldset, $inputfieldSelection); } } - - if($this->hasFieldtype !== false) { - $field = $this->modules->get('InputfieldCheckbox'); - $field->attr('name', 'addable'); - $field->attr('value', 1); - $field->label = $this->_('Allow new pages to be created from field?'); - $field->description = $this->_('If checked, an option to add new page(s) will also be present if the indicated requirements are met.'); - $field->notes = - $this->_('1. Both a parent and template must be selected above.') . "\n" . - $this->_('2. The editing user must have access to create/publish these pages.') . "\n" . - $this->_('3. The label-field must be set to "title (default)".'); - - if($this->addable) $field->attr('checked', 'checked'); - else $field->collapsed = Inputfield::collapsedYes; - $inputfields->append($field); - } - + $this->configMode = false; // reverse what was set at the top of this function return $inputfields; } diff --git a/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.module b/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.module index d9fb7fa5..287a1b8f 100644 --- a/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.module +++ b/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.module @@ -27,7 +27,7 @@ * @method string renderListItem($label, $value, $class = '') * */ -class InputfieldPageAutocomplete extends Inputfield implements InputfieldHasArrayValue { +class InputfieldPageAutocomplete extends Inputfield implements InputfieldHasArrayValue, InputfieldHasSortableValue { public static function getModuleInfo() { return array( @@ -365,9 +365,6 @@ _OUT; */ public function ___getConfigInputfields() { $inputfields = parent::___getConfigInputfields(); - $fieldset = $this->wire('modules')->get('InputfieldFieldset'); - $fieldset->label = $this->_('Autocomplete Advanced Options'); - $fieldset->collapsed = Inputfield::collapsedYes; $recommended = $this->_('Recommended'); /** @var InputfieldRadios $field */ @@ -377,21 +374,23 @@ _OUT; $field->description = $this->_("The search operator that is used in the API when performing autocomplete matches."); $field->notes = $this->_("If you aren't sure what you want here, leave it set at the default: *="); $field->required = false; - $field->addOption('*=', '*= ' . $this->_("Contains phrase or partial word (using fulltext index) - Recommended")); - $field->addOption('%=', '%= ' . $this->_("Contains phrase or partial word (using LIKE)") . " - $recommended"); - $field->addOption('~=', '~= ' . $this->_("Contains all the [full] words, in any order")); - $field->addOption('^=', '^= ' . $this->_("Starts with word/phrase")); - $field->addOption('$=', '$= ' . $this->_("Ends with word/phrase")); - $field->addOption('=', '= ' . $this->_("Equals [exact]")); + $field->addOption('*=', '`*= ` ' . $this->_("Contains phrase or partial word (using fulltext index) - Recommended")); + $field->addOption('%=', '`%= ` ' . $this->_("Contains phrase or partial word (using LIKE)") . " - $recommended"); + $field->addOption('~=', '`~= ` ' . $this->_("Contains all the [full] words, in any order")); + $field->addOption('^=', '`^= ` ' . $this->_("Starts with word/phrase")); + $field->addOption('$=', '`$= ` ' . $this->_("Ends with word/phrase")); + $field->addOption('=', '` = ` ' . $this->_("Equals [exact]")); $field->attr('value', $this->operator); $field->collapsed = Inputfield::collapsedNo; - $fieldset->add($field); + $inputfields->add($field); /** @var InputfieldText $field */ $field = $this->modules->get('InputfieldText'); $field->attr('name', 'searchFields'); $field->label = $this->_('Fields to query for autocomplete'); - $field->description = $this->_('Enter the names of the fields that should have their text queried for autocomplete matches. Typically this would just be the title field, but you may add others by separating each with a space. Note that this is different from the "Label Field" (above). Here you are specifying what fields will be searched, not what fields will be shown.'); + $field->description = $this->_('Enter the names of the fields that should have their text queried for autocomplete matches.'); + $field->description .= ' ' . $this->_('Typically this would just be the title field, but you may add others by separating each with a space.'); + $field->description .= ' ' . $this->_('Note that this is different from the “label field” because you are specifying what fields will be searched, not what fields will be shown.'); $field->collapsed = Inputfield::collapsedNo; $field->attr('value', $this->searchFields); $notes = $this->_('Indexed text fields include:'); @@ -400,9 +399,7 @@ _OUT; $notes .= ' ' . $f->name . ','; } $field->notes = rtrim($notes, ','); - $fieldset->add($field); - - $inputfields->add($fieldset); + $inputfields->add($field); return $inputfields; } diff --git a/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelect.module b/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelect.module index 41a5e473..58c29173 100644 --- a/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelect.module +++ b/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelect.module @@ -75,4 +75,8 @@ class InputfieldPageListSelect extends Inputfield implements InputfieldPageListS return $this; } + public function isEmpty() { + return ((int) $this->attr('value')) < 1; + } + } diff --git a/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelectMultiple.module b/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelectMultiple.module index a371cb4b..1e5cfc8a 100644 --- a/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelectMultiple.module +++ b/wire/modules/Inputfield/InputfieldPageListSelect/InputfieldPageListSelectMultiple.module @@ -20,7 +20,8 @@ * */ -class InputfieldPageListSelectMultiple extends Inputfield implements InputfieldHasArrayValue, InputfieldPageListSelection { +class InputfieldPageListSelectMultiple extends Inputfield + implements InputfieldHasArrayValue, InputfieldPageListSelection, InputfieldHasSortableValue { public static function getModuleInfo() { return array(