diff options
Diffstat (limited to 'includes/HTMLForm.php')
-rw-r--r-- | includes/HTMLForm.php | 781 |
1 files changed, 650 insertions, 131 deletions
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 6f89d5b8..d260862c 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -83,9 +83,8 @@ * $form = new HTMLForm( $someFields ); * $form->setMethod( 'get' ) * ->setWrapperLegendMsg( 'message-key' ) - * ->suppressReset() * ->prepareForm() - * ->displayForm(); + * ->displayForm( '' ); * @endcode * Note that you will have prepareForm and displayForm at the end. Other * methods call done after that would simply not be part of the form :( @@ -95,7 +94,7 @@ class HTMLForm extends ContextSource { // A mapping of 'type' inputs onto standard HTMLFormField subclasses - static $typeMappings = array( + public static $typeMappings = array( 'api' => 'HTMLApiField', 'text' => 'HTMLTextField', 'textarea' => 'HTMLTextAreaField', @@ -112,6 +111,7 @@ class HTMLForm extends ContextSource { 'submit' => 'HTMLSubmitField', 'hidden' => 'HTMLHiddenField', 'edittools' => 'HTMLEditTools', + 'checkmatrix' => 'HTMLCheckMatrix', // HTMLTextField will output the correct type="" attribute automagically. // There are about four zillion other HTML5 input types, like url, but @@ -127,6 +127,7 @@ class HTMLForm extends ContextSource { protected $mFieldTree; protected $mShowReset = false; + protected $mShowSubmit = true; public $mFieldData; protected $mSubmitCallback; @@ -139,6 +140,7 @@ class HTMLForm extends ContextSource { protected $mSectionFooters = array(); protected $mPost = ''; protected $mId; + protected $mTableId = ''; protected $mSubmitID; protected $mSubmitName; @@ -185,26 +187,27 @@ class HTMLForm extends ContextSource { 'table', 'div', 'raw', + 'vform', ); /** * Build a new HTMLForm from an array of field attributes - * @param $descriptor Array of Field constructs, as described above + * @param array $descriptor of Field constructs, as described above * @param $context IContextSource available since 1.18, will become compulsory in 1.18. * Obviates the need to call $form->setTitle() - * @param $messagePrefix String a prefix to go in front of default messages + * @param string $messagePrefix a prefix to go in front of default messages */ public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { if ( $context instanceof IContextSource ) { $this->setContext( $context ); $this->mTitle = false; // We don't need them to set a title $this->mMessagePrefix = $messagePrefix; - } else { + } elseif ( is_null( $context ) && $messagePrefix !== '' ) { + $this->mMessagePrefix = $messagePrefix; + } elseif ( is_string( $context ) && $messagePrefix === '' ) { // B/C since 1.18 - if ( is_string( $context ) && $messagePrefix === '' ) { - // it's actually $messagePrefix - $this->mMessagePrefix = $context; - } + // it's actually $messagePrefix + $this->mMessagePrefix = $context; } // Expand out into a tree. @@ -221,8 +224,15 @@ class HTMLForm extends ContextSource { } $field = self::loadInputFromParameters( $fieldname, $info ); + // FIXME During field's construct, the parent form isn't available! + // could add a 'parent' name-value to $info, could add a third parameter. $field->mParent = $this; + // vform gets too much space if empty labels generate HTML. + if ( $this->isVForm() ) { + $field->setShowEmptyLabel( false ); + } + $setSection =& $loadedDescriptor; if ( $section ) { $sectionParts = explode( '/', $section ); @@ -247,14 +257,15 @@ class HTMLForm extends ContextSource { /** * Set format in which to display the form - * @param $format String the name of the format to use, must be one of + * @param string $format the name of the format to use, must be one of * $this->availableDisplayFormats + * @throws MWException * @since 1.20 * @return HTMLForm $this for chaining calls (since 1.20) */ public function setDisplayFormat( $format ) { if ( !in_array( $format, $this->availableDisplayFormats ) ) { - throw new MWException ( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) ); + throw new MWException( 'Display format must be one of ' . print_r( $this->availableDisplayFormats, true ) ); } $this->displayFormat = $format; return $this; @@ -270,16 +281,28 @@ class HTMLForm extends ContextSource { } /** + * Test if displayFormat is 'vform' + * @since 1.22 + * @return Bool + */ + public function isVForm() { + return $this->displayFormat === 'vform'; + } + + /** * Add the HTMLForm-specific JavaScript, if it hasn't been * done already. * @deprecated since 1.18 load modules with ResourceLoader instead */ - static function addJS() { wfDeprecated( __METHOD__, '1.18' ); } + static function addJS() { + wfDeprecated( __METHOD__, '1.18' ); + } /** * Initialise a new Object for the field * @param $fieldname string - * @param $descriptor string input Descriptor, as described above + * @param string $descriptor input Descriptor, as described above + * @throws MWException * @return HTMLFormField subclass */ static function loadInputFromParameters( $fieldname, $descriptor ) { @@ -313,6 +336,7 @@ class HTMLForm extends ContextSource { * @attention When doing method chaining, that should be the very last * method call before displayForm(). * + * @throws MWException * @return HTMLForm $this for chaining calls (since 1.20) */ function prepareForm() { @@ -374,11 +398,12 @@ class HTMLForm extends ContextSource { } /** - * Validate all the fields, and call the submision callback + * Validate all the fields, and call the submission callback * function if everything is kosher. + * @throws MWException * @return Mixed Bool true == Successful submission, Bool false - * == No submission attempted, anything else == Error to - * display. + * == No submission attempted, anything else == Error to + * display. */ function trySubmit() { # Check for validation @@ -412,7 +437,7 @@ class HTMLForm extends ContextSource { /** * Set a callback to a function to do something with the form * once it's been successfully validated. - * @param $cb String function name. The function will be passed + * @param string $cb function name. The function will be passed * the output from HTMLForm::filterDataForSubmit, and must * return Bool true on success, Bool false if no submission * was attempted, or String HTML output to display on error. @@ -436,7 +461,7 @@ class HTMLForm extends ContextSource { /** * Set the introductory message, overwriting any existing message. - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @return HTMLForm $this for chaining calls (since 1.20) */ function setIntro( $msg ) { @@ -447,7 +472,7 @@ class HTMLForm extends ContextSource { /** * Set the introductory message, overwriting any existing message. * @since 1.19 - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @return HTMLForm $this for chaining calls (since 1.20) */ function setPreText( $msg ) { @@ -457,7 +482,7 @@ class HTMLForm extends ContextSource { /** * Add introductory text. - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @return HTMLForm $this for chaining calls (since 1.20) */ function addPreText( $msg ) { @@ -467,8 +492,8 @@ class HTMLForm extends ContextSource { /** * Add header text, inside the form. - * @param $msg String complete text of message to display - * @param $section string The section to add the header to + * @param string $msg complete text of message to display + * @param string $section The section to add the header to * @return HTMLForm $this for chaining calls (since 1.20) */ function addHeaderText( $msg, $section = null ) { @@ -486,7 +511,7 @@ class HTMLForm extends ContextSource { /** * Set header text, inside the form. * @since 1.19 - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @param $section The section to add the header to * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -501,8 +526,8 @@ class HTMLForm extends ContextSource { /** * Add footer text, inside the form. - * @param $msg String complete text of message to display - * @param $section string The section to add the footer text to + * @param string $msg complete text of message to display + * @param string $section The section to add the footer text to * @return HTMLForm $this for chaining calls (since 1.20) */ function addFooterText( $msg, $section = null ) { @@ -520,8 +545,8 @@ class HTMLForm extends ContextSource { /** * Set footer text, inside the form. * @since 1.19 - * @param $msg String complete text of message to display - * @param $section string The section to add the footer text to + * @param string $msg complete text of message to display + * @param string $section The section to add the footer text to * @return HTMLForm $this for chaining calls (since 1.20) */ function setFooterText( $msg, $section = null ) { @@ -535,7 +560,7 @@ class HTMLForm extends ContextSource { /** * Add text to the end of the display. - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @return HTMLForm $this for chaining calls (since 1.20) */ function addPostText( $msg ) { @@ -545,7 +570,7 @@ class HTMLForm extends ContextSource { /** * Set text at the end of the display. - * @param $msg String complete text of message to display + * @param string $msg complete text of message to display * @return HTMLForm $this for chaining calls (since 1.20) */ function setPostText( $msg ) { @@ -555,8 +580,8 @@ class HTMLForm extends ContextSource { /** * Add a hidden field to the output - * @param $name String field name. This will be used exactly as entered - * @param $value String field value + * @param string $name field name. This will be used exactly as entered + * @param string $value field value * @param $attribs Array * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -567,10 +592,25 @@ class HTMLForm extends ContextSource { } /** + * Add an array of hidden fields to the output + * + * @since 1.22 + * @param array $fields Associative array of fields to add; + * mapping names to their values + * @return HTMLForm $this for chaining calls + */ + public function addHiddenFields( array $fields ) { + foreach ( $fields as $name => $value ) { + $this->mHiddenFields[] = array( $value, array( 'name' => $name ) ); + } + return $this; + } + + /** * Add a button to the form - * @param $name String field name. - * @param $value String field value - * @param $id String DOM id for the button (default: null) + * @param string $name field name. + * @param string $value field value + * @param string $id DOM id for the button (default: null) * @param $attribs Array * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -580,8 +620,8 @@ class HTMLForm extends ContextSource { } /** - * Display the form (sending to $wgOut), with an appropriate error - * message or stack of messages, and any validation errors, etc. + * Display the form (sending to the context's OutputPage object), with an + * appropriate error message or stack of messages, and any validation errors, etc. * * @attention You should call prepareForm() before calling this function. * Moreover, when doing method chaining this should be the very last method @@ -603,6 +643,11 @@ class HTMLForm extends ContextSource { # For good measure (it is the default) $this->getOutput()->preventClickjacking(); $this->getOutput()->addModules( 'mediawiki.htmlform' ); + if ( $this->isVForm() ) { + $this->getOutput()->addModuleStyles( 'mediawiki.ui' ); + // TODO should vertical form set setWrapperLegend( false ) + // to hide ugly fieldsets? + } $html = '' . $this->getErrors( $submitResult ) @@ -620,7 +665,7 @@ class HTMLForm extends ContextSource { /** * Wrap the form innards in an actual "<form>" element - * @param $html String HTML contents to wrap. + * @param string $html HTML contents to wrap. * @return String wrapped HTML. */ function wrapForm( $html ) { @@ -635,15 +680,18 @@ class HTMLForm extends ContextSource { : 'application/x-www-form-urlencoded'; # Attributes $attribs = array( - 'action' => $this->mAction === false ? $this->getTitle()->getFullURL() : $this->mAction, - 'method' => $this->mMethod, - 'class' => 'visualClear', + 'action' => $this->getAction(), + 'method' => $this->getMethod(), + 'class' => array( 'visualClear' ), 'enctype' => $encType, ); if ( !empty( $this->mId ) ) { $attribs['id'] = $this->mId; } + if ( $this->isVForm() ) { + array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' ); + } return Html::rawElement( 'form', $attribs, $html ); } @@ -677,24 +725,40 @@ class HTMLForm extends ContextSource { * @return String HTML. */ function getButtons() { - $html = ''; - $attribs = array(); + $html = '<span class="mw-htmlform-submit-buttons">'; - if ( isset( $this->mSubmitID ) ) { - $attribs['id'] = $this->mSubmitID; - } + if ( $this->mShowSubmit ) { + $attribs = array(); - if ( isset( $this->mSubmitName ) ) { - $attribs['name'] = $this->mSubmitName; - } + if ( isset( $this->mSubmitID ) ) { + $attribs['id'] = $this->mSubmitID; + } - if ( isset( $this->mSubmitTooltip ) ) { - $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); - } + if ( isset( $this->mSubmitName ) ) { + $attribs['name'] = $this->mSubmitName; + } + + if ( isset( $this->mSubmitTooltip ) ) { + $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); + } + + $attribs['class'] = array( 'mw-htmlform-submit' ); - $attribs['class'] = 'mw-htmlform-submit'; + if ( $this->isVForm() ) { + // mw-ui-block is necessary because the buttons aren't necessarily in an + // immediate child div of the vform. + array_push( $attribs['class'], 'mw-ui-button', 'mw-ui-big', 'mw-ui-primary', 'mw-ui-block' ); + } + + $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; - $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n"; + // Buttons are top-level form elements in table and div layouts, + // but vform wants all elements inside divs to get spaced-out block + // styling. + if ( $this->isVForm() ) { + $html = Html::rawElement( 'div', null, "\n$html\n" ); + } + } if ( $this->mShowReset ) { $html .= Html::element( @@ -708,8 +772,8 @@ class HTMLForm extends ContextSource { foreach ( $this->mButtons as $button ) { $attrs = array( - 'type' => 'submit', - 'name' => $button['name'], + 'type' => 'submit', + 'name' => $button['name'], 'value' => $button['value'] ); @@ -724,6 +788,8 @@ class HTMLForm extends ContextSource { $html .= Html::element( 'input', $attrs ); } + $html .= '</span>'; + return $html; } @@ -732,7 +798,7 @@ class HTMLForm extends ContextSource { * @return String */ function getBody() { - return $this->displaySection( $this->mFieldTree ); + return $this->displaySection( $this->mFieldTree, $this->mTableId ); } /** @@ -760,7 +826,7 @@ class HTMLForm extends ContextSource { /** * Format a stack of error messages into a single HTML string - * @param $errors Array of message keys/values + * @param array $errors of message keys/values * @return String HTML, a "<ul>" list of errors */ public static function formatErrors( $errors ) { @@ -788,7 +854,7 @@ class HTMLForm extends ContextSource { /** * Set the text for the submit button - * @param $t String plaintext. + * @param string $t plaintext. * @return HTMLForm $this for chaining calls (since 1.20) */ function setSubmitText( $t ) { @@ -799,7 +865,7 @@ class HTMLForm extends ContextSource { /** * Set the text for the submit button to a message * @since 1.19 - * @param $msg String message key + * @param string $msg message key * @return HTMLForm $this for chaining calls (since 1.20) */ public function setSubmitTextMsg( $msg ) { @@ -818,7 +884,7 @@ class HTMLForm extends ContextSource { } /** - * @param $name String Submit button name + * @param string $name Submit button name * @return HTMLForm $this for chaining calls (since 1.20) */ public function setSubmitName( $name ) { @@ -827,7 +893,7 @@ class HTMLForm extends ContextSource { } /** - * @param $name String Tooltip for the submit button + * @param string $name Tooltip for the submit button * @return HTMLForm $this for chaining calls (since 1.20) */ public function setSubmitTooltip( $name ) { @@ -847,17 +913,46 @@ class HTMLForm extends ContextSource { } /** - * @param $id String DOM id for the form + * Stop a default submit button being shown for this form. This implies that an + * alternate submit method must be provided manually. + * + * @since 1.22 + * + * @param bool $suppressSubmit Set to false to re-enable the button again + * + * @return HTMLForm $this for chaining calls + */ + function suppressDefaultSubmit( $suppressSubmit = true ) { + $this->mShowSubmit = !$suppressSubmit; + return $this; + } + + /** + * Set the id of the \<table\> or outermost \<div\> element. + * + * @since 1.22 + * @param string $id new value of the id attribute, or "" to remove + * @return HTMLForm $this for chaining calls + */ + public function setTableId( $id ) { + $this->mTableId = $id; + return $this; + } + + /** + * @param string $id DOM id for the form * @return HTMLForm $this for chaining calls (since 1.20) */ public function setId( $id ) { $this->mId = $id; return $this; } + /** * Prompt the whole form to be wrapped in a "<fieldset>", with * this text as its "<legend>" element. - * @param $legend String HTML to go inside the "<legend>" element. + * @param string|false $legend HTML to go inside the "<legend>" element, or + * false for no <legend> * Will be escaped * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -870,7 +965,7 @@ class HTMLForm extends ContextSource { * Prompt the whole form to be wrapped in a "<fieldset>", with * this message as its "<legend>" element. * @since 1.19 - * @param $msg String message key + * @param string $msg message key * @return HTMLForm $this for chaining calls (since 1.20) */ public function setWrapperLegendMsg( $msg ) { @@ -881,7 +976,7 @@ class HTMLForm extends ContextSource { /** * Set the prefix for various default messages * @todo currently only used for the "<fieldset>" legend on forms - * with multiple sections; should be used elsewhre? + * with multiple sections; should be used elsewhere? * @param $p String * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -926,19 +1021,30 @@ class HTMLForm extends ContextSource { /** * @todo Document - * @param $fields array[]|HTMLFormField[] array of fields (either arrays or objects) - * @param $sectionName string ID attribute of the "<table>" tag for this section, ignored if empty - * @param $fieldsetIDPrefix string ID prefix for the "<fieldset>" tag of each subsection, ignored if empty + * @param array[]|HTMLFormField[] $fields array of fields (either arrays or objects) + * @param string $sectionName ID attribute of the "<table>" tag for this section, ignored if empty + * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of each subsection, ignored if empty + * @param boolean &$hasUserVisibleFields Whether the section had user-visible fields * @return String */ - public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '' ) { + public function displaySection( $fields, $sectionName = '', $fieldsetIDPrefix = '', &$hasUserVisibleFields = false ) { $displayFormat = $this->getDisplayFormat(); $html = ''; $subsectionHtml = ''; $hasLabel = false; - $getFieldHtmlMethod = ( $displayFormat == 'table' ) ? 'getTableRow' : 'get' . ucfirst( $displayFormat ); + switch( $displayFormat ) { + case 'table': + $getFieldHtmlMethod = 'getTableRow'; + break; + case 'vform': + // Close enough to a div. + $getFieldHtmlMethod = 'getDiv'; + break; + default: + $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat ); + } foreach ( $fields as $key => $value ) { if ( $value instanceof HTMLFormField ) { @@ -951,20 +1057,38 @@ class HTMLForm extends ContextSource { if ( $labelValue != ' ' && $labelValue !== '' ) { $hasLabel = true; } - } elseif ( is_array( $value ) ) { - $section = $this->displaySection( $value, $key ); - $legend = $this->getLegend( $key ); - if ( isset( $this->mSectionHeaders[$key] ) ) { - $section = $this->mSectionHeaders[$key] . $section; - } - if ( isset( $this->mSectionFooters[$key] ) ) { - $section .= $this->mSectionFooters[$key]; + + if ( get_class( $value ) !== 'HTMLHiddenField' && + get_class( $value ) !== 'HTMLApiField' ) { + $hasUserVisibleFields = true; } - $attributes = array(); - if ( $fieldsetIDPrefix ) { - $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" ); + } elseif ( is_array( $value ) ) { + $subsectionHasVisibleFields = false; + $section = $this->displaySection( $value, "mw-htmlform-$key", "$fieldsetIDPrefix$key-", $subsectionHasVisibleFields ); + $legend = null; + + if ( $subsectionHasVisibleFields === true ) { + // Display the section with various niceties. + $hasUserVisibleFields = true; + + $legend = $this->getLegend( $key ); + + if ( isset( $this->mSectionHeaders[$key] ) ) { + $section = $this->mSectionHeaders[$key] . $section; + } + if ( isset( $this->mSectionFooters[$key] ) ) { + $section .= $this->mSectionFooters[$key]; + } + + $attributes = array(); + if ( $fieldsetIDPrefix ) { + $attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" ); + } + $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n"; + } else { + // Just return the inputs, nothing fancy. + $subsectionHtml .= $section; } - $subsectionHtml .= Xml::fieldset( $legend, $section, $attributes ) . "\n"; } } @@ -980,13 +1104,13 @@ class HTMLForm extends ContextSource { ); if ( $sectionName ) { - $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" ); + $attribs['id'] = Sanitizer::escapeId( $sectionName ); } if ( $displayFormat === 'table' ) { $html = Html::rawElement( 'table', $attribs, Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n"; - } elseif ( $displayFormat === 'div' ) { + } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) { $html = Html::rawElement( 'div', $attribs, "\n$html\n" ); } } @@ -1025,7 +1149,7 @@ class HTMLForm extends ContextSource { /** * Stop a reset button being shown for this form - * @param $suppressReset Bool set to false to re-enable the + * @param bool $suppressReset set to false to re-enable the * button again * @return HTMLForm $this for chaining calls (since 1.20) */ @@ -1069,6 +1193,32 @@ class HTMLForm extends ContextSource { return $this; } + /** + * Get the value for the action attribute of the form. + * + * @since 1.22 + * + * @return string + */ + public function getAction() { + global $wgScript, $wgArticlePath; + + // If an action is alredy provided, return it + if ( $this->mAction !== false ) { + return $this->mAction; + } + + // Check whether we are in GET mode and $wgArticlePath contains a "?" + // meaning that getLocalURL() would return something like "index.php?title=...". + // As browser remove the query string before submitting GET forms, + // it means that the title would be lost. In such case use $wgScript instead + // and put title in an hidden field (see getHiddenFields()). + if ( strpos( $wgArticlePath, '?' ) !== false && $this->getMethod() === 'get' ) { + return $wgScript; + } + + return $this->getTitle()->getLocalURL(); + } } /** @@ -1087,6 +1237,12 @@ abstract class HTMLFormField { protected $mDefault; /** + * @var bool If true will generate an empty div element with no label + * @since 1.22 + */ + protected $mShowEmptyLabels = true; + + /** * @var HTMLForm */ public $mParent; @@ -1095,7 +1251,7 @@ abstract class HTMLFormField { * This function must be implemented to return the HTML to generate * the input object itself. It should not implement the surrounding * table cells/rows, or labels/help messages. - * @param $value String the value to set the input to; eg a default + * @param string $value the value to set the input to; eg a default * text for a text input. * @return String valid HTML. */ @@ -1104,7 +1260,7 @@ abstract class HTMLFormField { /** * Get a translated interface message * - * This is a wrapper arround $this->mParent->msg() if $this->mParent is set + * This is a wrapper around $this->mParent->msg() if $this->mParent is set * and wfMessage() otherwise. * * Parameters are the same as wfMessage(). @@ -1127,8 +1283,8 @@ abstract class HTMLFormField { * Override this function to add specific validation checks on the * field input. Don't forget to call parent::validate() to ensure * that the user-defined callback mValidationCallback is still run - * @param $value String the value the field was submitted with - * @param $alldata Array the data collected from the form + * @param string $value the value the field was submitted with + * @param array $alldata the data collected from the form * @return Mixed Bool true on success, or String error to display. */ function validate( $value, $alldata ) { @@ -1162,6 +1318,18 @@ abstract class HTMLFormField { } /** + * Tell the field whether to generate a separate label element if its label + * is blank. + * + * @since 1.22 + * @param bool $show Set to false to not generate a label. + * @return void + */ + public function setShowEmptyLabel( $show ) { + $this->mShowEmptyLabels = $show; + } + + /** * Get the value that this input has been set to from a posted form, * or the input's default value if it has not been set. * @param $request WebRequest @@ -1177,7 +1345,10 @@ abstract class HTMLFormField { /** * Initialise the object - * @param $params array Associative Array. See HTMLForm doc for syntax. + * @param array $params Associative Array. See HTMLForm doc for syntax. + * + * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead + * @throws MWException */ function __construct( $params ) { $this->mParams = $params; @@ -1195,7 +1366,14 @@ abstract class HTMLFormField { $this->mLabel = wfMessage( $msg, $msgInfo )->parse(); } elseif ( isset( $params['label'] ) ) { - $this->mLabel = $params['label']; + if ( $params['label'] === ' ' ) { + // Apparently some things set   directly and in an odd format + $this->mLabel = ' '; + } else { + $this->mLabel = htmlspecialchars( $params['label'] ); + } + } elseif ( isset( $params['label-raw'] ) ) { + $this->mLabel = $params['label-raw']; } $this->mName = "wp{$params['fieldname']}"; @@ -1240,12 +1418,16 @@ abstract class HTMLFormField { if ( isset( $params['flatlist'] ) ) { $this->mClass .= ' mw-htmlform-flatlist'; } + + if ( isset( $params['hidelabel'] ) ) { + $this->mShowEmptyLabels = false; + } } /** * Get the complete table row for the input, including help text, * labels, and whatever. - * @param $value String the value to set the input to. + * @param string $value the value to set the input to. * @return String complete HTML table row. */ function getTableRow( $value ) { @@ -1289,7 +1471,7 @@ abstract class HTMLFormField { * Get the complete div for the input, including help text, * labels, and whatever. * @since 1.20 - * @param $value String the value to set the input to. + * @param string $value the value to set the input to. * @return String complete HTML table row. */ public function getDiv( $value ) { @@ -1300,13 +1482,22 @@ abstract class HTMLFormField { $cellAttributes = array(); $label = $this->getLabelHtml( $cellAttributes ); + $outerDivClass = array( + 'mw-input', + 'mw-htmlform-nolabel' => ( $label === '' ) + ); + $field = Html::rawElement( 'div', - array( 'class' => 'mw-input' ) + $cellAttributes, + array( 'class' => $outerDivClass ) + $cellAttributes, $inputHtml . "\n$errors" ); + $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass ); + if ( $this->mParent->isVForm() ) { + $divCssClasses[] = 'mw-ui-vform-div'; + } $html = Html::rawElement( 'div', - array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + array( 'class' => $divCssClasses ), $label . $field ); $html .= $helptext; return $html; @@ -1316,13 +1507,12 @@ abstract class HTMLFormField { * Get the complete raw fields for the input, including help text, * labels, and whatever. * @since 1.20 - * @param $value String the value to set the input to. + * @param string $value the value to set the input to. * @return String complete HTML table row. */ public function getRaw( $value ) { - list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + list( $errors, ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() ); $cellAttributes = array(); $label = $this->getLabelHtml( $cellAttributes ); @@ -1415,7 +1605,7 @@ abstract class HTMLFormField { /** * Determine form errors to display and their classes * @since 1.20 - * @param $value String the value of the input + * @param string $value the value of the input * @return Array */ public function getErrorsAndErrorClass( $value ) { @@ -1432,7 +1622,7 @@ abstract class HTMLFormField { } function getLabel() { - return $this->mLabel; + return is_null( $this->mLabel ) ? '' : $this->mLabel; } function getLabelHtml( $cellAttributes = array() ) { @@ -1444,20 +1634,32 @@ abstract class HTMLFormField { $for['for'] = $this->mID; } + $labelValue = trim( $this->getLabel() ); + $hasLabel = false; + if ( $labelValue !== ' ' && $labelValue !== '' ) { + $hasLabel = true; + } + $displayFormat = $this->mParent->getDisplayFormat(); - $labelElement = Html::rawElement( 'label', $for, $this->getLabel() ); + $html = ''; - if ( $displayFormat == 'table' ) { - return Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, - Html::rawElement( 'label', $for, $this->getLabel() ) + if ( $displayFormat === 'table' ) { + $html = Html::rawElement( 'td', array( 'class' => 'mw-label' ) + $cellAttributes, + Html::rawElement( 'label', $for, $labelValue ) ); - } elseif ( $displayFormat == 'div' ) { - return Html::rawElement( 'div', array( 'class' => 'mw-label' ) + $cellAttributes, - Html::rawElement( 'label', $for, $this->getLabel() ) - ); - } else { - return $labelElement; + } elseif ( $hasLabel || $this->mShowEmptyLabels ) { + if ( $displayFormat === 'div' ) { + $html = Html::rawElement( + 'div', + array( 'class' => 'mw-label' ) + $cellAttributes, + Html::rawElement( 'label', $for, $labelValue ) + ); + } else { + $html = Html::rawElement( 'label', $for, $labelValue ); + } } + + return $html; } function getDefault() { @@ -1483,7 +1685,7 @@ abstract class HTMLFormField { /** * flatten an array of options to a single array, for instance, * a set of "<options>" inside "<optgroups>". - * @param $options array Associative Array with values either Strings + * @param array $options Associative Array with values either Strings * or Arrays * @return Array flattened input */ @@ -1597,16 +1799,19 @@ class HTMLTextField extends HTMLFormField { } } class HTMLTextAreaField extends HTMLFormField { + const DEFAULT_COLS = 80; + const DEFAULT_ROWS = 25; + function getCols() { return isset( $this->mParams['cols'] ) ? $this->mParams['cols'] - : 80; + : static::DEFAULT_COLS; } function getRows() { return isset( $this->mParams['rows'] ) ? $this->mParams['rows'] - : 25; + : static::DEFAULT_ROWS; } function getInputHTML( $value ) { @@ -1736,8 +1941,25 @@ class HTMLCheckField extends HTMLFormField { $attr['class'] = $this->mClass; } - return Xml::check( $this->mName, $value, $attr ) . ' ' . - Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); + if ( $this->mParent->isVForm() ) { + // Nest checkbox inside label. + return Html::rawElement( + 'label', + array( + 'class' => 'mw-ui-checkbox-label' + ), + Xml::check( + $this->mName, + $value, + $attr + ) . + // Html:rawElement doesn't escape contents. + htmlspecialchars( $this->mLabel ) + ); + } else { + return Xml::check( $this->mName, $value, $attr ) . ' ' . + Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel ); + } } /** @@ -1750,6 +1972,13 @@ class HTMLCheckField extends HTMLFormField { } /** + * checkboxes don't need a label. + */ + protected function needsLabel() { + return false; + } + + /** * @param $request WebRequest * @return String */ @@ -1778,6 +2007,246 @@ class HTMLCheckField extends HTMLFormField { } /** + * A checkbox matrix + * Operates similarly to HTMLMultiSelectField, but instead of using an array of + * options, uses an array of rows and an array of columns to dynamically + * construct a matrix of options. The tags used to identify a particular cell + * are of the form "columnName-rowName" + * + * Options: + * - columns + * - Required list of columns in the matrix. + * - rows + * - Required list of rows in the matrix. + * - force-options-on + * - Accepts array of column-row tags to be displayed as enabled but unavailable to change + * - force-options-off + * - Accepts array of column-row tags to be displayed as disabled but unavailable to change. + * - tooltips + * - Optional array mapping row label to tooltip content + * - tooltip-class + * - Optional CSS class used on tooltip container span. Defaults to mw-icon-question. + */ +class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { + + static private $requiredParams = array( + // Required by underlying HTMLFormField + 'fieldname', + // Required by HTMLCheckMatrix + 'rows', 'columns' + ); + + public function __construct( $params ) { + $missing = array_diff( self::$requiredParams, array_keys( $params ) ); + if ( $missing ) { + throw new HTMLFormFieldRequiredOptionsException( $this, $missing ); + } + parent::__construct( $params ); + } + + function validate( $value, $alldata ) { + $rows = $this->mParams['rows']; + $columns = $this->mParams['columns']; + + // Make sure user-defined validation callback is run + $p = parent::validate( $value, $alldata ); + if ( $p !== true ) { + return $p; + } + + // Make sure submitted value is an array + if ( !is_array( $value ) ) { + return false; + } + + // If all options are valid, array_intersect of the valid options + // and the provided options will return the provided options. + $validOptions = array(); + foreach ( $rows as $rowTag ) { + foreach ( $columns as $columnTag ) { + $validOptions[] = $columnTag . '-' . $rowTag; + } + } + $validValues = array_intersect( $value, $validOptions ); + if ( count( $validValues ) == count( $value ) ) { + return true; + } else { + return $this->msg( 'htmlform-select-badoption' )->parse(); + } + } + + /** + * Build a table containing a matrix of checkbox options. + * The value of each option is a combination of the row tag and column tag. + * mParams['rows'] is an array with row labels as keys and row tags as values. + * mParams['columns'] is an array with column labels as keys and column tags as values. + * @param array $value of the options that should be checked + * @return String + */ + function getInputHTML( $value ) { + $html = ''; + $tableContents = ''; + $attribs = array(); + $rows = $this->mParams['rows']; + $columns = $this->mParams['columns']; + + // If the disabled param is set, disable all the options + if ( !empty( $this->mParams['disabled'] ) ) { + $attribs['disabled'] = 'disabled'; + } + + // Build the column headers + $headerContents = Html::rawElement( 'td', array(), ' ' ); + foreach ( $columns as $columnLabel => $columnTag ) { + $headerContents .= Html::rawElement( 'td', array(), $columnLabel ); + } + $tableContents .= Html::rawElement( 'tr', array(), "\n$headerContents\n" ); + + $tooltipClass = 'mw-icon-question'; + if ( isset( $this->mParams['tooltip-class'] ) ) { + $tooltipClass = $this->mParams['tooltip-class']; + } + + // Build the options matrix + foreach ( $rows as $rowLabel => $rowTag ) { + // Append tooltip if configured + if ( isset( $this->mParams['tooltips'][$rowLabel] ) ) { + $tooltipAttribs = array( + 'class' => "mw-htmlform-tooltip $tooltipClass", + 'title' => $this->mParams['tooltips'][$rowLabel], + ); + $rowLabel .= ' ' . Html::element( 'span', $tooltipAttribs, '' ); + } + $rowContents = Html::rawElement( 'td', array(), $rowLabel ); + foreach ( $columns as $columnTag ) { + $thisTag = "$columnTag-$rowTag"; + // Construct the checkbox + $thisAttribs = array( + 'id' => "{$this->mID}-$thisTag", + 'value' => $thisTag, + ); + $checked = in_array( $thisTag, (array)$value, true ); + if ( $this->isTagForcedOff( $thisTag ) ) { + $checked = false; + $thisAttribs['disabled'] = 1; + } elseif ( $this->isTagForcedOn( $thisTag ) ) { + $checked = true; + $thisAttribs['disabled'] = 1; + } + $rowContents .= Html::rawElement( + 'td', + array(), + Xml::check( "{$this->mName}[]", $checked, $attribs + $thisAttribs ) + ); + } + $tableContents .= Html::rawElement( 'tr', array(), "\n$rowContents\n" ); + } + + // Put it all in a table + $html .= Html::rawElement( 'table', array( 'class' => 'mw-htmlform-matrix' ), + Html::rawElement( 'tbody', array(), "\n$tableContents\n" ) ) . "\n"; + + return $html; + } + + protected function isTagForcedOff( $tag ) { + return isset( $this->mParams['force-options-off'] ) + && in_array( $tag, $this->mParams['force-options-off'] ); + } + + protected function isTagForcedOn( $tag ) { + return isset( $this->mParams['force-options-on'] ) + && in_array( $tag, $this->mParams['force-options-on'] ); + } + + /** + * Get the complete table row for the input, including help text, + * labels, and whatever. + * We override this function since the label should always be on a separate + * line above the options in the case of a checkbox matrix, i.e. it's always + * a "vertical-label". + * @param string $value the value to set the input to + * @return String complete HTML table row + */ + function getTableRow( $value ) { + list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); + $inputHtml = $this->getInputHTML( $value ); + $fieldType = get_class( $this ); + $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); + $cellAttributes = array( 'colspan' => 2 ); + + $label = $this->getLabelHtml( $cellAttributes ); + + $field = Html::rawElement( + 'td', + array( 'class' => 'mw-input' ) + $cellAttributes, + $inputHtml . "\n$errors" + ); + + $html = Html::rawElement( 'tr', + array( 'class' => 'mw-htmlform-vertical-label' ), $label ); + $html .= Html::rawElement( 'tr', + array( 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass" ), + $field ); + + return $html . $helptext; + } + + /** + * @param $request WebRequest + * @return Array + */ + function loadDataFromRequest( $request ) { + if ( $this->mParent->getMethod() == 'post' ) { + if ( $request->wasPosted() ) { + // Checkboxes are not added to the request arrays if they're not checked, + // so it's perfectly possible for there not to be an entry at all + return $request->getArray( $this->mName, array() ); + } else { + // That's ok, the user has not yet submitted the form, so show the defaults + return $this->getDefault(); + } + } else { + // This is the impossible case: if we look at $_GET and see no data for our + // field, is it because the user has not yet submitted the form, or that they + // have submitted it with all the options unchecked. We will have to assume the + // latter, which basically means that you can't specify 'positive' defaults + // for GET forms. + return $request->getArray( $this->mName, array() ); + } + } + + function getDefault() { + if ( isset( $this->mDefault ) ) { + return $this->mDefault; + } else { + return array(); + } + } + + function filterDataForSubmit( $data ) { + $columns = HTMLFormField::flattenOptions( $this->mParams['columns'] ); + $rows = HTMLFormField::flattenOptions( $this->mParams['rows'] ); + $res = array(); + foreach ( $columns as $column ) { + foreach ( $rows as $row ) { + // Make sure option hasn't been forced + $thisTag = "$column-$row"; + if ( $this->isTagForcedOff( $thisTag ) ) { + $res[$thisTag] = false; + } elseif ( $this->isTagForcedOn( $thisTag ) ) { + $res[$thisTag] = true; + } else { + $res[$thisTag] = in_array( $thisTag, $data ); + } + } + } + + return $res; + } +} + +/** * A select dropdown field. Basically a wrapper for Xmlselect class */ class HTMLSelectField extends HTMLFormField { @@ -1790,10 +2259,11 @@ class HTMLSelectField extends HTMLFormField { $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] ); - if ( in_array( $value, $validOptions ) ) + if ( in_array( $value, $validOptions ) ) { return true; - else + } else { return $this->msg( 'htmlform-select-badoption' )->parse(); + } } function getInputHTML( $value ) { @@ -1827,7 +2297,6 @@ class HTMLSelectField extends HTMLFormField { * Select dropdown field, with an additional "other" textbox. */ class HTMLSelectOrOtherField extends HTMLTextField { - static $jsAdded = false; function __construct( $params ) { if ( !in_array( 'other', $params['options'], true ) ) { @@ -1916,7 +2385,7 @@ class HTMLSelectOrOtherField extends HTMLTextField { /** * Multi-select field */ -class HTMLMultiSelectField extends HTMLFormField { +class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable { function validate( $value, $alldata ) { $p = parent::validate( $value, $alldata ); @@ -2009,6 +2478,17 @@ class HTMLMultiSelectField extends HTMLFormField { } } + function filterDataForSubmit( $data ) { + $options = HTMLFormField::flattenOptions( $this->mParams['options'] ); + + $res = array(); + foreach ( $options as $opt ) { + $res["$opt"] = in_array( $opt, $data ); + } + + return $res; + } + protected function needsLabel() { return false; } @@ -2053,8 +2533,8 @@ class HTMLSelectAndOtherField extends HTMLSelectField { /** * Build a drop-down box from a textual list. - * @param $string String message text - * @param $otherName String name of "other reason" option + * @param string $string message text + * @param string $otherName name of "other reason" option * @return Array * TODO: this is copied from Xml::listDropDown(), deprecate/avoid duplication? */ @@ -2188,7 +2668,6 @@ class HTMLSelectAndOtherField extends HTMLSelectField { */ class HTMLRadioField extends HTMLFormField { - function validate( $value, $alldata ) { $p = parent::validate( $value, $alldata ); @@ -2344,14 +2823,28 @@ class HTMLHiddenField extends HTMLFormField { return $this->getTableRow( $value ); } - public function getInputHTML( $value ) { return ''; } + public function getInputHTML( $value ) { + return ''; + } } /** * Add a submit button inline in the form (as opposed to * HTMLForm::addButton(), which will add it at the end). */ -class HTMLSubmitField extends HTMLFormField { +class HTMLSubmitField extends HTMLButtonField { + protected $buttonType = 'submit'; +} + +/** + * Adds a generic button inline to the form. Does not do anything, you must add + * click handling code in JavaScript. Use a HTMLSubmitField if you merely + * wish to add a submit button to a form. + * + * @since 1.22 + */ +class HTMLButtonField extends HTMLFormField { + protected $buttonType = 'button'; public function __construct( $info ) { $info['nodata'] = true; @@ -2359,13 +2852,20 @@ class HTMLSubmitField extends HTMLFormField { } public function getInputHTML( $value ) { - return Xml::submitButton( + $attr = array( + 'class' => 'mw-htmlform-submit ' . $this->mClass, + 'id' => $this->mID, + ); + + if ( !empty( $this->mParams['disabled'] ) ) { + $attr['disabled'] = 'disabled'; + } + + return Html::input( + $this->mName, $value, - array( - 'class' => 'mw-htmlform-submit ' . $this->mClass, - 'name' => $this->mName, - 'id' => $this->mID, - ) + $this->buttonType, + $attr ); } @@ -2444,3 +2944,22 @@ class HTMLApiField extends HTMLFormField { return ''; } } + +interface HTMLNestedFilterable { + /** + * Support for seperating multi-option preferences into multiple preferences + * Due to lack of array support. + * @param $data array + */ + function filterDataForSubmit( $data ); +} + +class HTMLFormFieldRequiredOptionsException extends MWException { + public function __construct( HTMLFormField $field, array $missing ) { + parent::__construct( sprintf( + "Form type `%s` expected the following parameters to be set: %s", + get_class( $field ), + implode( ', ', $missing ) + ) ); + } +} |