Index: branches/salvatoreingala/Gadgets/Gadgets.i18n.php |
— | — | @@ -46,8 +46,9 @@ |
47 | 47 | 'gadgets-configure' => 'Configure', |
48 | 48 | 'gadgets-configuration-of' => 'Configuration of $1', |
49 | 49 | 'gadgets-prefs-save' => 'Save', |
50 | | - 'gadgets-prefs-cancel' => 'Cancel', |
| 50 | + 'gadgets-prefs-close' => 'Close', |
51 | 51 | 'gadgets-unexpected-error' => 'An unexpected error occurred. Please check your connection. If the problems persists, please contact the developers.', |
| 52 | + 'gadgets-save-invalid' => 'There are fields with invalid value.', |
52 | 53 | 'gadgets-save-success' => 'Settings saved successfully.', |
53 | 54 | 'gadgets-save-failed' => 'Failed to save settings. If the problems persists, please contact the developers.', |
54 | 55 | 'gadgets-ajax-wrongparams' => 'An AJAX request with wrong parameters has been made; this is most likely a bug.', |
Index: branches/salvatoreingala/Gadgets/backend/GadgetHooks.php |
— | — | @@ -131,14 +131,15 @@ |
132 | 132 | //Register the ext.gadgets.preferences module |
133 | 133 | //TODO: fix caching issues for user-defined messages |
134 | 134 | $resourceLoader->register( 'ext.gadgets.preferences', array( |
135 | | - 'scripts' => array( 'ext.gadgets.preferences.js' ), |
136 | | - 'dependencies' => array( |
| 135 | + 'scripts' => array( 'ext.gadgets.preferences.js' ), |
| 136 | + 'styles' => array( 'ext.gadgets.preferences.css' ), |
| 137 | + 'dependencies' => array( |
137 | 138 | 'jquery', 'jquery.json', 'jquery.ui.dialog', 'jquery.formBuilder', |
138 | 139 | 'mediawiki.htmlform', 'ext.gadgets' |
139 | 140 | ), |
140 | 141 | 'messages' => array_merge( $messages, array( |
141 | | - 'gadgets-configure', 'gadgets-configuration-of', 'gadgets-prefs-save', 'gadgets-prefs-cancel', |
142 | | - 'gadgets-unexpected-error', 'gadgets-save-success', 'gadgets-save-failed' |
| 142 | + 'gadgets-configure', 'gadgets-configuration-of', 'gadgets-prefs-save', 'gadgets-prefs-close', |
| 143 | + 'gadgets-unexpected-error', 'gadgets-save-invalid', 'gadgets-save-success', 'gadgets-save-failed' |
143 | 144 | ) ), |
144 | 145 | 'localBasePath' => dirname( dirname( __FILE__ ) ) . '/ui/resources/', |
145 | 146 | 'remoteExtPath' => 'Gadgets/ui/resources' |
Index: branches/salvatoreingala/Gadgets/ui/resources/jquery.formBuilder.js |
— | — | @@ -599,6 +599,20 @@ |
600 | 600 | return new F(); |
601 | 601 | } |
602 | 602 | |
| 603 | + //Add a "smart" listener to watch for changes to an <input /> element |
| 604 | + //This binds to several events, but calls the callback only if the value actually changed |
| 605 | + function addSmartChangeListener( $input, callback ) { |
| 606 | + var oldValue = $input.val(); |
| 607 | + //bind all events that may change the value of the field (some are brower-specific) |
| 608 | + $input.bind( 'keyup change propertychange input paste', function() { |
| 609 | + var newValue = $input.val(); |
| 610 | + if ( oldValue !== newValue ) { |
| 611 | + oldValue = newValue; |
| 612 | + callback(); |
| 613 | + } |
| 614 | + } ); |
| 615 | + } |
| 616 | + |
603 | 617 | /* Basic interface for fields */ |
604 | 618 | function Field( desc, options ) { |
605 | 619 | if ( typeof options.idPrefix == 'undefined' ) { |
— | — | @@ -721,6 +735,12 @@ |
722 | 736 | name: this.options.idPrefix + this.desc.name |
723 | 737 | } ); |
724 | 738 | |
| 739 | + if ( options.change ) { |
| 740 | + this.$c.change( function() { |
| 741 | + options.change() |
| 742 | + } ); |
| 743 | + } |
| 744 | + |
725 | 745 | var value = options.values && options.values[this.desc.name]; |
726 | 746 | if ( typeof value != 'undefined' ) { |
727 | 747 | if ( typeof value != 'boolean' ) { |
— | — | @@ -764,7 +784,7 @@ |
765 | 785 | id: this.options.idPrefix + this.desc.name, |
766 | 786 | name: this.options.idPrefix + this.desc.name |
767 | 787 | } ); |
768 | | - |
| 788 | + |
769 | 789 | var value = options.values && options.values[this.desc.name]; |
770 | 790 | if ( typeof value != 'undefined' ) { |
771 | 791 | if ( typeof value != 'string' ) { |
— | — | @@ -774,6 +794,11 @@ |
775 | 795 | this.$text.val( value ); |
776 | 796 | } |
777 | 797 | |
| 798 | + //Add the change event listener |
| 799 | + if ( options.change ) { |
| 800 | + addSmartChangeListener( this.$text, options.change ); |
| 801 | + } |
| 802 | + |
778 | 803 | this.$div.append( this.$text ); |
779 | 804 | } |
780 | 805 | |
— | — | @@ -849,6 +874,11 @@ |
850 | 875 | this.$text.val( value ); |
851 | 876 | } |
852 | 877 | |
| 878 | + //Add the change event listener |
| 879 | + if ( options.change ) { |
| 880 | + addSmartChangeListener( this.$text, options.change ); |
| 881 | + } |
| 882 | + |
853 | 883 | this.$div.append( this.$text ); |
854 | 884 | } |
855 | 885 | |
— | — | @@ -928,6 +958,13 @@ |
929 | 959 | $select.val( i ).prop( 'selected', 'selected' ); |
930 | 960 | } |
931 | 961 | |
| 962 | + //Add the change event listener |
| 963 | + if ( options.change ) { |
| 964 | + $select.change( function() { |
| 965 | + options.change(); |
| 966 | + } ); |
| 967 | + } |
| 968 | + |
932 | 969 | this.$div.append( $select ); |
933 | 970 | } |
934 | 971 | |
— | — | @@ -973,17 +1010,17 @@ |
974 | 1011 | var $slider = this.$slider = $( '<div/>' ) |
975 | 1012 | .attr( 'id', this.options.idPrefix + this.desc.name ); |
976 | 1013 | |
977 | | - var rangeOptions = { |
| 1014 | + var sliderOptions = { |
978 | 1015 | min: this.desc.min, |
979 | 1016 | max: this.desc.max |
980 | 1017 | }; |
981 | 1018 | |
982 | 1019 | if ( typeof value != 'undefined' ) { |
983 | | - rangeOptions.value = value; |
| 1020 | + sliderOptions.value = value; |
984 | 1021 | } |
985 | 1022 | |
986 | 1023 | if ( typeof this.desc.step != 'undefined' ) { |
987 | | - rangeOptions.step = this.desc.step; |
| 1024 | + sliderOptions.step = this.desc.step; |
988 | 1025 | } |
989 | 1026 | |
990 | 1027 | //A tooltip to show current value. |
— | — | @@ -1017,7 +1054,7 @@ |
1018 | 1055 | } ); |
1019 | 1056 | } |
1020 | 1057 | |
1021 | | - $.extend( rangeOptions, { |
| 1058 | + $.extend( sliderOptions, { |
1022 | 1059 | start: function( event, ui ) { |
1023 | 1060 | sliding = true; |
1024 | 1061 | }, |
— | — | @@ -1034,10 +1071,15 @@ |
1035 | 1072 | } |
1036 | 1073 | }, 300 ); |
1037 | 1074 | sliding = false; |
| 1075 | + }, |
| 1076 | + change: function( event, ui ) { |
| 1077 | + if ( options.change ) { |
| 1078 | + options.change(); |
| 1079 | + } |
1038 | 1080 | } |
1039 | 1081 | } ); |
1040 | 1082 | |
1041 | | - $slider.slider( rangeOptions ); |
| 1083 | + $slider.slider( sliderOptions ); |
1042 | 1084 | |
1043 | 1085 | var $handle = $slider.find( '.ui-slider-handle' ) |
1044 | 1086 | .focus( function( event ) { |
— | — | @@ -1074,7 +1116,7 @@ |
1075 | 1117 | function DateField( desc, options ){ |
1076 | 1118 | SimpleField.call( this, desc, options ); |
1077 | 1119 | |
1078 | | - this.$text = $( '<input/>' ) |
| 1120 | + var $text = this.$text = $( '<input/>' ) |
1079 | 1121 | .attr( { |
1080 | 1122 | type: 'text', |
1081 | 1123 | id: this.options.idPrefix + this.desc.name, |
— | — | @@ -1083,6 +1125,8 @@ |
1084 | 1126 | onSelect: function() { |
1085 | 1127 | //Force validation, so that a previous 'invalid' state is removed |
1086 | 1128 | $( this ).valid(); |
| 1129 | + //trigger change event on the textbox |
| 1130 | + $text.trigger( 'change' ); |
1087 | 1131 | } |
1088 | 1132 | } ); |
1089 | 1133 | |
— | — | @@ -1098,6 +1142,11 @@ |
1099 | 1143 | this.$text.datepicker( 'setDate', date ); |
1100 | 1144 | } |
1101 | 1145 | |
| 1146 | + //Add the change event listener |
| 1147 | + if ( options.change ) { |
| 1148 | + addSmartChangeListener( this.$text, options.change ); |
| 1149 | + } |
| 1150 | + |
1102 | 1151 | this.$div.append( this.$text ); |
1103 | 1152 | } |
1104 | 1153 | |
— | — | @@ -1196,6 +1245,11 @@ |
1197 | 1246 | } ) |
1198 | 1247 | .blur( closeColorPicker ); |
1199 | 1248 | |
| 1249 | + //Add the change event listener |
| 1250 | + if ( options.change ) { |
| 1251 | + addSmartChangeListener( this.$text, options.change ); |
| 1252 | + } |
| 1253 | + |
1200 | 1254 | this.$div.append( this.$text ); |
1201 | 1255 | } |
1202 | 1256 | |
— | — | @@ -1929,7 +1983,18 @@ |
1930 | 1984 | * Main method; takes the given preferences description object and builds |
1931 | 1985 | * the body of the form with the requested fields. |
1932 | 1986 | * |
1933 | | - * @param {Object} options |
| 1987 | + * @param {Object} options options to set properties of the form and to change its behaviour. |
| 1988 | + * Valid options: |
| 1989 | + * idPrefix: compulsory, a unique prefix for all ids of elements created by formBuilder, to avoid conflicts. |
| 1990 | + * msgPrefix: compulsory, a prefix to be added to all messages referred to by the description. |
| 1991 | + * values: optional, a map of values of preferences; if omitted, default values will be used. |
| 1992 | + * editable: optional, defaults to false; true if the form must be editable via the UI; used by the preferences editor. |
| 1993 | + * staticFields: optional, defaults to false; ignored if editable is not true. If both editable and staticFields are true, |
| 1994 | + * insertion or removal of fields is not allowed, but changing field properties is. Used by the preferences |
| 1995 | + * editor. |
| 1996 | + * change: optional, a function that will be called back if the value of a field changes (it's not strictly warranted that |
| 1997 | + * a field value has actually changed). |
| 1998 | + * |
1934 | 1999 | * @return {Element} the object with the requested form body. |
1935 | 2000 | */ |
1936 | 2001 | function buildFormBody( options ) { |
— | — | @@ -1946,16 +2011,10 @@ |
1947 | 2012 | return null; |
1948 | 2013 | } |
1949 | 2014 | |
1950 | | - var section = new SectionField( description, { |
1951 | | - idPrefix: options.idPrefix, |
1952 | | - msgPrefix: options.msgPrefix, |
1953 | | - values: options.values, |
1954 | | - editable: options.editable === true, |
1955 | | - staticFields: options.staticFields === true |
1956 | | - } ); |
1957 | | - |
| 2015 | + var section = new SectionField( description, options ); |
1958 | 2016 | section.getElement().appendTo( $form ); |
1959 | 2017 | |
| 2018 | + //Initialize validator |
1960 | 2019 | var validator = $form.validate( section.getValidationSettings() ); |
1961 | 2020 | |
1962 | 2021 | $form.data( 'formBuilder', { |
Index: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.css |
— | — | @@ -0,0 +1,17 @@ |
| 2 | +/* |
| 3 | + * Styles for ext.gadgets.preferences module |
| 4 | + */ |
| 5 | + |
| 6 | +#mw-gadgets-prefsDialog-message { |
| 7 | + float: right; |
| 8 | + padding: 4px; |
| 9 | + margin: 4px 25px 4px 4px; |
| 10 | + -o-box-shadow: 2px 2px 4px 3px #aaaaaa; |
| 11 | + -webkit-box-shadow: 2px 2px 3px 3px #aaaaaa; |
| 12 | + -moz-box-shadow: 2px 2px 3px 3px #aaaaaa; |
| 13 | + box-shadow: 2px 2px 3px 3px #aaaaaa; |
| 14 | + -o-border-radius: 5px; |
| 15 | + -webkit-border-radius: 5px; |
| 16 | + -moz-border-radius: 5px; |
| 17 | + border-radius: 5px; |
| 18 | +} |
Property changes on: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.css |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 19 | + native |
Index: branches/salvatoreingala/Gadgets/ui/resources/ext.gadgets.preferences.js |
— | — | @@ -2,11 +2,82 @@ |
3 | 3 | * JavaScript tweaks for Special:Preferences |
4 | 4 | */ |
5 | 5 | ( function( $, mw ) { |
| 6 | + |
| 7 | + //Deep comparison of two objects. It assumes that the two objects |
| 8 | + //have the same keys (recursively). |
| 9 | + function deepEquals( a, b ) { |
| 10 | + if ( a === b ) { |
| 11 | + return true; |
| 12 | + } |
| 13 | + |
| 14 | + if ( $.isArray( a ) ) { |
| 15 | + if ( !$.isArray( b ) || a.length != b.length ) { |
| 16 | + return false; |
| 17 | + } |
| 18 | + |
| 19 | + for ( var i = 0; i < a.length; i++ ) { |
| 20 | + if ( !deepEquals( a[i], b[i] ) ) { |
| 21 | + return false; |
| 22 | + } |
| 23 | + } |
| 24 | + } else { |
| 25 | + if ( typeof a != 'object' || typeof b != 'object' ) { |
| 26 | + return false; |
| 27 | + } |
| 28 | + |
| 29 | + for ( var key in a ) { |
| 30 | + if ( a.hasOwnProperty( key ) ) { |
| 31 | + if ( !deepEquals( a[key], b[key] ) ) { |
| 32 | + return false; |
| 33 | + } |
| 34 | + } |
| 35 | + } |
| 36 | + } |
| 37 | + return true; |
| 38 | + } |
6 | 39 | |
| 40 | + //Deletes a stylesheet object |
| 41 | + function removeStylesheet( styleSheet ) { |
| 42 | + var owner = |
| 43 | + styleSheet.ownerNode ? |
| 44 | + styleSheet.ownerNode : //not-IE or IE >= 9 |
| 45 | + styleSheet.owningElement //IE < 9 |
| 46 | + owner.parentNode.removeChild( owner ); |
| 47 | + } |
| 48 | + |
| 49 | + //Shows a message in the bottom of the dialog, with fading |
| 50 | + function showMsg( msg ) { |
| 51 | + if ( msg === null ) { |
| 52 | + $( '#mw-gadgets-prefsDialog-message' ).fadeTo( 200, 0 ); |
| 53 | + } else { |
| 54 | + $( '#mw-gadgets-prefsDialog-message' ) |
| 55 | + .text( msg ) |
| 56 | + .fadeTo( 200, 1 ); |
| 57 | + } |
| 58 | + } |
| 59 | + |
7 | 60 | //"Save" button click handler |
8 | 61 | function saveConfig( $dialog, gadget, config ) { |
9 | 62 | var prefsJson = $.toJSON( config ); |
10 | 63 | |
| 64 | + //disable all dialog buttons |
| 65 | + $( '#mw-gadgets-prefsDialog-close, #mw-gadgets-prefsDialog-save' ).button( 'disable' ); |
| 66 | + |
| 67 | + //Set cursor to "wait" for all elements; save the stylesheet so it can be removed later |
| 68 | + var waitCSS = mw.util.addCSS( '* { cursor: wait !important; }' ); |
| 69 | + |
| 70 | + //just to avoid code duplication |
| 71 | + function error() { |
| 72 | + //Remove "wait" cursor |
| 73 | + removeStylesheet( waitCSS ) |
| 74 | + |
| 75 | + //Warn the user |
| 76 | + showMsg( mw.msg( 'gadgets-save-failed' ) ); |
| 77 | + |
| 78 | + //Enable all dialog buttons |
| 79 | + $( '#mw-gadgets-prefsDialog-close, #mw-gadgets-prefsDialog-save' ).button( 'enable' ); |
| 80 | + } |
| 81 | + |
11 | 82 | $.ajax( { |
12 | 83 | url: mw.config.get( 'wgScriptPath' ) + '/api.php', |
13 | 84 | type: "POST", |
— | — | @@ -20,15 +91,21 @@ |
21 | 92 | dataType: "json", |
22 | 93 | success: function( response ) { |
23 | 94 | if ( typeof response.error == 'undefined' ) { |
24 | | - alert( mw.msg( 'gadgets-save-success' ) ); |
25 | | - $dialog.dialog( 'close' ); |
| 95 | + //Remove "wait" cursor |
| 96 | + removeStylesheet( waitCSS ) |
| 97 | + |
| 98 | + //Notify success to user |
| 99 | + showMsg( mw.msg( 'gadgets-save-success' ) ); |
| 100 | + |
| 101 | + //enable cancel button (leaving 'save' disabled) |
| 102 | + $( '#mw-gadgets-prefsDialog-close' ).button( 'enable' ); |
| 103 | + //update 'savedConfig' |
| 104 | + $dialog.data( 'savedValues', config ); |
26 | 105 | } else { |
27 | | - alert( mw.msg( 'gadgets-save-failed' ) ); |
| 106 | + error() |
28 | 107 | } |
29 | 108 | }, |
30 | | - error: function( response ) { |
31 | | - alert( mw.msg( 'gadgets-save-failed' ) ); |
32 | | - } |
| 109 | + error: error |
33 | 110 | } ); |
34 | 111 | } |
35 | 112 | |
— | — | @@ -66,22 +143,45 @@ |
67 | 144 | |
68 | 145 | //Create and show dialog |
69 | 146 | |
70 | | - var prefsDescription = response.description; |
71 | | - var values = response.values; |
| 147 | + var prefsDescription = response.description, |
| 148 | + values = response.values, |
| 149 | + $dialogBody; |
72 | 150 | |
73 | | - var dialogBody = $( prefsDescription ).formBuilder( { |
| 151 | + var $form = $( prefsDescription ).formBuilder( { |
74 | 152 | msgPrefix: "Gadget-" + gadget + "-", |
75 | 153 | idPrefix: "gadget-" + gadget + "-preferences-", |
76 | | - values: values |
| 154 | + values: values, |
| 155 | + change: function() { |
| 156 | + //Hide current message, if any |
| 157 | + showMsg( null ); |
| 158 | + |
| 159 | + var savedValues = $dialogBody.data( 'savedValues' ), |
| 160 | + currentValues = $form.formBuilder( 'getValues' ); |
| 161 | + //TODO: use a better way of comparing values... |
| 162 | + if ( !deepEquals( currentValues, savedValues ) ) { |
| 163 | + $( '#mw-gadgets-prefsDialog-save' ).button( 'enable' ); |
| 164 | + } else { |
| 165 | + $( '#mw-gadgets-prefsDialog-save' ).button( 'disable' ); |
| 166 | + } |
| 167 | + } |
77 | 168 | } ); |
78 | 169 | |
79 | | - $( dialogBody ).submit( function() { |
| 170 | + $form.submit( function() { |
80 | 171 | return false; //prevent form submission |
81 | 172 | } ); |
82 | 173 | |
83 | | - $( dialogBody ).attr( 'id', 'mw-gadgets-prefsDialog' ); |
| 174 | + $dialogBody = $( '<div/>' ) |
| 175 | + .attr( 'id', 'mw-gadgets-prefsDialog' ) |
| 176 | + .append( $form ) |
| 177 | + .data( 'savedValues', values ); |
84 | 178 | |
85 | | - $( dialogBody ).dialog( { |
| 179 | + //Add a div to show messages |
| 180 | + $( '<div> </div>' ) |
| 181 | + .attr( 'id', 'mw-gadgets-prefsDialog-message' ) |
| 182 | + .css( 'opacity', 0 ) //starts invisible |
| 183 | + .appendTo( $dialogBody ); |
| 184 | + |
| 185 | + $dialogBody.dialog( { |
86 | 186 | modal: true, |
87 | 187 | width: 550, |
88 | 188 | resizable: false, |
— | — | @@ -92,18 +192,22 @@ |
93 | 193 | buttons: [ |
94 | 194 | //TODO: add a "Restore defaults" button |
95 | 195 | { |
| 196 | + id: 'mw-gadgets-prefsDialog-save', |
96 | 197 | text: mw.msg( 'gadgets-prefs-save' ), |
| 198 | + disabled: true, |
97 | 199 | click: function() { |
98 | | - var isValid = $( dialogBody ).formBuilder( 'validate' ); |
99 | | - |
| 200 | + var isValid = $form.formBuilder( 'validate' ); |
100 | 201 | if ( isValid ) { |
101 | | - var values = $( dialogBody ).formBuilder( 'getValues' ); |
102 | | - saveConfig( $( this ), gadget, values ); |
| 202 | + var currentValues = $form.formBuilder( 'getValues' ); |
| 203 | + saveConfig( $( this ), gadget, currentValues ); |
| 204 | + } else { |
| 205 | + showMsg( mw.msg( 'gadgets-save-invalid' ) ); |
103 | 206 | } |
104 | 207 | } |
105 | 208 | }, |
106 | 209 | { |
107 | | - text: mw.msg( 'gadgets-prefs-cancel' ), |
| 210 | + id: 'mw-gadgets-prefsDialog-close', |
| 211 | + text: mw.msg( 'gadgets-prefs-close' ), |
108 | 212 | click: function() { |
109 | 213 | $( this ).dialog( "close" ); |
110 | 214 | } |