r99970 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r99969‎ | r99970 | r99971 >
Date:20:15, 16 October 2011
Author:catrope
Status:deferred
Tags:
Comment:
[RL2] Mostly integrate Salvatore's branch. This probably doesn't quite work yet
Modified paths:
  • /branches/RL2/extensions/Gadgets/Gadgets.i18n.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/Gadgets.php (modified) (history)
  • /branches/RL2/extensions/Gadgets/api/ApiGetGadgetPrefs.php (added) (history)
  • /branches/RL2/extensions/Gadgets/api/ApiSetGadgetPrefs.php (added) (history)
  • /branches/RL2/extensions/Gadgets/backend/GadgetOptionsResourceLoaderModule.php (added) (history)
  • /branches/RL2/extensions/Gadgets/backend/GadgetPrefs.php (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.css (added) (history)
  • /branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.js (added) (history)
  • /branches/RL2/extensions/Gadgets/tests/GadgetsTest.php (modified) (history)

Diff [purge]

Index: branches/RL2/extensions/Gadgets/Gadgets.i18n.php
@@ -118,6 +118,47 @@
119119 'group-gadgetmanagers' => 'Gadget managers',
120120 'group-gadgetmanagers-member' => 'gadget manager',
121121 'grouppage-gadgetmanagers' => '{{ns:project}}:Gadget managers',
 122+
 123+ # Gadget preferences
 124+ 'gadgets-configure' => 'Configure',
 125+ 'gadgets-configuration-of' => 'Configuration of $1',
 126+ 'gadgets-prefs-save' => 'Save',
 127+ 'gadgets-prefs-close' => 'Close',
 128+ 'gadgets-unexpected-error' => 'An unexpected error occurred. Please check your connection. If the problems persists, please contact the developers.',
 129+ 'gadgets-save-invalid' => 'There are fields with invalid value.',
 130+ 'gadgets-save-success' => 'Settings saved successfully.',
 131+ 'gadgets-save-failed' => 'Failed to save settings. If the problems persists, please contact the developers.',
 132+ 'gadgets-ajax-wrongparams' => 'An AJAX request with wrong parameters has been made; this is most likely a bug.',
 133+ 'gadgets-ajax-wrongsyntax' => 'There was an unexpected error while reading the saved gadget\'s configuration description.',
 134+ 'gadgets-ajax-unlogged' => 'This action is only allowed to registered, logged in users.',
 135+ 'gadgets-formbuilder-required' => 'This field is required.',
 136+ 'gadgets-formbuilder-minlength' => 'Please enter at least $1 characters.',
 137+ 'gadgets-formbuilder-maxlength' => 'Please enter no more than $1 characters.',
 138+ 'gadgets-formbuilder-min' => 'Please enter a value not less than $1.',
 139+ 'gadgets-formbuilder-max' => 'Please enter a value not greater than $1.',
 140+ 'gadgets-formbuilder-integer' => 'Please enter an integer number.',
 141+ 'gadgets-formbuilder-date' => 'Please enter a valid date.',
 142+ 'gadgets-formbuilder-color' => 'Please enter a valid color.',
 143+ 'gadgets-formbuilder-scalar' => 'Valid values are true, false, null, a floating point number or a double quoted string.',
 144+ 'gadgets-formbuilder-list-required' => 'Please create at least one item.',
 145+ 'gadgets-formbuilder-list-minlength' => 'Please insert at least $1 items.',
 146+ 'gadgets-formbuilder-list-maxlength' => 'Please insert no more than $1 items.',
 147+ 'gadgets-formbuilder-editor-ok' => 'OK',
 148+ 'gadgets-formbuilder-editor-cancel' => 'Cancel',
 149+ 'gadgets-formbuilder-editor-move-field' => 'Move this field',
 150+ 'gadgets-formbuilder-editor-delete-field' => 'Delete this field',
 151+ 'gadgets-formbuilder-editor-edit-field' => 'Edit field properties',
 152+ 'gadgets-formbuilder-editor-edit-field-title' => 'Edit field',
 153+ 'gadgets-formbuilder-editor-insert-field' => 'Insert a new field',
 154+ 'gadgets-formbuilder-editor-choose-field' => 'Choose the type of the new field:',
 155+ 'gadgets-formbuilder-editor-choose-field-title' => 'Choose field type',
 156+ 'gadgets-formbuilder-editor-create-field-title' => "Create '$1' field",
 157+ 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please choose a different name.',
 158+ 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title',
 159+ 'gadgets-formbuilder-editor-delete-section' => 'Delete this section and all his content',
 160+ 'gadgets-formbuilder-editor-new-section' => 'Create a new section',
 161+ 'gadgets-formbuilder-editor-choose-title' => 'Choose the title of the new section:',
 162+ 'gadgets-formbuilder-editor-choose-title-title' => 'Choose section title',
122163 );
123164
124165 /** Message documentation (Message documentation)
Index: branches/RL2/extensions/Gadgets/tests/GadgetsTest.php
@@ -6,6 +6,7 @@
77 class GadgetsTest extends PHPUnit_Framework_TestCase {
88
99 private function create( $line ) {
 10+ // TODO fails now
1011 $g = Gadget::newFromDefinition( $line );
1112 // assertInstanceOf() is available since PHPUnit 3.5
1213 $this->assertEquals( 'Gadget', get_class( $g ) );
@@ -18,7 +19,7 @@
1920 }
2021
2122 function testSimpleCases() {
22 - $g = $this->create( '* foo bar| foo.css|foo.js|foo.bar' );
 23+ $g = $this->create( '* foo bar| foo.css|foo.js|foo.bar' ); //FIXME
2324 $this->assertEquals( 'foo_bar', $g->getId() );
2425 $this->assertEquals( 'ext.gadget.foo_bar', $g->getModuleName() );
2526 $this->assertEquals( array( 'Gadget-foo.js' ), $g->getScripts() );
@@ -31,14 +32,14 @@
3233 }
3334
3435 function testRLtag() {
35 - $g = $this->create( '*foo [ResourceLoader]|foo.js|foo.css' );
 36+ $g = $this->create( '*foo [ResourceLoader]|foo.js|foo.css' ); //FIXME
3637 $this->assertEquals( 'foo', $g->getId() );
3738 $this->assertTrue( $g->supportsResourceLoader() );
3839 $this->assertEquals(0, count( $g->getLegacyScripts() ) );
3940 }
4041
4142 function testDependencies() {
42 - $g = $this->create( '* foo[ResourceLoader|dependencies=jquery.ui]|bar.js' );
 43+ $g = $this->create( '* foo[ResourceLoader|dependencies=jquery.ui]|bar.js' ); //FIXME
4344 $this->assertEquals( array( 'Gadget-bar.js' ), $g->getScripts() );
4445 $this->assertTrue( $g->supportsResourceLoader() );
4546 $this->assertEquals( array( 'jquery.ui' ), $g->getDependencies() );
@@ -79,4 +80,1029 @@
8081 $wgOut = $old_wgOut;
8182 $wgTitle = $old_wgTitle;
8283 }
 84+
 85+ //Test preferences descriptions validator (generic)
 86+ function testPrefsDescriptions() {
 87+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( null ) );
 88+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array() ) );
 89+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array( 'fields' => array() ) ) );
 90+
 91+ //Test with stdClass instead if array
 92+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( (object)array(
 93+ 'fields' => array(
 94+ array(
 95+ 'name' => 'testBoolean',
 96+ 'type' => 'boolean',
 97+ 'label' => 'foo',
 98+ 'default' => 'bar'
 99+ )
 100+ )
 101+ ) ) );
 102+
 103+ //Test with wrong type
 104+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 105+ 'fields' => array(
 106+ array(
 107+ 'name' => 'testUnexisting',
 108+ 'type' => 'unexistingtype',
 109+ 'label' => 'foo',
 110+ 'default' => 'bar'
 111+ )
 112+ )
 113+ ) ) );
 114+
 115+ //Test with missing name
 116+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 117+ 'fields' => array(
 118+ array(
 119+ 'type' => 'boolean',
 120+ 'label' => 'foo',
 121+ 'default' => true
 122+ )
 123+ )
 124+ ) ) );
 125+
 126+ //Test with wrong preference name
 127+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 128+ 'fields' => array(
 129+ array(
 130+ 'name' => 'testWrongN@me',
 131+ 'type' => 'boolean',
 132+ 'label' => 'foo',
 133+ 'default' => true
 134+ )
 135+ )
 136+ ) ) );
 137+
 138+ //Test with two fields with the same name
 139+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 140+ 'fields' => array(
 141+ array(
 142+ 'name' => 'testBoolean',
 143+ 'type' => 'boolean',
 144+ 'label' => 'foo',
 145+ 'default' => true
 146+ ),
 147+ array(
 148+ 'name' => 'testBoolean',
 149+ 'type' => 'string',
 150+ 'label' => 'foo',
 151+ 'default' => 'bar'
 152+ )
 153+ )
 154+ ) ) );
 155+
 156+ //Test with fields encoded as associative array instead of regular array
 157+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 158+ 'fields' => array(
 159+ 'testBoolean' => array(
 160+ 'name' => 'testBoolean',
 161+ 'type' => 'string',
 162+ 'label' => 'foo',
 163+ 'default' => 'bar'
 164+ )
 165+ )
 166+ ) ) );
 167+
 168+ //Test with too long preference name (41 characters)
 169+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 170+ 'fields' => array(
 171+ array(
 172+ 'name' => 'aPreferenceNameExceedingTheLimitOf40Chars',
 173+ 'type' => 'boolean',
 174+ 'label' => 'foo',
 175+ 'default' => true
 176+ )
 177+ )
 178+ ) ) );
 179+
 180+ //This must pass, instead (40 characters is fine)
 181+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( array(
 182+ 'fields' => array(
 183+ array(
 184+ 'name' => 'otherPreferenceNameThatS40CharactersLong',
 185+ 'type' => 'boolean',
 186+ 'label' => 'foo',
 187+ 'default' => true
 188+ )
 189+ )
 190+ ) ) );
 191+
 192+
 193+ //Test with an unexisting field parameter
 194+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( array(
 195+ 'fields' => array(
 196+ array(
 197+ 'name' => 'testBoolean',
 198+ 'type' => 'boolean',
 199+ 'label' => 'foo',
 200+ 'default' => true,
 201+ 'unexistingParamThatMustFail' => 'foo'
 202+ )
 203+ )
 204+ ) ) );
 205+ }
 206+
 207+ //Tests for 'label' type preferences
 208+ function testPrefsDescriptionsLabel() {
 209+ $correct = array(
 210+ 'fields' => array(
 211+ array(
 212+ 'type' => 'label',
 213+ 'label' => 'foo'
 214+ )
 215+ )
 216+ );
 217+
 218+ //Tests with correct values for 'label'
 219+ foreach ( array( '', '@', '@message', 'foo', '@@not message' ) as $def ) {
 220+ $correct['fields'][0]['label'] = $def;
 221+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 222+ }
 223+
 224+ //Tests with wrong values for 'label'
 225+ $wrong = $correct;
 226+ foreach ( array( 0, 1, true, false, null, array() ) as $label ) {
 227+ $wrong['fields'][0]['label'] = $label;
 228+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 229+ }
 230+
 231+ }
 232+
 233+ //Tests for 'boolean' type preferences
 234+ function testPrefsDescriptionsBoolean() {
 235+ $correct = array(
 236+ 'fields' => array(
 237+ array(
 238+ 'name' => 'testBoolean',
 239+ 'type' => 'boolean',
 240+ 'label' => 'some label',
 241+ 'default' => true
 242+ )
 243+ )
 244+ );
 245+
 246+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 247+
 248+ $correct2 = array(
 249+ 'fields' => array(
 250+ array(
 251+ 'name' => 'testBoolean',
 252+ 'type' => 'boolean',
 253+ 'label' => 'some label',
 254+ 'default' => false
 255+ )
 256+ )
 257+ );
 258+
 259+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 260+
 261+ //Tests with wrong default values
 262+ $wrong = $correct;
 263+ foreach ( array( 0, 1, '', 'false', 'true', null, array() ) as $def ) {
 264+ $wrong['fields'][0]['default'] = $def;
 265+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 266+ }
 267+ }
 268+
 269+ //Tests for 'string' type preferences
 270+ function testPrefsDescriptionsString() {
 271+ $correct = array(
 272+ 'fields' => array(
 273+ array(
 274+ 'name' => 'testString',
 275+ 'type' => 'string',
 276+ 'label' => 'some label',
 277+ 'minlength' => 6,
 278+ 'maxlength' => 10,
 279+ 'default' => 'default'
 280+ )
 281+ )
 282+ );
 283+
 284+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 285+
 286+ //Tests with wrong default values (when 'required' is not given)
 287+ $wrong = $correct;
 288+ foreach ( array( null, '', true, false, 0, 1, array(), 'short', 'veryverylongstring' ) as $def ) {
 289+ $wrong['fields'][0]['default'] = $def;
 290+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 291+ }
 292+
 293+ //Tests with correct default values (when required is not given)
 294+ $correct2 = $correct;
 295+ foreach ( array( '6chars', '1234567890' ) as $def ) {
 296+ $correct2['fields'][0]['default'] = $def;
 297+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 298+ }
 299+
 300+ //Tests with wrong default values (when 'required' is false)
 301+ $wrong = $correct;
 302+ $wrong['fields'][0]['required'] = false;
 303+ foreach ( array( null, true, false, 0, 1, array(), 'short', 'veryverylongstring' ) as $def ) {
 304+ $wrong['fields'][0]['default'] = $def;
 305+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 306+ }
 307+
 308+ //Tests with correct default values (when required is false)
 309+ $correct2 = $correct;
 310+ $correct2['fields'][0]['required'] = false;
 311+ foreach ( array( '', '6chars', '1234567890' ) as $def ) {
 312+ $correct2['fields'][0]['default'] = $def;
 313+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 314+ }
 315+
 316+ $correct = array(
 317+ 'fields' => array(
 318+ array(
 319+ 'name' => 'testString',
 320+ 'type' => 'string',
 321+ 'label' => 'some label',
 322+ 'default' => ''
 323+ )
 324+ )
 325+ );
 326+
 327+ //Test with empty default when "required" is true
 328+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 329+
 330+ //Test with empty default when "required" is true
 331+ $wrong = $correct;
 332+ $wrong['fields'][0]['required'] = true;
 333+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 334+
 335+ //Test with empty default when "required" is false and minlength is given
 336+ $correct2 = $correct;
 337+ $correct2['fields'][0]['required'] = false;
 338+ $correct2['fields'][0]['minlength'] = 3;
 339+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 340+ }
 341+
 342+ //Tests for 'number' type preferences
 343+ function testPrefsDescriptionsNumber() {
 344+ $correctFloat = array(
 345+ 'fields' => array(
 346+ array(
 347+ 'name' => 'testNumber',
 348+ 'type' => 'number',
 349+ 'label' => 'some label',
 350+ 'min' => -15,
 351+ 'max' => 36,
 352+ 'required' => true,
 353+ 'default' => 3.14
 354+ )
 355+ )
 356+ );
 357+
 358+ $correctInt = array(
 359+ 'fields' => array(
 360+ array(
 361+ 'name' => 'testNumber',
 362+ 'type' => 'number',
 363+ 'label' => 'some label',
 364+ 'min' => -15,
 365+ 'max' => 36,
 366+ 'integer' => true,
 367+ 'required' => true,
 368+ 'default' => 12
 369+ )
 370+ )
 371+ );
 372+
 373+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correctFloat ) );
 374+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correctInt ) );
 375+
 376+ //Tests with wrong default values (with 'required' = true)
 377+ $wrongFloat = $correctFloat;
 378+ foreach ( array( '', false, true, null, array(), -100, +100 ) as $def ) {
 379+ $wrongFloat['fields'][0]['default'] = $def;
 380+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrongFloat ) );
 381+ }
 382+
 383+ $wrongInt = $correctInt;
 384+ foreach ( array( '', false, true, null, array(), -100, +100, 2.7182818 ) as $def ) {
 385+ $wrongInt['fields'][0]['default'] = $def;
 386+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrongInt ) );
 387+ }
 388+
 389+ //If required=false, default=null must be accepted, too
 390+ foreach ( array( $correctFloat, $correctInt ) as $correct ) {
 391+ $correct['fields'][0]['required'] = false;
 392+ $correct['fields'][0]['default'] = null;
 393+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 394+ }
 395+ }
 396+
 397+ //Tests for 'select' type preferences
 398+ function testPrefsDescriptionsSelect() {
 399+ $correct = array(
 400+ 'fields' => array(
 401+ array(
 402+ 'name' => 'testSelect',
 403+ 'type' => 'select',
 404+ 'label' => 'some label',
 405+ 'default' => 3,
 406+ 'options' => array(
 407+ array( 'name' => 'opt1', 'value' => null ),
 408+ array( 'name' => 'opt2', 'value' => true ),
 409+ array( 'name' => 'opt3', 'value' => 3 ),
 410+ array( 'name' => 'opt4', 'value' => 'test' )
 411+ )
 412+ )
 413+ )
 414+ );
 415+
 416+
 417+ //Tests with correct default values
 418+ $correct2 = $correct;
 419+ foreach ( array( null, true, 3, 'test' ) as $def ) {
 420+ $correct2['fields'][0]['default'] = $def;
 421+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 422+ }
 423+
 424+ //Tests with wrong default values
 425+ $wrong = $correct;
 426+ foreach ( array( '', 'true', 'null', false, array(), 0, 1, 3.0001 ) as $def ) {
 427+ $wrong['fields'][0]['default'] = $def;
 428+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 429+ }
 430+ }
 431+
 432+ //Tests for 'range' type preferences
 433+ function testPrefsDescriptionsRange() {
 434+ $correct = array(
 435+ 'fields' => array(
 436+ array(
 437+ 'name' => 'testRange',
 438+ 'type' => 'range',
 439+ 'label' => 'some label',
 440+ 'default' => 35,
 441+ 'min' => 15,
 442+ 'max' => 45
 443+ )
 444+ )
 445+ );
 446+
 447+ //Tests with correct default values
 448+ $correct2 = $correct;
 449+ foreach ( array( 15, 33, 45 ) as $def ) {
 450+ $correct2['fields'][0]['default'] = $def;
 451+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 452+ }
 453+
 454+ //Tests with wrong default values
 455+ $wrong = $correct;
 456+ foreach ( array( '', true, false, null, array(), '35', 14, 46, 30.2 ) as $def ) {
 457+ $wrong['fields'][0]['default'] = $def;
 458+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 459+ }
 460+
 461+ //Test with max not in the set min + k*step (step not given, so it's 1)
 462+ $wrong = $correct;
 463+ $wrong['fields'][0]['max'] = 45.5;
 464+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 465+
 466+
 467+ //Tests with floating point min, max and step
 468+ $correct = array(
 469+ 'fields' => array(
 470+ array(
 471+ 'name' => 'testRange',
 472+ 'type' => 'range',
 473+ 'label' => 'some label',
 474+ 'default' => 0.20,
 475+ 'min' => -2.8,
 476+ 'max' => 4.2,
 477+ 'step' => 0.25
 478+ )
 479+ )
 480+ );
 481+
 482+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 483+
 484+ //Tests with correct default values
 485+ $correct2 = $correct;
 486+ foreach ( array( -2.8, -2.55, 0.20, 4.2 ) as $def ) {
 487+ $correct2['fields'][0]['default'] = $def;
 488+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 489+ }
 490+
 491+ //Tests with wrong default values
 492+ $wrong = $correct;
 493+ foreach ( array( '', true, false, null, array(), '0.20', -2.7, 0, 4.199999 ) as $def ) {
 494+ $wrong['fields'][0]['default'] = $def;
 495+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 496+ }
 497+ }
 498+
 499+ //Tests for 'date' type preferences
 500+ function testPrefsDescriptionsDate() {
 501+ $correct = array(
 502+ 'fields' => array(
 503+ array(
 504+ 'name' => 'testDate',
 505+ 'type' => 'date',
 506+ 'label' => 'some label',
 507+ 'default' => null
 508+ )
 509+ )
 510+ );
 511+
 512+ //Tests with correct default values
 513+ $correct2 = $correct;
 514+ foreach ( array(
 515+ null,
 516+ '2011-07-05T15:00:00Z',
 517+ '2011-01-01T00:00:00Z',
 518+ '2011-12-31T23:59:59Z',
 519+ ) as $def )
 520+ {
 521+ $correct2['fields'][0]['default'] = $def;
 522+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 523+ }
 524+
 525+ //Tests with wrong default values
 526+ $wrong = $correct;
 527+ foreach ( array(
 528+ '', true, false, array(), 0,
 529+ '2011-07-05T15:00:00',
 530+ '2011-07-05T15:00:61Z',
 531+ '2011-07-05T15:61:00Z',
 532+ '2011-07-05T25:00:00Z',
 533+ '2011-07-32T15:00:00Z',
 534+ '2011-07-5T15:00:00Z',
 535+ '2011-7-05T15:00:00Z',
 536+ '2011-13-05T15:00:00Z',
 537+ '2011-07-05T15:00-00Z',
 538+ '2011-07-05T15-00:00Z',
 539+ '2011-07-05S15:00:00Z',
 540+ '2011-07:05T15:00:00Z',
 541+ '2011:07-05T15:00:00Z'
 542+ ) as $def )
 543+ {
 544+ $wrong['fields'][0]['default'] = $def;
 545+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 546+ }
 547+ }
 548+
 549+ //Tests for 'color' type preferences
 550+ function testPrefsDescriptionsColor() {
 551+ $correct = array(
 552+ 'fields' => array(
 553+ array(
 554+ 'name' => 'testColor',
 555+ 'type' => 'color',
 556+ 'label' => 'some label',
 557+ 'default' => '#123456'
 558+ )
 559+ )
 560+ );
 561+
 562+ //Tests with correct default values
 563+ $correct2 = $correct;
 564+ foreach ( array(
 565+ '#000000',
 566+ '#ffffff',
 567+ '#8ed36e',
 568+ ) as $def )
 569+ {
 570+ $correct2['fields'][0]['default'] = $def;
 571+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 572+ }
 573+
 574+ //Tests with wrong default values
 575+ $wrong = $correct;
 576+ foreach ( array(
 577+ '', true, false, null, 0, array(),
 578+ '123456',
 579+ '#629af',
 580+ '##123456',
 581+ '#1aefdq',
 582+ '#145aeF', //uppercase letters not allowed
 583+ '#179', //syntax not allowed
 584+ 'red', //syntax not allowed
 585+ ) as $def )
 586+ {
 587+ $wrong['fields'][0]['default'] = $def;
 588+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 589+ }
 590+ }
 591+
 592+ //Tests for 'composite' type fields
 593+ function testPrefsDescriptionsComposite() {
 594+ $correct = array(
 595+ 'fields' => array(
 596+ array(
 597+ 'name' => 'foo',
 598+ 'type' => 'composite',
 599+ 'fields' => array(
 600+ array(
 601+ 'name' => 'bar',
 602+ 'type' => 'boolean',
 603+ 'label' => '@msg1',
 604+ 'default' => true
 605+ ),
 606+ array(
 607+ 'name' => 'car',
 608+ 'type' => 'color',
 609+ 'label' => '@msg2',
 610+ 'default' => '#123456'
 611+ )
 612+ )
 613+ )
 614+ )
 615+ );
 616+
 617+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 618+ $this->assertEquals(
 619+ GadgetPrefs::getDefaults( $correct ),
 620+ array( 'foo' => array( 'bar' => true, 'car' => '#123456' ) )
 621+ );
 622+ $this->assertEquals( GadgetPrefs::getMessages( $correct ), array( 'msg1', 'msg2' ) );
 623+
 624+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 625+ $correct,
 626+ array( 'foo' => array( 'bar' => false, 'car' => '#00aaff' ) )
 627+ ) );
 628+
 629+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 630+ $correct,
 631+ array( 'foo' => array( 'bar' => null, 'car' => '#00aaff' ) )
 632+ ) );
 633+
 634+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 635+ $correct,
 636+ array( 'foo' => array( 'bar' => false, 'car' => '#00aafz' ) )
 637+ ) );
 638+
 639+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 640+ $correct,
 641+ array( 'bar' => false, 'car' => '#00aaff' )
 642+ ) );
 643+
 644+ $prefs = array(
 645+ 'foo' => array(
 646+ 'bar' => false,
 647+ 'car' => null //wrong
 648+ )
 649+ );
 650+
 651+ GadgetPrefs::matchPrefsWithDescription( $correct, $prefs );
 652+ //Check if only the wrong subfield has been reset to default value
 653+ $this->assertEquals( $prefs, array( 'foo' => array( 'bar' => false, 'car' => '#123456' ) ) );
 654+ }
 655+
 656+ //Tests for 'list' type fields
 657+ function testPrefsDescriptionsList() {
 658+ $correct = array(
 659+ 'fields' => array(
 660+ array(
 661+ 'name' => 'foo',
 662+ 'type' => 'list',
 663+ 'default' => array(),
 664+ 'field' => array(
 665+ 'type' => 'composite',
 666+ 'fields' => array(
 667+ array(
 668+ 'name' => 'bar',
 669+ 'type' => 'boolean',
 670+ 'label' => '@msg1',
 671+ 'default' => true
 672+ ),
 673+ array(
 674+ 'name' => 'car',
 675+ 'type' => 'color',
 676+ 'label' => '@msg2',
 677+ 'default' => '#123456'
 678+ )
 679+ )
 680+ )
 681+ )
 682+ )
 683+ );
 684+
 685+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) );
 686+
 687+ //Specifying the 'name' member for field must fail
 688+ $wrong = $correct;
 689+ $wrong['fields'][0]['field']['name'] = 'composite';
 690+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 691+
 692+
 693+ $this->assertEquals(
 694+ GadgetPrefs::getDefaults( $correct ),
 695+ array( 'foo' => array() )
 696+ );
 697+
 698+ $this->assertEquals( GadgetPrefs::getMessages( $correct ), array( 'msg1', 'msg2' ) );
 699+
 700+ //Tests with correct pref values
 701+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 702+ $correct,
 703+ array( 'foo' => array() )
 704+ ) );
 705+
 706+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 707+ $correct,
 708+ array( 'foo' => array(
 709+ array(
 710+ 'bar' => true,
 711+ 'car' => '#115599'
 712+ ),
 713+ array(
 714+ 'bar' => false,
 715+ 'car' => '#123456'
 716+ ),
 717+ array(
 718+ 'bar' => true,
 719+ 'car' => '#ffffff'
 720+ )
 721+ )
 722+ )
 723+ ) );
 724+
 725+ //Tests with wrong pref values
 726+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 727+ $correct,
 728+ array( 'foo' => array(
 729+ array(
 730+ 'bar' => null, //wrong
 731+ 'car' => '#115599'
 732+ )
 733+ )
 734+ )
 735+ ) );
 736+
 737+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 738+ $correct,
 739+ array( 'foo' => array( //wrong, not enclosed in array
 740+ 'bar' => null,
 741+ 'car' => '#115599'
 742+ )
 743+ )
 744+ ) );
 745+
 746+
 747+ //Tests with 'minlength' and 'maxlength' options
 748+ $wrong = $correct;
 749+ $wrong['fields'][0]['minlength'] = 4;
 750+ $wrong['fields'][0]['maxlength'] = 3; //maxlength < minlength, wrong
 751+ $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) );
 752+
 753+ $correct2 = $correct;
 754+ $correct2['fields'][0]['minlength'] = 2;
 755+ $correct2['fields'][0]['maxlength'] = 3;
 756+ $correct2['fields'][0]['default'] = array(
 757+ array( 'bar' => true, 'car' => '#115599' ),
 758+ array( 'bar' => false, 'car' => '#123456' )
 759+ );
 760+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct2 ) );
 761+
 762+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 763+ $correct2,
 764+ array( 'foo' => array( //less than minlength items
 765+ array( 'bar' => true, 'car' => '#115599' )
 766+ )
 767+ )
 768+ ) );
 769+
 770+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 771+ $correct2,
 772+ array( 'foo' => array() ) //empty array, must fail because "required" is not false
 773+ ) );
 774+
 775+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription(
 776+ $correct2,
 777+ array( 'foo' => array( //more than minlength items
 778+ array( 'bar' => true, 'car' => '#115599' ),
 779+ array( 'bar' => false, 'car' => '#123456' ),
 780+ array( 'bar' => true, 'car' => '#ffffff' ),
 781+ array( 'bar' => false, 'car' => '#2357bd' )
 782+ )
 783+ )
 784+ ) );
 785+
 786+ //Test with 'required'
 787+ $correct2['fields'][0]['required'] = false;
 788+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription(
 789+ $correct2,
 790+ array( 'foo' => array() ) //empty array, must be accepted because "required" is false
 791+ ) );
 792+
 793+ //Tests matchPrefsWithDescription
 794+ $prefs = array( 'foo' => array(
 795+ array(
 796+ 'bar' => null,
 797+ 'car' => '#115599'
 798+ ),
 799+ array(
 800+ 'bar' => false,
 801+ 'car' => ''
 802+ ),
 803+ array(
 804+ 'bar' => true,
 805+ 'car' => '#ffffff'
 806+ )
 807+ )
 808+ );
 809+
 810+
 811+ GadgetPrefs::matchPrefsWithDescription( $correct, $prefs );
 812+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( $correct, $prefs ) );
 813+ }
 814+
 815+ //Data provider to be able to reuse a complex preference description for several tests.
 816+ function prefsDescProvider() {
 817+ return array( array(
 818+ array(
 819+ 'fields' => array(
 820+ array(
 821+ 'type' => 'bundle',
 822+ 'sections' => array(
 823+ array(
 824+ 'title' => '@section1',
 825+ 'fields' => array (
 826+ array(
 827+ 'name' => 'testBoolean',
 828+ 'type' => 'boolean',
 829+ 'label' => '@foo',
 830+ 'default' => true
 831+ ),
 832+ array(
 833+ 'name' => 'testBoolean2',
 834+ 'type' => 'boolean',
 835+ 'label' => '@@foo2',
 836+ 'default' => true
 837+ ),
 838+ array(
 839+ 'name' => 'testNumber',
 840+ 'type' => 'number',
 841+ 'label' => '@foo3',
 842+ 'min' => 2.3,
 843+ 'max' => 13.94,
 844+ 'default' => 7
 845+ )
 846+ )
 847+ ),
 848+ array(
 849+ 'title' => 'Section2',
 850+ 'fields' => array(
 851+ array(
 852+ 'name' => 'testNumber2',
 853+ 'type' => 'number',
 854+ 'label' => 'foo4',
 855+ 'min' => 2.3,
 856+ 'max' => 13.94,
 857+ 'default' => 7
 858+ ),
 859+ array(
 860+ 'name' => 'testSelect',
 861+ 'type' => 'select',
 862+ 'label' => 'foo',
 863+ 'default' => 3,
 864+ 'options' => array(
 865+ array( 'name' => '@opt1', 'value' => null ),
 866+ array( 'name' => '@opt2', 'value' => true ),
 867+ array( 'name' => 'opt3', 'value' => 3 ),
 868+ array( 'name' => '@opt4', 'value' => 'opt4value' )
 869+ )
 870+ ),
 871+ array(
 872+ 'name' => 'testSelect2',
 873+ 'type' => 'select',
 874+ 'label' => 'foo',
 875+ 'default' => 3,
 876+ 'options' => array(
 877+ array( 'name' => '@opt1', 'value' => null ),
 878+ array( 'name' => 'opt2', 'value' => true ),
 879+ array( 'name' => 'opt3', 'value' => 3 ),
 880+ array( 'name' => 'opt4', 'value' => 'opt4value' )
 881+ )
 882+ )
 883+ )
 884+ )
 885+ )
 886+ )
 887+ )
 888+ )
 889+ ) );
 890+ }
 891+
 892+ /**
 893+ * Tests Gadget::getDefaults
 894+ *
 895+ * @dataProvider prefsDescProvider
 896+ */
 897+ function testGetDefaults( $prefsDescription ) {
 898+ $this->assertEquals( GadgetPrefs::getDefaults( $prefsDescription ), array(
 899+ 'testBoolean' => true,
 900+ 'testBoolean2' => true,
 901+ 'testNumber' => 7,
 902+ 'testNumber2' => 7,
 903+ 'testSelect' => 3,
 904+ 'testSelect2' => 3
 905+ ) );
 906+ }
 907+
 908+ /**
 909+ * Tests Gadget::setPrefsDescription, GadgetPrefs::checkPrefsAgainstDescription,
 910+ * GadgetPrefs::matchPrefsWithDescription and Gadget::setPrefs.
 911+ *
 912+ * @dataProvider prefsDescProvider
 913+ */
 914+ function testSetPrefs( $prefsDescription ) {
 915+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $prefsDescription ) );
 916+
 917+ $prefs = array(
 918+ 'testBoolean' => false,
 919+ 'testBoolean2' => null, //wrong
 920+ 'testNumber' => 11,
 921+ 'testNumber2' => 45, //wrong
 922+ 'testSelect' => true,
 923+ 'testSelect2' => false //wrong
 924+ );
 925+
 926+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription( $prefsDescription, $prefs ) );
 927+
 928+ $prefs2 = $prefs;
 929+ GadgetPrefs::matchPrefsWithDescription( $prefsDescription, $prefs2 );
 930+ //Now $prefs2 should pass validation
 931+ $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( $prefsDescription, $prefs2 ) );
 932+
 933+ //$prefs2 should have testBoolean, testNumber and testSelect unchanged, the other reset to defaults
 934+ $this->assertEquals( $prefs2['testBoolean'], $prefs['testBoolean'] );
 935+ $this->assertEquals( $prefs2['testNumber'], $prefs['testNumber'] );
 936+ $this->assertEquals( $prefs2['testSelect'], $prefs['testSelect'] );
 937+
 938+ $defaults = GadgetPrefs::getDefaults( $prefsDescription );
 939+ $this->assertEquals( $prefs2['testBoolean2'], $defaults['testBoolean2'] );
 940+ $this->assertEquals( $prefs2['testNumber2'], $defaults['testNumber2'] );
 941+ $this->assertEquals( $prefs2['testSelect2'], $defaults['testSelect2'] );
 942+
 943+ $g = $this->create( '*foo[ResourceLoader]| foo.css|foo.js|foo.bar' ); //FIXME
 944+ $g->setPrefsDescription( $prefsDescription );
 945+ $this->assertTrue( $g->getPrefsDescription() !== null );
 946+
 947+ //Setting wrong preferences must fail
 948+ $this->assertFalse( $g->setPrefs( $prefs ) );
 949+
 950+ //Setting right preferences must succeed
 951+ $this->assertTrue( $g->setPrefs( $prefs2 ) );
 952+
 953+ //Adding a field not in the description must fail
 954+ $prefs2['someUnexistingPreference'] = 'bar';
 955+ $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription( $prefsDescription, $prefs2 ) );
 956+ }
 957+
 958+ /**
 959+ * @expectedException MWException
 960+ */
 961+ function testSetPrefsWithWrongParam() {
 962+ $g = $this->create( '*foo[ResourceLoader]| foo.css|foo.js|foo.bar' ); //FIXME
 963+ $g->setPrefsDescription( array(
 964+ 'fields' => array(
 965+ 'testBoolean' => array(
 966+ 'type' => 'boolean',
 967+ 'label' => 'foo',
 968+ 'default' => true
 969+ )
 970+ )
 971+ ) );
 972+
 973+ //Call with invalid param
 974+ $g->setPrefs( 'wrongparam' );
 975+ }
 976+
 977+
 978+ /**
 979+ * Tests GadgetPrefs::simplifyPrefs.
 980+ */
 981+ function testSimplifyPrefs() {
 982+ $prefsDescription = array(
 983+ 'fields' => array(
 984+ array(
 985+ 'type' => 'boolean',
 986+ 'name' => 'foo',
 987+ 'label' => 'some label',
 988+ 'default' => true
 989+ ),
 990+ array(
 991+ 'type' => 'bundle',
 992+ 'sections' => array(
 993+ array(
 994+ 'name' => 'Section 1',
 995+ 'fields' => array(
 996+ array(
 997+ 'type' => 'boolean',
 998+ 'name' => 'bar',
 999+ 'label' => 'dummy label',
 1000+ 'default' => false
 1001+ ),
 1002+ )
 1003+ ),
 1004+ array(
 1005+ 'name' => 'Section 2',
 1006+ 'fields' => array(
 1007+ array(
 1008+ 'type' => 'string',
 1009+ 'name' => 'baz',
 1010+ 'label' => 'A string',
 1011+ 'default' => 'qwerty'
 1012+ )
 1013+ )
 1014+ )
 1015+ )
 1016+ ),
 1017+ array(
 1018+ 'type' => 'composite',
 1019+ 'name' => 'cmp',
 1020+ 'fields' => array(
 1021+ array(
 1022+ 'type' => 'number',
 1023+ 'name' => 'aNumber',
 1024+ 'label' => 'A number',
 1025+ 'default' => 3.14
 1026+ ),
 1027+ array(
 1028+ 'type' => 'color',
 1029+ 'name' => 'aColor',
 1030+ 'label' => 'A color',
 1031+ 'default' => '#a023e2'
 1032+ )
 1033+ )
 1034+ ),
 1035+ array(
 1036+ 'type' => 'list',
 1037+ 'name' => 'aList',
 1038+ 'default' => array( 2, 3, 5, 7 ),
 1039+ 'field' => array(
 1040+ 'type' => 'range',
 1041+ 'label' => 'A range',
 1042+ 'min' => 0,
 1043+ 'max' => 256,
 1044+ 'default' => 128
 1045+ )
 1046+ )
 1047+ )
 1048+ );
 1049+
 1050+ $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $prefsDescription ) );
 1051+
 1052+ $prefs = array(
 1053+ 'foo' => true, //=default
 1054+ 'bar' => true,
 1055+ 'baz' => 'asdfgh',
 1056+ 'cmp' => array(
 1057+ 'aNumber' => 2.81,
 1058+ 'aColor' => '#a023e2' //=default
 1059+ ),
 1060+ 'aList' => array( 2, 3, 5, 9 )
 1061+ );
 1062+
 1063+ GadgetPrefs::simplifyPrefs( $prefsDescription, $prefs );
 1064+ $this->assertEquals(
 1065+ $prefs,
 1066+ array(
 1067+ 'bar' => true,
 1068+ 'baz' => 'asdfgh',
 1069+ 'cmp' => array(
 1070+ 'aNumber' => 2.81,
 1071+ ),
 1072+ 'aList' => array( 2, 3, 5, 9 )
 1073+ )
 1074+ );
 1075+
 1076+
 1077+ $prefs = array(
 1078+ 'foo' => false,
 1079+ 'bar' => false, //=default
 1080+ 'baz' => 'asdfgh',
 1081+ 'cmp' => array(
 1082+ 'aNumber' => 3.14, //=default
 1083+ 'aColor' => '#a023e2' //=default
 1084+ ),
 1085+ 'aList' => array( 2, 3, 5, 7 ) //=default
 1086+ );
 1087+ GadgetPrefs::simplifyPrefs( $prefsDescription, $prefs );
 1088+ $this->assertEquals(
 1089+ $prefs,
 1090+ array(
 1091+ 'foo' => false,
 1092+ 'baz' => 'asdfgh'
 1093+ )
 1094+ );
 1095+ }
 1096+
 1097+ /**
 1098+ * Tests GadgetPrefs::getMessages.
 1099+ *
 1100+ * @dataProvider prefsDescProvider
 1101+ */
 1102+ function testGetMessages( $prefsDescription ) {
 1103+ $msgs = GadgetPrefs::getMessages( $prefsDescription );
 1104+ sort( $msgs );
 1105+ $this->assertEquals( $msgs, array(
 1106+ 'foo', 'foo3', 'opt1', 'opt2', 'opt4', 'section1'
 1107+ ) );
 1108+ }
831109 }
Index: branches/RL2/extensions/Gadgets/Gadgets.php
@@ -123,12 +123,16 @@
124124 $wgAutoloadClasses['ApiQueryGadgetCategories'] = $dir . 'api/ApiQueryGadgetCategories.php';
125125 $wgAutoloadClasses['ApiQueryGadgetPages'] = $dir . 'api/ApiQueryGadgetPages.php';
126126 $wgAutoloadClasses['ApiQueryGadgets'] = $dir . 'api/ApiQueryGadgets.php';
 127+$wgAutoloadClasses['ApiGetGadgetPrefs'] = $dir . 'api/ApiGetGadgetPrefs.php';
 128+$wgAutoloadClasses['ApiSetGadgetPrefs'] = $dir . 'api/ApiSetGadgetPrefs.php';
127129 $wgAutoloadClasses['ForeignDBGadgetRepo'] = $dir . 'backend/ForeignDBGadgetRepo.php';
128130 $wgAutoloadClasses['Gadget'] = $dir . 'backend/Gadget.php';
129131 $wgAutoloadClasses['GadgetsHooks'] = $dir . 'Gadgets.hooks.php';
130132 $wgAutoloadClasses['GadgetPageList'] = $dir . 'backend/GadgetPageList.php';
131133 $wgAutoloadClasses['GadgetRepo'] = $dir . 'backend/GadgetRepo.php';
132134 $wgAutoloadClasses['GadgetResourceLoaderModule'] = $dir . 'backend/GadgetResourceLoaderModule.php';
 135+$wgAutoloadClasses['GadgetOptionsResourceLoaderModule'] = $dir . 'backend/GadgetOptionsResourceLoaderModule.php';
 136+$wgAutoloadClasses['GadgetPrefs'] = $dir . 'backend/GadgetPrefs.php';
133137 $wgAutoloadClasses['LocalGadgetRepo'] = $dir . 'backend/LocalGadgetRepo.php';
134138 $wgAutoloadClasses['SpecialGadgets'] = $dir . 'SpecialGadgets.php';
135139
@@ -139,6 +143,9 @@
140144 $wgAPIListModules['gadgets'] = 'ApiQueryGadgets';
141145 $wgAPIListModules['gadgetpages'] = 'ApiQueryGadgetPages';
142146
 147+$wgAPIModules['setgadgetprefs'] = 'ApiSetGadgetPrefs';
 148+$wgAPIModules['getgadgetprefs'] = 'ApiGetGadgetPrefs';
 149+
143150 $gadResourceTemplate = array(
144151 'localBasePath' => $dir . 'modules',
145152 'remoteExtPath' => 'Gadgets/modules'
@@ -228,4 +235,25 @@
229236 'ext.gadgets.preferences' => $gadResourceTemplate + array(
230237 'scripts' => 'ext.gadgets.preferences.js',
231238 ),
 239+ 'jquery.formBuilder' => $gadResourceTemplate + array(
 240+ 'scripts' => array( 'jquery.formBuilder.js' ),
 241+ 'styles' => array( 'jquery.formBuilder.css' ),
 242+ 'dependencies' => array(
 243+ // TODO load some of this stuff on-demand
 244+ 'jquery.ui.slider', 'jquery.ui.datepicker', 'jquery.ui.position',
 245+ 'jquery.ui.draggable', 'jquery.ui.droppable', 'jquery.ui.sortable', 'jquery.ui.dialog',
 246+ 'jquery.ui.tabs', 'jquery.farbtastic', 'jquery.colorUtil', 'jquery.validate'
 247+ ),
 248+ 'messages' => array(
 249+ 'gadgets-formbuilder-required', 'gadgets-formbuilder-minlength', 'gadgets-formbuilder-maxlength',
 250+ 'gadgets-formbuilder-min', 'gadgets-formbuilder-max', 'gadgets-formbuilder-integer', 'gadgets-formbuilder-date',
 251+ 'gadgets-formbuilder-color', 'gadgets-formbuilder-list-required', 'gadgets-formbuilder-list-minlength',
 252+ 'gadgets-formbuilder-list-maxlength', 'gadgets-formbuilder-scalar',
 253+ 'gadgets-formbuilder-editor-ok', 'gadgets-formbuilder-editor-cancel', 'gadgets-formbuilder-editor-move-field',
 254+ 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-edit-field-title', 'gadgets-formbuilder-editor-insert-field',
 255+ 'gadgets-formbuilder-editor-choose-field', 'gadgets-formbuilder-editor-choose-field-title', 'gadgets-formbuilder-editor-create-field-title',
 256+ 'gadgets-formbuilder-editor-duplicate-name', 'gadgets-formbuilder-editor-delete-section', 'gadgets-formbuilder-editor-new-section',
 257+ 'gadgets-formbuilder-editor-edit-section', 'gadgets-formbuilder-editor-choose-title', 'gadgets-formbuilder-editor-choose-title-title'
 258+ ),
 259+ ),
232260 );
Index: branches/RL2/extensions/Gadgets/backend/GadgetPrefs.php
@@ -0,0 +1,1150 @@
 2+<?php
 3+// TODO: This entire file needs to be reviewed
 4+
 5+/**
 6+ * Static methods for gadget preferences parsing, validation and so on.
 7+ *
 8+ * @author Salvatore Ingala
 9+ * @license GNU General Public Licence 2.0 or later
 10+ *
 11+ */
 12+
 13+class GadgetPrefs {
 14+
 15+ /**
 16+ * Syntax specifications of preference description language.
 17+ * Each element describes a field; a "simple" field encodes exactly one gadget preference, but some fields
 18+ * may encode for 0 or multiple gadget preferences.
 19+ * "Simple" field always have the 'name' member. Not "simple" fields never do.
 20+ * Each field has a 'description' and may have a 'validator', a 'flattener', and a 'checker'.
 21+ * - 'description' is an array that describes all the members of that fields. Each member description has this shape:
 22+ * - 'isMandatory' is a boolean that specifies if that member is mandatory for the field;
 23+ * - 'validator', if specified, is the name of a function that validates that member
 24+ * - 'validator' is an optional function that does validation of the entire field description,
 25+ * when member validators does not suffice since more complex semantics are needed.
 26+ * - 'flattener' is an optional function that takes a valid field description and returns an array of specification of
 27+ * gadget preferences, with preference names as keys and corresponding "simple" field descriptions as values.
 28+ * If omitted (for "simple fields"), the default flattener is used.
 29+ * - 'checker', only for "simple" fields, is the name of a function that takes a preference description and
 30+ * a preference value, and returns true if that value passes validation, false otherwise.
 31+ * - 'matcher', only for "simple" fields, is the name of a function that takes the description of a field $prefDescription,
 32+ * an array of preference values $prefs and the name of a preference $preferenceName and returns an array where
 33+ * $prefs[$prefName] is changed in a way that passes validation. If omitted, the default action is to set $prefs[$prefName]
 34+ * to $prefDescription['default'].
 35+ * - 'simplifier', only for "simple" fields, is an optional function that takes two arguments, a valid description
 36+ * of a field $prefDescription, and an array of preference values $prefs; it returns an array where the preference
 37+ * encoded by $prefDescription is removed if it is equal to default. If omitted, the preference is omitted if it
 38+ * equals $prefDescription['default'].
 39+ * - 'getDefault', only for "simple" fields, is a function that takes one argument, the description of the field, and
 40+ * returns its default value; if omitted, the value of the 'default' field is returned.
 41+ * - 'getMessages', if specified, is the name of a function that takes a valid description of a field and returns
 42+ * a list of messages referred to by it. If omitted, only the "label" field is returned (if it is a message).
 43+ */
 44+ private static $prefsDescriptionSpecifications = array(
 45+ 'label' => array(
 46+ 'description' => array(
 47+ 'label' => array(
 48+ 'isMandatory' => true,
 49+ 'validator' => 'is_string'
 50+ )
 51+ ),
 52+ 'flattener' => 'GadgetPrefs::flattenLabelDefinition'
 53+ ),
 54+ 'boolean' => array(
 55+ 'description' => array(
 56+ 'name' => array(
 57+ 'isMandatory' => true,
 58+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 59+ ),
 60+ 'default' => array(
 61+ 'isMandatory' => true,
 62+ 'validator' => 'is_bool'
 63+ ),
 64+ 'label' => array(
 65+ 'isMandatory' => true,
 66+ 'validator' => 'is_string'
 67+ )
 68+ ),
 69+ 'checker' => 'GadgetPrefs::checkBooleanPref'
 70+ ),
 71+ 'string' => array(
 72+ 'description' => array(
 73+ 'name' => array(
 74+ 'isMandatory' => true,
 75+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 76+ ),
 77+ 'default' => array(
 78+ 'isMandatory' => true,
 79+ 'validator' => 'is_string'
 80+ ),
 81+ 'label' => array(
 82+ 'isMandatory' => true,
 83+ 'validator' => 'is_string'
 84+ ),
 85+ 'required' => array(
 86+ 'isMandatory' => false,
 87+ 'validator' => 'is_bool'
 88+ ),
 89+ 'minlength' => array(
 90+ 'isMandatory' => false,
 91+ 'validator' => 'is_integer'
 92+ ),
 93+ 'maxlength' => array(
 94+ 'isMandatory' => false,
 95+ 'validator' => 'is_integer'
 96+ )
 97+ ),
 98+ 'validator' => 'GadgetPrefs::validateStringPrefDefinition',
 99+ 'checker' => 'GadgetPrefs::checkStringPref'
 100+ ),
 101+ 'number' => array(
 102+ 'description' => array(
 103+ 'name' => array(
 104+ 'isMandatory' => true,
 105+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 106+ ),
 107+ 'default' => array(
 108+ 'isMandatory' => true,
 109+ 'validator' => 'GadgetPrefs::isFloatOrIntOrNull'
 110+ ),
 111+ 'label' => array(
 112+ 'isMandatory' => true,
 113+ 'validator' => 'is_string'
 114+ ),
 115+ 'required' => array(
 116+ 'isMandatory' => false,
 117+ 'validator' => 'is_bool'
 118+ ),
 119+ 'integer' => array(
 120+ 'isMandatory' => false,
 121+ 'validator' => 'is_bool'
 122+ ),
 123+ 'min' => array(
 124+ 'isMandatory' => false,
 125+ 'validator' => 'GadgetPrefs::isFloatOrInt'
 126+ ),
 127+ 'max' => array(
 128+ 'isMandatory' => false,
 129+ 'validator' => 'GadgetPrefs::isFloatOrInt'
 130+ )
 131+ ),
 132+ 'validator' => 'GadgetPrefs::validateNumberPrefDefinition',
 133+ 'checker' => 'GadgetPrefs::checkNumberPref'
 134+ ),
 135+ 'select' => array(
 136+ 'description' => array(
 137+ 'name' => array(
 138+ 'isMandatory' => true,
 139+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 140+ ),
 141+ 'default' => array(
 142+ 'isMandatory' => true
 143+ ),
 144+ 'label' => array(
 145+ 'isMandatory' => true,
 146+ 'validator' => 'is_string'
 147+ ),
 148+ 'options' => array(
 149+ 'isMandatory' => true,
 150+ 'validator' => 'GadgetPrefs::isOrdinaryArray'
 151+ )
 152+ ),
 153+ 'validator' => 'GadgetPrefs::validateSelectPrefDefinition',
 154+ 'checker' => 'GadgetPrefs::checkSelectPref',
 155+ 'getMessages' => 'GadgetPrefs::getSelectMessages'
 156+ ),
 157+ 'range' => array(
 158+ 'description' => array(
 159+ 'name' => array(
 160+ 'isMandatory' => true,
 161+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 162+ ),
 163+ 'default' => array(
 164+ 'isMandatory' => true,
 165+ 'validator' => 'GadgetPrefs::isFloatOrIntOrNull'
 166+ ),
 167+ 'label' => array(
 168+ 'isMandatory' => true,
 169+ 'validator' => 'is_string'
 170+ ),
 171+ 'min' => array(
 172+ 'isMandatory' => true,
 173+ 'validator' => 'GadgetPrefs::isFloatOrInt'
 174+ ),
 175+ 'max' => array(
 176+ 'isMandatory' => true,
 177+ 'validator' => 'GadgetPrefs::isFloatOrInt'
 178+ ),
 179+ 'step' => array(
 180+ 'isMandatory' => false,
 181+ 'validator' => 'GadgetPrefs::isFloatOrInt'
 182+ )
 183+ ),
 184+ 'validator' => 'GadgetPrefs::validateRangePrefDefinition',
 185+ 'checker' => 'GadgetPrefs::checkRangePref'
 186+ ),
 187+ 'date' => array(
 188+ 'description' => array(
 189+ 'name' => array(
 190+ 'isMandatory' => true,
 191+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 192+ ),
 193+ 'default' => array(
 194+ 'isMandatory' => true
 195+ ),
 196+ 'label' => array(
 197+ 'isMandatory' => true,
 198+ 'validator' => 'is_string'
 199+ )
 200+ ),
 201+ 'checker' => 'GadgetPrefs::checkDatePref'
 202+ ),
 203+ 'color' => array(
 204+ 'description' => array(
 205+ 'name' => array(
 206+ 'isMandatory' => true,
 207+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 208+ ),
 209+ 'default' => array(
 210+ 'isMandatory' => true
 211+ ),
 212+ 'label' => array(
 213+ 'isMandatory' => true,
 214+ 'validator' => 'is_string'
 215+ )
 216+ ),
 217+ 'checker' => 'GadgetPrefs::checkColorPref'
 218+ ),
 219+ 'bundle' => array(
 220+ 'description' => array(
 221+ 'sections' => array(
 222+ 'isMandatory' => true,
 223+ 'checker' => 'GadgetPrefs::validateBundleSectionsDefinition'
 224+ )
 225+ ),
 226+ 'getMessages' => 'GadgetPrefs::getBundleMessages',
 227+ 'flattener' => 'GadgetPrefs::flattenBundleDefinition'
 228+ ),
 229+ 'composite' => array(
 230+ 'description' => array(
 231+ 'name' => array(
 232+ 'isMandatory' => true,
 233+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 234+ ),
 235+ 'fields' => array(
 236+ 'isMandatory' => true,
 237+ 'validator' => 'is_array'
 238+ )
 239+ ),
 240+ 'validator' => 'GadgetPrefs::validateSectionDefinition',
 241+ 'getMessages' => 'GadgetPrefs::getCompositeMessages',
 242+ 'getDefault' => 'GadgetPrefs::getCompositeDefault',
 243+ 'checker' => 'GadgetPrefs::checkCompositePref',
 244+ 'matcher' => 'GadgetPrefs::matchCompositePref',
 245+ 'simplifier' => 'GadgetPrefs::simplifyCompositePref'
 246+ ),
 247+ 'list' => array(
 248+ 'description' => array(
 249+ 'name' => array(
 250+ 'isMandatory' => true,
 251+ 'validator' => 'GadgetPrefs::isValidPreferenceName'
 252+ ),
 253+ 'field' => array(
 254+ 'isMandatory' => true,
 255+ 'validator' => 'is_array'
 256+ ),
 257+ 'default' => array(
 258+ 'isMandatory' => true
 259+ ),
 260+ 'required' => array(
 261+ 'isMandatory' => false,
 262+ 'validator' => 'is_bool'
 263+ ),
 264+ 'minlength' => array(
 265+ 'isMandatory' => false,
 266+ 'validator' => 'is_integer'
 267+ ),
 268+ 'maxlength' => array(
 269+ 'isMandatory' => false,
 270+ 'validator' => 'is_integer'
 271+ )
 272+ ),
 273+ 'validator' => 'GadgetPrefs::validateListPrefDefinition',
 274+ 'getMessages' => 'GadgetPrefs::getListMessages',
 275+ 'checker' => 'GadgetPrefs::checkListPref',
 276+ 'matcher' => 'GadgetPrefs::matchListPref'
 277+ )
 278+ );
 279+
 280+ private static function isValidPreferenceName( $name ) {
 281+ return strlen( $name ) <= 40
 282+ && preg_match( '/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name );
 283+ }
 284+
 285+
 286+ /**
 287+ * Further checks for 'string' options
 288+ */
 289+ private static function validateStringPrefDefinition( $prefDefinition ) {
 290+ if ( isset( $prefDefinition['minlength'] ) && $prefDefinition['minlength'] < 0 ) {
 291+ return false;
 292+ }
 293+
 294+ if ( isset( $prefDefinition['maxlength'] ) && $prefDefinition['maxlength'] <= 0 ) {
 295+ return false;
 296+ }
 297+
 298+ if ( isset( $prefDefinition['minlength']) && isset( $prefDefinition['maxlength'] ) ) {
 299+ if ( $prefDefinition['minlength'] > $prefDefinition['maxlength'] ) {
 300+ return false;
 301+ }
 302+ }
 303+
 304+ return true;
 305+ }
 306+
 307+ private static function isFloatOrInt( $param ) {
 308+ return is_float( $param ) || is_int( $param );
 309+ }
 310+
 311+ private static function isFloatOrIntOrNull( $param ) {
 312+ return is_float( $param ) || is_int( $param ) || $param === null;
 313+ }
 314+
 315+ /**
 316+ * Checks if $param is an ordinary (i.e.: not associative) array
 317+ */
 318+ private static function isOrdinaryArray( $param ) {
 319+ if ( !is_array( $param ) ) {
 320+ return false;
 321+ }
 322+
 323+ $count = count( $param );
 324+ return $count == 0 || array_keys( $param ) === range( 0, $count - 1 );
 325+ }
 326+
 327+ private static function flattenLabelDefinition( $fieldDescription ) {
 328+ return array();
 329+ }
 330+
 331+ /**
 332+ * Default flattener for simple fields that encode for a single preference
 333+ */
 334+ private static function flattenSimpleFieldDefinition( $fieldDescription ) {
 335+ return array( $fieldDescription['name'] => $fieldDescription );
 336+ }
 337+
 338+ /**
 339+ * Flattener for 'bundle' fields
 340+ */
 341+ private static function flattenBundleDefinition( $fieldDescription ) {
 342+ $flattenedPrefs = array();
 343+ foreach ( $fieldDescription['sections'] as $sectionDescription ) {
 344+ //Each section behaves like a full description of preferences
 345+ $flt = self::flattenPrefsDescription( $sectionDescription );
 346+ $flattenedPrefs = array_merge( $flattenedPrefs, $flt );
 347+ }
 348+ return $flattenedPrefs;
 349+ }
 350+
 351+ /**
 352+ * Further checks for 'number' options
 353+ */
 354+ private static function validateNumberPrefDefinition( $prefDefinition ) {
 355+ if ( isset( $prefDefinition['integer'] ) && $prefDefinition['integer'] === true ) {
 356+ //Check if 'min', 'max' and 'default' are integers (if given)
 357+ if ( intval( $prefDefinition['default'] ) != $prefDefinition['default'] ) {
 358+ return false;
 359+ }
 360+ if ( isset( $prefDefinition['min'] ) && intval( $prefDefinition['min'] ) != $prefDefinition['min'] ) {
 361+ return false;
 362+ }
 363+ if ( isset( $prefDefinition['max'] ) && intval( $prefDefinition['max'] ) != $prefDefinition['max'] ) {
 364+ return false;
 365+ }
 366+ }
 367+
 368+ return true;
 369+ }
 370+
 371+ private static function validateSelectPrefDefinition( $prefDefinition ) {
 372+ $options = $prefDefinition['options'];
 373+
 374+ //Check if it's a regular array
 375+ if ( !self::isOrdinaryArray( $options ) ) {
 376+ return false;
 377+ }
 378+
 379+ foreach ( $options as $option ) {
 380+ //Using array_key_exists() because isset fails for null values
 381+ if ( !isset( $option['name'] ) || !array_key_exists( 'value', $option ) ) {
 382+ return false;
 383+ }
 384+
 385+ //All names must be strings
 386+ if ( !is_string( $option['name'] ) ) {
 387+ return false;
 388+ }
 389+
 390+ //Correct value for $value are null, boolean, integer, float or string
 391+ $value = $option['value'];
 392+ if ( $value !== null &&
 393+ !is_bool( $value ) &&
 394+ !is_int( $value ) &&
 395+ !is_float( $value ) &&
 396+ !is_string( $value ) )
 397+ {
 398+ return false;
 399+ }
 400+ }
 401+
 402+ return true;
 403+ }
 404+
 405+ private static function validateRangePrefDefinition( $prefDefinition ) {
 406+ $step = isset( $prefDefinition['step'] ) ? $prefDefinition['step'] : 1;
 407+
 408+ if ( $step <= 0 ) {
 409+ return false;
 410+ }
 411+
 412+ $min = $prefDefinition['min'];
 413+ $max = $prefDefinition['max'];
 414+
 415+ //Checks if 'max' is a valid value
 416+ //Valid values are min, min + step, min + 2*step, ...
 417+ //Then ( $max - $min ) / $step must be close enough to an integer
 418+ $eps = 1.0e-6; //tolerance
 419+ $tmp = ( $max - $min ) / $step;
 420+ if ( abs( $tmp - floor( $tmp ) ) > $eps ) {
 421+ return false;
 422+ }
 423+
 424+ return true;
 425+ }
 426+
 427+ private static function validateListPrefDefinition( $prefDefinition ) {
 428+ //Name must not be set for the 'field' description
 429+ if ( array_key_exists( 'name', $prefDefinition['field'] ) ) {
 430+ return false;
 431+ }
 432+
 433+ //Validate minlength and maxlength
 434+ $minlength = isset( $prefDefinition['minlength'] ) ? $prefDefinition['minlength'] : 0;
 435+ $maxlength = isset( $prefDefinition['maxlength'] ) ? $prefDefinition['maxlength'] : 1024;
 436+ if ( $minlength < 0 || $maxlength <= 0 || $minlength > $maxlength ) {
 437+ return false;
 438+ }
 439+
 440+ //Check if the field definition is valid, apart from missing the name
 441+ $itemDescription = $prefDefinition['field'];
 442+ $itemDescription['name'] = 'dummy';
 443+ if ( !self::validateFieldDefinition( $itemDescription ) ) {
 444+ return false;
 445+ };
 446+
 447+ //Finally, type described by the 'field' member must be a simple type (e.g.: have "name" ).
 448+ $type = $itemDescription['type'];
 449+ return isset( self::$prefsDescriptionSpecifications[$type]['description']['name'] );
 450+ }
 451+
 452+ /**
 453+ * Flattens a simple field, by calling its field-specific flattener if there is any,
 454+ * or the default flattener otherwise.
 455+ */
 456+ private static function flattenFieldDescription( $fieldDescription ) {
 457+ $fieldSpec = self::$prefsDescriptionSpecifications[$fieldDescription['type']];
 458+ if ( isset( $fieldSpec['flattener'] ) ) {
 459+ $flattener = $fieldSpec['flattener'];
 460+ } else {
 461+ $flattener = 'GadgetPrefs::flattenSimpleFieldDefinition';
 462+ }
 463+ return call_user_func( $flattener, $fieldDescription );
 464+ }
 465+
 466+ /**
 467+ * Returns a map keyed at preference names, and with their corresponding
 468+ * "simple" field descriptions as values.
 469+ * It is assumed that $prefsDescription is valid.
 470+ */
 471+ private static function flattenPrefsDescription( $prefsDescription ) {
 472+ $flattenedPrefsDescription = array();
 473+ foreach ( $prefsDescription['fields'] as $fieldDescription ) {
 474+ $flt = self::flattenFieldDescription( $fieldDescription );
 475+ $flattenedPrefsDescription = array_merge( $flattenedPrefsDescription, $flt );
 476+ }
 477+
 478+ return $flattenedPrefsDescription;
 479+ }
 480+
 481+ /**
 482+ * Validates a single field
 483+ */
 484+ private static function validateFieldDefinition( $fieldDefinition ) {
 485+ static $mandatoryCount = array(), $initialized = false;
 486+
 487+ if ( !$initialized ) {
 488+ //Count of mandatory members for each type
 489+ foreach ( self::$prefsDescriptionSpecifications as $type => $typeSpec ) {
 490+ $mandatoryCount[$type] = 0;
 491+ foreach ( $typeSpec['description'] as $fieldName => $fieldSpec ) {
 492+ if ( $fieldSpec['isMandatory'] === true ) {
 493+ ++$mandatoryCount[$type];
 494+ }
 495+ }
 496+ }
 497+ $initialized = true;
 498+ }
 499+
 500+ //Check if 'type' is set
 501+ if ( !isset( $fieldDefinition['type'] ) ) {
 502+ return false;
 503+ }
 504+
 505+ $type = $fieldDefinition['type'];
 506+
 507+ //check if 'type' is valid
 508+ if ( !isset( self::$prefsDescriptionSpecifications[$type] ) ) {
 509+ return false;
 510+ }
 511+
 512+ //Check if all fields satisfy specification
 513+ $typeSpec = self::$prefsDescriptionSpecifications[$type];
 514+ $typeDescription = $typeSpec['description'];
 515+ $count = 0; //count of present mandatory members
 516+ foreach ( $fieldDefinition as $memberName => $memberValue ) {
 517+
 518+ if ( $memberName == 'type' ) {
 519+ continue; //'type' must not be checked
 520+ }
 521+
 522+ if ( !isset( $typeDescription[$memberName] ) ) {
 523+ return false;
 524+ }
 525+
 526+ if ( $typeDescription[$memberName]['isMandatory'] ) {
 527+ ++$count;
 528+ }
 529+
 530+ if ( isset( $typeDescription[$memberName]['validator'] ) ) {
 531+ $validator = $typeDescription[$memberName]['validator'];
 532+ if ( !call_user_func( $validator, $memberValue ) ) {
 533+ return false;
 534+ }
 535+ }
 536+ }
 537+
 538+ if ( $count != $mandatoryCount[$type] ) {
 539+ return false; //not all mandatory members are given
 540+ }
 541+
 542+ if ( isset( $typeSpec['validator'] ) ) {
 543+ //Call type-specific checker for finer validation
 544+ if ( !call_user_func( $typeSpec['validator'], $fieldDefinition ) ) {
 545+ return false;
 546+ }
 547+ }
 548+
 549+ return true;
 550+ }
 551+
 552+ /**
 553+ * Validate the description of a 'section' of preferences
 554+ */
 555+ private static function validateSectionDefinition( $sectionDescription ) {
 556+ if ( !is_array( $sectionDescription )
 557+ || !isset( $sectionDescription['fields'] )
 558+ || !is_array( $sectionDescription['fields'] ) )
 559+ {
 560+ return false;
 561+ }
 562+
 563+ //Check if 'fields' is a regular (not-associative) array, and that it is not empty
 564+ $count = count( $sectionDescription['fields'] );
 565+ if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) {
 566+ return false;
 567+ }
 568+
 569+ //TODO: validation of members other than $prefs['fields']
 570+
 571+ //Flattened preferences
 572+ $flattenedPrefs = array();
 573+
 574+ foreach ( $sectionDescription['fields'] as $fieldDefinition ) {
 575+
 576+ if ( self::validateFieldDefinition( $fieldDefinition ) == false ) {
 577+ return false;
 578+ }
 579+
 580+ //flatten preferences described by this field
 581+ $flt = self::flattenFieldDescription( $fieldDefinition );
 582+
 583+ foreach ( $flt as $prefName => $prefDescription ) {
 584+ //Finally, check that the 'default' fields exists and is valid
 585+ //for all preferences encoded by this field
 586+
 587+ $type = $prefDescription['type'];
 588+ if ( isset( self::$prefsDescriptionSpecifications[$type]['getDefault'] ) ) {
 589+ $getDefault = self::$prefsDescriptionSpecifications[$type]['getDefault'];
 590+ $value = call_user_func( $getDefault, $prefDescription );
 591+ } else {
 592+ if ( !array_key_exists( 'default', $prefDescription ) ) {
 593+ return false;
 594+ }
 595+ $value = $prefDescription['default'];
 596+ }
 597+
 598+ $prefs = array( $prefName => $value );
 599+ if ( !self::checkSinglePref( $prefDescription, $prefs, $prefName ) ) {
 600+ return false;
 601+ }
 602+ }
 603+
 604+ //If there are preferences with the same name of a previously encountered preference, fail
 605+ if ( array_intersect( array_keys( $flt ), array_keys( $flattenedPrefs ) ) ) {
 606+ return false;
 607+ }
 608+ $flattenedPrefs = array_merge( $flattenedPrefs, $flt );
 609+ }
 610+
 611+ return true;
 612+ }
 613+
 614+ /**
 615+ * Validates the 'sections' member of a 'bundle' field
 616+ */
 617+ private static function validateBundleSectionsDefinition( $sections ) {
 618+ //validate each section, then ensure that preference names
 619+ //of each section are disjoint
 620+
 621+ if ( !self::isOrdinaryArray( $sections ) ) {
 622+ return false;
 623+ }
 624+
 625+ $prefs = array(); //names of preferences
 626+
 627+ foreach ( $sections as $section ) {
 628+ if ( !self::validateSectionDefinition( $section ) ) {
 629+ return false;
 630+ }
 631+
 632+ //Bundle sections must have a "title" field
 633+ if ( !isset( $section['title'] ) || !is_string( $section['title'] ) ) {
 634+ return false;
 635+ }
 636+
 637+ $flt = self::flattenPrefsDescription( $section );
 638+ $newPrefs = array_keys( $flt );
 639+ if ( array_intersect( $prefs, $newPrefs ) ) {
 640+ return false;
 641+ }
 642+
 643+ $prefs = array_merge( $prefs, $newPrefs );
 644+ }
 645+
 646+ return true;
 647+ }
 648+
 649+ /**
 650+ * Checks validity of a preferences description.
 651+ *
 652+ * @param $prefsDescription Array: the preferences description to check.
 653+ *
 654+ * @return boolean true if $prefsDescription is a valid description of preferences, false otherwise.
 655+ */
 656+ public static function isPrefsDescriptionValid( $prefsDescription ) {
 657+ return self::validateSectionDefinition( $prefsDescription );
 658+ }
 659+
 660+ /**
 661+ * Check if a preference is valid, according to description.
 662+ * $prefDescription must be the description of a "simple" field (that is, with 'checker')
 663+ * NOTE: we pass both $prefs and $prefName (instead of just $prefs[$prefName])
 664+ * to allow checking for undefined values.
 665+ */
 666+ private static function checkSinglePref( $prefDescription, $prefs, $prefName ) {
 667+
 668+ //isset( $prefs[$prefName] ) would return false for null values
 669+ if ( !array_key_exists( $prefName, $prefs ) ) {
 670+ return false;
 671+ }
 672+
 673+ $value = $prefs[$prefName];
 674+ $type = $prefDescription['type'];
 675+
 676+ if ( !isset( self::$prefsDescriptionSpecifications[$type] )
 677+ || !isset( self::$prefsDescriptionSpecifications[$type]['checker'] ) )
 678+ {
 679+ return false;
 680+ }
 681+
 682+ $checker = self::$prefsDescriptionSpecifications[$type]['checker'];
 683+ return call_user_func( $checker, $prefDescription, $value );
 684+ }
 685+
 686+ /**
 687+ * Checker for 'boolean' preferences
 688+ */
 689+ private static function checkBooleanPref( $prefDescription, $value ) {
 690+ return is_bool( $value );
 691+ }
 692+
 693+ /**
 694+ * Checker for 'string' preferences
 695+ */
 696+ private static function checkStringPref( $prefDescription, $value ) {
 697+ if ( !is_string( $value ) ) {
 698+ return false;
 699+ }
 700+
 701+ $len = strlen( $value );
 702+
 703+ //Checks the "required" option, if present
 704+ if ( isset( $prefDescription['required'] ) ) {
 705+ $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true;
 706+ if ( $required === true && $len == 0 ) {
 707+ return false;
 708+ } elseif ( $required === false && $len == 0 ) {
 709+ return true; //overriding 'minlength', if given
 710+ }
 711+ }
 712+
 713+ //Checks the "minlength" option, if present
 714+ $minlength = isset( $prefDescription['minlength'] ) ? $prefDescription['minlength'] : 0;
 715+ if ( $len < $minlength ){
 716+ return false;
 717+ }
 718+
 719+ //Checks the "maxlength" option, if present
 720+ $maxlength = isset( $prefDescription['maxlength'] ) ? $prefDescription['maxlength'] : 1024; //TODO: what big integer here?
 721+ if ( $len > $maxlength ){
 722+ return false;
 723+ }
 724+
 725+ return true;
 726+ }
 727+
 728+ /**
 729+ * Checker for 'number' preferences
 730+ */
 731+ private static function checkNumberPref( $prefDescription, $value ) {
 732+ if ( !is_float( $value ) && !is_int( $value ) && $value !== null ) {
 733+ return false;
 734+ }
 735+
 736+ $required = isset( $prefDescription['required'] ) ? $prefDescription['required'] : true;
 737+ if ( $required === false && $value === null ) {
 738+ return true;
 739+ }
 740+
 741+ if ( $value === null ) {
 742+ return false; //$required === true, so null is not acceptable
 743+ }
 744+
 745+ $integer = isset( $prefDescription['integer'] ) ? $prefDescription['integer'] : false;
 746+
 747+ if ( $integer === true && intval( $value ) != $value ) {
 748+ return false; //not integer
 749+ }
 750+
 751+ if ( isset( $prefDescription['min'] ) ) {
 752+ $min = $prefDescription['min'];
 753+ if ( $value < $min ) {
 754+ return false; //value below minimum
 755+ }
 756+ }
 757+
 758+ if ( isset( $prefDescription['max'] ) ) {
 759+ $max = $prefDescription['max'];
 760+ if ( $value > $max ) {
 761+ return false; //value above maximum
 762+ }
 763+ }
 764+
 765+ return true;
 766+ }
 767+
 768+ /**
 769+ * Checker for 'select' preferences
 770+ */
 771+ private static function checkSelectPref( $prefDescription, $value ) {
 772+ foreach ( $prefDescription['options'] as $option ) {
 773+ if ( $option['value'] === $value ) {
 774+ return true;
 775+ }
 776+ }
 777+
 778+ return false;
 779+ }
 780+
 781+ /**
 782+ * Checker for 'range' preferences
 783+ */
 784+ private static function checkRangePref( $prefDescription, $value ) {
 785+ if ( !is_float( $value ) && !is_int( $value ) ) {
 786+ return false;
 787+ }
 788+
 789+ $min = $prefDescription['min'];
 790+ $max = $prefDescription['max'];
 791+
 792+ if ( $value < $min || $value > $max ) {
 793+ return false;
 794+ }
 795+
 796+ $step = isset( $prefDescription['step'] ) ? $prefDescription['step'] : 1;
 797+
 798+ if ( $step <= 0 ) {
 799+ return false;
 800+ }
 801+
 802+ //Valid values are min, min + step, min + 2*step, ...
 803+ //Then ( $value - $min ) / $step must be close enough to an integer
 804+ $eps = 1.0e-6; //tolerance
 805+ $tmp = ( $value - $min ) / $step;
 806+ if ( abs( $tmp - floor( $tmp ) ) > $eps ) {
 807+ return false;
 808+ }
 809+
 810+ return true;
 811+ }
 812+
 813+ /**
 814+ * Checker for 'date' preferences
 815+ */
 816+ private static function checkDatePref( $prefDescription, $value ) {
 817+ if ( $value === null ) {
 818+ return true;
 819+ }
 820+
 821+ //Basic syntactic checks
 822+ if ( !is_string( $value ) ||
 823+ !preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $value ) )
 824+ {
 825+ return false;
 826+ }
 827+
 828+ //Full parsing
 829+ return date_create( $value ) !== false;
 830+ }
 831+
 832+ /**
 833+ * Checker for 'color' preferences
 834+ */
 835+ private static function checkColorPref( $prefDescription, $value ) {
 836+ //Check if it's a string representing a color
 837+ //(with 6 hexadecimal lowercase characters).
 838+ return is_string( $value ) && preg_match( '/^#[0-9a-f]{6}$/', $value );
 839+ }
 840+
 841+ /**
 842+ * Checker for 'composite' preferences
 843+ */
 844+ private static function checkCompositePref( $prefDescription, $value ) {
 845+ if ( !is_array( $value ) ) {
 846+ return false;
 847+ }
 848+
 849+ $flattened = self::flattenPrefsDescription( $prefDescription );
 850+
 851+ foreach ( $flattened as $subPrefName => $subPrefDescription ) {
 852+ if ( !array_key_exists( $subPrefName, $value ) ||
 853+ !self::checkSinglePref( $subPrefDescription, $value, $subPrefName ) )
 854+ {
 855+ return false;
 856+ }
 857+ }
 858+ return true;
 859+ }
 860+
 861+ /**
 862+ * Checker for 'list' preferences
 863+ */
 864+ private static function checkListPref( $prefDescription, $value ) {
 865+ if ( !self::isOrdinaryArray( $value ) ) {
 866+ return false;
 867+ }
 868+
 869+ $nItems = count( $value );
 870+
 871+ //Checks the "required" option, if present
 872+ if ( isset( $prefDescription['required'] ) ) {
 873+ $required = $prefDescription['required'];
 874+ if ( $required === true && $nItems == 0 ) {
 875+ return false;
 876+ } elseif ( $required === false && $nItems == 0 ) {
 877+ return true; //overriding 'minlength'
 878+ }
 879+ }
 880+
 881+ $minlength = isset( $prefDescription['minlength'] ) ? $prefDescription['minlength'] : 0;
 882+ $maxlength = isset( $prefDescription['maxlength'] ) ? $prefDescription['maxlength'] : 1024;
 883+ if ( $nItems < $minlength || $nItems > $maxlength ) {
 884+ return false;
 885+ }
 886+
 887+ $itemDescription = $prefDescription['field'];
 888+ foreach ( $value as $item ) {
 889+ if ( !self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) {
 890+ return false;
 891+ }
 892+ }
 893+
 894+ return true;
 895+ }
 896+
 897+ /**
 898+ * Checks if $prefs is an array of preferences that passes validation.
 899+ * It is assumed that $prefsDescription is a valid description of preferences.
 900+ *
 901+ * @param $prefsDescription Array: the preferences description to use.
 902+ * @param $prefs Array: reference of the array of preferences to check.
 903+ *
 904+ * @return boolean true if $prefs passes validation against $prefsDescription, false otherwise.
 905+ */
 906+ public static function checkPrefsAgainstDescription( $prefsDescription, $prefs ) {
 907+ $flattenedPrefs = self::flattenPrefsDescription( $prefsDescription );
 908+ $validPrefs = array();
 909+ //Check that all the given preferences pass validation
 910+ foreach ( $flattenedPrefs as $prefDescription ) {
 911+ $prefName = $prefDescription['name'];
 912+ if ( !self::checkSinglePref( $prefDescription, $prefs, $prefName ) ) {
 913+ return false;
 914+ }
 915+ $validPrefs[$prefName] = true;
 916+ }
 917+
 918+ //Check that $prefs contains no preferences that are not described in $prefsDescription
 919+ foreach ( $prefs as $prefName => $value ) {
 920+ if ( !isset( $validPrefs[$prefName] ) ) {
 921+ return false;
 922+ }
 923+ }
 924+
 925+ return true;
 926+ }
 927+
 928+ /**
 929+ * Matcher for 'composite' type preferences
 930+ */
 931+ private static function matchCompositePref( $prefDescription, $prefs, $prefName ) {
 932+ if ( !array_key_exists( $prefName, $prefs ) || !is_array( $prefs[$prefName] ) ) {
 933+ $prefs[$prefName] = array();
 934+ }
 935+
 936+ self::matchPrefsWithDescription( $prefDescription, $prefs[$prefName] );
 937+
 938+ return $prefs;
 939+ }
 940+
 941+ /**
 942+ * Matcher for 'list' type preferences
 943+ * If value is not an array, just reset to default; otherwise, delete elements that fail validation
 944+ */
 945+ private static function matchListPref( $prefDescription, $prefs, $prefName ) {
 946+ if ( !isset( $prefs[$prefName] ) || !self::isOrdinaryArray( $prefs[$prefName] ) ) {
 947+ $prefs[$prefName] = $prefDescription['default'];
 948+ return $prefs;
 949+ }
 950+
 951+ $itemDescription = $prefDescription['field'];
 952+ $newItems = array();
 953+ foreach( $prefs[$prefName] as $item ) {
 954+ if ( self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) {
 955+ $newItems[] = $item;
 956+ }
 957+ }
 958+ $prefs[$prefName] = $newItems;
 959+
 960+ return $prefs;
 961+ }
 962+
 963+ /**
 964+ * Fixes $prefs so that it matches the description given by $prefsDescription.
 965+ * All values of $prefs that fail validation are replaced with default values.
 966+ * It is assumed that $prefsDescription is a valid description of preferences.
 967+ *
 968+ * @param $prefsDescription Array: the preferences description to use.
 969+ * @param &$prefs Array: reference of the array of preferences to match.
 970+ */
 971+ public static function matchPrefsWithDescription( $prefsDescription, &$prefs ) {
 972+ $flattenedPrefs = self::flattenPrefsDescription( $prefsDescription );
 973+ $validPrefs = array();
 974+
 975+ //Fix preferences that fail validation, by replacing their value with default
 976+ foreach ( $flattenedPrefs as $prefDescription ) {
 977+ $prefName = $prefDescription['name'];
 978+ if ( !self::checkSinglePref( $prefDescription, $prefs, $prefName ) ) {
 979+ $type = $prefDescription['type'];
 980+ if ( isset( self::$prefsDescriptionSpecifications[$type]['matcher'] ) ) {
 981+ //Use specific matcher for this type
 982+ $matcher = self::$prefsDescriptionSpecifications[$type]['matcher'];
 983+ $prefs = call_user_func( $matcher, $prefDescription, $prefs, $prefName );
 984+ } else {
 985+ //Default matcher, just use 'default' value
 986+ $prefs[$prefName] = $prefDescription['default'];
 987+ }
 988+ }
 989+ $validPrefs[$prefName] = true;
 990+ }
 991+
 992+ //Remove unexisting preferences from $prefs
 993+ foreach ( $prefs as $prefName => $value ) {
 994+ if ( !isset( $validPrefs[$prefName] ) ) {
 995+ unset( $prefs[$prefName] );
 996+ }
 997+ }
 998+ }
 999+
 1000+ /**
 1001+ * Return default preferences according to the given description.
 1002+ *
 1003+ * @param $prefsDescription Array: reference of the array of preferences to match.
 1004+ * It is assumed that $prefsDescription is a valid description of preferences.
 1005+ *
 1006+ * @return Array: the set of default preferences, keyed by preference name.
 1007+ */
 1008+ public static function getDefaults( $prefsDescription ) {
 1009+ $prefs = array();
 1010+ self::matchPrefsWithDescription( $prefsDescription, $prefs );
 1011+ return $prefs;
 1012+ }
 1013+
 1014+ /**
 1015+ * Removes from $prefs all preferences that don't need to be saved, because
 1016+ * they are equal to their default value.
 1017+ * It is assumed that $prefsDescription is a valid description of preferences.
 1018+ *
 1019+ * @param $prefsDescription Array: the preferences description to use.
 1020+ * @param &$prefs Array: reference of the array of preferences to simplify.
 1021+ */
 1022+ public static function simplifyPrefs( $prefsDescription, &$prefs ) {
 1023+ $flattenedPrefs = self::flattenPrefsDescription( $prefsDescription );
 1024+
 1025+ foreach( $flattenedPrefs as $prefName => $prefDescription ) {
 1026+ $type = $prefDescription['type'];
 1027+
 1028+ if ( isset( self::$prefsDescriptionSpecifications[$type]['simplifier'] ) ) {
 1029+ $simplify = self::$prefsDescriptionSpecifications[$type]['simplifier'];
 1030+ $prefs = call_user_func( $simplify, $prefDescription, $prefs );
 1031+ } else { //
 1032+ $prefDefault = $prefDescription['default'];
 1033+ if ( $prefs[$prefName] === $prefDefault ) {
 1034+ unset( $prefs[$prefName] );
 1035+ }
 1036+ }
 1037+ }
 1038+ }
 1039+
 1040+ /**
 1041+ * Simplifier for 'composite' type fields
 1042+ */
 1043+ private static function simplifyCompositePref( $prefDescription, $prefs ) {
 1044+ $name = $prefDescription['name'];
 1045+ if ( array_key_exists( $name, $prefs ) ) {
 1046+ self::simplifyPrefs( $prefDescription, $prefs[$name] );
 1047+ if ( count( $prefs[$name] ) == 0 ) {
 1048+ unset( $prefs[$name] );
 1049+ }
 1050+ }
 1051+ return $prefs;
 1052+ }
 1053+
 1054+ /**
 1055+ * Returns true if $str should be interpreted as a message, false otherwise.
 1056+ *
 1057+ * @param $str String
 1058+ * @return Mixed
 1059+ *
 1060+ */
 1061+ private static function isMessage( $str ) {
 1062+ return strlen( $str ) >= 2
 1063+ && $str[0] == '@'
 1064+ && $str[1] != '@';
 1065+ }
 1066+
 1067+ /**
 1068+ * Returns the list of messages used by a field. If the field type specifications define a "getMessages" method,
 1069+ * uses it, otherwise returns the message in the 'label' member (if any).
 1070+ */
 1071+ private static function getFieldMessages( $fieldDescription ) {
 1072+ $type = $fieldDescription['type'];
 1073+ $prefSpec = self::$prefsDescriptionSpecifications[$type];
 1074+ if ( isset( $prefSpec['getMessages'] ) ) {
 1075+ $getMessages = $prefSpec['getMessages'];
 1076+ return call_user_func( $getMessages, $fieldDescription );
 1077+ } else {
 1078+ if ( isset( $fieldDescription['label'] ) && self::isMessage( $fieldDescription['label'] ) ) {
 1079+ return array( substr( $fieldDescription['label'], 1 ) );
 1080+ }
 1081+ }
 1082+ return array();
 1083+ }
 1084+
 1085+ /**
 1086+ * Returns a list of (unprefixed) messages mentioned by $prefsDescription. It is assumed that
 1087+ * $prefsDescription is valid (i.e.: GadgetPrefs::isPrefsDescriptionValid( $prefsDescription ) === true).
 1088+ *
 1089+ * @param $prefsDescription Array: the preferences description to use.
 1090+ * @return Array: the messages needed by $prefsDescription.
 1091+ */
 1092+ public static function getMessages( $prefsDescription ) {
 1093+ $msgs = array();
 1094+ foreach ( $prefsDescription['fields'] as $fieldDescription ) {
 1095+ $msgs = array_merge( $msgs, self::getFieldMessages( $fieldDescription ) );
 1096+ }
 1097+ return array_unique( $msgs );
 1098+ }
 1099+
 1100+ /**
 1101+ * Returns the messages for a 'select' field description
 1102+ */
 1103+ private static function getSelectMessages( $prefDescription ) {
 1104+ $msgs = array();
 1105+ foreach ( $prefDescription['options'] as $option ) {
 1106+ $optName = $option['name'];
 1107+ if ( self::isMessage( $optName ) ) {
 1108+ $msgs[] = substr( $optName, 1 );
 1109+ }
 1110+ }
 1111+ return array_unique( $msgs );
 1112+ }
 1113+
 1114+ /**
 1115+ * Returns the messages for a 'bundle' field description
 1116+ */
 1117+ private static function getBundleMessages( $prefDescription ) {
 1118+ //returns the union of all messages of all sections, plus section names
 1119+ $msgs = array();
 1120+ foreach ( $prefDescription['sections'] as $sectionDescription ) {
 1121+ $msgs = array_merge( $msgs, self::getMessages( $sectionDescription ) );
 1122+ $sectionTitle = $sectionDescription['title'];
 1123+ if ( self::isMessage( $sectionTitle ) ) {
 1124+ $msgs[] = substr( $sectionTitle, 1 );
 1125+ }
 1126+ }
 1127+ return array_unique( $msgs );
 1128+ }
 1129+
 1130+ /**
 1131+ * Returns the messages for a 'list' field description
 1132+ */
 1133+ private static function getListMessages( $prefDescription ) {
 1134+ return self::getFieldMessages( $prefDescription['field'] );
 1135+ }
 1136+
 1137+ /**
 1138+ * Returns the default value of a 'composite' field, that is the object of the
 1139+ * default values of its subfields.
 1140+ */
 1141+ private static function getCompositeDefault( $prefDescription ) {
 1142+ return self::getDefaults( $prefDescription );
 1143+ }
 1144+
 1145+ /**
 1146+ * Returns the messages for a 'composite' field description
 1147+ */
 1148+ private static function getCompositeMessages( $prefDescription ) {
 1149+ return self::getMessages( $prefDescription );
 1150+ }
 1151+}
Property changes on: branches/RL2/extensions/Gadgets/backend/GadgetPrefs.php
___________________________________________________________________
Added: svn:eol-style
11152 + native
Index: branches/RL2/extensions/Gadgets/backend/GadgetOptionsResourceLoaderModule.php
@@ -0,0 +1,75 @@
 2+<?php
 3+
 4+/**
 5+ * Gadgets extension - lets users select custom javascript gadgets
 6+ *
 7+ *
 8+ * For more info see http://mediawiki.org/wiki/Extension:Gadgets
 9+ *
 10+ * @file
 11+ * @ingroup Extensions
 12+ * @author Daniel Kinzler, brightbyte.de
 13+ * @copyright © 2007 Daniel Kinzler
 14+ * @license GNU General Public Licence 2.0 or later
 15+ */
 16+
 17+/**
 18+ * Class representing the user-specific options for a gadget
 19+ */
 20+class GadgetOptionsResourceLoaderModule extends ResourceLoaderModule {
 21+ private $gadget;
 22+
 23+ /**
 24+ * Creates an instance of this class
 25+ * @param $gadget Gadget: the gadget this module is built upon.
 26+ */
 27+ public function __construct( $gadget ) {
 28+ $this->gadget = $gadget;
 29+ }
 30+
 31+ /**
 32+ * Overrides ResourceLoaderModule::getDependencies()
 33+ * @return Array: Names of resources this module depends on
 34+ */
 35+ public function getDependencies() {
 36+ return array( 'ext.gadgets' );
 37+ }
 38+
 39+ /**
 40+ * Overrides ResourceLoaderModule::getGroup()
 41+ * @return String
 42+ */
 43+ public function getGroup() {
 44+ return 'private';
 45+ }
 46+
 47+ /**
 48+ * Overrides ResourceLoaderModule::getScript()
 49+ * @param $context ResourceLoaderContext
 50+ * @return String
 51+ */
 52+ public function getScript( ResourceLoaderContext $context ) {
 53+ $gadgetInfo = array(
 54+ 'name' => $this->gadget->getID(),
 55+ 'config' => $this->gadget->getPrefs()
 56+ );
 57+ return Xml::encodeJsCall( 'mw.gadgets.info.set',
 58+ array( $this->gadget->getID(), $gadgetInfo ) );
 59+ }
 60+
 61+ /**
 62+ * Overrides ResourceLoaderModule::getModifiedTime()
 63+ * @param $context ResourceLoaderContext
 64+ * @return Integer
 65+ */
 66+ public function getModifiedTime( ResourceLoaderContext $context ) {
 67+ $prefsMTime = $this->gadget->getPrefsTimestamp();
 68+
 69+ $resourceLoader = $context->getResourceLoader();
 70+ $parentModule = $resourceLoader->getModule( $this->gadget->getModuleName() ); // FIXME does this work?
 71+ $gadgetMTime = $parentModule->getModifiedTime( $context );
 72+
 73+ return max( $gadgetMTime, $prefsMTime );
 74+ }
 75+}
 76+
Property changes on: branches/RL2/extensions/Gadgets/backend/GadgetOptionsResourceLoaderModule.php
___________________________________________________________________
Added: svn:eol-style
177 + native
Index: branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.css
@@ -0,0 +1,175 @@
 2+/**
 3+ * jQuery Form Builder
 4+ * Written by Salvatore Ingala in 2011
 5+ * Released under the MIT and GPL licenses.
 6+ */
 7+
 8+.formbuilder label {
 9+ display: inline-block;
 10+ text-align: right;
 11+ margin-right: 2%;
 12+ width: 45%;
 13+ vertical-align: middle;
 14+ /* IE7 */
 15+ *width: 42%;
 16+}
 17+
 18+.formbuilder label.error {
 19+ display: block;
 20+ margin-left: 50%;
 21+ font-size: 90%;
 22+}
 23+
 24+.formbuilder input {
 25+ vertical-align: middle;
 26+}
 27+
 28+.formbuilder input[type="text"] {
 29+ width: 50%;
 30+}
 31+
 32+.formbuilder .ui-slider {
 33+ vertical-align: middle;
 34+ display: inline-block;
 35+ width: 50%;
 36+ /* IE7 */
 37+ zoom: 1;
 38+ *display: inline;
 39+}
 40+
 41+.formBuilder-slider-tooltip {
 42+ padding: 2px;
 43+ min-width: 20px;
 44+ text-align: center;
 45+ -o-box-shadow: 0 0 3px #aaa;
 46+ -moz-box-shadow: 0 0 3px #aaa;
 47+ -webkit-box-shadow: 0 0 3px #aaa;
 48+ box-shadow: 0 0 3px #aaa;
 49+}
 50+
 51+.formbuilder select {
 52+ vertical-align: middle;
 53+ width: 40%;
 54+}
 55+
 56+#colorpicker {
 57+ width: 197;
 58+}
 59+
 60+.colorpicker-input.error {
 61+ border-color: red;
 62+}
 63+
 64+.farbtastic {
 65+ border: none;
 66+}
 67+
 68+.formbuilder-slot {
 69+ border: none;
 70+ padding: 3px;
 71+}
 72+
 73+/* type-specific styles */
 74+
 75+.formbuilder-field-label label {
 76+ width: 95%;
 77+ text-align: left;
 78+}
 79+
 80+.formbuilder-button {
 81+ cursor: pointer;
 82+}
 83+
 84+.formbuilder-list-items {
 85+ border: 1px solid #888;
 86+ border-bottom: none;
 87+}
 88+
 89+.formbuilder-list-item-container {
 90+ float: left;
 91+ margin-right: -35px;
 92+ width: 100%;
 93+ border-bottom: 1px solid #888;
 94+}
 95+
 96+/* Show all the borders during dragging */
 97+.ui-sortable-helper .formbuilder-list-item-container {
 98+ border: 1px solid #888;
 99+}
 100+
 101+.formbuilder-list-item-content {
 102+ margin-right: 35px;
 103+}
 104+
 105+.formbuilder-list-item-buttons {
 106+ float: right;
 107+ width: 35px;
 108+}
 109+
 110+.formbuilder-list-button-move, .formbuilder-list-button-delete {
 111+ float: right;
 112+}
 113+
 114+.formbuilder-list-button-move {
 115+ cursor: move;
 116+}
 117+
 118+/* Center the new item button */
 119+.formbuilder-list-button-new {
 120+ margin-left: auto;
 121+ margin-right: auto;
 122+}
 123+
 124+/* formBuilder editor */
 125+
 126+.formbuilder-slot-nonempty {
 127+ border: 1px solid #888;
 128+ padding: 0px;
 129+}
 130+
 131+.formbuilder-slot-empty {
 132+ border: 1px dashed #aaa;
 133+ padding: 0px;
 134+}
 135+
 136+.formbuilder-slot-can-drop {
 137+ background: blue;
 138+ opacity: 0.1;
 139+}
 140+
 141+.formbuilder-editor-slot-buttons {
 142+ height: 17px;
 143+}
 144+
 145+.formbuilder-editor-button-move {
 146+ cursor: move;
 147+}
 148+
 149+.formbuilder-editor-button-new, .formbuilder-editor-button-new-section,
 150+.formbuilder-editor-button-edit, .formbuilder-editor-button-move
 151+{
 152+ float: left;
 153+}
 154+
 155+.formbuilder-editor-button-delete {
 156+ float: right;
 157+}
 158+
 159+
 160+/* Fixes a minor glitch in Firefox (buttons in tabs not floating properly) */
 161+.formbuilder .ui-tabs-nav a > span :not(.formbuilder-editor-button) {
 162+ float: left;
 163+}
 164+
 165+.formbuilder-editor-button-delete-section {
 166+ float: right;
 167+}
 168+
 169+.formbuilder-editor-button-edit-section {
 170+ float: right;
 171+ margin-left: 6px;
 172+}
 173+
 174+.formbuilder-slot-editable .formbuilder-field-composite {
 175+ padding: 1em;
 176+}
Property changes on: branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.css
___________________________________________________________________
Added: svn:eol-style
1177 + native
Index: branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.js
@@ -0,0 +1,2385 @@
 2+/**
 3+ * jQuery Form Builder
 4+ * Written by Salvatore Ingala in 2011
 5+ * Released under the MIT and GPL licenses.
 6+ */
 7+
 8+// TODO: Review this whole file
 9+
 10+( function( $, mw ) {
 11+
 12+ //Field types that can be referred to by preference descriptions
 13+ var validFieldTypes = {}, //filled when constructors are initialized
 14+ prefsSpecifications; //defined later, declaring here to avoid references to undeclared variable
 15+
 16+ /* Utility functions */
 17+
 18+ /**
 19+ * Preprocesses strings end possibly replaces them with messages.
 20+ * If str starts with "@" the rest of the string is assumed to be
 21+ * a message, and the result of mw.msg is returned.
 22+ * Two "@@" at the beginning escape for a single "@".
 23+ */
 24+ function preproc( msgPrefix, str ) {
 25+ if ( str.length <= 1 || str.charAt( 0 ) !== '@' ) {
 26+ return str;
 27+ } else if ( str.substr( 0, 2 ) == '@@' ) {
 28+ return str.substr( 1 );
 29+ } else {
 30+ if ( !msgPrefix ) {
 31+ msgPrefix = "";
 32+ }
 33+ return mw.message( msgPrefix + str.substr( 1 ) ).plain();
 34+ }
 35+ }
 36+
 37+ /**
 38+ * Commodity function to avoid id conflicts
 39+ */
 40+ var getIncrementalCounter = ( function() {
 41+ var cnt = 0;
 42+ return function() {
 43+ return cnt++;
 44+ };
 45+ } )();
 46+
 47+ /**
 48+ * Pads a number with leading zeroes until length is n characters
 49+ */
 50+ function pad( n, len ) {
 51+ // TODO optimize
 52+ var res = '' + n;
 53+ while ( res.length < len ) {
 54+ res = '0' + res;
 55+ }
 56+ return res;
 57+ }
 58+
 59+ /**
 60+ * Returns an object with only one key and the corresponding value given in arguments
 61+ */
 62+ function pair( key, val ) {
 63+ var res = {};
 64+ res[key] = val;
 65+ return res;
 66+ }
 67+
 68+ function isInteger( val ) {
 69+ return typeof val == 'number' && val === Math.floor( val );
 70+ }
 71+
 72+ /**
 73+ * Returns true if val is either true, false, null, a number or a string
 74+ */
 75+ function isScalar( val ) {
 76+ return val === true || val === false || val === null
 77+ || typeof val == 'number' || typeof val == 'string';
 78+ }
 79+
 80+ /**
 81+ * Returns true if name is a valid preference name
 82+ */
 83+ function isValidPreferenceName( name ) {
 84+ return typeof name == 'string'
 85+ && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test( name )
 86+ && name.length <= 40;
 87+ }
 88+
 89+ /**
 90+ * Make a deep copy of an object
 91+ */
 92+ function clone( obj ) {
 93+ return $.extend( true, {}, obj );
 94+ }
 95+
 96+ /**
 97+ * Helper function for inheritance, see http://javascript.crockford.com/prototypal.html
 98+ */
 99+ function object( o ) {
 100+ function F() {}
 101+ F.prototype = o;
 102+ return new F();
 103+ }
 104+
 105+ /**
 106+ * Helper function for inheritance
 107+ */
 108+ function inherit( Derived, Base ) {
 109+ Derived.prototype = object( Base.prototype );
 110+ Derived.prototype.constructor = Derived;
 111+ }
 112+
 113+ /**
 114+ * Add a "smart" listener to watch for changes to an <input /> element
 115+ * This binds to several events, but calls the callback only if the value actually changed
 116+ */
 117+ function addSmartChangeListener( $input, callback ) {
 118+ var oldValue = $input.val();
 119+ //bind all events that may change the value of the field (some are brower-specific)
 120+ $input.bind( 'keyup change propertychange input paste', function() {
 121+ var newValue = $input.val();
 122+ if ( oldValue !== newValue ) {
 123+ oldValue = newValue;
 124+ callback();
 125+ }
 126+ } );
 127+ }
 128+
 129+ /* Validator plugin utility functions and methods */
 130+
 131+ /**
 132+ * Removes the field rules of "field" to the formbuilder form.
 133+ * NOTE: this method must be called before physically removing the element from the form.
 134+ */
 135+ function deleteFieldRules( field ) {
 136+ //Remove all its validation rules
 137+ var validationSettings = field.getValidationSettings();
 138+ if ( validationSettings.rules ) {
 139+ $.each( validationSettings.rules, function( name, value ) {
 140+ $( '#' + name ).rules( 'remove' );
 141+ } );
 142+ }
 143+ }
 144+
 145+ /**
 146+ * Adds the field rules of "field" to the formbuilder form.
 147+ * NOTE: the field's element must have been appended to the form, yet.
 148+ */
 149+ function addFieldRules( field ) {
 150+ var validationSettings = field.getValidationSettings();
 151+ if ( validationSettings.rules ) {
 152+ $.each( validationSettings.rules, function( name, rules ) {
 153+ //Find messages associated to this rule, if any
 154+ if ( typeof validationSettings.messages != 'undefined' &&
 155+ typeof validationSettings.messages[name] != 'undefined')
 156+ {
 157+ rules.messages = validationSettings.messages[name];
 158+ }
 159+
 160+ $( field.getElement() ).find( '#' + name ).rules( 'add', rules );
 161+ } );
 162+ }
 163+ }
 164+
 165+
 166+ function testOptional( value, element ) {
 167+ var rules = $( element ).rules();
 168+ if ( typeof rules.required == 'undefined' || rules.required === false ) {
 169+ if ( value.length === 0 ) {
 170+ return true;
 171+ }
 172+ }
 173+ return false;
 174+ }
 175+
 176+ //validator for "required" fields (without trimming whitespaces)
 177+ $.validator.addMethod( "requiredStrict", function( value, element ) {
 178+ return value.length > 0;
 179+ }, mw.msg( 'gadgets-formbuilder-required' ) );
 180+
 181+ //validator for "minlength" fields (without trimming whitespaces)
 182+ $.validator.addMethod( "minlengthStrict", function( value, element, param ) {
 183+ return testOptional( value, element ) || value.length >= param;
 184+ } );
 185+
 186+ //validator for "maxlength" fields (without trimming whitespaces)
 187+ $.validator.addMethod( "maxlengthStrict", function( value, element, param ) {
 188+ return testOptional( value, element ) || value.length <= param;
 189+ } );
 190+
 191+ //validator for integer fields
 192+ $.validator.addMethod( "integer", function( value, element ) {
 193+ return testOptional( value, element ) || /^-?\d+$/.test(value);
 194+ }, mw.msg( 'gadgets-formbuilder-integer' ) );
 195+
 196+ //validator for datepicker fields
 197+ $.validator.addMethod( "datePicker", function( value, element ) {
 198+ var format = $( element ).datepicker( 'option', 'dateFormat' );
 199+ try {
 200+ var date = $.datepicker.parseDate( format, value );
 201+ return true;
 202+ } catch ( e ) {
 203+ return false;
 204+ }
 205+ }, mw.msg( 'gadgets-formbuilder-date' ) );
 206+
 207+ //validator for colorpicker fields
 208+ $.validator.addMethod( "color", function( value, element ) {
 209+ return $.colorUtil.getRGB( value ) !== undefined;
 210+ }, mw.msg( 'gadgets-formbuilder-color' ) );
 211+
 212+ //validator for scalar fields
 213+ $.validator.addMethod( "scalar", function( value, element ) {
 214+ //parseJSON decodes interprets the empty string as 'null', and we don't want that
 215+ if ( value === '' ) {
 216+ return false;
 217+ }
 218+
 219+ try {
 220+ if ( isScalar( $.parseJSON( value ) ) ) {
 221+ return true;
 222+ }
 223+ } catch( e ) { /* nothing */ }
 224+
 225+ return false;
 226+ }, mw.msg( 'gadgets-formbuilder-scalar' ) );
 227+
 228+ /* Functions used by the preferences editor */
 229+ function createFieldDialog( params, options ) {
 230+ var self = this;
 231+
 232+ if ( !$.isFunction( params.callback ) ) {
 233+ $.error( 'createFieldDialog: missing or wrong "callback" parameter' );
 234+ }
 235+
 236+ if ( typeof options == 'undefined' ) {
 237+ options = {};
 238+ }
 239+
 240+ var type, description, values;
 241+ if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) {
 242+ //Create a dialog to choose the type of field to create
 243+ var selectOptions = [];
 244+ $.each( validFieldTypes, function( fieldType ) {
 245+ selectOptions.push( {
 246+ name: fieldType,
 247+ value: fieldType
 248+ } );
 249+ } );
 250+
 251+ $( {
 252+ fields: [ {
 253+ 'name': "type",
 254+ 'type': "select",
 255+ 'label': mw.msg( 'gadgets-formbuilder-editor-choose-field' ),
 256+ 'options': selectOptions,
 257+ 'default': selectOptions[0].value
 258+ } ]
 259+ } ).formBuilder( { idPrefix: 'choose-field-' } )
 260+ .submit( function() {
 261+ return false; //prevent form submission
 262+ } )
 263+ .dialog( {
 264+ width: 450,
 265+ modal: true,
 266+ resizable: false,
 267+ title: mw.msg( 'gadgets-formbuilder-editor-choose-field-title' ),
 268+ close: function() {
 269+ $( this ).remove();
 270+ },
 271+ buttons: [
 272+ {
 273+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 274+ click: function() {
 275+ var values = $( this ).formBuilder( 'getValues' );
 276+ $( this ).dialog( "close" );
 277+ createFieldDialog( {
 278+ type: values.type,
 279+ oldDescription: params.oldDescription,
 280+ callback: params.callback
 281+ }, options );
 282+ }
 283+ },
 284+ {
 285+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 286+ click: function() {
 287+ $( this ).dialog( "close" );
 288+ }
 289+ }
 290+ ]
 291+ } );
 292+
 293+ return;
 294+ } else {
 295+ type = params.type;
 296+ if ( typeof prefsSpecifications[type] == 'undefined' ) {
 297+ $.error( 'createFieldDialog: invalid type: ' + type );
 298+ } else if ( $.isFunction( prefsSpecifications[type].builder ) ) {
 299+ prefsSpecifications[type].builder( options, function( field ) {
 300+ if ( field !== null ) {
 301+ params.callback( field );
 302+ }
 303+ } );
 304+ return;
 305+ }
 306+
 307+ //typeof prefsSpecifications[type].builder == 'object'
 308+
 309+ description = {
 310+ fields: prefsSpecifications[type].builder
 311+ };
 312+ }
 313+
 314+ if ( typeof params.values != 'undefined' ) {
 315+ values = params.values;
 316+ } else {
 317+ values = {};
 318+ }
 319+
 320+ //Create the dialog to set field properties
 321+ var dlg = $( '<div/>' );
 322+ var form = $( description ).formBuilder( {
 323+ values: values,
 324+ idPrefix: 'create-field-'
 325+ } ).submit( function() {
 326+ return false; //prevent form submission
 327+ } ).appendTo( dlg );
 328+
 329+ dlg.dialog( {
 330+ modal: true,
 331+ width: 550,
 332+ resizable: false,
 333+ title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', type ),
 334+ close: function() {
 335+ $( this ).remove();
 336+ },
 337+ buttons: [
 338+ {
 339+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 340+ click: function() {
 341+ var isValid = $( form ).formBuilder( 'validate' );
 342+
 343+ if ( isValid ) {
 344+ var fieldDescription = $( form ).formBuilder( 'getValues' );
 345+
 346+ if ( typeof type != 'undefined' ) {
 347+ //Remove properties that equal their default
 348+ $.each( description.fields, function( index, fieldSpec ) {
 349+ var property = fieldSpec.name;
 350+ if ( fieldDescription[property] === fieldSpec['default'] ) {
 351+ delete fieldDescription[property];
 352+ }
 353+ } );
 354+ }
 355+
 356+ //Try to create the field. In case of error, warn the user.
 357+ fieldDescription.type = type;
 358+
 359+ if ( typeof params.oldDescription != 'undefined' ) {
 360+ //If there are values in the old description that cannot be set by
 361+ //the dialog, don't lose them (e.g.: 'fields' member in composite fields).
 362+ $.each( params.oldDescription, function( key, value ) {
 363+ if ( typeof fieldDescription[key] == 'undefined' ) {
 364+ fieldDescription[key] = value;
 365+ }
 366+ } );
 367+ }
 368+
 369+ var FieldConstructor = validFieldTypes[type],
 370+ field;
 371+
 372+ try {
 373+ field = new FieldConstructor( fieldDescription, options );
 374+ } catch ( err ) {
 375+ alert( "Invalid field options: " + err ); //TODO: i18n
 376+ return;
 377+ }
 378+
 379+ if ( params.callback( field ) === true ) {
 380+ $( this ).dialog( "close" );
 381+ }
 382+ }
 383+ }
 384+ },
 385+ {
 386+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 387+ click: function() {
 388+ $( this ).dialog( "close" );
 389+ params.callback( null );
 390+ }
 391+ }
 392+ ]
 393+ } );
 394+ }
 395+
 396+ function showEditFieldDialog( fieldDesc, listParams, options, callback ) {
 397+ $( { "fields": [ fieldDesc ] } )
 398+ .formBuilder( {
 399+ editable: true,
 400+ staticFields: true,
 401+ idPrefix: 'list-edit-field-'
 402+ } )
 403+ .submit( function() {
 404+ return false;
 405+ } )
 406+ .dialog( {
 407+ modal: true,
 408+ width: 550,
 409+ resizable: false,
 410+ title: mw.msg( 'gadgets-formbuilder-editor-edit-field-title' ),
 411+ close: function() {
 412+ $( this ).remove();
 413+ },
 414+ buttons: [
 415+ {
 416+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 417+ click: function() {
 418+
 419+ if ( !$( this ).formBuilder( 'validate' ) ) {
 420+ return;
 421+ }
 422+
 423+ var fieldDesc = $( this ).formBuilder( 'getDescription' ).fields[0],
 424+ name = fieldDesc.name;
 425+
 426+ delete fieldDesc.name;
 427+
 428+ $( this ).dialog( "close" );
 429+
 430+ var ListField = validFieldTypes.list;
 431+
 432+ $.extend( listParams, {
 433+ type: 'list',
 434+ name: name,
 435+ field: fieldDesc
 436+ } );
 437+
 438+ callback( new ListField( listParams, options ) );
 439+ }
 440+ },
 441+ {
 442+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 443+ click: function() {
 444+ $( this ).dialog( "close" );
 445+ callback( null );
 446+ }
 447+ }
 448+ ]
 449+ } );
 450+ }
 451+
 452+ /* Basic interface for fields */
 453+ function Field( desc, options ) {
 454+ if ( typeof options.idPrefix == 'undefined' ) {
 455+ options.idPrefix = 'formbuilder-';
 456+ }
 457+
 458+ this.desc = desc;
 459+ this.options = options;
 460+ }
 461+
 462+ Field.prototype.getDesc = function( useValuesAsDefaults ) {
 463+ return this.desc;
 464+ };
 465+
 466+ //Override expected
 467+ Field.prototype.getValues = function() {
 468+ return {};
 469+ };
 470+
 471+ //Override expected
 472+ Field.prototype.getElement = function() {
 473+ return null;
 474+ };
 475+
 476+ //Override expected
 477+ Field.prototype.getValidationSettings = function() {
 478+ return {
 479+ rules: {},
 480+ messages: {}
 481+ };
 482+ };
 483+
 484+ /*
 485+ * A field with no content, generating an empty container
 486+ * and checking existence and type of the 'type' member of description.
 487+ *
 488+ **/
 489+ function EmptyField( desc, options ) {
 490+ Field.call( this, desc, options );
 491+
 492+ //Check existence and type of the "type" field
 493+ if ( ( !this.desc.type || typeof this.desc.type != 'string' )
 494+ && !$.isFunction( this.desc.type ) )
 495+ {
 496+ $.error( "Missing 'type' parameter" );
 497+ }
 498+
 499+ this.$div = $( '<div/>' )
 500+ .addClass( 'formbuilder-field' )
 501+ .data( 'field', this );
 502+
 503+ if ( !$.isFunction( this.desc.type ) ) {
 504+ this.$div.addClass( 'formbuilder-field-' + this.desc.type );
 505+ }
 506+ }
 507+ inherit( EmptyField, Field );
 508+
 509+ EmptyField.prototype.getElement = function() {
 510+ return this.$div;
 511+ };
 512+
 513+ /* A field with just a label */
 514+ function LabelField( desc, options ) {
 515+ EmptyField.call( this, desc, options );
 516+
 517+ //Check existence and type of the "label" field
 518+ if ( typeof this.desc.label != 'string' ) {
 519+ $.error( "Missing or wrong 'label' parameter" );
 520+ }
 521+
 522+ this.$label = $( '<label/>' )
 523+ .text( preproc( this.options.msgPrefix, this.desc.label ) );
 524+
 525+ this.$div.append( this.$label );
 526+ }
 527+ inherit( LabelField, EmptyField );
 528+
 529+ validFieldTypes.label = LabelField;
 530+
 531+ /* Abstract base class for all "simple" fields. Should not be instantiated. */
 532+ function SimpleField( desc, options ) {
 533+ LabelField.call( this, desc, options );
 534+
 535+ //Validate the 'name' member
 536+ if ( !isValidPreferenceName( desc.name ) ) {
 537+ $.error( 'invalid name' );
 538+ }
 539+
 540+ this.$label.attr('for', this.options.idPrefix + this.desc.name );
 541+
 542+ //Use default if it is given and no value has been set
 543+ if ( ( typeof options.values == 'undefined' || typeof options.values[desc.name] == 'undefined' )
 544+ && typeof desc['default'] != 'undefined' )
 545+ {
 546+ if ( typeof options.values == 'undefined' ) {
 547+ options.values = {};
 548+ }
 549+
 550+ options.values[desc.name] = desc['default'];
 551+
 552+ this.options = options;
 553+ }
 554+ }
 555+ inherit( SimpleField, LabelField );
 556+
 557+ SimpleField.prototype.getDesc = function( useValuesAsDefaults ) {
 558+ var desc = clone( LabelField.prototype.getDesc.call( this, useValuesAsDefaults ) );
 559+ if ( useValuesAsDefaults === true ) {
 560+ //set 'default' to current value.
 561+ var values = this.getValues();
 562+ desc['default'] = values[this.desc.name];
 563+ }
 564+
 565+ return desc;
 566+ };
 567+
 568+
 569+ /* A field with a label and a checkbox */
 570+ function BooleanField( desc, options ) {
 571+ SimpleField.call( this, desc, options );
 572+
 573+ this.$c = $( '<input/>' ).attr( {
 574+ type: 'checkbox',
 575+ id: this.options.idPrefix + this.desc.name,
 576+ name: this.options.idPrefix + this.desc.name
 577+ } );
 578+
 579+ if ( options.change ) {
 580+ this.$c.change( function() {
 581+ options.change();
 582+ } );
 583+ }
 584+
 585+ var value = options.values && options.values[this.desc.name];
 586+ if ( typeof value != 'undefined' ) {
 587+ if ( typeof value != 'boolean' ) {
 588+ $.error( "value is invalid" );
 589+ }
 590+
 591+ this.$c.attr( 'checked', value );
 592+ }
 593+
 594+ this.$div.append( this.$c );
 595+ }
 596+ inherit( BooleanField, SimpleField );
 597+
 598+ BooleanField.prototype.getValues = function() {
 599+ return pair( this.desc.name, this.$c.is( ':checked' ) );
 600+ };
 601+
 602+ validFieldTypes.boolean = BooleanField;
 603+
 604+ /* A field with a textbox accepting string values */
 605+ function StringField( desc, options ) {
 606+ SimpleField.call( this, desc, options );
 607+
 608+ //Validate minlength and maxlength
 609+ var minlength = typeof desc.minlength != 'undefined' ? desc.minlength : 0,
 610+ maxlength = typeof desc.maxlength != 'undefined' ? desc.maxlength : 1024;
 611+
 612+ if ( !isInteger( minlength ) || minlength < 0 ) {
 613+ $.error( "minlength must be a non-negative integer" );
 614+ }
 615+ if ( !isInteger( maxlength ) || maxlength <= 0 ) {
 616+ $.error( "maxlength must be a positive integer" );
 617+ }
 618+ if ( maxlength < minlength ) {
 619+ $.error( "maxlength must be no less than minlength" );
 620+ }
 621+
 622+ this.$text = $( '<input/>' ).attr( {
 623+ type: 'text',
 624+ id: this.options.idPrefix + this.desc.name,
 625+ name: this.options.idPrefix + this.desc.name
 626+ } );
 627+
 628+ var value = options.values && options.values[this.desc.name];
 629+ if ( typeof value != 'undefined' ) {
 630+ if ( typeof value != 'string' ) {
 631+ $.error( "value is invalid" );
 632+ }
 633+
 634+ this.$text.val( value );
 635+ }
 636+
 637+ //Add the change event listener
 638+ if ( options.change ) {
 639+ addSmartChangeListener( this.$text, options.change );
 640+ }
 641+
 642+ this.$div.append( this.$text );
 643+ }
 644+ inherit( StringField, SimpleField );
 645+
 646+ StringField.prototype.getValues = function() {
 647+ return pair( this.desc.name, this.$text.val() );
 648+ };
 649+
 650+ StringField.prototype.getValidationSettings = function() {
 651+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
 652+ fieldId = this.options.idPrefix + this.desc.name;
 653+
 654+ settings.rules[fieldId] = {};
 655+ var fieldRules = settings.rules[fieldId],
 656+ desc = this.desc;
 657+
 658+ if ( desc.required === true ) {
 659+ fieldRules.requiredStrict = true;
 660+ }
 661+
 662+ if ( typeof desc.minlength != 'undefined' ) {
 663+ fieldRules.minlengthStrict = desc.minlength;
 664+ }
 665+ if ( typeof desc.maxlength != 'undefined' ) {
 666+ fieldRules.maxlengthStrict = desc.maxlength;
 667+ }
 668+
 669+ settings.messages = {};
 670+
 671+ settings.messages[fieldId] = {
 672+ "minlengthStrict": mw.msg( 'gadgets-formbuilder-minlength', desc.minlength ),
 673+ "maxlengthStrict": mw.msg( 'gadgets-formbuilder-maxlength', desc.maxlength )
 674+ };
 675+
 676+ return settings;
 677+ };
 678+
 679+ validFieldTypes.string = StringField;
 680+
 681+
 682+ /* A field with a textbox accepting numeric values */
 683+ function NumberField( desc, options ) {
 684+ SimpleField.call( this, desc, options );
 685+
 686+ //Validation of description
 687+ if ( desc.integer === true ) {
 688+ if ( typeof desc.min != 'undefined' && !isInteger( desc.min ) ) {
 689+ $.error( "min is not an integer" );
 690+ }
 691+ if ( typeof desc.max != 'undefined' && !isInteger( desc.max ) ) {
 692+ $.error( "max is not an integer" );
 693+ }
 694+ }
 695+
 696+ if ( typeof desc.min != 'undefined' && typeof desc.max != 'undefined' && desc.min > desc.max ) {
 697+ $.error( 'max must be no less than min' );
 698+ }
 699+
 700+
 701+ this.$text = $( '<input/>' ).attr( {
 702+ type: 'text',
 703+ id: this.options.idPrefix + this.desc.name,
 704+ name: this.options.idPrefix + this.desc.name
 705+ } );
 706+
 707+ var value = options.values && options.values[this.desc.name];
 708+ if ( typeof value != 'undefined' ) {
 709+ if ( value !== null && typeof value != 'number' ) {
 710+ $.error( "value is invalid" );
 711+ }
 712+
 713+ this.$text.val( value );
 714+ }
 715+
 716+ //Add the change event listener
 717+ if ( options.change ) {
 718+ addSmartChangeListener( this.$text, options.change );
 719+ }
 720+
 721+ this.$div.append( this.$text );
 722+ }
 723+ inherit( NumberField, SimpleField );
 724+
 725+ NumberField.prototype.getValues = function() {
 726+ var val = parseFloat( this.$text.val() );
 727+ return pair( this.desc.name, isNaN( val ) ? null : val );
 728+ };
 729+
 730+ NumberField.prototype.getValidationSettings = function() {
 731+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
 732+ fieldId = this.options.idPrefix + this.desc.name;
 733+
 734+ settings.rules[fieldId] = {};
 735+ var fieldRules = settings.rules[fieldId],
 736+ desc = this.desc;
 737+
 738+ if ( desc.required !== false ) {
 739+ fieldRules.requiredStrict = true;
 740+ }
 741+
 742+ if ( desc.integer === true ) {
 743+ fieldRules.integer = true;
 744+ }
 745+
 746+
 747+ if ( typeof desc.min != 'undefined' ) {
 748+ fieldRules.min = desc.min;
 749+ }
 750+ if ( typeof desc.max != 'undefined' ) {
 751+ fieldRules.max = desc.max;
 752+ }
 753+
 754+ settings.messages = {};
 755+
 756+ settings.messages[fieldId] = {
 757+ "required": mw.msg( 'gadgets-formbuilder-required' ),
 758+ "min": mw.msg( 'gadgets-formbuilder-min', desc.min ),
 759+ "max": mw.msg( 'gadgets-formbuilder-max', desc.max )
 760+ };
 761+
 762+ return settings;
 763+ };
 764+
 765+ validFieldTypes.number = NumberField;
 766+
 767+ /* A field with a drop-down list */
 768+ function SelectField( desc, options ) {
 769+ SimpleField.call( this, desc, options );
 770+
 771+ var $select = this.$select = $( '<select/>' ).attr( {
 772+ id: this.options.idPrefix + this.desc.name,
 773+ name: this.options.idPrefix + this.desc.name
 774+ } );
 775+
 776+ var validValues = [],
 777+ self = this;
 778+ $.each( this.desc.options, function( idx, option ) {
 779+ var i = validValues.length;
 780+ $( '<option/>' )
 781+ .text( preproc( self.options.msgPrefix, option.name ) )
 782+ .val( i )
 783+ .appendTo( $select );
 784+ validValues.push( option.value );
 785+ } );
 786+
 787+ this.validValues = validValues;
 788+
 789+ var value = options.values && options.values[this.desc.name];
 790+ if ( typeof value != 'undefined' ) {
 791+ if ( $.inArray( value, validValues ) == -1 ) {
 792+ $.error( "value is not in the list of possible values" );
 793+ }
 794+
 795+ var i = $.inArray( value, validValues );
 796+ $select.val( i ).prop( 'selected', 'selected' );
 797+ }
 798+
 799+ //Add the change event listener
 800+ if ( options.change ) {
 801+ $select.change( function() {
 802+ options.change();
 803+ } );
 804+ }
 805+
 806+ this.$div.append( $select );
 807+ }
 808+ inherit( SelectField, SimpleField );
 809+
 810+ SelectField.prototype.getValues = function() {
 811+ var i = parseInt( this.$select.val(), 10 );
 812+ return pair( this.desc.name, this.validValues[i] );
 813+ };
 814+
 815+ validFieldTypes.select = SelectField;
 816+
 817+ /* A field with a slider, representing ranges of numbers */
 818+ function RangeField( desc, options ) {
 819+ SimpleField.call( this, desc, options );
 820+
 821+ //Validation
 822+ if ( desc.min > desc.max ) {
 823+ $.error( "max must be no less than min" );
 824+ }
 825+ if ( desc.step <= 0 ) {
 826+ $.error( "step must be a positive number" );
 827+ }
 828+
 829+ //Check that max differs from min by an integer multiple of step
 830+ //(that is: (max - min) / step is integer, with good approximation)
 831+ var eps = 1.0e-6; //tolerance
 832+ var tmp = ( desc.max - desc.min ) / desc.step;
 833+ if ( Math.abs( tmp - Math.floor( tmp ) ) > eps ) {
 834+ $.error( "The list {min, min + step, min + 2*step, ...} must contain max" );
 835+ }
 836+
 837+ var value = options.values && options.values[this.desc.name];
 838+ if ( typeof value != 'undefined' ) {
 839+ if ( typeof value != 'number' ) {
 840+ $.error( "value is invalid" );
 841+ }
 842+ if ( value < this.desc.min || value > this.desc.max ) {
 843+ $.error( "value is out of range" );
 844+ }
 845+ }
 846+
 847+ var $slider = this.$slider = $( '<div/>' )
 848+ .attr( 'id', this.options.idPrefix + this.desc.name );
 849+
 850+ var sliderOptions = {
 851+ min: this.desc.min,
 852+ max: this.desc.max
 853+ };
 854+
 855+ if ( typeof value != 'undefined' ) {
 856+ sliderOptions.value = value;
 857+ }
 858+
 859+ if ( typeof this.desc.step != 'undefined' ) {
 860+ sliderOptions.step = this.desc.step;
 861+ }
 862+
 863+ //A tooltip to show current value.
 864+ //TODO: use jQuery UI tooltips when they are released (in 1.9)
 865+ var $tooltip = $( '<div/>' )
 866+ .addClass( 'formBuilder-slider-tooltip ui-widget ui-corner-all ui-widget-content' )
 867+ .css( {
 868+ position: 'absolute',
 869+ display: 'none'
 870+ } )
 871+ .appendTo( this.$div );
 872+
 873+ var tooltipShown = false, sliding = false, mouseOver = false;
 874+
 875+ function refreshTooltip( visible, handle, value ) {
 876+ if ( !tooltipShown && visible ) {
 877+ $tooltip.fadeIn( 'fast' );
 878+ tooltipShown = true;
 879+ } else if ( tooltipShown && !visible ) {
 880+ $tooltip.fadeOut( 'fast' );
 881+ tooltipShown = false;
 882+ }
 883+
 884+ $tooltip
 885+ .zIndex( $( handle ).parent().zIndex() + 1 )
 886+ .text( value )
 887+ .position( {
 888+ my: "bottom",
 889+ at: "top",
 890+ of: handle
 891+ } );
 892+ }
 893+
 894+ $.extend( sliderOptions, {
 895+ start: function( event, ui ) {
 896+ sliding = true;
 897+ },
 898+ slide: function( event, ui ) {
 899+ //Deferring to allow the widget to refresh his position
 900+ setTimeout( function() {
 901+ refreshTooltip( true, $slider.find( '.ui-slider-handle' ), ui.value );
 902+ }, 1 );
 903+ },
 904+ stop: function( event, ui ) {
 905+ //After a delay, hide tooltip if the handle doesn't have focus and pointer isn't over the handle.
 906+ setTimeout( function() {
 907+ if ( !$slider.find( '.ui-slider-handle' ).is( ':focus' ) && !mouseOver ) {
 908+ refreshTooltip( false, $slider.find( '.ui-slider-handle' ), ui.value );
 909+ }
 910+ }, 300 );
 911+
 912+ sliding = false;
 913+ },
 914+ change: function( event, ui ) {
 915+ if ( options.change ) {
 916+ options.change();
 917+ }
 918+ }
 919+ } );
 920+
 921+ $slider.slider( sliderOptions );
 922+
 923+ var $handle = $slider.find( '.ui-slider-handle' )
 924+ .focus( function( event ) {
 925+ refreshTooltip( true, $handle, $slider.slider( 'value' ) );
 926+ } )
 927+ .blur( function( event ) {
 928+ refreshTooltip( false, $handle, $slider.slider( 'value' ) );
 929+ } )
 930+ .mouseenter( function( event ) {
 931+ mouseOver = true;
 932+ refreshTooltip( true, $handle, $slider.slider( 'value' ) );
 933+ } )
 934+ .mouseleave( function( event ) {
 935+ setTimeout( function() {
 936+ if ( !$handle.is( ':focus' ) && !sliding ) {
 937+ refreshTooltip( false, $handle, $slider.slider( 'value' ) );
 938+ }
 939+ }, 1 );
 940+ mouseOver = false;
 941+ } );
 942+
 943+ this.$div.append( $slider );
 944+ }
 945+ inherit( RangeField, SimpleField );
 946+
 947+ RangeField.prototype.getValues = function() {
 948+ return pair( this.desc.name, this.$slider.slider( 'value' ) );
 949+ };
 950+
 951+ validFieldTypes.range = RangeField;
 952+
 953+ /* A field with a textbox with a datepicker */
 954+ function DateField( desc, options ) {
 955+ SimpleField.call( this, desc, options );
 956+
 957+ var $text = this.$text = $( '<input/>' )
 958+ .attr( {
 959+ type: 'text',
 960+ id: this.options.idPrefix + this.desc.name,
 961+ name: this.options.idPrefix + this.desc.name
 962+ } ).datepicker( {
 963+ onSelect: function() {
 964+ //Force validation, so that a previous 'invalid' state is removed
 965+ $( this ).valid();
 966+ //trigger change event on the textbox
 967+ $text.trigger( 'change' );
 968+ }
 969+ } );
 970+
 971+ var value = options.values && options.values[this.desc.name],
 972+ date;
 973+ if ( typeof value != 'undefined' && value !== null ) {
 974+ date = this._parseDate( value );
 975+
 976+ if ( !isFinite( date ) ) {
 977+ $.error( "value is invalid" );
 978+ }
 979+
 980+ this.$text.datepicker( 'setDate', date );
 981+ }
 982+
 983+ //Add the change event listener
 984+ if ( options.change ) {
 985+ addSmartChangeListener( this.$text, options.change );
 986+ }
 987+
 988+ this.$div.append( this.$text );
 989+ }
 990+ inherit( DateField, SimpleField );
 991+
 992+ //Parses a date in the [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z format, returns a date object
 993+ //Used to avoid the "new Date( dateString )" constructor, which is implementation-specific.
 994+ DateField.prototype._parseDate = function( str ) {
 995+ var date,
 996+ parts = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec( str );
 997+
 998+ if ( parts === null ) {
 999+ return new Date( NaN );
 1000+ }
 1001+
 1002+ var year = parseInt( parts[1], 10 ),
 1003+ month = parseInt( parts[2], 10 ) - 1,
 1004+ day = parseInt( parts[3], 10 ),
 1005+ h = parseInt( parts[4], 10 ),
 1006+ m = parseInt( parts[5], 10 ),
 1007+ s = parseInt( parts[6], 10 );
 1008+
 1009+ date = new Date();
 1010+ date.setUTCFullYear( year, month, day );
 1011+ date.setUTCHours( h );
 1012+ date.setUTCMinutes( m );
 1013+ date.setUTCSeconds( s );
 1014+
 1015+ //Check if the date was actually correct, since the date handling functions may wrap around invalid dates
 1016+ if ( date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day ||
 1017+ date.getUTCHours() !== h || date.getUTCMinutes() !== m || date.getUTCSeconds() !== s )
 1018+ {
 1019+ return new Date( NaN );
 1020+ }
 1021+
 1022+ return date;
 1023+ };
 1024+
 1025+ DateField.prototype.getValues = function() {
 1026+ var d = this.$text.datepicker( 'getDate' ),
 1027+ res = {};
 1028+
 1029+ if ( d === null ) {
 1030+ return pair( this.desc.name, null );
 1031+ }
 1032+
 1033+ //UTC date in ISO 8601 format [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]Z
 1034+ return pair( this.desc.name, '' +
 1035+ pad( d.getUTCFullYear(), 4 ) + '-' +
 1036+ pad( d.getUTCMonth() + 1, 2 ) + '-' +
 1037+ pad( d.getUTCDate(), 2 ) + 'T' +
 1038+ pad( d.getUTCHours(), 2 ) + ':' +
 1039+ pad( d.getUTCMinutes(), 2 ) + ':' +
 1040+ pad( d.getUTCSeconds(), 2 ) + 'Z' );
 1041+ };
 1042+
 1043+ DateField.prototype.getValidationSettings = function() {
 1044+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
 1045+ fieldId = this.options.idPrefix + this.desc.name;
 1046+
 1047+ settings.rules[fieldId] = {
 1048+ "datePicker": true
 1049+ };
 1050+ return settings;
 1051+ };
 1052+
 1053+ validFieldTypes.date = DateField;
 1054+
 1055+ /* A field with color picker */
 1056+
 1057+ function closeColorPicker() {
 1058+ $( '#colorpicker' ).fadeOut( 'fast', function() {
 1059+ $( this ).remove();
 1060+ } );
 1061+ }
 1062+
 1063+ //If a click happens outside the colorpicker while it is showed, remove it
 1064+ $( document ).mousedown( function( event ) {
 1065+ var $target = $( event.target );
 1066+ if ( $target.parents( '#colorpicker' ).length === 0 ) {
 1067+ closeColorPicker();
 1068+ }
 1069+ } );
 1070+
 1071+ function ColorField( desc, options ) {
 1072+ SimpleField.call( this, desc, options );
 1073+
 1074+ var value;
 1075+ if ( typeof options.values != 'undefined' && typeof options.values[this.desc.name] != 'undefined' ) {
 1076+ value = options.values[this.desc.name];
 1077+ } else {
 1078+ value = '';
 1079+ }
 1080+
 1081+ this.$text = $( '<input/>' ).attr( {
 1082+ type: 'text',
 1083+ id: this.options.idPrefix + this.desc.name,
 1084+ name: this.options.idPrefix + this.desc.name
 1085+ } )
 1086+ .addClass( 'colorpicker-input' )
 1087+ .val( value )
 1088+ .css( 'background-color', value )
 1089+ .focus( function() {
 1090+ $( '<div/>' )
 1091+ .addClass( 'ui-widget ui-widget-content' )
 1092+ .attr( 'id', 'colorpicker' )
 1093+ .css( 'position', 'absolute' )
 1094+ .hide()
 1095+ .appendTo( document.body )
 1096+ .zIndex( $( this ).zIndex() + 1 )
 1097+ .farbtastic( this )
 1098+ .position( {
 1099+ my: 'left bottom',
 1100+ at: 'left top',
 1101+ of: this,
 1102+ collision: 'none'
 1103+ } )
 1104+ .fadeIn( 'fast' );
 1105+ } )
 1106+ .keydown( function( event ) {
 1107+ if ( event.keyCode == 13 || event.keyCode == 27 ) {
 1108+ closeColorPicker();
 1109+ event.preventDefault();
 1110+ event.stopPropagation();
 1111+ }
 1112+ } )
 1113+ .change( function() {
 1114+ //Force validation
 1115+ $( this ).valid();
 1116+ } )
 1117+ .blur( closeColorPicker );
 1118+
 1119+ //Add the change event listener
 1120+ if ( options.change ) {
 1121+ addSmartChangeListener( this.$text, options.change );
 1122+ }
 1123+
 1124+ this.$div.append( this.$text );
 1125+ }
 1126+ inherit( ColorField, SimpleField );
 1127+
 1128+ ColorField.prototype.getValidationSettings = function() {
 1129+ var settings = SimpleField.prototype.getValidationSettings.call( this ),
 1130+ fieldId = this.options.idPrefix + this.desc.name;
 1131+
 1132+ settings.rules[fieldId] = {
 1133+ "color": true
 1134+ };
 1135+ return settings;
 1136+ };
 1137+
 1138+ ColorField.prototype.getValues = function() {
 1139+ var color = $.colorUtil.getRGB( this.$text.val() );
 1140+ if ( color ) {
 1141+ return pair( this.desc.name, '#' + pad( color[0].toString( 16 ), 2 ) +
 1142+ pad( color[1].toString( 16 ), 2 ) + pad( color[2].toString( 16 ), 2 ) );
 1143+ } else {
 1144+ return pair( this.desc.name, null );
 1145+ }
 1146+ };
 1147+
 1148+ validFieldTypes.color = ColorField;
 1149+
 1150+
 1151+ /* A field that represent a section (group of fields) */
 1152+
 1153+ function SectionField( desc, options, id ) {
 1154+ Field.call( this, desc, options );
 1155+
 1156+ this.$div = $( '<div/>' ).data( 'field', this );
 1157+
 1158+ if ( id !== undefined ) {
 1159+ this.$div.attr( 'id', id );
 1160+ }
 1161+
 1162+ for ( var i = 0; i < this.desc.fields.length; i++ ) {
 1163+ if ( options.editable === true && !options.staticFields ) {
 1164+ //add an empty slot
 1165+ this._createSlot( 'yes' ).appendTo( this.$div );
 1166+ }
 1167+
 1168+ var field = this.desc.fields[i],
 1169+ FieldConstructor;
 1170+
 1171+ if ( $.isFunction( field.type ) ) {
 1172+ FieldConstructor = field.type;
 1173+ } else {
 1174+ FieldConstructor = validFieldTypes[field.type];
 1175+ }
 1176+
 1177+ if ( !$.isFunction( FieldConstructor ) ) {
 1178+ $.error( "field with invalid type: " + field.type );
 1179+ }
 1180+
 1181+ var editable;
 1182+ if ( options.editable === true ) {
 1183+ editable = options.staticFields ? 'partial' : 'yes';
 1184+ } else {
 1185+ editable = 'no';
 1186+ }
 1187+
 1188+ var f = new FieldConstructor( field, options ),
 1189+ $slot = this._createSlot( editable, f );
 1190+
 1191+ $slot.appendTo( this.$div );
 1192+ }
 1193+
 1194+ if ( options.editable === true && !options.staticFields ) {
 1195+ //add an empty slot
 1196+ this._createSlot( 'yes' ).appendTo( this.$div );
 1197+ }
 1198+ }
 1199+ inherit( SectionField, Field );
 1200+
 1201+ SectionField.prototype.getElement = function() {
 1202+ return this.$div;
 1203+ };
 1204+
 1205+ SectionField.prototype.getDesc = function( useValuesAsDefaults ) {
 1206+ var desc = clone( this.desc );
 1207+ desc.fields = [];
 1208+ this.$div.children().each( function( idx, slot ) {
 1209+ var field = $( slot ).data( 'field' );
 1210+ if ( field !== undefined ) {
 1211+ desc.fields.push( field.getDesc( useValuesAsDefaults ) );
 1212+ }
 1213+ } );
 1214+ return desc;
 1215+ };
 1216+
 1217+ SectionField.prototype.setTitle = function( newTitle ) {
 1218+ this.desc.title = newTitle;
 1219+ };
 1220+
 1221+ SectionField.prototype.getValues = function() {
 1222+ var values = {};
 1223+ this.$div.children().each( function( idx, slot ) {
 1224+ var field = $( slot ).data( 'field' );
 1225+ if ( field !== undefined ) {
 1226+ $.extend( values, field.getValues() );
 1227+ }
 1228+ } );
 1229+ return values;
 1230+ };
 1231+
 1232+ SectionField.prototype.getValidationSettings = function() {
 1233+ var settings = {};
 1234+ this.$div.children().each( function( idx, slot ) {
 1235+ var field = $( slot ).data( 'field' );
 1236+ if ( field !== undefined ) {
 1237+ var fieldSettings = $( slot ).data( 'field' ).getValidationSettings();
 1238+ if ( fieldSettings ) {
 1239+ $.extend( true, settings, fieldSettings );
 1240+ }
 1241+ }
 1242+ } );
 1243+
 1244+ return settings;
 1245+ };
 1246+
 1247+ SectionField.prototype._deleteSlot = function( $slot ) {
 1248+ var field = $slot.data( 'field' );
 1249+ if ( field !== undefined ) {
 1250+ //Slot with a field
 1251+ deleteFieldRules( field );
 1252+ }
 1253+
 1254+ //Delete it
 1255+ $slot.remove();
 1256+ };
 1257+
 1258+ SectionField.prototype._createSlot = function( editable, field ) {
 1259+ var self = this,
 1260+ $slot = $( '<div/>' ).addClass( 'formbuilder-slot ui-widget' ),
 1261+ $divButtons;
 1262+
 1263+ if ( editable == 'partial' || editable == 'yes' ) {
 1264+ $slot.addClass( 'formbuilder-slot-editable' );
 1265+
 1266+ $divButtons = $( '<div/>' )
 1267+ .addClass( 'formbuilder-editor-slot-buttons' )
 1268+ .appendTo( $slot );
 1269+ }
 1270+
 1271+ if ( typeof field != 'undefined' ) {
 1272+ //Nonempty slot
 1273+ $slot.prepend( field.getElement() )
 1274+ .data( 'field', field );
 1275+
 1276+ if ( editable == 'partial' || editable == 'yes' ) {
 1277+ $slot.addClass( 'formbuilder-slot-nonempty' );
 1278+
 1279+ if ( editable == 'yes' ) {
 1280+ //Add the handle for moving slots
 1281+ $( '<span />' )
 1282+ .addClass( 'formbuilder-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' )
 1283+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
 1284+ .mousedown( function() {
 1285+ $( this ).focus();
 1286+ } )
 1287+ .appendTo( $divButtons );
 1288+ }
 1289+
 1290+ //Add the button for changing existing slots
 1291+ var type = field.getDesc().type;
 1292+ //TODO: using the 'builder' info is not optimal
 1293+ if ( !$.isFunction( prefsSpecifications[type].builder ) ) {
 1294+ $( '<a href="javascript:;" />' )
 1295+ .addClass( 'formbuilder-button formbuilder-editor-button-edit ui-icon ui-icon-gear' )
 1296+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) )
 1297+ .click( function() {
 1298+ createFieldDialog( {
 1299+ type: field.getDesc().type,
 1300+ values: field.getDesc(),
 1301+ oldDescription: field.getDesc(),
 1302+ callback: function( newField ) {
 1303+ if ( newField !== null ) {
 1304+ //check that there are no duplicate preference names
 1305+ var existingValues = self.$div.closest( '.formbuilder' ).formBuilder( 'getValues' ),
 1306+ removedValues = field.getValues(),
 1307+ duplicateName = null;
 1308+ $.each( field.getValues(), function( name, val ) {
 1309+ //Only complain for preference names that are not in names for the field being replaced
 1310+ if ( typeof existingValues[name] != 'undefined' && removedValues[name] == 'undefined' ) {
 1311+ duplicateName = name;
 1312+ return false;
 1313+ }
 1314+ } );
 1315+
 1316+ if ( duplicateName !== null ) {
 1317+ alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name', duplicateName ) );
 1318+ return false;
 1319+ }
 1320+
 1321+ var $newSlot = self._createSlot( 'yes', newField );
 1322+
 1323+ deleteFieldRules( field );
 1324+
 1325+ $slot.replaceWith( $newSlot );
 1326+
 1327+ //Add field's validation rules
 1328+ addFieldRules( newField );
 1329+ }
 1330+ return true;
 1331+ }
 1332+ }, this.options );
 1333+ } )
 1334+ .appendTo( $divButtons );
 1335+ }
 1336+
 1337+ if ( editable == 'yes' ) {
 1338+ //Add the button to delete slots
 1339+ $( '<a href="javascript:;" />' )
 1340+ .addClass( 'formbuilder-button formbuilder-editor-button-delete ui-icon ui-icon-trash' )
 1341+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) )
 1342+ .click( function( event, ui ) {
 1343+ //Make both slots disappear, then delete them
 1344+ $.each( [$slot, $slot.prev()], function( idx, $s ) {
 1345+ $s.slideUp( function() {
 1346+ self._deleteSlot( $s );
 1347+ } );
 1348+ } );
 1349+ } )
 1350+ .appendTo( $divButtons );
 1351+
 1352+ //Make this slot draggable to allow moving it
 1353+ $slot.draggable( {
 1354+ revert: true,
 1355+ handle: ".formbuilder-editor-button-move",
 1356+ helper: "original",
 1357+ zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way
 1358+ scroll: false,
 1359+ opacity: 0.8,
 1360+ cursor: "move",
 1361+ cursorAt: {
 1362+ top: -5,
 1363+ left: -5
 1364+ }
 1365+ } );
 1366+ }
 1367+ }
 1368+ } else {
 1369+ //Create empty slot
 1370+ $slot.addClass( 'formbuilder-slot-empty' )
 1371+ .droppable( {
 1372+ hoverClass: 'formbuilder-slot-can-drop',
 1373+ tolerance: 'pointer',
 1374+ drop: function( event, ui ) {
 1375+ var srcSlot = ui.draggable,
 1376+ dstSlot = this;
 1377+
 1378+ //Remove one empty slot surrounding source
 1379+ $( srcSlot ).prev().remove();
 1380+
 1381+ //Replace dstSlot with srcSlot:
 1382+ $( dstSlot ).replaceWith( srcSlot );
 1383+
 1384+ //Add one empty slot before and one after the new position
 1385+ self._createSlot( 'yes' ).insertBefore( srcSlot );
 1386+ self._createSlot( 'yes' ).insertAfter( srcSlot );
 1387+ },
 1388+ accept: function( draggable ) {
 1389+ //All non empty slots accepted, except for closest siblings
 1390+ return $( draggable ).hasClass( 'formbuilder-slot-nonempty' ) &&
 1391+ $( draggable ).prev().get( 0 ) !== $slot.get( 0 ) &&
 1392+ $( draggable ).next().get( 0 ) !== $slot.get( 0 );
 1393+ }
 1394+ } );
 1395+
 1396+ //The button to create a new field
 1397+ $( '<a href="javascript:;" />' )
 1398+ .addClass( 'formbuilder-button formbuilder-editor-button-new ui-icon ui-icon-plus' )
 1399+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) )
 1400+ .click( function() {
 1401+ createFieldDialog( {
 1402+ callback: function( field ) {
 1403+ if ( field !== null ) {
 1404+ //check that there are no duplicate preference names
 1405+ var existingValues = $slot.closest( '.formbuilder' ).formBuilder( 'getValues' ),
 1406+ duplicateName = null;
 1407+ $.each( field.getValues(), function( name, val ) {
 1408+ if ( typeof existingValues[name] != 'undefined' ) {
 1409+ duplicateName = name;
 1410+ return false;
 1411+ }
 1412+ } );
 1413+
 1414+ if ( duplicateName !== null ) {
 1415+ alert( mw.msg( 'gadgets-formbuilder-editor-duplicate-name' , duplicateName ) );
 1416+ return false;
 1417+ }
 1418+
 1419+ var $newSlot = self._createSlot( 'yes', field ).hide(),
 1420+ $newEmptySlot = self._createSlot( 'yes' ).hide();
 1421+
 1422+ $slot.after( $newSlot, $newEmptySlot );
 1423+
 1424+ $newSlot.slideDown();
 1425+ $newEmptySlot.slideDown();
 1426+
 1427+ //Add field's validation rules
 1428+ addFieldRules( field );
 1429+
 1430+ //Ensure immediate visual feedback if the current value is invalid
 1431+ self.$div.closest( '.formbuilder' ).formBuilder( 'validate' );
 1432+ }
 1433+ return true;
 1434+ }
 1435+ }, self.options );
 1436+ } )
 1437+ .appendTo( $divButtons );
 1438+ }
 1439+
 1440+ return $slot;
 1441+ };
 1442+
 1443+
 1444+ /* A field for 'bundle' type fields */
 1445+ function BundleField( desc, options ) {
 1446+ EmptyField.call( this, desc, options );
 1447+
 1448+ //Create tabs
 1449+ var $tabs = this.$tabs = $( '<div><ul></ul></div>' )
 1450+ .attr( 'id', this.options.idPrefix + 'tab-' + getIncrementalCounter() )
 1451+ .tabs( {
 1452+ add: function( event, ui ) {
 1453+ //Links the anchor to the panel
 1454+ $( ui.tab ).data( 'panel', ui.panel );
 1455+
 1456+ //Allow to drop over tabs to move slots around
 1457+ var section = ui.panel;
 1458+ $( ui.tab ).droppable( {
 1459+ tolerance: 'pointer',
 1460+ accept: '.formbuilder-slot-nonempty',
 1461+ drop: function( event, ui ) {
 1462+ var $slot = $( ui.draggable ),
 1463+ $srcSection = $slot.parent(),
 1464+ $dstSection = $( section );
 1465+
 1466+ if ( $dstSection.get( 0 ) !== $srcSection.get( 0 ) ) {
 1467+ //move the slot (and the next empty slot) to dstSection with a nice animation
 1468+ var $slots = $slot.add( $slot.next() );
 1469+ $slots.slideUp( 'fast' )
 1470+ .promise().done( function() {
 1471+ $tabs.tabs( 'select', '#' + $dstSection.attr( 'id' ) );
 1472+ $slots.detach()
 1473+ .appendTo( $dstSection )
 1474+ .slideDown( 'fast' );
 1475+ } );
 1476+ }
 1477+ }
 1478+ } );
 1479+
 1480+ if ( options.editable === true ) {
 1481+ //Add "delete section" button
 1482+ $( '<span />' )
 1483+ .addClass( 'formbuilder-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' )
 1484+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-section' ) )
 1485+ .click( function() {
 1486+ var sectionField = $( ui.panel ).data( 'field' );
 1487+ deleteFieldRules( sectionField );
 1488+
 1489+ var index = $( "li", $tabs ).index( $( this ).closest( "li" ) );
 1490+ index -= 1; //Don't count the "add section" button
 1491+
 1492+ $tabs.tabs( 'remove', index );
 1493+ } )
 1494+ .appendTo( ui.tab );
 1495+
 1496+ //Add "edit section" button
 1497+ $( '<span />' )
 1498+ .addClass( 'formbuilder-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' )
 1499+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-section' ) )
 1500+ .click( function() {
 1501+ var button = this,
 1502+ sectionField = $( ui.panel ).data( 'field' );
 1503+
 1504+ $( {
 1505+ fields: [ {
 1506+ 'name': "title",
 1507+ 'type': "string",
 1508+ 'label': mw.msg( 'gadgets-formbuilder-editor-choose-title' )
 1509+ } ]
 1510+ } ).formBuilder( {
 1511+ values: {
 1512+ title: sectionField.getDesc().title
 1513+ },
 1514+ idPrefix: 'section-edit-title-'
 1515+ } ).dialog( {
 1516+ modal: true,
 1517+ resizable: false,
 1518+ title: mw.msg( 'gadgets-formbuilder-editor-choose-title-title' ),
 1519+ close: function() {
 1520+ $( this ).remove(); //completely destroy on close
 1521+ },
 1522+ buttons: [
 1523+ {
 1524+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 1525+ click: function() {
 1526+ var title = $( this ).formBuilder( 'getValues' ).title;
 1527+
 1528+ //Update field description
 1529+ sectionField.setTitle( title );
 1530+
 1531+ //Update tab's title
 1532+ $( button ).parent().find( ':first-child' ).text( title );
 1533+
 1534+ $( this ).dialog( "close" );
 1535+ }
 1536+ },
 1537+ {
 1538+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 1539+ click: function() {
 1540+ $( this ).dialog( "close" );
 1541+ }
 1542+ }
 1543+ ]
 1544+ } );
 1545+ } )
 1546+ .appendTo( ui.tab );
 1547+ }
 1548+ }
 1549+ } );
 1550+
 1551+ //Save for future reference
 1552+ this.$ui_tabs_nav = $tabs.find( '.ui-tabs-nav' );
 1553+
 1554+ var self = this;
 1555+ $.each( this.desc.sections, function( index, sectionDescription ) {
 1556+ var id = self.options.idPrefix + 'section-' + getIncrementalCounter(),
 1557+ sec = new SectionField( sectionDescription, options, id );
 1558+
 1559+ $tabs.append( sec.getElement() )
 1560+ .tabs( 'add', '#' + id, preproc( options.msgPrefix, sectionDescription.title ) );
 1561+ } );
 1562+
 1563+ if ( options.editable === true ) {
 1564+ //Add the button to create a new section
 1565+ $( '<span>' )
 1566+ .addClass( 'formbuilder-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' )
 1567+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-new-section' ) )
 1568+ .click( function() {
 1569+ $( {
 1570+ fields: [ {
 1571+ 'name': "title",
 1572+ 'type': "string",
 1573+ 'label': mw.msg( 'gadgets-formbuilder-editor-choose-title' )
 1574+ } ]
 1575+ } ).formBuilder( { idPrefix: 'section-create-' } ).dialog( {
 1576+ modal: true,
 1577+ resizable: false,
 1578+ title: mw.msg( 'gadgets-formbuilder-editor-choose-title-title' ),
 1579+ close: function() {
 1580+ $( this ).remove();
 1581+ },
 1582+ buttons: [
 1583+ {
 1584+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 1585+ click: function() {
 1586+ var title = $( this ).formBuilder( 'getValues' ).title,
 1587+ id = self.options.idPrefix + 'section-' + getIncrementalCounter(),
 1588+ newSectionDescription = {
 1589+ title: title,
 1590+ fields: []
 1591+ },
 1592+ newSection = new SectionField( newSectionDescription, options, id );
 1593+
 1594+ $tabs.append( newSection.getElement() )
 1595+ .tabs( 'add', '#' + id, preproc( options.msgPrefix, title ) );
 1596+
 1597+ $( this ).dialog( "close" );
 1598+ }
 1599+ },
 1600+ {
 1601+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 1602+ click: function() {
 1603+ $( this ).dialog( "close" );
 1604+ }
 1605+ }
 1606+ ]
 1607+ } );
 1608+ } )
 1609+ .wrap( '<li />' ).parent()
 1610+ .prependTo( this.$ui_tabs_nav );
 1611+
 1612+ //Make the tabs sortable
 1613+ this.$ui_tabs_nav.sortable( {
 1614+ axis: 'x',
 1615+ items: 'li:not(:has(.formbuilder-editor-button-new-section))'
 1616+ } );
 1617+ }
 1618+
 1619+ this.$div.append( $tabs );
 1620+ }
 1621+ inherit( BundleField, EmptyField );
 1622+
 1623+ BundleField.prototype.getValidationSettings = function() {
 1624+ var settings = {};
 1625+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
 1626+ var panel = $( anchor ).data( 'panel' ),
 1627+ field = $( panel ).data( 'field' );
 1628+
 1629+ $.extend( true, settings, field.getValidationSettings() );
 1630+ } );
 1631+ return settings;
 1632+ };
 1633+
 1634+ BundleField.prototype.getDesc = function( useValuesAsDefaults ) {
 1635+ var desc = clone( this.desc );
 1636+ desc.sections = [];
 1637+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
 1638+ var panel = $( anchor ).data( 'panel' ),
 1639+ field = $( panel ).data( 'field' );
 1640+
 1641+ desc.sections.push( field.getDesc( useValuesAsDefaults ) );
 1642+ } );
 1643+ return desc;
 1644+ };
 1645+
 1646+ BundleField.prototype.getValues = function() {
 1647+ var values = {};
 1648+ this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) {
 1649+ var panel = $( anchor ).data( 'panel' ),
 1650+ field = $( panel ).data( 'field' );
 1651+
 1652+ $.extend( values, field.getValues() );
 1653+ } );
 1654+ return values;
 1655+ };
 1656+
 1657+ validFieldTypes.bundle = BundleField;
 1658+
 1659+
 1660+ /* A field for 'composite' fields */
 1661+
 1662+ function CompositeField( desc, options ) {
 1663+ EmptyField.call( this, desc, options );
 1664+
 1665+ //Validate the 'name' member
 1666+ if ( !isValidPreferenceName( desc.name ) ) {
 1667+ $.error( 'invalid name' );
 1668+ }
 1669+
 1670+ if ( !$.isArray( desc.fields ) ) {
 1671+ //Don't throw an error, to allow creating empty sections in the editor
 1672+ desc.fields = [];
 1673+ }
 1674+
 1675+ //TODO: add something to easily visually identify 'composite' fields during editing
 1676+
 1677+ var sectionOptions = clone( options );
 1678+
 1679+ //Add another chunk to the prefix, to ensure uniqueness
 1680+ sectionOptions.idPrefix += desc.name + '-';
 1681+ if ( typeof options.values != 'undefined' ) {
 1682+ //Tell the section the actual values it should show
 1683+ sectionOptions.values = options.values[desc.name];
 1684+ }
 1685+
 1686+ this._section = new SectionField( desc, sectionOptions );
 1687+ this.$div.append( this._section.getElement() );
 1688+ }
 1689+ inherit( CompositeField, EmptyField );
 1690+
 1691+ CompositeField.prototype.getDesc = function( useValuesAsDefaults ) {
 1692+ var desc = clone( this.desc );
 1693+ desc.fields = this._section.getDesc( useValuesAsDefaults ).fields;
 1694+ return desc;
 1695+ };
 1696+
 1697+ CompositeField.prototype.getValues = function() {
 1698+ return pair( this.desc.name, this._section.getValues() );
 1699+ };
 1700+
 1701+ CompositeField.prototype.getValidationSettings = function() {
 1702+ return this._section.getValidationSettings();
 1703+ };
 1704+
 1705+ validFieldTypes.composite = CompositeField;
 1706+
 1707+ /* A field for 'list' fields */
 1708+
 1709+ function ListField( desc, options ) {
 1710+ EmptyField.call( this, desc, options );
 1711+
 1712+ if ( typeof desc.field != 'object' ) {
 1713+ $.error( "The 'field' parameter is missing or wrong" );
 1714+ }
 1715+
 1716+ if ( typeof desc.field.name != 'undefined' ) {
 1717+ $.error( "The 'field' parameter must not specify the field 'name'" );
 1718+ }
 1719+
 1720+ if ( ( typeof desc.field.type != 'string' )
 1721+ || prefsSpecifications[desc.field.type].simple !== true )
 1722+ {
 1723+ $.error( "Missing or invalid field type specified in 'field' parameter." );
 1724+ }
 1725+
 1726+ this._$divItems = $( '<div/>' ).addClass( 'formbuilder-list-items' );
 1727+
 1728+ if ( typeof options.values == 'undefined' ) {
 1729+ options.values = {};
 1730+ }
 1731+
 1732+ var value = ( typeof options.values[desc.name] != 'undefined' ) ? options.values[desc.name] : desc['default'],
 1733+ self = this;
 1734+ if ( typeof value != 'undefined' ) {
 1735+ $.each( value, function( index, itemValue ) {
 1736+ self._createItem( false, itemValue );
 1737+ } );
 1738+ }
 1739+
 1740+ this._$divItems.sortable( {
 1741+ axis: 'y',
 1742+ items: '.formbuilder-list-item',
 1743+ handle: '.formbuilder-list-button-move',
 1744+ placeholder: 'ui-state-highlight',
 1745+ forcePlaceholderSize: true
 1746+ } )
 1747+ .appendTo( this.$div );
 1748+
 1749+ $( '<a href="javascript:;" />' )
 1750+ .addClass( 'formbuilder-button formbuilder-list-button-new ui-icon ui-icon-plus' )
 1751+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) )
 1752+ .click( function() {
 1753+ self._createItem( true );
 1754+ } )
 1755+ .appendTo( this.$div );
 1756+
 1757+ //Add a hidden input to attach validation rules on the number of items
 1758+ //We set its value to "" if there are no elements, or to the number of items otherwise
 1759+ this._$hiddenInput = $( '<input/>').attr( {
 1760+ type: 'hidden',
 1761+ name: this.options.idPrefix + this.desc.name,
 1762+ id: this.options.idPrefix + this.desc.name
 1763+ } )
 1764+ .hide()
 1765+ .appendTo( this.$div );
 1766+
 1767+ this._refreshHiddenField();
 1768+ }
 1769+ inherit( ListField, EmptyField );
 1770+
 1771+ ListField.prototype._refreshHiddenField = function() {
 1772+ var nItems = this._$divItems.children().length;
 1773+ this._$hiddenInput.val( nItems ? nItems : "" );
 1774+ };
 1775+
 1776+ ListField.prototype._createItem = function( afterInit, itemValue ) {
 1777+ var itemDesc = $.extend( {}, this.desc.field, {
 1778+ "name": this.desc.name
 1779+ } ),
 1780+ itemOptions = $.extend( {}, this.options, {
 1781+ editable: false,
 1782+ idPrefix: this.options.idPrefix + getIncrementalCounter() + "-"
 1783+ } );
 1784+
 1785+ if ( typeof itemValue != 'undefined' ) {
 1786+ itemOptions.values = pair( this.desc.name, itemValue );
 1787+ } else {
 1788+ itemOptions.values = pair( this.desc.name, this.desc.field['default'] );
 1789+ }
 1790+
 1791+ var FieldConstructor;
 1792+ if ( $.isFunction( this.desc.field.type ) ) {
 1793+ FieldConstructor = this.desc.field.type;
 1794+ } else {
 1795+ FieldConstructor = validFieldTypes[this.desc.field.type];
 1796+ }
 1797+
 1798+ var itemField = new FieldConstructor( itemDesc, itemOptions ),
 1799+ $itemDiv = $( '<div/>' )
 1800+ .addClass( 'formbuilder-list-item' )
 1801+ .data( 'field', itemField ),
 1802+ $itemContent = $( '<div/>' )
 1803+ .addClass( 'formbuilder-list-item-content' )
 1804+ .append( itemField.getElement() );
 1805+
 1806+ $( '<div/>' )
 1807+ .addClass( 'formbuilder-list-item-container' )
 1808+ .append( $itemContent )
 1809+ .appendTo( $itemDiv );
 1810+
 1811+ var $itemButtons = $( '<div/>' )
 1812+ .addClass( 'formbuilder-list-item-buttons' );
 1813+
 1814+ var self = this;
 1815+ $( '<span/>' )
 1816+ .addClass( 'formbuilder-button formbuilder-list-button-delete ui-icon ui-icon-trash' )
 1817+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete' ) )
 1818+ .click( function() {
 1819+ $itemDiv.slideUp( function() {
 1820+ deleteFieldRules( itemField );
 1821+ $itemDiv.remove();
 1822+ self._refreshHiddenField();
 1823+ self._$hiddenInput.valid(); //force revalidation of the number of items
 1824+ } );
 1825+ } )
 1826+ .appendTo( $itemButtons );
 1827+
 1828+ $( '<span/>' )
 1829+ .addClass( 'formbuilder-button formbuilder-list-button-move ui-icon ui-icon-arrow-4' )
 1830+ .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) )
 1831+ .appendTo( $itemButtons );
 1832+
 1833+ $itemButtons.appendTo( $itemDiv );
 1834+
 1835+ //Add an empty div with clear:both style
 1836+ $itemDiv.append( $('<div style="clear:both"></div>' ) );
 1837+
 1838+ if ( afterInit ) {
 1839+ $itemDiv.hide()
 1840+ .appendTo( this._$divItems )
 1841+ .slideDown();
 1842+
 1843+ addFieldRules( itemField );
 1844+
 1845+ this._refreshHiddenField();
 1846+ this._$hiddenInput.valid(); //force revalidation of the number of items
 1847+ } else {
 1848+ $itemDiv.appendTo( this._$divItems );
 1849+ }
 1850+ };
 1851+
 1852+ ListField.prototype.getDesc = function( useValuesAsDefaults ) {
 1853+ var desc = clone( this.desc );
 1854+ if ( useValuesAsDefaults ) {
 1855+ desc['default'] = this.getValues()[this.desc.name];
 1856+ }
 1857+ return desc;
 1858+ };
 1859+
 1860+ ListField.prototype.getValues = function() {
 1861+ var value = [];
 1862+ this._$divItems.children().each( function( index, divItem ) {
 1863+ var field = $( divItem ).data( 'field' );
 1864+ $.each( field.getValues(), function( name, v ) {
 1865+ value.push( v );
 1866+ } );
 1867+ } );
 1868+
 1869+ return pair( this.desc.name, value );
 1870+ };
 1871+
 1872+ ListField.prototype.getValidationSettings = function() {
 1873+ var validationSettings = EmptyField.prototype.getValidationSettings.call( this );
 1874+ hiddenFieldRules = {}, hiddenFieldMessages = {};
 1875+
 1876+ if ( typeof this.desc.required != 'undefined' ) {
 1877+ hiddenFieldRules.required = this.desc.required;
 1878+ hiddenFieldMessages.required = mw.msg( 'gadgets-formbuilder-list-required' );
 1879+ }
 1880+
 1881+ if ( typeof this.desc.minlength != 'undefined' ) {
 1882+ hiddenFieldRules.min = this.desc.minlength;
 1883+ hiddenFieldMessages.min = mw.msg( 'gadgets-formbuilder-list-minlength', this.desc.minlength );
 1884+ }
 1885+
 1886+ if ( typeof this.desc.maxlength == 'undefined' ) {
 1887+ this.desc.maxlength = 1024;
 1888+ }
 1889+ hiddenFieldRules.max = this.desc.maxlength;
 1890+ hiddenFieldMessages.max = mw.msg( 'gadgets-formbuilder-list-maxlength', this.desc.maxlength );
 1891+
 1892+ validationSettings.rules[this.options.idPrefix + this.desc.name] = hiddenFieldRules;
 1893+ validationSettings.messages[this.options.idPrefix + this.desc.name] = hiddenFieldMessages;
 1894+
 1895+ this._$divItems.children().each( function( index, divItem ) {
 1896+ var field = $( divItem ).data( 'field' );
 1897+ $.extend( true, validationSettings, field.getValidationSettings() );
 1898+ } );
 1899+ return validationSettings;
 1900+ };
 1901+
 1902+ validFieldTypes.list = ListField;
 1903+
 1904+ /* Fields for internal use only */
 1905+
 1906+ /*
 1907+ * A text field that allow an arbitrary javascript scalar value, that is:
 1908+ * true, false, a number, a (double quoted) string or null.
 1909+ *
 1910+ * Used to create editor's select options.
 1911+ *
 1912+ **/
 1913+ function ScalarField( desc, options ) {
 1914+ LabelField.call( this, desc, options );
 1915+
 1916+ this.$div.addClass( 'formbuilder-field-scalar' );
 1917+
 1918+ this.$text = $( '<input/>' )
 1919+ .attr( {
 1920+ type: 'text',
 1921+ id: this.options.idPrefix + this.desc.name,
 1922+ name: this.options.idPrefix + this.desc.name
 1923+ } )
 1924+ .appendTo( this.$div );
 1925+
 1926+ var value = options.values && options.values[this.desc.name];
 1927+ if ( typeof value != 'undefined' ) {
 1928+ if ( !isScalar( value ) ) {
 1929+ $.error( "value is invalid" );
 1930+ }
 1931+
 1932+ this.$text.val( $.toJSON( value ) );
 1933+ }
 1934+ }
 1935+ inherit( ScalarField, LabelField );
 1936+
 1937+ ScalarField.prototype.getDesc = function( useValuesAsDefault ) {
 1938+ var desc = clone( LabelField.prototype.getDesc.call( this, useValuesAsDefaults ) );
 1939+ if ( useValuesAsDefaults === true ) {
 1940+ //set 'default' to current value.
 1941+ var values = this.getValues();
 1942+ desc['default'] = values[this.desc.name];
 1943+ }
 1944+ };
 1945+
 1946+ ScalarField.prototype.getValues = function() {
 1947+ var text = this.$text.val();
 1948+
 1949+ try {
 1950+ var value = $.parseJSON( text );
 1951+ return pair( this.desc.name, value );
 1952+ } catch( e ) {
 1953+ return pair( this.desc.name, undefined );
 1954+ }
 1955+ };
 1956+
 1957+ ScalarField.prototype.getValidationSettings = function() {
 1958+ var settings = Field.prototype.getValidationSettings.call( this ),
 1959+ fieldId = this.options.idPrefix + this.desc.name;
 1960+
 1961+ settings.rules[fieldId] = pair( "scalar", true );
 1962+ return settings;
 1963+ };
 1964+
 1965+ /* Specifications of preferences descriptions syntax and field types */
 1966+
 1967+ //Describes 'name' and 'label' field members, common to all "simple" fields
 1968+ var simpleFields = [
 1969+ {
 1970+ "name": "name",
 1971+ "type": "string",
 1972+ "label": "name",
 1973+ "required": true,
 1974+ "maxlength": 40,
 1975+ "default": ""
 1976+ },
 1977+ {
 1978+ "name": "label",
 1979+ "type": "string",
 1980+ "label": "label",
 1981+ "required": false,
 1982+ "default": ""
 1983+ }
 1984+ ];
 1985+
 1986+ //Used by preference editor to build field properties dialogs
 1987+ //TODO: document
 1988+ prefsSpecifications = {
 1989+ "label": {
 1990+ "simple": false,
 1991+ "builder": [ {
 1992+ "name": "label",
 1993+ "type": "string",
 1994+ "label": "label",
 1995+ "required": false,
 1996+ "default": ""
 1997+ } ]
 1998+ },
 1999+ "boolean": {
 2000+ "simple": true,
 2001+ "builder": simpleFields
 2002+ },
 2003+ "string": {
 2004+ "simple": true,
 2005+ "builder": simpleFields.concat( [
 2006+ {
 2007+ "name": "required",
 2008+ "type": "select",
 2009+ "label": "required",
 2010+ "default": null,
 2011+ "options": [
 2012+ {
 2013+ "name": "not specified",
 2014+ "value": null
 2015+ },
 2016+ {
 2017+ "name": "true",
 2018+ "value": true
 2019+ },
 2020+ {
 2021+ "name": "false",
 2022+ "value": false
 2023+ }
 2024+ ]
 2025+ },
 2026+ {
 2027+ "name": "minlength",
 2028+ "type": "number",
 2029+ "label": "minlength",
 2030+ "integer": true,
 2031+ "min": 0,
 2032+ "required": false,
 2033+ "default": null
 2034+ },
 2035+ {
 2036+ "name": "maxlength",
 2037+ "type": "number",
 2038+ "label": "maxlength",
 2039+ "integer": true,
 2040+ "min": 1,
 2041+ "required": false,
 2042+ "default": null
 2043+ }
 2044+ ] )
 2045+ },
 2046+ "number": {
 2047+ "simple": true,
 2048+ "builder": simpleFields.concat( [
 2049+ {
 2050+ "name": "required",
 2051+ "type": "boolean",
 2052+ "label": "required",
 2053+ "default": true
 2054+ },
 2055+ {
 2056+ "name": "integer",
 2057+ "type": "boolean",
 2058+ "label": "integer",
 2059+ "default": false
 2060+ },
 2061+ {
 2062+ "name": "min",
 2063+ "type": "number",
 2064+ "label": "min",
 2065+ "required": false,
 2066+ "default": null
 2067+ },
 2068+ {
 2069+ "name": "max",
 2070+ "type": "number",
 2071+ "label": "max",
 2072+ "required": false,
 2073+ "default": null
 2074+ }
 2075+ ] )
 2076+ },
 2077+ "select": {
 2078+ "simple": true,
 2079+ "builder": simpleFields.concat( [
 2080+ {
 2081+ "name": "options",
 2082+ "type": "list",
 2083+ "default": [],
 2084+ "field": {
 2085+ "type": "composite",
 2086+ "fields": [
 2087+ {
 2088+ "name": "name",
 2089+ "type": "string",
 2090+ "label": "Option name", //TODO: i18n
 2091+ "default": ""
 2092+ },
 2093+ {
 2094+ "name": "value",
 2095+ "type": ScalarField,
 2096+ "label": "Option value", //TODO: i18n
 2097+ "default": ""
 2098+ }
 2099+ ]
 2100+ }
 2101+ }
 2102+ ] )
 2103+ },
 2104+ "range": {
 2105+ "simple": true,
 2106+ "builder": simpleFields.concat( [
 2107+ {
 2108+ "name": "min",
 2109+ "type": "number",
 2110+ "label": "min",
 2111+ "required": true
 2112+ },
 2113+ {
 2114+ "name": "step",
 2115+ "type": "number",
 2116+ "label": "step",
 2117+ "required": true,
 2118+ "default": 1
 2119+ },
 2120+ {
 2121+ "name": "max",
 2122+ "type": "number",
 2123+ "label": "max",
 2124+ "required": true
 2125+ }
 2126+ ] )
 2127+ },
 2128+ "date": {
 2129+ "simple": true,
 2130+ "builder": simpleFields
 2131+ },
 2132+ "color": {
 2133+ "simple": true,
 2134+ "builder": simpleFields
 2135+ },
 2136+ "bundle": {
 2137+ "simple": false,
 2138+ "builder": function( options, callback ) {
 2139+ callback(
 2140+ new BundleField( {
 2141+ "type": "bundle",
 2142+ "sections": [
 2143+ {
 2144+ "title": "Section 1",
 2145+ "fields": []
 2146+ },
 2147+ {
 2148+ "title": "Section 2",
 2149+ "fields": []
 2150+ }
 2151+ ]
 2152+ }, options )
 2153+ );
 2154+ }
 2155+ },
 2156+ "composite": {
 2157+ "simple": true,
 2158+ "builder": [ {
 2159+ "name": "name",
 2160+ "type": "string",
 2161+ "label": "name",
 2162+ "required": true,
 2163+ "maxlength": 40,
 2164+ "default": ""
 2165+ } ]
 2166+ },
 2167+ "list": {
 2168+ "simple": true,
 2169+ "builder": function( options, callback ) {
 2170+
 2171+ //Create list of "simple" types
 2172+ var selectOptions = [];
 2173+ $.each( prefsSpecifications, function( type, typeInfo ) {
 2174+ if ( typeInfo.simple === true ) {
 2175+ selectOptions.push( { "name": type, "value": type } );
 2176+ }
 2177+ } );
 2178+
 2179+ //Create the dialog to chose the field type and set list properties
 2180+ var description = {
 2181+ "fields": [
 2182+ {
 2183+ "name": "name",
 2184+ "type": "string",
 2185+ "label": "name",
 2186+ "required": true,
 2187+ "maxlength": 40,
 2188+ "default": ""
 2189+ },
 2190+ {
 2191+ "name": "required",
 2192+ "type": "select",
 2193+ "label": "required",
 2194+ "default": null,
 2195+ "options": [
 2196+ {
 2197+ "name": "not specified",
 2198+ "value": null
 2199+ },
 2200+ {
 2201+ "name": "true",
 2202+ "value": true
 2203+ },
 2204+ {
 2205+ "name": "false",
 2206+ "value": false
 2207+ }
 2208+ ]
 2209+ },
 2210+ {
 2211+ "name": "minlength",
 2212+ "type": "number",
 2213+ "label": "minlength",
 2214+ "integer": true,
 2215+ "min": 0,
 2216+ "required": false,
 2217+ "default": null
 2218+ },
 2219+ {
 2220+ "name": "maxlength",
 2221+ "type": "number",
 2222+ "label": "maxlength",
 2223+ "integer": true,
 2224+ "min": 1,
 2225+ "required": false,
 2226+ "default": null
 2227+ },
 2228+ {
 2229+ "name": "type",
 2230+ "type": "select",
 2231+ "label": "type",
 2232+ "options": selectOptions
 2233+ }
 2234+ ]
 2235+ };
 2236+ var $form = $( description ).formBuilder( { idPrefix: 'list-choose-type-' } )
 2237+ .submit( function() {
 2238+ return false; //prevent form submission
 2239+ } );
 2240+
 2241+ $form.dialog( {
 2242+ width: 450,
 2243+ modal: true,
 2244+ resizable: false,
 2245+ title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', 'list' ),
 2246+ close: function() {
 2247+ $( this ).remove();
 2248+ },
 2249+ buttons: [
 2250+ {
 2251+ text: mw.msg( 'gadgets-formbuilder-editor-ok' ),
 2252+ click: function() {
 2253+ var values = $( this ).formBuilder( 'getValues' );
 2254+ $( this ).dialog( "close" );
 2255+
 2256+ //Remove properties that equal their default
 2257+ $.each( description.fields, function( index, fieldSpec ) {
 2258+ var property = fieldSpec.name;
 2259+ if ( values[property] === fieldSpec['default'] ) {
 2260+ delete values[property];
 2261+ }
 2262+ } );
 2263+
 2264+ var $dialog = $( this );
 2265+ createFieldDialog( {
 2266+ type: values.type,
 2267+ values: {
 2268+ "name": values.name
 2269+ },
 2270+ callback: function( field ) {
 2271+ $dialog.dialog( 'close' );
 2272+ showEditFieldDialog( field.getDesc(), values, options, callback );
 2273+ return true;
 2274+ }
 2275+ }, { editable: true } );
 2276+ }
 2277+ },
 2278+ {
 2279+ text: mw.msg( 'gadgets-formbuilder-editor-cancel' ),
 2280+ click: function() {
 2281+ $( this ).dialog( "close" );
 2282+ }
 2283+ }
 2284+ ]
 2285+ } );
 2286+ }
 2287+ }
 2288+ };
 2289+
 2290+ /* Public methods */
 2291+
 2292+ /**
 2293+ * Main method; takes the given preferences description object and builds
 2294+ * the body of the form with the requested fields.
 2295+ *
 2296+ * @param {Object} options options to set properties of the form and to change its behaviour.
 2297+ * Valid options:
 2298+ * idPrefix: compulsory, a unique prefix for all ids of elements created by formBuilder, to avoid conflicts.
 2299+ * msgPrefix: compulsory, a prefix to be added to all messages referred to by the description.
 2300+ * values: optional, a map of values of preferences; if omitted, default values will be used.
 2301+ * editable: optional, defaults to false; true if the form must be editable via the UI; used by the preferences editor.
 2302+ * staticFields: optional, defaults to false; ignored if editable is not true. If both editable and staticFields are true,
 2303+ * insertion or removal of fields is not allowed, but changing field properties is. Used by the preferences
 2304+ * editor.
 2305+ * change: optional, a function that will be called back if the value of a field changes (it's not strictly warranted that
 2306+ * a field value has actually changed).
 2307+ *
 2308+ * @return {Element} the object with the requested form body.
 2309+ */
 2310+ function buildFormBody( options ) {
 2311+ var description = this.get( 0 );
 2312+ if ( typeof description != 'object' ) {
 2313+ mw.log( "description should be an object, instead of a " + typeof description );
 2314+ return null;
 2315+ }
 2316+
 2317+ var $form = $( '<form/>' ).addClass( 'formbuilder' );
 2318+
 2319+ if ( typeof description.fields != 'object' ) {
 2320+ mw.log( "description.fields should be an object, instead of a " + typeof description.fields );
 2321+ return null;
 2322+ }
 2323+
 2324+ var section = new SectionField( description, options );
 2325+ section.getElement().appendTo( $form );
 2326+
 2327+ //Initialize validator
 2328+ var validator = $form.validate( section.getValidationSettings() );
 2329+
 2330+ $form.data( 'formBuilder', {
 2331+ mainSection: section,
 2332+ validator: validator
 2333+ } );
 2334+
 2335+ return $form;
 2336+ }
 2337+
 2338+ var methods = {
 2339+
 2340+ /**
 2341+ * Returns a dictionary of field names and field values.
 2342+ * Returned values are not warranted to pass field validation.
 2343+ *
 2344+ * @return {Object}
 2345+ */
 2346+ getValues: function() {
 2347+ var data = this.data( 'formBuilder' );
 2348+ return data.mainSection.getValues();
 2349+ },
 2350+
 2351+ /**
 2352+ * Returns the current description, where current values as set for 'default' values.
 2353+ * Used by the preference editor.
 2354+ *
 2355+ * NOTE: it is responsibility of the caller to call 'validate' and ensure that
 2356+ * current values pass validation before calling this method.
 2357+ *
 2358+ * @return {Object}
 2359+ */
 2360+ getDescription: function() {
 2361+ var data = this.data( 'formBuilder' );
 2362+ return data.mainSection.getDesc( true );
 2363+ },
 2364+
 2365+ /**
 2366+ * Do validation of form fields and warn the user about wrong values, if any.
 2367+ *
 2368+ * @return {Boolean} true if all fields pass validation, false otherwise.
 2369+ */
 2370+ validate: function() {
 2371+ var data = this.data( 'formBuilder' );
 2372+ return data.validator.form();
 2373+ }
 2374+ };
 2375+
 2376+ $.fn.formBuilder = function( method ) {
 2377+ if ( methods[method] ) {
 2378+ return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
 2379+ } else if ( typeof method === 'object' || !method ) {
 2380+ return buildFormBody.apply( this, arguments );
 2381+ } else {
 2382+ $.error( 'Method ' + method + ' does not exist on jQuery.formBuilder' );
 2383+ }
 2384+ };
 2385+} )( jQuery, mediaWiki );
 2386+
Property changes on: branches/RL2/extensions/Gadgets/modules/jquery.formBuilder.js
___________________________________________________________________
Added: svn:eol-style
12387 + native
Index: branches/RL2/extensions/Gadgets/api/ApiSetGadgetPrefs.php
@@ -0,0 +1,123 @@
 2+<?php
 3+
 4+/**
 5+ *
 6+ * API for setting Gadget's preferences
 7+ *
 8+ * This program is free software; you can redistribute it and/or modify
 9+ * it under the terms of the GNU General Public License as published by
 10+ * the Free Software Foundation; either version 2 of the License, or
 11+ * (at your option) any later version.
 12+ *
 13+ * This program is distributed in the hope that it will be useful,
 14+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 16+ * GNU General Public License for more details.
 17+ *
 18+ * You should have received a copy of the GNU General Public License along
 19+ * with this program; if not, write to the Free Software Foundation, Inc.,
 20+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 21+ * http://www.gnu.org/copyleft/gpl.html
 22+ */
 23+
 24+// TODO: Needs to be changed for new backend
 25+class ApiSetGadgetPrefs extends ApiBase {
 26+
 27+ public function execute() {
 28+ $user = RequestContext::getMain()->getUser();
 29+
 30+ $params = $this->extractRequestParams();
 31+ //Check permissions
 32+ if ( !$user->isLoggedIn() ) {
 33+ $this->dieUsage( 'You must be logged-in to set gadget\'s preferences', 'notloggedin' );
 34+ }
 35+
 36+ //Check token
 37+ if ( !$user->matchEditToken( $params['token'] ) ) {
 38+ $this->dieUsageMsg( 'sessionfailure' );
 39+ }
 40+
 41+ $gadgetName = $params['gadget'];
 42+ $gadgets = Gadget::loadList();
 43+ $gadget = $gadgets && isset( $gadgets[$gadgetName] ) ? $gadgets[$gadgetName] : null;
 44+
 45+ if ( $gadget === null ) {
 46+ $this->dieUsage( 'Gadget not found', 'notfound' );
 47+ }
 48+
 49+ $prefsJson = $params['prefs'];
 50+ $prefs = FormatJson::decode( $prefsJson, true );
 51+
 52+ if ( !is_array( $prefs ) ) {
 53+ $this->dieUsage( 'The \'pref\' parameter must be valid JSON', 'notjson' );
 54+ }
 55+
 56+ $result = $gadget->setPrefs( $prefs, true );
 57+
 58+ if ( $result === true ) {
 59+ $this->getResult()->addValue(
 60+ null, $this->getModuleName(), array( 'result' => 'Success' ) );
 61+ } else {
 62+ $this->dieUsage( 'Invalid preferences', 'invalidprefs' );
 63+ }
 64+ }
 65+
 66+ public function mustBePosted() {
 67+ return true;
 68+ }
 69+
 70+ public function isWriteMode() {
 71+ return true;
 72+ }
 73+
 74+ public function getAllowedParams() {
 75+ return array(
 76+ 'gadget' => array(
 77+ ApiBase::PARAM_TYPE => 'string',
 78+ ApiBase::PARAM_REQUIRED => true
 79+ ),
 80+ 'prefs' => array(
 81+ ApiBase::PARAM_TYPE => 'string',
 82+ ApiBase::PARAM_REQUIRED => true
 83+ ),
 84+ 'token' => array(
 85+ ApiBase::PARAM_TYPE => 'string',
 86+ ApiBase::PARAM_REQUIRED => true
 87+ ),
 88+ );
 89+ }
 90+
 91+ public function getParamDescription() {
 92+ return array(
 93+ 'gadget' => 'The name of the gadget',
 94+ 'prefs' => 'The new preferences in JSON format',
 95+ 'token' => 'An edit token'
 96+ );
 97+ }
 98+
 99+ public function getDescription() {
 100+ return 'Allows user code to set preferences for gadgets';
 101+ }
 102+
 103+ public function getPossibleErrors() {
 104+ return array_merge( parent::getPossibleErrors(), array(
 105+ array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to get gadget\'s preferences' ),
 106+ array( 'sessionfailure' ),
 107+ array( 'code' => 'notfound', 'info' => 'Gadget not found' ),
 108+ array( 'code' => 'notjson', 'info' => 'The \'pref\' parameter must be valid JSON' ),
 109+ array( 'code' => 'invalidprefs', 'info' => 'Invalid preferences' ),
 110+ ) );
 111+ }
 112+
 113+ public function needsToken() {
 114+ return true;
 115+ }
 116+
 117+ public function getSalt() {
 118+ return '';
 119+ }
 120+
 121+ public function getVersion() {
 122+ return __CLASS__ . ': $Id: ApiSetGadgetPrefs.php 90469 2011-06-20 16:42:35Z salvatoreingala $';
 123+ }
 124+}
Property changes on: branches/RL2/extensions/Gadgets/api/ApiSetGadgetPrefs.php
___________________________________________________________________
Added: svn:eol-style
1125 + native
Index: branches/RL2/extensions/Gadgets/api/ApiGetGadgetPrefs.php
@@ -0,0 +1,92 @@
 2+<?php
 3+
 4+/**
 5+ *
 6+ * API for getting Gadget's preferences
 7+ *
 8+ * This program is free software; you can redistribute it and/or modify
 9+ * it under the terms of the GNU General Public License as published by
 10+ * the Free Software Foundation; either version 2 of the License, or
 11+ * (at your option) any later version.
 12+ *
 13+ * This program is distributed in the hope that it will be useful,
 14+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 16+ * GNU General Public License for more details.
 17+ *
 18+ * You should have received a copy of the GNU General Public License along
 19+ * with this program; if not, write to the Free Software Foundation, Inc.,
 20+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 21+ * http://www.gnu.org/copyleft/gpl.html
 22+ */
 23+
 24+// TODO: Needs to be changed for new backend
 25+class ApiGetGadgetPrefs extends ApiBase {
 26+
 27+ public function execute() {
 28+ $user = RequestContext::getMain()->getUser();
 29+
 30+ $this->getMain()->setCacheMaxAge( 0 ); //results should not be cached
 31+
 32+ $params = $this->extractRequestParams();
 33+ //Check permissions
 34+ if ( !$user->isLoggedIn() ) {
 35+ $this->dieUsage( 'You must be logged-in to get gadget\'s preferences', 'notloggedin' );
 36+ }
 37+
 38+ $gadgetName = $params['gadget'];
 39+
 40+ $gadgets = Gadget::loadList();
 41+ $gadget = $gadgets && isset( $gadgets[$gadgetName] ) ? $gadgets[$gadgetName] : null;
 42+
 43+ if ( $gadget === null ) {
 44+ $this->dieUsage( 'Gadget not found', 'notfound' );
 45+ }
 46+
 47+ $prefsDescription = $gadget->getPrefsDescription();
 48+
 49+ if ( $prefsDescription === null ) {
 50+ $this->dieUsage( 'Gadget ' . $gadget->getName() . ' does not have any preference', 'noprefs' );
 51+ }
 52+
 53+ $userPrefs = $gadget->getPrefs();
 54+
 55+ if ( $userPrefs === null ) {
 56+ throw new MWException( __METHOD__ . ': $userPrefs should not be null.' );
 57+ }
 58+
 59+ $this->getResult()->addValue( null, 'description', $prefsDescription );
 60+ $this->getResult()->addValue( null, 'values', $userPrefs );
 61+ }
 62+
 63+ public function getAllowedParams() {
 64+ return array(
 65+ 'gadget' => array(
 66+ ApiBase::PARAM_TYPE => 'string',
 67+ ApiBase::PARAM_REQUIRED => true
 68+ )
 69+ );
 70+ }
 71+
 72+ public function getParamDescription() {
 73+ return array(
 74+ 'gadget' => 'The name of the gadget'
 75+ );
 76+ }
 77+
 78+ public function getDescription() {
 79+ return 'Allows user code to get preferences for gadgets, along with preference descriptions and values for the currently logged-in user';
 80+ }
 81+
 82+ public function getPossibleErrors() {
 83+ return array_merge( parent::getPossibleErrors(), array(
 84+ array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to get gadget\'s preferences' ),
 85+ array( 'code' => 'notfound', 'info' => 'Gadget not found' ),
 86+ array( 'code' => 'noprefs', 'info' => 'Gadget gadgetname does not have any preferences' ),
 87+ ) );
 88+ }
 89+
 90+ public function getVersion() {
 91+ return __CLASS__ . ': $Id: ApiGetGadgetPrefs.php 95100 2011-08-20 17:49:07Z salvatoreingala $';
 92+ }
 93+}
Property changes on: branches/RL2/extensions/Gadgets/api/ApiGetGadgetPrefs.php
___________________________________________________________________
Added: svn:eol-style
194 + native

Status & tagging log