r93975 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r93974‎ | r93975 | r93976 >
Date:14:37, 5 August 2011
Author:salvatoreingala
Status:deferred
Tags:
Comment:
- Changed syntax for 'select' options and for 'bundle' sections, using ordinary arrays instead of name/value pairs, since order is relevant and it's difficult to manage otherwise.
- Heavy refactoring of formBuilder.
- Added "editable" option for formBuilder, to allow GUI editing of preference descriptions; this will be used to create an editor for preferences. This is a work in progress: it has most features one would expect from a GUI editor (moving, inserting, deleting, changing properties, and so on), but inserting and changing field properties is still limited to 'boolean', 'string' and 'number' field types; trying to create or edit other kind of fields currently fails silently.
Modified paths:
  • /branches/salvatoreingala/Gadgets/Gadgets.i18n.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/Gadgets.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/Gadgets_tests.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/backend/GadgetPrefs.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
@@ -61,6 +61,21 @@
6262 'gadgets-formbuilder-integer' => 'Please enter an integer number.',
6363 'gadgets-formbuilder-date' => 'Please enter a valid date.',
6464 'gadgets-formbuilder-color' => 'Please enter a valid color.',
 65+ 'gadgets-formbuilder-editor-ok' => 'OK',
 66+ 'gadgets-formbuilder-editor-cancel' => 'Cancel',
 67+ 'gadgets-formbuilder-editor-move-field' => 'Move this field',
 68+ 'gadgets-formbuilder-editor-delete-field' => 'Delete this field',
 69+ 'gadgets-formbuilder-editor-edit-field' => 'Edit field properties',
 70+ 'gadgets-formbuilder-editor-insert-field' => 'Insert a new field',
 71+ 'gadgets-formbuilder-editor-chose-field' => 'Chose the type of the new field:',
 72+ 'gadgets-formbuilder-editor-chose-field-title' => 'Chose field type',
 73+ 'gadgets-formbuilder-editor-create-field-title' => 'Create field',
 74+ 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please chose a different name.',
 75+ 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title',
 76+ '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',
 78+ 'gadgets-formbuilder-editor-chose-title' => 'Chose the title of the new section:',
 79+ 'gadgets-formbuilder-editor-chose-title-title' => 'Chose section title',
6580 );
6681
6782 /** Message documentation (Message documentation)
Index: branches/salvatoreingala/Gadgets/Gadgets.php
@@ -86,12 +86,18 @@
8787 'styles' => array( 'jquery.formBuilder.css' ),
8888 'dependencies' => array(
8989 'jquery', 'jquery.ui.slider', 'jquery.ui.datepicker', 'jquery.ui.position',
 90+ 'jquery.ui.draggable', 'jquery.ui.droppable', 'jquery.ui.sortable', 'jquery.ui.dialog',
9091 'jquery.ui.tabs', 'jquery.farbtastic', 'jquery.colorUtil', 'jquery.validate'
9192 ),
9293 'messages' => array(
9394 'gadgets-formbuilder-required', 'gadgets-formbuilder-minlength', 'gadgets-formbuilder-maxlength',
9495 'gadgets-formbuilder-min', 'gadgets-formbuilder-max', 'gadgets-formbuilder-integer', 'gadgets-formbuilder-date',
95 - 'gadgets-formbuilder-color'
 96+ 'gadgets-formbuilder-color',
 97+ 'gadgets-formbuilder-editor-ok', 'gadgets-formbuilder-editor-cancel', 'gadgets-formbuilder-editor-move-field',
 98+ 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-insert-field',
 99+ 'gadgets-formbuilder-editor-chose-field', 'gadgets-formbuilder-editor-chose-field-title', 'gadgets-formbuilder-editor-create-field-title',
 100+ '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'
96102 ),
97103 'localBasePath' => $dir . 'ui/resources/',
98104 'remoteExtPath' => 'Gadgets/ui/resources'
Index: branches/salvatoreingala/Gadgets/Gadgets_tests.php
@@ -342,10 +342,10 @@
343343 'label' => 'some label',
344344 'default' => 3,
345345 'options' => array(
346 - 'opt1' => null,
347 - 'opt2' => true,
348 - 'opt3' => 3,
349 - 'opt4' => 'test'
 346+ array( 'name' => 'opt1', 'value' => null ),
 347+ array( 'name' => 'opt2', 'value' => true ),
 348+ array( 'name' => 'opt3', 'value' => 3 ),
 349+ array( 'name' => 'opt4', 'value' => 'test' )
350350 )
351351 )
352352 )
@@ -536,7 +536,8 @@
537537 array(
538538 'type' => 'bundle',
539539 'sections' => array(
540 - '@section1' => array(
 540+ array(
 541+ 'title' => '@section1',
541542 'fields' => array (
542543 array(
543544 'name' => 'testBoolean',
@@ -560,7 +561,8 @@
561562 )
562563 )
563564 ),
564 - 'Section2' => array(
 565+ array(
 566+ 'title' => 'Section2',
565567 'fields' => array(
566568 array(
567569 'name' => 'testNumber2',
@@ -576,10 +578,10 @@
577579 'label' => 'foo',
578580 'default' => 3,
579581 'options' => array(
580 - '@opt1' => null,
581 - '@opt2' => true,
582 - 'opt3' => 3,
583 - '@opt4' => 'opt4value'
 582+ array( 'name' => '@opt1', 'value' => null ),
 583+ array( 'name' => '@opt2', 'value' => true ),
 584+ array( 'name' => 'opt3', 'value' => 3 ),
 585+ array( 'name' => '@opt4', 'value' => 'opt4value' )
584586 )
585587 ),
586588 array(
@@ -588,10 +590,10 @@
589591 'label' => 'foo',
590592 'default' => 3,
591593 'options' => array(
592 - '@opt1' => null,
593 - 'opt2' => true,
594 - 'opt3' => 3,
595 - 'opt4' => 'opt4value'
 594+ array( 'name' => '@opt1', 'value' => null ),
 595+ array( 'name' => 'opt2', 'value' => true ),
 596+ array( 'name' => 'opt3', 'value' => 3 ),
 597+ array( 'name' => 'opt4', 'value' => 'opt4value' )
596598 )
597599 )
598600 )
Index: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php
@@ -74,7 +74,7 @@
7575 'validator' => 'is_integer'
7676 )
7777 ),
78 - 'validator' => 'GadgetPrefs::validateStringOptionDefinition',
 78+ 'validator' => 'GadgetPrefs::validateStringPrefDefinition',
7979 'checker' => 'GadgetPrefs::checkStringPref'
8080 ),
8181 'number' => array(
@@ -108,7 +108,7 @@
109109 'validator' => 'GadgetPrefs::isFloatOrInt'
110110 )
111111 ),
112 - 'validator' => 'GadgetPrefs::validateNumberOptionDefinition',
 112+ 'validator' => 'GadgetPrefs::validateNumberPrefDefinition',
113113 'checker' => 'GadgetPrefs::checkNumberPref'
114114 ),
115115 'select' => array(
@@ -126,10 +126,10 @@
127127 ),
128128 'options' => array(
129129 'isMandatory' => true,
130 - 'validator' => 'is_array'
 130+ 'validator' => 'GadgetPrefs::isOrdinaryArray'
131131 )
132132 ),
133 - 'validator' => 'GadgetPrefs::validateSelectOptionDefinition',
 133+ 'validator' => 'GadgetPrefs::validateSelectPrefDefinition',
134134 'checker' => 'GadgetPrefs::checkSelectPref',
135135 'getMessages' => 'GadgetPrefs::getSelectMessages'
136136 ),
@@ -160,7 +160,7 @@
161161 'validator' => 'GadgetPrefs::isFloatOrInt'
162162 )
163163 ),
164 - 'validator' => 'GadgetPrefs::validateRangeOptionDefinition',
 164+ 'validator' => 'GadgetPrefs::validateRangePrefDefinition',
165165 'checker' => 'GadgetPrefs::checkRangePref'
166166 ),
167167 'date' => array(
@@ -214,17 +214,17 @@
215215
216216
217217 //Further checks for 'string' options
218 - private static function validateStringOptionDefinition( $option ) {
219 - if ( isset( $option['minlength'] ) && $option['minlength'] < 0 ) {
 218+ private static function validateStringPrefDefinition( $prefDefinition ) {
 219+ if ( isset( $prefDefinition['minlength'] ) && $prefDefinition['minlength'] < 0 ) {
220220 return false;
221221 }
222222
223 - if ( isset( $option['maxlength'] ) && $option['maxlength'] <= 0 ) {
 223+ if ( isset( $prefDefinition['maxlength'] ) && $prefDefinition['maxlength'] <= 0 ) {
224224 return false;
225225 }
226226
227 - if ( isset( $option['minlength']) && isset( $option['maxlength'] ) ) {
228 - if ( $option['minlength'] > $option['maxlength'] ) {
 227+ if ( isset( $prefDefinition['minlength']) && isset( $prefDefinition['maxlength'] ) ) {
 228+ if ( $prefDefinition['minlength'] > $prefDefinition['maxlength'] ) {
229229 return false;
230230 }
231231 }
@@ -240,6 +240,16 @@
241241 return is_float( $param ) || is_int( $param ) || $param === null;
242242 }
243243
 244+ //Checks if $param is an ordinary (i.e.: not associative) array
 245+ private static function isOrdinaryArray( $param ) {
 246+ if ( !is_array( $param ) ) {
 247+ return false;
 248+ }
 249+
 250+ $count = count( $param );
 251+ return $count == 0 || array_keys( $param ) === range( 0, $count - 1 );
 252+ }
 253+
244254 //default flattener for simple fields that encode for a single preference
245255 private static function flattenSimpleField( $fieldDescription ) {
246256 return array( $fieldDescription['name'] => $fieldDescription );
@@ -248,7 +258,7 @@
249259 //flattener for 'bundle' fields
250260 private static function flattenBundleDefinition( $fieldDescription ) {
251261 $flattenedPrefs = array();
252 - foreach ( $fieldDescription['sections'] as $sectionName => $sectionDescription ) {
 262+ foreach ( $fieldDescription['sections'] as $sectionDescription ) {
253263 //Each section behaves like a full description of preferences
254264 $flt = self::flattenPrefsDescription( $sectionDescription );
255265 $flattenedPrefs = array_merge( $flattenedPrefs, $flt );
@@ -257,16 +267,16 @@
258268 }
259269
260270 //Further checks for 'number' options
261 - private static function validateNumberOptionDefinition( $option ) {
262 - if ( isset( $option['integer'] ) && $option['integer'] === true ) {
 271+ private static function validateNumberPrefDefinition( $prefDefinition ) {
 272+ if ( isset( $prefDefinition['integer'] ) && $prefDefinition['integer'] === true ) {
263273 //Check if 'min', 'max' and 'default' are integers (if given)
264 - if ( intval( $option['default'] ) != $option['default'] ) {
 274+ if ( intval( $prefDefinition['default'] ) != $prefDefinition['default'] ) {
265275 return false;
266276 }
267 - if ( isset( $option['min'] ) && intval( $option['min'] ) != $option['min'] ) {
 277+ if ( isset( $prefDefinition['min'] ) && intval( $prefDefinition['min'] ) != $prefDefinition['min'] ) {
268278 return false;
269279 }
270 - if ( isset( $option['max'] ) && intval( $option['max'] ) != $option['max'] ) {
 280+ if ( isset( $prefDefinition['max'] ) && intval( $prefDefinition['max'] ) != $prefDefinition['max'] ) {
271281 return false;
272282 }
273283 }
@@ -274,35 +284,49 @@
275285 return true;
276286 }
277287
278 - private static function validateSelectOptionDefinition( $option ) {
279 - $options = $option['options'];
 288+ private static function validateSelectPrefDefinition( $prefDefinition ) {
 289+ $options = $prefDefinition['options'];
280290
281 - foreach ( $options as $opt => $optVal ) {
282 - //Correct value for $optVal are NULL, boolean, integer, float or string
283 - if ( $optVal !== NULL &&
284 - !is_bool( $optVal ) &&
285 - !is_int( $optVal ) &&
286 - !is_float( $optVal ) &&
287 - !is_string( $optVal ) )
 291+ //Check if it's a regular array
 292+ if ( !self::isOrdinaryArray( $options ) ) {
 293+ return false;
 294+ }
 295+
 296+ foreach ( $options as $option ) {
 297+ //Using array_key_exists() because isset fails for null values
 298+ if ( !isset( $option['name'] ) || !array_key_exists( 'value', $option ) ) {
 299+ return false;
 300+ }
 301+
 302+ //All names must be strings
 303+ if ( !is_string( $option['name'] ) ) {
 304+ return false;
 305+ }
 306+
 307+ //Correct value for $value are null, boolean, integer, float or string
 308+ $value = $option['value'];
 309+ if ( $value !== null &&
 310+ !is_bool( $value ) &&
 311+ !is_int( $value ) &&
 312+ !is_float( $value ) &&
 313+ !is_string( $value ) )
288314 {
289315 return false;
290316 }
291317 }
292 -
293 - $values = array_values( $options );
294 -
 318+
295319 return true;
296320 }
297321
298 - private static function validateRangeOptionDefinition( $option ) {
299 - $step = isset( $option['step'] ) ? $option['step'] : 1;
 322+ private static function validateRangePrefDefinition( $prefDefinition ) {
 323+ $step = isset( $prefDefinition['step'] ) ? $prefDefinition['step'] : 1;
300324
301325 if ( $step <= 0 ) {
302326 return false;
303327 }
304328
305 - $min = $option['min'];
306 - $max = $option['max'];
 329+ $min = $prefDefinition['min'];
 330+ $max = $prefDefinition['max'];
307331
308332 //Checks if 'max' is a valid value
309333 //Valid values are min, min + step, min + 2*step, ...
@@ -458,6 +482,10 @@
459483 //validate each section, then ensure that preference names
460484 //of each section are disjoint
461485
 486+ if ( !self::isOrdinaryArray( $sections ) ) {
 487+ return false;
 488+ }
 489+
462490 $prefs = array(); //names of preferences
463491
464492 foreach ( $sections as $section ) {
@@ -465,6 +493,11 @@
466494 return false;
467495 }
468496
 497+ //Bundle sections must have a "title" field
 498+ if ( !isset( $section['title'] ) || !is_istring( $section['title'] ) ) {
 499+ return false;
 500+ }
 501+
469502 $flt = self::flattenPrefsDescription( $section );
470503 $newPrefs = array_keys( $flt );
471504 if ( array_intersect( $prefs, $newPrefs ) ) {
@@ -582,8 +615,13 @@
583616
584617 //Checker for 'select' preferences
585618 private static function checkSelectPref( $prefDescription, $value ) {
586 - $values = array_values( $prefDescription['options'] );
587 - return in_array( $value, $values, true );
 619+ foreach ( $prefDescription['options'] as $option ) {
 620+ if ( $option['value'] === $value ) {
 621+ return true;
 622+ }
 623+ }
 624+
 625+ return false;
588626 }
589627
590628 //Checker for 'range' preferences
@@ -762,7 +800,8 @@
763801 //Returns the messages for a 'select' field description
764802 private static function getSelectMessages( $prefDescription ) {
765803 $msgs = array();
766 - foreach ( $prefDescription['options'] as $optName => $value ) {
 804+ foreach ( $prefDescription['options'] as $option ) {
 805+ $optName = $option['name'];
767806 if ( self::isMessage( $optName ) ) {
768807 $msgs[] = substr( $optName, 1 );
769808 }
@@ -774,10 +813,11 @@
775814 private static function getBundleMessages( $prefDescription ) {
776815 //returns the union of all messages of all sections, plus section names
777816 $msgs = array();
778 - foreach ( $prefDescription['sections'] as $sectionName => $sectionDescription ) {
 817+ foreach ( $prefDescription['sections'] as $sectionDescription ) {
779818 $msgs = array_merge( $msgs, self::getMessages( $sectionDescription ) );
780 - if ( self::isMessage( $sectionName ) ) {
781 - $msgs[] = substr( $sectionName, 1 );
 819+ $sectionTitle = $sectionDescription['title'];
 820+ if ( self::isMessage( $sectionTitle ) ) {
 821+ $msgs[] = substr( $sectionTitle, 1 );
782822 }
783823 }
784824 return array_unique( $msgs );
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css
@@ -43,6 +43,65 @@
4444 }
4545
4646 .farbtastic {
47 - border: 1px solid #cccccc;
 47+ border: none;
4848 }
4949
 50+.formbuilder-slot {
 51+ border: none;
 52+ padding-top: 0.5em;
 53+ padding-bottom: 0.5em;
 54+}
 55+
 56+/* formBuilder editor */
 57+
 58+.formbuilder-slot-nonempty {
 59+ border: 1px solid #888;
 60+ padding: 0px;
 61+}
 62+
 63+.formbuilder-slot-empty {
 64+ border: 1px dashed #aaa;
 65+ padding: 0px;
 66+}
 67+
 68+.formbuilder-slot-can-drop {
 69+ border: 1px solid black;
 70+}
 71+
 72+.formbuilder-editor-slot-buttons {
 73+ height: 19px;
 74+}
 75+
 76+.formbuilder-editor-button {
 77+ cursor: pointer;
 78+}
 79+
 80+.formbuilder-editor-button-move {
 81+ cursor: move;
 82+}
 83+
 84+.formbuilder-editor-button-new, .formbuilder-editor-button-new-section {
 85+ float: left;
 86+}
 87+
 88+.formbuilder-editor-button-edit {
 89+ float: left;
 90+}
 91+
 92+.formbuilder-editor-button-move {
 93+ float: left;
 94+}
 95+
 96+.formbuilder-editor-button-delete {
 97+ float: right;
 98+}
 99+
 100+.formbuilder-editor-button-delete-section {
 101+ float: right;
 102+}
 103+
 104+.formbuilder-editor-button-edit-section {
 105+ float: right;
 106+ margin-left: 6px;
 107+}
 108+
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js
@@ -12,13 +12,13 @@
1313 //If str starts with "@" the rest of the string is assumed to be
1414 //a message, and the result of mw.msg is returned.
1515 //Two "@@" at the beginning escape for a single "@".
16 - function preproc( $form, str ) {
 16+ function preproc( prefix, str ) {
1717 if ( str.length <= 1 || str[0] !== '@' ) {
1818 return str;
1919 } else if ( str.substr( 0, 2 ) == '@@' ) {
2020 return str.substr( 1 );
2121 } else {
22 - return mw.message( $form.data( 'formBuilder' ).prefix + str.substring( 1 ) ).plain();
 22+ return mw.message( prefix + str.substr( 1 ) ).plain();
2323 }
2424 }
2525
@@ -38,6 +38,12 @@
3939 return res;
4040 }
4141
 42+ //Returns an object with only one key and the corresponding value given in arguments;
 43+ function pair( key, val ) {
 44+ var res = {};
 45+ res[key] = val;
 46+ return res;
 47+ }
4248
4349 function testOptional( value, element ) {
4450 var rules = $( element ).rules();
@@ -92,13 +98,98 @@
9399 return new F();
94100 }
95101
 102+
 103+ //Field types that can be referred to by preference descriptions
 104+ var validFieldTypes = {};
 105+
 106+
 107+ //Describes 'name' and 'label' field members
 108+ var simpleField = [
 109+ {
 110+ "name": "name",
 111+ "type": "string",
 112+ "label": "name",
 113+ "required": true,
 114+ "maxlength": 40,
 115+ "default": ""
 116+ },
 117+ {
 118+ "name": "label",
 119+ "type": "string",
 120+ "label": "label",
 121+ "required": false,
 122+ "default": ""
 123+ }
 124+ ];
 125+
 126+ //Used by preference editor to build field properties dialogs
 127+ var prefsDescriptionSpecifications = {
 128+ "boolean": simpleField,
 129+ "string" : simpleField.concat( [
 130+ {
 131+ "name": "required",
 132+ "type": "boolean",
 133+ "label": "required",
 134+ "default": false
 135+ },
 136+ {
 137+ "name": "minlength",
 138+ "type": "number",
 139+ "label": "minlength",
 140+ "integer": true,
 141+ "min": 0,
 142+ "required": false,
 143+ "default": null
 144+ },
 145+ {
 146+ "name": "maxlength",
 147+ "type": "number",
 148+ "label": "maxlength",
 149+ "integer": true,
 150+ "min": 0,
 151+ "required": false,
 152+ "default": null
 153+ }
 154+ ] ),
 155+ "number" : simpleField.concat( [
 156+ {
 157+ "name": "required",
 158+ "type": "boolean",
 159+ "label": "required",
 160+ "default": true
 161+ },
 162+ {
 163+ "name": "integer",
 164+ "type": "boolean",
 165+ "label": "integer",
 166+ "default": false
 167+ },
 168+ {
 169+ "name": "min",
 170+ "type": "number",
 171+ "label": "min",
 172+ "required": false,
 173+ "default": null
 174+ },
 175+ {
 176+ "name": "max",
 177+ "type": "number",
 178+ "label": "max",
 179+ "required": false,
 180+ "default": null
 181+ }
 182+ //TODO: other fields
 183+ ] )
 184+ };
 185+
96186 /* Basic interface for fields */
97 - function Field( $form, desc, values ) {
98 - this.$form = $form;
 187+ function Field( desc, options ) {
 188+ this.prefix = options.prefix;
99189 this.desc = desc;
 190+ this.options = options;
100191 }
101192
102 - Field.prototype.getDesc = function() {
 193+ Field.prototype.getDesc = function( useValuesAsDefaults ) {
103194 return this.desc;
104195 };
105196
@@ -120,97 +211,121 @@
121212 };
122213 };
123214
124 -
125215 /* A field with no content, generating an empty container */
126216 EmptyField.prototype = object( Field.prototype );
127217 EmptyField.prototype.constructor = EmptyField;
128 - function EmptyField( $form, desc, values ) {
129 - Field.call( this, $form, desc, values );
 218+ function EmptyField( desc, options ) {
 219+ Field.call( this, desc, options );
130220
131221 //Check existence and type of the "type" field
132 - if ( !desc.type || typeof desc.type != 'string' ) {
 222+ if ( !this.desc.type || typeof this.desc.type != 'string' ) {
133223 $.error( "Missing 'type' parameter" );
134224 }
135225
136 - this.$p = $( '<p/>' );
 226+ this.$div = $( '<div/>' ).data( 'field', this );
137227 }
138228
139229 EmptyField.prototype.getElement = function() {
140 - return this.$p;
 230+ return this.$div;
141231 };
142232
143233 /* A field with just a label */
144234 LabelField.prototype = object( EmptyField.prototype );
145235 LabelField.prototype.constructor = LabelField;
146 - function LabelField( $form, desc, values ) {
147 - EmptyField.call( this, $form, desc, values );
 236+ function LabelField( desc, options ) {
 237+ EmptyField.call( this, desc, options );
148238
149239 //Check existence and type of the "label" field
150 - if ( !desc.label || typeof desc.label != 'string' ) {
151 - $.error( "Missing 'label' parameter" );
 240+ if ( typeof this.desc.label != 'string' ) {
 241+ $.error( "Missing or wrong 'label' parameter" );
152242 }
153243
154244 var $label = $( '<label/>' )
155 - .text( preproc( this.$form, this.desc.label ) )
 245+ .text( preproc( this.prefix, this.desc.label ) )
156246 .attr('for', idPrefix + this.desc.name );
157247
158 - this.$p.append( $label );
 248+ this.$div.append( $label );
159249 }
160250
 251+ /* Abstract base class for all "simple" fields. Should not be instantiated. */
 252+ SimpleField.prototype = object( LabelField.prototype );
 253+ SimpleField.prototype.constructor = SimpleField;
 254+ function SimpleField( desc, options ){
 255+ LabelField.call( this, desc, options );
 256+ }
 257+
 258+ SimpleField.prototype.getDesc = function( useValuesAsDefaults ) {
 259+ var desc = LabelField.prototype.getDesc.call( this, useValuesAsDefaults );
 260+ if ( useValuesAsDefaults === true ) {
 261+ //set 'default' to current value.
 262+ var values = this.getValues();
 263+ desc['default'] = values[this.desc.name];
 264+ }
 265+
 266+ return desc;
 267+ };
 268+
 269+
161270 /* A field with a label and a checkbox */
162 - BooleanField.prototype = object( LabelField.prototype );
 271+ BooleanField.prototype = object( SimpleField.prototype );
163272 BooleanField.prototype.constructor = BooleanField;
164 - function BooleanField( $form, desc, values ){
165 - LabelField.call( this, $form, desc, values );
 273+ function BooleanField( desc, options ){
 274+ SimpleField.call( this, desc, options );
166275
167 - var value = values[this.desc.name];
168 - if ( typeof value != 'boolean' ) {
169 - $.error( "value is invalid" );
 276+ this.$c = $( '<input/>' ).attr( {
 277+ type: 'checkbox',
 278+ id: idPrefix + this.desc.name,
 279+ name: idPrefix + this.desc.name
 280+ } );
 281+
 282+ var value = options.values && options.values[this.desc.name];
 283+ if ( typeof value != 'undefined' ) {
 284+ if ( typeof value != 'boolean' ) {
 285+ $.error( "value is invalid" );
 286+ }
 287+
 288+ this.$c.attr( 'checked', value );
170289 }
171 -
172 - this.$c = $( '<input/>' )
173 - .attr( 'type', 'checkbox' )
174 - .attr( 'id', idPrefix + this.desc.name )
175 - .attr( 'name', idPrefix + this.desc.name )
176 - .attr( 'checked', value );
177290
178 - this.$p.append( this.$c );
 291+ this.$div.append( this.$c );
179292 }
180293
181294 BooleanField.prototype.getValues = function() {
182 - var res = {};
183 - res[this.desc.name] = this.$c.is( ':checked' );
184 - return res;
 295+ return pair( this.desc.name, this.$c.is( ':checked' ) );
185296 };
186297
 298+ validFieldTypes["boolean"] = BooleanField;
 299+
187300 /* A field with a textbox accepting string values */
188 - StringField.prototype = object( LabelField.prototype );
 301+ StringField.prototype = object( SimpleField.prototype );
189302 StringField.prototype.constructor = StringField;
190 - function StringField( $form, desc, values ){
191 - LabelField.call( this, $form, desc, values );
 303+ function StringField( desc, options ){
 304+ SimpleField.call( this, desc, options );
192305
193 - var value = values[this.desc.name];
194 - if ( typeof value != 'string' ) {
195 - $.error( "value is invalid" );
 306+ this.$text = $( '<input/>' ).attr( {
 307+ type: 'text',
 308+ id: idPrefix + this.desc.name,
 309+ name: idPrefix + this.desc.name
 310+ } );
 311+
 312+ var value = options.values && options.values[this.desc.name];
 313+ if ( typeof value != 'undefined' ) {
 314+ if ( typeof value != 'string' ) {
 315+ $.error( "value is invalid" );
 316+ }
 317+
 318+ this.$text.val( value );
196319 }
197320
198 - this.$text = $( '<input/>' )
199 - .attr( 'type', 'text' )
200 - .attr( 'id', idPrefix + this.desc.name )
201 - .attr( 'name', idPrefix + this.desc.name )
202 - .val( value );
203 -
204 - this.$p.append( this.$text );
 321+ this.$div.append( this.$text );
205322 }
206323
207324 StringField.prototype.getValues = function() {
208 - var res = {};
209 - res[this.desc.name] = this.$text.val();
210 - return res;
 325+ return pair( this.desc.name, this.$text.val() );
211326 };
212327
213328 StringField.prototype.getValidationSettings = function() {
214 - var settings = LabelField.prototype.getValidationSettings.call( this ),
 329+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
215330 fieldId = idPrefix + this.desc.name;
216331
217332 settings.rules[fieldId] = {};
@@ -238,35 +353,40 @@
239354 return settings;
240355 };
241356
 357+ validFieldTypes["string"] = StringField;
 358+
 359+
242360 /* A field with a textbox accepting numeric values */
243 - NumberField.prototype = object( LabelField.prototype );
 361+ NumberField.prototype = object( SimpleField.prototype );
244362 NumberField.prototype.constructor = NumberField;
245 - function NumberField( $form, desc, values ){
246 - LabelField.call( this, $form, desc, values );
 363+ function NumberField( desc, options ){
 364+ SimpleField.call( this, desc, options );
247365
248 - var value = values[this.desc.name];
249 - if ( value !== null && typeof value != 'number' ) {
250 - $.error( "value is invalid" );
 366+ this.$text = $( '<input/>' ).attr( {
 367+ type: 'text',
 368+ id: idPrefix + this.desc.name,
 369+ name: idPrefix + this.desc.name
 370+ } );
 371+
 372+ var value = options.values && options.values[this.desc.name];
 373+ if ( typeof value != 'undefined' ) {
 374+ if ( value !== null && typeof value != 'number' ) {
 375+ $.error( "value is invalid" );
 376+ }
 377+
 378+ this.$text.val( value );
251379 }
252380
253 - this.$text = $( '<input/>' )
254 - .attr( 'type', 'text' )
255 - .attr( 'id', idPrefix + this.desc.name )
256 - .attr( 'name', idPrefix + this.desc.name )
257 - .val( value );
258 -
259 - this.$p.append( this.$text );
 381+ this.$div.append( this.$text );
260382 }
261383
262384 NumberField.prototype.getValues = function() {
263 - var val = parseFloat( this.$text.val() ),
264 - res = {};
265 - res[this.desc.name] = isNaN( val ) ? null : val;
266 - return res;
 385+ var val = parseFloat( this.$text.val() );
 386+ return pair( this.desc.name, isNaN( val ) ? null : val );
267387 };
268388
269389 NumberField.prototype.getValidationSettings = function() {
270 - var settings = LabelField.prototype.getValidationSettings.call( this ),
 390+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
271391 fieldId = idPrefix + this.desc.name;
272392
273393 settings.rules[fieldId] = {};
@@ -300,137 +420,137 @@
301421 return settings;
302422 };
303423
 424+ validFieldTypes["number"] = NumberField;
 425+
304426 /* A field with a drop-down list */
305 - SelectField.prototype = object( LabelField.prototype );
 427+ SelectField.prototype = object( SimpleField.prototype );
306428 SelectField.prototype.constructor = SelectField;
307 - function SelectField( $form, desc, values ){
308 - LabelField.call( this, $form, desc, values );
 429+ function SelectField( desc, options ){
 430+ SimpleField.call( this, desc, options );
309431
310 - var $select = this.$select = $( '<select/>' )
311 - .attr( 'id', idPrefix + this.desc.name )
312 - .attr( 'name', idPrefix + this.desc.name );
 432+ var $select = this.$select = $( '<select/>' ).attr( {
 433+ id: idPrefix + this.desc.name,
 434+ name: idPrefix + this.desc.name
 435+ } );
313436
314437 var validValues = [];
315438 var self = this;
316 - $.each( desc.options, function( optName, optVal ) {
 439+ $.each( this.desc.options, function( idx, option ) {
317440 var i = validValues.length;
318441 $( '<option/>' )
319 - .text( preproc( self.$form, optName ) )
 442+ .text( preproc( self.prefix, option.name ) )
320443 .val( i )
321444 .appendTo( $select );
322 - validValues.push( optVal );
 445+ validValues.push( option.value );
323446 } );
324447
325448 this.validValues = validValues;
326449
327 - var value = values[this.desc.name];
328 - if ( $.inArray( value, validValues ) == -1 ) {
329 - $.error( "value is not in the list of possible values" );
 450+ var value = options.values && options.values[this.desc.name];
 451+ if ( typeof value != 'undefined' ) {
 452+ if ( $.inArray( value, validValues ) == -1 ) {
 453+ $.error( "value is not in the list of possible values" );
 454+ }
 455+
 456+ var i = $.inArray( value, validValues );
 457+ $select.val( i ).prop( 'selected', 'selected' );
330458 }
331459
332 - var i = $.inArray( value, validValues );
333 - $select.val( i ).attr( 'selected', 'selected' );
334 -
335 - this.$p.append( $select );
 460+ this.$div.append( $select );
336461 }
337462
338463 SelectField.prototype.getValues = function() {
339 - var i = parseInt( this.$select.val(), 10 ),
340 - res = {};
341 - res[this.desc.name] = this.validValues[i];
342 - return res;
 464+ var i = parseInt( this.$select.val(), 10 );
 465+ return pair( this.desc.name, this.validValues[i] );
343466 };
344467
 468+ validFieldTypes["select"] = SelectField;
345469
346470 /* A field with a slider, representing ranges of numbers */
347 - RangeField.prototype = object( LabelField.prototype );
 471+ RangeField.prototype = object( SimpleField.prototype );
348472 RangeField.prototype.constructor = RangeField;
349 - function RangeField( $form, desc, values ){
350 - LabelField.call( this, $form, desc, values );
 473+ function RangeField( desc, options ){
 474+ SimpleField.call( this, desc, options );
351475
352 - var value = values[this.desc.name];
353 - if ( typeof value != 'number' ) {
354 - $.error( "value is invalid" );
355 - }
356 -
357 - if ( typeof desc.min != 'number' ) {
 476+ if ( typeof this.desc.min != 'number' ) {
358477 $.error( "desc.min is invalid" );
359478 }
360479
361 - if ( typeof desc.max != 'number' ) {
 480+ if ( typeof this.desc.max != 'number' ) {
362481 $.error( "desc.max is invalid" );
363482 }
364483
365 - if ( typeof desc.step != 'undefined' && typeof desc.step != 'number' ) {
 484+ if ( typeof this.desc.step != 'undefined' && typeof this.desc.step != 'number' ) {
366485 $.error( "desc.step is invalid" );
367486 }
368487
369 - if ( value < desc.min || value > desc.max ) {
370 - $.error( "value is out of range" );
 488+ var value = options.values && options.values[this.desc.name];
 489+ if ( typeof value != 'undefined' ) {
 490+ if ( typeof value != 'number' ) {
 491+ $.error( "value is invalid" );
 492+ }
 493+ if ( value < this.desc.min || value > this.desc.max ) {
 494+ $.error( "value is out of range" );
 495+ }
371496 }
372497
373498 var $slider = this.$slider = $( '<div/>' )
374499 .attr( 'id', idPrefix + this.desc.name );
375500
376 - var options = {
377 - min: desc.min,
378 - max: desc.max,
379 - value: value
 501+ var rangeOptions = {
 502+ min: this.desc.min,
 503+ max: this.desc.max
380504 };
381505
382 - if ( typeof desc.step != 'undefined' ) {
383 - options['step'] = desc.step;
 506+ if ( typeof value != 'undefined' ) {
 507+ rangeOptions['value'] = value;
384508 }
385509
386 - $slider.slider( options );
 510+ if ( typeof this.desc.step != 'undefined' ) {
 511+ rangeOptions['step'] = this.desc.step;
 512+ }
387513
388 - this.$p.append( $slider );
 514+ $slider.slider( rangeOptions );
 515+
 516+ this.$div.append( $slider );
389517 }
390518
391519 RangeField.prototype.getValues = function() {
392 - var res = {};
393 - res[this.desc.name] = this.$slider.slider( 'value' );
394 - return res;
 520+ return pair( this.desc.name, this.$slider.slider( 'value' ) );
395521 };
 522+
 523+ validFieldTypes["range"] = RangeField;
396524
397 -
398525 /* A field with a textbox with a datepicker */
399 - DateField.prototype = object( LabelField.prototype );
 526+ DateField.prototype = object( SimpleField.prototype );
400527 DateField.prototype.constructor = DateField;
401 - function DateField( $form, desc, values ){
402 - LabelField.call( this, $form, desc, values );
 528+ function DateField( desc, options ){
 529+ SimpleField.call( this, desc, options );
403530
404 - var value = values[this.desc.name];
405 - if ( typeof value == 'undefined' ) {
406 - $.error( "value is invalid" );
407 - }
 531+ this.$text = $( '<input/>' ).attr( {
 532+ type: 'text',
 533+ id: idPrefix + this.desc.name,
 534+ name: idPrefix + this.desc.name
 535+ } ).datepicker( {
 536+ onSelect: function() {
 537+ //Force validation, so that a previous 'invalid' state is removed
 538+ $( this ).valid();
 539+ }
 540+ } );
408541
 542+ var value = options.values && options.values[this.desc.name];
409543 var date;
410 - if ( value !== null ) {
 544+ if ( typeof value != 'undefined' && value !== null ) {
411545 date = new Date( value );
412546
413547 if ( !isFinite( date ) ) {
414548 $.error( "value is invalid" );
415549 }
416 - }
417550
418 - this.$text = $( '<input/>' )
419 - .attr( 'type', 'text' )
420 - .attr( 'id', idPrefix + this.desc.name )
421 - .attr( 'name', idPrefix + this.desc.name )
422 - .datepicker( {
423 - onSelect: function() {
424 - //Force validation, so that a previous 'invalid' state is removed
425 - $( this ).valid();
426 - }
427 - } );
428 -
429 - if ( value !== null ) {
430551 this.$text.datepicker( 'setDate', date );
431552 }
432553
433 -
434 - this.$p.append( this.$text );
 554+ this.$div.append( this.$text );
435555 }
436556
437557 DateField.prototype.getValues = function() {
@@ -442,18 +562,17 @@
443563 }
444564
445565 //UTC date in ISO 8601 format [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z
446 - res[this.desc.name] = '' +
 566+ return pair( this.desc.name, '' +
447567 pad( d.getUTCFullYear(), 4 ) + '-' +
448568 pad( d.getUTCMonth() + 1, 2 ) + '-' +
449569 pad( d.getUTCDate(), 2 ) + 'T' +
450570 pad( d.getUTCHours(), 2 ) + ':' +
451571 pad( d.getUTCMinutes(), 2 ) + ':' +
452 - pad( d.getUTCSeconds(), 2 ) + 'Z';
453 - return res;
 572+ pad( d.getUTCSeconds(), 2 ) + 'Z' );
454573 };
455574
456575 DateField.prototype.getValidationSettings = function() {
457 - var settings = LabelField.prototype.getValidationSettings.call( this ),
 576+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
458577 fieldId = idPrefix + this.desc.name;
459578
460579 settings.rules[fieldId] = {
@@ -462,6 +581,8 @@
463582 return settings;
464583 };
465584
 585+ validFieldTypes["date"] = DateField;
 586+
466587 /* A field with color picker */
467588
468589 function closeColorPicker() {
@@ -470,7 +591,6 @@
471592 } );
472593 }
473594
474 -
475595 //If a click happens outside the colorpicker while it is showed, remove it
476596 $( document ).mousedown( function( event ) {
477597 var $target = $( event.target );
@@ -479,25 +599,24 @@
480600 }
481601 } );
482602
483 - ColorField.prototype = object( LabelField.prototype );
 603+ ColorField.prototype = object( SimpleField.prototype );
484604 ColorField.prototype.constructor = ColorField;
485 - function ColorField( $form, desc, values ){
486 - LabelField.call( this, $form, desc, values );
 605+ function ColorField( desc, options ){
 606+ SimpleField.call( this, desc, options );
487607
488 - var value = values[this.desc.name];
489 - if ( typeof value == 'undefined' ) {
490 - $.error( "value is invalid" );
491 - }
 608+ var value = ( options.values && options.values[this.desc.name] ) || '';
492609
493 - this.$text = $( '<input/>' )
494 - .attr( 'type', 'text' )
495 - .attr( 'id', idPrefix + this.desc.name )
496 - .attr( 'name', idPrefix + this.desc.name )
 610+ this.$text = $( '<input/>' ).attr( {
 611+ type: 'text',
 612+ id: idPrefix + this.desc.name,
 613+ name: idPrefix + this.desc.name
 614+ } )
497615 .addClass( 'colorpicker-input' )
498616 .val( value )
499617 .css( 'background-color', value )
500618 .focus( function() {
501619 $( '<div/>' )
 620+ .addClass( 'ui-widget ui-widget-content' )
502621 .attr( 'id', 'colorpicker' )
503622 .css( 'position', 'absolute' )
504623 .hide()
@@ -525,11 +644,11 @@
526645 } )
527646 .blur( closeColorPicker );
528647
529 - this.$p.append( this.$text );
 648+ this.$div.append( this.$text );
530649 }
531650
532651 ColorField.prototype.getValidationSettings = function() {
533 - var settings = LabelField.prototype.getValidationSettings.call( this ),
 652+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
534653 fieldId = idPrefix + this.desc.name;
535654
536655 settings.rules[fieldId] = {
@@ -539,128 +658,657 @@
540659 };
541660
542661 ColorField.prototype.getValues = function() {
543 - var color = $.colorUtil.getRGB( this.$text.val() ),
544 - res = {};
545 - res[this.desc.name] = '#' + pad( color[0].toString( 16 ), 2 ) +
546 - pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 );
547 - return res;
 662+ 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 ) );
548665 };
 666+
 667+ validFieldTypes["color"] = ColorField;
549668
 669+
550670 /* A field that represent a section (group of fields) */
 671+
 672+ function deleteFieldRules( field ) {
 673+ //Remove all its validation rules
 674+ var validationSettings = field.getValidationSettings();
 675+ if ( validationSettings.rules ) {
 676+ $.each( validationSettings.rules, function( name, value ) {
 677+ var $input = $( '#' + name );
 678+ if ( $input.length > 0 ) {
 679+ $( '#' + name ).rules( 'remove' );
 680+ }
 681+ } );
 682+ }
 683+ }
 684+
 685+ function addFieldRules( field ) {
 686+ var validationSettings = field.getValidationSettings();
 687+ if ( validationSettings.rules ) {
 688+ $.each( validationSettings.rules, function( name, rules ) {
 689+ var $input = $( '#' + name );
 690+
 691+ //Find messages associated to this rule, if any
 692+ if ( typeof validationSettings.messages != 'undefined' &&
 693+ typeof validationSettings.messages[name] != 'undefined')
 694+ {
 695+ rules.messages = validationSettings.messages[name];
 696+ }
 697+
 698+ if ( $input.length > 0 ) {
 699+ $( '#' + name ).rules( 'add', rules );
 700+ }
 701+ } );
 702+ }
 703+ }
 704+
 705+
551706 SectionField.prototype = object( Field.prototype );
552707 SectionField.prototype.constructor = SectionField;
553 - function SectionField( $form, desc, values, id ) {
554 - Field.call( this, $form, desc, values );
 708+ function SectionField( desc, options, id ) {
 709+ Field.call( this, desc, options );
555710
556 - this.$p = $( '<p/>' );
 711+ this.$div = $( '<div/>' ).data( 'field', this );
557712
558713 if ( id !== undefined ) {
559 - this.$p.attr( 'id', id );
 714+ this.$div.attr( 'id', id );
560715 }
561 -
562 - var fields = [],
563 - settings = {}; //validator settings
564716
565 - for ( var i = 0; i < desc.fields.length; i++ ) {
 717+ //If there is an "intro", adds it to the section as a label
 718+ //TODO: kill "intro"s and make "label" fields, instead?
 719+ if ( typeof this.desc.intro == 'string' ) {
 720+ $( '<p/>' )
 721+ .text( preproc( this.prefix, this.desc.intro ) )
 722+ .addClass( 'formBuilder-intro' )
 723+ .appendTo( this.$div );
 724+ }
 725+
 726+ for ( var i = 0; i < this.desc.fields.length; i++ ) {
566727 //TODO: validate fieldName
567 - var field = desc.fields[i],
 728+
 729+ if ( options.editable === true ) {
 730+ //add an empty slot
 731+ this._createSlot( true ).appendTo( this.$div );
 732+ }
 733+
 734+ var field = this.desc.fields[i],
568735 FieldConstructor = validFieldTypes[field.type];
569736
570737 if ( typeof FieldConstructor != 'function' ) {
571738 $.error( "field with invalid type: " + field.type );
572739 }
573740
574 - var f = new FieldConstructor( $form, field, values );
 741+ var f = new FieldConstructor( field, options ),
 742+ $slot = this._createSlot( options.editable === true, f );
575743
576 - this.$p.append( f.getElement() );
577 -
578 - //If this field has validation rules, add them to settings
579 - var fieldSettings = f.getValidationSettings();
580 -
581 - if ( fieldSettings ) {
582 - $.extend( true, settings, fieldSettings );
583 - }
584 -
585 - fields.push( f );
 744+ $slot.appendTo( this.$div );
586745 }
587746
588 - this.settings = settings;
589 - this.fields = fields;
 747+ if ( options.editable === true ) {
 748+ //add an empty slot
 749+ this._createSlot( true ).appendTo( this.$div );
 750+ }
590751 }
591752
592753 SectionField.prototype.getElement = function() {
593 - return this.$p;
 754+ return this.$div;
594755 };
595756
 757+ SectionField.prototype.getDesc = function( useValuesAsDefaults ) {
 758+ var desc = this.desc;
 759+ desc.fields = [];
 760+ this.$div.children().each( function( idx, slot ) {
 761+ var field = $( slot ).data( 'field' );
 762+ if ( field !== undefined ) {
 763+ desc.fields.push( field.getDesc( useValuesAsDefaults ) );
 764+ }
 765+ } );
 766+ return desc;
 767+ };
 768+
 769+ SectionField.prototype.setTitle = function( newTitle ) {
 770+ this.desc.title = newTitle;
 771+ };
 772+
596773 SectionField.prototype.getValues = function() {
597774 var values = {};
598 - for ( var i = 0; i < this.fields.length; i++ ) {
599 - $.extend( values, this.fields[i].getValues() );
600 - }
 775+ this.$div.children().each( function( idx, slot ) {
 776+ var field = $( slot ).data( 'field' );
 777+ if ( field !== undefined ) {
 778+ $.extend( values, field.getValues() );
 779+ }
 780+ } );
601781 return values;
602782 };
603783
604784 SectionField.prototype.getValidationSettings = function() {
605 - return this.settings;
 785+ var settings = {};
 786+ this.$div.children().each( function( idx, slot ) {
 787+ var field = $( slot ).data( 'field' );
 788+ if ( field !== undefined ) {
 789+ var fieldSettings = $( slot ).data( 'field' ).getValidationSettings();
 790+ if ( fieldSettings ) {
 791+ $.extend( true, settings, fieldSettings );
 792+ }
 793+ }
 794+ } );
 795+
 796+ return settings;
606797 };
 798+
 799+ SectionField.prototype._createFieldDialog = function( params ) {
 800+ var self = this;
 801+
 802+ if ( typeof params.callback != 'function' ) {
 803+ $.error( 'createFieldDialog: missing or wrong "callback" parameter' );
 804+ }
 805+
 806+ var type, description, values;
 807+ if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) {
 808+ //Create a dialog to choose the type of field to create
 809+ var selectOptions = [];
 810+ $.each( validFieldTypes, function( fieldType ) {
 811+ selectOptions.push( {
 812+ name: fieldType,
 813+ value: fieldType
 814+ } );
 815+ } );
 816+
 817+ $( {
 818+ fields: [ {
 819+ 'name': "type",
 820+ 'type': "select",
 821+ 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ),
 822+ 'options': selectOptions,
 823+ 'default': selectOptions[0].value
 824+ } ]
 825+ } ).formBuilder( {} ).dialog( {
 826+ width: 450,
 827+ modal: true,
 828+ resizable: false,
 829+ title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ),
 830+ close: function() {
 831+ $( this ).remove();
 832+ },
 833+ buttons: [
 834+ {
 835+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 836+ click: function() {
 837+ var values = $( this ).formBuilder( 'getValues' );
 838+ $( this ).dialog( "close" );
 839+ self._createFieldDialog( {
 840+ type: values.type,
 841+ callback: params.callback
 842+ } );
 843+ }
 844+ },
 845+ {
 846+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 847+ click: function() {
 848+ $( this ).dialog( "close" );
 849+ }
 850+ }
 851+ ]
 852+ } );
 853+
 854+ return;
 855+ } else {
 856+ type = params.type;
 857+ if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) {
 858+ $.error( 'createFieldDialog: invalid type: ' + type );
 859+ }
 860+
 861+ description = {
 862+ fields: prefsDescriptionSpecifications[type]
 863+ };
 864+ }
 865+
 866+ if ( typeof params.values != 'undefined' ) {
 867+ values = params.values;
 868+ } else {
 869+ 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+ } );
 877+ }
 878+
 879+ //Create the dialog to set field properties
 880+ var dlg = $( '<div/>' );
 881+ var form = $( description ).formBuilder( {
 882+ values: values
 883+ } ).appendTo( dlg );
 884+
 885+ dlg.dialog( {
 886+ modal: true,
 887+ width: 550,
 888+ resizable: false,
 889+ title: mw.msg( 'gadgets-formbuilder-editor-create-field-title' ),
 890+ close: function() {
 891+ $( this ).remove();
 892+ },
 893+ buttons: [
 894+ {
 895+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 896+ click: function() {
 897+ var isValid = $( form ).formBuilder( 'validate' );
 898+
 899+ if ( isValid ) {
 900+ var fieldDescription = $( form ).formBuilder( 'getValues' );
 901+
 902+ if ( typeof type != 'undefined' ) {
 903+ //Remove properties the equal their default
 904+ $.each( description.fields, function( index, fieldSpec ) {
 905+ var property = fieldSpec.name;
 906+ if ( fieldDescription[property] === fieldSpec['default'] ) {
 907+ delete fieldDescription[property];
 908+ }
 909+ } );
 910+ }
 911+
 912+ //Try to create the field. In case of error, warn the user.
 913+ fieldDescription.type = type;
 914+
 915+ var FieldConstructor = validFieldTypes[type];
 916+ var field;
 917+
 918+ try {
 919+ field = new FieldConstructor( fieldDescription, self.options );
 920+ } catch ( err ) {
 921+ alert( "Invalid field options: " + err ); //TODO: i18n
 922+ return;
 923+ }
 924+
 925+ if ( params.callback( field ) === true ) {
 926+ $( this ).dialog( "close" );
 927+ }
 928+ }
 929+ }
 930+ },
 931+ {
 932+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 933+ click: function() {
 934+ $( this ).dialog( "close" );
 935+ params.callback( null );
 936+ }
 937+ }
 938+ ]
 939+ } );
 940+ };
 941+
 942+ SectionField.prototype._deleteSlot = function( $slot ) {
 943+ var field = $slot.data( 'field' );
 944+ if ( field !== undefined ) {
 945+ //Slot with a field
 946+ deleteFieldRules( field );
 947+ }
 948+
 949+ //Delete it
 950+ $slot.remove();
 951+ };
607952
 953+ SectionField.prototype._createSlot = function( editable, field ) {
 954+ var self = this,
 955+ $slot = $( '<div/>' ).addClass( 'formbuilder-slot ui-widget' ),
 956+ $divButtons;
 957+
 958+ if ( editable ) {
 959+ $slot.addClass( 'formbuilder-slot-editable' );
 960+
 961+ $divButtons = $( '<div/>' )
 962+ .addClass( 'formbuilder-editor-slot-buttons' )
 963+ .appendTo( $slot );
 964+ }
 965+
 966+ if ( typeof field != 'undefined' ) {
 967+ //Nonempty slot
 968+ $slot.prepend( field.getElement() )
 969+ .data( 'field', field );
 970+
 971+ if ( editable ) {
 972+ $slot.addClass( 'formbuilder-slot-nonempty' );
 973+
 974+ //Add the handle for moving slots
 975+ $( '<span />' )
 976+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' )
 977+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
 978+ .mousedown( function() {
 979+ $( this ).focus();
 980+ } )
 981+ .appendTo( $divButtons );
 982+
 983+ //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;
 1001+ return false;
 1002+ }
 1003+ } );
 1004+
 1005+ if ( duplicateName !== null ) {
 1006+ alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name', duplicateName ) );
 1007+ return false;
 1008+ }
 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 );
 1018+ }
 1019+ return true;
 1020+ }
 1021+ } );
 1022+ } )
 1023+ .appendTo( $divButtons );
 1024+
 1025+ //Add the button to delete slots
 1026+ $( '<a href="javascript:;" />' )
 1027+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete ui-icon ui-icon-trash' )
 1028+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) )
 1029+ .click( function( event, ui ) {
 1030+ //Make both slots disappear, then delete them
 1031+ $.each( [$slot, $slot.prev()], function( idx, $s ) {
 1032+ $s.slideUp( function() {
 1033+ self._deleteSlot( $s );
 1034+ } );
 1035+ } );
 1036+ } )
 1037+ .appendTo( $divButtons );
 1038+
 1039+ //Make this slot draggable to allow moving it
 1040+ $slot.draggable( {
 1041+ revert: true,
 1042+ handle: ".formbuilder-editor-button-move",
 1043+ helper: "original",
 1044+ zIndex: $slot.closest( 'form' ).zIndex() + 1000, //TODO: ugly, find a better way
 1045+ opacity: 0.8,
 1046+ cursor: "move",
 1047+ cursorAt: {
 1048+ top: -5,
 1049+ left: -5
 1050+ }
 1051+ } );
 1052+ }
 1053+ } else {
 1054+ //Create empty slot
 1055+ $slot.addClass( 'formbuilder-slot-empty' )
 1056+ .droppable( {
 1057+ hoverClass: 'formbuilder-slot-can-drop',
 1058+ tolerance: 'pointer',
 1059+ drop: function( event, ui ) {
 1060+ var srcSlot = ui.draggable, dstSlot = this;
 1061+
 1062+ //Remove one empty slot surrounding source
 1063+ $( srcSlot ).prev().remove();
 1064+
 1065+ //Replace dstSlot with srcSlot:
 1066+ $( dstSlot ).replaceWith( srcSlot );
 1067+
 1068+ //Add one empty slot before and one after the new position
 1069+ self._createSlot( true ).insertBefore( srcSlot );
 1070+ self._createSlot( true ).insertAfter( srcSlot );
 1071+ },
 1072+ accept: function( draggable ) {
 1073+ //All non empty slots accepted, except for closest siblings
 1074+ return $( draggable ).hasClass( 'formbuilder-slot-nonempty' ) &&
 1075+ $( draggable ).prev().get( 0 ) !== $slot.get( 0 ) &&
 1076+ $( draggable ).next().get( 0 ) !== $slot.get( 0 );
 1077+ }
 1078+ } );
 1079+
 1080+ //The button to create a new field
 1081+ $( '<a href="javascript:;" />' )
 1082+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-new ui-icon ui-icon-plus' )
 1083+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) )
 1084+ .click( function() {
 1085+ self._createFieldDialog( {
 1086+ callback: function( field ) {
 1087+ if ( field !== null ) {
 1088+ //check that there are no duplicate preference names
 1089+ var existingValues = $slot.closest( 'form' ).formBuilder( 'getValues' ),
 1090+ duplicateName = null;
 1091+ $.each( field.getValues(), function( name, val ) {
 1092+ if ( typeof existingValues[name] != 'undefined' ) {
 1093+ duplicateName = name;
 1094+ return false;
 1095+ }
 1096+ } );
 1097+
 1098+ if ( duplicateName !== null ) {
 1099+ alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name' , duplicateName ) );
 1100+ return false;
 1101+ }
 1102+
 1103+ var $newSlot = self._createSlot( true, field ).hide(),
 1104+ $newEmptySlot = self._createSlot( true ).hide();
 1105+
 1106+ $slot.after( $newSlot, $newEmptySlot );
 1107+
 1108+ $newSlot.slideDown();
 1109+ $newEmptySlot.slideDown();
 1110+
 1111+ //Add field's validation rules
 1112+ addFieldRules( field );
 1113+ }
 1114+ return true;
 1115+ }
 1116+ } );
 1117+ } )
 1118+ .appendTo( $divButtons );
 1119+ }
 1120+
 1121+ return $slot;
 1122+ };
 1123+
 1124+
6081125 /* A field for 'bundle's */
6091126 BundleField.prototype = object( EmptyField.prototype );
6101127 BundleField.prototype.constructor = BundleField;
611 - function BundleField( $form, desc, values ) {
612 - EmptyField.call( this, $form, desc, values );
 1128+ function BundleField( desc, options ) {
 1129+ EmptyField.call( this, desc, options );
6131130
6141131 //Create tabs
6151132 var $tabs = this.$tabs = $( '<div><ul></ul></div>' )
6161133 .attr( 'id', idPrefix + 'tab-' + getIncrementalCounter() )
617 - .tabs();
 1134+ .tabs( {
 1135+ add: function( event, ui ) {
 1136+ //Links the anchor to the panel
 1137+ $( ui.tab ).data( 'panel', ui.panel );
 1138+
 1139+ if ( options.editable === true ) {
 1140+ //Add "delete section" button
 1141+ $( '<span />' )
 1142+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' )
 1143+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-section' ) )
 1144+ .click( function() {
 1145+ var sectionField = $( ui.panel ).data( 'field' );
 1146+ deleteFieldRules( sectionField );
6181147
619 - this.sections = [];
 1148+ var index = $( "li", $tabs ).index( $( this ).closest( "li" ) );
 1149+ index -= 1; //Don't count the "add section" button
 1150+
 1151+ $tabs.tabs( 'remove', index );
 1152+ } )
 1153+ .appendTo( ui.tab );
6201154
 1155+ //Add "edit section" button
 1156+ $( '<span />' )
 1157+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' )
 1158+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-section' ) )
 1159+ .click( function() {
 1160+ var button = this,
 1161+ sectionField = $( ui.panel ).data( 'field' );
 1162+
 1163+ $( {
 1164+ fields: [ {
 1165+ 'name': "title",
 1166+ 'type': "string",
 1167+ 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' )
 1168+ } ]
 1169+ } ).formBuilder( {
 1170+ values: {
 1171+ title: sectionField.getDesc().title
 1172+ }
 1173+ } ).dialog( {
 1174+ modal: true,
 1175+ resizable: false,
 1176+ title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ),
 1177+ close: function() {
 1178+ $( this ).remove(); //completely destroy on close
 1179+ },
 1180+ buttons: [
 1181+ {
 1182+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 1183+ click: function() {
 1184+ var title = $( this ).formBuilder( 'getValues' ).title;
 1185+
 1186+ //Update field description
 1187+ sectionField.setTitle( title );
 1188+
 1189+ //Update tab's title
 1190+ $( button ).parent().find( ':first-child' ).text( title );
 1191+
 1192+ $( this ).dialog( "close" );
 1193+ }
 1194+ },
 1195+ {
 1196+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 1197+ click: function() {
 1198+ $( this ).dialog( "close" );
 1199+ }
 1200+ }
 1201+ ]
 1202+ } );
 1203+ } )
 1204+ .appendTo( ui.tab );
 1205+ }
 1206+ }
 1207+ } );
 1208+
6211209 var self = this;
622 - $.each( desc.sections, function( sectionName, sectionDescription ) {
 1210+ $.each( this.desc.sections, function( index, sectionDescription ) {
6231211 var id = idPrefix + 'section-' + getIncrementalCounter(),
624 - sec = new SectionField( $form, sectionDescription, values, id );
 1212+ sec = new SectionField( sectionDescription, options, id );
6251213
626 - self.sections.push( sec );
627 -
6281214 $tabs.append( sec.getElement() )
629 - .tabs( 'add', '#' + id, preproc( $form, sectionName ) );
 1215+ .tabs( 'add', '#' + id, preproc( options.prefix, sectionDescription.title ) );
6301216 } );
6311217
632 - this.$p.append( $tabs );
 1218+ if ( options.editable === true ) {
 1219+ //Add the button to create a new section
 1220+ $( '<span>' )
 1221+ .addClass( 'formbuilder-editor-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' )
 1222+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-new-section' ) )
 1223+ .click( function() {
 1224+ $( {
 1225+ fields: [ {
 1226+ 'name': "title",
 1227+ 'type': "string",
 1228+ 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' )
 1229+ } ]
 1230+ } ).formBuilder( {} ).dialog( {
 1231+ modal: true,
 1232+ resizable: false,
 1233+ title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ),
 1234+ close: function() {
 1235+ $( this ).remove();
 1236+ },
 1237+ buttons: [
 1238+ {
 1239+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 1240+ click: function() {
 1241+ var title = $( this ).formBuilder( 'getValues' ).title,
 1242+ id = idPrefix + 'section-' + getIncrementalCounter(),
 1243+ newSectionDescription = {
 1244+ title: title,
 1245+ fields: []
 1246+ },
 1247+ newSection = new SectionField( newSectionDescription, options, id );
 1248+
 1249+ $tabs.append( newSection.getElement() )
 1250+ .tabs( 'add', '#' + id, preproc( options.prefix, title ) );
 1251+
 1252+ $( this ).dialog( "close" );
 1253+ }
 1254+ },
 1255+ {
 1256+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 1257+ click: function() {
 1258+ $( this ).dialog( "close" );
 1259+ }
 1260+ }
 1261+ ]
 1262+ } );
 1263+ } )
 1264+ .wrap( '<li />' ).parent()
 1265+ .prependTo( $tabs.find('.ui-tabs-nav' ) );
 1266+
 1267+ //Make the tabs sortable
 1268+ $tabs.tabs().find( '.ui-tabs-nav' ).sortable( {
 1269+ axis: 'x',
 1270+ items: 'li:not(:has(.formbuilder-editor-button-new-section))'
 1271+ } );
 1272+ }
 1273+
 1274+ this.$div.append( $tabs );
6331275 }
6341276
6351277 BundleField.prototype.getValidationSettings = function() {
6361278 var settings = {};
637 - $.each( this.sections, function( idx, section ) {
638 - $.extend( true, settings, section.getValidationSettings() );
 1279+ this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1280+ var panel = $( anchor ).data( 'panel' ),
 1281+ field = $( panel ).data( 'field' );
 1282+
 1283+ $.extend( true, settings, field.getValidationSettings() );
6391284 } );
6401285 return settings;
6411286 };
6421287
 1288+ BundleField.prototype.getDesc = function( useValuesAsDefaults ) {
 1289+ var desc = this.desc;
 1290+ desc.sections = [];
 1291+ this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1292+ var panel = $( anchor ).data( 'panel' ),
 1293+ field = $( panel ).data( 'field' );
 1294+
 1295+ desc.sections.push( field.getDesc( useValuesAsDefaults ) );
 1296+ } );
 1297+ return desc;
 1298+ };
 1299+
6431300 BundleField.prototype.getValues = function() {
6441301 var values = {};
645 - $.each( this.sections, function( idx, section ) {
646 - $.extend( values, section.getValues() );
 1302+ this.$div.find( '.ui-tabs-nav a' ).each( function( idx, anchor ) {
 1303+ var panel = $( anchor ).data( 'panel' ),
 1304+ field = $( panel ).data( 'field' );
 1305+
 1306+ $.extend( values, field.getValues() );
6471307 } );
6481308 return values;
6491309 };
6501310
 1311+ validFieldTypes["bundle"] = BundleField;
6511312
652 - //Field types that can be referred to by preference descriptions
653 - var validFieldTypes = {
654 - "boolean": BooleanField,
655 - "string" : StringField,
656 - "number" : NumberField,
657 - "select" : SelectField,
658 - "range" : RangeField,
659 - "date" : DateField,
660 - "color" : ColorField,
661 - "bundle" : BundleField
662 - };
663 -
664 -
6651313 /* Public methods */
6661314
6671315 /**
@@ -670,8 +1318,8 @@
6711319 * @param {Object} options
6721320 * @return {Element} the object with the requested form body.
6731321 */
674 - function buildFormBody( options ) {
675 - var description = this.get( 0 );
 1322+ function buildFormBody( options ) {
 1323+ var description = this.get( 0 );
6761324 if ( typeof description != 'object' ) {
6771325 mw.log( "description should be an object, instead of a " + typeof description );
6781326 return null;
@@ -679,32 +1327,26 @@
6801328
6811329 var $form = $( '<form/>' ).addClass( 'formbuilder' );
6821330 var prefix = options.gadget === undefined ? '' : ( 'Gadget-' + options.gadget + '-' );
683 - $form.data( 'formBuilder', {
684 - prefix: prefix //prefix for messages
685 - } );
6861331
687 - //If there is an "intro", adds it to the form as a label
688 - if ( typeof description.intro == 'string' ) {
689 - $( '<p/>' )
690 - .text( preproc( $form, description.intro ) )
691 - .addClass( 'formBuilder-intro' )
692 - .appendTo( $form );
693 - }
694 -
6951332 if ( typeof description.fields != 'object' ) {
6961333 mw.log( "description.fields should be an object, instead of a " + typeof description.fields );
6971334 return null;
6981335 }
6991336
700 - var section = new SectionField( $form, description, options.values );
 1337+ var section = new SectionField( description, {
 1338+ prefix: prefix,
 1339+ values: options.values,
 1340+ editable: options.editable === true
 1341+ } );
7011342
7021343 section.getElement().appendTo( $form );
7031344
7041345 var validator = $form.validate( section.getValidationSettings() );
7051346
706 - var data = $form.data( 'formBuilder' );
707 - data.mainSection = section;
708 - data.validator = validator;
 1347+ $form.data( 'formBuilder', {
 1348+ mainSection: section,
 1349+ validator: validator
 1350+ } );
7091351
7101352 return $form;
7111353 }
@@ -723,6 +1365,20 @@
7241366 },
7251367
7261368 /**
 1369+ * Returns the current description, where current values as set for 'default' values.
 1370+ * Used by the preference editor.
 1371+ *
 1372+ * NOTE: it is responsibility of the caller to call 'validate' and ensure that
 1373+ * current values pass validation before calling this method.
 1374+ *
 1375+ * @return {Object}
 1376+ */
 1377+ getDescription: function() {
 1378+ var data = this.data( 'formBuilder' );
 1379+ return data.mainSection.getDesc( true );
 1380+ },
 1381+
 1382+ /**
7271383 * Do validation of form fields and warn the user about wrong values, if any.
7281384 *
7291385 * @return {Boolean} true if all fields pass validation, false otherwise.
@@ -731,7 +1387,6 @@
7321388 var data = this.data( 'formBuilder' );
7331389 return data.validator.form();
7341390 }
735 -
7361391 };
7371392
7381393 $.fn.formBuilder = function( method ) {
Index: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js
@@ -86,7 +86,7 @@
8787 resizable: false,
8888 title: mw.msg( 'gadgets-configuration-of', gadget ),
8989 close: function() {
90 - $( this ).dialog( 'destroy' ).empty(); //completely destroy on close
 90+ $( this ).remove();
9191 },
9292 buttons: [
9393 //TODO: add a "Restore defaults" button

Status & tagging log