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