MediaWiki  master
HTMLFormFieldCloner.php
Go to the documentation of this file.
1 <?php
2 
40  private static $counter = 0;
41 
47  protected $uniqueId;
48 
53  public function __construct( $params ) {
54  $this->uniqueId = static::class . ++self::$counter . 'x';
55  parent::__construct( $params );
56 
57  if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
58  throw new MWException( 'HTMLFormFieldCloner called without any fields' );
59  }
60 
61  // Make sure the delete button, if explicitly specified, is sane
62  // @phan-suppress-next-line PhanTypeMismatchDimFetch Phan is very confused
63  if ( isset( $this->mParams['fields']['delete'] ) ) {
64  $class = 'mw-htmlform-cloner-delete-button';
65  $info = $this->mParams['fields']['delete'] + [
66  'formnovalidate' => true,
67  'cssclass' => $class
68  ];
69  unset( $info['name'], $info['class'] );
70 
71  if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
72  throw new MWException(
73  'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
74  );
75  }
76 
77  if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
78  $info['cssclass'] .= " $class";
79  }
80 
81  $this->mParams['fields']['delete'] = $info;
82  }
83  }
84 
92  protected function createFieldsForKey( $key ) {
93  $fields = [];
94  foreach ( $this->mParams['fields'] as $fieldname => $info ) {
95  $name = "{$this->mName}[$key][$fieldname]";
96  if ( isset( $info['name'] ) ) {
97  $info['name'] = "{$this->mName}[$key][{$info['name']}]";
98  } else {
99  $info['name'] = $name;
100  }
101  if ( isset( $info['id'] ) ) {
102  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--{$info['id']}" );
103  } else {
104  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" );
105  }
106  // Copy the hide-if rules to "child" fields, so that the JavaScript code handling them
107  // (resources/src/mediawiki/htmlform/hide-if.js) doesn't have to handle nested fields.
108  if ( $this->mHideIf ) {
109  if ( isset( $info['hide-if'] ) ) {
110  // Hide child field if either its rules say it's hidden, or parent's rules say it's hidden
111  $info['hide-if'] = [ 'OR', $info['hide-if'], $this->mHideIf ];
112  } else {
113  // Hide child field if parent's rules say it's hidden
114  $info['hide-if'] = $this->mHideIf;
115  }
116  }
117  $field = HTMLForm::loadInputFromParameters( $name, $info, $this->mParent );
118  $fields[$fieldname] = $field;
119  }
120  return $fields;
121  }
122 
131  protected function rekeyValuesArray( $key, $values ) {
132  $data = [];
133  foreach ( $values as $fieldname => $value ) {
134  $name = "{$this->mName}[$key][$fieldname]";
135  $data[$name] = $value;
136  }
137  return $data;
138  }
139 
140  protected function needsLabel() {
141  return false;
142  }
143 
144  public function loadDataFromRequest( $request ) {
145  // It's possible that this might be posted with no fields. Detect that
146  // by looking for an edit token.
147  if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
148  return $this->getDefault();
149  }
150 
151  $values = $request->getArray( $this->mName );
152  if ( $values === null ) {
153  $values = [];
154  }
155 
156  $ret = [];
157  foreach ( $values as $key => $value ) {
158  if ( $key === 'create' || isset( $value['delete'] ) ) {
159  $ret['nonjs'] = 1;
160  continue;
161  }
162 
163  // Add back in $request->getValues() so things that look for e.g.
164  // wpEditToken don't fail.
165  $data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
166 
167  $fields = $this->createFieldsForKey( $key );
168  $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
169  $row = [];
170  foreach ( $fields as $fieldname => $field ) {
171  if ( $field->skipLoadData( $subrequest ) ) {
172  continue;
173  } elseif ( !empty( $field->mParams['disabled'] ) ) {
174  $row[$fieldname] = $field->getDefault();
175  } else {
176  $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
177  }
178  }
179  $ret[] = $row;
180  }
181 
182  if ( isset( $values['create'] ) ) {
183  // Non-JS client clicked the "create" button.
184  $fields = $this->createFieldsForKey( $this->uniqueId );
185  $row = [];
186  foreach ( $fields as $fieldname => $field ) {
187  if ( !empty( $field->mParams['nodata'] ) ) {
188  continue;
189  } else {
190  $row[$fieldname] = $field->getDefault();
191  }
192  }
193  $ret[] = $row;
194  }
195 
196  return $ret;
197  }
198 
199  public function getDefault() {
200  $ret = parent::getDefault();
201 
202  // The default default is one entry with all subfields at their
203  // defaults.
204  if ( $ret === null ) {
205  $fields = $this->createFieldsForKey( $this->uniqueId );
206  $row = [];
207  foreach ( $fields as $fieldname => $field ) {
208  if ( !empty( $field->mParams['nodata'] ) ) {
209  continue;
210  } else {
211  $row[$fieldname] = $field->getDefault();
212  }
213  }
214  $ret = [ $row ];
215  }
216 
217  return $ret;
218  }
219 
224  public function cancelSubmit( $values, $alldata ) {
225  if ( isset( $values['nonjs'] ) ) {
226  return true;
227  }
228 
229  foreach ( $values as $key => $value ) {
230  $fields = $this->createFieldsForKey( $key );
231  foreach ( $fields as $fieldname => $field ) {
232  if ( !array_key_exists( $fieldname, $value ) ) {
233  continue;
234  }
235  if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
236  return true;
237  }
238  }
239  }
240 
241  return parent::cancelSubmit( $values, $alldata );
242  }
243 
248  public function validate( $values, $alldata ) {
249  if ( isset( $this->mParams['required'] )
250  && $this->mParams['required'] !== false
251  && !$values
252  ) {
253  return $this->msg( 'htmlform-cloner-required' );
254  }
255 
256  if ( isset( $values['nonjs'] ) ) {
257  // The submission was a non-JS create/delete click, so fail
258  // validation in case cancelSubmit() somehow didn't already handle
259  // it.
260  return false;
261  }
262 
263  foreach ( $values as $key => $value ) {
264  $fields = $this->createFieldsForKey( $key );
265  foreach ( $fields as $fieldname => $field ) {
266  if ( !array_key_exists( $fieldname, $value ) ) {
267  continue;
268  }
269  if ( $field->isHidden( $alldata ) ) {
270  continue;
271  }
272  $ok = $field->validate( $value[$fieldname], $alldata );
273  if ( $ok !== true ) {
274  return false;
275  }
276  }
277  }
278 
279  return parent::validate( $values, $alldata );
280  }
281 
289  protected function getInputHTMLForKey( $key, array $values ) {
290  $displayFormat = $this->mParams['format'] ?? $this->mParent->getDisplayFormat();
291 
292  // Conveniently, PHP method names are case-insensitive.
293  $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
294 
295  $html = '';
296  $hidden = '';
297  $hasLabel = false;
298 
299  $fields = $this->createFieldsForKey( $key );
300  foreach ( $fields as $fieldname => $field ) {
301  $v = array_key_exists( $fieldname, $values )
302  ? $values[$fieldname]
303  : $field->getDefault();
304 
305  if ( $field instanceof HTMLHiddenField ) {
306  // HTMLHiddenField doesn't generate its own HTML
307  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
308  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
309  } else {
310  $html .= $field->$getFieldHtmlMethod( $v );
311 
312  $labelValue = trim( $field->getLabel() );
313  if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
314  $hasLabel = true;
315  }
316  }
317  }
318 
319  if ( !isset( $fields['delete'] ) ) {
320  $field = $this->getDeleteButtonHtml( $key );
321 
322  if ( $displayFormat === 'table' ) {
323  $html .= $field->$getFieldHtmlMethod( $field->getDefault() );
324  } else {
325  $html .= $field->getInputHTML( $field->getDefault() );
326  }
327  }
328 
329  if ( $displayFormat !== 'raw' ) {
330  $classes = [
331  'mw-htmlform-cloner-row',
332  ];
333 
334  if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
335  $classes[] = 'mw-htmlform-nolabel';
336  }
337 
338  $attribs = [
339  'class' => implode( ' ', $classes ),
340  ];
341 
342  if ( $displayFormat === 'table' ) {
343  $html = Html::rawElement( 'table',
344  $attribs,
345  Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
346  } else {
347  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
348  }
349  }
350 
351  $html .= $hidden;
352 
353  if ( !empty( $this->mParams['row-legend'] ) ) {
354  $legend = $this->msg( $this->mParams['row-legend'] )->text();
355  $html = Xml::fieldset( $legend, $html );
356  }
357 
358  return $html;
359  }
360 
365  protected function getDeleteButtonHtml( $key ) : HTMLFormField {
366  $name = "{$this->mName}[$key][delete]";
367  $label = $this->mParams['delete-button-message'] ?? 'htmlform-cloner-delete';
368  $field = HTMLForm::loadInputFromParameters( $name, [
369  'type' => 'submit',
370  'formnovalidate' => true,
371  'name' => $name,
372  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
373  'cssclass' => 'mw-htmlform-cloner-delete-button',
374  'default' => $this->getMessage( $label )->text(),
375  ], $this->mParent );
376  return $field;
377  }
378 
379  protected function getCreateButtonHtml() : HTMLFormField {
380  $name = "{$this->mName}[create]";
381  $label = $this->mParams['create-button-message'] ?? 'htmlform-cloner-create';
382  return HTMLForm::loadInputFromParameters( $name, [
383  'type' => 'submit',
384  'formnovalidate' => true,
385  'name' => $name,
386  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
387  'cssclass' => 'mw-htmlform-cloner-create-button',
388  'default' => $this->getMessage( $label )->text(),
389  ], $this->mParent );
390  }
391 
392  public function getInputHTML( $values ) {
393  $html = '';
394 
395  foreach ( (array)$values as $key => $value ) {
396  if ( $key === 'nonjs' ) {
397  continue;
398  }
399  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
400  $this->getInputHTMLForKey( $key, $value )
401  );
402  }
403 
404  $template = $this->getInputHTMLForKey( $this->uniqueId, [] );
405  $html = Html::rawElement( 'ul', [
406  'id' => "mw-htmlform-cloner-list-{$this->mID}",
407  'class' => 'mw-htmlform-cloner-ul',
408  'data-template' => $template,
409  'data-unique-id' => $this->uniqueId,
410  ], $html );
411 
412  $field = $this->getCreateButtonHtml();
413  $html .= $field->getInputHTML( $field->getDefault() );
414 
415  return $html;
416  }
417 
425  protected function getInputOOUIForKey( $key, array $values ) {
426  $html = '';
427  $hidden = '';
428 
429  $fields = $this->createFieldsForKey( $key );
430  foreach ( $fields as $fieldname => $field ) {
431  $v = array_key_exists( $fieldname, $values )
432  ? $values[$fieldname]
433  : $field->getDefault();
434 
435  if ( $field instanceof HTMLHiddenField ) {
436  // HTMLHiddenField doesn't generate its own HTML
437  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
438  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
439  } else {
440  $html .= $field->getOOUI( $v );
441  }
442  }
443 
444  if ( !isset( $fields['delete'] ) ) {
445  $field = $this->getDeleteButtonHtml( $key );
446  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
447  $fieldHtml->setInfusable( true );
448 
449  $html .= $fieldHtml;
450  }
451 
452  $classes = [
453  'mw-htmlform-cloner-row',
454  ];
455 
456  $attribs = [
457  'class' => implode( ' ', $classes ),
458  ];
459 
460  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
461 
462  $html .= $hidden;
463 
464  if ( !empty( $this->mParams['row-legend'] ) ) {
465  $legend = $this->msg( $this->mParams['row-legend'] )->text();
466  $html = Xml::fieldset( $legend, $html );
467  }
468 
469  return $html;
470  }
471 
472  public function getInputOOUI( $values ) {
473  $html = '';
474 
475  foreach ( (array)$values as $key => $value ) {
476  if ( $key === 'nonjs' ) {
477  continue;
478  }
479  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
480  $this->getInputOOUIForKey( $key, $value )
481  );
482  }
483 
484  $template = $this->getInputOOUIForKey( $this->uniqueId, [] );
485  $html = Html::rawElement( 'ul', [
486  'id' => "mw-htmlform-cloner-list-{$this->mID}",
487  'class' => 'mw-htmlform-cloner-ul',
488  'data-template' => $template,
489  'data-unique-id' => $this->uniqueId,
490  ], $html );
491 
492  $field = $this->getCreateButtonHtml();
493  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
494  $fieldHtml->setInfusable( true );
495 
496  $html .= $fieldHtml;
497 
498  return $html;
499  }
500 }
DerivativeRequest
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
Definition: DerivativeRequest.php:36
HTMLFormFieldCloner\getDefault
getDefault()
Stable to override.
Definition: HTMLFormFieldCloner.php:199
Sanitizer\escapeIdForAttribute
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1072
HTMLFormField\getMessage
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
Definition: HTMLFormField.php:1196
HTMLFormFieldCloner\createFieldsForKey
createFieldsForKey( $key)
Create the HTMLFormFields that go inside this element, using the specified key.
Definition: HTMLFormFieldCloner.php:92
Xml\fieldset
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:613
HTMLFormFieldCloner\getDeleteButtonHtml
getDeleteButtonHtml( $key)
Definition: HTMLFormFieldCloner.php:365
HTMLFormFieldCloner\$counter
static $counter
Definition: HTMLFormFieldCloner.php:40
MWException
MediaWiki exception.
Definition: MWException.php:29
HTMLFormField
The parent class to generate form fields.
Definition: HTMLFormField.php:9
HTMLFormFieldCloner\getCreateButtonHtml
getCreateButtonHtml()
Definition: HTMLFormFieldCloner.php:379
HTMLForm\loadInputFromParameters
static loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent=null)
Initialise a new Object for the field Stable to override.
Definition: HTMLForm.php:523
HTMLFormFieldCloner\getInputOOUI
getInputOOUI( $values)
Same as getInputHTML, but returns an OOUI object.
Definition: HTMLFormFieldCloner.php:472
HTMLFormFieldCloner\rekeyValuesArray
rekeyValuesArray( $key, $values)
Re-key the specified values array to match the names applied by createFieldsForKey().
Definition: HTMLFormFieldCloner.php:131
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
HTMLFormFieldCloner\__construct
__construct( $params)
Stable to call Initialise the object.Stable to call Associative Array. See HTMLForm doc for syntax....
Definition: HTMLFormFieldCloner.php:53
HTMLFormFieldCloner\getInputOOUIForKey
getInputOOUIForKey( $key, array $values)
Get the input OOUI HTML for the specified key.
Definition: HTMLFormFieldCloner.php:425
HTMLFormFieldCloner\$uniqueId
string $uniqueId
String uniquely identifying this cloner instance and unlikely to exist otherwise in the generated HTM...
Definition: HTMLFormFieldCloner.php:47
HTMLHiddenField
Definition: HTMLHiddenField.php:6
HTMLFormFieldCloner\getInputHTMLForKey
getInputHTMLForKey( $key, array $values)
Get the input HTML for the specified key.
Definition: HTMLFormFieldCloner.php:289
HTMLFormFieldCloner\needsLabel
needsLabel()
Should this field have a label, or is there no input element with the appropriate id for the label to...
Definition: HTMLFormFieldCloner.php:140
HTMLFormFieldCloner\validate
validate( $values, $alldata)
Override this function to add specific validation checks on the field input.Don't forget to call pare...
Definition: HTMLFormFieldCloner.php:248
HTMLFormFieldCloner\loadDataFromRequest
loadDataFromRequest( $request)
Get the value that this input has been set to from a posted form, or the input's default value if it ...
Definition: HTMLFormFieldCloner.php:144
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
HTMLFormFieldCloner\getInputHTML
getInputHTML( $values)
This function must be implemented to return the HTML to generate the input object itself.
Definition: HTMLFormFieldCloner.php:392
HTMLFormField\$mHideIf
$mHideIf
Definition: HTMLFormField.php:28
HTMLFormField\msg
msg( $key,... $params)
Get a translated interface message.
Definition: HTMLFormField.php:87
HTMLFormFieldCloner\cancelSubmit
cancelSubmit( $values, $alldata)
Override this function if the control can somehow trigger a form submission that shouldn't actually s...
Definition: HTMLFormFieldCloner.php:224
HTMLFormFieldCloner
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
Definition: HTMLFormFieldCloner.php:39