MediaWiki master
HTMLFormFieldCloner.php
Go to the documentation of this file.
1<?php
2
4
5use InvalidArgumentException;
12
50 private static $counter = 0;
51
57 protected $uniqueId;
58
60 protected $mFields = [];
61
66 public function __construct( $params ) {
67 $this->uniqueId = $this->getClassName() . ++self::$counter . 'x';
68 parent::__construct( $params );
69
70 if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
71 throw new InvalidArgumentException( 'HTMLFormFieldCloner called without any fields' );
72 }
73
74 // Make sure the delete button, if explicitly specified, is sensible
75 if ( isset( $this->mParams['fields']['delete'] ) ) {
76 $class = 'mw-htmlform-cloner-delete-button';
77 $info = $this->mParams['fields']['delete'] + [
78 'formnovalidate' => true,
79 'cssclass' => $class
80 ];
81 unset( $info['name'], $info['class'] );
82
83 if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
84 throw new InvalidArgumentException(
85 'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
86 );
87 }
88
89 if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
90 $info['cssclass'] .= " $class";
91 }
92
93 $this->mParams['fields']['delete'] = $info;
94 }
95 }
96
101 protected function getFieldsForKey( $key ) {
102 if ( !isset( $this->mFields[$key] ) ) {
103 $this->mFields[$key] = $this->createFieldsForKey( $key );
104 }
105 return $this->mFields[$key];
106 }
107
115 protected function createFieldsForKey( $key ) {
116 $fields = [];
117 foreach ( $this->mParams['fields'] as $fieldname => $info ) {
118 $name = "{$this->mName}[$key][$fieldname]";
119 if ( isset( $info['name'] ) ) {
120 $info['name'] = "{$this->mName}[$key][{$info['name']}]";
121 } else {
122 $info['name'] = $name;
123 }
124 if ( isset( $info['id'] ) ) {
125 $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--{$info['id']}" );
126 } else {
127 $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" );
128 }
129 // Copy the hide-if and disable-if rules to "child" fields, so that the JavaScript code handling them
130 // (resources/src/mediawiki.htmlform/cond-state.js) doesn't have to handle nested fields.
131 if ( $this->mCondState ) {
132 foreach ( [ 'hide', 'disable' ] as $type ) {
133 if ( !isset( $this->mCondState[$type] ) ) {
134 continue;
135 }
136 $param = $type . '-if';
137 if ( isset( $info[$param] ) ) {
138 // Hide or disable child field if either its rules say so, or parent's rules say so.
139 $info[$param] = [ 'OR', $info[$param], $this->mCondState[$type] ];
140 } else {
141 // Hide or disable child field if parent's rules say so.
142 $info[$param] = $this->mCondState[$type];
143 }
144 }
145 }
146 $cloner = $this;
147 $info['cloner'] = &$cloner;
148 $info['cloner-key'] = $key;
149 $field = HTMLForm::loadInputFromParameters( $fieldname, $info, $this->mParent );
150 $fields[$fieldname] = $field;
151 }
152 return $fields;
153 }
154
163 protected function rekeyValuesArray( $key, $values ) {
164 $data = [];
165 foreach ( $values as $fieldname => $value ) {
166 $name = "{$this->mName}[$key][$fieldname]";
167 $data[$name] = $value;
168 }
169 return $data;
170 }
171
176 protected function parseFieldPath( $name ) {
177 $fieldKeys = [];
178 while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $name, $m ) ) {
179 array_unshift( $fieldKeys, $m[2] );
180 $name = $m[1];
181 }
182 array_unshift( $fieldKeys, $name );
183 return $fieldKeys;
184 }
185
194 public function findNearestField( $field, $find ) {
195 $findPath = $this->parseFieldPath( $find );
196 // Access to fields as child or in other group is not allowed.
197 // Further support for a more complicated path may conduct here.
198 if ( count( $findPath ) > 1 ) {
199 return null;
200 }
201 if ( !isset( $this->mParams['fields'][$find] ) ) {
202 $cloner = $this->mParams['cloner'] ?? null;
203 if ( $cloner instanceof self ) {
204 return $cloner->findNearestField( $this, $find );
205 }
206 return null;
207 }
208 $fields = $this->getFieldsForKey( $field->mParams['cloner-key'] );
209 return $fields[$find];
210 }
211
216 protected function getFieldPath( $field ) {
217 $path = [ $this->mParams['fieldname'], $field->mParams['cloner-key'] ];
218 $cloner = $this->mParams['cloner'] ?? null;
219 if ( $cloner instanceof self ) {
220 $path = array_merge( $cloner->getFieldPath( $this ), $path );
221 }
222 return $path;
223 }
224
232 public function extractFieldData( $field, $alldata ) {
233 foreach ( $this->getFieldPath( $field ) as $key ) {
234 $alldata = $alldata[$key];
235 }
236 return $alldata[$field->mParams['fieldname']];
237 }
238
239 protected function needsLabel() {
240 return false;
241 }
242
243 public function loadDataFromRequest( $request ) {
244 // It's possible that this might be posted with no fields. Detect that
245 // by looking for an edit token.
246 if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
247 return $this->getDefault();
248 }
249
250 $values = $request->getArray( $this->mName ) ?? [];
251
252 $ret = [];
253 foreach ( $values as $key => $value ) {
254 if ( $key === 'create' || isset( $value['delete'] ) ) {
255 $ret['nonjs'] = 1;
256 continue;
257 }
258
259 // Add back in $request->getValues() so things that look for e.g.
260 // wpEditToken don't fail.
261 $data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
262
263 $fields = $this->getFieldsForKey( $key );
264 $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
265 $row = [];
266 foreach ( $fields as $fieldname => $field ) {
267 if ( $field->skipLoadData( $subrequest ) ) {
268 continue;
269 }
270 if ( !empty( $field->mParams['disabled'] ) ) {
271 $row[$fieldname] = $field->getDefault();
272 } else {
273 $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
274 }
275 }
276 $ret[] = $row;
277 }
278
279 if ( isset( $values['create'] ) ) {
280 // Non-JS client clicked the "create" button.
281 $fields = $this->getFieldsForKey( $this->uniqueId );
282 $row = [];
283 foreach ( $fields as $fieldname => $field ) {
284 if ( !empty( $field->mParams['nodata'] ) ) {
285 continue;
286 }
287 $row[$fieldname] = $field->getDefault();
288 }
289 $ret[] = $row;
290 }
291
292 return $ret;
293 }
294
295 public function getDefault() {
296 $ret = parent::getDefault();
297
298 // The default is one entry with all subfields at their defaults.
299 if ( $ret === null ) {
300 $fields = $this->getFieldsForKey( $this->uniqueId );
301 $row = [];
302 foreach ( $fields as $fieldname => $field ) {
303 if ( !empty( $field->mParams['nodata'] ) ) {
304 continue;
305 }
306 $row[$fieldname] = $field->getDefault();
307 }
308 $ret = [ $row ];
309 }
310
311 return $ret;
312 }
313
318 public function cancelSubmit( $values, $alldata ) {
319 if ( isset( $values['nonjs'] ) ) {
320 return true;
321 }
322
323 foreach ( $values as $key => $value ) {
324 $fields = $this->getFieldsForKey( $key );
325 foreach ( $fields as $fieldname => $field ) {
326 if ( !array_key_exists( $fieldname, $value ) ) {
327 continue;
328 }
329 if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
330 return true;
331 }
332 }
333 }
334
335 return parent::cancelSubmit( $values, $alldata );
336 }
337
342 public function validate( $values, $alldata ) {
343 if ( isset( $this->mParams['required'] )
344 && $this->mParams['required'] !== false
345 && !$values
346 ) {
347 return $this->msg( 'htmlform-cloner-required' );
348 }
349
350 if ( isset( $values['nonjs'] ) ) {
351 // The submission was a non-JS create/delete click, so fail
352 // validation in case cancelSubmit() somehow didn't already handle
353 // it.
354 return false;
355 }
356
357 foreach ( $values as $key => $value ) {
358 $fields = $this->getFieldsForKey( $key );
359 foreach ( $fields as $fieldname => $field ) {
360 if ( !array_key_exists( $fieldname, $value ) || $field->isHidden( $alldata ) ) {
361 continue;
362 }
363 $ok = $field->validate( $value[$fieldname], $alldata );
364 if ( $ok !== true ) {
365 return false;
366 }
367 }
368 }
369
370 return parent::validate( $values, $alldata );
371 }
372
380 protected function getInputHTMLForKey( $key, array $values ) {
381 $displayFormat = $this->mParams['format'] ?? $this->mParent->getDisplayFormat();
382
383 // Conveniently, PHP method names are case-insensitive.
384 $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
385
386 $html = '';
387 $hidden = '';
388 $hasLabel = false;
389
390 $fields = $this->getFieldsForKey( $key );
391 foreach ( $fields as $fieldname => $field ) {
392 $v = array_key_exists( $fieldname, $values )
393 ? $values[$fieldname]
394 : $field->getDefault();
395
396 if ( $field instanceof HTMLHiddenField ) {
397 // HTMLHiddenField doesn't generate its own HTML
398 [ $name, $value, $params ] = $field->getHiddenFieldData( $v );
399 $hidden .= Html::hidden( $name, $value, $params ) . "\n";
400 } else {
401 $html .= $field->$getFieldHtmlMethod( $v );
402
403 $labelValue = trim( $field->getLabel() );
404 if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
405 $hasLabel = true;
406 }
407 }
408 }
409
410 if ( !isset( $fields['delete'] ) ) {
411 $field = $this->getDeleteButtonHtml( $key );
412
413 if ( $displayFormat === 'table' ) {
414 $html .= $field->$getFieldHtmlMethod( $field->getDefault() );
415 } else {
416 $html .= $field->getInputHTML( $field->getDefault() );
417 }
418 }
419
420 if ( $displayFormat !== 'raw' ) {
421 $classes = [ 'mw-htmlform-cloner-row' ];
422
423 if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
424 $classes[] = 'mw-htmlform-nolabel';
425 }
426
427 $attribs = [ 'class' => $classes ];
428
429 if ( $displayFormat === 'table' ) {
430 $html = Html::rawElement( 'table',
431 $attribs,
432 Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
433 } else {
434 $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
435 }
436 }
437
438 $html .= $hidden;
439
440 if ( !empty( $this->mParams['row-legend'] ) ) {
441 $legend = $this->msg( $this->mParams['row-legend'] )->text();
442 $html = Xml::fieldset( $legend, $html );
443 }
444
445 return $html;
446 }
447
452 protected function getDeleteButtonHtml( $key ): HTMLFormField {
453 $name = "{$this->mName}[$key][delete]";
454 $label = $this->mParams['delete-button-message'] ?? 'htmlform-cloner-delete';
455 $field = HTMLForm::loadInputFromParameters( $name, [
456 'type' => 'submit',
457 'formnovalidate' => true,
458 'name' => $name,
459 'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
460 'cssclass' => 'mw-htmlform-cloner-delete-button',
461 'default' => $this->getMessage( $label )->text(),
462 'disabled' => $this->mParams['disabled'] ?? false,
463 ], $this->mParent );
464 return $field;
465 }
466
467 protected function getCreateButtonHtml(): HTMLFormField {
468 $name = "{$this->mName}[create]";
469 $label = $this->mParams['create-button-message'] ?? 'htmlform-cloner-create';
470 return HTMLForm::loadInputFromParameters( $name, [
471 'type' => 'submit',
472 'formnovalidate' => true,
473 'name' => $name,
474 'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
475 'cssclass' => 'mw-htmlform-cloner-create-button',
476 'default' => $this->getMessage( $label )->text(),
477 'disabled' => $this->mParams['disabled'] ?? false,
478 ], $this->mParent );
479 }
480
481 public function getInputHTML( $values ) {
482 $html = '';
483
484 foreach ( (array)$values as $key => $value ) {
485 if ( $key === 'nonjs' ) {
486 continue;
487 }
488 $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
489 $this->getInputHTMLForKey( $key, $value )
490 );
491 }
492
493 $template = $this->getInputHTMLForKey( $this->uniqueId, [] );
494 $html = Html::rawElement( 'ul', [
495 'id' => "mw-htmlform-cloner-list-{$this->mID}",
496 'class' => 'mw-htmlform-cloner-ul',
497 'data-template' => $template,
498 'data-unique-id' => $this->uniqueId,
499 ], $html );
500
501 $field = $this->getCreateButtonHtml();
502 $html .= $field->getInputHTML( $field->getDefault() );
503
504 return $html;
505 }
506
514 protected function getInputOOUIForKey( $key, array $values ) {
515 $html = '';
516 $hidden = '';
517
518 $fields = $this->getFieldsForKey( $key );
519 foreach ( $fields as $fieldname => $field ) {
520 $v = array_key_exists( $fieldname, $values )
521 ? $values[$fieldname]
522 : $field->getDefault();
523
524 if ( $field instanceof HTMLHiddenField ) {
525 // HTMLHiddenField doesn't generate its own HTML
526 [ $name, $value, $params ] = $field->getHiddenFieldData( $v );
527 $hidden .= Html::hidden( $name, $value, $params ) . "\n";
528 } else {
529 $html .= $field->getOOUI( $v );
530 }
531 }
532
533 if ( !isset( $fields['delete'] ) ) {
534 $field = $this->getDeleteButtonHtml( $key );
535 $fieldHtml = $field->getInputOOUI( $field->getDefault() );
536 $fieldHtml->setInfusable( true );
537
538 $html .= $fieldHtml;
539 }
540
541 $html = Html::rawElement( 'div', [ 'class' => 'mw-htmlform-cloner-row' ], "\n$html\n" );
542
543 $html .= $hidden;
544
545 if ( !empty( $this->mParams['row-legend'] ) ) {
546 $legend = $this->msg( $this->mParams['row-legend'] )->text();
547 $html = Xml::fieldset( $legend, $html );
548 }
549
550 return $html;
551 }
552
553 public function getInputOOUI( $values ) {
554 $html = '';
555
556 foreach ( (array)$values as $key => $value ) {
557 if ( $key === 'nonjs' ) {
558 continue;
559 }
560 $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
561 $this->getInputOOUIForKey( $key, $value )
562 );
563 }
564
565 $template = $this->getInputOOUIForKey( $this->uniqueId, [] );
566 $html = Html::rawElement( 'ul', [
567 'id' => "mw-htmlform-cloner-list-{$this->mID}",
568 'class' => 'mw-htmlform-cloner-ul',
569 'data-template' => $template,
570 'data-unique-id' => $this->uniqueId,
571 ], $html );
572
573 $field = $this->getCreateButtonHtml();
574 $fieldHtml = $field->getInputOOUI( $field->getDefault() );
575 $fieldHtml->setInfusable( true );
576
577 $html .= $fieldHtml;
578
579 return $html;
580 }
581}
582
584class_alias( HTMLFormFieldCloner::class, 'HTMLFormFieldCloner' );
array $params
The job parameters.
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
needsLabel()
Should this field have a label, or is there no input element with the appropriate id for the label to...
extractFieldData( $field, $alldata)
Extract field data for a given field that belongs to this cloner.
getInputHTML( $values)
This function must be implemented to return the HTML to generate the input object itself.
if(empty( $this->mParams['fields'])||!is_array( $this->mParams['fields'])) if(isset($this->mParams[ 'fields'][ 'delete'])) getFieldsForKey( $key)
cancelSubmit( $values, $alldata)
Override this function if the control can somehow trigger a form submission that shouldn't actually s...
getInputOOUIForKey( $key, array $values)
Get the input OOUI HTML for the specified key.
getInputOOUI( $values)
Same as getInputHTML, but returns an OOUI object.
getInputHTMLForKey( $key, array $values)
Get the input HTML for the specified key.
rekeyValuesArray( $key, $values)
Re-key the specified values array to match the names applied by createFieldsForKey().
array< string, $mFields=[];public function __construct($params) { $this-> uniqueId
HTMLFormField[]>
findNearestField( $field, $find)
Find the nearest field to a field in this cloner matched the given name, walk through the chain of cl...
validate( $values, $alldata)
Override this function to add specific validation checks on the field input.Don't forget to call pare...
createFieldsForKey( $key)
Create the HTMLFormFields that go inside this element, using the specified key.
loadDataFromRequest( $request)
Get the value that this input has been set to from a posted form, or the input's default value if it ...
string $uniqueId
String uniquely identifying this cloner instance and unlikely to exist otherwise in the generated HTM...
The parent class to generate form fields.
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
__construct( $params)
Initialise the object.
getClassName()
Gets the non namespaced class name.
msg( $key,... $params)
Get a translated interface message.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:206
static loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent=null)
Initialise a new Object for the field.
Definition HTMLForm.php:604
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
Module of static functions for generating XML.
Definition Xml.php:37