],
'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',
],
'mediawiki.htmlform.checker' => [
'scripts' => [
- 'resources/src/mediawiki/htmlform/htmlform.Checker.js',
+ 'resources/src/mediawiki.htmlform.checker.js',
],
'dependencies' => [
'jquery.throttle-debounce',
],
'mediawiki.htmlform.ooui' => [
'scripts' => [
- 'resources/src/mediawiki/htmlform/htmlform.Element.js',
+ 'resources/src/mediawiki.htmlform.ooui/Element.js',
],
'dependencies' => [
'oojs-ui-core',
'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' => [
--- /dev/null
+( 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 = $( '<span>' );
+ 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 <input type=text>
+ // 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
+ * `<span>` or `<li>`, 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(), <span> or <ul>
+ // 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 : $( '<li>' ).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 : $( '<li>' ).append( e );
+ } ) )
+ .slideDown();
+ };
+ if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) {
+ $oldErrorBox.slideUp( showFunc );
+ } else {
+ showFunc();
+ }
+ }
+
+ return this;
+ };
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+@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;
+}
--- /dev/null
+( 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 ) );
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21.059" height="21.06">
+ <path fill="#54595d" d="M10.529 0c-5.814 0-10.529 4.714-10.529 10.529s4.715 10.53 10.529 10.53c5.816 0 10.529-4.715 10.529-10.53s-4.712-10.529-10.529-10.529zm-.002 16.767c-.861 0-1.498-.688-1.498-1.516 0-.862.637-1.534 1.498-1.534.828 0 1.5.672 1.5 1.534 0 .827-.672 1.516-1.5 1.516zm2.137-6.512c-.723.568-1 .931-1 1.739v.5h-2.205v-.603c0-1.517.449-2.136 1.154-2.688.707-.552 1.139-.845 1.139-1.637 0-.672-.414-1.051-1.24-1.051-.707 0-1.328.189-1.982.638l-1.051-1.807c.861-.604 1.93-1.034 3.342-1.034 1.912 0 3.516 1.051 3.516 3.066-.001 1.43-.794 2.188-1.673 2.877z"/>
+</svg>
--- /dev/null
+/* 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 );
+}
--- /dev/null
+/*
+ * 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 ) );
--- /dev/null
+/*
+ * 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 ) );
--- /dev/null
+/*
+ * 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 ) );
--- /dev/null
+/*
+ * 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 = $( '<li>' )
+ .addClass( 'mw-htmlform-cloner-li' )
+ .html( html )
+ .appendTo( $ul );
+
+ mw.hook( 'htmlform.enhance' ).fire( $li );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*
+ * 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 ) );
--- /dev/null
+( 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 ) );
--- /dev/null
+/*
+ * 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 = $( '<select>' ),
+ dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
+ oldClass = oldClass.trim();
+ $select.attr( {
+ name: name,
+ multiple: 'multiple',
+ 'data-placeholder': dataPlaceholder.plain(),
+ 'class': 'htmlform-chzn-select mw-input ' + oldClass
+ } );
+ $oldContainer.find( 'input' ).each( function () {
+ var $oldInput = $( this ),
+ checked = $oldInput.prop( 'checked' ),
+ $option = $( '<option>' );
+ $option.prop( 'value', $oldInput.prop( 'value' ) );
+ if ( checked ) {
+ $option.prop( 'selected', true );
+ }
+ $option.text( $oldInput.prop( 'value' ) );
+ $select.append( $option );
+ } );
+ $container.append( $select );
+ }
+
+ function convertCheckboxesToMulti( $oldContainer, type ) {
+ var $fieldLabel = $( '<td>' ),
+ $td = $( '<td>' ),
+ $fieldLabelText = $( '<label>' ),
+ $container;
+ if ( type === 'tr' ) {
+ addMulti( $oldContainer, $td );
+ $container = $( '<tr>' );
+ $container.append( $td );
+ } else if ( type === 'div' ) {
+ $fieldLabel = $( '<div>' );
+ $container = $( '<div>' );
+ addMulti( $oldContainer, $container );
+ }
+ $fieldLabel.attr( 'class', 'mw-label' );
+ $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() );
+ $fieldLabel.append( $fieldLabelText );
+ $container.prepend( $fieldLabel );
+ $oldContainer.replaceWith( $container );
+ return $container;
+ }
+
+ function convertCheckboxesWidgetToTags( fieldLayout ) {
+ var checkboxesWidget, checkboxesOptions, menuTagOptions, menuTagWidget;
+
+ checkboxesWidget = fieldLayout.fieldWidget;
+ checkboxesOptions = checkboxesWidget.checkboxMultiselectWidget.getItems();
+ menuTagOptions = checkboxesOptions.map( function ( option ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: option.getData(),
+ label: option.getLabel()
+ } );
+ } );
+ menuTagWidget = new OO.ui.MenuTagMultiselectWidget( {
+ $overlay: true,
+ menu: {
+ items: menuTagOptions
+ }
+ } );
+ menuTagWidget.setValue( checkboxesWidget.getValue() );
+
+ // Data from CapsuleMultiselectWidget will not be submitted with the form, so keep the original
+ // CheckboxMultiselectInputWidget up-to-date.
+ menuTagWidget.on( 'change', function () {
+ checkboxesWidget.setValue( menuTagWidget.getValue() );
+ } );
+
+ // Hide original widget and add new one in its place. This is a bit hacky, since the FieldLayout
+ // still thinks it's connected to the old widget.
+ checkboxesWidget.toggle( false );
+ checkboxesWidget.$element.after( menuTagWidget.$element );
+ }
+
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ var $dropdowns = $root.find( '.mw-htmlform-field-HTMLMultiSelectField.mw-htmlform-dropdown' );
+ if ( $dropdowns.length ) {
+ $dropdowns.each( function () {
+ var $el = $( this ),
+ data, modules, extraModules;
+ if ( $el.is( '[data-ooui]' ) ) {
+ // Load 'oojs-ui-widgets' for CapsuleMultiselectWidget
+ modules = [ 'mediawiki.htmlform.ooui', 'oojs-ui-widgets' ];
+ 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, function () {
+ convertCheckboxesWidgetToTags( OO.ui.FieldLayout.static.infuse( $el ) );
+ } );
+ } else {
+ mw.loader.using( 'jquery.chosen', function () {
+ var type = $el.is( 'tr' ) ? 'tr' : 'div',
+ $converted = convertCheckboxesToMulti( $el, type );
+ $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
+ } );
+ }
+ } );
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*
+ * HTMLForm enhancements:
+ * Add a dynamic max length to the reason field of SelectAndOther.
+ */
+( function ( mw, $ ) {
+
+ // cache the separator to avoid object creation on each keypress
+ var colonSeparator = mw.message( 'colon-separator' ).text();
+
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ // This checks the length together with the value from the select field
+ // When the reason list is changed and the bytelimit is longer than the allowed,
+ // nothing is done
+ $root
+ .find( '.mw-htmlform-select-and-other-field' )
+ .each( function () {
+ var $reasonList, currentValReasonList, maxlengthUnit, lengthLimiter, widget,
+ $this = $( this ),
+ $widget = $this.closest( '.oo-ui-widget[data-ooui]' );
+
+ if ( $widget ) {
+ mw.loader.using( 'mediawiki.widgets.SelectWithInputWidget', function () {
+ widget = OO.ui.Widget.static.infuse( $widget );
+ maxlengthUnit = widget.getData().maxlengthUnit;
+ lengthLimiter = maxlengthUnit === 'codepoints' ? 'codePointLimit' : 'byteLimit';
+ widget.textinput.$input[ lengthLimiter ]( function ( input ) {
+ // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
+ var comment = widget.dropdowninput.getValue();
+ if ( comment === 'other' ) {
+ comment = input;
+ } else if ( input !== '' ) {
+ // Entry from drop down menu + additional comment
+ comment += colonSeparator + input;
+ }
+ return comment;
+ } );
+ } );
+ } else {
+ // find the reason list
+ $reasonList = $root.find( '#' + $this.data( 'id-select' ) );
+ // cache the current selection to avoid expensive lookup
+ currentValReasonList = $reasonList.val();
+
+ $reasonList.change( function () {
+ currentValReasonList = $reasonList.val();
+ } );
+
+ // Select the function for the length limit
+ maxlengthUnit = $this.data( 'mw-maxlength-unit' );
+ lengthLimiter = maxlengthUnit === 'codepoints' ? 'codePointLimit' : 'byteLimit';
+ $this[ lengthLimiter ]( function ( input ) {
+ // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
+ var comment = currentValReasonList;
+ if ( comment === 'other' ) {
+ comment = input;
+ } else if ( input !== '' ) {
+ // Entry from drop down menu + additional comment
+ comment += colonSeparator + input;
+ }
+ return comment;
+ } );
+ }
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*
+ * HTMLForm enhancements:
+ * Animate the SelectOrOther fields, to only show the text field when 'other' is selected.
+ */
+( function ( mw, $ ) {
+
+ /**
+ * @class jQuery.plugin.htmlform
+ */
+
+ /**
+ * jQuery plugin to fade or snap to visible state.
+ *
+ * @param {boolean} [instantToggle=false]
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.goIn = function ( instantToggle ) {
+ if ( instantToggle === true ) {
+ return this.show();
+ }
+ return this.stop( true, true ).fadeIn();
+ };
+
+ /**
+ * jQuery plugin to fade or snap to hiding state.
+ *
+ * @param {boolean} [instantToggle=false]
+ * @return {jQuery}
+ * @chainable
+ */
+ $.fn.goOut = function ( instantToggle ) {
+ if ( instantToggle === true ) {
+ return this.hide();
+ }
+ return this.stop( true, true ).fadeOut();
+ };
+
+ mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+ /**
+ * @ignore
+ * @param {boolean|jQuery.Event} instant
+ */
+ function handleSelectOrOther( instant ) {
+ var $other = $root.find( '#' + $( this ).attr( 'id' ) + '-other' );
+ $other = $other.add( $other.siblings( 'br' ) );
+ if ( $( this ).val() === 'other' ) {
+ $other.goIn( instant );
+ } else {
+ $other.goOut( instant );
+ }
+ }
+
+ $root
+ .on( 'change', '.mw-htmlform-select-or-other', handleSelectOrOther )
+ .find( '.mw-htmlform-select-or-other' )
+ .each( function () {
+ handleSelectOrOther.call( this, true );
+ } );
+ } );
+
+}( mediaWiki, jQuery ) );
+++ /dev/null
-/*
- * 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 ) );
+++ /dev/null
-/*
- * 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 ) );
+++ /dev/null
-/*
- * 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 ) );
+++ /dev/null
-/*
- * 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 = $( '<li>' )
- .addClass( 'mw-htmlform-cloner-li' )
- .html( html )
- .appendTo( $ul );
-
- mw.hook( 'htmlform.enhance' ).fire( $li );
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*
- * 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 ) );
+++ /dev/null
-( function ( mw, $ ) {
-
- 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 = $( '<span>' );
- 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 <input type=text>
- // 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
- * `<span>` or `<li>`, 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(), <span> or <ul>
- // 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 : $( '<li>' ).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 : $( '<li>' ).append( e );
- } ) )
- .slideDown();
- };
- if ( $oldErrorBox !== $errorBox && $oldErrorBox.hasClass( 'error' ) ) {
- $oldErrorBox.slideUp( showFunc );
- } else {
- showFunc();
- }
- }
-
- return this;
- };
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( 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 ) );
+++ /dev/null
-( 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 ) );
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="21.059" height="21.06">
- <path fill="#54595d" d="M10.529 0c-5.814 0-10.529 4.714-10.529 10.529s4.715 10.53 10.529 10.53c5.816 0 10.529-4.715 10.529-10.53s-4.712-10.529-10.529-10.529zm-.002 16.767c-.861 0-1.498-.688-1.498-1.516 0-.862.637-1.534 1.498-1.534.828 0 1.5.672 1.5 1.534 0 .827-.672 1.516-1.5 1.516zm2.137-6.512c-.723.568-1 .931-1 1.739v.5h-2.205v-.603c0-1.517.449-2.136 1.154-2.688.707-.552 1.139-.845 1.139-1.637 0-.672-.414-1.051-1.24-1.051-.707 0-1.328.189-1.982.638l-1.051-1.807c.861-.604 1.93-1.034 3.342-1.034 1.912 0 3.516 1.051 3.516 3.066-.001 1.43-.794 2.188-1.673 2.877z"/>
-</svg>
+++ /dev/null
-/*
- * 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 = $( '<select>' ),
- dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
- oldClass = oldClass.trim();
- $select.attr( {
- name: name,
- multiple: 'multiple',
- 'data-placeholder': dataPlaceholder.plain(),
- 'class': 'htmlform-chzn-select mw-input ' + oldClass
- } );
- $oldContainer.find( 'input' ).each( function () {
- var $oldInput = $( this ),
- checked = $oldInput.prop( 'checked' ),
- $option = $( '<option>' );
- $option.prop( 'value', $oldInput.prop( 'value' ) );
- if ( checked ) {
- $option.prop( 'selected', true );
- }
- $option.text( $oldInput.prop( 'value' ) );
- $select.append( $option );
- } );
- $container.append( $select );
- }
-
- function convertCheckboxesToMulti( $oldContainer, type ) {
- var $fieldLabel = $( '<td>' ),
- $td = $( '<td>' ),
- $fieldLabelText = $( '<label>' ),
- $container;
- if ( type === 'tr' ) {
- addMulti( $oldContainer, $td );
- $container = $( '<tr>' );
- $container.append( $td );
- } else if ( type === 'div' ) {
- $fieldLabel = $( '<div>' );
- $container = $( '<div>' );
- addMulti( $oldContainer, $container );
- }
- $fieldLabel.attr( 'class', 'mw-label' );
- $fieldLabelText.text( $oldContainer.find( '.mw-label label' ).text() );
- $fieldLabel.append( $fieldLabelText );
- $container.prepend( $fieldLabel );
- $oldContainer.replaceWith( $container );
- return $container;
- }
-
- function convertCheckboxesWidgetToTags( fieldLayout ) {
- var checkboxesWidget, checkboxesOptions, menuTagOptions, menuTagWidget;
-
- checkboxesWidget = fieldLayout.fieldWidget;
- checkboxesOptions = checkboxesWidget.checkboxMultiselectWidget.getItems();
- menuTagOptions = checkboxesOptions.map( function ( option ) {
- return new OO.ui.MenuOptionWidget( {
- data: option.getData(),
- label: option.getLabel()
- } );
- } );
- menuTagWidget = new OO.ui.MenuTagMultiselectWidget( {
- $overlay: true,
- menu: {
- items: menuTagOptions
- }
- } );
- menuTagWidget.setValue( checkboxesWidget.getValue() );
-
- // Data from CapsuleMultiselectWidget will not be submitted with the form, so keep the original
- // CheckboxMultiselectInputWidget up-to-date.
- menuTagWidget.on( 'change', function () {
- checkboxesWidget.setValue( menuTagWidget.getValue() );
- } );
-
- // Hide original widget and add new one in its place. This is a bit hacky, since the FieldLayout
- // still thinks it's connected to the old widget.
- checkboxesWidget.toggle( false );
- checkboxesWidget.$element.after( menuTagWidget.$element );
- }
-
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- var $dropdowns = $root.find( '.mw-htmlform-field-HTMLMultiSelectField.mw-htmlform-dropdown' );
- if ( $dropdowns.length ) {
- $dropdowns.each( function () {
- var $el = $( this ),
- data, modules, extraModules;
- if ( $el.is( '[data-ooui]' ) ) {
- // Load 'oojs-ui-widgets' for CapsuleMultiselectWidget
- modules = [ 'mediawiki.htmlform.ooui', 'oojs-ui-widgets' ];
- 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, function () {
- convertCheckboxesWidgetToTags( OO.ui.FieldLayout.static.infuse( $el ) );
- } );
- } else {
- mw.loader.using( 'jquery.chosen', function () {
- var type = $el.is( 'tr' ) ? 'tr' : 'div',
- $converted = convertCheckboxesToMulti( $el, type );
- $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
- } );
- }
- } );
- }
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-@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;
-}
+++ /dev/null
-/*
- * HTMLForm enhancements:
- * Add a dynamic max length to the reason field of SelectAndOther.
- */
-( function ( mw, $ ) {
-
- // cache the separator to avoid object creation on each keypress
- var colonSeparator = mw.message( 'colon-separator' ).text();
-
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- // This checks the length together with the value from the select field
- // When the reason list is changed and the bytelimit is longer than the allowed,
- // nothing is done
- $root
- .find( '.mw-htmlform-select-and-other-field' )
- .each( function () {
- var $reasonList, currentValReasonList, maxlengthUnit, lengthLimiter, widget,
- $this = $( this ),
- $widget = $this.closest( '.oo-ui-widget[data-ooui]' );
-
- if ( $widget ) {
- mw.loader.using( 'mediawiki.widgets.SelectWithInputWidget', function () {
- widget = OO.ui.Widget.static.infuse( $widget );
- maxlengthUnit = widget.getData().maxlengthUnit;
- lengthLimiter = maxlengthUnit === 'codepoints' ? 'codePointLimit' : 'byteLimit';
- widget.textinput.$input[ lengthLimiter ]( function ( input ) {
- // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
- var comment = widget.dropdowninput.getValue();
- if ( comment === 'other' ) {
- comment = input;
- } else if ( input !== '' ) {
- // Entry from drop down menu + additional comment
- comment += colonSeparator + input;
- }
- return comment;
- } );
- } );
- } else {
- // find the reason list
- $reasonList = $root.find( '#' + $this.data( 'id-select' ) );
- // cache the current selection to avoid expensive lookup
- currentValReasonList = $reasonList.val();
-
- $reasonList.change( function () {
- currentValReasonList = $reasonList.val();
- } );
-
- // Select the function for the length limit
- maxlengthUnit = $this.data( 'mw-maxlength-unit' );
- lengthLimiter = maxlengthUnit === 'codepoints' ? 'codePointLimit' : 'byteLimit';
- $this[ lengthLimiter ]( function ( input ) {
- // Should be built the same as in HTMLSelectAndOtherField::loadDataFromRequest
- var comment = currentValReasonList;
- if ( comment === 'other' ) {
- comment = input;
- } else if ( input !== '' ) {
- // Entry from drop down menu + additional comment
- comment += colonSeparator + input;
- }
- return comment;
- } );
- }
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/*
- * HTMLForm enhancements:
- * Animate the SelectOrOther fields, to only show the text field when 'other' is selected.
- */
-( function ( mw, $ ) {
-
- /**
- * @class jQuery.plugin.htmlform
- */
-
- /**
- * jQuery plugin to fade or snap to visible state.
- *
- * @param {boolean} [instantToggle=false]
- * @return {jQuery}
- * @chainable
- */
- $.fn.goIn = function ( instantToggle ) {
- if ( instantToggle === true ) {
- return this.show();
- }
- return this.stop( true, true ).fadeIn();
- };
-
- /**
- * jQuery plugin to fade or snap to hiding state.
- *
- * @param {boolean} [instantToggle=false]
- * @return {jQuery}
- * @chainable
- */
- $.fn.goOut = function ( instantToggle ) {
- if ( instantToggle === true ) {
- return this.hide();
- }
- return this.stop( true, true ).fadeOut();
- };
-
- mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
- /**
- * @ignore
- * @param {boolean|jQuery.Event} instant
- */
- function handleSelectOrOther( instant ) {
- var $other = $root.find( '#' + $( this ).attr( 'id' ) + '-other' );
- $other = $other.add( $other.siblings( 'br' ) );
- if ( $( this ).val() === 'other' ) {
- $other.goIn( instant );
- } else {
- $other.goOut( instant );
- }
- }
-
- $root
- .on( 'change', '.mw-htmlform-select-or-other', handleSelectOrOther )
- .find( '.mw-htmlform-select-or-other' )
- .each( function () {
- handleSelectOrOther.call( this, true );
- } );
- } );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/* 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 );
-}