2014-03-07 16:03:32 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A container for HTMLFormFields that allows for multiple copies of the set of
|
|
|
|
|
* fields to be displayed to and entered by the user.
|
|
|
|
|
*
|
|
|
|
|
* Recognized parameters, besides the general ones, include:
|
|
|
|
|
* fields - HTMLFormField descriptors for the subfields this cloner manages.
|
|
|
|
|
* The format is just like for the HTMLForm. A field with key 'delete' is
|
|
|
|
|
* special: it must have type = submit and will serve to delete the group
|
|
|
|
|
* of fields.
|
|
|
|
|
* required - If specified, at least one group of fields must be submitted.
|
|
|
|
|
* format - HTMLForm display format to use when displaying the subfields:
|
|
|
|
|
* 'table', 'div', or 'raw'.
|
|
|
|
|
* row-legend - If non-empty, each group of subfields will be enclosed in a
|
|
|
|
|
* fieldset. The value is the name of a message key to use as the legend.
|
|
|
|
|
* create-button-message - Message key to use as the text of the button to
|
|
|
|
|
* add an additional group of fields.
|
|
|
|
|
* delete-button-message - Message key to use as the text of automatically-
|
|
|
|
|
* generated 'delete' button. Ignored if 'delete' is included in 'fields'.
|
|
|
|
|
*
|
|
|
|
|
* In the generated HTML, the subfields will be named along the lines of
|
|
|
|
|
* "clonerName[index][fieldname]", with ids "clonerId--index--fieldid". 'index'
|
|
|
|
|
* may be a number or an arbitrary string, and may likely change when the page
|
|
|
|
|
* is resubmitted. Cloners may be nested, resulting in field names along the
|
|
|
|
|
* lines of "cloner1Name[index1][cloner2Name][index2][fieldname]" and
|
|
|
|
|
* corresponding ids.
|
|
|
|
|
*
|
|
|
|
|
* Use of cloner may result in submissions of the page that are not submissions
|
|
|
|
|
* of the HTMLForm, when non-JavaScript clients use the create or remove buttons.
|
|
|
|
|
*
|
|
|
|
|
* The result is an array, with values being arrays mapping subfield names to
|
|
|
|
|
* their values. On non-HTMLForm-submission page loads, there may also be
|
|
|
|
|
* additional (string) keys present with other types of values.
|
|
|
|
|
*
|
|
|
|
|
* @since 1.23
|
|
|
|
|
*/
|
|
|
|
|
class HTMLFormFieldCloner extends HTMLFormField {
|
|
|
|
|
private static $counter = 0;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string String uniquely identifying this cloner instance and
|
|
|
|
|
* unlikely to exist otherwise in the generated HTML, while still being
|
|
|
|
|
* valid as part of an HTML id.
|
|
|
|
|
*/
|
|
|
|
|
protected $uniqueId;
|
|
|
|
|
|
|
|
|
|
public function __construct( $params ) {
|
|
|
|
|
$this->uniqueId = get_class( $this ) . ++self::$counter . 'x';
|
|
|
|
|
parent::__construct( $params );
|
|
|
|
|
|
|
|
|
|
if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
|
|
|
|
|
throw new MWException( 'HTMLFormFieldCloner called without any fields' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure the delete button, if explicitly specified, is sane
|
|
|
|
|
if ( isset( $this->mParams['fields']['delete'] ) ) {
|
|
|
|
|
$class = 'mw-htmlform-cloner-delete-button';
|
|
|
|
|
$info = $this->mParams['fields']['delete'] + array(
|
|
|
|
|
'cssclass' => $class
|
|
|
|
|
);
|
|
|
|
|
unset( $info['name'], $info['class'] );
|
|
|
|
|
|
|
|
|
|
if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
|
|
|
|
|
throw new MWException(
|
|
|
|
|
'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
|
|
|
|
|
$info['cssclass'] .= " $class";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->mParams['fields']['delete'] = $info;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create the HTMLFormFields that go inside this element, using the
|
|
|
|
|
* specified key.
|
|
|
|
|
*
|
|
|
|
|
* @param string $key Array key under which these fields should be named
|
2014-09-11 16:47:26 +00:00
|
|
|
* @return HTMLFormField[]
|
2014-03-07 16:03:32 +00:00
|
|
|
*/
|
|
|
|
|
protected function createFieldsForKey( $key ) {
|
|
|
|
|
$fields = array();
|
|
|
|
|
foreach ( $this->mParams['fields'] as $fieldname => $info ) {
|
|
|
|
|
$name = "{$this->mName}[$key][$fieldname]";
|
|
|
|
|
if ( isset( $info['name'] ) ) {
|
|
|
|
|
$info['name'] = "{$this->mName}[$key][{$info['name']}]";
|
|
|
|
|
} else {
|
|
|
|
|
$info['name'] = $name;
|
|
|
|
|
}
|
|
|
|
|
if ( isset( $info['id'] ) ) {
|
|
|
|
|
$info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--{$info['id']}" );
|
|
|
|
|
} else {
|
|
|
|
|
$info['id'] = Sanitizer::escapeId( "{$this->mID}--$key--$fieldname" );
|
|
|
|
|
}
|
|
|
|
|
$field = HTMLForm::loadInputFromParameters( $name, $info );
|
|
|
|
|
$field->mParent = $this->mParent;
|
|
|
|
|
$fields[$fieldname] = $field;
|
|
|
|
|
}
|
|
|
|
|
return $fields;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Re-key the specified values array to match the names applied by
|
|
|
|
|
* createFieldsForKey().
|
|
|
|
|
*
|
|
|
|
|
* @param string $key Array key under which these fields should be named
|
|
|
|
|
* @param array $values Values array from the request
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function rekeyValuesArray( $key, $values ) {
|
|
|
|
|
$data = array();
|
|
|
|
|
foreach ( $values as $fieldname => $value ) {
|
|
|
|
|
$name = "{$this->mName}[$key][$fieldname]";
|
|
|
|
|
$data[$name] = $value;
|
|
|
|
|
}
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function needsLabel() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadDataFromRequest( $request ) {
|
|
|
|
|
// It's possible that this might be posted with no fields. Detect that
|
|
|
|
|
// by looking for an edit token.
|
|
|
|
|
if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
|
|
|
|
|
return $this->getDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$values = $request->getArray( $this->mName );
|
|
|
|
|
if ( $values === null ) {
|
|
|
|
|
$values = array();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ret = array();
|
|
|
|
|
foreach ( $values as $key => $value ) {
|
|
|
|
|
if ( $key === 'create' || isset( $value['delete'] ) ) {
|
|
|
|
|
$ret['nonjs'] = 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add back in $request->getValues() so things that look for e.g.
|
|
|
|
|
// wpEditToken don't fail.
|
|
|
|
|
$data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
|
|
|
|
|
|
|
|
|
|
$fields = $this->createFieldsForKey( $key );
|
|
|
|
|
$subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
|
|
|
|
|
$row = array();
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
if ( !empty( $field->mParams['nodata'] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
} elseif ( !empty( $field->mParams['disabled'] ) ) {
|
|
|
|
|
$row[$fieldname] = $field->getDefault();
|
|
|
|
|
} else {
|
|
|
|
|
$row[$fieldname] = $field->loadDataFromRequest( $subrequest );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$ret[] = $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $values['create'] ) ) {
|
|
|
|
|
// Non-JS client clicked the "create" button.
|
|
|
|
|
$fields = $this->createFieldsForKey( $this->uniqueId );
|
|
|
|
|
$row = array();
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
if ( !empty( $field->mParams['nodata'] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
} else {
|
|
|
|
|
$row[$fieldname] = $field->getDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$ret[] = $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getDefault() {
|
|
|
|
|
$ret = parent::getDefault();
|
|
|
|
|
|
|
|
|
|
// The default default is one entry with all subfields at their
|
|
|
|
|
// defaults.
|
|
|
|
|
if ( $ret === null ) {
|
|
|
|
|
$fields = $this->createFieldsForKey( $this->uniqueId );
|
|
|
|
|
$row = array();
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
if ( !empty( $field->mParams['nodata'] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
} else {
|
|
|
|
|
$row[$fieldname] = $field->getDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$ret = array( $row );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function cancelSubmit( $values, $alldata ) {
|
|
|
|
|
if ( isset( $values['nonjs'] ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $values as $key => $value ) {
|
|
|
|
|
$fields = $this->createFieldsForKey( $key );
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
if ( !empty( $field->mParams['nodata'] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parent::cancelSubmit( $values, $alldata );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function validate( $values, $alldata ) {
|
|
|
|
|
if ( isset( $this->mParams['required'] )
|
|
|
|
|
&& $this->mParams['required'] !== false
|
|
|
|
|
&& !$values
|
|
|
|
|
) {
|
|
|
|
|
return $this->msg( 'htmlform-cloner-required' )->parseAsBlock();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $values['nonjs'] ) ) {
|
|
|
|
|
// The submission was a non-JS create/delete click, so fail
|
|
|
|
|
// validation in case cancelSubmit() somehow didn't already handle
|
|
|
|
|
// it.
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ( $values as $key => $value ) {
|
|
|
|
|
$fields = $this->createFieldsForKey( $key );
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
if ( !empty( $field->mParams['nodata'] ) ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$ok = $field->validate( $value[$fieldname], $alldata );
|
|
|
|
|
if ( $ok !== true ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parent::validate( $values, $alldata );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the input HTML for the specified key.
|
|
|
|
|
*
|
|
|
|
|
* @param string $key Array key under which the fields should be named
|
|
|
|
|
* @param array $values
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getInputHTMLForKey( $key, $values ) {
|
|
|
|
|
$displayFormat = isset( $this->mParams['format'] )
|
|
|
|
|
? $this->mParams['format']
|
|
|
|
|
: $this->mParent->getDisplayFormat();
|
|
|
|
|
|
|
|
|
|
switch ( $displayFormat ) {
|
|
|
|
|
case 'table':
|
|
|
|
|
$getFieldHtmlMethod = 'getTableRow';
|
|
|
|
|
break;
|
|
|
|
|
case 'vform':
|
|
|
|
|
// Close enough to a div.
|
|
|
|
|
$getFieldHtmlMethod = 'getDiv';
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
$getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$html = '';
|
|
|
|
|
$hasLabel = false;
|
|
|
|
|
|
|
|
|
|
$fields = $this->createFieldsForKey( $key );
|
|
|
|
|
foreach ( $fields as $fieldname => $field ) {
|
|
|
|
|
$v = ( empty( $field->mParams['nodata'] ) && $values !== null )
|
|
|
|
|
? $values[$fieldname]
|
|
|
|
|
: $field->getDefault();
|
|
|
|
|
$html .= $field->$getFieldHtmlMethod( $v );
|
|
|
|
|
|
|
|
|
|
$labelValue = trim( $field->getLabel() );
|
|
|
|
|
if ( $labelValue != ' ' && $labelValue !== '' ) {
|
|
|
|
|
$hasLabel = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $fields['delete'] ) ) {
|
|
|
|
|
$name = "{$this->mName}[$key][delete]";
|
|
|
|
|
$label = isset( $this->mParams['delete-button-message'] )
|
|
|
|
|
? $this->mParams['delete-button-message']
|
|
|
|
|
: 'htmlform-cloner-delete';
|
|
|
|
|
$field = HTMLForm::loadInputFromParameters( $name, array(
|
|
|
|
|
'type' => 'submit',
|
|
|
|
|
'name' => $name,
|
|
|
|
|
'id' => Sanitizer::escapeId( "{$this->mID}--$key--delete" ),
|
|
|
|
|
'cssclass' => 'mw-htmlform-cloner-delete-button',
|
|
|
|
|
'default' => $this->msg( $label )->text(),
|
|
|
|
|
) );
|
2014-09-11 16:47:26 +00:00
|
|
|
$field->mParent = $this->mParent;
|
2014-03-07 16:03:32 +00:00
|
|
|
$v = $field->getDefault();
|
|
|
|
|
|
|
|
|
|
if ( $displayFormat === 'table' ) {
|
|
|
|
|
$html .= $field->$getFieldHtmlMethod( $v );
|
|
|
|
|
} else {
|
|
|
|
|
$html .= $field->getInputHTML( $v );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $displayFormat !== 'raw' ) {
|
|
|
|
|
$classes = array(
|
|
|
|
|
'mw-htmlform-cloner-row',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
|
|
|
|
|
$classes[] = 'mw-htmlform-nolabel';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$attribs = array(
|
|
|
|
|
'class' => implode( ' ', $classes ),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ( $displayFormat === 'table' ) {
|
|
|
|
|
$html = Html::rawElement( 'table',
|
|
|
|
|
$attribs,
|
|
|
|
|
Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
|
|
|
|
|
} elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
|
|
|
|
|
$html = Html::rawElement( 'div', $attribs, "\n$html\n" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !empty( $this->mParams['row-legend'] ) ) {
|
|
|
|
|
$legend = $this->msg( $this->mParams['row-legend'] )->text();
|
|
|
|
|
$html = Xml::fieldset( $legend, $html );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getInputHTML( $values ) {
|
|
|
|
|
$html = '';
|
|
|
|
|
|
|
|
|
|
foreach ( (array)$values as $key => $value ) {
|
|
|
|
|
if ( $key === 'nonjs' ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$html .= Html::rawElement( 'li', array( 'class' => 'mw-htmlform-cloner-li' ),
|
|
|
|
|
$this->getInputHTMLForKey( $key, $value )
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$template = $this->getInputHTMLForKey( $this->uniqueId, null );
|
|
|
|
|
$html = Html::rawElement( 'ul', array(
|
|
|
|
|
'id' => "mw-htmlform-cloner-list-{$this->mID}",
|
|
|
|
|
'class' => 'mw-htmlform-cloner-ul',
|
|
|
|
|
'data-template' => $template,
|
|
|
|
|
'data-unique-id' => $this->uniqueId,
|
|
|
|
|
), $html );
|
|
|
|
|
|
|
|
|
|
$name = "{$this->mName}[create]";
|
|
|
|
|
$label = isset( $this->mParams['create-button-message'] )
|
|
|
|
|
? $this->mParams['create-button-message']
|
|
|
|
|
: 'htmlform-cloner-create';
|
|
|
|
|
$field = HTMLForm::loadInputFromParameters( $name, array(
|
|
|
|
|
'type' => 'submit',
|
|
|
|
|
'name' => $name,
|
|
|
|
|
'id' => Sanitizer::escapeId( "{$this->mID}--create" ),
|
|
|
|
|
'cssclass' => 'mw-htmlform-cloner-create-button',
|
|
|
|
|
'default' => $this->msg( $label )->text(),
|
|
|
|
|
) );
|
2014-09-11 16:47:26 +00:00
|
|
|
$field->mParent = $this->mParent;
|
2014-03-07 16:03:32 +00:00
|
|
|
$html .= $field->getInputHTML( $field->getDefault() );
|
|
|
|
|
|
|
|
|
|
return $html;
|
|
|
|
|
}
|
|
|
|
|
}
|