r94653 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r94652‎ | r94653 | r94654 >
Date:16:08, 16 August 2011
Author:salvatoreingala
Status:deferred
Tags:
Comment:
- Added 'list' type preferences (and done some refactoring of jquery.formBuilder.js)
- Fixed a bug in Gadget::newFromDefinition, which was causing warnings
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/Gadget.php (modified) (history)
  • /branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php (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
@@ -66,10 +66,11 @@
6767 'gadgets-formbuilder-editor-move-field' => 'Move this field',
6868 'gadgets-formbuilder-editor-delete-field' => 'Delete this field',
6969 'gadgets-formbuilder-editor-edit-field' => 'Edit field properties',
 70+ 'gadgets-formbuilder-editor-edit-field-title' => 'Edit field',
7071 'gadgets-formbuilder-editor-insert-field' => 'Insert a new field',
7172 'gadgets-formbuilder-editor-chose-field' => 'Chose the type of the new field:',
7273 'gadgets-formbuilder-editor-chose-field-title' => 'Chose field type',
73 - 'gadgets-formbuilder-editor-create-field-title' => 'Create field',
 74+ 'gadgets-formbuilder-editor-create-field-title' => "Create '$1' field",
7475 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please chose a different name.',
7576 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title',
7677 'gadgets-formbuilder-editor-delete-section' => 'Delete this section and all his content',
Index: branches/salvatoreingala/Gadgets/Gadgets.php
@@ -94,7 +94,7 @@
9595 'gadgets-formbuilder-min', 'gadgets-formbuilder-max', 'gadgets-formbuilder-integer', 'gadgets-formbuilder-date',
9696 'gadgets-formbuilder-color',
9797 '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',
 98+ 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-edit-field-title', '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',
101101 'gadgets-formbuilder-editor-edit-section', 'gadgets-formbuilder-editor-chose-title', 'gadgets-formbuilder-editor-chose-title-title'
Index: branches/salvatoreingala/Gadgets/Gadgets_tests.php
@@ -616,7 +616,117 @@
617617 //Check if only the wrong subfield has been reset to default value
618618 $this->assertEquals( $prefs, array( 'foo' => array( 'bar' => false, 'car' => '#123456' ) ) );
619619 }
620 -
 620+
 621+ //Tests for 'list' type fields
 622+ function testPrefsDescriptionsList() {
 623+ $correct = array(
 624+ 'fields' => array(
 625+ array(
 626+ 'name' => 'foo',
 627+ 'type' => 'list',
 628+ 'default' => array(),
 629+ 'field' => array(
 630+ 'type' => 'composite',
 631+ 'fields' => array(
 632+ array(
 633+ 'name' => 'bar',
 634+ 'type' => 'boolean',
 635+ 'label' => '@msg1',
 636+ 'default' => true
 637+ ),
 638+ array(
 639+ 'name' => 'car',
 640+ 'type' => 'color',
 641+ 'label' => '@msg2',
 642+ 'default' => '#123456'
 643+ )
 644+ )
 645+ )
 646+ )
 647+ )
 648+ );
 649+
 650+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 651+
 652+ //Specifying the 'name' member for field must fail
 653+ $wrong = $correct;
 654+ $wrong['fields'][0]['field']['name'] = 'composite';
 655+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 656+
 657+
 658+ $this->assertEquals(
 659+ GadgetPrefs::getDefaults( $correct ),
 660+ array( 'foo' => array() )
 661+ );
 662+
 663+ $this->assertEquals( GadgetPrefs::getMessages( $correct ), array( 'msg1', 'msg2' ) );
 664+
 665+ //Tests with correct pref values
 666+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 667+ $correct,
 668+ array( 'foo' => array() )
 669+ ) );
 670+
 671+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 672+ $correct,
 673+ array( 'foo' => array(
 674+ array(
 675+ 'bar' => true,
 676+ 'car' => '#115599'
 677+ ),
 678+ array(
 679+ 'bar' => false,
 680+ 'car' => '#123456'
 681+ ),
 682+ array(
 683+ 'bar' => true,
 684+ 'car' => '#ffffff'
 685+ )
 686+ )
 687+ )
 688+ ) );
 689+
 690+ //Tests with wrong pref values
 691+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 692+ $correct,
 693+ array( 'foo' => array(
 694+ array(
 695+ 'bar' => null, //wrong
 696+ 'car' => '#115599'
 697+ )
 698+ )
 699+ )
 700+ ) );
 701+
 702+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 703+ $correct,
 704+ array( 'foo' => array( //wrong, not enclosed in array
 705+ 'bar' => null,
 706+ 'car' => '#115599'
 707+ )
 708+ )
 709+ ) );
 710+
 711+ $prefs = array( 'foo' => array(
 712+ array(
 713+ 'bar' => null,
 714+ 'car' => '#115599'
 715+ ),
 716+ array(
 717+ 'bar' => false,
 718+ 'car' => ''
 719+ ),
 720+ array(
 721+ 'bar' => true,
 722+ 'car' => '#ffffff'
 723+ )
 724+ )
 725+ );
 726+
 727+ GadgetPrefs::matchPrefsWithDescription( $correct, $prefs );
 728+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( $correct, $prefs ) );
 729+ }
 730+
621731 //Data provider to be able to reuse a complex preference description for several tests.
622732 function prefsDescProvider() {
623733 return array( array(
Index: branches/salvatoreingala/Gadgets/backend/Gadget.php
@@ -113,9 +113,10 @@
114114 if ( isset( $prefsDescriptionJson ) ) {
115115 $prefsDescription = FormatJson::decode( $prefsDescriptionJson, true );
116116 $gadget->setPrefsDescription( $prefsDescription );
117 -
118 - //Load default gadget preferences. Only useful for anonymous users
119 - $gadget->setPrefs( GadgetPrefs::getDefaults( $prefsDescription ) );
 117+ if ( $gadget->getPrefsDescription() !== null ) {
 118+ //Load default gadget preferences. Only useful for anonymous users
 119+ $gadget->setPrefs( GadgetPrefs::getDefaults( $prefsDescription ) );
 120+ }
120121 }
121122 }
122123
Index: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php
@@ -14,7 +14,7 @@
1515 * Syntax specifications of preference description language.
1616 * Each element describes a field; a "simple" field encodes exactly one gadget preference, but some fields
1717 * may encode for 0 or multiple gadget preferences.
18 - * "Simple" field always have the 'name', the 'label' and the 'default' members.
 18+ * "Simple" field always have the 'name' member. Not "simple" fields never do.
1919 * Each field has a 'description' and may have a 'validator', a 'flattener', and a 'checker'.
2020 * - 'description' is an array that describes all the members of that fields. Each member description has this shape:
2121 * - 'isMandatory' is a boolean that specifies if that member is mandatory for the field;
@@ -30,7 +30,7 @@
3131 * an array of preference values $prefs and the name of a preference $preferenceName and returns an array where
3232 * $prefs[$prefName] is changed in a way that passes validation. If omitted, the default action is to set $prefs[$prefName]
3333 * to $prefDescription['default'].
34 - * - 'getDefault', only for "simple" fields, if a function che takes one argument, the descriptio of the field, and
 34+ * - 'getDefault', only for "simple" fields, if a function che takes one argument, the description of the field, and
3535 * returns its default value; if omitted, the value of the 'default' field is returned.
3636 * - 'getMessages', if specified, is the name of a function that takes a valid description of a field and returns
3737 * a list of messages referred to by it. If omitted, only the "label" field is returned (if it is a message).
@@ -236,7 +236,26 @@
237237 'getDefault' => 'GadgetPrefs::getCompositeDefault',
238238 'checker' => 'GadgetPrefs::checkCompositePref',
239239 'matcher' => 'GadgetPrefs::matchCompositePref'
240 - )
 240+ ),
 241+ 'list' => array(
 242+ 'description' => array(
 243+ 'name' => array(
 244+ 'isMandatory' => true,
 245+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 246+ ),
 247+ 'field' => array(
 248+ 'isMandatory' => true,
 249+ 'validator' => 'is_array'
 250+ ),
 251+ 'default' => array(
 252+ 'isMandatory' => true
 253+ )
 254+ ),
 255+ 'validator' => 'GadgetPrefs::validateListPrefDefinition',
 256+ 'getMessages' => 'GadgetPrefs::getListMessages',
 257+ 'checker' => 'GadgetPrefs::checkListPref',
 258+ 'matcher' => 'GadgetPrefs::matchListPref'
 259+ )
241260 );
242261
243262 private static function isValidPreferenceName( $name ) {
@@ -376,6 +395,24 @@
377396 return true;
378397 }
379398
 399+ private static function validateListPrefDefinition( $prefDefinition ) {
 400+ //Name must not be set for the 'field' description
 401+ if ( array_key_exists( 'name', $prefDefinition['field'] ) ) {
 402+ return false;
 403+ }
 404+
 405+ //Check if the field definition is valid, apart from missing the name
 406+ $itemDescription = $prefDefinition['field'];
 407+ $itemDescription['name'] = 'dummy';
 408+ if ( !self::validateFieldDefinition( $itemDescription ) ) {
 409+ return false;
 410+ };
 411+
 412+ //Finally, type described by the 'field' member must be a simple type (e.g.: have "name" ).
 413+ $type = $itemDescription['type'];
 414+ return isset( self::$prefsDescriptionSpecifications[$type]['description']['name'] );
 415+ }
 416+
380417 //Flattens a simple field, by calling its field-specific flattener if there is any,
381418 //or the default flattener otherwise.
382419 private static function flattenFieldDescription( $fieldDescription ) {
@@ -401,17 +438,10 @@
402439 return $flattenedPrefsDescription;
403440 }
404441
405 - //Validate the description of a 'section' of preferences
406 - private static function validateSectionDefinition( $sectionDescription ) {
 442+ //Validates a single field
 443+ private static function validateFieldDefinition( $fieldDefinition ) {
407444 static $mandatoryCount = array(), $initialized = false;
408445
409 - if ( !is_array( $sectionDescription )
410 - || !isset( $sectionDescription['fields'] )
411 - || !is_array( $sectionDescription['fields'] ) )
412 - {
413 - return false;
414 - }
415 -
416446 if ( !$initialized ) {
417447 //Count of mandatory members for each type
418448 foreach ( self::$prefsDescriptionSpecifications as $type => $typeSpec ) {
@@ -424,71 +454,87 @@
425455 }
426456 $initialized = true;
427457 }
 458+
 459+ //Check if 'type' is set
 460+ if ( !isset( $fieldDefinition['type'] ) ) {
 461+ return false;
 462+ }
428463
429 - //Check if 'fields' is a regular (not-associative) array, and that it is not empty
430 - $count = count( $sectionDescription['fields'] );
431 - if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) {
 464+ $type = $fieldDefinition['type'];
 465+
 466+ //check if 'type' is valid
 467+ if ( !isset( self::$prefsDescriptionSpecifications[$type] ) ) {
432468 return false;
433469 }
434470
435 - //TODO: validation of members other than $prefs['fields']
436 -
437 - //Flattened preferences
438 - $flattenedPrefs = array();
439 -
440 - foreach ( $sectionDescription['fields'] as $optionDefinition ) {
 471+ //Check if all fields satisfy specification
 472+ $typeSpec = self::$prefsDescriptionSpecifications[$type];
 473+ $typeDescription = $typeSpec['description'];
 474+ $count = 0; //count of present mandatory members
 475+ foreach ( $fieldDefinition as $memberName => $memberValue ) {
441476
442 - //Check if 'type' is set
443 - if ( !isset( $optionDefinition['type'] ) ) {
444 - return false;
 477+ if ( $memberName == 'type' ) {
 478+ continue; //'type' must not be checked
445479 }
446480
447 - $type = $optionDefinition['type'];
448 -
449 - //check if 'type' is valid
450 - if ( !isset( self::$prefsDescriptionSpecifications[$type] ) ) {
 481+ if ( !isset( $typeDescription[$memberName] ) ) {
451482 return false;
452483 }
453484
454 - //Check if all fields satisfy specification
455 - $typeSpec = self::$prefsDescriptionSpecifications[$type];
456 - $typeDescription = $typeSpec['description'];
457 - $count = 0; //count of present mandatory members
458 - foreach ( $optionDefinition as $fieldName => $fieldValue ) {
459 -
460 - if ( $fieldName == 'type' ) {
461 - continue; //'type' must not be checked
462 - }
463 -
464 - if ( !isset( $typeDescription[$fieldName] ) ) {
 485+ if ( $typeDescription[$memberName]['isMandatory'] ) {
 486+ ++$count;
 487+ }
 488+
 489+ if ( isset( $typeDescription[$memberName]['validator'] ) ) {
 490+ $validator = $typeDescription[$memberName]['validator'];
 491+ if ( !call_user_func( $validator, $memberValue ) ) {
465492 return false;
466493 }
467 -
468 - if ( $typeDescription[$fieldName]['isMandatory'] ) {
469 - ++$count;
470 - }
471 -
472 - if ( isset( $typeDescription[$fieldName]['validator'] ) ) {
473 - $validator = $typeDescription[$fieldName]['validator'];
474 - if ( !call_user_func( $validator, $fieldValue ) ) {
475 - return false;
476 - }
477 - }
478494 }
479 -
480 - if ( $count != $mandatoryCount[$type] ) {
481 - return false; //not all mandatory members are given
 495+ }
 496+
 497+ if ( $count != $mandatoryCount[$type] ) {
 498+ return false; //not all mandatory members are given
 499+ }
 500+
 501+ if ( isset( $typeSpec['validator'] ) ) {
 502+ //Call type-specific checker for finer validation
 503+ if ( !call_user_func( $typeSpec['validator'], $fieldDefinition ) ) {
 504+ return false;
482505 }
 506+ }
 507+
 508+ return true;
 509+ }
 510+
 511+ //Validate the description of a 'section' of preferences
 512+ private static function validateSectionDefinition( $sectionDescription ) {
 513+ if ( !is_array( $sectionDescription )
 514+ || !isset( $sectionDescription['fields'] )
 515+ || !is_array( $sectionDescription['fields'] ) )
 516+ {
 517+ return false;
 518+ }
 519+
 520+ //Check if 'fields' is a regular (not-associative) array, and that it is not empty
 521+ $count = count( $sectionDescription['fields'] );
 522+ if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) {
 523+ return false;
 524+ }
 525+
 526+ //TODO: validation of members other than $prefs['fields']
 527+
 528+ //Flattened preferences
 529+ $flattenedPrefs = array();
 530+
 531+ foreach ( $sectionDescription['fields'] as $fieldDefinition ) {
483532
484 - if ( isset( $typeSpec['validator'] ) ) {
485 - //Call type-specific checker for finer validation
486 - if ( !call_user_func( $typeSpec['validator'], $optionDefinition ) ) {
487 - return false;
488 - }
 533+ if ( self::validateFieldDefinition( $fieldDefinition ) == false ) {
 534+ return false;
489535 }
490536
491537 //flatten preferences described by this field
492 - $flt = self::flattenFieldDescription( $optionDefinition );
 538+ $flt = self::flattenFieldDescription( $fieldDefinition );
493539
494540 foreach ( $flt as $prefName => $prefDescription ) {
495541 //Finally, check that the 'default' fields exists and is valid
@@ -740,6 +786,22 @@
741787 return true;
742788 }
743789
 790+ //Checker for 'list' preferences
 791+ private static function checkListPref( $prefDescription, $value ) {
 792+ if ( !self::isOrdinaryArray( $value ) ) {
 793+ return false;
 794+ }
 795+
 796+ $itemDescription = $prefDescription['field'];
 797+ foreach ( $value as $item ) {
 798+ if ( !self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) {
 799+ return false;
 800+ }
 801+ }
 802+
 803+ return true;
 804+ }
 805+
744806 /**
745807 * Checks if $prefs is an array of preferences that passes validation.
746808 * It is assumed that $prefsDescription is a valid description of preferences.
@@ -782,6 +844,26 @@
783845 return $prefs;
784846 }
785847
 848+ //Matcher for 'list' type preferences
 849+ //If value is not an array, just reset to default; otherwise, delete elements that fail validation
 850+ private static function matchListPref( $prefDescription, $prefs, $prefName ) {
 851+ if ( !isset( $prefs[$prefName] ) || !self::isOrdinaryArray( $prefs[$prefName] ) ) {
 852+ $prefs[$prefName] = $prefDescription['default'];
 853+ return $prefs;
 854+ }
 855+
 856+ $itemDescription = $prefDescription['field'];
 857+ $newItems = array();
 858+ foreach( $prefs[$prefName] as $item ) {
 859+ if ( self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) {
 860+ $newItems[] = $item;
 861+ }
 862+ }
 863+ $prefs[$prefName] = $newItems;
 864+
 865+ return $prefs;
 866+ }
 867+
786868 /**
787869 * Fixes $prefs so that it matches the description given by $prefsDescription.
788870 * All values of $prefs that fail validation are replaced with default values.
@@ -847,6 +929,24 @@
848930 }
849931
850932 /**
 933+ * Returns the list of messages used by a field. If the field type specifications define a "getMessages" method,
 934+ * uses it, otherwise returns the message in the 'label' member (if any).
 935+ */
 936+ private static function getFieldMessages( $fieldDescription ) {
 937+ $type = $fieldDescription['type'];
 938+ $prefSpec = self::$prefsDescriptionSpecifications[$type];
 939+ if ( isset( $prefSpec['getMessages'] ) ) {
 940+ $getMessages = $prefSpec['getMessages'];
 941+ return call_user_func( $getMessages, $fieldDescription );
 942+ } else {
 943+ if ( isset( $fieldDescription['label'] ) && self::isMessage( $fieldDescription['label'] ) ) {
 944+ return array( substr( $fieldDescription['label'], 1 ) );
 945+ }
 946+ }
 947+ return array();
 948+ }
 949+
 950+ /**
851951 * Returns a list of (unprefixed) messages mentioned by $prefsDescription. It is assumed that
852952 * $prefsDescription is valid (i.e.: GadgetPrefs::isPrefsDescriptionValid( $prefsDescription ) === true).
853953 *
@@ -855,20 +955,9 @@
856956 */
857957 public static function getMessages( $prefsDescription ) {
858958 $msgs = array();
859 -
860 - foreach ( $prefsDescription['fields'] as $prefDesc ) {
861 - $type = $prefDesc['type'];
862 - $prefSpec = self::$prefsDescriptionSpecifications[$type];
863 - if ( isset( $prefSpec['getMessages'] ) ) {
864 - $getMessages = $prefSpec['getMessages'];
865 - $msgs = array_merge( $msgs, call_user_func( $getMessages, $prefDesc ) );
866 - } else {
867 - if ( isset( $prefDesc['label'] ) && self::isMessage( $prefDesc['label'] ) ) {
868 - $msgs[] = substr( $prefDesc['label'], 1 );
869 - }
870 - }
 959+ foreach ( $prefsDescription['fields'] as $fieldDescription ) {
 960+ $msgs = array_merge( $msgs, self::getFieldMessages( $fieldDescription ) );
871961 }
872 -
873962 return array_unique( $msgs );
874963 }
875964
@@ -898,6 +987,11 @@
899988 return array_unique( $msgs );
900989 }
901990
 991+ //Returns the messages for a 'list' field description
 992+ private static function getListMessages( $prefDescription ) {
 993+ return self::getFieldMessages( $prefDescription['field'] );
 994+ }
 995+
902996 //Returns the default value of a 'composite' field, that is the object of the
903997 //default values of its subfields.
904998 private static function getCompositeDefault( $prefDescription ) {
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css
@@ -4,10 +4,6 @@
55 * Released under the MIT and GPL licenses.
66 */
77
8 -.formBuilder-intro {
9 - margin-left: 1em;
10 -}
11 -
128 .formbuilder label {
139 display: inline-block;
1410 text-align: right;
@@ -53,11 +49,48 @@
5450
5551 /* type-specific styles */
5652
57 -.formbuilder .formbuilder-slot-type-label label {
58 - width: 100%;
 53+.formbuilder-field-label label {
 54+ width: 95%;
5955 text-align: left;
6056 }
6157
 58+.formbuilder-button {
 59+ cursor: pointer;
 60+}
 61+
 62+.formbuilder-field-list {
 63+ border: 2px ridge;
 64+}
 65+
 66+.formbuilder-list-item-container {
 67+ float: left;
 68+ margin-right: -35px;
 69+ width: 100%;
 70+}
 71+
 72+.formbuilder-list-item-content {
 73+ margin-right: 35px;
 74+}
 75+
 76+.formbuilder-list-item-buttons {
 77+ float: right;
 78+ width: 35px;
 79+}
 80+
 81+.formbuilder-list-button-move, .formbuilder-list-button-delete {
 82+ float: right;
 83+}
 84+
 85+.formbuilder-list-button-move {
 86+ cursor: move;
 87+}
 88+
 89+/* Center the new item button */
 90+.formbuilder-list-button-new {
 91+ margin-left: auto;
 92+ margin-right: auto;
 93+}
 94+
6295 /* formBuilder editor */
6396
6497 .formbuilder-slot-nonempty {
@@ -79,10 +112,6 @@
80113 height: 17px;
81114 }
82115
83 -.formbuilder-editor-button {
84 - cursor: pointer;
85 -}
86 -
87116 .formbuilder-editor-button-move {
88117 cursor: move;
89118 }
@@ -97,6 +126,7 @@
98127 float: right;
99128 }
100129
 130+
101131 /* Fixes a minor glitch in Firefox (buttons in tabs not floating properly) */
102132 .formbuilder .ui-tabs-nav a > span :not(.formbuilder-editor-button) {
103133 float: left;
@@ -111,6 +141,6 @@
112142 margin-left: 6px;
113143 }
114144
115 -.formbuilder-slot-editable .formbuilder-slot-type-composite {
 145+.formbuilder-slot-editable .formbuilder-field-composite {
116146 padding: 1em;
117147 }
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js
@@ -5,6 +5,244 @@
66 */
77
88 (function($, mw) {
 9+
 10+ //Field types that can be referred to by preference descriptions
 11+ var validFieldTypes = {};
 12+
 13+ //Describes 'name' and 'label' field members, common to all "simple" fields
 14+ var simpleFields = [
 15+ {
 16+ "name": "name",
 17+ "type": "string",
 18+ "label": "name",
 19+ "required": true,
 20+ "maxlength": 40,
 21+ "default": ""
 22+ },
 23+ {
 24+ "name": "label",
 25+ "type": "string",
 26+ "label": "label",
 27+ "required": false,
 28+ "default": ""
 29+ }
 30+ ];
 31+
 32+ //Used by preference editor to build field properties dialogs
 33+ //TODO: document
 34+ var prefsDescriptionSpecifications = {
 35+ "label": {
 36+ "simple": false,
 37+ "builder": [ {
 38+ "name": "label",
 39+ "type": "string",
 40+ "label": "label",
 41+ "required": false,
 42+ "default": ""
 43+ } ]
 44+ },
 45+ "boolean": {
 46+ "simple": true,
 47+ "builder": simpleFields
 48+ },
 49+ "string": {
 50+ "simple": true,
 51+ "builder": simpleFields.concat( [
 52+ {
 53+ "name": "required",
 54+ "type": "boolean",
 55+ "label": "required",
 56+ "default": false
 57+ },
 58+ {
 59+ "name": "minlength",
 60+ "type": "number",
 61+ "label": "minlength",
 62+ "integer": true,
 63+ "min": 0,
 64+ "required": false,
 65+ "default": null
 66+ },
 67+ {
 68+ "name": "maxlength",
 69+ "type": "number",
 70+ "label": "maxlength",
 71+ "integer": true,
 72+ "min": 1,
 73+ "required": false,
 74+ "default": null
 75+ }
 76+ ] )
 77+ },
 78+ "number": {
 79+ "simple": true,
 80+ "builder": simpleFields.concat( [
 81+ {
 82+ "name": "required",
 83+ "type": "boolean",
 84+ "label": "required",
 85+ "default": true
 86+ },
 87+ {
 88+ "name": "integer",
 89+ "type": "boolean",
 90+ "label": "integer",
 91+ "default": false
 92+ },
 93+ {
 94+ "name": "min",
 95+ "type": "number",
 96+ "label": "min",
 97+ "required": false,
 98+ "default": null
 99+ },
 100+ {
 101+ "name": "max",
 102+ "type": "number",
 103+ "label": "max",
 104+ "required": false,
 105+ "default": null
 106+ }
 107+ ] )
 108+ },
 109+ "range": {
 110+ "simple": true,
 111+ "builder": simpleFields.concat( [
 112+ {
 113+ "name": "min",
 114+ "type": "number",
 115+ "label": "min",
 116+ "required": true,
 117+ },
 118+ {
 119+ "name": "step",
 120+ "type": "number",
 121+ "label": "step",
 122+ "required": true,
 123+ "default": 1
 124+ },
 125+ {
 126+ "name": "max",
 127+ "type": "number",
 128+ "label": "max",
 129+ "required": true,
 130+ }
 131+ ] )
 132+ },
 133+ "date": {
 134+ "simple": true,
 135+ "builder": simpleFields
 136+ },
 137+ "color": {
 138+ "simple": true,
 139+ "builder": simpleFields
 140+ },
 141+ "bundle": {
 142+ "simple": false,
 143+ "builder": function( options, callback ) {
 144+ callback(
 145+ new BundleField( {
 146+ "type": "bundle",
 147+ "sections": [
 148+ {
 149+ "title": "Section 1",
 150+ "fields": []
 151+ },
 152+ {
 153+ "title": "Section 2",
 154+ "fields": []
 155+ }
 156+ ]
 157+ }, options )
 158+ );
 159+ }
 160+ },
 161+ "composite": {
 162+ "simple": true,
 163+ "builder": [ {
 164+ "name": "name",
 165+ "type": "string",
 166+ "label": "name",
 167+ "required": true,
 168+ "maxlength": 40,
 169+ "default": ""
 170+ } ]
 171+ },
 172+ "list": {
 173+ "simple": true,
 174+ "builder": function( options, callback ) {
 175+
 176+ //Create list of "simple" types
 177+ var selectOptions = [];
 178+ $.each( prefsDescriptionSpecifications, function( type, typeInfo ) {
 179+ if ( typeInfo.simple === true ) {
 180+ selectOptions.push( { "name": type, "value": type } );
 181+ }
 182+ } );
 183+
 184+ //Create the dialog to chose the field type
 185+ var $form = $( {
 186+ fields: [ {
 187+ "name": "name",
 188+ "type": "string",
 189+ "label": "name",
 190+ "required": true,
 191+ "maxlength": 40,
 192+ "default": ""
 193+ },
 194+ {
 195+ "name": "type",
 196+ "type": "select",
 197+ "label": "type",
 198+ "options": selectOptions
 199+ } ]
 200+ } ).formBuilder( { idPrefix: 'list-chose-type-' } )
 201+ .submit( function() {
 202+ return false; //prevent form submission
 203+ } );
 204+
 205+ $form.dialog( {
 206+ width: 450,
 207+ modal: true,
 208+ resizable: false,
 209+ title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', 'list' ),
 210+ close: function() {
 211+ $( this ).remove();
 212+ },
 213+ buttons: [
 214+ {
 215+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 216+ click: function() {
 217+ var values = $( this ).formBuilder( 'getValues' );
 218+ $( this ).dialog( "close" );
 219+
 220+ var dialog = this;
 221+ createFieldDialog( {
 222+ type: values.type,
 223+ values: {
 224+ "name": values.name
 225+ },
 226+ callback: function( field ) {
 227+ $( dialog ).dialog( 'close' );
 228+ showEditFieldDialog( field.getDesc(), options, callback );
 229+ return true;
 230+ }
 231+ }, { editable: true } );
 232+ }
 233+ },
 234+ {
 235+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 236+ click: function() {
 237+ $( this ).dialog( "close" );
 238+ }
 239+ }
 240+ ]
 241+ } );
 242+ }
 243+ }
 244+ };
 245+
 246+ /* Utility functions */
9247
10248 //Preprocesses strings end possibly replaces them with messages.
11249 //If str starts with "@" the rest of the string is assumed to be
@@ -56,6 +294,258 @@
57295 && name.length <= 40;
58296 }
59297
 298+ //Make a deep copy of an object
 299+ function clone( obj ) {
 300+ return $.extend( true, {}, obj );
 301+ }
 302+
 303+ function deleteFieldRules( field ) {
 304+ //Remove all its validation rules
 305+ var validationSettings = field.getValidationSettings();
 306+ if ( validationSettings.rules ) {
 307+ $.each( validationSettings.rules, function( name, value ) {
 308+ var $input = $( '#' + name );
 309+ if ( $input.length > 0 ) {
 310+ $( '#' + name ).rules( 'remove' );
 311+ }
 312+ } );
 313+ }
 314+ }
 315+
 316+ function addFieldRules( field ) {
 317+ var validationSettings = field.getValidationSettings();
 318+ if ( validationSettings.rules ) {
 319+ $.each( validationSettings.rules, function( name, rules ) {
 320+ var $input = $( '#' + name );
 321+
 322+ //Find messages associated to this rule, if any
 323+ if ( typeof validationSettings.messages != 'undefined' &&
 324+ typeof validationSettings.messages[name] != 'undefined')
 325+ {
 326+ rules.messages = validationSettings.messages[name];
 327+ }
 328+
 329+ if ( $input.length > 0 ) {
 330+ $( '#' + name ).rules( 'add', rules );
 331+ }
 332+ } );
 333+ }
 334+ }
 335+
 336+ function createFieldDialog( params, options ) {
 337+ var self = this;
 338+
 339+ if ( typeof params.callback != 'function' ) {
 340+ $.error( 'createFieldDialog: missing or wrong "callback" parameter' );
 341+ }
 342+
 343+ if ( typeof options == 'undefined' ) {
 344+ options = {};
 345+ }
 346+
 347+ var type, description, values;
 348+ if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) {
 349+ //Create a dialog to choose the type of field to create
 350+ var selectOptions = [];
 351+ $.each( validFieldTypes, function( fieldType ) {
 352+ selectOptions.push( {
 353+ name: fieldType,
 354+ value: fieldType
 355+ } );
 356+ } );
 357+
 358+ $( {
 359+ fields: [ {
 360+ 'name': "type",
 361+ 'type': "select",
 362+ 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ),
 363+ 'options': selectOptions,
 364+ 'default': selectOptions[0].value
 365+ } ]
 366+ } ).formBuilder( { idPrefix: 'chose-field-' } )
 367+ .submit( function() {
 368+ return false; //prevent form submission
 369+ } )
 370+ .dialog( {
 371+ width: 450,
 372+ modal: true,
 373+ resizable: false,
 374+ title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ),
 375+ close: function() {
 376+ $( this ).remove();
 377+ },
 378+ buttons: [
 379+ {
 380+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 381+ click: function() {
 382+ var values = $( this ).formBuilder( 'getValues' );
 383+ $( this ).dialog( "close" );
 384+ createFieldDialog( {
 385+ type: values.type,
 386+ oldDescription: params.oldDescription,
 387+ callback: params.callback
 388+ }, options );
 389+ }
 390+ },
 391+ {
 392+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 393+ click: function() {
 394+ $( this ).dialog( "close" );
 395+ }
 396+ }
 397+ ]
 398+ } );
 399+
 400+ return;
 401+ } else {
 402+ type = params.type;
 403+ if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) {
 404+ $.error( 'createFieldDialog: invalid type: ' + type );
 405+ } else if ( typeof prefsDescriptionSpecifications[type].builder == 'function' ) {
 406+ prefsDescriptionSpecifications[type].builder( options, function( field ) {
 407+ if ( field !== null ) {
 408+ params.callback( field );
 409+ }
 410+ } );
 411+ return;
 412+ }
 413+
 414+ //typeof prefsDescriptionSpecifications[type].builder == 'object'
 415+
 416+ description = {
 417+ fields: prefsDescriptionSpecifications[type].builder
 418+ };
 419+ }
 420+
 421+ if ( typeof params.values != 'undefined' ) {
 422+ values = params.values;
 423+ } else {
 424+ values = {};
 425+ }
 426+
 427+ //Create the dialog to set field properties
 428+ var dlg = $( '<div/>' );
 429+ var form = $( description ).formBuilder( {
 430+ values: values,
 431+ idPrefix: 'create-field-'
 432+ } ).submit( function() {
 433+ return false; //prevent form submission
 434+ } ).appendTo( dlg );
 435+
 436+ dlg.dialog( {
 437+ modal: true,
 438+ width: 550,
 439+ resizable: false,
 440+ title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', type ),
 441+ close: function() {
 442+ $( this ).remove();
 443+ },
 444+ buttons: [
 445+ {
 446+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 447+ click: function() {
 448+ var isValid = $( form ).formBuilder( 'validate' );
 449+
 450+ if ( isValid ) {
 451+ var fieldDescription = $( form ).formBuilder( 'getValues' );
 452+
 453+ if ( typeof type != 'undefined' ) {
 454+ //Remove properties that equal their default
 455+ $.each( description.fields, function( index, fieldSpec ) {
 456+ var property = fieldSpec.name;
 457+ if ( fieldDescription[property] === fieldSpec['default'] ) {
 458+ delete fieldDescription[property];
 459+ }
 460+ } );
 461+ }
 462+
 463+ //Try to create the field. In case of error, warn the user.
 464+ fieldDescription.type = type;
 465+
 466+ if ( typeof params.oldDescription != 'undefined' ) {
 467+ //If there are values in the old description that cannot be set by
 468+ //the dialog, don't lose them (e.g.: 'fields' member in composite fields).
 469+ $.each( params.oldDescription, function( key, value ) {
 470+ if ( typeof fieldDescription[key] == 'undefined' ) {
 471+ fieldDescription[key] = value;
 472+ }
 473+ } );
 474+ }
 475+
 476+ var FieldConstructor = validFieldTypes[type];
 477+ var field;
 478+
 479+ try {
 480+ field = new FieldConstructor( fieldDescription, options );
 481+ } catch ( err ) {
 482+ alert( "Invalid field options: " + err ); //TODO: i18n
 483+ return;
 484+ }
 485+
 486+ if ( params.callback( field ) === true ) {
 487+ $( this ).dialog( "close" );
 488+ }
 489+ }
 490+ }
 491+ },
 492+ {
 493+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 494+ click: function() {
 495+ $( this ).dialog( "close" );
 496+ params.callback( null );
 497+ }
 498+ }
 499+ ]
 500+ } );
 501+ }
 502+
 503+ function showEditFieldDialog( fieldDesc, options, callback ) {
 504+ $( { "fields": [ fieldDesc ] } )
 505+ .formBuilder( {
 506+ editable: true,
 507+ staticFields: true,
 508+ idPrefix: 'list-edit-field-'
 509+ } )
 510+ .submit( function() {
 511+ return false;
 512+ } )
 513+ .dialog( {
 514+ modal: true,
 515+ width: 550,
 516+ resizable: false,
 517+ title: mw.msg( 'gadgets-formbuilder-editor-edit-field-title' ),
 518+ close: function() {
 519+ $( this ).remove();
 520+ },
 521+ buttons: [
 522+ {
 523+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 524+ click: function() {
 525+ var fieldDesc = $( this ).formBuilder( 'getDescription' ).fields[0];
 526+ name = fieldDesc.name;
 527+
 528+ delete fieldDesc.name;
 529+
 530+ $( this ).dialog( "close" );
 531+
 532+ callback( new ListField( {
 533+ type: 'list',
 534+ name: name,
 535+ field: fieldDesc
 536+ }, options ) );
 537+ }
 538+ },
 539+ {
 540+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 541+ click: function() {
 542+ $( this ).dialog( "close" );
 543+ callback( null );
 544+ }
 545+ }
 546+ ]
 547+ } );
 548+ }
 549+
60550 function testOptional( value, element ) {
61551 var rules = $( element ).rules();
62552 if ( typeof rules.required == 'undefined' || rules.required === false ) {
@@ -109,143 +599,6 @@
110600 return new F();
111601 }
112602
113 -
114 - //Field types that can be referred to by preference descriptions
115 - var validFieldTypes = {};
116 -
117 -
118 - //Describes 'name' and 'label' field members
119 - var simpleField = [
120 - {
121 - "name": "name",
122 - "type": "string",
123 - "label": "name",
124 - "required": true,
125 - "maxlength": 40,
126 - "default": ""
127 - },
128 - {
129 - "name": "label",
130 - "type": "string",
131 - "label": "label",
132 - "required": false,
133 - "default": ""
134 - }
135 - ];
136 -
137 - //Used by preference editor to build field properties dialogs
138 - var prefsDescriptionSpecifications = {
139 - "label": [ {
140 - "name": "label",
141 - "type": "string",
142 - "label": "label",
143 - "required": false,
144 - "default": ""
145 - } ],
146 - "boolean": simpleField,
147 - "string" : simpleField.concat( [
148 - {
149 - "name": "required",
150 - "type": "boolean",
151 - "label": "required",
152 - "default": false
153 - },
154 - {
155 - "name": "minlength",
156 - "type": "number",
157 - "label": "minlength",
158 - "integer": true,
159 - "min": 0,
160 - "required": false,
161 - "default": null
162 - },
163 - {
164 - "name": "maxlength",
165 - "type": "number",
166 - "label": "maxlength",
167 - "integer": true,
168 - "min": 1,
169 - "required": false,
170 - "default": null
171 - }
172 - ] ),
173 - "number" : simpleField.concat( [
174 - {
175 - "name": "required",
176 - "type": "boolean",
177 - "label": "required",
178 - "default": true
179 - },
180 - {
181 - "name": "integer",
182 - "type": "boolean",
183 - "label": "integer",
184 - "default": false
185 - },
186 - {
187 - "name": "min",
188 - "type": "number",
189 - "label": "min",
190 - "required": false,
191 - "default": null
192 - },
193 - {
194 - "name": "max",
195 - "type": "number",
196 - "label": "max",
197 - "required": false,
198 - "default": null
199 - }
200 - ] ),
201 - //TODO: "select" is missing
202 - "range": simpleField.concat( [
203 - {
204 - "name": "min",
205 - "type": "number",
206 - "label": "min",
207 - "required": true,
208 - },
209 - {
210 - "name": "step",
211 - "type": "number",
212 - "label": "step",
213 - "required": true,
214 - "default": 1
215 - },
216 - {
217 - "name": "max",
218 - "type": "number",
219 - "label": "max",
220 - "required": true,
221 - }
222 - ] ),
223 - "date": simpleField,
224 - "color": simpleField,
225 - "bundle": function( options ) {
226 - return new BundleField( {
227 - "type": "bundle",
228 - "sections": [
229 - {
230 - "title": "Section 1",
231 - "fields": []
232 - },
233 - {
234 - "title": "Section 2",
235 - "fields": []
236 - }
237 - ]
238 - }, options )
239 - },
240 - "composite": [ {
241 - "name": "name",
242 - "type": "string",
243 - "label": "name",
244 - "required": true,
245 - "maxlength": 40,
246 - "default": ""
247 - } ]
248 - };
249 -
250603 /* Basic interface for fields */
251604 function Field( desc, options ) {
252605 if ( typeof options.idPrefix == 'undefined' ) {
@@ -290,7 +643,7 @@
291644 }
292645
293646 this.$div = $( '<div/>' )
294 - .addClass( 'formbuilder-slot-type-' + this.desc.type )
 647+ .addClass( 'formbuilder-field formbuilder-field-' + this.desc.type )
295648 .data( 'field', this );
296649 }
297650
@@ -345,7 +698,7 @@
346699 }
347700
348701 SimpleField.prototype.getDesc = function( useValuesAsDefaults ) {
349 - var desc = LabelField.prototype.getDesc.call( this, useValuesAsDefaults );
 702+ var desc = clone( LabelField.prototype.getDesc.call( this, useValuesAsDefaults ) );
350703 if ( useValuesAsDefaults === true ) {
351704 //set 'default' to current value.
352705 var values = this.getValues();
@@ -800,40 +1153,6 @@
8011154
8021155 /* A field that represent a section (group of fields) */
8031156
804 - function deleteFieldRules( field ) {
805 - //Remove all its validation rules
806 - var validationSettings = field.getValidationSettings();
807 - if ( validationSettings.rules ) {
808 - $.each( validationSettings.rules, function( name, value ) {
809 - var $input = $( '#' + name );
810 - if ( $input.length > 0 ) {
811 - $( '#' + name ).rules( 'remove' );
812 - }
813 - } );
814 - }
815 - }
816 -
817 - function addFieldRules( field ) {
818 - var validationSettings = field.getValidationSettings();
819 - if ( validationSettings.rules ) {
820 - $.each( validationSettings.rules, function( name, rules ) {
821 - var $input = $( '#' + name );
822 -
823 - //Find messages associated to this rule, if any
824 - if ( typeof validationSettings.messages != 'undefined' &&
825 - typeof validationSettings.messages[name] != 'undefined')
826 - {
827 - rules.messages = validationSettings.messages[name];
828 - }
829 -
830 - if ( $input.length > 0 ) {
831 - $( '#' + name ).rules( 'add', rules );
832 - }
833 - } );
834 - }
835 - }
836 -
837 -
8381157 SectionField.prototype = object( Field.prototype );
8391158 SectionField.prototype.constructor = SectionField;
8401159 function SectionField( desc, options, id ) {
@@ -846,9 +1165,9 @@
8471166 }
8481167
8491168 for ( var i = 0; i < this.desc.fields.length; i++ ) {
850 - if ( options.editable === true ) {
 1169+ if ( options.editable === true && !options.staticFields ) {
8511170 //add an empty slot
852 - this._createSlot( true ).appendTo( this.$div );
 1171+ this._createSlot( 'yes' ).appendTo( this.$div );
8531172 }
8541173
8551174 var field = this.desc.fields[i],
@@ -858,15 +1177,22 @@
8591178 $.error( "field with invalid type: " + field.type );
8601179 }
8611180
 1181+ var editable;
 1182+ if ( options.editable === true ) {
 1183+ editable = options.staticFields ? 'partial' : 'yes';
 1184+ } else {
 1185+ editable = 'no';
 1186+ }
 1187+
8621188 var f = new FieldConstructor( field, options ),
863 - $slot = this._createSlot( options.editable === true, f );
 1189+ $slot = this._createSlot( editable, f );
8641190
8651191 $slot.appendTo( this.$div );
8661192 }
8671193
868 - if ( options.editable === true ) {
 1194+ if ( options.editable === true && !options.staticFields ) {
8691195 //add an empty slot
870 - this._createSlot( true ).appendTo( this.$div );
 1196+ this._createSlot( 'yes' ).appendTo( this.$div );
8711197 }
8721198 }
8731199
@@ -875,7 +1201,7 @@
8761202 };
8771203
8781204 SectionField.prototype.getDesc = function( useValuesAsDefaults ) {
879 - var desc = this.desc;
 1205+ var desc = clone( this.desc );
8801206 desc.fields = [];
8811207 this.$div.children().each( function( idx, slot ) {
8821208 var field = $( slot ).data( 'field' );
@@ -916,167 +1242,6 @@
9171243 return settings;
9181244 };
9191245
920 - SectionField.prototype._createFieldDialog = function( params ) {
921 - var self = this;
922 -
923 - if ( typeof params.callback != 'function' ) {
924 - $.error( 'createFieldDialog: missing or wrong "callback" parameter' );
925 - }
926 -
927 - var type, description, values;
928 - if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) {
929 - //Create a dialog to choose the type of field to create
930 - var selectOptions = [];
931 - $.each( validFieldTypes, function( fieldType ) {
932 - selectOptions.push( {
933 - name: fieldType,
934 - value: fieldType
935 - } );
936 - } );
937 -
938 - $( {
939 - fields: [ {
940 - 'name': "type",
941 - 'type': "select",
942 - 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ),
943 - 'options': selectOptions,
944 - 'default': selectOptions[0].value
945 - } ]
946 - } ).formBuilder( {} )
947 - .submit( function() {
948 - return false; //prevent form submission
949 - } )
950 - .dialog( {
951 - width: 450,
952 - modal: true,
953 - resizable: false,
954 - title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ),
955 - close: function() {
956 - $( this ).remove();
957 - },
958 - buttons: [
959 - {
960 - text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
961 - click: function() {
962 - var values = $( this ).formBuilder( 'getValues' );
963 - $( this ).dialog( "close" );
964 - self._createFieldDialog( {
965 - type: values.type,
966 - oldDescription: params.oldDescription,
967 - callback: params.callback
968 - } );
969 - }
970 - },
971 - {
972 - text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
973 - click: function() {
974 - $( this ).dialog( "close" );
975 - }
976 - }
977 - ]
978 - } );
979 -
980 - return;
981 - } else {
982 - type = params.type;
983 - if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) {
984 - $.error( 'createFieldDialog: invalid type: ' + type );
985 - } else if ( typeof prefsDescriptionSpecifications[type] == 'function' ) {
986 - var field = prefsDescriptionSpecifications[type]( this.options );
987 - if ( params.callback( field ) === true ) {
988 - $( this ).dialog( "close" );
989 - }
990 - return;
991 - }
992 -
993 - //typeof prefsDescriptionSpecifications[type] == 'object'
994 -
995 - description = {
996 - fields: prefsDescriptionSpecifications[type]
997 - };
998 - }
999 -
1000 - if ( typeof params.values != 'undefined' ) {
1001 - values = params.values;
1002 - } else {
1003 - values = {};
1004 - }
1005 -
1006 - //Create the dialog to set field properties
1007 - var dlg = $( '<div/>' );
1008 - var form = $( description ).formBuilder( {
1009 - values: values
1010 - } ).submit( function() {
1011 - return false; //prevent form submission
1012 - } ).appendTo( dlg );
1013 -
1014 - dlg.dialog( {
1015 - modal: true,
1016 - width: 550,
1017 - resizable: false,
1018 - title: mw.msg( 'gadgets-formbuilder-editor-create-field-title' ),
1019 - close: function() {
1020 - $( this ).remove();
1021 - },
1022 - buttons: [
1023 - {
1024 - text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
1025 - click: function() {
1026 - var isValid = $( form ).formBuilder( 'validate' );
1027 -
1028 - if ( isValid ) {
1029 - var fieldDescription = $( form ).formBuilder( 'getValues' );
1030 -
1031 - if ( typeof type != 'undefined' ) {
1032 - //Remove properties that equal their default
1033 - $.each( description.fields, function( index, fieldSpec ) {
1034 - var property = fieldSpec.name;
1035 - if ( fieldDescription[property] === fieldSpec['default'] ) {
1036 - delete fieldDescription[property];
1037 - }
1038 - } );
1039 - }
1040 -
1041 - //Try to create the field. In case of error, warn the user.
1042 - fieldDescription.type = type;
1043 -
1044 - if ( typeof params.oldDescription != 'undefined' ) {
1045 - //If there are values in the old description that cannot be set by
1046 - //the dialog, don't lose them (e.g.: 'fields' member in composite fields).
1047 - $.each( params.oldDescription, function( key, value ) {
1048 - if ( typeof fieldDescription[key] == 'undefined' ) {
1049 - fieldDescription[key] = value;
1050 - }
1051 - } );
1052 - }
1053 -
1054 - var FieldConstructor = validFieldTypes[type];
1055 - var field;
1056 -
1057 - try {
1058 - field = new FieldConstructor( fieldDescription, self.options );
1059 - } catch ( err ) {
1060 - alert( "Invalid field options: " + err ); //TODO: i18n
1061 - return;
1062 - }
1063 -
1064 - if ( params.callback( field ) === true ) {
1065 - $( this ).dialog( "close" );
1066 - }
1067 - }
1068 - }
1069 - },
1070 - {
1071 - text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
1072 - click: function() {
1073 - $( this ).dialog( "close" );
1074 - params.callback( null );
1075 - }
1076 - }
1077 - ]
1078 - } );
1079 - };
1080 -
10811246 SectionField.prototype._deleteSlot = function( $slot ) {
10821247 var field = $slot.data( 'field' );
10831248 if ( field !== undefined ) {
@@ -1093,7 +1258,7 @@
10941259 $slot = $( '<div/>' ).addClass( 'formbuilder-slot ui-widget' ),
10951260 $divButtons;
10961261
1097 - if ( editable ) {
 1262+ if ( editable == 'partial' || editable == 'yes' ) {
10981263 $slot.addClass( 'formbuilder-slot-editable' );
10991264
11001265 $divButtons = $( '<div/>' )
@@ -1106,26 +1271,29 @@
11071272 $slot.prepend( field.getElement() )
11081273 .data( 'field', field );
11091274
1110 - if ( editable ) {
 1275+ if ( editable == 'partial' || editable == 'yes' ) {
11111276 $slot.addClass( 'formbuilder-slot-nonempty' );
11121277
1113 - //Add the handle for moving slots
1114 - $( '<span />' )
1115 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' )
1116 - .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
1117 - .mousedown( function() {
1118 - $( this ).focus();
1119 - } )
1120 - .appendTo( $divButtons );
 1278+ if ( editable == 'yes' ) {
 1279+ //Add the handle for moving slots
 1280+ $( '<span />' )
 1281+ .addClass( 'formbuilder-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' )
 1282+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
 1283+ .mousedown( function() {
 1284+ $( this ).focus();
 1285+ } )
 1286+ .appendTo( $divButtons );
 1287+ }
11211288
11221289 //Add the button for changing existing slots
11231290 var type = field.getDesc().type;
1124 - if ( typeof prefsDescriptionSpecifications[type] != 'function' ) {
 1291+ //TODO: using the 'builder' info is not optimal
 1292+ if ( typeof prefsDescriptionSpecifications[type].builder != 'function' ) {
11251293 $( '<a href="javascript:;" />' )
1126 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit ui-icon ui-icon-gear' )
 1294+ .addClass( 'formbuilder-button formbuilder-editor-button-edit ui-icon ui-icon-gear' )
11271295 .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) )
11281296 .click( function() {
1129 - self._createFieldDialog( {
 1297+ createFieldDialog( {
11301298 type: field.getDesc().type,
11311299 values: field.getDesc(),
11321300 oldDescription: field.getDesc(),
@@ -1148,7 +1316,7 @@
11491317 return false;
11501318 }
11511319
1152 - var $newSlot = self._createSlot( true, newField );
 1320+ var $newSlot = self._createSlot( 'yes', newField );
11531321
11541322 deleteFieldRules( field );
11551323
@@ -1159,39 +1327,41 @@
11601328 }
11611329 return true;
11621330 }
1163 - } );
 1331+ }, this.options );
11641332 } )
11651333 .appendTo( $divButtons );
11661334 }
11671335
1168 - //Add the button to delete slots
1169 - $( '<a href="javascript:;" />' )
1170 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete ui-icon ui-icon-trash' )
1171 - .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) )
1172 - .click( function( event, ui ) {
1173 - //Make both slots disappear, then delete them
1174 - $.each( [$slot, $slot.prev()], function( idx, $s ) {
1175 - $s.slideUp( function() {
1176 - self._deleteSlot( $s );
 1336+ if ( editable == 'yes' ) {
 1337+ //Add the button to delete slots
 1338+ $( '<a href="javascript:;" />' )
 1339+ .addClass( 'formbuilder-button formbuilder-editor-button-delete ui-icon ui-icon-trash' )
 1340+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) )
 1341+ .click( function( event, ui ) {
 1342+ //Make both slots disappear, then delete them
 1343+ $.each( [$slot, $slot.prev()], function( idx, $s ) {
 1344+ $s.slideUp( function() {
 1345+ self._deleteSlot( $s );
 1346+ } );
11771347 } );
1178 - } );
1179 - } )
1180 - .appendTo( $divButtons );
 1348+ } )
 1349+ .appendTo( $divButtons );
11811350
1182 - //Make this slot draggable to allow moving it
1183 - $slot.draggable( {
1184 - revert: true,
1185 - handle: ".formbuilder-editor-button-move",
1186 - helper: "original",
1187 - zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way
1188 - scroll: false,
1189 - opacity: 0.8,
1190 - cursor: "move",
1191 - cursorAt: {
1192 - top: -5,
1193 - left: -5
1194 - }
1195 - } );
 1351+ //Make this slot draggable to allow moving it
 1352+ $slot.draggable( {
 1353+ revert: true,
 1354+ handle: ".formbuilder-editor-button-move",
 1355+ helper: "original",
 1356+ zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way
 1357+ scroll: false,
 1358+ opacity: 0.8,
 1359+ cursor: "move",
 1360+ cursorAt: {
 1361+ top: -5,
 1362+ left: -5
 1363+ }
 1364+ } );
 1365+ }
11961366 }
11971367 } else {
11981368 //Create empty slot
@@ -1209,8 +1379,8 @@
12101380 $( dstSlot ).replaceWith( srcSlot );
12111381
12121382 //Add one empty slot before and one after the new position
1213 - self._createSlot( true ).insertBefore( srcSlot );
1214 - self._createSlot( true ).insertAfter( srcSlot );
 1383+ self._createSlot( 'yes' ).insertBefore( srcSlot );
 1384+ self._createSlot( 'yes' ).insertAfter( srcSlot );
12151385 },
12161386 accept: function( draggable ) {
12171387 //All non empty slots accepted, except for closest siblings
@@ -1222,10 +1392,10 @@
12231393
12241394 //The button to create a new field
12251395 $( '<a href="javascript:;" />' )
1226 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-new ui-icon ui-icon-plus' )
 1396+ .addClass( 'formbuilder-button formbuilder-editor-button-new ui-icon ui-icon-plus' )
12271397 .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) )
12281398 .click( function() {
1229 - self._createFieldDialog( {
 1399+ createFieldDialog( {
12301400 callback: function( field ) {
12311401 if ( field !== null ) {
12321402 //check that there are no duplicate preference names
@@ -1243,8 +1413,8 @@
12441414 return false;
12451415 }
12461416
1247 - var $newSlot = self._createSlot( true, field ).hide(),
1248 - $newEmptySlot = self._createSlot( true ).hide();
 1417+ var $newSlot = self._createSlot( 'yes', field ).hide(),
 1418+ $newEmptySlot = self._createSlot( 'yes' ).hide();
12491419
12501420 $slot.after( $newSlot, $newEmptySlot );
12511421
@@ -1259,7 +1429,7 @@
12601430 }
12611431 return true;
12621432 }
1263 - } );
 1433+ }, self.options );
12641434 } )
12651435 .appendTo( $divButtons );
12661436 }
@@ -1309,7 +1479,7 @@
13101480 if ( options.editable === true ) {
13111481 //Add "delete section" button
13121482 $( '<span />' )
1313 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' )
 1483+ .addClass( 'formbuilder-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' )
13141484 .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-section' ) )
13151485 .click( function() {
13161486 var sectionField = $( ui.panel ).data( 'field' );
@@ -1324,7 +1494,7 @@
13251495
13261496 //Add "edit section" button
13271497 $( '<span />' )
1328 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' )
 1498+ .addClass( 'formbuilder-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' )
13291499 .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-section' ) )
13301500 .click( function() {
13311501 var button = this,
@@ -1339,7 +1509,8 @@
13401510 } ).formBuilder( {
13411511 values: {
13421512 title: sectionField.getDesc().title
1343 - }
 1513+ },
 1514+ idPrefix: 'section-edit-title-'
13441515 } ).dialog( {
13451516 modal: true,
13461517 resizable: false,
@@ -1391,7 +1562,7 @@
13921563 if ( options.editable === true ) {
13931564 //Add the button to create a new section
13941565 $( '<span>' )
1395 - .addClass( 'formbuilder-editor-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' )
 1566+ .addClass( 'formbuilder-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' )
13961567 .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-new-section' ) )
13971568 .click( function() {
13981569 $( {
@@ -1400,7 +1571,7 @@
14011572 'type': "string",
14021573 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' )
14031574 } ]
1404 - } ).formBuilder( {} ).dialog( {
 1575+ } ).formBuilder( { idPrefix: 'section-create-' } ).dialog( {
14051576 modal: true,
14061577 resizable: false,
14071578 title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ),
@@ -1459,7 +1630,7 @@
14601631 };
14611632
14621633 BundleField.prototype.getDesc = function( useValuesAsDefaults ) {
1463 - var desc = this.desc;
 1634+ var desc = clone( this.desc );
14641635 desc.sections = [];
14651636 this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
14661637 var panel = $( anchor ).data( 'panel' ),
@@ -1503,7 +1674,7 @@
15041675
15051676 //TODO: add something to easily visually identify 'composite' fields during editing
15061677
1507 - var sectionOptions = $.extend( {}, options );
 1678+ var sectionOptions = clone( options );
15081679
15091680 //Add another chunk to the prefix, to ensure uniqueness
15101681 sectionOptions.idPrefix += desc.name + '-';
@@ -1517,7 +1688,7 @@
15181689 }
15191690
15201691 CompositeField.prototype.getDesc = function( useValuesAsDefaults ) {
1521 - var desc = this.desc;
 1692+ var desc = clone( this.desc );
15221693 desc.fields = this._section.getDesc( useValuesAsDefaults ).fields;
15231694 return desc;
15241695 };
@@ -1532,6 +1703,155 @@
15331704
15341705 validFieldTypes["composite"] = CompositeField;
15351706
 1707+ /* A field for 'composite' fields */
 1708+
 1709+ ListField.prototype = object( EmptyField.prototype );
 1710+ ListField.prototype.constructor = ListField;
 1711+ function ListField( desc, options ) {
 1712+ EmptyField.call( this, desc, options );
 1713+
 1714+ if ( typeof desc.field != 'object' ) {
 1715+ $.error( "The 'field' parameter is missing or wrong" );
 1716+ }
 1717+
 1718+ if ( typeof desc.field.name != 'undefined' ) {
 1719+ $.error( "The 'field' parameter must not specify the field 'name'" );
 1720+ }
 1721+
 1722+ if ( ( typeof desc.field.type != 'string' )
 1723+ || prefsDescriptionSpecifications[desc.field.type].simple !== true )
 1724+ {
 1725+ $.error( "Missing or invalid field type specified in 'field' parameter." );
 1726+ }
 1727+
 1728+ this._$divItems = $( '<div/>' ).addClass( 'formbuilder-list-items' );
 1729+
 1730+ if ( typeof options.values == 'undefined' ) {
 1731+ options.values = {};
 1732+ }
 1733+
 1734+ var value = ( typeof options.values[desc.name] != 'undefined' ) ? options.values[desc.name] : desc['default'];
 1735+ var self = this;
 1736+ if ( typeof value != 'undefined' ) {
 1737+ $.each( value, function( index, itemValue ) {
 1738+ self._createItem( false, itemValue );
 1739+ } );
 1740+ }
 1741+
 1742+ this._$divItems.sortable( {
 1743+ axis: 'y',
 1744+ items: '.formbuilder-list-item',
 1745+ handle: '.formbuilder-list-button-move',
 1746+ placeholder: 'ui-state-highlight',
 1747+ forcePlaceholderSize: true
 1748+ } )
 1749+ .appendTo( this.$div );
 1750+
 1751+ $( '<a href="javascript:;" />' )
 1752+ .addClass( 'formbuilder-button formbuilder-list-button-new ui-icon ui-icon-plus' )
 1753+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) )
 1754+ .click( function() {
 1755+ self._createItem( true );
 1756+ } )
 1757+ .appendTo( this.$div );
 1758+ }
 1759+
 1760+ ListField.prototype._createItem = function( animated, itemValue ) {
 1761+ var itemDesc = $.extend( {}, this.desc.field, {
 1762+ "name": this.desc.name
 1763+ } );
 1764+ var itemOptions = $.extend( {}, this.options, {
 1765+ editable: false,
 1766+ idPrefix: this.options.idPrefix + getIncrementalCounter() + "-"
 1767+ } );
 1768+
 1769+ if ( typeof itemValue != 'undefined' ) {
 1770+ itemOptions.values = pair( this.desc.name, itemValue );
 1771+ } else {
 1772+ itemOptions.values = pair( this.desc.name, this.desc.field['default'] );
 1773+ }
 1774+
 1775+ var FieldConstructor = validFieldTypes[this.desc.field.type];
 1776+ var itemField = new FieldConstructor( itemDesc, itemOptions );
 1777+ var $itemDiv = $( '<div/>' )
 1778+ .addClass( 'formbuilder-list-item' )
 1779+ .data( 'field', itemField );
 1780+
 1781+ var $itemContent = $( '<div/>' )
 1782+ .addClass( 'formbuilder-list-item-content' )
 1783+ .append( itemField.getElement() );
 1784+
 1785+ $( '<div/>' )
 1786+ .addClass( 'formbuilder-list-item-container' )
 1787+ .append( $itemContent )
 1788+ .appendTo( $itemDiv );
 1789+
 1790+ var $itemButtons = $( '<div/>' )
 1791+ .addClass( 'formbuilder-list-item-buttons' );
 1792+
 1793+
 1794+ $( '<span/>' )
 1795+ .addClass( 'formbuilder-button formbuilder-list-button-delete ui-icon ui-icon-trash' )
 1796+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete' ) )
 1797+ .click( function() {
 1798+ $itemDiv.slideUp( function() {
 1799+ deleteFieldRules( itemField );
 1800+ $itemDiv.remove();
 1801+ } );
 1802+ } )
 1803+ .appendTo( $itemButtons );
 1804+
 1805+ $( '<span/>' )
 1806+ .addClass( 'formbuilder-button formbuilder-list-button-move ui-icon ui-icon-arrow-4' )
 1807+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
 1808+ .appendTo( $itemButtons );
 1809+
 1810+ $itemButtons.appendTo( $itemDiv );
 1811+
 1812+ //Add an empty div with clear:both style
 1813+ $itemDiv.append( $('<div style="clear:both"></div>' ) );
 1814+
 1815+ if ( animated ) {
 1816+ $itemDiv.hide()
 1817+ .appendTo( this._$divItems )
 1818+ .slideDown();
 1819+ } else {
 1820+ $itemDiv.appendTo( this._$divItems );
 1821+ }
 1822+ };
 1823+
 1824+ ListField.prototype.getDesc = function( useValuesAsDefaults ) {
 1825+ var desc = clone( this.desc );
 1826+ if ( useValuesAsDefaults ) {
 1827+ desc['default'] = this.getValues()[this.desc.name];
 1828+ }
 1829+ return desc;
 1830+ };
 1831+
 1832+ ListField.prototype.getValues = function() {
 1833+ var value = [];
 1834+ this._$divItems.children().each( function( index, divItem ) {
 1835+ var field = $( divItem ).data( 'field' );
 1836+ $.each( field.getValues(), function( name, v ) {
 1837+ value.push( v );
 1838+ } );
 1839+ } );
 1840+
 1841+ return pair( this.desc.name, value );
 1842+ };
 1843+
 1844+ ListField.prototype.getValidationSettings = function() {
 1845+ var validationSettings = {};
 1846+ this._$divItems.children().each( function( index, divItem ) {
 1847+ var field = $( divItem ).data( 'field' );
 1848+ $.extend( true, validationSettings, field.getValidationSettings() );
 1849+ } );
 1850+ return validationSettings;
 1851+ };
 1852+
 1853+ validFieldTypes["list"] = ListField;
 1854+
 1855+
15361856 /* Public methods */
15371857
15381858 /**
@@ -1559,7 +1879,8 @@
15601880 idPrefix: options.idPrefix,
15611881 msgPrefix: options.msgPrefix,
15621882 values: options.values,
1563 - editable: options.editable === true
 1883+ editable: options.editable === true,
 1884+ staticFields: options.staticFields === true
15641885 } );
15651886
15661887 section.getElement().appendTo( $form );

Status & tagging log