Index: branches/salvatoreingala/Gadgets/Gadgets.i18n.php |
— | — | @@ -66,10 +66,11 @@ |
67 | 67 | 'gadgets-formbuilder-editor-move-field' => 'Move this field', |
68 | 68 | 'gadgets-formbuilder-editor-delete-field' => 'Delete this field', |
69 | 69 | 'gadgets-formbuilder-editor-edit-field' => 'Edit field properties', |
| 70 | + 'gadgets-formbuilder-editor-edit-field-title' => 'Edit field', |
70 | 71 | 'gadgets-formbuilder-editor-insert-field' => 'Insert a new field', |
71 | 72 | 'gadgets-formbuilder-editor-chose-field' => 'Chose the type of the new field:', |
72 | 73 | 'gadgets-formbuilder-editor-chose-field-title' => 'Chose field type', |
73 | | - 'gadgets-formbuilder-editor-create-field-title' => 'Create field', |
| 74 | + 'gadgets-formbuilder-editor-create-field-title' => "Create '$1' field", |
74 | 75 | 'gadgets-formbuilder-editor-duplicate-name' => 'The preference name $1 has been used. Please chose a different name.', |
75 | 76 | 'gadgets-formbuilder-editor-edit-section' => 'Edit this section\'s title', |
76 | 77 | 'gadgets-formbuilder-editor-delete-section' => 'Delete this section and all his content', |
Index: branches/salvatoreingala/Gadgets/Gadgets.php |
— | — | @@ -94,7 +94,7 @@ |
95 | 95 | 'gadgets-formbuilder-min', 'gadgets-formbuilder-max', 'gadgets-formbuilder-integer', 'gadgets-formbuilder-date', |
96 | 96 | 'gadgets-formbuilder-color', |
97 | 97 | 'gadgets-formbuilder-editor-ok', 'gadgets-formbuilder-editor-cancel', 'gadgets-formbuilder-editor-move-field', |
98 | | - 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-insert-field', |
| 98 | + 'gadgets-formbuilder-editor-delete-field', 'gadgets-formbuilder-editor-edit-field', 'gadgets-formbuilder-editor-edit-field-title', 'gadgets-formbuilder-editor-insert-field', |
99 | 99 | 'gadgets-formbuilder-editor-chose-field', 'gadgets-formbuilder-editor-chose-field-title', 'gadgets-formbuilder-editor-create-field-title', |
100 | 100 | 'gadgets-formbuilder-editor-duplicate-name', 'gadgets-formbuilder-editor-delete-section', 'gadgets-formbuilder-editor-new-section', |
101 | 101 | 'gadgets-formbuilder-editor-edit-section', 'gadgets-formbuilder-editor-chose-title', 'gadgets-formbuilder-editor-chose-title-title' |
Index: branches/salvatoreingala/Gadgets/Gadgets_tests.php |
— | — | @@ -616,7 +616,117 @@ |
617 | 617 | //Check if only the wrong subfield has been reset to default value |
618 | 618 | $this->assertEquals( $prefs, array( 'foo' => array( 'bar' => false, 'car' => '#123456' ) ) ); |
619 | 619 | } |
620 | | - |
| 620 | + |
| 621 | + //Tests for 'list' type fields |
| 622 | + function testPrefsDescriptionsList() { |
| 623 | + $correct = array( |
| 624 | + 'fields' => array( |
| 625 | + array( |
| 626 | + 'name' => 'foo', |
| 627 | + 'type' => 'list', |
| 628 | + 'default' => array(), |
| 629 | + 'field' => array( |
| 630 | + 'type' => 'composite', |
| 631 | + 'fields' => array( |
| 632 | + array( |
| 633 | + 'name' => 'bar', |
| 634 | + 'type' => 'boolean', |
| 635 | + 'label' => '@msg1', |
| 636 | + 'default' => true |
| 637 | + ), |
| 638 | + array( |
| 639 | + 'name' => 'car', |
| 640 | + 'type' => 'color', |
| 641 | + 'label' => '@msg2', |
| 642 | + 'default' => '#123456' |
| 643 | + ) |
| 644 | + ) |
| 645 | + ) |
| 646 | + ) |
| 647 | + ) |
| 648 | + ); |
| 649 | + |
| 650 | + $this->assertTrue( GadgetPrefs::isPrefsDescriptionValid( $correct ) ); |
| 651 | + |
| 652 | + //Specifying the 'name' member for field must fail |
| 653 | + $wrong = $correct; |
| 654 | + $wrong['fields'][0]['field']['name'] = 'composite'; |
| 655 | + $this->assertFalse( GadgetPrefs::isPrefsDescriptionValid( $wrong ) ); |
| 656 | + |
| 657 | + |
| 658 | + $this->assertEquals( |
| 659 | + GadgetPrefs::getDefaults( $correct ), |
| 660 | + array( 'foo' => array() ) |
| 661 | + ); |
| 662 | + |
| 663 | + $this->assertEquals( GadgetPrefs::getMessages( $correct ), array( 'msg1', 'msg2' ) ); |
| 664 | + |
| 665 | + //Tests with correct pref values |
| 666 | + $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( |
| 667 | + $correct, |
| 668 | + array( 'foo' => array() ) |
| 669 | + ) ); |
| 670 | + |
| 671 | + $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( |
| 672 | + $correct, |
| 673 | + array( 'foo' => array( |
| 674 | + array( |
| 675 | + 'bar' => true, |
| 676 | + 'car' => '#115599' |
| 677 | + ), |
| 678 | + array( |
| 679 | + 'bar' => false, |
| 680 | + 'car' => '#123456' |
| 681 | + ), |
| 682 | + array( |
| 683 | + 'bar' => true, |
| 684 | + 'car' => '#ffffff' |
| 685 | + ) |
| 686 | + ) |
| 687 | + ) |
| 688 | + ) ); |
| 689 | + |
| 690 | + //Tests with wrong pref values |
| 691 | + $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription( |
| 692 | + $correct, |
| 693 | + array( 'foo' => array( |
| 694 | + array( |
| 695 | + 'bar' => null, //wrong |
| 696 | + 'car' => '#115599' |
| 697 | + ) |
| 698 | + ) |
| 699 | + ) |
| 700 | + ) ); |
| 701 | + |
| 702 | + $this->assertFalse( GadgetPrefs::checkPrefsAgainstDescription( |
| 703 | + $correct, |
| 704 | + array( 'foo' => array( //wrong, not enclosed in array |
| 705 | + 'bar' => null, |
| 706 | + 'car' => '#115599' |
| 707 | + ) |
| 708 | + ) |
| 709 | + ) ); |
| 710 | + |
| 711 | + $prefs = array( 'foo' => array( |
| 712 | + array( |
| 713 | + 'bar' => null, |
| 714 | + 'car' => '#115599' |
| 715 | + ), |
| 716 | + array( |
| 717 | + 'bar' => false, |
| 718 | + 'car' => '' |
| 719 | + ), |
| 720 | + array( |
| 721 | + 'bar' => true, |
| 722 | + 'car' => '#ffffff' |
| 723 | + ) |
| 724 | + ) |
| 725 | + ); |
| 726 | + |
| 727 | + GadgetPrefs::matchPrefsWithDescription( $correct, $prefs ); |
| 728 | + $this->assertTrue( GadgetPrefs::checkPrefsAgainstDescription( $correct, $prefs ) ); |
| 729 | + } |
| 730 | + |
621 | 731 | //Data provider to be able to reuse a complex preference description for several tests. |
622 | 732 | function prefsDescProvider() { |
623 | 733 | return array( array( |
Index: branches/salvatoreingala/Gadgets/backend/Gadget.php |
— | — | @@ -113,9 +113,10 @@ |
114 | 114 | if ( isset( $prefsDescriptionJson ) ) { |
115 | 115 | $prefsDescription = FormatJson::decode( $prefsDescriptionJson, true ); |
116 | 116 | $gadget->setPrefsDescription( $prefsDescription ); |
117 | | - |
118 | | - //Load default gadget preferences. Only useful for anonymous users |
119 | | - $gadget->setPrefs( GadgetPrefs::getDefaults( $prefsDescription ) ); |
| 117 | + if ( $gadget->getPrefsDescription() !== null ) { |
| 118 | + //Load default gadget preferences. Only useful for anonymous users |
| 119 | + $gadget->setPrefs( GadgetPrefs::getDefaults( $prefsDescription ) ); |
| 120 | + } |
120 | 121 | } |
121 | 122 | } |
122 | 123 | |
Index: branches/salvatoreingala/Gadgets/backend/GadgetPrefs.php |
— | — | @@ -14,7 +14,7 @@ |
15 | 15 | * Syntax specifications of preference description language. |
16 | 16 | * Each element describes a field; a "simple" field encodes exactly one gadget preference, but some fields |
17 | 17 | * may encode for 0 or multiple gadget preferences. |
18 | | - * "Simple" field always have the 'name', the 'label' and the 'default' members. |
| 18 | + * "Simple" field always have the 'name' member. Not "simple" fields never do. |
19 | 19 | * Each field has a 'description' and may have a 'validator', a 'flattener', and a 'checker'. |
20 | 20 | * - 'description' is an array that describes all the members of that fields. Each member description has this shape: |
21 | 21 | * - 'isMandatory' is a boolean that specifies if that member is mandatory for the field; |
— | — | @@ -30,7 +30,7 @@ |
31 | 31 | * an array of preference values $prefs and the name of a preference $preferenceName and returns an array where |
32 | 32 | * $prefs[$prefName] is changed in a way that passes validation. If omitted, the default action is to set $prefs[$prefName] |
33 | 33 | * to $prefDescription['default']. |
34 | | - * - 'getDefault', only for "simple" fields, if a function che takes one argument, the descriptio of the field, and |
| 34 | + * - 'getDefault', only for "simple" fields, if a function che takes one argument, the description of the field, and |
35 | 35 | * returns its default value; if omitted, the value of the 'default' field is returned. |
36 | 36 | * - 'getMessages', if specified, is the name of a function that takes a valid description of a field and returns |
37 | 37 | * a list of messages referred to by it. If omitted, only the "label" field is returned (if it is a message). |
— | — | @@ -236,7 +236,26 @@ |
237 | 237 | 'getDefault' => 'GadgetPrefs::getCompositeDefault', |
238 | 238 | 'checker' => 'GadgetPrefs::checkCompositePref', |
239 | 239 | 'matcher' => 'GadgetPrefs::matchCompositePref' |
240 | | - ) |
| 240 | + ), |
| 241 | + 'list' => array( |
| 242 | + 'description' => array( |
| 243 | + 'name' => array( |
| 244 | + 'isMandatory' => true, |
| 245 | + 'validator' => 'GadgetPrefs::isValidPreferenceName' |
| 246 | + ), |
| 247 | + 'field' => array( |
| 248 | + 'isMandatory' => true, |
| 249 | + 'validator' => 'is_array' |
| 250 | + ), |
| 251 | + 'default' => array( |
| 252 | + 'isMandatory' => true |
| 253 | + ) |
| 254 | + ), |
| 255 | + 'validator' => 'GadgetPrefs::validateListPrefDefinition', |
| 256 | + 'getMessages' => 'GadgetPrefs::getListMessages', |
| 257 | + 'checker' => 'GadgetPrefs::checkListPref', |
| 258 | + 'matcher' => 'GadgetPrefs::matchListPref' |
| 259 | + ) |
241 | 260 | ); |
242 | 261 | |
243 | 262 | private static function isValidPreferenceName( $name ) { |
— | — | @@ -376,6 +395,24 @@ |
377 | 396 | return true; |
378 | 397 | } |
379 | 398 | |
| 399 | + private static function validateListPrefDefinition( $prefDefinition ) { |
| 400 | + //Name must not be set for the 'field' description |
| 401 | + if ( array_key_exists( 'name', $prefDefinition['field'] ) ) { |
| 402 | + return false; |
| 403 | + } |
| 404 | + |
| 405 | + //Check if the field definition is valid, apart from missing the name |
| 406 | + $itemDescription = $prefDefinition['field']; |
| 407 | + $itemDescription['name'] = 'dummy'; |
| 408 | + if ( !self::validateFieldDefinition( $itemDescription ) ) { |
| 409 | + return false; |
| 410 | + }; |
| 411 | + |
| 412 | + //Finally, type described by the 'field' member must be a simple type (e.g.: have "name" ). |
| 413 | + $type = $itemDescription['type']; |
| 414 | + return isset( self::$prefsDescriptionSpecifications[$type]['description']['name'] ); |
| 415 | + } |
| 416 | + |
380 | 417 | //Flattens a simple field, by calling its field-specific flattener if there is any, |
381 | 418 | //or the default flattener otherwise. |
382 | 419 | private static function flattenFieldDescription( $fieldDescription ) { |
— | — | @@ -401,17 +438,10 @@ |
402 | 439 | return $flattenedPrefsDescription; |
403 | 440 | } |
404 | 441 | |
405 | | - //Validate the description of a 'section' of preferences |
406 | | - private static function validateSectionDefinition( $sectionDescription ) { |
| 442 | + //Validates a single field |
| 443 | + private static function validateFieldDefinition( $fieldDefinition ) { |
407 | 444 | static $mandatoryCount = array(), $initialized = false; |
408 | 445 | |
409 | | - if ( !is_array( $sectionDescription ) |
410 | | - || !isset( $sectionDescription['fields'] ) |
411 | | - || !is_array( $sectionDescription['fields'] ) ) |
412 | | - { |
413 | | - return false; |
414 | | - } |
415 | | - |
416 | 446 | if ( !$initialized ) { |
417 | 447 | //Count of mandatory members for each type |
418 | 448 | foreach ( self::$prefsDescriptionSpecifications as $type => $typeSpec ) { |
— | — | @@ -424,71 +454,87 @@ |
425 | 455 | } |
426 | 456 | $initialized = true; |
427 | 457 | } |
| 458 | + |
| 459 | + //Check if 'type' is set |
| 460 | + if ( !isset( $fieldDefinition['type'] ) ) { |
| 461 | + return false; |
| 462 | + } |
428 | 463 | |
429 | | - //Check if 'fields' is a regular (not-associative) array, and that it is not empty |
430 | | - $count = count( $sectionDescription['fields'] ); |
431 | | - if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) { |
| 464 | + $type = $fieldDefinition['type']; |
| 465 | + |
| 466 | + //check if 'type' is valid |
| 467 | + if ( !isset( self::$prefsDescriptionSpecifications[$type] ) ) { |
432 | 468 | return false; |
433 | 469 | } |
434 | 470 | |
435 | | - //TODO: validation of members other than $prefs['fields'] |
436 | | - |
437 | | - //Flattened preferences |
438 | | - $flattenedPrefs = array(); |
439 | | - |
440 | | - foreach ( $sectionDescription['fields'] as $optionDefinition ) { |
| 471 | + //Check if all fields satisfy specification |
| 472 | + $typeSpec = self::$prefsDescriptionSpecifications[$type]; |
| 473 | + $typeDescription = $typeSpec['description']; |
| 474 | + $count = 0; //count of present mandatory members |
| 475 | + foreach ( $fieldDefinition as $memberName => $memberValue ) { |
441 | 476 | |
442 | | - //Check if 'type' is set |
443 | | - if ( !isset( $optionDefinition['type'] ) ) { |
444 | | - return false; |
| 477 | + if ( $memberName == 'type' ) { |
| 478 | + continue; //'type' must not be checked |
445 | 479 | } |
446 | 480 | |
447 | | - $type = $optionDefinition['type']; |
448 | | - |
449 | | - //check if 'type' is valid |
450 | | - if ( !isset( self::$prefsDescriptionSpecifications[$type] ) ) { |
| 481 | + if ( !isset( $typeDescription[$memberName] ) ) { |
451 | 482 | return false; |
452 | 483 | } |
453 | 484 | |
454 | | - //Check if all fields satisfy specification |
455 | | - $typeSpec = self::$prefsDescriptionSpecifications[$type]; |
456 | | - $typeDescription = $typeSpec['description']; |
457 | | - $count = 0; //count of present mandatory members |
458 | | - foreach ( $optionDefinition as $fieldName => $fieldValue ) { |
459 | | - |
460 | | - if ( $fieldName == 'type' ) { |
461 | | - continue; //'type' must not be checked |
462 | | - } |
463 | | - |
464 | | - if ( !isset( $typeDescription[$fieldName] ) ) { |
| 485 | + if ( $typeDescription[$memberName]['isMandatory'] ) { |
| 486 | + ++$count; |
| 487 | + } |
| 488 | + |
| 489 | + if ( isset( $typeDescription[$memberName]['validator'] ) ) { |
| 490 | + $validator = $typeDescription[$memberName]['validator']; |
| 491 | + if ( !call_user_func( $validator, $memberValue ) ) { |
465 | 492 | return false; |
466 | 493 | } |
467 | | - |
468 | | - if ( $typeDescription[$fieldName]['isMandatory'] ) { |
469 | | - ++$count; |
470 | | - } |
471 | | - |
472 | | - if ( isset( $typeDescription[$fieldName]['validator'] ) ) { |
473 | | - $validator = $typeDescription[$fieldName]['validator']; |
474 | | - if ( !call_user_func( $validator, $fieldValue ) ) { |
475 | | - return false; |
476 | | - } |
477 | | - } |
478 | 494 | } |
479 | | - |
480 | | - if ( $count != $mandatoryCount[$type] ) { |
481 | | - return false; //not all mandatory members are given |
| 495 | + } |
| 496 | + |
| 497 | + if ( $count != $mandatoryCount[$type] ) { |
| 498 | + return false; //not all mandatory members are given |
| 499 | + } |
| 500 | + |
| 501 | + if ( isset( $typeSpec['validator'] ) ) { |
| 502 | + //Call type-specific checker for finer validation |
| 503 | + if ( !call_user_func( $typeSpec['validator'], $fieldDefinition ) ) { |
| 504 | + return false; |
482 | 505 | } |
| 506 | + } |
| 507 | + |
| 508 | + return true; |
| 509 | + } |
| 510 | + |
| 511 | + //Validate the description of a 'section' of preferences |
| 512 | + private static function validateSectionDefinition( $sectionDescription ) { |
| 513 | + if ( !is_array( $sectionDescription ) |
| 514 | + || !isset( $sectionDescription['fields'] ) |
| 515 | + || !is_array( $sectionDescription['fields'] ) ) |
| 516 | + { |
| 517 | + return false; |
| 518 | + } |
| 519 | + |
| 520 | + //Check if 'fields' is a regular (not-associative) array, and that it is not empty |
| 521 | + $count = count( $sectionDescription['fields'] ); |
| 522 | + if ( $count == 0 || array_keys( $sectionDescription['fields'] ) !== range( 0, $count - 1 ) ) { |
| 523 | + return false; |
| 524 | + } |
| 525 | + |
| 526 | + //TODO: validation of members other than $prefs['fields'] |
| 527 | + |
| 528 | + //Flattened preferences |
| 529 | + $flattenedPrefs = array(); |
| 530 | + |
| 531 | + foreach ( $sectionDescription['fields'] as $fieldDefinition ) { |
483 | 532 | |
484 | | - if ( isset( $typeSpec['validator'] ) ) { |
485 | | - //Call type-specific checker for finer validation |
486 | | - if ( !call_user_func( $typeSpec['validator'], $optionDefinition ) ) { |
487 | | - return false; |
488 | | - } |
| 533 | + if ( self::validateFieldDefinition( $fieldDefinition ) == false ) { |
| 534 | + return false; |
489 | 535 | } |
490 | 536 | |
491 | 537 | //flatten preferences described by this field |
492 | | - $flt = self::flattenFieldDescription( $optionDefinition ); |
| 538 | + $flt = self::flattenFieldDescription( $fieldDefinition ); |
493 | 539 | |
494 | 540 | foreach ( $flt as $prefName => $prefDescription ) { |
495 | 541 | //Finally, check that the 'default' fields exists and is valid |
— | — | @@ -740,6 +786,22 @@ |
741 | 787 | return true; |
742 | 788 | } |
743 | 789 | |
| 790 | + //Checker for 'list' preferences |
| 791 | + private static function checkListPref( $prefDescription, $value ) { |
| 792 | + if ( !self::isOrdinaryArray( $value ) ) { |
| 793 | + return false; |
| 794 | + } |
| 795 | + |
| 796 | + $itemDescription = $prefDescription['field']; |
| 797 | + foreach ( $value as $item ) { |
| 798 | + if ( !self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) { |
| 799 | + return false; |
| 800 | + } |
| 801 | + } |
| 802 | + |
| 803 | + return true; |
| 804 | + } |
| 805 | + |
744 | 806 | /** |
745 | 807 | * Checks if $prefs is an array of preferences that passes validation. |
746 | 808 | * It is assumed that $prefsDescription is a valid description of preferences. |
— | — | @@ -782,6 +844,26 @@ |
783 | 845 | return $prefs; |
784 | 846 | } |
785 | 847 | |
| 848 | + //Matcher for 'list' type preferences |
| 849 | + //If value is not an array, just reset to default; otherwise, delete elements that fail validation |
| 850 | + private static function matchListPref( $prefDescription, $prefs, $prefName ) { |
| 851 | + if ( !isset( $prefs[$prefName] ) || !self::isOrdinaryArray( $prefs[$prefName] ) ) { |
| 852 | + $prefs[$prefName] = $prefDescription['default']; |
| 853 | + return $prefs; |
| 854 | + } |
| 855 | + |
| 856 | + $itemDescription = $prefDescription['field']; |
| 857 | + $newItems = array(); |
| 858 | + foreach( $prefs[$prefName] as $item ) { |
| 859 | + if ( self::checkSinglePref( $itemDescription, array( 'dummy' => $item ), 'dummy' ) ) { |
| 860 | + $newItems[] = $item; |
| 861 | + } |
| 862 | + } |
| 863 | + $prefs[$prefName] = $newItems; |
| 864 | + |
| 865 | + return $prefs; |
| 866 | + } |
| 867 | + |
786 | 868 | /** |
787 | 869 | * Fixes $prefs so that it matches the description given by $prefsDescription. |
788 | 870 | * All values of $prefs that fail validation are replaced with default values. |
— | — | @@ -847,6 +929,24 @@ |
848 | 930 | } |
849 | 931 | |
850 | 932 | /** |
| 933 | + * Returns the list of messages used by a field. If the field type specifications define a "getMessages" method, |
| 934 | + * uses it, otherwise returns the message in the 'label' member (if any). |
| 935 | + */ |
| 936 | + private static function getFieldMessages( $fieldDescription ) { |
| 937 | + $type = $fieldDescription['type']; |
| 938 | + $prefSpec = self::$prefsDescriptionSpecifications[$type]; |
| 939 | + if ( isset( $prefSpec['getMessages'] ) ) { |
| 940 | + $getMessages = $prefSpec['getMessages']; |
| 941 | + return call_user_func( $getMessages, $fieldDescription ); |
| 942 | + } else { |
| 943 | + if ( isset( $fieldDescription['label'] ) && self::isMessage( $fieldDescription['label'] ) ) { |
| 944 | + return array( substr( $fieldDescription['label'], 1 ) ); |
| 945 | + } |
| 946 | + } |
| 947 | + return array(); |
| 948 | + } |
| 949 | + |
| 950 | + /** |
851 | 951 | * Returns a list of (unprefixed) messages mentioned by $prefsDescription. It is assumed that |
852 | 952 | * $prefsDescription is valid (i.e.: GadgetPrefs::isPrefsDescriptionValid( $prefsDescription ) === true). |
853 | 953 | * |
— | — | @@ -855,20 +955,9 @@ |
856 | 956 | */ |
857 | 957 | public static function getMessages( $prefsDescription ) { |
858 | 958 | $msgs = array(); |
859 | | - |
860 | | - foreach ( $prefsDescription['fields'] as $prefDesc ) { |
861 | | - $type = $prefDesc['type']; |
862 | | - $prefSpec = self::$prefsDescriptionSpecifications[$type]; |
863 | | - if ( isset( $prefSpec['getMessages'] ) ) { |
864 | | - $getMessages = $prefSpec['getMessages']; |
865 | | - $msgs = array_merge( $msgs, call_user_func( $getMessages, $prefDesc ) ); |
866 | | - } else { |
867 | | - if ( isset( $prefDesc['label'] ) && self::isMessage( $prefDesc['label'] ) ) { |
868 | | - $msgs[] = substr( $prefDesc['label'], 1 ); |
869 | | - } |
870 | | - } |
| 959 | + foreach ( $prefsDescription['fields'] as $fieldDescription ) { |
| 960 | + $msgs = array_merge( $msgs, self::getFieldMessages( $fieldDescription ) ); |
871 | 961 | } |
872 | | - |
873 | 962 | return array_unique( $msgs ); |
874 | 963 | } |
875 | 964 | |
— | — | @@ -898,6 +987,11 @@ |
899 | 988 | return array_unique( $msgs ); |
900 | 989 | } |
901 | 990 | |
| 991 | + //Returns the messages for a 'list' field description |
| 992 | + private static function getListMessages( $prefDescription ) { |
| 993 | + return self::getFieldMessages( $prefDescription['field'] ); |
| 994 | + } |
| 995 | + |
902 | 996 | //Returns the default value of a 'composite' field, that is the object of the |
903 | 997 | //default values of its subfields. |
904 | 998 | private static function getCompositeDefault( $prefDescription ) { |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.css |
— | — | @@ -4,10 +4,6 @@ |
5 | 5 | * Released under the MIT and GPL licenses. |
6 | 6 | */ |
7 | 7 | |
8 | | -.formBuilder-intro { |
9 | | - margin-left: 1em; |
10 | | -} |
11 | | - |
12 | 8 | .formbuilder label { |
13 | 9 | display: inline-block; |
14 | 10 | text-align: right; |
— | — | @@ -53,11 +49,48 @@ |
54 | 50 | |
55 | 51 | /* type-specific styles */ |
56 | 52 | |
57 | | -.formbuilder .formbuilder-slot-type-label label { |
58 | | - width: 100%; |
| 53 | +.formbuilder-field-label label { |
| 54 | + width: 95%; |
59 | 55 | text-align: left; |
60 | 56 | } |
61 | 57 | |
| 58 | +.formbuilder-button { |
| 59 | + cursor: pointer; |
| 60 | +} |
| 61 | + |
| 62 | +.formbuilder-field-list { |
| 63 | + border: 2px ridge; |
| 64 | +} |
| 65 | + |
| 66 | +.formbuilder-list-item-container { |
| 67 | + float: left; |
| 68 | + margin-right: -35px; |
| 69 | + width: 100%; |
| 70 | +} |
| 71 | + |
| 72 | +.formbuilder-list-item-content { |
| 73 | + margin-right: 35px; |
| 74 | +} |
| 75 | + |
| 76 | +.formbuilder-list-item-buttons { |
| 77 | + float: right; |
| 78 | + width: 35px; |
| 79 | +} |
| 80 | + |
| 81 | +.formbuilder-list-button-move, .formbuilder-list-button-delete { |
| 82 | + float: right; |
| 83 | +} |
| 84 | + |
| 85 | +.formbuilder-list-button-move { |
| 86 | + cursor: move; |
| 87 | +} |
| 88 | + |
| 89 | +/* Center the new item button */ |
| 90 | +.formbuilder-list-button-new { |
| 91 | + margin-left: auto; |
| 92 | + margin-right: auto; |
| 93 | +} |
| 94 | + |
62 | 95 | /* formBuilder editor */ |
63 | 96 | |
64 | 97 | .formbuilder-slot-nonempty { |
— | — | @@ -79,10 +112,6 @@ |
80 | 113 | height: 17px; |
81 | 114 | } |
82 | 115 | |
83 | | -.formbuilder-editor-button { |
84 | | - cursor: pointer; |
85 | | -} |
86 | | - |
87 | 116 | .formbuilder-editor-button-move { |
88 | 117 | cursor: move; |
89 | 118 | } |
— | — | @@ -97,6 +126,7 @@ |
98 | 127 | float: right; |
99 | 128 | } |
100 | 129 | |
| 130 | + |
101 | 131 | /* Fixes a minor glitch in Firefox (buttons in tabs not floating properly) */ |
102 | 132 | .formbuilder .ui-tabs-nav a > span :not(.formbuilder-editor-button) { |
103 | 133 | float: left; |
— | — | @@ -111,6 +141,6 @@ |
112 | 142 | margin-left: 6px; |
113 | 143 | } |
114 | 144 | |
115 | | -.formbuilder-slot-editable .formbuilder-slot-type-composite { |
| 145 | +.formbuilder-slot-editable .formbuilder-field-composite { |
116 | 146 | padding: 1em; |
117 | 147 | } |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js |
— | — | @@ -5,6 +5,244 @@ |
6 | 6 | */ |
7 | 7 | |
8 | 8 | (function($, mw) { |
| 9 | + |
| 10 | + //Field types that can be referred to by preference descriptions |
| 11 | + var validFieldTypes = {}; |
| 12 | + |
| 13 | + //Describes 'name' and 'label' field members, common to all "simple" fields |
| 14 | + var simpleFields = [ |
| 15 | + { |
| 16 | + "name": "name", |
| 17 | + "type": "string", |
| 18 | + "label": "name", |
| 19 | + "required": true, |
| 20 | + "maxlength": 40, |
| 21 | + "default": "" |
| 22 | + }, |
| 23 | + { |
| 24 | + "name": "label", |
| 25 | + "type": "string", |
| 26 | + "label": "label", |
| 27 | + "required": false, |
| 28 | + "default": "" |
| 29 | + } |
| 30 | + ]; |
| 31 | + |
| 32 | + //Used by preference editor to build field properties dialogs |
| 33 | + //TODO: document |
| 34 | + var prefsDescriptionSpecifications = { |
| 35 | + "label": { |
| 36 | + "simple": false, |
| 37 | + "builder": [ { |
| 38 | + "name": "label", |
| 39 | + "type": "string", |
| 40 | + "label": "label", |
| 41 | + "required": false, |
| 42 | + "default": "" |
| 43 | + } ] |
| 44 | + }, |
| 45 | + "boolean": { |
| 46 | + "simple": true, |
| 47 | + "builder": simpleFields |
| 48 | + }, |
| 49 | + "string": { |
| 50 | + "simple": true, |
| 51 | + "builder": simpleFields.concat( [ |
| 52 | + { |
| 53 | + "name": "required", |
| 54 | + "type": "boolean", |
| 55 | + "label": "required", |
| 56 | + "default": false |
| 57 | + }, |
| 58 | + { |
| 59 | + "name": "minlength", |
| 60 | + "type": "number", |
| 61 | + "label": "minlength", |
| 62 | + "integer": true, |
| 63 | + "min": 0, |
| 64 | + "required": false, |
| 65 | + "default": null |
| 66 | + }, |
| 67 | + { |
| 68 | + "name": "maxlength", |
| 69 | + "type": "number", |
| 70 | + "label": "maxlength", |
| 71 | + "integer": true, |
| 72 | + "min": 1, |
| 73 | + "required": false, |
| 74 | + "default": null |
| 75 | + } |
| 76 | + ] ) |
| 77 | + }, |
| 78 | + "number": { |
| 79 | + "simple": true, |
| 80 | + "builder": simpleFields.concat( [ |
| 81 | + { |
| 82 | + "name": "required", |
| 83 | + "type": "boolean", |
| 84 | + "label": "required", |
| 85 | + "default": true |
| 86 | + }, |
| 87 | + { |
| 88 | + "name": "integer", |
| 89 | + "type": "boolean", |
| 90 | + "label": "integer", |
| 91 | + "default": false |
| 92 | + }, |
| 93 | + { |
| 94 | + "name": "min", |
| 95 | + "type": "number", |
| 96 | + "label": "min", |
| 97 | + "required": false, |
| 98 | + "default": null |
| 99 | + }, |
| 100 | + { |
| 101 | + "name": "max", |
| 102 | + "type": "number", |
| 103 | + "label": "max", |
| 104 | + "required": false, |
| 105 | + "default": null |
| 106 | + } |
| 107 | + ] ) |
| 108 | + }, |
| 109 | + "range": { |
| 110 | + "simple": true, |
| 111 | + "builder": simpleFields.concat( [ |
| 112 | + { |
| 113 | + "name": "min", |
| 114 | + "type": "number", |
| 115 | + "label": "min", |
| 116 | + "required": true, |
| 117 | + }, |
| 118 | + { |
| 119 | + "name": "step", |
| 120 | + "type": "number", |
| 121 | + "label": "step", |
| 122 | + "required": true, |
| 123 | + "default": 1 |
| 124 | + }, |
| 125 | + { |
| 126 | + "name": "max", |
| 127 | + "type": "number", |
| 128 | + "label": "max", |
| 129 | + "required": true, |
| 130 | + } |
| 131 | + ] ) |
| 132 | + }, |
| 133 | + "date": { |
| 134 | + "simple": true, |
| 135 | + "builder": simpleFields |
| 136 | + }, |
| 137 | + "color": { |
| 138 | + "simple": true, |
| 139 | + "builder": simpleFields |
| 140 | + }, |
| 141 | + "bundle": { |
| 142 | + "simple": false, |
| 143 | + "builder": function( options, callback ) { |
| 144 | + callback( |
| 145 | + new BundleField( { |
| 146 | + "type": "bundle", |
| 147 | + "sections": [ |
| 148 | + { |
| 149 | + "title": "Section 1", |
| 150 | + "fields": [] |
| 151 | + }, |
| 152 | + { |
| 153 | + "title": "Section 2", |
| 154 | + "fields": [] |
| 155 | + } |
| 156 | + ] |
| 157 | + }, options ) |
| 158 | + ); |
| 159 | + } |
| 160 | + }, |
| 161 | + "composite": { |
| 162 | + "simple": true, |
| 163 | + "builder": [ { |
| 164 | + "name": "name", |
| 165 | + "type": "string", |
| 166 | + "label": "name", |
| 167 | + "required": true, |
| 168 | + "maxlength": 40, |
| 169 | + "default": "" |
| 170 | + } ] |
| 171 | + }, |
| 172 | + "list": { |
| 173 | + "simple": true, |
| 174 | + "builder": function( options, callback ) { |
| 175 | + |
| 176 | + //Create list of "simple" types |
| 177 | + var selectOptions = []; |
| 178 | + $.each( prefsDescriptionSpecifications, function( type, typeInfo ) { |
| 179 | + if ( typeInfo.simple === true ) { |
| 180 | + selectOptions.push( { "name": type, "value": type } ); |
| 181 | + } |
| 182 | + } ); |
| 183 | + |
| 184 | + //Create the dialog to chose the field type |
| 185 | + var $form = $( { |
| 186 | + fields: [ { |
| 187 | + "name": "name", |
| 188 | + "type": "string", |
| 189 | + "label": "name", |
| 190 | + "required": true, |
| 191 | + "maxlength": 40, |
| 192 | + "default": "" |
| 193 | + }, |
| 194 | + { |
| 195 | + "name": "type", |
| 196 | + "type": "select", |
| 197 | + "label": "type", |
| 198 | + "options": selectOptions |
| 199 | + } ] |
| 200 | + } ).formBuilder( { idPrefix: 'list-chose-type-' } ) |
| 201 | + .submit( function() { |
| 202 | + return false; //prevent form submission |
| 203 | + } ); |
| 204 | + |
| 205 | + $form.dialog( { |
| 206 | + width: 450, |
| 207 | + modal: true, |
| 208 | + resizable: false, |
| 209 | + title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', 'list' ), |
| 210 | + close: function() { |
| 211 | + $( this ).remove(); |
| 212 | + }, |
| 213 | + buttons: [ |
| 214 | + { |
| 215 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 216 | + click: function() { |
| 217 | + var values = $( this ).formBuilder( 'getValues' ); |
| 218 | + $( this ).dialog( "close" ); |
| 219 | + |
| 220 | + var dialog = this; |
| 221 | + createFieldDialog( { |
| 222 | + type: values.type, |
| 223 | + values: { |
| 224 | + "name": values.name |
| 225 | + }, |
| 226 | + callback: function( field ) { |
| 227 | + $( dialog ).dialog( 'close' ); |
| 228 | + showEditFieldDialog( field.getDesc(), options, callback ); |
| 229 | + return true; |
| 230 | + } |
| 231 | + }, { editable: true } ); |
| 232 | + } |
| 233 | + }, |
| 234 | + { |
| 235 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 236 | + click: function() { |
| 237 | + $( this ).dialog( "close" ); |
| 238 | + } |
| 239 | + } |
| 240 | + ] |
| 241 | + } ); |
| 242 | + } |
| 243 | + } |
| 244 | + }; |
| 245 | + |
| 246 | + /* Utility functions */ |
9 | 247 | |
10 | 248 | //Preprocesses strings end possibly replaces them with messages. |
11 | 249 | //If str starts with "@" the rest of the string is assumed to be |
— | — | @@ -56,6 +294,258 @@ |
57 | 295 | && name.length <= 40; |
58 | 296 | } |
59 | 297 | |
| 298 | + //Make a deep copy of an object |
| 299 | + function clone( obj ) { |
| 300 | + return $.extend( true, {}, obj ); |
| 301 | + } |
| 302 | + |
| 303 | + function deleteFieldRules( field ) { |
| 304 | + //Remove all its validation rules |
| 305 | + var validationSettings = field.getValidationSettings(); |
| 306 | + if ( validationSettings.rules ) { |
| 307 | + $.each( validationSettings.rules, function( name, value ) { |
| 308 | + var $input = $( '#' + name ); |
| 309 | + if ( $input.length > 0 ) { |
| 310 | + $( '#' + name ).rules( 'remove' ); |
| 311 | + } |
| 312 | + } ); |
| 313 | + } |
| 314 | + } |
| 315 | + |
| 316 | + function addFieldRules( field ) { |
| 317 | + var validationSettings = field.getValidationSettings(); |
| 318 | + if ( validationSettings.rules ) { |
| 319 | + $.each( validationSettings.rules, function( name, rules ) { |
| 320 | + var $input = $( '#' + name ); |
| 321 | + |
| 322 | + //Find messages associated to this rule, if any |
| 323 | + if ( typeof validationSettings.messages != 'undefined' && |
| 324 | + typeof validationSettings.messages[name] != 'undefined') |
| 325 | + { |
| 326 | + rules.messages = validationSettings.messages[name]; |
| 327 | + } |
| 328 | + |
| 329 | + if ( $input.length > 0 ) { |
| 330 | + $( '#' + name ).rules( 'add', rules ); |
| 331 | + } |
| 332 | + } ); |
| 333 | + } |
| 334 | + } |
| 335 | + |
| 336 | + function createFieldDialog( params, options ) { |
| 337 | + var self = this; |
| 338 | + |
| 339 | + if ( typeof params.callback != 'function' ) { |
| 340 | + $.error( 'createFieldDialog: missing or wrong "callback" parameter' ); |
| 341 | + } |
| 342 | + |
| 343 | + if ( typeof options == 'undefined' ) { |
| 344 | + options = {}; |
| 345 | + } |
| 346 | + |
| 347 | + var type, description, values; |
| 348 | + if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) { |
| 349 | + //Create a dialog to choose the type of field to create |
| 350 | + var selectOptions = []; |
| 351 | + $.each( validFieldTypes, function( fieldType ) { |
| 352 | + selectOptions.push( { |
| 353 | + name: fieldType, |
| 354 | + value: fieldType |
| 355 | + } ); |
| 356 | + } ); |
| 357 | + |
| 358 | + $( { |
| 359 | + fields: [ { |
| 360 | + 'name': "type", |
| 361 | + 'type': "select", |
| 362 | + 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ), |
| 363 | + 'options': selectOptions, |
| 364 | + 'default': selectOptions[0].value |
| 365 | + } ] |
| 366 | + } ).formBuilder( { idPrefix: 'chose-field-' } ) |
| 367 | + .submit( function() { |
| 368 | + return false; //prevent form submission |
| 369 | + } ) |
| 370 | + .dialog( { |
| 371 | + width: 450, |
| 372 | + modal: true, |
| 373 | + resizable: false, |
| 374 | + title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ), |
| 375 | + close: function() { |
| 376 | + $( this ).remove(); |
| 377 | + }, |
| 378 | + buttons: [ |
| 379 | + { |
| 380 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 381 | + click: function() { |
| 382 | + var values = $( this ).formBuilder( 'getValues' ); |
| 383 | + $( this ).dialog( "close" ); |
| 384 | + createFieldDialog( { |
| 385 | + type: values.type, |
| 386 | + oldDescription: params.oldDescription, |
| 387 | + callback: params.callback |
| 388 | + }, options ); |
| 389 | + } |
| 390 | + }, |
| 391 | + { |
| 392 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 393 | + click: function() { |
| 394 | + $( this ).dialog( "close" ); |
| 395 | + } |
| 396 | + } |
| 397 | + ] |
| 398 | + } ); |
| 399 | + |
| 400 | + return; |
| 401 | + } else { |
| 402 | + type = params.type; |
| 403 | + if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) { |
| 404 | + $.error( 'createFieldDialog: invalid type: ' + type ); |
| 405 | + } else if ( typeof prefsDescriptionSpecifications[type].builder == 'function' ) { |
| 406 | + prefsDescriptionSpecifications[type].builder( options, function( field ) { |
| 407 | + if ( field !== null ) { |
| 408 | + params.callback( field ); |
| 409 | + } |
| 410 | + } ); |
| 411 | + return; |
| 412 | + } |
| 413 | + |
| 414 | + //typeof prefsDescriptionSpecifications[type].builder == 'object' |
| 415 | + |
| 416 | + description = { |
| 417 | + fields: prefsDescriptionSpecifications[type].builder |
| 418 | + }; |
| 419 | + } |
| 420 | + |
| 421 | + if ( typeof params.values != 'undefined' ) { |
| 422 | + values = params.values; |
| 423 | + } else { |
| 424 | + values = {}; |
| 425 | + } |
| 426 | + |
| 427 | + //Create the dialog to set field properties |
| 428 | + var dlg = $( '<div/>' ); |
| 429 | + var form = $( description ).formBuilder( { |
| 430 | + values: values, |
| 431 | + idPrefix: 'create-field-' |
| 432 | + } ).submit( function() { |
| 433 | + return false; //prevent form submission |
| 434 | + } ).appendTo( dlg ); |
| 435 | + |
| 436 | + dlg.dialog( { |
| 437 | + modal: true, |
| 438 | + width: 550, |
| 439 | + resizable: false, |
| 440 | + title: mw.msg( 'gadgets-formbuilder-editor-create-field-title', type ), |
| 441 | + close: function() { |
| 442 | + $( this ).remove(); |
| 443 | + }, |
| 444 | + buttons: [ |
| 445 | + { |
| 446 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 447 | + click: function() { |
| 448 | + var isValid = $( form ).formBuilder( 'validate' ); |
| 449 | + |
| 450 | + if ( isValid ) { |
| 451 | + var fieldDescription = $( form ).formBuilder( 'getValues' ); |
| 452 | + |
| 453 | + if ( typeof type != 'undefined' ) { |
| 454 | + //Remove properties that equal their default |
| 455 | + $.each( description.fields, function( index, fieldSpec ) { |
| 456 | + var property = fieldSpec.name; |
| 457 | + if ( fieldDescription[property] === fieldSpec['default'] ) { |
| 458 | + delete fieldDescription[property]; |
| 459 | + } |
| 460 | + } ); |
| 461 | + } |
| 462 | + |
| 463 | + //Try to create the field. In case of error, warn the user. |
| 464 | + fieldDescription.type = type; |
| 465 | + |
| 466 | + if ( typeof params.oldDescription != 'undefined' ) { |
| 467 | + //If there are values in the old description that cannot be set by |
| 468 | + //the dialog, don't lose them (e.g.: 'fields' member in composite fields). |
| 469 | + $.each( params.oldDescription, function( key, value ) { |
| 470 | + if ( typeof fieldDescription[key] == 'undefined' ) { |
| 471 | + fieldDescription[key] = value; |
| 472 | + } |
| 473 | + } ); |
| 474 | + } |
| 475 | + |
| 476 | + var FieldConstructor = validFieldTypes[type]; |
| 477 | + var field; |
| 478 | + |
| 479 | + try { |
| 480 | + field = new FieldConstructor( fieldDescription, options ); |
| 481 | + } catch ( err ) { |
| 482 | + alert( "Invalid field options: " + err ); //TODO: i18n |
| 483 | + return; |
| 484 | + } |
| 485 | + |
| 486 | + if ( params.callback( field ) === true ) { |
| 487 | + $( this ).dialog( "close" ); |
| 488 | + } |
| 489 | + } |
| 490 | + } |
| 491 | + }, |
| 492 | + { |
| 493 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 494 | + click: function() { |
| 495 | + $( this ).dialog( "close" ); |
| 496 | + params.callback( null ); |
| 497 | + } |
| 498 | + } |
| 499 | + ] |
| 500 | + } ); |
| 501 | + } |
| 502 | + |
| 503 | + function showEditFieldDialog( fieldDesc, options, callback ) { |
| 504 | + $( { "fields": [ fieldDesc ] } ) |
| 505 | + .formBuilder( { |
| 506 | + editable: true, |
| 507 | + staticFields: true, |
| 508 | + idPrefix: 'list-edit-field-' |
| 509 | + } ) |
| 510 | + .submit( function() { |
| 511 | + return false; |
| 512 | + } ) |
| 513 | + .dialog( { |
| 514 | + modal: true, |
| 515 | + width: 550, |
| 516 | + resizable: false, |
| 517 | + title: mw.msg( 'gadgets-formbuilder-editor-edit-field-title' ), |
| 518 | + close: function() { |
| 519 | + $( this ).remove(); |
| 520 | + }, |
| 521 | + buttons: [ |
| 522 | + { |
| 523 | + text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
| 524 | + click: function() { |
| 525 | + var fieldDesc = $( this ).formBuilder( 'getDescription' ).fields[0]; |
| 526 | + name = fieldDesc.name; |
| 527 | + |
| 528 | + delete fieldDesc.name; |
| 529 | + |
| 530 | + $( this ).dialog( "close" ); |
| 531 | + |
| 532 | + callback( new ListField( { |
| 533 | + type: 'list', |
| 534 | + name: name, |
| 535 | + field: fieldDesc |
| 536 | + }, options ) ); |
| 537 | + } |
| 538 | + }, |
| 539 | + { |
| 540 | + text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
| 541 | + click: function() { |
| 542 | + $( this ).dialog( "close" ); |
| 543 | + callback( null ); |
| 544 | + } |
| 545 | + } |
| 546 | + ] |
| 547 | + } ); |
| 548 | + } |
| 549 | + |
60 | 550 | function testOptional( value, element ) { |
61 | 551 | var rules = $( element ).rules(); |
62 | 552 | if ( typeof rules.required == 'undefined' || rules.required === false ) { |
— | — | @@ -109,143 +599,6 @@ |
110 | 600 | return new F(); |
111 | 601 | } |
112 | 602 | |
113 | | - |
114 | | - //Field types that can be referred to by preference descriptions |
115 | | - var validFieldTypes = {}; |
116 | | - |
117 | | - |
118 | | - //Describes 'name' and 'label' field members |
119 | | - var simpleField = [ |
120 | | - { |
121 | | - "name": "name", |
122 | | - "type": "string", |
123 | | - "label": "name", |
124 | | - "required": true, |
125 | | - "maxlength": 40, |
126 | | - "default": "" |
127 | | - }, |
128 | | - { |
129 | | - "name": "label", |
130 | | - "type": "string", |
131 | | - "label": "label", |
132 | | - "required": false, |
133 | | - "default": "" |
134 | | - } |
135 | | - ]; |
136 | | - |
137 | | - //Used by preference editor to build field properties dialogs |
138 | | - var prefsDescriptionSpecifications = { |
139 | | - "label": [ { |
140 | | - "name": "label", |
141 | | - "type": "string", |
142 | | - "label": "label", |
143 | | - "required": false, |
144 | | - "default": "" |
145 | | - } ], |
146 | | - "boolean": simpleField, |
147 | | - "string" : simpleField.concat( [ |
148 | | - { |
149 | | - "name": "required", |
150 | | - "type": "boolean", |
151 | | - "label": "required", |
152 | | - "default": false |
153 | | - }, |
154 | | - { |
155 | | - "name": "minlength", |
156 | | - "type": "number", |
157 | | - "label": "minlength", |
158 | | - "integer": true, |
159 | | - "min": 0, |
160 | | - "required": false, |
161 | | - "default": null |
162 | | - }, |
163 | | - { |
164 | | - "name": "maxlength", |
165 | | - "type": "number", |
166 | | - "label": "maxlength", |
167 | | - "integer": true, |
168 | | - "min": 1, |
169 | | - "required": false, |
170 | | - "default": null |
171 | | - } |
172 | | - ] ), |
173 | | - "number" : simpleField.concat( [ |
174 | | - { |
175 | | - "name": "required", |
176 | | - "type": "boolean", |
177 | | - "label": "required", |
178 | | - "default": true |
179 | | - }, |
180 | | - { |
181 | | - "name": "integer", |
182 | | - "type": "boolean", |
183 | | - "label": "integer", |
184 | | - "default": false |
185 | | - }, |
186 | | - { |
187 | | - "name": "min", |
188 | | - "type": "number", |
189 | | - "label": "min", |
190 | | - "required": false, |
191 | | - "default": null |
192 | | - }, |
193 | | - { |
194 | | - "name": "max", |
195 | | - "type": "number", |
196 | | - "label": "max", |
197 | | - "required": false, |
198 | | - "default": null |
199 | | - } |
200 | | - ] ), |
201 | | - //TODO: "select" is missing |
202 | | - "range": simpleField.concat( [ |
203 | | - { |
204 | | - "name": "min", |
205 | | - "type": "number", |
206 | | - "label": "min", |
207 | | - "required": true, |
208 | | - }, |
209 | | - { |
210 | | - "name": "step", |
211 | | - "type": "number", |
212 | | - "label": "step", |
213 | | - "required": true, |
214 | | - "default": 1 |
215 | | - }, |
216 | | - { |
217 | | - "name": "max", |
218 | | - "type": "number", |
219 | | - "label": "max", |
220 | | - "required": true, |
221 | | - } |
222 | | - ] ), |
223 | | - "date": simpleField, |
224 | | - "color": simpleField, |
225 | | - "bundle": function( options ) { |
226 | | - return new BundleField( { |
227 | | - "type": "bundle", |
228 | | - "sections": [ |
229 | | - { |
230 | | - "title": "Section 1", |
231 | | - "fields": [] |
232 | | - }, |
233 | | - { |
234 | | - "title": "Section 2", |
235 | | - "fields": [] |
236 | | - } |
237 | | - ] |
238 | | - }, options ) |
239 | | - }, |
240 | | - "composite": [ { |
241 | | - "name": "name", |
242 | | - "type": "string", |
243 | | - "label": "name", |
244 | | - "required": true, |
245 | | - "maxlength": 40, |
246 | | - "default": "" |
247 | | - } ] |
248 | | - }; |
249 | | - |
250 | 603 | /* Basic interface for fields */ |
251 | 604 | function Field( desc, options ) { |
252 | 605 | if ( typeof options.idPrefix == 'undefined' ) { |
— | — | @@ -290,7 +643,7 @@ |
291 | 644 | } |
292 | 645 | |
293 | 646 | this.$div = $( '<div/>' ) |
294 | | - .addClass( 'formbuilder-slot-type-' + this.desc.type ) |
| 647 | + .addClass( 'formbuilder-field formbuilder-field-' + this.desc.type ) |
295 | 648 | .data( 'field', this ); |
296 | 649 | } |
297 | 650 | |
— | — | @@ -345,7 +698,7 @@ |
346 | 699 | } |
347 | 700 | |
348 | 701 | SimpleField.prototype.getDesc = function( useValuesAsDefaults ) { |
349 | | - var desc = LabelField.prototype.getDesc.call( this, useValuesAsDefaults ); |
| 702 | + var desc = clone( LabelField.prototype.getDesc.call( this, useValuesAsDefaults ) ); |
350 | 703 | if ( useValuesAsDefaults === true ) { |
351 | 704 | //set 'default' to current value. |
352 | 705 | var values = this.getValues(); |
— | — | @@ -800,40 +1153,6 @@ |
801 | 1154 | |
802 | 1155 | /* A field that represent a section (group of fields) */ |
803 | 1156 | |
804 | | - function deleteFieldRules( field ) { |
805 | | - //Remove all its validation rules |
806 | | - var validationSettings = field.getValidationSettings(); |
807 | | - if ( validationSettings.rules ) { |
808 | | - $.each( validationSettings.rules, function( name, value ) { |
809 | | - var $input = $( '#' + name ); |
810 | | - if ( $input.length > 0 ) { |
811 | | - $( '#' + name ).rules( 'remove' ); |
812 | | - } |
813 | | - } ); |
814 | | - } |
815 | | - } |
816 | | - |
817 | | - function addFieldRules( field ) { |
818 | | - var validationSettings = field.getValidationSettings(); |
819 | | - if ( validationSettings.rules ) { |
820 | | - $.each( validationSettings.rules, function( name, rules ) { |
821 | | - var $input = $( '#' + name ); |
822 | | - |
823 | | - //Find messages associated to this rule, if any |
824 | | - if ( typeof validationSettings.messages != 'undefined' && |
825 | | - typeof validationSettings.messages[name] != 'undefined') |
826 | | - { |
827 | | - rules.messages = validationSettings.messages[name]; |
828 | | - } |
829 | | - |
830 | | - if ( $input.length > 0 ) { |
831 | | - $( '#' + name ).rules( 'add', rules ); |
832 | | - } |
833 | | - } ); |
834 | | - } |
835 | | - } |
836 | | - |
837 | | - |
838 | 1157 | SectionField.prototype = object( Field.prototype ); |
839 | 1158 | SectionField.prototype.constructor = SectionField; |
840 | 1159 | function SectionField( desc, options, id ) { |
— | — | @@ -846,9 +1165,9 @@ |
847 | 1166 | } |
848 | 1167 | |
849 | 1168 | for ( var i = 0; i < this.desc.fields.length; i++ ) { |
850 | | - if ( options.editable === true ) { |
| 1169 | + if ( options.editable === true && !options.staticFields ) { |
851 | 1170 | //add an empty slot |
852 | | - this._createSlot( true ).appendTo( this.$div ); |
| 1171 | + this._createSlot( 'yes' ).appendTo( this.$div ); |
853 | 1172 | } |
854 | 1173 | |
855 | 1174 | var field = this.desc.fields[i], |
— | — | @@ -858,15 +1177,22 @@ |
859 | 1178 | $.error( "field with invalid type: " + field.type ); |
860 | 1179 | } |
861 | 1180 | |
| 1181 | + var editable; |
| 1182 | + if ( options.editable === true ) { |
| 1183 | + editable = options.staticFields ? 'partial' : 'yes'; |
| 1184 | + } else { |
| 1185 | + editable = 'no'; |
| 1186 | + } |
| 1187 | + |
862 | 1188 | var f = new FieldConstructor( field, options ), |
863 | | - $slot = this._createSlot( options.editable === true, f ); |
| 1189 | + $slot = this._createSlot( editable, f ); |
864 | 1190 | |
865 | 1191 | $slot.appendTo( this.$div ); |
866 | 1192 | } |
867 | 1193 | |
868 | | - if ( options.editable === true ) { |
| 1194 | + if ( options.editable === true && !options.staticFields ) { |
869 | 1195 | //add an empty slot |
870 | | - this._createSlot( true ).appendTo( this.$div ); |
| 1196 | + this._createSlot( 'yes' ).appendTo( this.$div ); |
871 | 1197 | } |
872 | 1198 | } |
873 | 1199 | |
— | — | @@ -875,7 +1201,7 @@ |
876 | 1202 | }; |
877 | 1203 | |
878 | 1204 | SectionField.prototype.getDesc = function( useValuesAsDefaults ) { |
879 | | - var desc = this.desc; |
| 1205 | + var desc = clone( this.desc ); |
880 | 1206 | desc.fields = []; |
881 | 1207 | this.$div.children().each( function( idx, slot ) { |
882 | 1208 | var field = $( slot ).data( 'field' ); |
— | — | @@ -916,167 +1242,6 @@ |
917 | 1243 | return settings; |
918 | 1244 | }; |
919 | 1245 | |
920 | | - SectionField.prototype._createFieldDialog = function( params ) { |
921 | | - var self = this; |
922 | | - |
923 | | - if ( typeof params.callback != 'function' ) { |
924 | | - $.error( 'createFieldDialog: missing or wrong "callback" parameter' ); |
925 | | - } |
926 | | - |
927 | | - var type, description, values; |
928 | | - if ( typeof params.description == 'undefined' && typeof params.type == 'undefined' ) { |
929 | | - //Create a dialog to choose the type of field to create |
930 | | - var selectOptions = []; |
931 | | - $.each( validFieldTypes, function( fieldType ) { |
932 | | - selectOptions.push( { |
933 | | - name: fieldType, |
934 | | - value: fieldType |
935 | | - } ); |
936 | | - } ); |
937 | | - |
938 | | - $( { |
939 | | - fields: [ { |
940 | | - 'name': "type", |
941 | | - 'type': "select", |
942 | | - 'label': mw.msg( 'gadgets-formbuilder-editor-chose-field' ), |
943 | | - 'options': selectOptions, |
944 | | - 'default': selectOptions[0].value |
945 | | - } ] |
946 | | - } ).formBuilder( {} ) |
947 | | - .submit( function() { |
948 | | - return false; //prevent form submission |
949 | | - } ) |
950 | | - .dialog( { |
951 | | - width: 450, |
952 | | - modal: true, |
953 | | - resizable: false, |
954 | | - title: mw.msg( 'gadgets-formbuilder-editor-chose-field-title' ), |
955 | | - close: function() { |
956 | | - $( this ).remove(); |
957 | | - }, |
958 | | - buttons: [ |
959 | | - { |
960 | | - text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
961 | | - click: function() { |
962 | | - var values = $( this ).formBuilder( 'getValues' ); |
963 | | - $( this ).dialog( "close" ); |
964 | | - self._createFieldDialog( { |
965 | | - type: values.type, |
966 | | - oldDescription: params.oldDescription, |
967 | | - callback: params.callback |
968 | | - } ); |
969 | | - } |
970 | | - }, |
971 | | - { |
972 | | - text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
973 | | - click: function() { |
974 | | - $( this ).dialog( "close" ); |
975 | | - } |
976 | | - } |
977 | | - ] |
978 | | - } ); |
979 | | - |
980 | | - return; |
981 | | - } else { |
982 | | - type = params.type; |
983 | | - if ( typeof prefsDescriptionSpecifications[type] == 'undefined' ) { |
984 | | - $.error( 'createFieldDialog: invalid type: ' + type ); |
985 | | - } else if ( typeof prefsDescriptionSpecifications[type] == 'function' ) { |
986 | | - var field = prefsDescriptionSpecifications[type]( this.options ); |
987 | | - if ( params.callback( field ) === true ) { |
988 | | - $( this ).dialog( "close" ); |
989 | | - } |
990 | | - return; |
991 | | - } |
992 | | - |
993 | | - //typeof prefsDescriptionSpecifications[type] == 'object' |
994 | | - |
995 | | - description = { |
996 | | - fields: prefsDescriptionSpecifications[type] |
997 | | - }; |
998 | | - } |
999 | | - |
1000 | | - if ( typeof params.values != 'undefined' ) { |
1001 | | - values = params.values; |
1002 | | - } else { |
1003 | | - values = {}; |
1004 | | - } |
1005 | | - |
1006 | | - //Create the dialog to set field properties |
1007 | | - var dlg = $( '<div/>' ); |
1008 | | - var form = $( description ).formBuilder( { |
1009 | | - values: values |
1010 | | - } ).submit( function() { |
1011 | | - return false; //prevent form submission |
1012 | | - } ).appendTo( dlg ); |
1013 | | - |
1014 | | - dlg.dialog( { |
1015 | | - modal: true, |
1016 | | - width: 550, |
1017 | | - resizable: false, |
1018 | | - title: mw.msg( 'gadgets-formbuilder-editor-create-field-title' ), |
1019 | | - close: function() { |
1020 | | - $( this ).remove(); |
1021 | | - }, |
1022 | | - buttons: [ |
1023 | | - { |
1024 | | - text: mw.msg( 'gadgets-formbuilder-editor-ok' ), |
1025 | | - click: function() { |
1026 | | - var isValid = $( form ).formBuilder( 'validate' ); |
1027 | | - |
1028 | | - if ( isValid ) { |
1029 | | - var fieldDescription = $( form ).formBuilder( 'getValues' ); |
1030 | | - |
1031 | | - if ( typeof type != 'undefined' ) { |
1032 | | - //Remove properties that equal their default |
1033 | | - $.each( description.fields, function( index, fieldSpec ) { |
1034 | | - var property = fieldSpec.name; |
1035 | | - if ( fieldDescription[property] === fieldSpec['default'] ) { |
1036 | | - delete fieldDescription[property]; |
1037 | | - } |
1038 | | - } ); |
1039 | | - } |
1040 | | - |
1041 | | - //Try to create the field. In case of error, warn the user. |
1042 | | - fieldDescription.type = type; |
1043 | | - |
1044 | | - if ( typeof params.oldDescription != 'undefined' ) { |
1045 | | - //If there are values in the old description that cannot be set by |
1046 | | - //the dialog, don't lose them (e.g.: 'fields' member in composite fields). |
1047 | | - $.each( params.oldDescription, function( key, value ) { |
1048 | | - if ( typeof fieldDescription[key] == 'undefined' ) { |
1049 | | - fieldDescription[key] = value; |
1050 | | - } |
1051 | | - } ); |
1052 | | - } |
1053 | | - |
1054 | | - var FieldConstructor = validFieldTypes[type]; |
1055 | | - var field; |
1056 | | - |
1057 | | - try { |
1058 | | - field = new FieldConstructor( fieldDescription, self.options ); |
1059 | | - } catch ( err ) { |
1060 | | - alert( "Invalid field options: " + err ); //TODO: i18n |
1061 | | - return; |
1062 | | - } |
1063 | | - |
1064 | | - if ( params.callback( field ) === true ) { |
1065 | | - $( this ).dialog( "close" ); |
1066 | | - } |
1067 | | - } |
1068 | | - } |
1069 | | - }, |
1070 | | - { |
1071 | | - text: mw.msg( 'gadgets-formbuilder-editor-cancel' ), |
1072 | | - click: function() { |
1073 | | - $( this ).dialog( "close" ); |
1074 | | - params.callback( null ); |
1075 | | - } |
1076 | | - } |
1077 | | - ] |
1078 | | - } ); |
1079 | | - }; |
1080 | | - |
1081 | 1246 | SectionField.prototype._deleteSlot = function( $slot ) { |
1082 | 1247 | var field = $slot.data( 'field' ); |
1083 | 1248 | if ( field !== undefined ) { |
— | — | @@ -1093,7 +1258,7 @@ |
1094 | 1259 | $slot = $( '<div/>' ).addClass( 'formbuilder-slot ui-widget' ), |
1095 | 1260 | $divButtons; |
1096 | 1261 | |
1097 | | - if ( editable ) { |
| 1262 | + if ( editable == 'partial' || editable == 'yes' ) { |
1098 | 1263 | $slot.addClass( 'formbuilder-slot-editable' ); |
1099 | 1264 | |
1100 | 1265 | $divButtons = $( '<div/>' ) |
— | — | @@ -1106,26 +1271,29 @@ |
1107 | 1272 | $slot.prepend( field.getElement() ) |
1108 | 1273 | .data( 'field', field ); |
1109 | 1274 | |
1110 | | - if ( editable ) { |
| 1275 | + if ( editable == 'partial' || editable == 'yes' ) { |
1111 | 1276 | $slot.addClass( 'formbuilder-slot-nonempty' ); |
1112 | 1277 | |
1113 | | - //Add the handle for moving slots |
1114 | | - $( '<span />' ) |
1115 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' ) |
1116 | | - .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) ) |
1117 | | - .mousedown( function() { |
1118 | | - $( this ).focus(); |
1119 | | - } ) |
1120 | | - .appendTo( $divButtons ); |
| 1278 | + if ( editable == 'yes' ) { |
| 1279 | + //Add the handle for moving slots |
| 1280 | + $( '<span />' ) |
| 1281 | + .addClass( 'formbuilder-button formbuilder-editor-button-move ui-icon ui-icon-arrow-4' ) |
| 1282 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) ) |
| 1283 | + .mousedown( function() { |
| 1284 | + $( this ).focus(); |
| 1285 | + } ) |
| 1286 | + .appendTo( $divButtons ); |
| 1287 | + } |
1121 | 1288 | |
1122 | 1289 | //Add the button for changing existing slots |
1123 | 1290 | var type = field.getDesc().type; |
1124 | | - if ( typeof prefsDescriptionSpecifications[type] != 'function' ) { |
| 1291 | + //TODO: using the 'builder' info is not optimal |
| 1292 | + if ( typeof prefsDescriptionSpecifications[type].builder != 'function' ) { |
1125 | 1293 | $( '<a href="javascript:;" />' ) |
1126 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit ui-icon ui-icon-gear' ) |
| 1294 | + .addClass( 'formbuilder-button formbuilder-editor-button-edit ui-icon ui-icon-gear' ) |
1127 | 1295 | .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-field' ) ) |
1128 | 1296 | .click( function() { |
1129 | | - self._createFieldDialog( { |
| 1297 | + createFieldDialog( { |
1130 | 1298 | type: field.getDesc().type, |
1131 | 1299 | values: field.getDesc(), |
1132 | 1300 | oldDescription: field.getDesc(), |
— | — | @@ -1148,7 +1316,7 @@ |
1149 | 1317 | return false; |
1150 | 1318 | } |
1151 | 1319 | |
1152 | | - var $newSlot = self._createSlot( true, newField ); |
| 1320 | + var $newSlot = self._createSlot( 'yes', newField ); |
1153 | 1321 | |
1154 | 1322 | deleteFieldRules( field ); |
1155 | 1323 | |
— | — | @@ -1159,39 +1327,41 @@ |
1160 | 1328 | } |
1161 | 1329 | return true; |
1162 | 1330 | } |
1163 | | - } ); |
| 1331 | + }, this.options ); |
1164 | 1332 | } ) |
1165 | 1333 | .appendTo( $divButtons ); |
1166 | 1334 | } |
1167 | 1335 | |
1168 | | - //Add the button to delete slots |
1169 | | - $( '<a href="javascript:;" />' ) |
1170 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete ui-icon ui-icon-trash' ) |
1171 | | - .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) ) |
1172 | | - .click( function( event, ui ) { |
1173 | | - //Make both slots disappear, then delete them |
1174 | | - $.each( [$slot, $slot.prev()], function( idx, $s ) { |
1175 | | - $s.slideUp( function() { |
1176 | | - self._deleteSlot( $s ); |
| 1336 | + if ( editable == 'yes' ) { |
| 1337 | + //Add the button to delete slots |
| 1338 | + $( '<a href="javascript:;" />' ) |
| 1339 | + .addClass( 'formbuilder-button formbuilder-editor-button-delete ui-icon ui-icon-trash' ) |
| 1340 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-field' ) ) |
| 1341 | + .click( function( event, ui ) { |
| 1342 | + //Make both slots disappear, then delete them |
| 1343 | + $.each( [$slot, $slot.prev()], function( idx, $s ) { |
| 1344 | + $s.slideUp( function() { |
| 1345 | + self._deleteSlot( $s ); |
| 1346 | + } ); |
1177 | 1347 | } ); |
1178 | | - } ); |
1179 | | - } ) |
1180 | | - .appendTo( $divButtons ); |
| 1348 | + } ) |
| 1349 | + .appendTo( $divButtons ); |
1181 | 1350 | |
1182 | | - //Make this slot draggable to allow moving it |
1183 | | - $slot.draggable( { |
1184 | | - revert: true, |
1185 | | - handle: ".formbuilder-editor-button-move", |
1186 | | - helper: "original", |
1187 | | - zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way |
1188 | | - scroll: false, |
1189 | | - opacity: 0.8, |
1190 | | - cursor: "move", |
1191 | | - cursorAt: { |
1192 | | - top: -5, |
1193 | | - left: -5 |
1194 | | - } |
1195 | | - } ); |
| 1351 | + //Make this slot draggable to allow moving it |
| 1352 | + $slot.draggable( { |
| 1353 | + revert: true, |
| 1354 | + handle: ".formbuilder-editor-button-move", |
| 1355 | + helper: "original", |
| 1356 | + zIndex: $slot.closest( '.formbuilder' ).zIndex() + 1000, //TODO: ugly, find a better way |
| 1357 | + scroll: false, |
| 1358 | + opacity: 0.8, |
| 1359 | + cursor: "move", |
| 1360 | + cursorAt: { |
| 1361 | + top: -5, |
| 1362 | + left: -5 |
| 1363 | + } |
| 1364 | + } ); |
| 1365 | + } |
1196 | 1366 | } |
1197 | 1367 | } else { |
1198 | 1368 | //Create empty slot |
— | — | @@ -1209,8 +1379,8 @@ |
1210 | 1380 | $( dstSlot ).replaceWith( srcSlot ); |
1211 | 1381 | |
1212 | 1382 | //Add one empty slot before and one after the new position |
1213 | | - self._createSlot( true ).insertBefore( srcSlot ); |
1214 | | - self._createSlot( true ).insertAfter( srcSlot ); |
| 1383 | + self._createSlot( 'yes' ).insertBefore( srcSlot ); |
| 1384 | + self._createSlot( 'yes' ).insertAfter( srcSlot ); |
1215 | 1385 | }, |
1216 | 1386 | accept: function( draggable ) { |
1217 | 1387 | //All non empty slots accepted, except for closest siblings |
— | — | @@ -1222,10 +1392,10 @@ |
1223 | 1393 | |
1224 | 1394 | //The button to create a new field |
1225 | 1395 | $( '<a href="javascript:;" />' ) |
1226 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-new ui-icon ui-icon-plus' ) |
| 1396 | + .addClass( 'formbuilder-button formbuilder-editor-button-new ui-icon ui-icon-plus' ) |
1227 | 1397 | .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) ) |
1228 | 1398 | .click( function() { |
1229 | | - self._createFieldDialog( { |
| 1399 | + createFieldDialog( { |
1230 | 1400 | callback: function( field ) { |
1231 | 1401 | if ( field !== null ) { |
1232 | 1402 | //check that there are no duplicate preference names |
— | — | @@ -1243,8 +1413,8 @@ |
1244 | 1414 | return false; |
1245 | 1415 | } |
1246 | 1416 | |
1247 | | - var $newSlot = self._createSlot( true, field ).hide(), |
1248 | | - $newEmptySlot = self._createSlot( true ).hide(); |
| 1417 | + var $newSlot = self._createSlot( 'yes', field ).hide(), |
| 1418 | + $newEmptySlot = self._createSlot( 'yes' ).hide(); |
1249 | 1419 | |
1250 | 1420 | $slot.after( $newSlot, $newEmptySlot ); |
1251 | 1421 | |
— | — | @@ -1259,7 +1429,7 @@ |
1260 | 1430 | } |
1261 | 1431 | return true; |
1262 | 1432 | } |
1263 | | - } ); |
| 1433 | + }, self.options ); |
1264 | 1434 | } ) |
1265 | 1435 | .appendTo( $divButtons ); |
1266 | 1436 | } |
— | — | @@ -1309,7 +1479,7 @@ |
1310 | 1480 | if ( options.editable === true ) { |
1311 | 1481 | //Add "delete section" button |
1312 | 1482 | $( '<span />' ) |
1313 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' ) |
| 1483 | + .addClass( 'formbuilder-button formbuilder-editor-button-delete-section ui-icon ui-icon-trash' ) |
1314 | 1484 | .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete-section' ) ) |
1315 | 1485 | .click( function() { |
1316 | 1486 | var sectionField = $( ui.panel ).data( 'field' ); |
— | — | @@ -1324,7 +1494,7 @@ |
1325 | 1495 | |
1326 | 1496 | //Add "edit section" button |
1327 | 1497 | $( '<span />' ) |
1328 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' ) |
| 1498 | + .addClass( 'formbuilder-button formbuilder-editor-button-edit-section ui-icon ui-icon-gear' ) |
1329 | 1499 | .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-edit-section' ) ) |
1330 | 1500 | .click( function() { |
1331 | 1501 | var button = this, |
— | — | @@ -1339,7 +1509,8 @@ |
1340 | 1510 | } ).formBuilder( { |
1341 | 1511 | values: { |
1342 | 1512 | title: sectionField.getDesc().title |
1343 | | - } |
| 1513 | + }, |
| 1514 | + idPrefix: 'section-edit-title-' |
1344 | 1515 | } ).dialog( { |
1345 | 1516 | modal: true, |
1346 | 1517 | resizable: false, |
— | — | @@ -1391,7 +1562,7 @@ |
1392 | 1563 | if ( options.editable === true ) { |
1393 | 1564 | //Add the button to create a new section |
1394 | 1565 | $( '<span>' ) |
1395 | | - .addClass( 'formbuilder-editor-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' ) |
| 1566 | + .addClass( 'formbuilder-button formbuilder-editor-button-new-section ui-icon ui-icon-plus' ) |
1396 | 1567 | .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-new-section' ) ) |
1397 | 1568 | .click( function() { |
1398 | 1569 | $( { |
— | — | @@ -1400,7 +1571,7 @@ |
1401 | 1572 | 'type': "string", |
1402 | 1573 | 'label': mw.msg( 'gadgets-formbuilder-editor-chose-title' ) |
1403 | 1574 | } ] |
1404 | | - } ).formBuilder( {} ).dialog( { |
| 1575 | + } ).formBuilder( { idPrefix: 'section-create-' } ).dialog( { |
1405 | 1576 | modal: true, |
1406 | 1577 | resizable: false, |
1407 | 1578 | title: mw.msg( 'gadgets-formbuilder-editor-chose-title-title' ), |
— | — | @@ -1459,7 +1630,7 @@ |
1460 | 1631 | }; |
1461 | 1632 | |
1462 | 1633 | BundleField.prototype.getDesc = function( useValuesAsDefaults ) { |
1463 | | - var desc = this.desc; |
| 1634 | + var desc = clone( this.desc ); |
1464 | 1635 | desc.sections = []; |
1465 | 1636 | this.$ui_tabs_nav.find( 'a' ).each( function( idx, anchor ) { |
1466 | 1637 | var panel = $( anchor ).data( 'panel' ), |
— | — | @@ -1503,7 +1674,7 @@ |
1504 | 1675 | |
1505 | 1676 | //TODO: add something to easily visually identify 'composite' fields during editing |
1506 | 1677 | |
1507 | | - var sectionOptions = $.extend( {}, options ); |
| 1678 | + var sectionOptions = clone( options ); |
1508 | 1679 | |
1509 | 1680 | //Add another chunk to the prefix, to ensure uniqueness |
1510 | 1681 | sectionOptions.idPrefix += desc.name + '-'; |
— | — | @@ -1517,7 +1688,7 @@ |
1518 | 1689 | } |
1519 | 1690 | |
1520 | 1691 | CompositeField.prototype.getDesc = function( useValuesAsDefaults ) { |
1521 | | - var desc = this.desc; |
| 1692 | + var desc = clone( this.desc ); |
1522 | 1693 | desc.fields = this._section.getDesc( useValuesAsDefaults ).fields; |
1523 | 1694 | return desc; |
1524 | 1695 | }; |
— | — | @@ -1532,6 +1703,155 @@ |
1533 | 1704 | |
1534 | 1705 | validFieldTypes["composite"] = CompositeField; |
1535 | 1706 | |
| 1707 | + /* A field for 'composite' fields */ |
| 1708 | + |
| 1709 | + ListField.prototype = object( EmptyField.prototype ); |
| 1710 | + ListField.prototype.constructor = ListField; |
| 1711 | + function ListField( desc, options ) { |
| 1712 | + EmptyField.call( this, desc, options ); |
| 1713 | + |
| 1714 | + if ( typeof desc.field != 'object' ) { |
| 1715 | + $.error( "The 'field' parameter is missing or wrong" ); |
| 1716 | + } |
| 1717 | + |
| 1718 | + if ( typeof desc.field.name != 'undefined' ) { |
| 1719 | + $.error( "The 'field' parameter must not specify the field 'name'" ); |
| 1720 | + } |
| 1721 | + |
| 1722 | + if ( ( typeof desc.field.type != 'string' ) |
| 1723 | + || prefsDescriptionSpecifications[desc.field.type].simple !== true ) |
| 1724 | + { |
| 1725 | + $.error( "Missing or invalid field type specified in 'field' parameter." ); |
| 1726 | + } |
| 1727 | + |
| 1728 | + this._$divItems = $( '<div/>' ).addClass( 'formbuilder-list-items' ); |
| 1729 | + |
| 1730 | + if ( typeof options.values == 'undefined' ) { |
| 1731 | + options.values = {}; |
| 1732 | + } |
| 1733 | + |
| 1734 | + var value = ( typeof options.values[desc.name] != 'undefined' ) ? options.values[desc.name] : desc['default']; |
| 1735 | + var self = this; |
| 1736 | + if ( typeof value != 'undefined' ) { |
| 1737 | + $.each( value, function( index, itemValue ) { |
| 1738 | + self._createItem( false, itemValue ); |
| 1739 | + } ); |
| 1740 | + } |
| 1741 | + |
| 1742 | + this._$divItems.sortable( { |
| 1743 | + axis: 'y', |
| 1744 | + items: '.formbuilder-list-item', |
| 1745 | + handle: '.formbuilder-list-button-move', |
| 1746 | + placeholder: 'ui-state-highlight', |
| 1747 | + forcePlaceholderSize: true |
| 1748 | + } ) |
| 1749 | + .appendTo( this.$div ); |
| 1750 | + |
| 1751 | + $( '<a href="javascript:;" />' ) |
| 1752 | + .addClass( 'formbuilder-button formbuilder-list-button-new ui-icon ui-icon-plus' ) |
| 1753 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-insert-field' ) ) |
| 1754 | + .click( function() { |
| 1755 | + self._createItem( true ); |
| 1756 | + } ) |
| 1757 | + .appendTo( this.$div ); |
| 1758 | + } |
| 1759 | + |
| 1760 | + ListField.prototype._createItem = function( animated, itemValue ) { |
| 1761 | + var itemDesc = $.extend( {}, this.desc.field, { |
| 1762 | + "name": this.desc.name |
| 1763 | + } ); |
| 1764 | + var itemOptions = $.extend( {}, this.options, { |
| 1765 | + editable: false, |
| 1766 | + idPrefix: this.options.idPrefix + getIncrementalCounter() + "-" |
| 1767 | + } ); |
| 1768 | + |
| 1769 | + if ( typeof itemValue != 'undefined' ) { |
| 1770 | + itemOptions.values = pair( this.desc.name, itemValue ); |
| 1771 | + } else { |
| 1772 | + itemOptions.values = pair( this.desc.name, this.desc.field['default'] ); |
| 1773 | + } |
| 1774 | + |
| 1775 | + var FieldConstructor = validFieldTypes[this.desc.field.type]; |
| 1776 | + var itemField = new FieldConstructor( itemDesc, itemOptions ); |
| 1777 | + var $itemDiv = $( '<div/>' ) |
| 1778 | + .addClass( 'formbuilder-list-item' ) |
| 1779 | + .data( 'field', itemField ); |
| 1780 | + |
| 1781 | + var $itemContent = $( '<div/>' ) |
| 1782 | + .addClass( 'formbuilder-list-item-content' ) |
| 1783 | + .append( itemField.getElement() ); |
| 1784 | + |
| 1785 | + $( '<div/>' ) |
| 1786 | + .addClass( 'formbuilder-list-item-container' ) |
| 1787 | + .append( $itemContent ) |
| 1788 | + .appendTo( $itemDiv ); |
| 1789 | + |
| 1790 | + var $itemButtons = $( '<div/>' ) |
| 1791 | + .addClass( 'formbuilder-list-item-buttons' ); |
| 1792 | + |
| 1793 | + |
| 1794 | + $( '<span/>' ) |
| 1795 | + .addClass( 'formbuilder-button formbuilder-list-button-delete ui-icon ui-icon-trash' ) |
| 1796 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-delete' ) ) |
| 1797 | + .click( function() { |
| 1798 | + $itemDiv.slideUp( function() { |
| 1799 | + deleteFieldRules( itemField ); |
| 1800 | + $itemDiv.remove(); |
| 1801 | + } ); |
| 1802 | + } ) |
| 1803 | + .appendTo( $itemButtons ); |
| 1804 | + |
| 1805 | + $( '<span/>' ) |
| 1806 | + .addClass( 'formbuilder-button formbuilder-list-button-move ui-icon ui-icon-arrow-4' ) |
| 1807 | + .attr( 'title', mw.msg( 'gadgets-formbuilder-editor-move' ) ) |
| 1808 | + .appendTo( $itemButtons ); |
| 1809 | + |
| 1810 | + $itemButtons.appendTo( $itemDiv ); |
| 1811 | + |
| 1812 | + //Add an empty div with clear:both style |
| 1813 | + $itemDiv.append( $('<div style="clear:both"></div>' ) ); |
| 1814 | + |
| 1815 | + if ( animated ) { |
| 1816 | + $itemDiv.hide() |
| 1817 | + .appendTo( this._$divItems ) |
| 1818 | + .slideDown(); |
| 1819 | + } else { |
| 1820 | + $itemDiv.appendTo( this._$divItems ); |
| 1821 | + } |
| 1822 | + }; |
| 1823 | + |
| 1824 | + ListField.prototype.getDesc = function( useValuesAsDefaults ) { |
| 1825 | + var desc = clone( this.desc ); |
| 1826 | + if ( useValuesAsDefaults ) { |
| 1827 | + desc['default'] = this.getValues()[this.desc.name]; |
| 1828 | + } |
| 1829 | + return desc; |
| 1830 | + }; |
| 1831 | + |
| 1832 | + ListField.prototype.getValues = function() { |
| 1833 | + var value = []; |
| 1834 | + this._$divItems.children().each( function( index, divItem ) { |
| 1835 | + var field = $( divItem ).data( 'field' ); |
| 1836 | + $.each( field.getValues(), function( name, v ) { |
| 1837 | + value.push( v ); |
| 1838 | + } ); |
| 1839 | + } ); |
| 1840 | + |
| 1841 | + return pair( this.desc.name, value ); |
| 1842 | + }; |
| 1843 | + |
| 1844 | + ListField.prototype.getValidationSettings = function() { |
| 1845 | + var validationSettings = {}; |
| 1846 | + this._$divItems.children().each( function( index, divItem ) { |
| 1847 | + var field = $( divItem ).data( 'field' ); |
| 1848 | + $.extend( true, validationSettings, field.getValidationSettings() ); |
| 1849 | + } ); |
| 1850 | + return validationSettings; |
| 1851 | + }; |
| 1852 | + |
| 1853 | + validFieldTypes["list"] = ListField; |
| 1854 | + |
| 1855 | + |
1536 | 1856 | /* Public methods */ |
1537 | 1857 | |
1538 | 1858 | /** |
— | — | @@ -1559,7 +1879,8 @@ |
1560 | 1880 | idPrefix: options.idPrefix, |
1561 | 1881 | msgPrefix: options.msgPrefix, |
1562 | 1882 | values: options.values, |
1563 | | - editable: options.editable === true |
| 1883 | + editable: options.editable === true, |
| 1884 | + staticFields: options.staticFields === true |
1564 | 1885 | } ); |
1565 | 1886 | |
1566 | 1887 | section.getElement().appendTo( $form ); |