2022-03-08 15:55:41 +01:00
|
|
|
<?php namespace ProcessWire;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* InputfieldForm: An Inputfield for containing form elements
|
|
|
|
*
|
2022-11-05 18:32:48 +01:00
|
|
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
2022-03-08 15:55:41 +01:00
|
|
|
* https://processwire.com
|
|
|
|
*
|
|
|
|
* @property string $prependMarkup Optional markup to prepend to the form output
|
|
|
|
* @property string $appendMarkup Optional markup to append to the form output
|
|
|
|
* @property bool $protectCSRF Set to false to disable automatic CSRF protection
|
|
|
|
* @property int $columnWidthSpacing Optionally set the column width spacing (pixels)
|
|
|
|
* @property string $description Optionally set a description headline for the form
|
|
|
|
* @property string $confirmText Confirmation text that precedes list of changes when class InputfieldFormConfirm is active
|
|
|
|
* @property string $method Form method attribute (default="post")
|
|
|
|
* @property string $action Form action attribute (default="./")
|
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* @method bool process() Process the form and return true on success, false on error (3.0.205+).
|
2022-03-08 15:55:41 +01:00
|
|
|
* @method void renderOrProcessReady($type) Hook called before form render or process (3.0.171+)
|
|
|
|
*
|
|
|
|
* Optional classes:
|
|
|
|
* =================
|
|
|
|
* InputfieldFormNoHeights: tells it not to worry about lining up all columns vertically.
|
|
|
|
* InputfieldFormNoWidths: indicates that form will be in 2-column label => input format (column widths do not apply)
|
|
|
|
* InputfieldFormConfirm: tell it to notify user if they make any changes and forgot to submit.
|
|
|
|
*
|
|
|
|
* #pw-body =
|
|
|
|
* Here is an example of creating an InputfieldForm using Inputfield modules. This particular example
|
|
|
|
* is an email subscription form.
|
|
|
|
* ~~~~~
|
|
|
|
* $form = $modules->get('InputfieldForm');
|
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* $f = $form->InputfieldText;
|
|
|
|
* $f->attr('name', 'your_name');
|
|
|
|
* $f->label = 'Your Name';
|
|
|
|
* $form->add($f);
|
2022-03-08 15:55:41 +01:00
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* $f = $form->InputfieldEmail;
|
|
|
|
* $f->attr('name', 'your_email');
|
|
|
|
* $f->label = 'Your Email Address';
|
|
|
|
* $f->required = true;
|
|
|
|
* $form->add($f);
|
2022-03-08 15:55:41 +01:00
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* $f = $form->InputfieldSubmit;
|
|
|
|
* $f->attr('name', 'submit_subscribe');
|
|
|
|
* $f->val('Subscribe');
|
|
|
|
* $form->add($f);
|
|
|
|
*
|
|
|
|
* // ProcessWire versions 3.0.205+
|
|
|
|
* if($form->isSubmitted('submit_subscribe')) {
|
|
|
|
* if($form->process()) {
|
|
|
|
* $name = $form->getValueByName('your_name');
|
|
|
|
* $email = $form->getValueByName('your_email');
|
|
|
|
* echo "<h3>Thank you, you have been subscribed!</h3>";
|
|
|
|
* } else {
|
|
|
|
* echo "<h3>There were errors, please fix</h3>";
|
|
|
|
* echo $form->render();
|
|
|
|
* }
|
|
|
|
* } else {
|
|
|
|
* // form not submitted, just display it
|
|
|
|
* echo $form->render();
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // same as above but works in any ProcessWire version
|
2022-03-08 15:55:41 +01:00
|
|
|
* if($input->post('submit_subscribe')) {
|
|
|
|
* // form submitted
|
|
|
|
* $form->processInput($input->post);
|
|
|
|
* $errors = $form->getErrors();
|
|
|
|
* if(count($errors)) {
|
|
|
|
* // unsuccessful submit, re-display form
|
|
|
|
* echo "<h3>There were errors, please fix</h3>";
|
|
|
|
* echo $form->render();
|
|
|
|
* } else {
|
|
|
|
* // successful submit (save $name and $email somewhere)
|
2023-03-10 19:41:40 +01:00
|
|
|
* $name = $form->getChildByName('your_name')->attr('value');
|
|
|
|
* $email = $form->getChildByName('your_email')->attr('value');
|
2022-03-08 15:55:41 +01:00
|
|
|
* echo "<h3>Thank you, you have been subscribed!</h3>";
|
|
|
|
* }
|
|
|
|
* } else {
|
|
|
|
* // form not submitted, just display it
|
|
|
|
* echo $form->render();
|
|
|
|
* }
|
|
|
|
* ~~~~~
|
|
|
|
*
|
|
|
|
* #pw-body
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
class InputfieldForm extends InputfieldWrapper {
|
|
|
|
|
|
|
|
public static function getModuleInfo() {
|
|
|
|
return array(
|
|
|
|
'title' => __('Form', __FILE__), // Module Title
|
|
|
|
'summary' => __('Contains one or more fields in a form', __FILE__), // Module Summary
|
|
|
|
'version' => 107,
|
|
|
|
'permanent' => true,
|
2022-11-05 18:32:48 +01:00
|
|
|
);
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const debug = false; // set to true to enable debug mode for field dependencies
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WireInputData provided to processInput() method
|
|
|
|
*
|
|
|
|
* @var WireInputData|null
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected $_processInputData = null;
|
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
/**
|
|
|
|
* @var array|null
|
|
|
|
*/
|
|
|
|
protected $errorCache = null;
|
|
|
|
|
2022-03-08 15:55:41 +01:00
|
|
|
/**
|
|
|
|
* Construct
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function __construct() {
|
|
|
|
$this->set('protectCSRF', true);
|
|
|
|
parent::__construct();
|
|
|
|
$this->attr('method', 'post');
|
|
|
|
$this->attr('action', './');
|
|
|
|
$this->set('class', '');
|
|
|
|
$this->set('prependMarkup', '');
|
|
|
|
$this->set('appendMarkup', '');
|
|
|
|
$this->set('confirmText', $this->_('There are unsaved changes:'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render form
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function ___render() {
|
|
|
|
|
|
|
|
if($this->hasHook('renderOrProcessReady()')) $this->renderOrProcessReady('render');
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
$method = strtolower($this->attr('method'));
|
2022-03-08 15:55:41 +01:00
|
|
|
$markup = self::getMarkup();
|
|
|
|
$classes = self::getClasses();
|
|
|
|
if(!empty($classes['form'])) $this->addClass($classes['form']);
|
|
|
|
|
|
|
|
$this->attr('data-colspacing', (int) $this->getSetting('columnWidthSpacing'));
|
|
|
|
$this->addClass('InputfieldForm');
|
|
|
|
|
|
|
|
if($this->hasClass('InputfieldFormConfirm')) {
|
2023-03-10 19:41:40 +01:00
|
|
|
if($this->wire()->modules->isInstalled('FormSaveReminder')) {
|
2022-03-08 15:55:41 +01:00
|
|
|
// let FormSaveReminder module have control, if it's installed
|
|
|
|
$this->removeClass('InputfieldFormConfirm');
|
|
|
|
} else {
|
|
|
|
$this->attr('data-confirm', $this->getSetting('confirmText'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$attrs = $this->getAttributes();
|
|
|
|
unset($attrs['value']);
|
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
if($this->wire()->input->get('modal') && strpos($attrs['action'], 'modal=1') === false) {
|
2022-03-08 15:55:41 +01:00
|
|
|
// retain a modal=1 state in the form action
|
|
|
|
$attrs['action'] .= (strpos($attrs['action'], '?') === false ? '?' : '&') . 'modal=1';
|
|
|
|
}
|
|
|
|
|
|
|
|
$description = $this->getSetting('description');
|
|
|
|
if($description) $description = str_replace('{out}', $this->entityEncode($description), $markup['item_head']);
|
|
|
|
|
|
|
|
$attrStr = $this->getAttributesString($attrs);
|
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
if($this->getSetting('protectCSRF') && $method === 'post') {
|
2024-04-04 14:37:20 +02:00
|
|
|
$tokenField = $this->wire()->session->CSRF->renderInput();
|
2022-03-08 15:55:41 +01:00
|
|
|
} else {
|
|
|
|
$tokenField = '';
|
|
|
|
}
|
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
if($method === 'post') {
|
|
|
|
$className = $this->className();
|
|
|
|
$formName = $this->wire()->sanitizer->entities($this->getFormName());
|
|
|
|
$landmark = "<input type='hidden' name='_$className' value='$formName' />";
|
|
|
|
} else {
|
|
|
|
$landmark = '';
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
return
|
2022-03-08 15:55:41 +01:00
|
|
|
"<form $attrStr>" .
|
2023-03-10 19:41:40 +01:00
|
|
|
$description .
|
|
|
|
$this->getSetting('prependMarkup') .
|
|
|
|
parent::___render() .
|
|
|
|
$tokenField .
|
|
|
|
$this->getSetting('appendMarkup') .
|
|
|
|
$landmark .
|
2022-03-08 15:55:41 +01:00
|
|
|
"</form>";
|
2023-03-10 19:41:40 +01:00
|
|
|
}
|
2022-03-08 15:55:41 +01:00
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
/**
|
|
|
|
* Process the form
|
|
|
|
*
|
|
|
|
* - Optionally use this rather than processInput() for processing forms.
|
|
|
|
* - Returns true on processing success or false if processed with errors.
|
|
|
|
* - Use the getErrors() method to retrieve errors.
|
|
|
|
*
|
|
|
|
* ~~~~~
|
|
|
|
* if($form->process()) {
|
|
|
|
* // form processed successfully without errors
|
|
|
|
* } else {
|
|
|
|
* $errors = $form->getErrors(); // array of error messages…
|
|
|
|
* $inputs = $form->getErrorInputfields(); // …or array of Inputfields with errors
|
|
|
|
* }
|
|
|
|
* ~~~~~
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
* @throws WireException
|
|
|
|
* @since 3.0.205
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function ___process() {
|
|
|
|
$input = $this->wire()->input;
|
|
|
|
$method = strtolower($this->attr('method'));
|
|
|
|
$this->processInput(($method === 'get' ? $input->get : $input->post));
|
|
|
|
return count($this->getErrors()) === 0;
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process input
|
|
|
|
*
|
|
|
|
* @param WireInputData $input
|
|
|
|
* @return InputfieldWrapper
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function ___processInput(WireInputData $input) {
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
$this->errorCache = null;
|
2022-03-08 15:55:41 +01:00
|
|
|
$this->_processInputData = $input;
|
|
|
|
if($this->hasHook('renderOrProcessReady()')) $this->renderOrProcessReady('process');
|
|
|
|
|
|
|
|
$this->getErrors(true); // reset
|
|
|
|
|
|
|
|
if($this->getSetting('protectCSRF') && strtolower($this->attr('method')) === 'post') {
|
|
|
|
$this->wire()->session->CSRF->validate(); // throws exception if invalid
|
|
|
|
}
|
|
|
|
|
|
|
|
$result = parent::___processInput($input);
|
|
|
|
|
|
|
|
$delayedChildren = $this->_getDelayedChildren(true);
|
|
|
|
$delayedChildren = $this->processInputShowIf($input, $delayedChildren);
|
|
|
|
$this->processInputRequiredIf($input, $delayedChildren);
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process input for show-if dependencies
|
|
|
|
*
|
|
|
|
* @param WireInputData $input
|
|
|
|
* @param array $delayedChildren
|
|
|
|
* @return array
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function processInputShowIf(WireInputData $input, array $delayedChildren) {
|
|
|
|
|
|
|
|
if(!count($delayedChildren)) return $delayedChildren;
|
|
|
|
|
|
|
|
$maxN = 255;
|
|
|
|
$n = 0;
|
|
|
|
$delayedN = count($delayedChildren);
|
|
|
|
$processedN = 0;
|
|
|
|
$unprocessedN = 0;
|
|
|
|
|
|
|
|
/** @var Inputfield[] $savedChildren */
|
|
|
|
$savedChildren = $delayedChildren;
|
|
|
|
|
|
|
|
while(count($delayedChildren)) {
|
|
|
|
|
|
|
|
if(++$n >= $maxN) {
|
|
|
|
$this->error("Max number of iterations reached for processing field dependencies", Notice::debug);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// shift first $child off the array
|
|
|
|
$child = array_shift($delayedChildren);
|
|
|
|
if(self::debug) $this->debugNote("Processing delayed child: $child->name ($child->label)");
|
|
|
|
$selectorString = $child->getSetting('showIf');
|
|
|
|
if(!strlen($selectorString)) {
|
|
|
|
if(self::debug) $this->debugNote("Skipping $child->name ($child->label): No selector string");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(self::debug) $this->debugNote("showIf selector: $selectorString");
|
2022-11-05 18:32:48 +01:00
|
|
|
|
|
|
|
/** @var Selectors $selectors */
|
2022-03-08 15:55:41 +01:00
|
|
|
$selectors = $this->wire(new Selectors($selectorString));
|
|
|
|
|
|
|
|
// whether we should process $child now or not
|
|
|
|
$processNow = true;
|
|
|
|
$selector = null;
|
|
|
|
|
|
|
|
foreach($selectors as $selector) {
|
|
|
|
|
|
|
|
$fields = is_array($selector->field) ? $selector->field : array($selector->field);
|
|
|
|
|
|
|
|
// first determine that the dependency fields have already been processed
|
|
|
|
foreach($fields as $name) {
|
|
|
|
if(self::debug) $this->debugNote("$child->name requires: $name");
|
|
|
|
|
|
|
|
if(isset($savedChildren[$name]) && $name !== "1") {
|
|
|
|
|
|
|
|
// if field had already been through the loop, but was not processed, add it back in for processing
|
|
|
|
if(!isset($delayedChildren[$name])
|
|
|
|
&& !$savedChildren[$name]->getSetting('showIfProcessed')
|
|
|
|
&& !$savedChildren[$name]->getSetting('showIfSkipped')) {
|
|
|
|
$delayedChildren[$name] = $savedChildren[$name];
|
|
|
|
}
|
|
|
|
|
|
|
|
// force $delayedChildren[$name] to match so that it is processed here, by giving it special selector: 1>0
|
|
|
|
if(!strlen($savedChildren[$name]->getSetting('showIf'))) {
|
|
|
|
$savedChildren[$name]->showIf = '1>0'; // forced match
|
|
|
|
}
|
|
|
|
|
|
|
|
if($savedChildren[$name]->getSetting('showIfSkipped')) {
|
|
|
|
// dependency $field does not need to be processed, so neither does this field
|
|
|
|
list($val, $op) = array($selector->value, $selector->operator);
|
|
|
|
if(($op === '!=' && $val) || $op === '=' && empty($val)) {
|
|
|
|
// allow it through to match non-presence (via issue #890)
|
|
|
|
} else {
|
|
|
|
unset($delayedChildren[$child->name]);
|
|
|
|
$processNow = false;
|
|
|
|
if(self::debug) $this->debugNote("Removing field '$child->name' because '$name' it not shown.");
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if(!$savedChildren[$name]->getSetting('showIfProcessed')) {
|
|
|
|
// dependency $field is another one in $delayedChildren, send it back to the end
|
|
|
|
unset($delayedChildren[$child->name]);
|
|
|
|
// put it back on the end
|
|
|
|
$delayedChildren[$child->name] = $child;
|
|
|
|
if(self::debug) $this->debugNote("Sending field '$child->name' back to the end.");
|
|
|
|
$processNow = false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// $field is most likely a form field that has already been processed and is good to use
|
|
|
|
$processNow = true;
|
|
|
|
}
|
|
|
|
} // foreach($fields)
|
|
|
|
|
|
|
|
if(!$processNow) break; // out to next $child
|
|
|
|
|
|
|
|
$numFieldsMatched = 0;
|
|
|
|
|
|
|
|
// good to process $child
|
|
|
|
foreach($fields as $name) {
|
|
|
|
|
|
|
|
if($name == '1') {
|
|
|
|
$numFieldsMatched++;
|
|
|
|
} else if($this->selectorMatchesInputfield($selector, $name, "'showIf' from '$child->label'")) {
|
|
|
|
$numFieldsMatched++;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // $fields
|
|
|
|
|
|
|
|
$processNow = $numFieldsMatched > 0;
|
|
|
|
// @todo 3.0.150: if($processNow) $child->set('showIfSkipped', false); // https://processwire.com/talk/topic/22130-forced-10-inputfield-dependency-issues/
|
|
|
|
if(!$processNow) break;
|
|
|
|
|
|
|
|
if(self::debug) $this->debugNote("$child->name ($child->label) - matched: showIf($selector)");
|
|
|
|
$processedN++;
|
|
|
|
|
|
|
|
} // $selectors
|
|
|
|
|
|
|
|
|
|
|
|
if(!$processNow) {
|
|
|
|
if(self::debug) {
|
|
|
|
$this->debugNote("$child->name ($child->label) - did not match: showIf($selector)");
|
|
|
|
$this->debugNote("Skipped processing for: $child->name ($child->label)");
|
|
|
|
}
|
|
|
|
$child->set('showIfSkipped', true); // flag the field as skipped
|
|
|
|
$unprocessedN++;
|
|
|
|
// since this didn't match, then no other selectors in the group for this child can match, so break out of the selector loop
|
|
|
|
continue; // to next $child
|
|
|
|
}
|
|
|
|
|
|
|
|
// the required dependency is in place so that $child can be processed
|
|
|
|
// temporarily remove the showIf property to prevent InputfieldWrapper's from delaying it again
|
|
|
|
$showIf = $child->getSetting('showIf');
|
|
|
|
$child->set('showIf', ''); // remove showIf property
|
|
|
|
$child->processInput($input); // process input
|
|
|
|
if($showIf != '1>0') $child->set('showIf', $showIf); // restore showIf property
|
|
|
|
$child->set('showIfProcessed', true); // flag it as processed
|
|
|
|
if(self::debug) $this->debugNote("$child->name - processed!");
|
|
|
|
|
|
|
|
// now check if the processed child has children of it's own that may have been delayed
|
|
|
|
if($child instanceof InputfieldWrapper) {
|
|
|
|
$delayed = $child->_getDelayedChildren(true);
|
|
|
|
if(count($delayed)) {
|
|
|
|
foreach($delayed as $d) {
|
|
|
|
$dname = $d->attr('name');
|
|
|
|
if(!$dname) $dname = $d->attr('id');
|
|
|
|
if(self::debug) $this->debugNote("Delayed: $dname ($d->label)");
|
|
|
|
}
|
|
|
|
$delayedChildren = array_merge($delayedChildren, $delayed); // add them to delayed children
|
|
|
|
$savedChildren = array_merge($savedChildren, $delayed); // add them to saved children (to be sent to requiredIf too)
|
|
|
|
$delayedN += count($delayed);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // count($delayedChildren)
|
|
|
|
|
|
|
|
if(self::debug) $this->debugNote("delayedChildren: $delayedN ($processedN processed, $unprocessedN not)");
|
|
|
|
return $savedChildren;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process input for fields with a required-if dependency
|
|
|
|
*
|
|
|
|
* @param WireInputData $input
|
|
|
|
* @param array|Inputfield[] $delayedChildren
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function processInputRequiredIf(WireInputData $input, array $delayedChildren) {
|
|
|
|
|
|
|
|
// process input for any remaining delayedChildren not already processed by processInputShowIf
|
|
|
|
foreach($delayedChildren as $child) {
|
|
|
|
if($child->getSetting('showIfSkipped') || $child->getSetting('showIfProcessed')) continue;
|
|
|
|
if(self::debug) $this->debugNote("Now Processing requiredIf delayed child: $child->name");
|
|
|
|
$child->processInput($input);
|
|
|
|
}
|
|
|
|
|
|
|
|
while(count($delayedChildren)) {
|
|
|
|
|
|
|
|
// shift first $child off the array
|
|
|
|
$child = array_shift($delayedChildren);
|
|
|
|
if(!$child->getSetting('required') || $child->getSetting('requiredSkipped')) continue;
|
|
|
|
|
|
|
|
// if field was not shown, then it can't be required
|
|
|
|
if($child->getSetting('showIfSkipped') && !$child->getSetting('showIfProcessed')) {
|
|
|
|
$child->set('requiredSkipped', true);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$required = true;
|
|
|
|
$selectorString = $child->getSetting('requiredIf');
|
|
|
|
|
|
|
|
if(strlen($selectorString)) {
|
|
|
|
if(self::debug) $this->debugNote("requiredIf selector: $selectorString");
|
2022-11-05 18:32:48 +01:00
|
|
|
|
|
|
|
/** @var Selectors $selectors */
|
2022-03-08 15:55:41 +01:00
|
|
|
$selectors = $this->wire(new Selectors($selectorString));
|
|
|
|
|
|
|
|
foreach($selectors as $selector) {
|
|
|
|
|
|
|
|
$fields = is_array($selector->field) ? $selector->field : array($selector->field);
|
|
|
|
|
|
|
|
foreach($fields as $name) {
|
|
|
|
|
|
|
|
$matches = $this->selectorMatchesInputfield($selector, $name, 'requiredIf');
|
|
|
|
if($matches === null) continue;
|
|
|
|
if($matches === false) {
|
|
|
|
$required = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // foreach($fields)
|
|
|
|
|
|
|
|
if(!$required) break;
|
|
|
|
|
|
|
|
} // foreach($selectors)
|
|
|
|
} // if(strlen($selectorString))
|
|
|
|
|
|
|
|
if($required) {
|
|
|
|
if($child->isEmpty()) {
|
|
|
|
if(self::debug) $this->debugNote("$child->name - determined that value IS required and is not present (error)");
|
|
|
|
$requiredLabel = $child->getSetting('requiredLabel');
|
|
|
|
if(empty($requiredLabel)) $requiredLabel = $this->requiredLabel; // requiredLabel from InputfieldWrapper
|
|
|
|
$child->error($requiredLabel);
|
|
|
|
} else {
|
|
|
|
if(self::debug) $this->debugNote("$child->name - determined that value IS required and is populated (good)");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if(self::debug) $this->debugNote("$child->name - determined that value is not required");
|
|
|
|
$child->set('requiredSkipped', true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Does the selector match the given Inputfield name?
|
|
|
|
*
|
|
|
|
* @param Selector $selector
|
|
|
|
* @param string $name Name of Inputfield
|
|
|
|
* @param string $debugNote Optional qualifier note for debugging
|
|
|
|
* @return bool|null Returns true|false if match determined, or NULL if $name is not present in form
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function selectorMatchesInputfield(Selector $selector, $name, $debugNote = '') {
|
|
|
|
|
|
|
|
$subfield = '';
|
|
|
|
if(strpos($name, '.')) list($name, $subfield) = explode('.', $name);
|
|
|
|
|
|
|
|
// get the inputfield that $child has a dependency on
|
|
|
|
$inputfield = $this->getChildByName($name);
|
|
|
|
|
|
|
|
// if field is not present in this form, we assume a blank value for it
|
|
|
|
if(!$inputfield) {
|
|
|
|
if($name != 'collapsed') {
|
|
|
|
$this->error("Warning ($debugNote): dependency field '$name' is not present in this form.", Notice::debug);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$value = $inputfield->attr('value');
|
|
|
|
$value2 = null;
|
|
|
|
$matches = false;
|
|
|
|
|
|
|
|
if($subfield == 'count') {
|
|
|
|
$value = wireCount($value);
|
|
|
|
if(self::debug) $this->debugNote("Actual count ($debugNote): $value");
|
|
|
|
}
|
|
|
|
if(is_object($value)) $value = "$value";
|
|
|
|
|
|
|
|
if($inputfield instanceof InputfieldSelect && $subfield != 'count') {
|
|
|
|
$allowCheckLabels = false; // allow for match by field labels, in addition to field values
|
|
|
|
$options = $inputfield->getOptions();
|
|
|
|
// determine if selector values are referring to a value or a label
|
|
|
|
foreach($selector->values as $selectorValue) {
|
|
|
|
// if value in selector matches a known option 'label' then we allow use of labels
|
|
|
|
$key = array_search($selectorValue, $options);
|
|
|
|
if(self::debug) $this->debugNote("OPTIONS ($debugNote): Searching for label '$selectorValue' in " . print_r($options, true));
|
|
|
|
if($key !== false) {
|
|
|
|
$allowCheckLabels = true;
|
|
|
|
if(self::debug) $this->debugNote("OPTIONS ($debugNote): Found '$selectorValue' so allowing label check");
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
if(self::debug) $this->debugNote("OPTIONS ($debugNote): Did not find '$selectorValue'");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if($allowCheckLabels) {
|
|
|
|
if($inputfield instanceof InputfieldHasArrayValue) {
|
|
|
|
$value2 = array(); // matching of labels rather than values
|
2023-03-10 19:41:40 +01:00
|
|
|
foreach($value as $v) {
|
2022-03-08 15:55:41 +01:00
|
|
|
if(isset($options[$v])) $value2[] = $options[$v];
|
|
|
|
}
|
|
|
|
if(empty($value2)) $value2 = null;
|
|
|
|
} else {
|
|
|
|
if(isset($options[$value])) $value2 = $options[$value];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if($selector->matches($value)) {
|
|
|
|
if(self::debug) $this->debugNote("Selector ($debugNote) matched value \"$selector\" (field=$name, value=" . print_r($value, true) . ")");
|
|
|
|
$matches = true;
|
|
|
|
} else if($value2 !== null && $selector->matches($value2)) {
|
|
|
|
if(self::debug) {
|
|
|
|
$this->debugNote("Selector ($debugNote) did NOT match value \"$selector\" (field=$name, value=" . print_r($value, true) . ")");
|
|
|
|
$this->debugNote("Selector ($debugNote) matched label \"$selector\" (field=$name, label=" . print_r($value2, true) . ")");
|
|
|
|
}
|
|
|
|
$matches = true;
|
|
|
|
} else {
|
|
|
|
if(self::debug) $this->debugNote("Selector ($debugNote) failed to match \"$selector\" (field=$name, value=" . print_r($value, true) . ")");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $matches;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-03-10 19:41:40 +01:00
|
|
|
* Is form submitted and ready to process?
|
2022-03-08 15:55:41 +01:00
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* - Optionally use this method to test if form is submitted before calling processInput().
|
|
|
|
* - Specify the submit button name to confirm that was the button used to submit.
|
|
|
|
* - Returns the name of the submit button used when form is submitted, false otherwise.
|
|
|
|
* - If no arguments specified, requires that the form has one or more InputfieldSubmit fields.
|
|
|
|
* - Like with processInput(), make sure form is fully built before calling this.
|
|
|
|
* - If given $submitName argument that corresponds to InputfieldSubmit field then
|
|
|
|
* it will also be confirmed that the input value matches the field value prior to submit.
|
|
|
|
* - If given a $submitName argument that corresponds to some other Inputfield type then
|
|
|
|
* only its presence in the input will be confirmed.
|
2022-03-08 15:55:41 +01:00
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* ~~~~~
|
|
|
|
* if($form->isSubmitted()) {
|
|
|
|
* // form was submitted
|
|
|
|
* }
|
2022-03-08 15:55:41 +01:00
|
|
|
*
|
2023-03-10 19:41:40 +01:00
|
|
|
* // specify the button name to confirm it was used to submit the form
|
|
|
|
* if($form->isSubmitted('submit_save')) {
|
|
|
|
* // form is submitted with button named 'submit_save'
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // omit button name to have it return button name used to submit
|
|
|
|
* $submit = $form->isSubmitted(true);
|
|
|
|
* if($submit === 'add') {
|
|
|
|
* // form was submitted with button named 'add'
|
|
|
|
* } else if($submit === 'save') {
|
|
|
|
* // form submitted with button named 'save'
|
|
|
|
* } else if($submit === false) {
|
|
|
|
* // form not submitted
|
|
|
|
* } else {
|
|
|
|
* // submitted using some other button (name in $submit)
|
|
|
|
* }
|
|
|
|
* ~~~~~
|
|
|
|
*
|
|
|
|
* @param string|Inputfield|bool $submitName Any one of the following:
|
|
|
|
* - Name (string) or Inputfield (object) of submit button or other input to check.
|
|
|
|
* - Boolean true to find and return the clicked submit button name.
|
|
|
|
* - Boolean false or omit to only return true or false if form submitted (default).
|
|
|
|
* @return bool|string Returns one of the following:
|
|
|
|
* - Boolean false if form not submitted.
|
|
|
|
* - Boolean true if form submitted and submit button name not requested.
|
|
|
|
* - Submit/input name (string) if form submitted and `$submitName` argument is true or string.
|
|
|
|
* @throws WireException
|
|
|
|
* @since 3.0.205
|
|
|
|
*
|
|
|
|
*/
|
2022-03-08 15:55:41 +01:00
|
|
|
public function isSubmitted($submitName = '') {
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
$session = $this->wire()->session;
|
|
|
|
$input = $this->wire()->input;
|
2022-03-08 15:55:41 +01:00
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
$className = $this->className();
|
|
|
|
$formName = $this->getFormName();
|
|
|
|
$method = $this->attr('method') ? strtolower($this->attr('method')) : 'post';
|
|
|
|
$checkToken = $this->getSetting('protectCSRF') && $method != 'get';
|
|
|
|
|
2022-03-08 15:55:41 +01:00
|
|
|
if(!$input->requestMethod($method)) return false;
|
2023-03-10 19:41:40 +01:00
|
|
|
if($method === 'post' && $input->$method("_$className") !== $formName) return false;
|
|
|
|
if($checkToken && !$session->CSRF()->hasValidToken()) return false;
|
2022-03-08 15:55:41 +01:00
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
if(empty($submitName)) {
|
|
|
|
// no submit button name requested
|
|
|
|
$submitted = true;
|
|
|
|
|
|
|
|
} else if($submitName === true) {
|
2024-04-04 14:37:20 +02:00
|
|
|
// find which submit button was clicked to return its name
|
2023-03-10 19:41:40 +01:00
|
|
|
$submitted = false;
|
|
|
|
foreach($this->getAll() as $f) {
|
|
|
|
if(!$f instanceof InputfieldSubmit) continue;
|
|
|
|
$name = $f->attr('name');
|
|
|
|
$submitted = $input->$method($name) === $f->val() ? $name : false;
|
|
|
|
if($submitted) break;
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
} else {
|
|
|
|
// confirm that requested submit button was clicked and has same value
|
|
|
|
if($submitName instanceof Inputfield) $submitName = $submitName->attr('name');
|
|
|
|
$value = $input->$method($submitName);
|
|
|
|
if($value === null) return false;
|
|
|
|
$f = $this->getChildByName($submitName);
|
|
|
|
if($f instanceof InputfieldSubmit && $f->val() != $value) return false;
|
|
|
|
$submitted = $submitName;
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
return $submitted;
|
2022-03-08 15:55:41 +01:00
|
|
|
}
|
2023-03-10 19:41:40 +01:00
|
|
|
|
2022-03-08 15:55:41 +01:00
|
|
|
/**
|
|
|
|
* For internal debugging purposes
|
|
|
|
*
|
|
|
|
* @param $note
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function debugNote($note) {
|
|
|
|
static $n = 0;
|
|
|
|
if(self::debug) $this->message((++$n) . ". $note");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return WireInputData provided to processInput() method or null if not yet applicable
|
|
|
|
*
|
|
|
|
* @return WireInputData|null
|
|
|
|
* @since 3.0.171
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function getInput() {
|
|
|
|
return $this->_processInputData;
|
|
|
|
}
|
2023-03-10 19:41:40 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an array of errors that occurred on any of the children during input processing.
|
|
|
|
*
|
|
|
|
* Should only be called after `process()` or `processInput()`.
|
|
|
|
*
|
|
|
|
* #pw-group-errors
|
|
|
|
*
|
2024-04-04 14:37:20 +02:00
|
|
|
* @param bool|null $clear Clear or re-check for errors? (default=false)
|
|
|
|
* - Specify `true` to clear out the errors.
|
|
|
|
* - Specify `false` or omit the argument to neither clear or uncache errors. (default)
|
|
|
|
* - Specify `null` to uncache a previous call and re-check all fields in the form. (3.0.223+)
|
2023-03-10 19:41:40 +01:00
|
|
|
* @return array Array of error strings
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function getErrors($clear = false) {
|
2024-04-04 14:37:20 +02:00
|
|
|
if($clear === null) {
|
|
|
|
$this->errorCache = null;
|
|
|
|
$clear = false;
|
|
|
|
}
|
|
|
|
if($this->errorCache !== null && $clear === false) return $this->errorCache;
|
2023-03-10 19:41:40 +01:00
|
|
|
$errors = parent::getErrors($clear);
|
|
|
|
$this->errorCache = $clear ? null : $errors;
|
|
|
|
return $errors;
|
|
|
|
}
|
2022-03-08 15:55:41 +01:00
|
|
|
|
2023-03-10 19:41:40 +01:00
|
|
|
/**
|
|
|
|
* Get name for this form
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @since 3.0.205
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function getFormName() {
|
|
|
|
$formName = $this->attr('name');
|
|
|
|
if(empty($formName)) $formName = $this->attr('id');
|
|
|
|
if(empty($formName)) $formName = $this->className();
|
|
|
|
return (string) $formName;
|
|
|
|
}
|
|
|
|
|
2022-03-08 15:55:41 +01:00
|
|
|
/**
|
|
|
|
* Hook called right before form is rendered or processed
|
|
|
|
*
|
|
|
|
* @param string $type One of 'render' or 'process'
|
|
|
|
* @since 3.0.171
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected function ___renderOrProcessReady($type) {
|
|
|
|
}
|
|
|
|
}
|