Index: branches/salvatoreingala/Gadgets/Gadgets.php |
— | — | @@ -86,7 +86,7 @@ |
87 | 87 | 'styles' => array( 'jquery.formBuilder.css' ), |
88 | 88 | 'dependencies' => array( |
89 | 89 | 'jquery', 'jquery.ui.slider', 'jquery.ui.datepicker', 'jquery.ui.position', |
90 | | - 'jquery.farbtastic', 'jquery.colorUtil', 'jquery.validate' |
| 90 | + 'jquery.ui.tabs', 'jquery.farbtastic', 'jquery.colorUtil', 'jquery.validate' |
91 | 91 | ), |
92 | 92 | 'messages' => array( |
93 | 93 | 'gadgets-formbuilder-required', 'gadgets-formbuilder-minlength', 'gadgets-formbuilder-maxlength', |
Index: branches/salvatoreingala/Gadgets/Gadgets_tests.php |
— | — | @@ -528,62 +528,75 @@ |
529 | 529 | } |
530 | 530 | |
531 | 531 | |
532 | | - //Data provider to be able to reuse this preference description for several tests. |
| 532 | + //Data provider to be able to reuse a complex preference description for several tests. |
533 | 533 | function prefsDescProvider() { |
534 | 534 | return array( array( |
535 | 535 | array( |
536 | 536 | 'fields' => array( |
537 | 537 | array( |
538 | | - 'name' => 'testBoolean', |
539 | | - 'type' => 'boolean', |
540 | | - 'label' => '@foo', |
541 | | - 'default' => true |
542 | | - ), |
543 | | - array( |
544 | | - 'name' => 'testBoolean2', |
545 | | - 'type' => 'boolean', |
546 | | - 'label' => '@@foo2', |
547 | | - 'default' => true |
548 | | - ), |
549 | | - array( |
550 | | - 'name' => 'testNumber', |
551 | | - 'type' => 'number', |
552 | | - 'label' => '@foo3', |
553 | | - 'min' => 2.3, |
554 | | - 'max' => 13.94, |
555 | | - 'default' => 7 |
556 | | - ), |
557 | | - array( |
558 | | - 'name' => 'testNumber2', |
559 | | - 'type' => 'number', |
560 | | - 'label' => 'foo4', |
561 | | - 'min' => 2.3, |
562 | | - 'max' => 13.94, |
563 | | - 'default' => 7 |
564 | | - ), |
565 | | - array( |
566 | | - 'name' => 'testSelect', |
567 | | - 'type' => 'select', |
568 | | - 'label' => 'foo', |
569 | | - 'default' => 3, |
570 | | - 'options' => array( |
571 | | - '@opt1' => null, |
572 | | - '@opt2' => true, |
573 | | - 'opt3' => 3, |
574 | | - '@opt4' => 'opt4value' |
| 538 | + 'type' => 'bundle', |
| 539 | + 'sections' => array( |
| 540 | + '@section1' => array( |
| 541 | + 'fields' => array ( |
| 542 | + array( |
| 543 | + 'name' => 'testBoolean', |
| 544 | + 'type' => 'boolean', |
| 545 | + 'label' => '@foo', |
| 546 | + 'default' => true |
| 547 | + ), |
| 548 | + array( |
| 549 | + 'name' => 'testBoolean2', |
| 550 | + 'type' => 'boolean', |
| 551 | + 'label' => '@@foo2', |
| 552 | + 'default' => true |
| 553 | + ), |
| 554 | + array( |
| 555 | + 'name' => 'testNumber', |
| 556 | + 'type' => 'number', |
| 557 | + 'label' => '@foo3', |
| 558 | + 'min' => 2.3, |
| 559 | + 'max' => 13.94, |
| 560 | + 'default' => 7 |
| 561 | + ) |
| 562 | + ) |
| 563 | + ), |
| 564 | + 'Section2' => array( |
| 565 | + 'fields' => array( |
| 566 | + array( |
| 567 | + 'name' => 'testNumber2', |
| 568 | + 'type' => 'number', |
| 569 | + 'label' => 'foo4', |
| 570 | + 'min' => 2.3, |
| 571 | + 'max' => 13.94, |
| 572 | + 'default' => 7 |
| 573 | + ), |
| 574 | + array( |
| 575 | + 'name' => 'testSelect', |
| 576 | + 'type' => 'select', |
| 577 | + 'label' => 'foo', |
| 578 | + 'default' => 3, |
| 579 | + 'options' => array( |
| 580 | + '@opt1' => null, |
| 581 | + '@opt2' => true, |
| 582 | + 'opt3' => 3, |
| 583 | + '@opt4' => 'opt4value' |
| 584 | + ) |
| 585 | + ), |
| 586 | + array( |
| 587 | + 'name' => 'testSelect2', |
| 588 | + 'type' => 'select', |
| 589 | + 'label' => 'foo', |
| 590 | + 'default' => 3, |
| 591 | + 'options' => array( |
| 592 | + '@opt1' => null, |
| 593 | + 'opt2' => true, |
| 594 | + 'opt3' => 3, |
| 595 | + 'opt4' => 'opt4value' |
| 596 | + ) |
| 597 | + ) |
| 598 | + ) |
| 599 | + ) |
575 | 600 | ) |
576 | | - ), |
577 | | - array( |
578 | | - 'name' => 'testSelect2', |
579 | | - 'type' => 'select', |
580 | | - 'label' => 'foo', |
581 | | - 'default' => 3, |
582 | | - 'options' => array( |
583 | | - '@opt1' => null, |
584 | | - 'opt2' => true, |
585 | | - 'opt3' => 3, |
586 | | - 'opt4' => 'opt4value' |
587 | | - ) |
588 | 601 | ) |
589 | 602 | ) |
590 | 603 | ) |
— | — | @@ -636,9 +649,10 @@ |
637 | 650 | $this->assertEquals( $prefs2['testNumber'], $prefs['testNumber'] ); |
638 | 651 | $this->assertEquals( $prefs2['testSelect'], $prefs['testSelect'] ); |
639 | 652 | |
640 | | - $this->assertEquals( $prefs2['testBoolean2'], $prefsDescription['fields'][1]['default'] ); |
641 | | - $this->assertEquals( $prefs2['testNumber2'], $prefsDescription['fields'][3]['default'] ); |
642 | | - $this->assertEquals( $prefs2['testSelect2'], $prefsDescription['fields'][5]['default'] ); |
| 653 | + $defaults = GadgetPrefs::getDefaults( $prefsDescription ); |
| 654 | + $this->assertEquals( $prefs2['testBoolean2'], $defaults['testBoolean2'] ); |
| 655 | + $this->assertEquals( $prefs2['testNumber2'], $defaults['testNumber2'] ); |
| 656 | + $this->assertEquals( $prefs2['testSelect2'], $defaults['testSelect2'] ); |
643 | 657 | |
644 | 658 | $g = $this->create( '*foo[ResourceLoader]| foo.css|foo.js|foo.bar' ); |
645 | 659 | $g->setPrefsDescription( $prefsDescription ); |
— | — | @@ -682,8 +696,9 @@ |
683 | 697 | */ |
684 | 698 | function testGetMessages( $prefsDescription ) { |
685 | 699 | $msgs = GadgetPrefs::getMessages( $prefsDescription ); |
| 700 | + sort( $msgs ); |
686 | 701 | $this->assertEquals( $msgs, array( |
687 | | - 'foo', 'foo3', 'opt1', 'opt2', 'opt4' |
| 702 | + 'foo', 'foo3', 'opt1', 'opt2', 'opt4', 'section1' |
688 | 703 | ) ); |
689 | 704 | } |
690 | 705 | } |
Index: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php |
— | — | @@ -14,6 +14,7 @@ |
15 | 15 | * Syntax specifications of preference description language. |
16 | 16 | * Each element describes a field; a "simple" field encodes exactly one gadget preference, but some fields |
17 | 17 | * may encode for 0 or multiple gadget preferences. |
| 18 | + * "Simple" field always have the 'name', the 'label' and the 'default' members. |
18 | 19 | * Each field has a 'description' and may have a 'validator', a 'flattener', and a 'checker'. |
19 | 20 | * - 'description' is an array that describes all the members of that fields. Each member description has this shape: |
20 | 21 | * - 'isMandatory' is a boolean that specifies if that member is mandatory for the field; |
— | — | @@ -193,6 +194,16 @@ |
194 | 195 | ) |
195 | 196 | ), |
196 | 197 | 'checker' => 'GadgetPrefs::checkColorPref' |
| 198 | + ), |
| 199 | + 'bundle' => array( |
| 200 | + 'description' => array( |
| 201 | + 'sections' => array( |
| 202 | + 'isMandatory' => true, |
| 203 | + 'checker' => 'GadgetPrefs::validateBundleSectionsDefinition' |
| 204 | + ) |
| 205 | + ), |
| 206 | + 'getMessages' => 'GadgetPrefs::getBundleMessages', |
| 207 | + 'flattener' => 'GadgetPrefs::flattenBundleDefinition' |
197 | 208 | ) |
198 | 209 | ); |
199 | 210 | |
— | — | @@ -234,6 +245,17 @@ |
235 | 246 | return array( $fieldDescription['name'] => $fieldDescription ); |
236 | 247 | } |
237 | 248 | |
| 249 | + //flattener for 'bundle' fields |
| 250 | + private static function flattenBundleDefinition( $fieldDescription ) { |
| 251 | + $flattenedPrefs = array(); |
| 252 | + foreach ( $fieldDescription['sections'] as $sectionName => $sectionDescription ) { |
| 253 | + //Each section behaves like a full description of preferences |
| 254 | + $flt = self::flattenPrefsDescription( $sectionDescription ); |
| 255 | + $flattenedPrefs = array_merge( $flattenedPrefs, $flt ); |
| 256 | + } |
| 257 | + return $flattenedPrefs; |
| 258 | + } |
| 259 | + |
238 | 260 | //Further checks for 'number' options |
239 | 261 | private static function validateNumberOptionDefinition( $option ) { |
240 | 262 | if ( isset( $option['integer'] ) && $option['integer'] === true ) { |
— | — | @@ -297,10 +319,9 @@ |
298 | 320 | //Flattens a simple field, by calling its field-specific flattener if there is any, |
299 | 321 | //or the default flattener otherwise. |
300 | 322 | private static function flattenFieldDescription( $fieldDescription ) { |
301 | | - $typeSpec = self::$prefsDescriptionSpecifications[$fieldDescription['type']]; |
302 | | - $typeDescription = $typeSpec['description']; |
303 | | - if ( isset( $typeSpec['flattener'] ) ) { |
304 | | - $flattener = $typeSpec['flattener']; |
| 323 | + $fieldSpec = self::$prefsDescriptionSpecifications[$fieldDescription['type']]; |
| 324 | + if ( isset( $fieldSpec['flattener'] ) ) { |
| 325 | + $flattener = $fieldSpec['flattener']; |
305 | 326 | } else { |
306 | 327 | $flattener = 'GadgetPrefs::flattenSimpleField'; |
307 | 328 | } |
— | — | @@ -316,6 +337,7 @@ |
317 | 338 | $flt = self::flattenFieldDescription( $fieldDescription ); |
318 | 339 | $flattenedPrefsDescription = array_merge( $flattenedPrefsDescription, $flt ); |
319 | 340 | } |
| 341 | + |
320 | 342 | return $flattenedPrefsDescription; |
321 | 343 | } |
322 | 344 | |
— | — | @@ -415,8 +437,8 @@ |
416 | 438 | return false; |
417 | 439 | } |
418 | 440 | |
419 | | - $prefs = array( 'dummy' => $optionDefinition['default'] ); |
420 | | - if ( !self::checkSinglePref( $optionDefinition, $prefs, 'dummy' ) ) { |
| 441 | + $prefs = array( 'dummy' => $prefDescription['default'] ); |
| 442 | + if ( !self::checkSinglePref( $prefDescription, $prefs, 'dummy' ) ) { |
421 | 443 | return false; |
422 | 444 | } |
423 | 445 | } |
— | — | @@ -431,6 +453,30 @@ |
432 | 454 | return true; |
433 | 455 | } |
434 | 456 | |
| 457 | + //validates the 'sections' member of a 'bundle' field |
| 458 | + private static function validateBundleSectionsDefinition( $sections ) { |
| 459 | + //validate each section, then ensure that preference names |
| 460 | + //of each section are disjoint |
| 461 | + |
| 462 | + $prefs = array(); //names of preferences |
| 463 | + |
| 464 | + foreach ( $sections as $section ) { |
| 465 | + if ( !self::validateSectionDefinition( $section ) ) { |
| 466 | + return false; |
| 467 | + } |
| 468 | + |
| 469 | + $flt = self::flattenPrefsDescription( $section ); |
| 470 | + $newPrefs = array_keys( $flt ); |
| 471 | + if ( array_intersect( $prefs, $newPrefs ) ) { |
| 472 | + return false; |
| 473 | + } |
| 474 | + |
| 475 | + $prefs = array_merge( $prefs, $newPrefs ); |
| 476 | + } |
| 477 | + |
| 478 | + return true; |
| 479 | + } |
| 480 | + |
435 | 481 | //Checks if the given description of the preferences is valid |
436 | 482 | public static function isPrefsDescriptionValid( $prefsDescription ) { |
437 | 483 | return self::validateSectionDefinition( $prefsDescription ); |
— | — | @@ -721,6 +767,19 @@ |
722 | 768 | $msgs[] = substr( $optName, 1 ); |
723 | 769 | } |
724 | 770 | } |
725 | | - return $msgs; |
| 771 | + return array_unique( $msgs ); |
726 | 772 | } |
| 773 | + |
| 774 | + //Returns the messages for a 'bundle' field description |
| 775 | + private static function getBundleMessages( $prefDescription ) { |
| 776 | + //returns the union of all messages of all sections, plus section names |
| 777 | + $msgs = array(); |
| 778 | + foreach ( $prefDescription['sections'] as $sectionName => $sectionDescription ) { |
| 779 | + $msgs = array_merge( $msgs, self::getMessages( $sectionDescription ) ); |
| 780 | + if ( self::isMessage( $sectionName ) ) { |
| 781 | + $msgs[] = substr( $sectionName, 1 ); |
| 782 | + } |
| 783 | + } |
| 784 | + return array_unique( $msgs ); |
| 785 | + } |
727 | 786 | } |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js |
— | — | @@ -18,11 +18,17 @@ |
19 | 19 | } else if ( str.substr( 0, 2 ) == '@@' ) { |
20 | 20 | return str.substr( 1 ); |
21 | 21 | } else { |
22 | | - //TODO: better validation |
23 | 22 | return mw.message( $form.data( 'formBuilder' ).prefix + str.substring( 1 ) ).plain(); |
24 | 23 | } |
25 | 24 | } |
26 | 25 | |
| 26 | + //Commodity function to avoid id conflicts |
| 27 | + var getIncrementalCounter = ( function() { |
| 28 | + var cnt = 0; |
| 29 | + return function() { |
| 30 | + return cnt++; |
| 31 | + }; |
| 32 | + } )(); |
27 | 33 | |
28 | 34 | function pad( n, len ) { |
29 | 35 | var res = '' + n; |
— | — | @@ -86,46 +92,64 @@ |
87 | 93 | return new F(); |
88 | 94 | } |
89 | 95 | |
90 | | - //A field with no content |
91 | | - function EmptyField( $form, desc, values ) { |
92 | | - //Check existence of compulsory fields |
93 | | - if ( !desc.type || !desc.label ) { |
94 | | - $.error( "Missing arguments" ); |
95 | | - } |
96 | | - |
| 96 | + /* Basic interface for fields */ |
| 97 | + function Field( $form, desc, values ) { |
97 | 98 | this.$form = $form; |
98 | | - |
99 | | - this.$p = $( '<p/>' ); |
100 | | - |
101 | 99 | this.desc = desc; |
102 | 100 | } |
103 | 101 | |
104 | | - EmptyField.prototype.getDesc = function() { |
| 102 | + Field.prototype.getDesc = function() { |
105 | 103 | return this.desc; |
106 | 104 | }; |
107 | 105 | |
108 | 106 | //Override expected |
109 | | - EmptyField.prototype.getValues = function() { |
| 107 | + Field.prototype.getValues = function() { |
110 | 108 | return {}; |
111 | 109 | }; |
112 | 110 | |
113 | | - EmptyField.prototype.getElement = function() { |
114 | | - return this.$p; |
| 111 | + //Override expected |
| 112 | + Field.prototype.getElement = function() { |
| 113 | + return null; |
115 | 114 | }; |
116 | 115 | |
117 | | - EmptyField.prototype.getValidationSettings = function() { |
| 116 | + //Override expected |
| 117 | + Field.prototype.getValidationSettings = function() { |
118 | 118 | return { |
119 | 119 | rules: {}, |
120 | 120 | messages: {} |
121 | 121 | }; |
122 | 122 | }; |
123 | 123 | |
124 | | - //A field with just a label |
| 124 | + |
| 125 | + /* A field with no content, generating an empty container */ |
| 126 | + EmptyField.prototype = object( Field.prototype ); |
| 127 | + EmptyField.prototype.constructor = EmptyField; |
| 128 | + function EmptyField( $form, desc, values ) { |
| 129 | + Field.call( this, $form, desc, values ); |
| 130 | + |
| 131 | + //Check existence and type of the "type" field |
| 132 | + if ( !desc.type || typeof desc.type != 'string' ) { |
| 133 | + $.error( "Missing 'type' parameter" ); |
| 134 | + } |
| 135 | + |
| 136 | + this.$p = $( '<p/>' ); |
| 137 | + } |
| 138 | + |
| 139 | + EmptyField.prototype.getElement = function() { |
| 140 | + return this.$p; |
| 141 | + }; |
| 142 | + |
| 143 | + /* A field with just a label */ |
125 | 144 | LabelField.prototype = object( EmptyField.prototype ); |
126 | 145 | LabelField.prototype.constructor = LabelField; |
127 | 146 | function LabelField( $form, desc, values ) { |
128 | 147 | EmptyField.call( this, $form, desc, values ); |
129 | 148 | |
| 149 | + //Check existence and type of the "label" field |
| 150 | + if ( !desc.label || typeof desc.label != 'string' ) { |
| 151 | + $.error( "Missing 'label' parameter" ); |
| 152 | + } |
| 153 | + |
130 | 154 | var $label = $( '<label/>' ) |
131 | 155 | .text( preproc( this.$form, this.desc.label ) ) |
132 | 156 | .attr('for', idPrefix + this.desc.name ); |
— | — | @@ -133,7 +157,7 @@ |
134 | 158 | this.$p.append( $label ); |
135 | 159 | } |
136 | 160 | |
137 | | - //A field with a label and a checkbox |
| 161 | + /* A field with a label and a checkbox */ |
138 | 162 | BooleanField.prototype = object( LabelField.prototype ); |
139 | 163 | BooleanField.prototype.constructor = BooleanField; |
140 | 164 | function BooleanField( $form, desc, values ){ |
— | — | @@ -159,8 +183,7 @@ |
160 | 184 | return res; |
161 | 185 | }; |
162 | 186 | |
163 | | - //A field with a textbox accepting string values |
164 | | - |
| 187 | + /* A field with a textbox accepting string values */ |
165 | 188 | StringField.prototype = object( LabelField.prototype ); |
166 | 189 | StringField.prototype.constructor = StringField; |
167 | 190 | function StringField( $form, desc, values ){ |
— | — | @@ -215,7 +238,7 @@ |
216 | 239 | return settings; |
217 | 240 | }; |
218 | 241 | |
219 | | - //A field with a textbox accepting numeric values |
| 242 | + /* A field with a textbox accepting numeric values */ |
220 | 243 | NumberField.prototype = object( LabelField.prototype ); |
221 | 244 | NumberField.prototype.constructor = NumberField; |
222 | 245 | function NumberField( $form, desc, values ){ |
— | — | @@ -277,7 +300,7 @@ |
278 | 301 | return settings; |
279 | 302 | }; |
280 | 303 | |
281 | | - //A field with a drop-down list |
| 304 | + /* A field with a drop-down list */ |
282 | 305 | SelectField.prototype = object( LabelField.prototype ); |
283 | 306 | SelectField.prototype.constructor = SelectField; |
284 | 307 | function SelectField( $form, desc, values ){ |
— | — | @@ -319,7 +342,7 @@ |
320 | 343 | }; |
321 | 344 | |
322 | 345 | |
323 | | - //A field with a slider, representing ranges of numbers |
| 346 | + /* A field with a slider, representing ranges of numbers */ |
324 | 347 | RangeField.prototype = object( LabelField.prototype ); |
325 | 348 | RangeField.prototype.constructor = RangeField; |
326 | 349 | function RangeField( $form, desc, values ){ |
— | — | @@ -371,7 +394,7 @@ |
372 | 395 | }; |
373 | 396 | |
374 | 397 | |
375 | | - //A field with a textbox with a datepicker |
| 398 | + /* A field with a textbox with a datepicker */ |
376 | 399 | DateField.prototype = object( LabelField.prototype ); |
377 | 400 | DateField.prototype.constructor = DateField; |
378 | 401 | function DateField( $form, desc, values ){ |
— | — | @@ -437,15 +460,24 @@ |
438 | 461 | "datePicker": true |
439 | 462 | }; |
440 | 463 | return settings; |
441 | | - } |
| 464 | + }; |
442 | 465 | |
443 | | - //A field with color picker |
| 466 | + /* A field with color picker */ |
444 | 467 | |
445 | 468 | function closeColorPicker() { |
446 | 469 | $( '#colorpicker' ).fadeOut( 'fast', function() { |
447 | | - $( this ).remove() |
| 470 | + $( this ).remove(); |
448 | 471 | } ); |
449 | 472 | } |
| 473 | + |
| 474 | + |
| 475 | + //If a click happens outside the colorpicker while it is showed, remove it |
| 476 | + $( document ).mousedown( function( event ) { |
| 477 | + var $target = $( event.target ); |
| 478 | + if ( $target.parents( '#colorpicker' ).length == 0 ) { |
| 479 | + closeColorPicker(); |
| 480 | + } |
| 481 | + } ); |
450 | 482 | |
451 | 483 | ColorField.prototype = object( LabelField.prototype ); |
452 | 484 | ColorField.prototype.constructor = ColorField; |
— | — | @@ -504,25 +536,119 @@ |
505 | 537 | "color": true |
506 | 538 | }; |
507 | 539 | return settings; |
508 | | - } |
| 540 | + }; |
509 | 541 | |
510 | 542 | ColorField.prototype.getValues = function() { |
511 | 543 | var color = $.colorUtil.getRGB( this.$text.val() ), |
512 | | - res = {} |
| 544 | + res = {}; |
513 | 545 | res[this.desc.name] = '#' + pad( color[0].toString( 16 ), 2 ) + |
514 | 546 | pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ); |
515 | 547 | return res; |
516 | 548 | }; |
| 549 | + |
| 550 | + /* A field that represent a section (group of fields) */ |
| 551 | + SectionField.prototype = object( Field.prototype ); |
| 552 | + SectionField.prototype.constructor = SectionField; |
| 553 | + function SectionField( $form, desc, values, id ) { |
| 554 | + Field.call( this, $form, desc, values ); |
| 555 | + |
| 556 | + this.$p = $( '<p/>' ); |
| 557 | + |
| 558 | + if ( id !== undefined ) { |
| 559 | + this.$p.attr( 'id', id ); |
| 560 | + } |
| 561 | + |
| 562 | + var fields = [], |
| 563 | + settings = {}; //validator settings |
517 | 564 | |
518 | | - //If a click happens outside the colorpicker while it is showed, remove it |
519 | | - $( document ).mousedown( function( event ) { |
520 | | - var $target = $( event.target ); |
521 | | - if ( $target.parents( '#colorpicker' ).length == 0 ) { |
522 | | - closeColorPicker(); |
| 565 | + for ( var i = 0; i < desc.fields.length; i++ ) { |
| 566 | + //TODO: validate fieldName |
| 567 | + var field = desc.fields[i], |
| 568 | + FieldConstructor = validFieldTypes[field.type]; |
| 569 | + |
| 570 | + if ( typeof FieldConstructor != 'function' ) { |
| 571 | + $.error( "field with invalid type: " + field.type ); |
| 572 | + } |
| 573 | + |
| 574 | + var f = new FieldConstructor( $form, field, values ); |
| 575 | + |
| 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 ); |
523 | 586 | } |
524 | | - } ); |
| 587 | + |
| 588 | + this.settings = settings; |
| 589 | + this.fields = fields; |
| 590 | + } |
525 | 591 | |
| 592 | + SectionField.prototype.getElement = function() { |
| 593 | + return this.$p; |
| 594 | + }; |
526 | 595 | |
| 596 | + SectionField.prototype.getValues = function() { |
| 597 | + var values = {}; |
| 598 | + for ( var i = 0; i < this.fields.length; i++ ) { |
| 599 | + $.extend( values, this.fields[i].getValues() ); |
| 600 | + } |
| 601 | + return values; |
| 602 | + }; |
| 603 | + |
| 604 | + SectionField.prototype.getValidationSettings = function() { |
| 605 | + return this.settings; |
| 606 | + }; |
| 607 | + |
| 608 | + /* A field for 'bundle's */ |
| 609 | + BundleField.prototype = object( EmptyField.prototype ); |
| 610 | + BundleField.prototype.constructor = BundleField; |
| 611 | + function BundleField( $form, desc, values ) { |
| 612 | + EmptyField.call( this, $form, desc, values ); |
| 613 | + |
| 614 | + //Create tabs |
| 615 | + var $tabs = this.$tabs = $( '<div><ul></ul></div>' ) |
| 616 | + .attr( 'id', idPrefix + 'tab-' + getIncrementalCounter() ) |
| 617 | + .tabs(); |
| 618 | + |
| 619 | + this.sections = []; |
| 620 | + |
| 621 | + var self = this; |
| 622 | + $.each( desc.sections, function( sectionName, sectionDescription ) { |
| 623 | + var id = idPrefix + 'section-' + getIncrementalCounter(), |
| 624 | + sec = new SectionField( $form, sectionDescription, values, id ); |
| 625 | + |
| 626 | + self.sections.push( sec ); |
| 627 | + |
| 628 | + $tabs.append( sec.getElement() ) |
| 629 | + .tabs( 'add', '#' + id, preproc( $form, sectionName ) ); |
| 630 | + } ); |
| 631 | + |
| 632 | + this.$p.append( $tabs ); |
| 633 | + } |
| 634 | + |
| 635 | + BundleField.prototype.getValidationSettings = function() { |
| 636 | + var settings = {}; |
| 637 | + $.each( this.sections, function( idx, section ) { |
| 638 | + $.extend( true, settings, section.getValidationSettings() ); |
| 639 | + } ); |
| 640 | + return settings; |
| 641 | + }; |
| 642 | + |
| 643 | + BundleField.prototype.getValues = function() { |
| 644 | + var values = {}; |
| 645 | + $.each( this.sections, function( idx, section ) { |
| 646 | + $.extend( values, section.getValues() ); |
| 647 | + } ); |
| 648 | + return values; |
| 649 | + }; |
| 650 | + |
| 651 | + |
| 652 | + //Field types that can be referred to by preference descriptions |
527 | 653 | var validFieldTypes = { |
528 | 654 | "boolean": BooleanField, |
529 | 655 | "string" : StringField, |
— | — | @@ -530,9 +656,11 @@ |
531 | 657 | "select" : SelectField, |
532 | 658 | "range" : RangeField, |
533 | 659 | "date" : DateField, |
534 | | - "color" : ColorField |
| 660 | + "color" : ColorField, |
| 661 | + "bundle" : BundleField |
535 | 662 | }; |
536 | 663 | |
| 664 | + |
537 | 665 | /* Public methods */ |
538 | 666 | |
539 | 667 | /** |
— | — | @@ -552,7 +680,7 @@ |
553 | 681 | var $form = $( '<form/>' ).addClass( 'formbuilder' ); |
554 | 682 | var prefix = options.gadget === undefined ? '' : ( 'Gadget-' + options.gadget + '-' ); |
555 | 683 | $form.data( 'formBuilder', { |
556 | | - prefix: prefix, //prefix for messages |
| 684 | + prefix: prefix //prefix for messages |
557 | 685 | } ); |
558 | 686 | |
559 | 687 | //If there is an "intro", adds it to the form as a label |
— | — | @@ -568,45 +696,15 @@ |
569 | 697 | return null; |
570 | 698 | } |
571 | 699 | |
572 | | - var fields = []; |
| 700 | + var section = new SectionField( $form, description, options.values ); |
| 701 | + |
| 702 | + section.getElement().appendTo( $form ); |
573 | 703 | |
574 | | - var settings = {}; //validator settings |
| 704 | + var validator = $form.validate( section.getValidationSettings() ); |
575 | 705 | |
576 | | - for ( var i = 0; i < description.fields.length; i++ ) { |
577 | | - //TODO: validate fieldName |
578 | | - var field = description.fields[i], |
579 | | - FieldConstructor = validFieldTypes[field.type]; |
580 | | - |
581 | | - if ( typeof FieldConstructor != 'function' ) { |
582 | | - mw.log( "field with invalid type: " + field.type ); |
583 | | - return null; |
584 | | - } |
585 | | - |
586 | | - var f; |
587 | | - try { |
588 | | - f = new FieldConstructor( $form, field, options.values ); |
589 | | - } catch ( e ) { |
590 | | - mw.log( e ); |
591 | | - return null; //constructor failed, wrong syntax in field description |
592 | | - } |
593 | | - |
594 | | - $form.append( f.getElement() ); |
595 | | - |
596 | | - //If this field has validation rules, add them to settings |
597 | | - var fieldSettings = f.getValidationSettings(); |
598 | | - |
599 | | - if ( fieldSettings ) { |
600 | | - $.extend( true, settings, fieldSettings ); |
601 | | - } |
602 | | - |
603 | | - fields.push( f ); |
604 | | - } |
605 | | - |
606 | | - var validator = $form.validate( settings ); |
607 | | - |
608 | 706 | var data = $form.data( 'formBuilder' ); |
609 | | - data.fields = fields, |
610 | | - data.validator = validator |
| 707 | + data.mainSection = section; |
| 708 | + data.validator = validator; |
611 | 709 | |
612 | 710 | return $form; |
613 | 711 | } |
— | — | @@ -620,15 +718,8 @@ |
621 | 719 | * @return {Object} |
622 | 720 | */ |
623 | 721 | getValues: function() { |
624 | | - var data = this.data( 'formBuilder' ), |
625 | | - result = {}; |
626 | | - |
627 | | - for ( var i = 0; i < data.fields.length; i++ ) { |
628 | | - var f = data.fields[i]; |
629 | | - $.extend( result, f.getValues() ); |
630 | | - } |
631 | | - |
632 | | - return result; |
| 722 | + var data = this.data( 'formBuilder' ); |
| 723 | + return data.mainSection.getValues(); |
633 | 724 | }, |
634 | 725 | |
635 | 726 | /** |