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