r94051 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r94050‎ | r94051 | r94052 >
Date:23:20, 7 August 2011
Author:salvatoreingala
Status:deferred
Tags:
Comment:
- Added possibility to insert 'range', 'date', 'color' and 'bundle' fields. 'select' is still missing.
- Added ability to move fields between tabs by drag&drop over the tab.
- Improved validation of field descriptions in costructors.
- Fixed several bugs, improved code.
- Minor refactoring of jquery.formBuilder.js, in order to loosen coupling with gadgets.
Modified paths:
  • /branches/salvatoreingala/Gadgets/Gadgets.i18n.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/Gadgets.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js (modified) (history)
  • /branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css (modified) (history)
  • /branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js (modified) (history)

Diff [purge]

Index: branches/salvatoreingala/Gadgets/Gadgets.i18n.php
@@ -73,7 +73,7 @@
7474 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please chose a different name.',
7575 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title',
7676 'gadgets-formbuilder-editor-delete-section' => 'Delete this section and all his content',
77 - 'gadgets-formbuilder-editor-new-section' => 'Delete this section and all his content',
 77+ 'gadgets-formbuilder-editor-new-section' => 'Create a new section',
7878 'gadgets-formbuilder-editor-chose-title' => 'Chose the title of the new section:',
7979 'gadgets-formbuilder-editor-chose-title-title' => 'Chose section title',
8080 );
Index: branches/salvatoreingala/Gadgets/Gadgets.php
@@ -97,7 +97,7 @@
9898 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-insert-field',
9999 'gadgets-formbuilder-editor-chose-field', 'gadgets-formbuilder-editor-chose-field-title', 'gadgets-formbuilder-editor-create-field-title',
100100 'gadgets-formbuilder-editor-duplicate-name', 'gadgets-formbuilder-editor-delete-section', 'gadgets-formbuilder-editor-new-section',
101 - 'gadgets-formbuilder-editor-chose-title', 'gadgets-formbuilder-editor-chose-title-title'
 101+ 'gadgets-formbuilder-editor-edit-section', 'gadgets-formbuilder-editor-chose-title', 'gadgets-formbuilder-editor-chose-title-title'
102102 ),
103103 'localBasePath' => $dir . 'ui/resources/',
104104 'remoteExtPath' => 'Gadgets/ui/resources'
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css
@@ -48,8 +48,6 @@
4949
5050 .formbuilder-slot {
5151 border: none;
52 - padding-top: 0.5em;
53 - padding-bottom: 0.5em;
5452 }
5553
5654 /* formBuilder editor */
@@ -65,11 +63,12 @@
6664 }
6765
6866 .formbuilder-slot-can-drop {
69 - border: 1px solid black;
 67+ background: blue;
 68+ opacity: 0.1;
7069 }
7170
7271 .formbuilder-editor-slot-buttons {
73 - height: 19px;
 72+ height: 17px;
7473 }
7574
7675 .formbuilder-editor-button {
@@ -80,22 +79,21 @@
8180 cursor: move;
8281 }
8382
84 -.formbuilder-editor-button-new, .formbuilder-editor-button-new-section {
 83+.formbuilder-editor-button-new, .formbuilder-editor-button-new-section,
 84+.formbuilder-editor-button-edit, .formbuilder-editor-button-move
 85+{
8586 float: left;
8687 }
8788
88 -.formbuilder-editor-button-edit {
89 - float: left;
 89+.formbuilder-editor-button-delete {
 90+ float: right;
9091 }
9192
92 -.formbuilder-editor-button-move {
 93+/* Fixes a minor glitch in Firefox (buttons in tabs not floating properly) */
 94+.formbuilder .ui-tabs-nav a > span :not(.formbuilder-editor-button) {
9395 float: left;
9496 }
9597
96 -.formbuilder-editor-button-delete {
97 - float: right;
98 -}
99 -
10098 .formbuilder-editor-button-delete-section {
10199 float: right;
102100 }
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js
@@ -5,20 +5,21 @@
66 */
77
88 (function($, mw) {
9 -
10 - var idPrefix = "mw-gadgets-dialog-";
11 -
 9+
1210 //Preprocesses strings end possibly replaces them with messages.
1311 //If str starts with "@" the rest of the string is assumed to be
1412 //a message, and the result of mw.msg is returned.
1513 //Two "@@" at the beginning escape for a single "@".
16 - function preproc( prefix, str ) {
 14+ function preproc( msgPrefix, str ) {
1715 if ( str.length <= 1 || str[0] !== '@' ) {
1816 return str;
1917 } else if ( str.substr( 0, 2 ) == '@@' ) {
2018 return str.substr( 1 );
2119 } else {
22 - return mw.message( prefix + str.substr( 1 ) ).plain();
 20+ if ( !msgPrefix ) {
 21+ msgPrefix = "";
 22+ }
 23+ return mw.message( msgPrefix + str.substr( 1 ) ).plain();
2324 }
2425 }
2526
@@ -45,6 +46,10 @@
4647 return res;
4748 }
4849
 50+ function isInteger( val ) {
 51+ return typeof val == 'number' && val === Math.floor( val );
 52+ }
 53+
4954 function testOptional( value, element ) {
5055 var rules = $( element ).rules();
5156 if ( typeof rules.required == 'undefined' || rules.required === false ) {
@@ -146,7 +151,7 @@
147152 "type": "number",
148153 "label": "maxlength",
149154 "integer": true,
150 - "min": 0,
 155+ "min": 1,
151156 "required": false,
152157 "default": null
153158 }
@@ -178,13 +183,54 @@
179184 "required": false,
180185 "default": null
181186 }
182 - //TODO: other fields
183 - ] )
 187+ ] ),
 188+ //TODO: "select" is missing
 189+ "range": simpleField.concat( [
 190+ {
 191+ "name": "min",
 192+ "type": "number",
 193+ "label": "min",
 194+ "required": true,
 195+ },
 196+ {
 197+ "name": "step",
 198+ "type": "number",
 199+ "label": "step",
 200+ "required": true,
 201+ "default": 1
 202+ },
 203+ {
 204+ "name": "max",
 205+ "type": "number",
 206+ "label": "max",
 207+ "required": true,
 208+ }
 209+ ] ),
 210+ "date": simpleField,
 211+ "color": simpleField,
 212+ "bundle": function( options ) {
 213+ return new BundleField( {
 214+ "type": "bundle",
 215+ "sections": [
 216+ {
 217+ "title": "Section 1",
 218+ "fields": []
 219+ },
 220+ {
 221+ "title": "Section 2",
 222+ "fields": []
 223+ }
 224+ ]
 225+ }, options )
 226+ }
184227 };
185228
186229 /* Basic interface for fields */
187230 function Field( desc, options ) {
188 - this.prefix = options.prefix;
 231+ if ( typeof options.idPrefix == 'undefined' ) {
 232+ options.idPrefix = 'formbuilder-';
 233+ }
 234+
189235 this.desc = desc;
190236 this.options = options;
191237 }
@@ -241,8 +287,8 @@
242288 }
243289
244290 var $label = $( '<label/>' )
245 - .text( preproc( this.prefix, this.desc.label ) )
246 - .attr('for', idPrefix + this.desc.name );
 291+ .text( preproc( this.options.msgPrefix, this.desc.label ) )
 292+ .attr('for', this.options.idPrefix + this.desc.name );
247293
248294 this.$div.append( $label );
249295 }
@@ -252,6 +298,27 @@
253299 SimpleField.prototype.constructor = SimpleField;
254300 function SimpleField( desc, options ){
255301 LabelField.call( this, desc, options );
 302+
 303+ //Validate the 'name' member
 304+ if ( typeof desc.name != 'string' || /^[a-zA-Z_][a-zA-Z0-9_]*$/.test( desc.name ) === false ) {
 305+ $.error( 'invalid name' );
 306+ }
 307+ if ( typeof desc.name.length > 40 ) {
 308+ $.error( 'name must be no longer than 40 characters' );
 309+ }
 310+
 311+ //Use default if it is given and no value has been set
 312+ if ( ( typeof options.values == 'undefined' || typeof options.values[desc.name] == 'undefined' )
 313+ && typeof desc['default'] != 'undefined' )
 314+ {
 315+ if ( typeof options.values == 'undefined' ) {
 316+ options.values = {};
 317+ }
 318+
 319+ options.values[desc.name] = desc['default'];
 320+
 321+ this.options = options;
 322+ }
256323 }
257324
258325 SimpleField.prototype.getDesc = function( useValuesAsDefaults ) {
@@ -274,8 +341,8 @@
275342
276343 this.$c = $( '<input/>' ).attr( {
277344 type: 'checkbox',
278 - id: idPrefix + this.desc.name,
279 - name: idPrefix + this.desc.name
 345+ id: this.options.idPrefix + this.desc.name,
 346+ name: this.options.idPrefix + this.desc.name
280347 } );
281348
282349 var value = options.values && options.values[this.desc.name];
@@ -302,10 +369,24 @@
303370 function StringField( desc, options ){
304371 SimpleField.call( this, desc, options );
305372
 373+ //Validate minlength and maxlength
 374+ var minlength = typeof desc.minlength != 'undefined' ? desc.minlength : 0,
 375+ maxlength = typeof desc.maxlength != 'undefined' ? desc.maxlength : 1024;
 376+
 377+ if ( !isInteger( minlength ) || minlength < 0 ) {
 378+ $.error( "minlength must be a non-negative integer" );
 379+ }
 380+ if ( !isInteger( maxlength ) || maxlength <= 0 ) {
 381+ $.error( "maxlength must be a positive integer" );
 382+ }
 383+ if ( maxlength < minlength ) {
 384+ $.error( "maxlength must be no less than minlength" );
 385+ }
 386+
306387 this.$text = $( '<input/>' ).attr( {
307388 type: 'text',
308 - id: idPrefix + this.desc.name,
309 - name: idPrefix + this.desc.name
 389+ id: this.options.idPrefix + this.desc.name,
 390+ name: this.options.idPrefix + this.desc.name
310391 } );
311392
312393 var value = options.values && options.values[this.desc.name];
@@ -326,7 +407,7 @@
327408
328409 StringField.prototype.getValidationSettings = function() {
329410 var settings = SimpleField.prototype.getValidationSettings.call( this ),
330 - fieldId = idPrefix + this.desc.name;
 411+ fieldId = this.options.idPrefix + this.desc.name;
331412
332413 settings.rules[fieldId] = {};
333414 var fieldRules = settings.rules[fieldId],
@@ -362,10 +443,25 @@
363444 function NumberField( desc, options ){
364445 SimpleField.call( this, desc, options );
365446
 447+ //Validation of description
 448+ if ( desc.integer === true ) {
 449+ if ( typeof desc.min != 'undefined' && !isInteger( desc.min ) ){
 450+ $.error( "min is not an integer" );
 451+ }
 452+ if ( typeof desc.max != 'undefined' && !isInteger( desc.max ) ){
 453+ $.error( "max is not an integer" );
 454+ }
 455+ }
 456+
 457+ if ( typeof desc.min != 'undefined' && typeof desc.max != 'undefined' && desc.min > desc.max ) {
 458+ $.error( 'max must be no less than min' );
 459+ }
 460+
 461+
366462 this.$text = $( '<input/>' ).attr( {
367463 type: 'text',
368 - id: idPrefix + this.desc.name,
369 - name: idPrefix + this.desc.name
 464+ id: this.options.idPrefix + this.desc.name,
 465+ name: this.options.idPrefix + this.desc.name
370466 } );
371467
372468 var value = options.values && options.values[this.desc.name];
@@ -387,7 +483,7 @@
388484
389485 NumberField.prototype.getValidationSettings = function() {
390486 var settings = SimpleField.prototype.getValidationSettings.call( this ),
391 - fieldId = idPrefix + this.desc.name;
 487+ fieldId = this.options.idPrefix + this.desc.name;
392488
393489 settings.rules[fieldId] = {};
394490 var fieldRules = settings.rules[fieldId],
@@ -429,8 +525,8 @@
430526 SimpleField.call( this, desc, options );
431527
432528 var $select = this.$select = $( '<select/>' ).attr( {
433 - id: idPrefix + this.desc.name,
434 - name: idPrefix + this.desc.name
 529+ id: this.options.idPrefix + this.desc.name,
 530+ name: this.options.idPrefix + this.desc.name
435531 } );
436532
437533 var validValues = [];
@@ -438,7 +534,7 @@
439535 $.each( this.desc.options, function( idx, option ) {
440536 var i = validValues.length;
441537 $( '<option/>' )
442 - .text( preproc( self.prefix, option.name ) )
 538+ .text( preproc( self.options.msgPrefix, option.name ) )
443539 .val( i )
444540 .appendTo( $select );
445541 validValues.push( option.value );
@@ -472,18 +568,22 @@
473569 function RangeField( desc, options ){
474570 SimpleField.call( this, desc, options );
475571
476 - if ( typeof this.desc.min != 'number' ) {
477 - $.error( "desc.min is invalid" );
 572+ //Validation
 573+ if ( desc.min > desc.max ) {
 574+ $.error( "max must be no less than min" );
478575 }
479 -
480 - if ( typeof this.desc.max != 'number' ) {
481 - $.error( "desc.max is invalid" );
 576+ if ( desc.step <= 0 ) {
 577+ $.error( "step must be a positive number" );
482578 }
483579
484 - if ( typeof this.desc.step != 'undefined' && typeof this.desc.step != 'number' ) {
485 - $.error( "desc.step is invalid" );
 580+ //Check that max differs from min by an integer multiple of step
 581+ //(that is: (max - min) / step is integer, with good approximation)
 582+ var eps = 1.0e-6; //tolerance
 583+ var tmp = ( desc.max - desc.min ) / desc.step;
 584+ if ( Math.abs( tmp - Math.floor( tmp ) ) > eps ) {
 585+ $.error( "The list {min, min + step, min + 2*step, ...} must contain max" );
486586 }
487 -
 587+
488588 var value = options.values && options.values[this.desc.name];
489589 if ( typeof value != 'undefined' ) {
490590 if ( typeof value != 'number' ) {
@@ -495,7 +595,7 @@
496596 }
497597
498598 var $slider = this.$slider = $( '<div/>' )
499 - .attr( 'id', idPrefix + this.desc.name );
 599+ .attr( 'id', this.options.idPrefix + this.desc.name );
500600
501601 var rangeOptions = {
502602 min: this.desc.min,
@@ -527,11 +627,12 @@
528628 function DateField( desc, options ){
529629 SimpleField.call( this, desc, options );
530630
531 - this.$text = $( '<input/>' ).attr( {
532 - type: 'text',
533 - id: idPrefix + this.desc.name,
534 - name: idPrefix + this.desc.name
535 - } ).datepicker( {
 631+ this.$text = $( '<input/>' )
 632+ .attr( {
 633+ type: 'text',
 634+ id: this.options.idPrefix + this.desc.name,
 635+ name: this.options.idPrefix + this.desc.name
 636+ } ).datepicker( {
536637 onSelect: function() {
537638 //Force validation, so that a previous 'invalid' state is removed
538639 $( this ).valid();
@@ -558,7 +659,7 @@
559660 res = {};
560661
561662 if ( d === null ) {
562 - return null;
 663+ return pair( this.desc.name, null );
563664 }
564665
565666 //UTC date in ISO 8601 format [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z
@@ -573,7 +674,7 @@
574675
575676 DateField.prototype.getValidationSettings = function() {
576677 var settings = SimpleField.prototype.getValidationSettings.call( this ),
577 - fieldId = idPrefix + this.desc.name;
 678+ fieldId = this.options.idPrefix + this.desc.name;
578679
579680 settings.rules[fieldId] = {
580681 "datePicker": true
@@ -604,13 +705,17 @@
605706 function ColorField( desc, options ){
606707 SimpleField.call( this, desc, options );
607708
608 - var value = ( options.values && options.values[this.desc.name] ) || '';
 709+ if ( typeof options.values != 'undefined' && typeof options.values[this.desc.name] != 'undefined' ) {
 710+ value = options.values[this.desc.name];
 711+ } else {
 712+ value = '';
 713+ }
609714
610715 this.$text = $( '<input/>' ).attr( {
611 - type: 'text',
612 - id: idPrefix + this.desc.name,
613 - name: idPrefix + this.desc.name
614 - } )
 716+ type: 'text',
 717+ id: this.options.idPrefix + this.desc.name,
 718+ name: this.options.idPrefix + this.desc.name
 719+ } )
615720 .addClass( 'colorpicker-input' )
616721 .val( value )
617722 .css( 'background-color', value )
@@ -649,7 +754,7 @@
650755
651756 ColorField.prototype.getValidationSettings = function() {
652757 var settings = SimpleField.prototype.getValidationSettings.call( this ),
653 - fieldId = idPrefix + this.desc.name;
 758+ fieldId = this.options.idPrefix + this.desc.name;
654759
655760 settings.rules[fieldId] = {
656761 "color": true
@@ -659,8 +764,12 @@
660765
661766 ColorField.prototype.getValues = function() {
662767 var color = $.colorUtil.getRGB( this.$text.val() );
663 - return pair( this.desc.name, '#' + pad( color[0].toString( 16 ), 2 ) +
664 - pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ) );
 768+ if ( color ) {
 769+ return pair( this.desc.name, '#' + pad( color[0].toString( 16 ), 2 ) +
 770+ pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ) );
 771+ } else {
 772+ return pair( this.desc.name, null );
 773+ }
665774 };
666775
667776 validFieldTypes["color"] = ColorField;
@@ -717,14 +826,12 @@
718827 //TODO: kill "intro"s and make "label" fields, instead?
719828 if ( typeof this.desc.intro == 'string' ) {
720829 $( '<p/>' )
721 - .text( preproc( this.prefix, this.desc.intro ) )
 830+ .text( preproc( this.options.msgPrefix, this.desc.intro ) )
722831 .addClass( 'formBuilder-intro' )
723832 .appendTo( this.$div );
724833 }
725834
726835 for ( var i = 0; i < this.desc.fields.length; i++ ) {
727 - //TODO: validate fieldName
728 -
729836 if ( options.editable === true ) {
730837 //add an empty slot
731838 this._createSlot( true ).appendTo( this.$div );
@@ -855,8 +962,16 @@
856963 type = params.type;
857964 if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) {
858965 $.error( 'createFieldDialog: invalid type: ' + type );
 966+ } else if ( typeof prefsDescriptionSpecifications[type] == 'function' ) {
 967+ var field = prefsDescriptionSpecifications[type]( this.options );
 968+ if ( params.callback( field ) === true ) {
 969+ $( this ).dialog( "close" );
 970+ }
 971+ return;
859972 }
860973
 974+ //typeof prefsDescriptionSpecifications[type] == 'object'
 975+
861976 description = {
862977 fields: prefsDescriptionSpecifications[type]
863978 };
@@ -866,13 +981,6 @@
867982 values = params.values;
868983 } else {
869984 values = {};
870 - //Set defaults (if given) as starting values
871 - //TODO: should field constructors just use default if no value is given?
872 - $.each( description.fields, function( idx, field ) {
873 - if ( typeof field['default'] != 'undefined' ) {
874 - values[field.name] = field['default'];
875 - }
876 - } );
877985 }
878986
879987 //Create the dialog to set field properties
@@ -899,7 +1007,7 @@
9001008 var fieldDescription = $( form ).formBuilder( 'getValues' );
9011009
9021010 if ( typeof type != 'undefined' ) {
903 - //Remove properties the equal their default
 1011+ //Remove properties that equal their default
9041012 $.each( description.fields, function( index, fieldSpec ) {
9051013 var property = fieldSpec.name;
9061014 if ( fieldDescription[property] === fieldSpec['default'] ) {
@@ -969,7 +1077,7 @@
9701078
9711079 if ( editable ) {
9721080 $slot.addClass( 'formbuilder-slot-nonempty' );
973 -
 1081+
9741082 //Add the handle for moving slots
9751083 $( '<span />' )
9761084 .addClass( 'formbuilder-editor-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' )
@@ -980,46 +1088,49 @@
9811089 .appendTo( $divButtons );
9821090
9831091 //Add the button for changing existing slots
984 - $( '<a href="javascript:;" />' )
985 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit ui-icon ui-icon-gear' )
986 - .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) )
987 - .click( function() {
988 - self._createFieldDialog( {
989 - type: field.getDesc().type,
990 - values: field.getDesc(),
991 - callback: function( newField ) {
992 - if ( newField !== null ) {
993 - //check that there are no duplicate preference names
994 - var existingValues = self.$div.closest( 'form' ).formBuilder( 'getValues' ),
995 - removedValues = field.getValues(),
996 - duplicateName = null;
997 - $.each( field.getValues(), function( name, val ) {
998 - //Only complain for preference names that are not in names for the field being replaced
999 - if ( typeof existingValues[name] != 'undefined' && removedValues[name] == 'undefined' ) {
1000 - duplicateName = name;
 1092+ var type = field.getDesc().type;
 1093+ if ( typeof prefsDescriptionSpecifications[type] != 'function' ) {
 1094+ $( '<a href="javascript:;" />' )
 1095+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit ui-icon ui-icon-gear' )
 1096+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) )
 1097+ .click( function() {
 1098+ self._createFieldDialog( {
 1099+ type: field.getDesc().type,
 1100+ values: field.getDesc(),
 1101+ callback: function( newField ) {
 1102+ if ( newField !== null ) {
 1103+ //check that there are no duplicate preference names
 1104+ var existingValues = self.$div.closest( '.formbuilder' ).formBuilder( 'getValues' ),
 1105+ removedValues = field.getValues(),
 1106+ duplicateName = null;
 1107+ $.each( field.getValues(), function( name, val ) {
 1108+ //Only complain for preference names that are not in names for the field being replaced
 1109+ if ( typeof existingValues[name] != 'undefined' && removedValues[name] == 'undefined' ) {
 1110+ duplicateName = name;
 1111+ return false;
 1112+ }
 1113+ } );
 1114+
 1115+ if ( duplicateName !== null ) {
 1116+ alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name', duplicateName ) );
10011117 return false;
10021118 }
1003 - } );
1004 -
1005 - if ( duplicateName !== null ) {
1006 - alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name', duplicateName ) );
1007 - return false;
 1119+
 1120+ var $newSlot = self._createSlot( true, newField );
 1121+
 1122+ deleteFieldRules( field );
 1123+
 1124+ $slot.replaceWith( $newSlot );
 1125+
 1126+ //Add field's validation rules
 1127+ addFieldRules( newField );
10081128 }
1009 -
1010 - var $newSlot = self._createSlot( true, newField );
1011 -
1012 - deleteFieldRules( field );
1013 -
1014 - $slot.replaceWith( $newSlot );
1015 -
1016 - //Add field's validation rules
1017 - addFieldRules( newField );
 1129+ return true;
10181130 }
1019 - return true;
1020 - }
1021 - } );
1022 - } )
1023 - .appendTo( $divButtons );
 1131+ } );
 1132+ } )
 1133+ .appendTo( $divButtons );
 1134+ }
10241135
10251136 //Add the button to delete slots
10261137 $( '<a href="javascript:;" />' )
@@ -1040,7 +1151,8 @@
10411152 revert: true,
10421153 handle: ".formbuilder-editor-button-move",
10431154 helper: "original",
1044 - zIndex: $slot.closest( 'form' ).zIndex() + 1000, //TODO: ugly, find a better way
 1155+ zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way
 1156+ scroll: false,
10451157 opacity: 0.8,
10461158 cursor: "move",
10471159 cursorAt: {
@@ -1085,7 +1197,7 @@
10861198 callback: function( field ) {
10871199 if ( field !== null ) {
10881200 //check that there are no duplicate preference names
1089 - var existingValues = $slot.closest( 'form' ).formBuilder( 'getValues' ),
 1201+ var existingValues = $slot.closest( '.formbuilder' ).formBuilder( 'getValues' ),
10901202 duplicateName = null;
10911203 $.each( field.getValues(), function( name, val ) {
10921204 if ( typeof existingValues[name] != 'undefined' ) {
@@ -1109,6 +1221,9 @@
11101222
11111223 //Add field's validation rules
11121224 addFieldRules( field );
 1225+
 1226+ //Ensure immediate visual feedback if the current value is invalid
 1227+ self.$div.closest( '.formbuilder' ).formBuilder( 'validate' );
11131228 }
11141229 return true;
11151230 }
@@ -1129,12 +1244,36 @@
11301245
11311246 //Create tabs
11321247 var $tabs = this.$tabs = $( '<div><ul></ul></div>' )
1133 - .attr( 'id', idPrefix + 'tab-' + getIncrementalCounter() )
 1248+ .attr( 'id', this.options.idPrefix + 'tab-' + getIncrementalCounter() )
11341249 .tabs( {
11351250 add: function( event, ui ) {
11361251 //Links the anchor to the panel
11371252 $( ui.tab ).data( 'panel', ui.panel );
11381253
 1254+ //Allow to drop over tabs to move slots around
 1255+ var section = ui.panel;
 1256+ $( ui.tab ).droppable( {
 1257+ tolerance: 'pointer',
 1258+ accept: '.formbuilder-slot-nonempty',
 1259+ drop: function( event, ui ) {
 1260+ var $slot = $( ui.draggable ),
 1261+ $srcSection = $slot.parent(),
 1262+ $dstSection = $( section );
 1263+
 1264+ if ( $dstSection.get( 0 ) !== $srcSection.get( 0 ) ) {
 1265+ //move the slot (and the next empty slot) to dstSection with a nice animation
 1266+ var $slots = $slot.add( $slot.next() );
 1267+ $slots.slideUp( 'fast' )
 1268+ .promise().done( function() {
 1269+ $tabs.tabs( 'select', '#' + $dstSection.attr( 'id' ) );
 1270+ $slots.detach()
 1271+ .appendTo( $dstSection )
 1272+ .slideDown( 'fast' );
 1273+ } );
 1274+ }
 1275+ }
 1276+ } );
 1277+
11391278 if ( options.editable === true ) {
11401279 //Add "delete section" button
11411280 $( '<span />' )
@@ -1205,13 +1344,16 @@
12061345 }
12071346 } );
12081347
 1348+ //Save for future reference
 1349+ this.$ui_tabs_nav = $tabs.find( '.ui-tabs-nav' )
 1350+
12091351 var self = this;
12101352 $.each( this.desc.sections, function( index, sectionDescription ) {
1211 - var id = idPrefix + 'section-' + getIncrementalCounter(),
 1353+ var id = self.options.idPrefix + 'section-' + getIncrementalCounter(),
12121354 sec = new SectionField( sectionDescription, options, id );
12131355
12141356 $tabs.append( sec.getElement() )
1215 - .tabs( 'add', '#' + id, preproc( options.prefix, sectionDescription.title ) );
 1357+ .tabs( 'add', '#' + id, preproc( options.msgPrefix, sectionDescription.title ) );
12161358 } );
12171359
12181360 if ( options.editable === true ) {
@@ -1238,7 +1380,7 @@
12391381 text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
12401382 click: function() {
12411383 var title = $( this ).formBuilder( 'getValues' ).title,
1242 - id = idPrefix + 'section-' + getIncrementalCounter(),
 1384+ id = self.options.idPrefix + 'section-' + getIncrementalCounter(),
12431385 newSectionDescription = {
12441386 title: title,
12451387 fields: []
@@ -1246,7 +1388,7 @@
12471389 newSection = new SectionField( newSectionDescription, options, id );
12481390
12491391 $tabs.append( newSection.getElement() )
1250 - .tabs( 'add', '#' + id, preproc( options.prefix, title ) );
 1392+ .tabs( 'add', '#' + id, preproc( options.msgPrefix, title ) );
12511393
12521394 $( this ).dialog( "close" );
12531395 }
@@ -1261,10 +1403,10 @@
12621404 } );
12631405 } )
12641406 .wrap( '<li />' ).parent()
1265 - .prependTo( $tabs.find('.ui-tabs-nav' ) );
 1407+ .prependTo( this.$ui_tabs_nav );
12661408
12671409 //Make the tabs sortable
1268 - $tabs.tabs().find( '.ui-tabs-nav' ).sortable( {
 1410+ this.$ui_tabs_nav.sortable( {
12691411 axis: 'x',
12701412 items: 'li:not(:has(.formbuilder-editor-button-new-section))'
12711413 } );
@@ -1275,7 +1417,7 @@
12761418
12771419 BundleField.prototype.getValidationSettings = function() {
12781420 var settings = {};
1279 - this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1421+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
12801422 var panel = $( anchor ).data( 'panel' ),
12811423 field = $( panel ).data( 'field' );
12821424
@@ -1287,7 +1429,7 @@
12881430 BundleField.prototype.getDesc = function( useValuesAsDefaults ) {
12891431 var desc = this.desc;
12901432 desc.sections = [];
1291 - this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1433+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
12921434 var panel = $( anchor ).data( 'panel' ),
12931435 field = $( panel ).data( 'field' );
12941436
@@ -1298,7 +1440,7 @@
12991441
13001442 BundleField.prototype.getValues = function() {
13011443 var values = {};
1302 - this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1444+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
13031445 var panel = $( anchor ).data( 'panel' ),
13041446 field = $( panel ).data( 'field' );
13051447
@@ -1326,7 +1468,6 @@
13271469 }
13281470
13291471 var $form = $( '<form/>' ).addClass( 'formbuilder' );
1330 - var prefix = options.gadget === undefined ? '' : ( 'Gadget-' + options.gadget + '-' );
13311472
13321473 if ( typeof description.fields != 'object' ) {
13331474 mw.log( "description.fields should be an object, instead of a " + typeof description.fields );
@@ -1334,7 +1475,8 @@
13351476 }
13361477
13371478 var section = new SectionField( description, {
1338 - prefix: prefix,
 1479+ idPrefix: options.idPrefix,
 1480+ msgPrefix: options.msgPrefix,
13391481 values: options.values,
13401482 editable: options.editable === true
13411483 } );
Index: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js
@@ -70,7 +70,8 @@
7171 var values = response.values;
7272
7373 var dialogBody = $( prefsDescription ).formBuilder( {
74 - gadget: gadget,
 74+ msgPrefix: "Gadget-" + gadget + "-",
 75+ idPrefix: "gadget-" + gadget + "-preferences-",
7576 values: values
7677 } );
7778

Status & tagging log