From: Timo Tijhof Date: Wed, 9 May 2018 19:10:05 +0000 (+0100) Subject: mediawiki.htmlform: Move files to their own module directory X-Git-Tag: 1.34.0-rc.0~5466^2 X-Git-Url: http://git.cyclocoop.org/%22.%24info%5B?a=commitdiff_plain;h=7e4928ed655272564944f4e11f5b34e80eb5a20f;p=lhc%2Fweb%2Fwiklou.git mediawiki.htmlform: Move files to their own module directory * mediawiki.htmlform.styles: - mediawiki/htmlform/styles.css - mediawiki/htmlform/images/* Only contains two images, only used by this module. * mediawiki.htmlform.checker.js. * mediawiki.htmlform.ooui: Only Element.js. * mediawiki.htmlform.ooui.styles.less. * mediawiki.htmlform: Other files from mediawiki/htmlform. Bug: T193826 Change-Id: I5c057c758933e905d5c7940ade5bf43282088159 --- diff --git a/resources/Resources.php b/resources/Resources.php index 1a3d2f049c..81a32c2448 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1073,15 +1073,15 @@ return [ ], 'mediawiki.htmlform' => [ 'scripts' => [ - 'resources/src/mediawiki/htmlform/htmlform.js', - 'resources/src/mediawiki/htmlform/autocomplete.js', - 'resources/src/mediawiki/htmlform/autoinfuse.js', - 'resources/src/mediawiki/htmlform/checkmatrix.js', - 'resources/src/mediawiki/htmlform/cloner.js', - 'resources/src/mediawiki/htmlform/hide-if.js', - 'resources/src/mediawiki/htmlform/multiselect.js', - 'resources/src/mediawiki/htmlform/selectandother.js', - 'resources/src/mediawiki/htmlform/selectorother.js', + 'resources/src/mediawiki.htmlform/htmlform.js', + 'resources/src/mediawiki.htmlform/autocomplete.js', + 'resources/src/mediawiki.htmlform/autoinfuse.js', + 'resources/src/mediawiki.htmlform/checkmatrix.js', + 'resources/src/mediawiki.htmlform/cloner.js', + 'resources/src/mediawiki.htmlform/hide-if.js', + 'resources/src/mediawiki.htmlform/multiselect.js', + 'resources/src/mediawiki.htmlform/selectandother.js', + 'resources/src/mediawiki.htmlform/selectorother.js', ], 'dependencies' => [ 'mediawiki.RegExp', @@ -1096,7 +1096,7 @@ return [ ], 'mediawiki.htmlform.checker' => [ 'scripts' => [ - 'resources/src/mediawiki/htmlform/htmlform.Checker.js', + 'resources/src/mediawiki.htmlform.checker.js', ], 'dependencies' => [ 'jquery.throttle-debounce', @@ -1105,7 +1105,7 @@ return [ ], 'mediawiki.htmlform.ooui' => [ 'scripts' => [ - 'resources/src/mediawiki/htmlform/htmlform.Element.js', + 'resources/src/mediawiki.htmlform.ooui/Element.js', ], 'dependencies' => [ 'oojs-ui-core', @@ -1113,11 +1113,11 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.htmlform.styles' => [ - 'styles' => 'resources/src/mediawiki/htmlform/styles.css', + 'styles' => 'resources/src/mediawiki.htmlform.styles/styles.css', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.htmlform.ooui.styles' => [ - 'styles' => 'resources/src/mediawiki/htmlform/ooui.styles.less', + 'styles' => 'resources/src/mediawiki.htmlform.ooui.styles.less', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.icon' => [ diff --git a/resources/src/mediawiki.htmlform.checker.js b/resources/src/mediawiki.htmlform.checker.js new file mode 100644 index 0000000000..a9f08db9d8 --- /dev/null +++ b/resources/src/mediawiki.htmlform.checker.js @@ -0,0 +1,181 @@ +( function ( mw, $ ) { + + // FIXME: mw.htmlform.Element also sets this to empty object + mw.htmlform = {}; + + /** + * @class mw.htmlform.Checker + */ + + /** + * A helper class to add validation to non-OOUI HtmlForm fields. + * + * @constructor + * @param {jQuery} $element Form field generated by HTMLForm + * @param {Function} validator Validation callback + * @param {string} validator.value Value of the form field to be validated + * @param {jQuery.Promise} validator.return The promise should be resolved + * with an object with two properties: Boolean 'valid' to indicate success + * or failure of validation, and an array 'messages' to be passed to + * setErrors() on failure. + */ + mw.htmlform.Checker = function ( $element, validator ) { + this.validator = validator; + this.$element = $element; + + this.$errorBox = $element.next( '.error' ); + if ( !this.$errorBox.length ) { + this.$errorBox = $( '' ); + this.$errorBox.hide(); + $element.after( this.$errorBox ); + } + + this.currentValue = this.$element.val(); + }; + + /** + * Attach validation events to the form element + * + * @param {jQuery} [$extraElements] Additional elements to listen for change + * events on. + * @return {mw.htmlform.Checker} + * @chainable + */ + mw.htmlform.Checker.prototype.attach = function ( $extraElements ) { + var $e, + // We need to hook to all of these events to be sure we are + // notified of all changes to the value of an + // field. + events = 'keyup keydown change mouseup cut paste focus blur'; + + $e = this.$element; + if ( $extraElements ) { + $e = $e.add( $extraElements ); + } + $e.on( events, $.debounce( 1000, this.validate.bind( this ) ) ); + + return this; + }; + + /** + * Validate the form element + * @return {jQuery.Promise} + */ + mw.htmlform.Checker.prototype.validate = function () { + var currentRequestInternal, + that = this, + value = this.$element.val(); + + // Abort any pending requests. + if ( this.currentRequest && this.currentRequest.abort ) { + this.currentRequest.abort(); + } + + if ( value === '' ) { + this.currentValue = value; + this.setErrors( [] ); + return; + } + + this.currentRequest = currentRequestInternal = this.validator( value ) + .done( function ( info ) { + var forceReplacement = value !== that.currentValue; + + // Another request was fired in the meantime, the result we got here is no longer current. + // This shouldn't happen as we abort pending requests, but you never know. + if ( that.currentRequest !== currentRequestInternal ) { + return; + } + // If we're here, then the current request has finished, avoid calling .abort() needlessly. + that.currentRequest = undefined; + + that.currentValue = value; + + if ( info.valid ) { + that.setErrors( [], forceReplacement ); + } else { + that.setErrors( info.messages, forceReplacement ); + } + } ).fail( function () { + that.currentValue = null; + that.setErrors( [] ); + } ); + + return currentRequestInternal; + }; + + /** + * Display errors associated with the form element + * @param {Array} errors Error messages. Each error message will be appended to a + * `` or `
  • `, as with jQuery.append(). + * @param {boolean} [forceReplacement] Set true to force a visual replacement even + * if the errors are the same. Ignored if errors are empty. + * @return {mw.htmlform.Checker} + * @chainable + */ + mw.htmlform.Checker.prototype.setErrors = function ( errors, forceReplacement ) { + var $oldErrorBox, tagName, showFunc, text, replace, + $errorBox = this.$errorBox; + + if ( errors.length === 0 ) { + $errorBox.slideUp( function () { + $errorBox + .removeAttr( 'class' ) + .empty(); + } ); + } else { + // Match behavior of HTMLFormField::formatErrors(), or
      + // depending on the count. + tagName = errors.length === 1 ? 'span' : 'ul'; + + // We have to animate the replacement if we're changing the tag. We + // also want to if told to by the caller (i.e. to make it visually + // obvious that the changed field value gives the same error) or if + // the error text changes (because it makes more sense than + // changing the text with no animation). + replace = ( + forceReplacement || $errorBox.length > 1 || + $errorBox[ 0 ].tagName.toLowerCase() !== tagName + ); + if ( !replace ) { + text = $( '<' + tagName + '>' ) + .append( errors.map( function ( e ) { + return errors.length === 1 ? e : $( '
    • ' ).append( e ); + } ) ); + if ( text.text() !== $errorBox.text() ) { + replace = true; + } + } + + $oldErrorBox = $errorBox; + if ( replace ) { + this.$errorBox = $errorBox = $( '<' + tagName + '>' ); + $errorBox.hide(); + $oldErrorBox.after( this.$errorBox ); + } + + showFunc = function () { + if ( $oldErrorBox !== $errorBox ) { + $oldErrorBox + .removeAttr( 'class' ) + .detach(); + } + $errorBox + .attr( 'class', 'error' ) + .empty() + .append( errors.map( function ( e ) { + return errors.length === 1 ? e : $( '
    • ' ).append( e ); + } ) ) + .slideDown(); + }; + if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) { + $oldErrorBox.slideUp( showFunc ); + } else { + showFunc(); + } + } + + return this; + }; + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform.ooui.styles.less b/resources/src/mediawiki.htmlform.ooui.styles.less new file mode 100644 index 0000000000..61a1c9cc56 --- /dev/null +++ b/resources/src/mediawiki.htmlform.ooui.styles.less @@ -0,0 +1,64 @@ +@import 'mediawiki.mixins'; + +// OOUIHTMLForm styles +@ooui-font-size-browser: 16; // assumed browser default of `16px` +@ooui-font-size-base: 0.875em; // equals `14px` at browser default of `16px` + +@ooui-spacing-medium: 12 / @ooui-font-size-browser / @ooui-font-size-base; // equals `0.8571429em`≈`12px` +@ooui-spacing-large: 16 / @ooui-font-size-browser / @ooui-font-size-base; // equals `1.1428571em`≈`16px` + +.mw-htmlform-ooui-wrapper.oo-ui-panelLayout-padded { + padding: @ooui-spacing-medium @ooui-spacing-large @ooui-spacing-large; +} + +.mw-htmlform-ooui { + line-height: 1.4; // Override MediaWiki's default of `1.6` + + .oo-ui-fieldLayout.oo-ui-labelElement > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header { + line-height: 16 / @ooui-font-size-browser / @ooui-font-size-base; + } + + .mw-htmlform-field-HTMLCheckMatrix { + width: 100%; + } + + .mw-htmlform-matrix { + border-spacing: 0; + + td { + padding: 0.35em 0.7em; + .transition( background-color 250ms ); + } + + tbody tr:nth-child( even ) td { + background-color: #f8f9fa; + } + + tbody tr:not( :first-child ):hover td { + background-color: #eaecf0; + } + + tbody tr:first-child td { + background-color: #fff; + } + + td.first { + margin-right: 5%; + width: 39%; + } + } +} + +// Flatlist styling for PHP widgets... +.mw-htmlform-flatlist .oo-ui-fieldLayout-align-inline, +// ...and for JS widgets +.mw-htmlform-flatlist .oo-ui-radioOptionWidget, +.mw-htmlform-flatlist .oo-ui-checkboxMultioptionWidget { + display: inline-block; + margin-right: @ooui-spacing-medium; +} + +.mw-htmlform-ooui .htmlform-tip, +.mw-htmlform-ooui .mw-htmlform-submit-buttons { + margin-top: @ooui-spacing-medium; +} diff --git a/resources/src/mediawiki.htmlform.ooui/Element.js b/resources/src/mediawiki.htmlform.ooui/Element.js new file mode 100644 index 0000000000..e195ccbd9e --- /dev/null +++ b/resources/src/mediawiki.htmlform.ooui/Element.js @@ -0,0 +1,47 @@ +( function ( mw ) { + + // FIXME: mw.htmlform.Checker also sets this to empty object + mw.htmlform = {}; + + /** + * Allows custom data specific to HTMLFormField to be set for OOUI forms. This picks up the + * extra config from a matching PHP widget (defined in HTMLFormElement.php) when constructed using + * OO.ui.infuse(). + * + * Currently only supports passing 'hide-if' data. + * + * @ignore + * @param {Object} [config] Configuration options + */ + mw.htmlform.Element = function ( config ) { + // Configuration initialization + config = config || {}; + + // Properties + this.hideIf = config.hideIf; + + // Initialization + if ( this.hideIf ) { + this.$element.addClass( 'mw-htmlform-hide-if' ); + } + }; + + mw.htmlform.FieldLayout = function ( config ) { + // Parent constructor + mw.htmlform.FieldLayout.parent.call( this, config ); + // Mixin constructors + mw.htmlform.Element.call( this, config ); + }; + OO.inheritClass( mw.htmlform.FieldLayout, OO.ui.FieldLayout ); + OO.mixinClass( mw.htmlform.FieldLayout, mw.htmlform.Element ); + + mw.htmlform.ActionFieldLayout = function ( config ) { + // Parent constructor + mw.htmlform.ActionFieldLayout.parent.call( this, config ); + // Mixin constructors + mw.htmlform.Element.call( this, config ); + }; + OO.inheritClass( mw.htmlform.ActionFieldLayout, OO.ui.ActionFieldLayout ); + OO.mixinClass( mw.htmlform.ActionFieldLayout, mw.htmlform.Element ); + +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.htmlform.styles/images/question.png b/resources/src/mediawiki.htmlform.styles/images/question.png new file mode 100644 index 0000000000..7fc08a05e9 Binary files /dev/null and b/resources/src/mediawiki.htmlform.styles/images/question.png differ diff --git a/resources/src/mediawiki.htmlform.styles/images/question.svg b/resources/src/mediawiki.htmlform.styles/images/question.svg new file mode 100644 index 0000000000..655076f341 --- /dev/null +++ b/resources/src/mediawiki.htmlform.styles/images/question.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/src/mediawiki.htmlform.styles/styles.css b/resources/src/mediawiki.htmlform.styles/styles.css new file mode 100644 index 0000000000..0f331ee0d3 --- /dev/null +++ b/resources/src/mediawiki.htmlform.styles/styles.css @@ -0,0 +1,54 @@ +/* HTMLForm styles */ + +.mw-htmlform { + clear: both; +} + +table.mw-htmlform-nolabel td.mw-label { + display: none; +} + +.mw-htmlform-invalid-input td.mw-input input { + border-color: #d33; +} + +.mw-htmlform-flatlist div.mw-htmlform-flatlist-item { + display: inline; + margin-right: 1em; + white-space: nowrap; +} + +/* HTMLCheckMatrix */ + +.mw-htmlform-matrix td { + padding-left: 0.5em; + padding-right: 0.5em; +} + +tr.mw-htmlform-vertical-label td.mw-label { + text-align: left !important; /* stylelint-disable-line declaration-no-important */ +} + +.mw-icon-question { + /* SVG support using a transparent gradient to guarantee cross-browser + * compatibility (browsers able to understand gradient syntax support also SVG). + * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique */ + background-image: url( images/question.png ); + /* @embed */ + background-image: linear-gradient( transparent, transparent ), url( images/question.svg ); + background-repeat: no-repeat; + background-size: 13px 13px; + display: inline-block; + height: 13px; + width: 13px; + margin-left: 4px; +} + +/* stylelint-disable indentation */ +.mw-icon-question:lang( ar ), +.mw-icon-question:lang( fa ), +.mw-icon-question:lang( ur ) { + -webkit-transform: scaleX( -1 ); + -ms-transform: scaleX( -1 ); + transform: scaleX( -1 ); +} diff --git a/resources/src/mediawiki.htmlform/autocomplete.js b/resources/src/mediawiki.htmlform/autocomplete.js new file mode 100644 index 0000000000..8157975560 --- /dev/null +++ b/resources/src/mediawiki.htmlform/autocomplete.js @@ -0,0 +1,25 @@ +/* + * HTMLForm enhancements: + * Set up autocomplete fields. + */ +( function ( mw, $ ) { + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + var $autocomplete = $root.find( '.mw-htmlform-autocomplete' ); + if ( $autocomplete.length ) { + mw.loader.using( 'jquery.suggestions', function () { + $autocomplete.suggestions( { + fetch: function ( val ) { + var $el = $( this ); + $el.suggestions( 'suggestions', + $.grep( $el.data( 'autocomplete' ), function ( v ) { + return v.indexOf( val ) === 0; + } ) + ); + } + } ); + } ); + } + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform/autoinfuse.js b/resources/src/mediawiki.htmlform/autoinfuse.js new file mode 100644 index 0000000000..c39e43aaa8 --- /dev/null +++ b/resources/src/mediawiki.htmlform/autoinfuse.js @@ -0,0 +1,32 @@ +/* + * HTMLForm enhancements: + * Infuse some OOUI HTMLForm fields (those which benefit from always being infused). + */ +( function ( mw, $ ) { + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + var $oouiNodes, modules, extraModules; + + $oouiNodes = $root.find( '.mw-htmlform-field-autoinfuse' ); + if ( $oouiNodes.length ) { + // The modules are preloaded (added server-side in HTMLFormField, and the individual fields + // which need extra ones), but this module doesn't depend on them. Wait until they're loaded. + modules = [ 'mediawiki.htmlform.ooui' ]; + $oouiNodes.each( function () { + var data = $( this ).data( 'mw-modules' ); + if ( data ) { + // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer + extraModules = data.split( ',' ); + modules.push.apply( modules, extraModules ); + } + } ); + mw.loader.using( modules ).done( function () { + $oouiNodes.each( function () { + OO.ui.infuse( this ); + } ); + } ); + } + + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform/checkmatrix.js b/resources/src/mediawiki.htmlform/checkmatrix.js new file mode 100644 index 0000000000..b825f12b98 --- /dev/null +++ b/resources/src/mediawiki.htmlform/checkmatrix.js @@ -0,0 +1,16 @@ +/* + * HTMLForm enhancements: + * Show fancy tooltips for checkmatrix fields. + */ +( function ( mw ) { + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + var $matrixTooltips = $root.find( '.mw-htmlform-matrix .mw-htmlform-tooltip' ); + if ( $matrixTooltips.length ) { + mw.loader.using( 'jquery.tipsy', function () { + $matrixTooltips.tipsy( { gravity: 's' } ); + } ); + } + } ); + +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.htmlform/cloner.js b/resources/src/mediawiki.htmlform/cloner.js new file mode 100644 index 0000000000..ab81580bee --- /dev/null +++ b/resources/src/mediawiki.htmlform/cloner.js @@ -0,0 +1,36 @@ +/* + * HTMLForm enhancements: + * Add/remove cloner clones without having to resubmit the form. + */ +( function ( mw, $ ) { + + var cloneCounter = 0; + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + $root.find( '.mw-htmlform-cloner-delete-button' ).filter( ':input' ).click( function ( ev ) { + ev.preventDefault(); + $( this ).closest( 'li.mw-htmlform-cloner-li' ).remove(); + } ); + + $root.find( '.mw-htmlform-cloner-create-button' ).filter( ':input' ).click( function ( ev ) { + var $ul, $li, html; + + ev.preventDefault(); + + $ul = $( this ).prev( 'ul.mw-htmlform-cloner-ul' ); + + html = $ul.data( 'template' ).replace( + new RegExp( mw.RegExp.escape( $ul.data( 'uniqueId' ) ), 'g' ), + 'clone' + ( ++cloneCounter ) + ); + + $li = $( '
    • ' ) + .addClass( 'mw-htmlform-cloner-li' ) + .html( html ) + .appendTo( $ul ); + + mw.hook( 'htmlform.enhance' ).fire( $li ); + } ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform/hide-if.js b/resources/src/mediawiki.htmlform/hide-if.js new file mode 100644 index 0000000000..6d3c9fd189 --- /dev/null +++ b/resources/src/mediawiki.htmlform/hide-if.js @@ -0,0 +1,295 @@ +/* + * HTMLForm enhancements: + * Set up 'hide-if' behaviors for form fields that have them. + */ +( function ( mw, $ ) { + + /** + * Helper function for hide-if to find the nearby form field. + * + * Find the closest match for the given name, "closest" being the minimum + * level of parents to go to find a form field matching the given name or + * ending in array keys matching the given name (e.g. "baz" matches + * "foo[bar][baz]"). + * + * @ignore + * @private + * @param {jQuery} $el + * @param {string} name + * @return {jQuery|OO.ui.Widget|null} + */ + function hideIfGetField( $el, name ) { + var $found, $p, $widget, + suffix = name.replace( /^([^[]+)/, '[$1]' ); + + function nameFilter() { + return this.name === name || + ( this.name === ( 'wp' + name ) ) || + this.name.slice( -suffix.length ) === suffix; + } + + for ( $p = $el.parent(); $p.length > 0; $p = $p.parent() ) { + $found = $p.find( '[name]' ).filter( nameFilter ); + if ( $found.length ) { + $widget = $found.closest( '.oo-ui-widget[data-ooui]' ); + if ( $widget.length ) { + return OO.ui.Widget.static.infuse( $widget ); + } + return $found; + } + } + return null; + } + + /** + * Helper function for hide-if to return a test function and list of + * dependent fields for a hide-if specification. + * + * @ignore + * @private + * @param {jQuery} $el + * @param {Array} spec + * @return {Array} + * @return {Array} return.0 Dependent fields, array of jQuery objects or OO.ui.Widgets + * @return {Function} return.1 Test function + */ + function hideIfParse( $el, spec ) { + var op, i, l, v, field, $field, fields, func, funcs, getVal; + + op = spec[ 0 ]; + l = spec.length; + switch ( op ) { + case 'AND': + case 'OR': + case 'NAND': + case 'NOR': + funcs = []; + fields = []; + for ( i = 1; i < l; i++ ) { + if ( !Array.isArray( spec[ i ] ) ) { + throw new Error( op + ' parameters must be arrays' ); + } + v = hideIfParse( $el, spec[ i ] ); + fields = fields.concat( v[ 0 ] ); + funcs.push( v[ 1 ] ); + } + + l = funcs.length; + switch ( op ) { + case 'AND': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( !funcs[ i ]() ) { + return false; + } + } + return true; + }; + break; + + case 'OR': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( funcs[ i ]() ) { + return true; + } + } + return false; + }; + break; + + case 'NAND': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( !funcs[ i ]() ) { + return true; + } + } + return false; + }; + break; + + case 'NOR': + func = function () { + var i; + for ( i = 0; i < l; i++ ) { + if ( funcs[ i ]() ) { + return false; + } + } + return true; + }; + break; + } + + return [ fields, func ]; + + case 'NOT': + if ( l !== 2 ) { + throw new Error( 'NOT takes exactly one parameter' ); + } + if ( !Array.isArray( spec[ 1 ] ) ) { + throw new Error( 'NOT parameters must be arrays' ); + } + v = hideIfParse( $el, spec[ 1 ] ); + fields = v[ 0 ]; + func = v[ 1 ]; + return [ fields, function () { + return !func(); + } ]; + + case '===': + case '!==': + if ( l !== 3 ) { + throw new Error( op + ' takes exactly two parameters' ); + } + field = hideIfGetField( $el, spec[ 1 ] ); + if ( !field ) { + return [ [], function () { + return false; + } ]; + } + v = spec[ 2 ]; + + if ( !( field instanceof jQuery ) ) { + // field is a OO.ui.Widget + if ( field.supports( 'isSelected' ) ) { + getVal = function () { + var selected = field.isSelected(); + return selected ? field.getValue() : ''; + }; + } else { + getVal = function () { + return field.getValue(); + }; + } + } else { + $field = $( field ); + if ( $field.prop( 'type' ) === 'radio' || $field.prop( 'type' ) === 'checkbox' ) { + getVal = function () { + var $selected = $field.filter( ':checked' ); + return $selected.length ? $selected.val() : ''; + }; + } else { + getVal = function () { + return $field.val(); + }; + } + } + + switch ( op ) { + case '===': + func = function () { + return getVal() === v; + }; + break; + case '!==': + func = function () { + return getVal() !== v; + }; + break; + } + + return [ [ field ], func ]; + + default: + throw new Error( 'Unrecognized operation \'' + op + '\'' ); + } + } + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + var + $fields = $root.find( '.mw-htmlform-hide-if' ), + $oouiFields = $fields.filter( '[data-ooui]' ), + modules = []; + + if ( $oouiFields.length ) { + modules.push( 'mediawiki.htmlform.ooui' ); + $oouiFields.each( function () { + var data, extraModules, + $el = $( this ); + + data = $el.data( 'mw-modules' ); + if ( data ) { + // We can trust this value, 'data-mw-*' attributes are banned from user content in Sanitizer + extraModules = data.split( ',' ); + modules.push.apply( modules, extraModules ); + } + } ); + } + + mw.loader.using( modules ).done( function () { + $fields.each( function () { + var v, i, fields, test, func, spec, self, + $el = $( this ); + + if ( $el.is( '[data-ooui]' ) ) { + // self should be a FieldLayout that mixes in mw.htmlform.Element + self = OO.ui.FieldLayout.static.infuse( $el ); + spec = self.hideIf; + // The original element has been replaced with infused one + $el = self.$element; + } else { + self = $el; + spec = $el.data( 'hideIf' ); + } + + if ( !spec ) { + return; + } + + v = hideIfParse( $el, spec ); + fields = v[ 0 ]; + test = v[ 1 ]; + // The .toggle() method works mostly the same for jQuery objects and OO.ui.Widget + func = function () { + var shouldHide = test(); + self.toggle( !shouldHide ); + + // It is impossible to submit a form with hidden fields failing validation, e.g. one that + // is required. However, validity is not checked for disabled fields, as these are not + // submitted with the form. So we should also disable fields when hiding them. + if ( self instanceof jQuery ) { + // This also finds elements inside any nested fields (in case of HTMLFormFieldCloner), + // which is problematic. But it works because: + // * HTMLFormFieldCloner::createFieldsForKey() copies 'hide-if' rules to nested fields + // * jQuery collections like $fields are in document order, so we register event + // handlers for parents first + // * Event handlers are fired in the order they were registered, so even if the handler + // for parent messed up the child, the handle for child will run next and fix it + self.find( 'input, textarea, select' ).each( function () { + var $this = $( this ); + if ( shouldHide ) { + if ( $this.data( 'was-disabled' ) === undefined ) { + $this.data( 'was-disabled', $this.prop( 'disabled' ) ); + } + $this.prop( 'disabled', true ); + } else { + $this.prop( 'disabled', $this.data( 'was-disabled' ) ); + } + } ); + } else { + // self is a OO.ui.FieldLayout + if ( shouldHide ) { + if ( self.wasDisabled === undefined ) { + self.wasDisabled = self.fieldWidget.isDisabled(); + } + self.fieldWidget.setDisabled( true ); + } else if ( self.wasDisabled !== undefined ) { + self.fieldWidget.setDisabled( self.wasDisabled ); + } + } + }; + for ( i = 0; i < fields.length; i++ ) { + // The .on() method works mostly the same for jQuery objects and OO.ui.Widget + fields[ i ].on( 'change', func ); + } + func(); + } ); + } ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform/htmlform.js b/resources/src/mediawiki.htmlform/htmlform.js new file mode 100644 index 0000000000..bc835b5924 --- /dev/null +++ b/resources/src/mediawiki.htmlform/htmlform.js @@ -0,0 +1,14 @@ +( function ( mw, $ ) { + + $( function () { + mw.hook( 'htmlform.enhance' ).fire( $( document ) ); + } ); + + mw.hook( 'htmlform.enhance' ).add( function ( $root ) { + // Turn HTML5 form validation back on, in cases where it was disabled server-side (see + // HTMLForm::needsJSForHtml5FormValidation()) because we need extra logic implemented in JS to + // validate correctly. Currently, this is only used for forms containing fields with 'hide-if'. + $root.find( '.mw-htmlform' ).removeAttr( 'novalidate' ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.htmlform/multiselect.js b/resources/src/mediawiki.htmlform/multiselect.js new file mode 100644 index 0000000000..e48376367c --- /dev/null +++ b/resources/src/mediawiki.htmlform/multiselect.js @@ -0,0 +1,115 @@ +/* + * HTMLForm enhancements: + * Convert multiselect fields from checkboxes to Chosen selector when requested. + */ +( function ( mw, $ ) { + + function addMulti( $oldContainer, $container ) { + var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ), + oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen|mw-htmlform-dropdown)/g, '' ), + $select = $( ' - // field. - events = 'keyup keydown change mouseup cut paste focus blur'; - - $e = this.$element; - if ( $extraElements ) { - $e = $e.add( $extraElements ); - } - $e.on( events, $.debounce( 1000, this.validate.bind( this ) ) ); - - return this; - }; - - /** - * Validate the form element - * @return {jQuery.Promise} - */ - mw.htmlform.Checker.prototype.validate = function () { - var currentRequestInternal, - that = this, - value = this.$element.val(); - - // Abort any pending requests. - if ( this.currentRequest && this.currentRequest.abort ) { - this.currentRequest.abort(); - } - - if ( value === '' ) { - this.currentValue = value; - this.setErrors( [] ); - return; - } - - this.currentRequest = currentRequestInternal = this.validator( value ) - .done( function ( info ) { - var forceReplacement = value !== that.currentValue; - - // Another request was fired in the meantime, the result we got here is no longer current. - // This shouldn't happen as we abort pending requests, but you never know. - if ( that.currentRequest !== currentRequestInternal ) { - return; - } - // If we're here, then the current request has finished, avoid calling .abort() needlessly. - that.currentRequest = undefined; - - that.currentValue = value; - - if ( info.valid ) { - that.setErrors( [], forceReplacement ); - } else { - that.setErrors( info.messages, forceReplacement ); - } - } ).fail( function () { - that.currentValue = null; - that.setErrors( [] ); - } ); - - return currentRequestInternal; - }; - - /** - * Display errors associated with the form element - * @param {Array} errors Error messages. Each error message will be appended to a - * `` or `
    • `, as with jQuery.append(). - * @param {boolean} [forceReplacement] Set true to force a visual replacement even - * if the errors are the same. Ignored if errors are empty. - * @return {mw.htmlform.Checker} - * @chainable - */ - mw.htmlform.Checker.prototype.setErrors = function ( errors, forceReplacement ) { - var $oldErrorBox, tagName, showFunc, text, replace, - $errorBox = this.$errorBox; - - if ( errors.length === 0 ) { - $errorBox.slideUp( function () { - $errorBox - .removeAttr( 'class' ) - .empty(); - } ); - } else { - // Match behavior of HTMLFormField::formatErrors(), or
        - // depending on the count. - tagName = errors.length === 1 ? 'span' : 'ul'; - - // We have to animate the replacement if we're changing the tag. We - // also want to if told to by the caller (i.e. to make it visually - // obvious that the changed field value gives the same error) or if - // the error text changes (because it makes more sense than - // changing the text with no animation). - replace = ( - forceReplacement || $errorBox.length > 1 || - $errorBox[ 0 ].tagName.toLowerCase() !== tagName - ); - if ( !replace ) { - text = $( '<' + tagName + '>' ) - .append( errors.map( function ( e ) { - return errors.length === 1 ? e : $( '
      • ' ).append( e ); - } ) ); - if ( text.text() !== $errorBox.text() ) { - replace = true; - } - } - - $oldErrorBox = $errorBox; - if ( replace ) { - this.$errorBox = $errorBox = $( '<' + tagName + '>' ); - $errorBox.hide(); - $oldErrorBox.after( this.$errorBox ); - } - - showFunc = function () { - if ( $oldErrorBox !== $errorBox ) { - $oldErrorBox - .removeAttr( 'class' ) - .detach(); - } - $errorBox - .attr( 'class', 'error' ) - .empty() - .append( errors.map( function ( e ) { - return errors.length === 1 ? e : $( '
      • ' ).append( e ); - } ) ) - .slideDown(); - }; - if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) { - $oldErrorBox.slideUp( showFunc ); - } else { - showFunc(); - } - } - - return this; - }; - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/htmlform/htmlform.Element.js b/resources/src/mediawiki/htmlform/htmlform.Element.js deleted file mode 100644 index 01108e6573..0000000000 --- a/resources/src/mediawiki/htmlform/htmlform.Element.js +++ /dev/null @@ -1,46 +0,0 @@ -( function ( mw ) { - - mw.htmlform = {}; - - /** - * Allows custom data specific to HTMLFormField to be set for OOUI forms. This picks up the - * extra config from a matching PHP widget (defined in HTMLFormElement.php) when constructed using - * OO.ui.infuse(). - * - * Currently only supports passing 'hide-if' data. - * - * @ignore - * @param {Object} [config] Configuration options - */ - mw.htmlform.Element = function ( config ) { - // Configuration initialization - config = config || {}; - - // Properties - this.hideIf = config.hideIf; - - // Initialization - if ( this.hideIf ) { - this.$element.addClass( 'mw-htmlform-hide-if' ); - } - }; - - mw.htmlform.FieldLayout = function ( config ) { - // Parent constructor - mw.htmlform.FieldLayout.parent.call( this, config ); - // Mixin constructors - mw.htmlform.Element.call( this, config ); - }; - OO.inheritClass( mw.htmlform.FieldLayout, OO.ui.FieldLayout ); - OO.mixinClass( mw.htmlform.FieldLayout, mw.htmlform.Element ); - - mw.htmlform.ActionFieldLayout = function ( config ) { - // Parent constructor - mw.htmlform.ActionFieldLayout.parent.call( this, config ); - // Mixin constructors - mw.htmlform.Element.call( this, config ); - }; - OO.inheritClass( mw.htmlform.ActionFieldLayout, OO.ui.ActionFieldLayout ); - OO.mixinClass( mw.htmlform.ActionFieldLayout, mw.htmlform.Element ); - -}( mediaWiki ) ); diff --git a/resources/src/mediawiki/htmlform/htmlform.js b/resources/src/mediawiki/htmlform/htmlform.js deleted file mode 100644 index bc835b5924..0000000000 --- a/resources/src/mediawiki/htmlform/htmlform.js +++ /dev/null @@ -1,14 +0,0 @@ -( function ( mw, $ ) { - - $( function () { - mw.hook( 'htmlform.enhance' ).fire( $( document ) ); - } ); - - mw.hook( 'htmlform.enhance' ).add( function ( $root ) { - // Turn HTML5 form validation back on, in cases where it was disabled server-side (see - // HTMLForm::needsJSForHtml5FormValidation()) because we need extra logic implemented in JS to - // validate correctly. Currently, this is only used for forms containing fields with 'hide-if'. - $root.find( '.mw-htmlform' ).removeAttr( 'novalidate' ); - } ); - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/htmlform/images/question.png b/resources/src/mediawiki/htmlform/images/question.png deleted file mode 100644 index 7fc08a05e9..0000000000 Binary files a/resources/src/mediawiki/htmlform/images/question.png and /dev/null differ diff --git a/resources/src/mediawiki/htmlform/images/question.svg b/resources/src/mediawiki/htmlform/images/question.svg deleted file mode 100644 index 655076f341..0000000000 --- a/resources/src/mediawiki/htmlform/images/question.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/src/mediawiki/htmlform/multiselect.js b/resources/src/mediawiki/htmlform/multiselect.js deleted file mode 100644 index e48376367c..0000000000 --- a/resources/src/mediawiki/htmlform/multiselect.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * HTMLForm enhancements: - * Convert multiselect fields from checkboxes to Chosen selector when requested. - */ -( function ( mw, $ ) { - - function addMulti( $oldContainer, $container ) { - var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ), - oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen|mw-htmlform-dropdown)/g, '' ), - $select = $( '