From e1959b29741761edf413256becd63fe692351f6c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Sun, 31 Jul 2016 01:15:07 +0200 Subject: [PATCH] Split the 'mediawiki.htmlform' module code into multiple files This module implements several related, but separate enhancements to HTMLForm. While it makes sense to use a single ResourceLoader module to serve this code, it doesn't make sense to keep all of it in a single file. It was approaching 500 lines of code, and pieces of the separate features were mixed together. This commit mostly shuffles code around, only tweaking some indentation, 'var' statements and function wrappers. There is one small functional change: the enhancements now use the 'htmlform.enhance' mw.hook, which is fired on document-ready and when new fields are added dynamically. Previously it was only used to allow extensions to define their own HTMLForm enhancements. (Also moved HTMLForm styles into the same directory as the newly-split scripts.) Change-Id: I22054b39868239ddb59317dadfaaa067653f8804 --- resources/Resources.php | 15 +- .../src/mediawiki/htmlform/autocomplete.js | 25 ++ .../src/mediawiki/htmlform/checkmatrix.js | 16 + resources/src/mediawiki/htmlform/cloner.js | 36 ++ resources/src/mediawiki/htmlform/hide-if.js | 212 +++++++++ resources/src/mediawiki/htmlform/htmlform.js | 7 + .../{ => htmlform}/images/question.png | Bin .../{ => htmlform}/images/question.svg | 0 .../src/mediawiki/htmlform/multiselect.js | 67 +++ .../ooui.styles.css} | 0 .../src/mediawiki/htmlform/selectandother.js | 41 ++ .../src/mediawiki/htmlform/selectorother.js | 61 +++ .../styles.css} | 0 resources/src/mediawiki/mediawiki.htmlform.js | 417 ------------------ 14 files changed, 477 insertions(+), 420 deletions(-) create mode 100644 resources/src/mediawiki/htmlform/autocomplete.js create mode 100644 resources/src/mediawiki/htmlform/checkmatrix.js create mode 100644 resources/src/mediawiki/htmlform/cloner.js create mode 100644 resources/src/mediawiki/htmlform/hide-if.js create mode 100644 resources/src/mediawiki/htmlform/htmlform.js rename resources/src/mediawiki/{ => htmlform}/images/question.png (100%) rename resources/src/mediawiki/{ => htmlform}/images/question.svg (100%) create mode 100644 resources/src/mediawiki/htmlform/multiselect.js rename resources/src/mediawiki/{mediawiki.htmlform.ooui.css => htmlform/ooui.styles.css} (100%) create mode 100644 resources/src/mediawiki/htmlform/selectandother.js create mode 100644 resources/src/mediawiki/htmlform/selectorother.js rename resources/src/mediawiki/{mediawiki.htmlform.css => htmlform/styles.css} (100%) delete mode 100644 resources/src/mediawiki/mediawiki.htmlform.js diff --git a/resources/Resources.php b/resources/Resources.php index d2ee1cdc0b..1372a8e8f0 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1068,7 +1068,16 @@ return [ 'styles' => 'resources/src/mediawiki/mediawiki.hlist.css', ], 'mediawiki.htmlform' => [ - 'scripts' => 'resources/src/mediawiki/mediawiki.htmlform.js', + 'scripts' => [ + 'resources/src/mediawiki/htmlform/htmlform.js', + 'resources/src/mediawiki/htmlform/autocomplete.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', 'jquery.byteLimit', @@ -1081,12 +1090,12 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.htmlform.styles' => [ - 'styles' => 'resources/src/mediawiki/mediawiki.htmlform.css', + 'styles' => 'resources/src/mediawiki/htmlform/styles.css', 'position' => 'top', 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.htmlform.ooui.styles' => [ - 'styles' => 'resources/src/mediawiki/mediawiki.htmlform.ooui.css', + 'styles' => 'resources/src/mediawiki/htmlform/ooui.styles.css', 'position' => 'top', 'targets' => [ 'desktop', 'mobile' ], ], 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/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..f739746f83 --- /dev/null +++ b/resources/src/mediawiki/htmlform/hide-if.js @@ -0,0 +1,212 @@ +/* + * 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|null} + */ + function hideIfGetField( $el, name ) { + var $found, $p, + 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 ) { + 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 {jQuery} return.0 Dependent fields + * @return {Function} return.1 Test function + */ + function hideIfParse( $el, spec ) { + var op, i, l, v, $field, $fields, 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 ( !$.isArray( spec[ i ] ) ) { + throw new Error( op + ' parameters must be arrays' ); + } + v = hideIfParse( $el, spec[ i ] ); + fields = fields.concat( v[ 0 ].toArray() ); + funcs.push( v[ 1 ] ); + } + $fields = $( fields ); + + 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 ( !$.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.first().prop( 'type' ) === 'radio' || + $field.first().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 ) { + $root.find( '.mw-htmlform-hide-if' ).each( function () { + var v, $fields, test, func, + $el = $( this ), + spec = $el.data( 'hideIf' ); + + if ( !spec ) { + return; + } + + v = hideIfParse( $el, spec ); + $fields = v[ 0 ]; + test = v[ 1 ]; + func = function () { + if ( test() ) { + $el.hide(); + } else { + $el.show(); + } + }; + $fields.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..19f8f3ece5 --- /dev/null +++ b/resources/src/mediawiki/htmlform/htmlform.js @@ -0,0 +1,7 @@ +( function ( mw, $ ) { + + $( function () { + mw.hook( 'htmlform.enhance' ).fire( $( document ) ); + } ); + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/images/question.png b/resources/src/mediawiki/htmlform/images/question.png similarity index 100% rename from resources/src/mediawiki/images/question.png rename to resources/src/mediawiki/htmlform/images/question.png diff --git a/resources/src/mediawiki/images/question.svg b/resources/src/mediawiki/htmlform/images/question.svg similarity index 100% rename from resources/src/mediawiki/images/question.svg rename to resources/src/mediawiki/htmlform/images/question.svg diff --git a/resources/src/mediawiki/htmlform/multiselect.js b/resources/src/mediawiki/htmlform/multiselect.js new file mode 100644 index 0000000000..3cdab3c63c --- /dev/null +++ b/resources/src/mediawiki/htmlform/multiselect.js @@ -0,0 +1,67 @@ +/* + * 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)/g, '' ), + $select = $( '' ), - dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' ); - oldClass = $.trim( oldClass ); - $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 = $( '
  • ' ) - .addClass( 'mw-htmlform-cloner-li' ) - .html( html ) - .appendTo( $ul ); - - enhance( $li ); - } ); - - mw.hook( 'htmlform.enhance' ).fire( $root ); - - } - - $( function () { - enhance( $( document ) ); - } ); - - /** - * @class jQuery - * @mixins jQuery.plugin.htmlform - */ -}( mediaWiki, jQuery ) ); -- 2.20.1