4 * A container for HTMLFormFields that allows for multiple copies of the set of
5 * fields to be displayed to and entered by the user.
7 * Recognized parameters, besides the general ones, include:
8 * fields - HTMLFormField descriptors for the subfields this cloner manages.
9 * The format is just like for the HTMLForm. A field with key 'delete' is
10 * special: it must have type = submit and will serve to delete the group
12 * required - If specified, at least one group of fields must be submitted.
13 * format - HTMLForm display format to use when displaying the subfields:
14 * 'table', 'div', or 'raw'.
15 * row-legend - If non-empty, each group of subfields will be enclosed in a
16 * fieldset. The value is the name of a message key to use as the legend.
17 * create-button-message - Message to use as the text of the button to
18 * add an additional group of fields.
19 * delete-button-message - Message to use as the text of automatically-
20 * generated 'delete' button. Ignored if 'delete' is included in 'fields'.
22 * In the generated HTML, the subfields will be named along the lines of
23 * "clonerName[index][fieldname]", with ids "clonerId--index--fieldid". 'index'
24 * may be a number or an arbitrary string, and may likely change when the page
25 * is resubmitted. Cloners may be nested, resulting in field names along the
26 * lines of "cloner1Name[index1][cloner2Name][index2][fieldname]" and
29 * Use of cloner may result in submissions of the page that are not submissions
30 * of the HTMLForm, when non-JavaScript clients use the create or remove buttons.
32 * The result is an array, with values being arrays mapping subfield names to
33 * their values. On non-HTMLForm-submission page loads, there may also be
34 * additional (string) keys present with other types of values.
38 class HTMLFormFieldCloner
extends HTMLFormField
{
39 private static $counter = 0;
42 * @var string String uniquely identifying this cloner instance and
43 * unlikely to exist otherwise in the generated HTML, while still being
44 * valid as part of an HTML id.
48 public function __construct( $params ) {
49 $this->uniqueId
= get_class( $this ) . ++self
::$counter . 'x';
50 parent
::__construct( $params );
52 if ( empty( $this->mParams
['fields'] ) ||
!is_array( $this->mParams
['fields'] ) ) {
53 throw new MWException( 'HTMLFormFieldCloner called without any fields' );
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'] +
[
62 unset( $info['name'], $info['class'] );
64 if ( !isset( $info['type'] ) ||
$info['type'] !== 'submit' ) {
65 throw new MWException(
66 'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
70 if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
71 $info['cssclass'] .= " $class";
74 $this->mParams
['fields']['delete'] = $info;
79 * Create the HTMLFormFields that go inside this element, using the
82 * @param string $key Array key under which these fields should be named
83 * @return HTMLFormField[]
85 protected function createFieldsForKey( $key ) {
87 foreach ( $this->mParams
['fields'] as $fieldname => $info ) {
88 $name = "{$this->mName}[$key][$fieldname]";
89 if ( isset( $info['name'] ) ) {
90 $info['name'] = "{$this->mName}[$key][{$info['name']}]";
92 $info['name'] = $name;
94 if ( isset( $info['id'] ) ) {
95 $info['id'] = Sanitizer
::escapeId( "{$this->mID}--$key--{$info['id']}" );
97 $info['id'] = Sanitizer
::escapeId( "{$this->mID}--$key--$fieldname" );
99 $field = HTMLForm
::loadInputFromParameters( $name, $info, $this->mParent
);
100 $fields[$fieldname] = $field;
106 * Re-key the specified values array to match the names applied by
107 * createFieldsForKey().
109 * @param string $key Array key under which these fields should be named
110 * @param array $values Values array from the request
113 protected function rekeyValuesArray( $key, $values ) {
115 foreach ( $values as $fieldname => $value ) {
116 $name = "{$this->mName}[$key][$fieldname]";
117 $data[$name] = $value;
122 protected function needsLabel() {
126 public function loadDataFromRequest( $request ) {
127 // It's possible that this might be posted with no fields. Detect that
128 // by looking for an edit token.
129 if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName
) === null ) {
130 return $this->getDefault();
133 $values = $request->getArray( $this->mName
);
134 if ( $values === null ) {
139 foreach ( $values as $key => $value ) {
140 if ( $key === 'create' ||
isset( $value['delete'] ) ) {
145 // Add back in $request->getValues() so things that look for e.g.
146 // wpEditToken don't fail.
147 $data = $this->rekeyValuesArray( $key, $value ) +
$request->getValues();
149 $fields = $this->createFieldsForKey( $key );
150 $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
152 foreach ( $fields as $fieldname => $field ) {
153 if ( $field->skipLoadData( $subrequest ) ) {
155 } elseif ( !empty( $field->mParams
['disabled'] ) ) {
156 $row[$fieldname] = $field->getDefault();
158 $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
164 if ( isset( $values['create'] ) ) {
165 // Non-JS client clicked the "create" button.
166 $fields = $this->createFieldsForKey( $this->uniqueId
);
168 foreach ( $fields as $fieldname => $field ) {
169 if ( !empty( $field->mParams
['nodata'] ) ) {
172 $row[$fieldname] = $field->getDefault();
181 public function getDefault() {
182 $ret = parent
::getDefault();
184 // The default default is one entry with all subfields at their
186 if ( $ret === null ) {
187 $fields = $this->createFieldsForKey( $this->uniqueId
);
189 foreach ( $fields as $fieldname => $field ) {
190 if ( !empty( $field->mParams
['nodata'] ) ) {
193 $row[$fieldname] = $field->getDefault();
202 public function cancelSubmit( $values, $alldata ) {
203 if ( isset( $values['nonjs'] ) ) {
207 foreach ( $values as $key => $value ) {
208 $fields = $this->createFieldsForKey( $key );
209 foreach ( $fields as $fieldname => $field ) {
210 if ( !array_key_exists( $fieldname, $value ) ) {
213 if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
219 return parent
::cancelSubmit( $values, $alldata );
222 public function validate( $values, $alldata ) {
223 if ( isset( $this->mParams
['required'] )
224 && $this->mParams
['required'] !== false
227 return $this->msg( 'htmlform-cloner-required' )->parseAsBlock();
230 if ( isset( $values['nonjs'] ) ) {
231 // The submission was a non-JS create/delete click, so fail
232 // validation in case cancelSubmit() somehow didn't already handle
237 foreach ( $values as $key => $value ) {
238 $fields = $this->createFieldsForKey( $key );
239 foreach ( $fields as $fieldname => $field ) {
240 if ( !array_key_exists( $fieldname, $value ) ) {
243 $ok = $field->validate( $value[$fieldname], $alldata );
244 if ( $ok !== true ) {
250 return parent
::validate( $values, $alldata );
254 * Get the input HTML for the specified key.
256 * @param string $key Array key under which the fields should be named
257 * @param array $values
260 protected function getInputHTMLForKey( $key, $values ) {
261 $displayFormat = isset( $this->mParams
['format'] )
262 ?
$this->mParams
['format']
263 : $this->mParent
->getDisplayFormat();
265 // Conveniently, PHP method names are case-insensitive.
266 $getFieldHtmlMethod = $displayFormat == 'table' ?
'getTableRow' : ( 'get' . $displayFormat );
272 $fields = $this->createFieldsForKey( $key );
273 foreach ( $fields as $fieldname => $field ) {
274 $v = array_key_exists( $fieldname, $values )
275 ?
$values[$fieldname]
276 : $field->getDefault();
278 if ( $field instanceof HTMLHiddenField
) {
279 // HTMLHiddenField doesn't generate its own HTML
280 list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
281 $hidden .= Html
::hidden( $name, $value, $params ) . "\n";
283 $html .= $field->$getFieldHtmlMethod( $v );
285 $labelValue = trim( $field->getLabel() );
286 if ( $labelValue != ' ' && $labelValue !== '' ) {
292 if ( !isset( $fields['delete'] ) ) {
293 $name = "{$this->mName}[$key][delete]";
294 $label = isset( $this->mParams
['delete-button-message'] )
295 ?
$this->mParams
['delete-button-message']
296 : 'htmlform-cloner-delete';
297 $field = HTMLForm
::loadInputFromParameters( $name, [
300 'id' => Sanitizer
::escapeId( "{$this->mID}--$key--delete" ),
301 'cssclass' => 'mw-htmlform-cloner-delete-button',
302 'default' => $this->getMessage( $label )->text(),
304 $v = $field->getDefault();
306 if ( $displayFormat === 'table' ) {
307 $html .= $field->$getFieldHtmlMethod( $v );
309 $html .= $field->getInputHTML( $v );
313 if ( $displayFormat !== 'raw' ) {
315 'mw-htmlform-cloner-row',
318 if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
319 $classes[] = 'mw-htmlform-nolabel';
323 'class' => implode( ' ', $classes ),
326 if ( $displayFormat === 'table' ) {
327 $html = Html
::rawElement( 'table',
329 Html
::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
331 $html = Html
::rawElement( 'div', $attribs, "\n$html\n" );
337 if ( !empty( $this->mParams
['row-legend'] ) ) {
338 $legend = $this->msg( $this->mParams
['row-legend'] )->text();
339 $html = Xml
::fieldset( $legend, $html );
345 public function getInputHTML( $values ) {
348 foreach ( (array)$values as $key => $value ) {
349 if ( $key === 'nonjs' ) {
352 $html .= Html
::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
353 $this->getInputHTMLForKey( $key, $value )
357 $template = $this->getInputHTMLForKey( $this->uniqueId
, null );
358 $html = Html
::rawElement( 'ul', [
359 'id' => "mw-htmlform-cloner-list-{$this->mID}",
360 'class' => 'mw-htmlform-cloner-ul',
361 'data-template' => $template,
362 'data-unique-id' => $this->uniqueId
,
365 $name = "{$this->mName}[create]";
366 $label = isset( $this->mParams
['create-button-message'] )
367 ?
$this->mParams
['create-button-message']
368 : 'htmlform-cloner-create';
369 $field = HTMLForm
::loadInputFromParameters( $name, [
372 'id' => Sanitizer
::escapeId( "{$this->mID}--create" ),
373 'cssclass' => 'mw-htmlform-cloner-create-button',
374 'default' => $this->getMessage( $label )->text(),
376 $html .= $field->getInputHTML( $field->getDefault() );