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 * @return HTMLFormField[] */ 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(), ) ); $field->mParent = $this->mParent; $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(), ) ); $field->mParent = $this->mParent; $html .= $field->getInputHTML( $field->getDefault() ); return $html; } }